diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d081ba1..f92998c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -26,13 +26,22 @@ TODO: in `main.cpp` check the returned policy of pybind11 and also the `py::call TODO: a cpp class that is able to compute (DC powerflow) ContingencyAnalysis and TimeSeries using PTDF and LODF TODO: integration test with pandapower (see `pandapower/contingency/contingency.py` and import `lightsim2grid_installed` and check it's True) -[0.10.0] 2024-12-16 +[0.10.0] 2024-12-17 ------------------- -- [BREAKING] disconnected storage now raises errors if some power is produced / absorbed +- [BREAKING] disconnected storage now raises errors if some power is produced / absorbed, when using legacy grid2op version, + you can retrieve the previous behaviour by initializing the `LightSimBackend` with + `backend = LightSimBackend(..., stop_if_storage_disco=False)` +- [BREAKING] with the new `detachment_is_allowed` feature in grid2op, the kwargs `stop_if_load_disco`, + `stop_if_gen_disco` (and `stop_if_storage_disco`) are now optional. They are set up from the + call to `grid2op.make(...)` and are erased by the `allow_detachment` kwargs. In other words, + you don't need to set `stop_if_load_disco`, `stop_if_gen_disco` or `stop_if_storage_disco`. It is + automatically set by `grid2op.make(..., allow_detachment=XXX)` to have the correct bahaviour. - [FIXED] an issue with the storage units (when asking it to produce / consume but deactivating them with the same action the grid did not diverge) - [IMPROVED] add the grid2op "detachement" support (loads and generators are allowed to be disconnected from the grid) +- [ADDED] a kwargs `stop_if_storage_disco` to control (in legacy grid2op version) the behaviour + of the backend when a storage unit is disconnected. [0.9.2.post2] 2024-11-29 -------------------------- diff --git a/lightsim2grid/lightSimBackend.py b/lightsim2grid/lightSimBackend.py index 3e3c299..d64d687 100644 --- a/lightsim2grid/lightSimBackend.py +++ b/lightsim2grid/lightSimBackend.py @@ -40,7 +40,7 @@ try: from grid2op.Space import DEFAULT_ALLOW_DETACHMENT except ImportError: - # for backward compatibility with grid2op <= 1.11.0 + # for backward compatibility with grid2op < 1.11.0 DEFAULT_ALLOW_DETACHMENT = False try: @@ -84,6 +84,7 @@ def __init__(self, loader_kwargs : Optional[dict] = None, stop_if_load_disco : Optional[bool] = None, stop_if_gen_disco : Optional[bool] = None, + stop_if_storage_disco : Optional[bool] = None, ): #: ``int`` maximum number of iteration allowed for the solver #: if the solver has not converge after this, it will @@ -147,6 +148,13 @@ def __init__(self, #: if set to ``True`` (default) then the backend will raise a #: BackendError in case of disconnected generator self._stop_if_gen_disco = stop_if_gen_disco + + #: .. versionadded:: 0.10.0 + #: + #: if set to ``True`` (default) then the backend will raise a + #: BackendError in case of disconnected storage that are + #: asked to produce / absorb something + self._stop_if_storage_disco = stop_if_storage_disco self._aux_init_super(detailed_infos_for_cascading_failures, can_be_copied, @@ -159,7 +167,8 @@ def __init__(self, loader_method, loader_kwargs, stop_if_load_disco, - stop_if_gen_disco) + stop_if_gen_disco, + stop_if_storage_disco) # backward compat: need to define it if not done by grid2op if not hasattr(self, "_can_be_copied"): @@ -339,7 +348,8 @@ def _aux_init_super(self, loader_method, loader_kwargs, stop_if_load_disco, - stop_if_gen_disco): + stop_if_gen_disco, + stop_if_storage_disco): try: # for grid2Op >= 1.7.1 Backend.__init__(self, @@ -355,6 +365,7 @@ def _aux_init_super(self, loader_kwargs=loader_kwargs, stop_if_load_disco=stop_if_load_disco, stop_if_gen_disco=stop_if_gen_disco, + stop_if_storage_disco=stop_if_storage_disco ) except TypeError as exc_: warnings.warn("Please use grid2op >= 1.7.1: with older grid2op versions, " @@ -578,6 +589,17 @@ def _aux_set_correct_detach_flags(self): warnings.warn("Call to `grid2op.make(..., allow_detachement=True)` will erase the lightsim2grid kwargs `stop_if_load_disco=True`") self._stop_if_load_disco = False + if self._stop_if_storage_disco is None: + # user did not specify anything + self._stop_if_storage_disco = False + elif not self._stop_if_storage_disco: + # force conversion to the proper type + self._stop_if_storage_disco = False + elif self._stop_if_storage_disco: + # erase default values and continue like the grid2op call specifies + warnings.warn("Call to `grid2op.make(..., allow_detachement=True)` will erase the lightsim2grid kwargs `stop_if_storage_disco=True`") + self._stop_if_storage_disco = False + else: # user did not allow detachment (or it's a legacy grid2op version), I check the correct flags if self._stop_if_gen_disco is None: @@ -601,6 +623,17 @@ def _aux_set_correct_detach_flags(self): # erase default values and continue like the grid2op call specifies warnings.warn("Call to `grid2op.make(..., allow_detachement=False)` will erase the lightsim2grid kwargs `stop_if_load_disco=False`") self._stop_if_load_disco = True + + if self._stop_if_storage_disco is None: + # user did not specify anything + self._stop_if_storage_disco = True + elif self._stop_if_storage_disco: + # force conversion to proper type + self._stop_if_storage_disco = True + elif not self._stop_if_storage_disco: + # erase default values and continue like the grid2op call specifies + warnings.warn("Call to `grid2op.make(..., allow_detachement=False)` will erase the lightsim2grid kwargs `stop_if_storage_disco=False`") + self._stop_if_storage_disco = True def load_grid(self, path : Union[os.PathLike, str], @@ -1426,6 +1459,17 @@ def runpf(self, is_dc : bool=False) -> Tuple[bool, Union[Exception, None]]: gen_disco = np.where(disco)[0] self._timer_postproc += time.perf_counter() - beg_postroc raise BackendError(f"At least one generator is disconnected (check gen {gen_disco})") + + if self.__has_storage: + sto_active = (self.storage_p != 0.) + sto_act_disco = (((~np.isfinite(self.storage_v)) & sto_active).any() or + ((self.storage_v <= 0.) & sto_active).any() + ) + if self._stop_if_storage_disco and sto_act_disco: + disco = ((~np.isfinite(self.storage_v)) | (self.storage_v <= 0.)) & sto_active + sto_disco = np.where(disco)[0] + self._timer_postproc += time.perf_counter() - beg_postroc + raise BackendError(f"At least one storage unit is disconnected (check gen {sto_disco})") # TODO storage case of divergence ! if type(self).shunts_data_available: @@ -1537,7 +1581,8 @@ def copy(self) -> Self: self._loader_method, self._loader_kwargs, self._stop_if_load_disco, - self._stop_if_gen_disco) + self._stop_if_gen_disco, + self._stop_if_storage_disco) # for backward compat (attribute was not necessarily present in early grid2op) if not hasattr(res, "_can_be_copied"):