From 3e89dd00b5f563794496cd2c0e91145946902fea Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 29 Aug 2025 08:40:40 +0200 Subject: [PATCH 01/38] increase version number: 1.12.2.dev0 Signed-off-by: DONNOT Benjamin --- docs/conf.py | 2 +- grid2op/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index cf0de3e8..36045487 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ author = 'Benjamin Donnot' # The full version, including alpha/beta/rc tags -release = '1.12.1' +release = '1.12.2.dev0' version = '1.12' diff --git a/grid2op/__init__.py b/grid2op/__init__.py index d6df426e..ef2e8325 100644 --- a/grid2op/__init__.py +++ b/grid2op/__init__.py @@ -11,7 +11,7 @@ Grid2Op a testbed platform to model sequential decision making in power systems. """ -__version__ = '1.12.1' +__version__ = '1.12.2.dev0' __all__ = [ "Action", From 4ea31637fe23f32e39156b5e01760037608087c8 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Fri, 29 Aug 2025 10:46:01 +0200 Subject: [PATCH 02/38] reduce tar.gz size + include specific test in CI for it Signed-off-by: DONNOT Benjamin --- .github/workflows/main.yml | 59 ++++++++++++++++++++++++++++---- MANIFEST.in | 26 ++++++++++++++ grid2op/Action/_backendAction.py | 4 +-- pyproject.toml | 5 ++- 4 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 MANIFEST.in diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c513126a..ed5ffb21 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -193,23 +193,68 @@ jobs: python -c "from grid2op import *" python -c "from grid2op.Action._backendAction import _BackendAction" - - name: Build source archive - if: matrix.config.name == 'darwin' && matrix.python.name == 'cp310' - run: python -m build --sdist . - - name: Upload wheel uses: actions/upload-artifact@v4 with: name: grid2op-wheel-${{ matrix.config.name }}-${{ matrix.python.name }} path: dist/*.whl + source_build: + name: Build tar.gz grid2op file on ${{ matrix.config.name}} + runs-on: ${{ matrix.config.os }} + strategy: + matrix: + config: + - { + name: darwin, + os: macos-latest, + } + - { + name: windows, + os: windows-2022, + } + - { + name: linux, + os: ubuntu-latest, + } + + steps: + - name: Checkout sources + uses: actions/checkout@v4 + with: + submodules: true + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.12 + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip wheel setuptools build + + - name: Build tar gz + run: python -m build --sdist . + + - name: Install tar gz + shell: bash + run: | + python -m pip install dist/*.tar.gz --user + pip freeze + + - name: Check package can be imported + run: | + python -c "import grid2op" + python -c "from grid2op import *" + python -c "from grid2op.Action._backendAction import _BackendAction" + - name: Upload source archive + if: (matrix.config.name == 'darwin') uses: actions/upload-artifact@v4 - if: matrix.config.name == 'darwin' && matrix.python.name == 'cp310' with: name: grid2op-sources - path: dist/*.tar.gz - + path: dist/*.tar.gz + auto_class_in_file: name: Test ${{ matrix.config.name }} OS can handle automatic class generation for python ${{matrix.python.version}} runs-on: ${{ matrix.config.os }} diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..7a7bdce8 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,26 @@ +global-exclude */__pycache__/* +global-exclude *.pyc +recursive-exclude grid2op/data_test * +recursive-exclude binder * +recursive-exclude build * +recursive-exclude dist * +recursive-exclude docs * +recursive-exclude documentation * +recursive-exclude examples * +recursive-exclude getting_started * +recursive-exclude logo * +recursive-exclude utils * +recursive-exclude _profiling * +recursive-exclude .circleci * +recursive-exclude .git * +recursive-exclude .github * +recursive-exclude .idea * +recursive-exclude .ipynb_checkpoints * +recursive-exclude .pytest_cache * +recursive-exclude .vscode * +recursive-exclude . test_* .readthedocs.yml MakeFile Dockerfile +recursive-exclude grid2op/data/l2rpn_idf_2023/chronics keep_only_beginning.py +prune **venv* +prune **__pycache__* +prune **_grid2op_classes* +prune **.DS_Store* \ No newline at end of file diff --git a/grid2op/Action/_backendAction.py b/grid2op/Action/_backendAction.py index 2ea206de..1aafe621 100644 --- a/grid2op/Action/_backendAction.py +++ b/grid2op/Action/_backendAction.py @@ -6,12 +6,10 @@ # SPDX-License-Identifier: MPL-2.0 # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. -from collections.abc import Callable import copy import numpy as np -from typing import List, Tuple, Union +from typing import Tuple, Union -from grid2op.typing_variables import BACKEND_TYPE try: from typing import Self except ImportError: diff --git a/pyproject.toml b/pyproject.toml index 6512383b..09e2d44b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ version = { attr = "grid2op.__version__" } # All the following settings are optional: where = ["."] # by default include = ["grid2op*"] # ["*"] by default -exclude = ["grid2op/tests*"] # empty by default +exclude = ["binder*", "documentation*", "utils*"] # empty by default namespaces = false [project.urls] @@ -139,4 +139,7 @@ grid2op = [ "*/__pycache__/*", "**/*.pyc", "*_grid2op_classes*", + "binder*", + "documentation*", + "utils*" ] \ No newline at end of file From a15b74e39381e47685963e746ce31b02efa8bcd5 Mon Sep 17 00:00:00 2001 From: DEUCE1957 <2246306W@student.gla.ac.uk> Date: Mon, 15 Sep 2025 22:29:32 +0200 Subject: [PATCH 03/38] Add: Changes to BaseAction, SerializableActionSpace and GridObjects Signed-off-by: DEUCE1957 <2246306W@student.gla.ac.uk> --- docs/grid2op_dev/action.rst | 33 +- grid2op/Action/__init__.py | 2 + grid2op/Action/baseAction.py | 303 +++++++++++++++++- grid2op/Action/flexibilityAction.py | 26 ++ grid2op/Action/serializableActionSpace.py | 171 +++++++++- grid2op/Space/GridObjects.py | 1 + .../l2rpn_neurips_2020_track2/__init__.py | 1 + 7 files changed, 517 insertions(+), 20 deletions(-) create mode 100644 grid2op/Action/flexibilityAction.py create mode 100644 grid2op/data/l2rpn_neurips_2020_track2/__init__.py diff --git a/docs/grid2op_dev/action.rst b/docs/grid2op_dev/action.rst index 3cf66962..73a6d170 100644 --- a/docs/grid2op_dev/action.rst +++ b/docs/grid2op_dev/action.rst @@ -97,11 +97,42 @@ the `cls.authorized_keys` and function `def supports_type` of SerializableActionSpace and `cls.mapping_vect_auth_keys` (BaseAction) +Checklist for SerializableActionSpace: + Add a new ID number for your action to `SerializeableActionSpace`'s class attributes. Add this to the sample() + and "_get_possible_action_types()" methods. + Add a _sample_YOURACTION() method to `SerializableActionSpace` + Add a _aux_get_back_to_ref_state_YOURACTION() method. + Add your action to get_back_to_ref_state() method. + Optional: If your new action is continuous, add a get_all_unitary_YOURACTION() method to + `SerializableActionSpace` to help users discretize it. + +Create a new YourAction.py file in `Grid2op/Action/*` and add this to the `Grid2Op/Action/__init__.py`. + Add the vectorization *********************** -TODO add the `cls.attr_list_vect` +TODO add the `cls.attr_list_vect` to baseAction: +* Add to baseAction.authorized_keys +* Add to baseAction.attr_list_vect +* Add to baseAction.mapping_vect_auth_keys +* Make sure your `self._private_YOURACTION = None` attribute is in baseAction's `__init__` +* Add a self._modif_YOURACTION to baseAction's `__init__` +* Double check you added a @property for your action in BaseAction +* Update the BaseAction._aux_copy() method to include your action (ideally in an if-statement) +* Modify the end of `process_grid2op_compat()` +* Add your _modif_YOURACTION to `reset_modified_flags()` and `can_affect_something()` +* Add your action to `_post_process_from_vect()` as appropriate +* Add a new method called `_aux_iadd_YOURACTION()` and reference this in `__iadd__()` +* Add your action to `__call__(self)` by referencing self._private_YOURACTION +* Add a `_digest_YOURACTION()` method +* Add an example to the docstring of `update(self)` +* Add modification check to `_check_for_corret_modif_flags()` +* Add ambiguity checks for your action in `_check_for_ambiguity()`, for instance by adding and calling a new `_is_YOURACTION_ambigious()` method +* Add a string formatting for your action to `__str__()` +* Add a check to `impact_on_objects()` for your action +* Add a boolean for your action in `get_types()` +* Modify `_aux_effect_on_XXX()` as appropriate Worry about the backward compatibility **************************************** diff --git a/grid2op/Action/__init__.py b/grid2op/Action/__init__.py index 03b116be..91436258 100644 --- a/grid2op/Action/__init__.py +++ b/grid2op/Action/__init__.py @@ -20,6 +20,7 @@ "TopologyChangeAction", "TopologyChangeAndDispatchAction", "DispatchAction", + "FlexibilityAction", "_BackendAction" ] @@ -50,4 +51,5 @@ TopologyChangeAndDispatchAction, ) from grid2op.Action.dispatchAction import DispatchAction +from grid2op.Action.flexibilityAction import FlexibilityAction import grid2op.Action._backendAction as _BackendAction diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 2de4d095..2eaf6721 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -239,6 +239,14 @@ class BaseAction(GridObjects): on a generator, and on another you ask +10 MW then the total setpoint for this generator that the environment will try to implement is +20MW. + _flexibility: :class:`numpy.ndarray`, dtype:float + Amount of demand response that this action will perform. Flexiblity will increase the load's active + consumption value. This will be added to the value of the loads. The Environment will make sure that every + physical constraint is met. This means that the agent provides a change in flexiblity, but there is no guarantee + that this value will be achievable. Flexibility actions are cumulative, this means that if at a given timestep + you ask +5 MW on a load, and on another you ask +2 MW then the total flexibility adjustment for this load that + the environment will try to implement is +7MW. + _private_storage_power: :class:`numpy.ndarray`, dtype:float Amount of power you want each storage units to produce / absorbs. Storage units are in "loads" convention. This means that if you ask for a positive number, the storage unit will absorb @@ -454,6 +462,7 @@ def change_whatever(grid): "set_bus", "change_bus", "redispatch", + "flexibility", # new in 1.12.x "set_storage", "curtail", "raise_alarm", @@ -469,6 +478,7 @@ def change_whatever(grid): "load_p", "load_q", "_redispatch", + "_flexibility", # new in 1.12.x "_set_line_status", "_switch_line_status", "_set_topo_vect", @@ -491,6 +501,7 @@ def change_whatever(grid): "load_p": "injection", "load_q": "injection", "_redispatch": "redispatch", + "_flexibility": "flexibility", # new in 1.12.x "_set_line_status": "set_line_status", "_switch_line_status": "change_line_status", "_set_topo_vect": "set_bus", @@ -582,6 +593,9 @@ def __init__(self, _names_chronics_to_backend: Optional[Dict[Literal["loads", "p # redispatching vector self._private_redispatch : Optional[np.ndarray] = None # np.full(shape=cls.n_gen, fill_value=0.0, dtype=dt_float) + + # demand response / flexibility vector + self._private_flexibility : Optional[np.ndarray] = None # storage unit vector self._private_storage_power : Optional[np.ndarray] = None # np.full( @@ -643,6 +657,7 @@ def __init__(self, _names_chronics_to_backend: Optional[Dict[Literal["loads", "p self._modif_set_status = False self._modif_change_status = False self._modif_redispatch = False + self._modif_flexibility = False self._modif_storage = False self._modif_curtailment = False self._modif_alarm = False @@ -664,6 +679,13 @@ def _redispatch(self) -> np.ndarray: self._private_redispatch = cls._build_attr("_redispatch") return self._private_redispatch + @property + def _flexibility(self) -> np.ndarray: + if self._private_flexibility is None: + cls = type(self) + self._private_flexibility = cls._build_attr("_flexibility") + return self._private_flexibility + @property def _set_line_status(self) -> np.ndarray: if self._private_set_line_status is None: @@ -834,6 +856,10 @@ def _build_dict_attr_if_needed(cls): cls.DICT_ATTR_["_detach_gen"] = np.full(cls.n_gen, dtype=dt_bool, fill_value=False) cls.DICT_ATTR_["_detach_storage"] = np.full(cls.n_storage, dtype=dt_bool, fill_value=False) + # new in 1.12.x + if cls.flexibility_is_available: + cls.DICT_ATTR_["_flexibility"] = np.full(shape=cls.n_load, fill_value=0.0, dtype=dt_float), + @classmethod def _build_attr(cls, attr_nm: str): # False(line is disconnected) / True(line is connected) @@ -902,6 +928,7 @@ def _aux_copy(self, other: Self) -> None: "_modif_set_status", "_modif_change_status", "_modif_redispatch", + "_modif_flexibility", # new in 1.12.x "_modif_storage", "_modif_curtailment", "_modif_alarm", @@ -933,7 +960,10 @@ def _aux_copy(self, other: Self) -> None: if cls.detachment_is_allowed: attr_vect += ["_private_detach_load", "_private_detach_gen", "_private_detach_storage"] - + + if cls.flexibility_is_available: + attr_vect += ["_private_flexibility"] + for attr_nm in attr_simple: setattr(other, attr_nm, getattr(self, attr_nm)) @@ -1109,6 +1139,16 @@ def as_serializable_dict(self) -> dict: ] if not res["curtail"]: del res["curtail"] + + if self.flexibility_is_available: # new in 1.12.x + if self._modif_flexibility: + res["flexibility"] = [ + (str(cls.name_Load[id_]), float(val)) + for id_, val in enumerate(self._private_flexibility) + if np.abs(val) >= 1e-7 + ] + if not res["flexibility"]: + del res["flexibility"] # more advanced options if self._modif_inj: @@ -1281,6 +1321,17 @@ def process_grid2op_compat(cls): if attr_vect in cls.attr_list_vect: cls.attr_list_vect.remove(attr_vect) + if glop_ver < cls.MIN_VERSION_FLEX: + # this feature did not exist before. + cls.authorized_keys = copy.deepcopy(cls.authorized_keys) + cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) + attr_key = "flexibility" + if attr_key in cls.authorized_keys: + cls.authorized_keys.remove(attr_key) + attr_vect = "_private_flexibility" + if attr_vect in cls.attr_list_vect: + cls.attr_list_vect.remove(attr_vect) + if (cls.n_busbar_per_sub >= 3) or (cls.n_busbar_per_sub == 1): # only relevant for grid2op >= 1.10.0 # remove "change_bus" if it's there more than 3 buses (no sense: where to change it ???) @@ -1308,6 +1359,8 @@ def _reset_modified_flags(self): self._modif_detach_load = False self._modif_detach_gen = False self._modif_detach_storage = False + # flexibility + self._modif_flexibility = False def can_affect_something(self) -> bool: """ @@ -1325,6 +1378,7 @@ def can_affect_something(self) -> bool: or self._modif_set_status or self._modif_change_status or self._modif_redispatch + or self._modif_flexibility or self._modif_storage or self._modif_curtailment or self._modif_alarm @@ -1405,6 +1459,7 @@ def _post_process_from_vect_continuous_flags(self): self._modif_redispatch = self._private_redispatch is not None and ( np.isfinite(self._private_redispatch) & (np.abs(self._private_redispatch) >= 1e-7) ).any() + self._modif_storage = self._private_storage_power is not None and (np.abs(self._private_storage_power) >= 1e-7).any() self._modif_curtailment = self._private_curtail is not None and (np.abs(self._private_curtail + 1.0) >= 1e-7).any() self._modif_alarm = self._private_raise_alarm is not None and self._private_raise_alarm.any() @@ -1430,7 +1485,12 @@ def _post_process_from_vect(self): self._modif_detach_load = self._private_detach_load is not None and (self._private_detach_load).any() self._modif_detach_gen = self._private_detach_gen is not None and (self._private_detach_gen).any() self._modif_detach_storage = self._private_detach_storage is not None and (self._private_detach_storage).any() - + + if cls.flexibility_is_available: + self._modif_flexibility = self._modif_flexibility is not None and ( + np.isfinite(self._private_flexibility) & (np.abs(self._private_flexibility) >= 1e-7) + ).any() + if self.can_affect_something(): # reset the ambiguous cache self._cached_is_not_ambiguous = False @@ -2263,6 +2323,9 @@ def reset(self): # redispatching vector self._private_redispatch[:] = 0.0 + + # flexibility vector + self._private_flexibility[:] = 0.0 # storage self._private_storage_power[:] = 0.0 @@ -2330,6 +2393,19 @@ def _aux_iadd_redisp(self, other: "BaseAction"): else: ok_ind = np.isfinite(redispatching) self._redispatch[ok_ind] += redispatching[ok_ind] + + def _aux_iadd_flexibility(self, other: "BaseAction"): + if other._private_flexibility is None: + return + flexibility = other._flexibility + if (np.abs(flexibility) >= 1e-7).any(): + if "_flexibility" not in self.attr_list_set: + warnings.warn( + type(self).ERR_ACTION_CUT.format("_flexibility") + ) + else: + ok_ind = np.isfinite(flexibility) + self._flexibility[ok_ind] += flexibility[ok_ind] def _aux_iadd_curtail(self, other): curtailment = other._curtail @@ -2529,6 +2605,10 @@ def __iadd__(self, other: Self): # redispatching self._aux_iadd_redisp(other) + + # flexibility, new in 1.12.x + if type(self).flexibility_is_available: + self._aux_iadd_flexibility(other) # storage self._aux_iadd_storage(other) @@ -2620,6 +2700,10 @@ def __call__(self) -> Tuple[dict, np.ndarray, np.ndarray, np.ndarray, np.ndarray This array, that has the same size as the number of generators indicates for each generator the amount of redispatching performed by the action. + flexibility: :class:`numpy.ndarray`, dtype:float + This array, that has the same size as the number of loads indicates for each load the amount of + flexibility performed by the action. + storage_power: :class:`numpy.ndarray`, dtype:float Indicates, for all storage units, what is the production / absorbtion setpoint @@ -2646,6 +2730,7 @@ def __call__(self) -> Tuple[dict, np.ndarray, np.ndarray, np.ndarray, np.ndarray set_topo_vect = self._private_set_topo_vect change_bus_vect = self._private_change_bus_vect redispatch = self._private_redispatch + flexibility = self._private_flexibility # new in 1.12.x storage_power = self._private_storage_power # remark: curtailment is handled by an algorithm in the environment, so don't need to be returned here shunts = {} @@ -2653,6 +2738,7 @@ def __call__(self) -> Tuple[dict, np.ndarray, np.ndarray, np.ndarray, np.ndarray shunts["_private_shunt_p"] = self._private_shunt_p shunts["_private_shunt_q"] = self._private_shunt_q shunts["_private_shunt_bus"] = self._private_shunt_bus + # other remark: alarm and alert are not handled in the backend, this is why it does not appear here ! return ( dict_inj, @@ -2661,6 +2747,7 @@ def __call__(self) -> Tuple[dict, np.ndarray, np.ndarray, np.ndarray, np.ndarray set_topo_vect, change_bus_vect, redispatch, + flexibility, storage_power, shunts, ) @@ -2987,6 +3074,10 @@ def _digest_redispatching(self, dict_): if "redispatch" in dict_: self.redispatch = dict_["redispatch"] + def _digest_flexibility(self, dict_): + if "flexibility" in dict_: + self.flexibility = dict_["flexibility"] + def _digest_storage(self, dict_): if "set_storage" in dict_: try: @@ -3058,6 +3149,12 @@ def _check_keys_exist(action_cls:GridObjects, act_dict): # with shunt in "init_state.json" with a backend that does not # handle shunt continue + if kk == "flexibility" and not action_cls.flexibility_is_available: + # no warnings are raised in this case because if a warning + # were raised it could crash some environment + # with flexibility in "init_state.json" with a backend that does not + # handle flexibility + continue if kk == "set_storage" and action_cls.n_storage == 0: # no warnings are raised in this case because if a warning # were raised it could crash some environment @@ -3143,6 +3240,11 @@ def update(self, tuple, each tuple being 2 elements: first the generator ID, second the amount of redispatching, for example `[(1, -23), (12, +17)]` + - "flexibility": the best use of this is to specify either the numpy array of the flexibility / demand response + vector you want to apply (that should have the size of the number of loads on the grid) or to specify a list + of tuples, each tuple being 2 elements: first the load ID, second the amount of flexibility, + for example `[(0, -12), (3, +7)]` + - "set_storage": the best use of this is to specify either the numpy array of the storage units vector you want to apply (that should have the size of the number of storage units on the grid) or to specify a list of @@ -3271,6 +3373,13 @@ def update(self, storage_act = env.action_space({"curtail": [(renewable_energy_source, 0.8)]}) print(storage_act) + *Example 9*: apply flexibility of +7.21 MW at load with id 2 and -12.3 at load with id 1 + + .. code-block:: python + + flex_act = env.action_space({"flexibility": [(2, +7.21), (1, -12.3)]}) + print(flex_act) + Returns ------- self: :class:`BaseAction` @@ -3286,6 +3395,9 @@ def update(self, if cls.shunts_data_available: # do not digest shunt when backend does not support it self._digest_shunt(dict_) + if cls.flexibility_is_available: + self._digest_flexibility(dict_) + self._digest_injection(dict_) self._digest_redispatching(dict_) if cls.n_storage > 0: @@ -3399,6 +3511,18 @@ def _check_for_correct_modif_flags(self): ) if "redispatch" not in cls.authorized_keys: raise IllegalAction("You illegally act on the redispatching") + + if cls.flexibility_is_available: + if self._private_flexibility is not None and (np.abs(self._private_flexibility) >= 1e-7).any(): + if not self._modif_flexibility: + raise AmbiguousAction( + "A action of type flexibility is performed while the appropriate flag " + "is not " + "set. Please use the official grid2op action API to perform flexibility " + "action." + ) + if "flexibility" not in cls.authorized_keys: + raise IllegalAction("You illegally act on the flexibility") if self._private_storage_power is not None and (np.abs(self._private_storage_power) >= 1e-7).any(): if not self._modif_storage: @@ -3575,6 +3699,13 @@ def _check_for_ambiguity(self): "This action acts on {} generators (redispatching= while " "there are {} in the grid".format(len(self._private_redispatch), cls.n_gen) ) + + if self._private_flexibility is not None and len(self._private_flexibility) != cls.n_load: + raise InvalidNumberOfLoads( + "This action acts on {} loads (flexibility= while " + "there are {} in the grid".format(len(self._private_flexibility), cls.n_load) + ) + # redispatching specific check if self._modif_redispatch: @@ -3794,6 +3925,7 @@ def _check_for_ambiguity(self): ) self._is_detachment_ambiguous() + self._is_flexibility_ambiguous() def _is_detachment_ambiguous(self): """check if any of the detachment action is ambiguous""" @@ -3843,7 +3975,31 @@ def _is_detachment_ambiguous(self): if (issue_xxx).any(): raise AmbiguousAction(f"Trying to both set a {el_nm} of busbar (set_bus) AND detach it from the grid. " f"Check {el_nm}: {name_xxx[issue_xxx]}") - + + def _is_flexibility_ambiguous(self): + "check if flexibility actions are ambiguous" + if self.flexibility_is_available: + if self._modif_flexibility: # new in 1.12.x + if "flexibility" not in type(self).authorized_keys: + raise AmbiguousAction( + 'Action of type "flexibility" are not supported by this action type' + ) + + if (np.abs(self._private_flex[~type(self).load_flexible]) >= 1e-7).any(): + raise InvalidFlexibility( + "Trying to apply a flexibility action on a non redispatchable generator" + ) + + if self._single_act: + if (self._private_flexibility > type(self).load_max_ramp_up).any(): + raise InvalidFlexibility( + "Some flexibility amount are above the maximum ramp up" + ) + if (-self._private_flexibility > type(self).load_max_ramp_down).any(): + raise InvalidFlexibility( + "Some flexibility amount are bellow the maximum ramp down" + ) + def _is_storage_ambiguous(self): """check if storage actions are ambiguous""" cls = type(self) @@ -4021,10 +4177,10 @@ def __str__(self) -> str: res.append( "\t - Modify the generators with redispatching in the following way:" ) - for gen_idx in range(self.n_gen): - if np.abs(self._private_redispatch[gen_idx]) >= 1e-7: - gen_name = self.name_gen[gen_idx] - r_amount = self._private_redispatch[gen_idx] + for load_idx in range(self.n_gen): + if np.abs(self._private_redispatch[load_idx]) >= 1e-7: + gen_name = self.name_gen[load_idx] + r_amount = self._private_redispatch[load_idx] res.append( '\t \t - Redispatch "{}" of {:.2f} MW'.format( gen_name, r_amount @@ -4055,10 +4211,10 @@ def __str__(self) -> str: # curtailment if self._modif_curtailment: res.append("\t - Perform the following curtailment:") - for gen_idx in range(self.n_gen): - amount_ = self._private_curtail[gen_idx] + for load_idx in range(self.n_gen): + amount_ = self._private_curtail[load_idx] if np.isfinite(amount_) and np.abs(amount_ + 1.0) >= 1e-7: - name_ = self.name_gen[gen_idx] + name_ = self.name_gen[load_idx] res.append( '\t \t - Limit unit "{}" to {:.1f}% of its Pmax (setpoint: {:.3f})' "".format(name_, 100.0 * amount_, amount_) @@ -4173,7 +4329,26 @@ def __str__(self) -> str: res.append(f"\t - Detach {el}: {_detach_xxx.nonzero()[0]}") else: res.append(f"\t - Not detach any {el}") - + + + # flexibility, new in 1.12.x + if self.flexibility_is_available: + if self._modif_flexibility: + res.append( + "\t - Modify the loads with flexibility in the following way:" + ) + for load_idx in range(self.n_load): + if np.abs(self._private_flexibility[load_idx]) >= 1e-7: + load_name = self.name_load[load_idx] + f_amount = self._private_flexibility[load_idx] + res.append( + '\t \t - Flexibility "{}" of {:.2f} MW'.format( + load_name, f_amount + ) + ) + else: + res.append("\t - NOT perform any flexibility action") + return "\n".join(res) def impact_on_objects(self) -> dict: @@ -4184,7 +4359,7 @@ def impact_on_objects(self) -> dict: ------- dict: :class:`dict` The dictionary representation of an action impact on objects with keys, "has_impact", "injection", - "force_line", "switch_line", "topology", "redispatch", "storage", "curtailment". + "force_line", "switch_line", "topology", "redispatch", "flexibility", "storage", "curtailment". """ # handles actions on injections @@ -4332,6 +4507,20 @@ def impact_on_objects(self) -> dict: ) curtailment["changed"] = True has_impact = True + + flexibility = {"changed": False, "loads": []} + if self.flexibility_is_available: # new in 1.12.x + # handle flexibility + if self._private_flexibility is not None and (np.abs(self._private_flexibility) >= 1e-7).any(): + for load_idx in range(cls.n_load): + if np.abs(self._private_flexibility[load_idx]) >= 1e-7: + load_name = self.name_load[load_idx] + f_amount = self._private_flexibility[load_idx] + redispatch["loads"].append( + {"load_id": load_idx, "gen_name": load_name, "amount": f_amount} + ) + flexibility["changed"] = True + has_impact = True return { "has_impact": has_impact, @@ -4340,6 +4529,7 @@ def impact_on_objects(self) -> dict: "switch_line": switch_line_status, "topology": topology, "redispatch": redispatch, + "flexibility": flexibility, "storage": storage, "curtailment": curtailment, } @@ -4449,7 +4639,8 @@ def _aux_as_dict_hazards_maintenance(self, res): def as_dict(self) -> Dict[Literal["load_p", "load_q", "prod_p", "prod_v", "change_line_status", "set_line_status", "change_bus_vect", "set_bus_vect", - "redispatch", "storage_power", "curtailment"], + "redispatch", "flexibility", + "storage_power", "curtailment"], Any]: """ Represent an action "as a" dictionary. This dictionary is useful to further inspect on which elements @@ -4503,6 +4694,8 @@ def as_dict(self) -> Dict[Literal["load_p", "load_q", "prod_p", "prod_v", disconnected because of maintenance operations. * `redispatch` the redispatching action (if any). It gives, for each generator (all generator, not just the dispatchable one) the amount of power redispatched in this action. + * `flexibility` the flexibility action (if any). It gives, for each load (all loads, not just the + flexible ones) the amount of demand flexibility in this action. * `storage_power`: the setpoint for production / consumption for all storage units * `curtailment`: the curtailment performed on all generator * `shunt` : @@ -4536,9 +4729,13 @@ def as_dict(self) -> Dict[Literal["load_p", "load_q", "prod_p", "prod_v", if type(self).shunts_data_available: self._aux_as_dict_shunt(res) + if type(self).flexibility_is_available: + if self._private_flexibility is not None and (np.abs(self._private_flexibility) >= 1e-7).any(): + res["flexibility"] = 1.0 * self._private_flexibility + return res - def get_types(self) -> Tuple[bool, bool, bool, bool, bool, bool, bool]: + def get_types(self) -> Tuple[bool, bool, bool, bool, bool, bool, bool, bool]: """ Shorthand to get the type of an action. The type of an action is among: @@ -4547,6 +4744,7 @@ def get_types(self) -> Tuple[bool, bool, bool, bool, bool, bool, bool]: - "topology": does this action modifies the topology of the grid (*ie* set or switch some buses) - "line": does this action modifies the line status - "redispatching" does this action modifies the redispatching + - "flexibility" does this action modify the flexibility - "storage" does this action impact the production / consumption of storage units - "curtailment" does this action impact the non renewable generators through curtailment @@ -4580,7 +4778,8 @@ def get_types(self) -> Tuple[bool, bool, bool, bool, bool, bool, bool]: Does it performs (explicitly) any action on the storage production / consumption curtailment: ``bool`` Does it performs (explicitly) any action on renewable generator - + flexibility: ``bool`` + Does it performs (explicitly) any flexibility / demand response """ injection = "load_p" in self._dict_inj or "prod_p" in self._dict_inj voltage = "prod_v" in self._dict_inj @@ -4595,7 +4794,12 @@ def get_types(self) -> Tuple[bool, bool, bool, bool, bool, bool, bool]: redispatching = self._private_redispatch is not None and (np.abs(self._private_redispatch) >= 1e-7).any() storage = self._modif_storage curtailment = self._modif_curtailment - return injection, voltage, topology, line, redispatching, storage, curtailment + # flexibility, new in 1.12.x + if type(self).flexibility_is_available: + flexibility = self._private_flexibility is not None and (np.abs(self._private_flexibility) >= 1e-7).any() + else: + flexibility = False + return injection, voltage, topology, line, redispatching, storage, curtailment, flexibility def _aux_effect_on_load(self, load_id): if load_id >= self.n_load: @@ -4614,6 +4818,8 @@ def _aux_effect_on_load(self, load_id): my_id = cls.load_pos_topo_vect[load_id] res["change_bus"] = self._private_change_bus_vect[my_id] if self._private_change_bus_vect is not None else False res["set_bus"] = self._private_set_topo_vect[my_id] if self._private_set_topo_vect is not None else 0 + if cls.flexibility_is_available: + res["flexibility"] = self._private_flexibility[load_id] if self._private_flexibility is not None else 0. return res def _aux_effect_on_gen(self, gen_id): @@ -4758,6 +4964,7 @@ def effect_on( - "set_bus" the new bus where the load will be moved (int: id of the bus, 0 no change, -1 disconnected) - "change_bus" whether or not this load will be moved from one bus to another (for example is an action asked it to go from bus 1 to bus 2) + - "flexibility" the amount of demand response activated on this load. - if a generator is inspected, then the keys are: @@ -6951,7 +7158,38 @@ def redispatch(self, values): f"Please consult the documentation. " f'The error was:\n"{exc_}"' ) - + + @property + def flexibility(self) -> np.ndarray: + res = 1.0 * self._flexibility + res.flags.writeable = False + return res + + @flexibility.setter + def flexibility(self, values): + if "flexibility" not in self.authorized_keys: + raise IllegalAction( + "Impossible to perform flexibility with this action type." + ) + orig_ = self.flexibility + try: + self._aux_affect_object_float( + values, + "flexibility", + self.n_load, + self.name_load, + np.arange(self.n_load), + self._flexibility, + _nm_ch_bk_key="loads", + ) + self._modif_flexibility = True + except Exception as exc_: + self._private_flexibility[:] = orig_ + raise AmbiguousAction( + f"Impossible to modify the flexibility with your input. " + f"Please consult the documentation. " + f'The error was:\n"{exc_}"' + ) @property def storage_p(self) -> np.ndarray: """ @@ -7691,6 +7929,24 @@ def _aux_decompose_as_unary_actions_redisp(self, tmp._modif_redispatch = True tmp._redispatch[g_id] = self._private_redispatch[g_id] res["redispatch"].append(tmp) + + def _aux_decompose_as_unary_actions_flexibility(self, + cls: Type[Self], + group_flexibility: bool, + res): + if group_flexibility: + tmp = cls() + tmp._modif_flexibility = True + tmp._flexibility[:] = self._private_flexibility + res["flexibility"] = [tmp] + else: + load_changed = (np.abs(self._private_flexibility) >= 1e-7).nonzero()[0] + res["flexibility"] = [] + for load_id in load_changed: + tmp = cls() + tmp._modif_flexibility = True + tmp._redis_flexibilitypatch[load_id] = self._private_flexibility[load_id] + res["flexibility"].append(tmp) def _aux_decompose_as_unary_actions_storage(self, cls: Type[Self], @@ -7732,12 +7988,14 @@ def decompose_as_unary_actions(self, group_topo=False, group_line_status=False, group_redispatch=True, + group_flexibility=True, group_storage=True, group_curtail=True) -> Dict[Literal["change_bus", "set_bus", "change_line_status", "set_line_status", "redispatch", + "flexibility", "set_storage", "curtail"], List["BaseAction"]]: @@ -7767,6 +8025,9 @@ def decompose_as_unary_actions(self, - "redispatch" if the action affects the grid with `redispatch` In this case the value associated with this key is a list containing only action that performs `redispatch` + - "flexibility" if the action affects the grid with `flexibility` + In this case the value associated with this key is a list containing + only action that performs `flexibility` - "set_storage" if the action affects the grid with `set_storage` In this case the value associated with this key is a list containing only action that performs `set_storage` @@ -7842,6 +8103,11 @@ def decompose_as_unary_actions(self, "redispatching" instead of "powerline" and `set_line_status`, by default True (meaning the value associated with the key `redispatch` will be a list of one element performing a redispatching action on all generators modified by the current action) + group_flexibility : bool, optional + same behaviour as `group_line_status` but for "loads" and + "flexibility" instead of "powerline" and `set_line_status`, by default True (meaning the value associated with + the key `flexibility` will be a list of one element performing + a flexibility action on all loads modified by the current action) group_storage : bool, optional same behaviour as `group_line_status` but for "storage units" and "set setpoint" instead of "powerline" and `set_line_status`, by default True (meaning the value associated with @@ -7870,6 +8136,9 @@ def decompose_as_unary_actions(self, self._aux_decompose_as_unary_actions_set_ls(cls, group_line_status, res) if self._modif_redispatch: self._aux_decompose_as_unary_actions_redisp(cls, group_redispatch, res) + if cls.flexibility_is_available: # new in 1.12.x + if self._modif_flexibility: + self._aux_decompose_as_unary_actions_flexibility(cls, group_flexibility, res) if self._modif_storage: self._aux_decompose_as_unary_actions_storage(cls, group_storage, res) if self._modif_curtailment: diff --git a/grid2op/Action/flexibilityAction.py b/grid2op/Action/flexibilityAction.py new file mode 100644 index 00000000..c321ccdc --- /dev/null +++ b/grid2op/Action/flexibilityAction.py @@ -0,0 +1,26 @@ +# Copyright (c) 2019-2020, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + +from typing import Optional, Dict, Literal +from grid2op.Action.playableAction import PlayableAction + +class FlexibilityAction(PlayableAction): + """ + This type of :class:`PlayableAction` only implements the modifications of the grid through "flexibility" keyword. + + Nothing else is supported and any attempt to use something else will have not impact. + + """ + + authorized_keys = {"flexibility"} + + attr_list_vect = ["_flexibility"] + attr_list_set = set(attr_list_vect) + + def __init__(self, _names_chronics_to_backend: Optional[Dict[Literal["loads", "prods", "lines"], Dict[str, str]]]=None): + super().__init__(_names_chronics_to_backend) diff --git a/grid2op/Action/serializableActionSpace.py b/grid2op/Action/serializableActionSpace.py index 5fb02da9..522c7e23 100644 --- a/grid2op/Action/serializableActionSpace.py +++ b/grid2op/Action/serializableActionSpace.py @@ -49,6 +49,7 @@ class SerializableActionSpace(SerializableSpace): STORAGE_POWER_ID = 5 RAISE_ALARM_ID = 6 RAISE_ALERT_ID = 7 + FLEXIBILITY_ID = 8 ERR_MSG_WRONG_TYPE = ('The action to update using `ActionSpace` is of type "{}" ' '"which is not the type of action handled by this action space "' @@ -126,6 +127,8 @@ def _get_possible_action_types(self): rnd_types.append(cls.RAISE_ALARM_ID) if cls.dim_alerts > 0 and "raise_alert" in self.actionClass.authorized_keys: rnd_types.append(cls.RAISE_ALERT_ID) + if "flexibility" in self.actionClass.authorized_keys: + rnd_types.append(cls.FLEXIBILITY_ID) return rnd_types def supports_type(self, @@ -134,6 +137,7 @@ def supports_type(self, "set_bus", "change_bus", "redispatch", + "flexibility", "storage_power", "set_storage", "curtail", @@ -148,7 +152,8 @@ def supports_type(self, ---------- action_type: ``str`` One of "set_line_status", "change_line_status", "set_bus", "change_bus", "redispatch", - "storage_power", "set_storage", "curtail", "curtail_mw", "raise_alarm" or "raise_alert" + "flexibility", "storage_power", "set_storage", "curtail", "curtail_mw", "raise_alarm" + or "raise_alert". A string representing the action types you want to inspect. Returns @@ -179,6 +184,7 @@ def supports_type(self, "set_bus", "change_bus", "redispatch", + "flexibility", # new in 1.12.x "storage_power", "set_storage", "curtail", @@ -249,6 +255,20 @@ def _sample_redispatch(self, rnd_update=None): rnd_update["redispatch"] = rnd_disp rnd_update["redispatch"] = rnd_update["redispatch"].astype(dt_float) return rnd_update + + def _sample_flexibility(self, rnd_update=None): + if rnd_update is None: + rnd_update = {} + loads = np.arange(self.n_load)[self.load_flexible] + rnd_load = self.space_prng.choice(loads) + rd = -self.load_max_ramp_down[rnd_load] + ru = self.load_max_ramp_up[rnd_load] + rnd_load_disp = (ru - rd) * self.space_prng.random() + rd + rnd_disp = np.zeros(self.n_load) + rnd_disp[rnd_load] = rnd_load_disp + rnd_update["flexibility"] = rnd_disp + rnd_update["flexibility"] = rnd_update["flexibility"].astype(dt_float) + return rnd_update def _sample_storage_power(self, rnd_update=None): if rnd_update is None: @@ -365,6 +385,8 @@ def sample(self) -> BaseAction: rnd_update = self._sample_raise_alarm() elif rnd_type == cls.RAISE_ALERT_ID: rnd_update = self._sample_raise_alert() + elif rnd_type == cls.FLEXIBILITY_ID: + rnd_update = self._sample_flexibility() else: raise Grid2OpException( "Impossible to sample action of type {}".format(rnd_type) @@ -1389,6 +1411,85 @@ def get_all_unitary_redispatch( return res + @staticmethod + def get_all_unitary_flexibility( + action_space, num_down=5, num_up=5, max_ratio_value=1.0 + ) -> List[BaseAction]: + """ + Flexibility (demand response) actions are continuous. This method is an helper to convert the continuous + action into "discrete actions" (by rounding). + + The number of actions is equal to num_down + num_up (by default 10) per dispatchable generator. + + + This method acts as followed: + + - it will divide the interval [-load_max_ramp_down, 0] into `num_down`, each will make + a distinct action (then counting `num_down` different action, because 0.0 is removed) + - it will do the same for [0, load_max_ramp_up] + + .. note:: + With this "helper" only one load is affected by one action. For example + there are no action acting on both load 1 and load 2 at the same + time. + + Parameters + ---------- + action_space: :class:`ActionSpace` + The action space used. + + num_down: ``int`` + In how many intervals the "flexibility down" will be split + + num_up: ``int`` + In how many intervals the "flexibility up" will be split + + max_ratio_value: ``float`` + Expressed in terms of ratio of `load_max_ramp_up` / `load_max_ramp_down`, it gives the maximum value + that will be used to generate the actions. For example, if `max_ratio_value=0.5`, then it will not + generate actions that attempts to apply flexibility more than `0.5 * load_max_ramp_up` (or less than + `- 0.5 * load_max_ramp_down`). This helps reducing the instability that can be caused by demand + response. + + Returns + ------- + res: ``list`` + The list of all discretized flexibility actions. + + """ + + res = [] + n_load = len(action_space.load_flexible) + + for load_idx in range(n_load): + # Skip non-dispatchable generators + if not action_space.load_flexible[load_idx]: + continue + + # Create evenly spaced positive interval + ramps_up = np.linspace( + 0.0, max_ratio_value * action_space.load_max_ramp_up[load_idx], num=num_up + ) + ramps_up = ramps_up[1:] # Exclude flexibility of 0MW + + # Create evenly spaced negative interval + ramps_down = np.linspace( + -max_ratio_value * action_space.load_max_ramp_down[load_idx], + 0.0, + num=num_down, + ) + ramps_down = ramps_down[:-1] # Exclude flexibility of 0MW + + # Merge intervals + ramps = np.append(ramps_up, ramps_down) + + # Create ramp up actions + for ramp in ramps: + action = action_space({"flexibility": [(load_idx, ramp)]}) + res.append(action) + + return res + @staticmethod def get_all_unitary_curtail(action_space : Self, num_bin: int=10, min_value: float=0.5) -> List[BaseAction]: """ @@ -1629,7 +1730,68 @@ def _aux_get_back_to_ref_state_redisp(self, res, obs, precision=1e-5): (gen_id, red_) for gen_id, red_ in zip(need_redisp, reds) ] res["redispatching"].append(act) + + def _aux_get_back_to_ref_state_flexibility(self, res, obs, precision=1e-5): + # TODO this is ugly, probably slow and could definitely be optimized + notflex_setpoint = np.abs(obs.target_flex) >= 1e-7 + if notflex_setpoint.any(): + need_flex = (notflex_setpoint).nonzero()[0] + res["flexibility"] = [] + # combine generators and do not exceed ramps (up or down) + rem = np.zeros(self.n_load, dtype=dt_float) + nb_ = np.zeros(self.n_load, dtype=dt_int) + for load_id in need_flex: + if obs.target_flex[load_id] > 0.0: + div_ = obs.target_flex[load_id] / obs.load_max_ramp_down[load_id] + else: + div_ = -obs.target_flex[load_id] / obs.load_max_ramp_up[load_id] + div_ = np.round(div_, precision) + nb_[load_id] = int(div_) + if div_ != int(div_): + if obs.target_flex[load_id] > 0.0: + rem[load_id] = ( + obs.target_flex[load_id] + - obs.load_max_ramp_down[load_id] * nb_[load_id] + ) + else: + rem[load_id] = ( + -obs.target_flex[load_id] + - obs.load_max_ramp_up[load_id] * nb_[load_id] + ) + nb_[load_id] += 1 + # now create the proper actions + for nb_act in range(np.max(nb_)): + act = self.actionClass() + if not self.supports_type("flexibility"): + warnings.warn( + "Some flexibility has been set, but you cannot modify it with your action type. Impossible to get back to the original flexibility settings." + ) + break + reds = np.zeros(self.n_load, dtype=dt_float) + for load_id in need_flex: + if nb_act >= nb_[load_id]: + # nothing to add for this generator in this case + continue + if obs.target_dispatch[load_id] > 0.0: + if nb_act < nb_[load_id] - 1 or ( + np.abs(rem[load_id]) <= 1e-7 and nb_act == nb_[load_id] - 1 + ): + reds[load_id] = -obs.load_max_ramp_down[load_id] + else: + reds[load_id] = -rem[load_id] + else: + if nb_act < nb_[load_id] - 1 or ( + np.abs(rem[load_id]) <= 1e-7 and nb_act == nb_[load_id] - 1 + ): + reds[load_id] = obs.load_max_ramp_up[load_id] + else: + reds[load_id] = rem[load_id] + act.flexibility = [ + (load_id, red_) for load_id, red_ in zip(need_flex, reds) + ] + res["flexibility"].append(act) + def _aux_get_back_to_ref_state_storage( self, res, obs, storage_setpoint, precision=5 ): @@ -1709,6 +1871,7 @@ def get_back_to_ref_state( ) -> Dict[Literal["powerline", "substation", "redispatching", + "flexibility", "storage", "curtailment"], List[BaseAction]]: @@ -1726,17 +1889,19 @@ def get_back_to_ref_state( - an action that acts on a single powerline - an action on a single substation - a redispatching action (acting possibly on all generators) + - a flexibility action (acting possibly on all loads) - a storage action (acting possibly on all generators) The list might be relatively long, in the case where lots of actions are needed. Depending on the rules of the game (for example limiting the action on one single substation), in order to get back to this topology, multiple consecutive actions will need to be implemented. - It is returned as a dictionnary of list. This dictionnary has 4 keys: + It is returned as a dictionnary of list. This dictionnary has 5 keys: - "powerline" for the list of actions needed to set back the powerlines in a proper state (connected). They can be of type "change_line" or "set_line". - "substation" for the list of actions needed to set back each substation in its initial state (everything connected to bus 1). They can be implemented as "set_bus" or "change_bus" - "redispatching": for the redispatching actions (there can be multiple redispatching actions needed because of the ramps of the generator) + - "flexibility": for the flexibility actions (there can be multiple flexibility actions needed because of the ramps of the loads) - "storage": for action on storage units (you might need to perform multiple storage actions because of the maximum power these units can absorb / produce ) - "curtailment": for curtailment action (usually at most one such action is needed) @@ -1795,6 +1960,8 @@ def get_back_to_ref_state( self._aux_get_back_to_ref_state_sub(res, obs) # redispatching self._aux_get_back_to_ref_state_redisp(res, obs, precision=precision) + # flexibility + self._aux_get_back_to_ref_state_flexibility(res, obs, precision=precision) # storage self._aux_get_back_to_ref_state_storage( res, obs, storage_setpoint, precision=precision diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 7ac2f902..5c8b9c05 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -483,6 +483,7 @@ class GridObjects: BEFORE_COMPAT_VERSION : ClassVar[str] = "neurips_2020_compat" MIN_VERSION_DETACH : ClassVar[str] = version.parse("1.11.0.dev0") + MIN_VERSION_FLEX : ClassVar[str] = version.parse("1.12.0") glop_version : ClassVar[str] = GRID2OP_CURRENT_VERSION_STR _INIT_GRID_CLS = None # do not modify that, this is handled by grid2op automatically diff --git a/grid2op/data/l2rpn_neurips_2020_track2/__init__.py b/grid2op/data/l2rpn_neurips_2020_track2/__init__.py new file mode 100644 index 00000000..bd6582d7 --- /dev/null +++ b/grid2op/data/l2rpn_neurips_2020_track2/__init__.py @@ -0,0 +1 @@ +# DO NOT REMOVE, automatically generated by grid2op \ No newline at end of file From ea920296422631417eb7e28660e0854d564c884d Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Tue, 16 Sep 2025 09:53:59 +0200 Subject: [PATCH 04/38] Add: flexibility_is_available attribute to GridObjects Signed-off-by: Xavier Weiss --- docs/grid2op_dev/action.rst | 2 ++ grid2op/Action/baseAction.py | 34 +++++++++++++++++++++++++++------- grid2op/Space/GridObjects.py | 17 +++++++++++++++++ 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/docs/grid2op_dev/action.rst b/docs/grid2op_dev/action.rst index 73a6d170..27c40395 100644 --- a/docs/grid2op_dev/action.rst +++ b/docs/grid2op_dev/action.rst @@ -197,6 +197,8 @@ Add tests ----------- TODO +* Add a test for creating the action type + Add documentation ------------------- diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 2eaf6721..90ea7d02 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -679,13 +679,6 @@ def _redispatch(self) -> np.ndarray: self._private_redispatch = cls._build_attr("_redispatch") return self._private_redispatch - @property - def _flexibility(self) -> np.ndarray: - if self._private_flexibility is None: - cls = type(self) - self._private_flexibility = cls._build_attr("_flexibility") - return self._private_flexibility - @property def _set_line_status(self) -> np.ndarray: if self._private_set_line_status is None: @@ -797,6 +790,13 @@ def _detach_storage(self) -> Optional[np.ndarray]: if self._private_detach_storage is None and cls.detachment_is_allowed: self._private_detach_storage = cls._build_attr("_detach_storage") return self._private_detach_storage + + @property + def _flexibility(self) -> np.ndarray: + cls = type(self) + if self._private_flexibility is None and cls.flexibility_is_available: + self._private_flexibility = cls._build_attr("_flexibility") + return self._private_flexibility @classmethod def _build_dict_attr_if_needed(cls): @@ -908,6 +908,26 @@ def process_detachment(cls): cls.authorized_keys.remove(el) cls._update_value_set() return super().process_detachment() + + @classmethod + def process_flexibility(cls): + if not cls.flexibility_is_available: + # this is really important, otherwise things from grid2op base types will be affected + cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) + cls.authorized_keys = copy.deepcopy(cls.authorized_keys) + # remove the flexibility from the list to vector + for el in ["_flexibility"]: + if el in cls.attr_list_vect: + try: + cls.attr_list_vect.remove(el) + except ValueError: + pass + # remove the flexibility from the allowed action + for el in ["flexibility"]: + if el in cls.authorized_keys: + cls.authorized_keys.remove(el) + cls._update_value_set() + return super().process_flexibility() def copy(self) -> "BaseAction": # sometimes this method is used... diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 5c8b9c05..7febfd34 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -399,6 +399,10 @@ class GridObjects: shunts_data_available: ``bool`` Whether or not the backend support the shunt data. [*class attribute*] + + flexibility_is_available: ``bool`` + Whether or not flexibility data is available in the environment [*class attribute*] + .. versionadded:: 1.12.x n_shunt: ``int`` or ``None`` Number of shunts on the grid. It might be ``None`` if the backend does not support shunts. [*class attribute*] @@ -616,6 +620,9 @@ class GridObjects: n_shunt : ClassVar[Optional[int]] = None name_shunt : ClassVar[Optional[np.ndarray]] = None shunt_to_subid : ClassVar[Optional[np.ndarray]] = None + + # flexibility, not available in every environment + flexibility_is_available: ClassVar[bool] = False # alarm / alert assistant_warning_type = None @@ -2915,6 +2922,7 @@ def _aux_finish_init_grid_from_file(cls): cls._compute_pos_big_topo_cls() cls.process_shunt_static_data() cls.process_detachment() + cls.process_flexiblity() @classmethod def _aux_init_grid_from_cls(cls, gridobj, name_res): @@ -4479,6 +4487,13 @@ def process_detachment(cls): """ pass + @classmethod + def process_flexiblity(cls): + """process demand response / flexibility that is applied to loads, is overloaded for :class:`grid2op.Action.BaseAction` + or :class:`grid2op.Observation.BaseObservation` + """ + pass + @classmethod def set_no_storage(cls): """ @@ -4575,6 +4590,7 @@ def _build_cls_from_import(name_cls, path_env): my_class.process_grid2op_compat() my_class.process_detachment() my_class.process_shunt_static_data() + my_class.process_flexiblity() return my_class @staticmethod @@ -4612,6 +4628,7 @@ def init_grid_from_dict_for_pickle(name_res, orig_cls, cls_attr): res_cls.process_grid2op_compat() res_cls.process_shunt_static_data() res_cls.process_detachment() + res_cls.process_flexibility() # add the class in the "globals" for reuse later globals()[name_res] = res_cls From 8dc001b43243ddb711d4e2675719cb7bd0e69ffe Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Tue, 16 Sep 2025 09:59:20 +0200 Subject: [PATCH 05/38] Add: Flexibility Test Environment Signed-off-by: Xavier Weiss --- .../chronics/0/load_p.csv.bz2 | Bin 0 -> 87 bytes .../chronics/0/load_p_forecasted.csv.bz2 | Bin 0 -> 98 bytes .../chronics/0/prod_p.csv.bz2 | Bin 0 -> 83 bytes .../chronics/0/prod_p_forecasted.csv.bz2 | Bin 0 -> 83 bytes .../chronics/0/prod_v_forecasted.csv.bz2 | Bin 0 -> 83 bytes .../5bus_example_with_flexibility/config.py | 19 + .../difficulty_levels.json | 62 + .../flex_loads_charac.csv | 4 + .../5bus_example_with_flexibility/grid.json | 1339 +++++++++++++++++ .../grid_layout.json | 22 + .../5bus_example_with_flexibility/params.json | 1 + .../prods_charac.csv | 3 + grid2op/tests/test_flexibility.py | 96 ++ 13 files changed, 1546 insertions(+) create mode 100644 grid2op/data_test/5bus_example_with_flexibility/chronics/0/load_p.csv.bz2 create mode 100644 grid2op/data_test/5bus_example_with_flexibility/chronics/0/load_p_forecasted.csv.bz2 create mode 100644 grid2op/data_test/5bus_example_with_flexibility/chronics/0/prod_p.csv.bz2 create mode 100644 grid2op/data_test/5bus_example_with_flexibility/chronics/0/prod_p_forecasted.csv.bz2 create mode 100644 grid2op/data_test/5bus_example_with_flexibility/chronics/0/prod_v_forecasted.csv.bz2 create mode 100644 grid2op/data_test/5bus_example_with_flexibility/config.py create mode 100644 grid2op/data_test/5bus_example_with_flexibility/difficulty_levels.json create mode 100644 grid2op/data_test/5bus_example_with_flexibility/flex_loads_charac.csv create mode 100644 grid2op/data_test/5bus_example_with_flexibility/grid.json create mode 100644 grid2op/data_test/5bus_example_with_flexibility/grid_layout.json create mode 100644 grid2op/data_test/5bus_example_with_flexibility/params.json create mode 100644 grid2op/data_test/5bus_example_with_flexibility/prods_charac.csv create mode 100644 grid2op/tests/test_flexibility.py diff --git a/grid2op/data_test/5bus_example_with_flexibility/chronics/0/load_p.csv.bz2 b/grid2op/data_test/5bus_example_with_flexibility/chronics/0/load_p.csv.bz2 new file mode 100644 index 0000000000000000000000000000000000000000..0b4140b9fef499f7f9f068a99db66cbc14bc33fe GIT binary patch literal 87 zcmV-d0I2^$T4*^jL0KkKSv6-l`T#RQ+W-I(00DeR005)}pa60kQZx-T&`nhafDENT t#;TxjRD#5+2UDq34b?!=R17MCg(Ltr+GDm(yY%CIzaj8qGbEPXWBnn6ALHUonY17oEI14C1z0z;7`N9HTJ1+!K%WEg6(-PLMr m4X_Ml*1hU*EY#s`2;b5Gqg6uw%mIgP%ZnGeS_Di083F)9pd2m$ literal 0 HcmV?d00001 diff --git a/grid2op/data_test/5bus_example_with_flexibility/chronics/0/prod_p_forecasted.csv.bz2 b/grid2op/data_test/5bus_example_with_flexibility/chronics/0/prod_p_forecasted.csv.bz2 new file mode 100644 index 0000000000000000000000000000000000000000..81e867b529a3ed5ff5192205533ec0dd75fe5843 GIT binary patch literal 83 zcmV-Z0IdH)T4*^jL0KkKSu6`C5&$P_+W-I(00DW3004r4AOLb15YyBif@-7zpv0&f pa;gU^plGTFTdIMKs)5?59ZH~OR3x5;p^hdjUC9*TLO`%Am`FKI9jpKV literal 0 HcmV?d00001 diff --git a/grid2op/data_test/5bus_example_with_flexibility/chronics/0/prod_v_forecasted.csv.bz2 b/grid2op/data_test/5bus_example_with_flexibility/chronics/0/prod_v_forecasted.csv.bz2 new file mode 100644 index 0000000000000000000000000000000000000000..c685c39c018b1ba892967015af0a2923fa8abcb0 GIT binary patch literal 83 zcmV-Z0IdH)T4*^jL0KkKSq8~9X#g)uTL1tM00D3a004r4AOLVc3uTcBmuHbT0jyk9CrW! literal 0 HcmV?d00001 diff --git a/grid2op/data_test/5bus_example_with_flexibility/config.py b/grid2op/data_test/5bus_example_with_flexibility/config.py new file mode 100644 index 00000000..cdc2758f --- /dev/null +++ b/grid2op/data_test/5bus_example_with_flexibility/config.py @@ -0,0 +1,19 @@ +from grid2op.Action import CompleteAction +from grid2op.Reward import L2RPNReward +from grid2op.Rules import DefaultRules +from grid2op.Chronics import Multifolder +from grid2op.Chronics import GridStateFromFileWithForecasts +from grid2op.Backend import PandaPowerBackend + +config = { + "backend": PandaPowerBackend, + "action_class": CompleteAction, + "observation_class": None, + "reward_class": L2RPNReward, + "gamerules_class": DefaultRules, + "chronics_class": Multifolder, + "grid_value_class": GridStateFromFileWithForecasts, + "volagecontroler_class": None, + "thermal_limits": None, + "names_chronics_to_grid": None, +} diff --git a/grid2op/data_test/5bus_example_with_flexibility/difficulty_levels.json b/grid2op/data_test/5bus_example_with_flexibility/difficulty_levels.json new file mode 100644 index 00000000..581f9117 --- /dev/null +++ b/grid2op/data_test/5bus_example_with_flexibility/difficulty_levels.json @@ -0,0 +1,62 @@ +{ + "0": { + "NO_OVERFLOW_DISCONNECTION": true, + "NB_TIMESTEP_OVERFLOW_ALLOWED": 9999, + "NB_TIMESTEP_COOLDOWN_SUB": 0, + "NB_TIMESTEP_COOLDOWN_LINE": 0, + "HARD_OVERFLOW_THRESHOLD": 9999, + "NB_TIMESTEP_RECONNECTION": 0, + "IGNORE_MIN_UP_DOWN_TIME": true, + "ALLOW_FLEX_LOAD_SWITCH_OFF": true, + "ALLOW_DISPATCH_GEN_SWITCH_OFF": true, + "ENV_DC": false, + "FORECAST_DC": false, + "MAX_SUB_CHANGED": 1, + "MAX_LINE_STATUS_CHANGED": 1 + }, + "1": { + "NO_OVERFLOW_DISCONNECTION": false, + "NB_TIMESTEP_OVERFLOW_ALLOWED": 6, + "NB_TIMESTEP_COOLDOWN_SUB": 0, + "NB_TIMESTEP_COOLDOWN_LINE": 0, + "HARD_OVERFLOW_THRESHOLD": 300, + "NB_TIMESTEP_RECONNECTION": 1, + "IGNORE_MIN_UP_DOWN_TIME": true, + "ALLOW_FLEX_LOAD_SWITCH_OFF": true, + "ALLOW_DISPATCH_GEN_SWITCH_OFF": true, + "ENV_DC": false, + "FORECAST_DC": false, + "MAX_SUB_CHANGED": 1, + "MAX_LINE_STATUS_CHANGED": 1 + }, + "2": { + "NO_OVERFLOW_DISCONNECTION": false, + "NB_TIMESTEP_OVERFLOW_ALLOWED": 3, + "NB_TIMESTEP_COOLDOWN_SUB": 1, + "NB_TIMESTEP_COOLDOWN_LINE": 1, + "HARD_OVERFLOW_THRESHOLD": 250, + "NB_TIMESTEP_RECONNECTION": 6, + "IGNORE_MIN_UP_DOWN_TIME": true, + "ALLOW_FLEX_LOAD_SWITCH_OFF": true, + "ALLOW_DISPATCH_GEN_SWITCH_OFF": true, + "ENV_DC": false, + "FORECAST_DC": false, + "MAX_SUB_CHANGED": 1, + "MAX_LINE_STATUS_CHANGED": 1 + }, + "competition": { + "NO_OVERFLOW_DISCONNECTION": false, + "NB_TIMESTEP_OVERFLOW_ALLOWED": 3, + "NB_TIMESTEP_COOLDOWN_SUB": 3, + "NB_TIMESTEP_COOLDOWN_LINE": 3, + "HARD_OVERFLOW_THRESHOLD": 200, + "NB_TIMESTEP_RECONNECTION": 12, + "IGNORE_MIN_UP_DOWN_TIME": true, + "ALLOW_FLEX_LOAD_SWITCH_OFF": true, + "ALLOW_DISPATCH_GEN_SWITCH_OFF": true, + "ENV_DC": false, + "FORECAST_DC": false, + "MAX_SUB_CHANGED": 1, + "MAX_LINE_STATUS_CHANGED": 1 + } +} diff --git a/grid2op/data_test/5bus_example_with_flexibility/flex_loads_charac.csv b/grid2op/data_test/5bus_example_with_flexibility/flex_loads_charac.csv new file mode 100644 index 00000000..9d9f763e --- /dev/null +++ b/grid2op/data_test/5bus_example_with_flexibility/flex_loads_charac.csv @@ -0,0 +1,4 @@ +size,name,bus,is_flexible,max_ramp_up,max_ramp_down,min_up_time,min_down_time,marginal_cost,x,y,V +10.7,load_0_0,5,false,0,0,0,0,99999,0,0,102. +9.7,load_3_1,5,True,2.0,2.0,4,140,0,0,0,102. +10.0,load_4_2,0,True,3.0,3.0,4,4,140,0,400,102. \ No newline at end of file diff --git a/grid2op/data_test/5bus_example_with_flexibility/grid.json b/grid2op/data_test/5bus_example_with_flexibility/grid.json new file mode 100644 index 00000000..2e9dd0c7 --- /dev/null +++ b/grid2op/data_test/5bus_example_with_flexibility/grid.json @@ -0,0 +1,1339 @@ +{ + "_module": "pandapower.auxiliary", + "_class": "pandapowerNet", + "_object": { + "bus": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"vn_kv\",\"type\",\"zone\",\"in_service\"],\"index\":[0,1,2,3,4],\"data\":[[\"substation_1\",100.0,\"b\",null,true],[\"substation_2\",100.0,\"b\",null,true],[\"substation_3\",100.0,\"b\",null,true],[\"substation_4\",100.0,\"b\",null,true],[\"substation_5\",100.0,\"b\",null,true]]}", + "dtype": { + "name": "object", + "vn_kv": "float64", + "type": "object", + "zone": "object", + "in_service": "bool" + }, + "orient": "split" + }, + "load": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"p_mw\",\"q_mvar\",\"const_z_percent\",\"const_i_percent\",\"sn_mva\",\"scaling\",\"in_service\",\"type\"],\"index\":[0,1,2],\"data\":[[\"load_0_0\",0,10.0,7.0,0.0,0.0,null,1.0,true,null],[\"load_3_1\",3,10.0,7.0,0.0,0.0,null,1.0,true,null],[\"load_4_2\",4,10.0,7.0,0.0,0.0,null,1.0,true,null]]}", + "dtype": { + "name": "object", + "bus": "uint32", + "p_mw": "float64", + "q_mvar": "float64", + "const_z_percent": "float64", + "const_i_percent": "float64", + "sn_mva": "float64", + "scaling": "float64", + "in_service": "bool", + "type": "object" + }, + "orient": "split" + }, + "sgen": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"p_mw\",\"q_mvar\",\"sn_mva\",\"scaling\",\"in_service\",\"type\",\"current_source\"],\"index\":[],\"data\":[]}", + "dtype": { + "name": "object", + "bus": "int64", + "p_mw": "float64", + "q_mvar": "float64", + "sn_mva": "float64", + "scaling": "float64", + "in_service": "bool", + "type": "object", + "current_source": "bool" + }, + "orient": "split" + }, + "storage": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"p_mw\",\"q_mvar\",\"sn_mva\",\"soc_percent\",\"min_e_mwh\",\"max_e_mwh\",\"scaling\",\"in_service\",\"type\"],\"index\":[],\"data\":[]}", + "dtype": { + "name": "object", + "bus": "int64", + "p_mw": "float64", + "q_mvar": "float64", + "sn_mva": "float64", + "soc_percent": "float64", + "min_e_mwh": "float64", + "max_e_mwh": "float64", + "scaling": "float64", + "in_service": "bool", + "type": "object" + }, + "orient": "split" + }, + "gen": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"p_mw\",\"vm_pu\",\"sn_mva\",\"min_q_mvar\",\"max_q_mvar\",\"scaling\",\"slack\",\"in_service\",\"type\"],\"index\":[0,1],\"data\":[[\"gen_0_0\",0,10.0,1.02,null,null,null,1.0,false,true,null],[\"gen_1_1\",1,20.0,1.02,null,null,null,1.0,true,true,null]]}", + "dtype": { + "name": "object", + "bus": "uint32", + "p_mw": "float64", + "vm_pu": "float64", + "sn_mva": "float64", + "min_q_mvar": "float64", + "max_q_mvar": "float64", + "scaling": "float64", + "slack": "bool", + "in_service": "bool", + "type": "object" + }, + "orient": "split" + }, + "switch": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"bus\",\"element\",\"et\",\"type\",\"closed\",\"name\",\"z_ohm\"],\"index\":[],\"data\":[]}", + "dtype": { + "bus": "int64", + "element": "int64", + "et": "object", + "type": "object", + "closed": "bool", + "name": "object", + "z_ohm": "float64" + }, + "orient": "split" + }, + "shunt": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"bus\",\"name\",\"q_mvar\",\"p_mw\",\"vn_kv\",\"step\",\"max_step\",\"in_service\"],\"index\":[],\"data\":[]}", + "dtype": { + "bus": "uint32", + "name": "object", + "q_mvar": "float64", + "p_mw": "float64", + "vn_kv": "float64", + "step": "uint32", + "max_step": "uint32", + "in_service": "bool" + }, + "orient": "split" + }, + "ext_grid": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"vm_pu\",\"va_degree\",\"in_service\"],\"index\":[],\"data\":[]}", + "dtype": { + "name": "object", + "bus": "uint32", + "vm_pu": "float64", + "va_degree": "float64", + "in_service": "bool" + }, + "orient": "split" + }, + "line": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"std_type\",\"from_bus\",\"to_bus\",\"length_km\",\"r_ohm_per_km\",\"x_ohm_per_km\",\"c_nf_per_km\",\"g_us_per_km\",\"max_i_ka\",\"df\",\"parallel\",\"type\",\"in_service\"],\"index\":[0,1,2,3,4,5,6,7],\"data\":[[null,\"NAYY 4x50 SE\",0,1,4.0,0.642,0.083,210.0,0.0,0.6,1.0,1,\"cs\",true],[\"0_2_2\",\"NAYY 4x50 SE\",0,2,4.47,0.642,0.083,210.0,0.0,0.22,1.0,1,\"cs\",true],[\"0_3_3\",\"NAYY 4x50 SE\",0,3,5.65,0.642,0.083,210.0,0.0,0.16,1.0,1,\"cs\",true],[\"0_4_4\",\"NAYY 4x50 SE\",0,4,4.0,0.642,0.083,210.0,0.0,0.16,1.0,1,\"cs\",true],[\"1_2_5\",\"NAYY 4x50 SE\",1,2,2.0,0.642,0.083,210.0,0.0,0.6,1.0,1,\"cs\",true],[\"2_3_6\",\"NAYY 4x50 SE\",2,3,2.0,0.642,0.083,210.0,0.0,0.3,1.0,1,\"cs\",true],[\"2_3_7\",\"NAYY 4x50 SE\",2,3,2.0,0.642,0.083,210.0,0.0,0.3,1.0,1,\"cs\",true],[\"3_4_8\",\"NAYY 4x50 SE\",3,4,4.0,0.642,0.083,210.0,0.0,0.16,1.0,1,\"cs\",true]]}", + "dtype": { + "name": "object", + "std_type": "object", + "from_bus": "uint32", + "to_bus": "uint32", + "length_km": "float64", + "r_ohm_per_km": "float64", + "x_ohm_per_km": "float64", + "c_nf_per_km": "float64", + "g_us_per_km": "float64", + "max_i_ka": "float64", + "df": "float64", + "parallel": "uint32", + "type": "object", + "in_service": "bool" + }, + "orient": "split" + }, + "trafo": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"std_type\",\"hv_bus\",\"lv_bus\",\"sn_mva\",\"vn_hv_kv\",\"vn_lv_kv\",\"vk_percent\",\"vkr_percent\",\"pfe_kw\",\"i0_percent\",\"shift_degree\",\"tap_side\",\"tap_neutral\",\"tap_min\",\"tap_max\",\"tap_step_percent\",\"tap_step_degree\",\"tap_pos\",\"tap_phase_shifter\",\"parallel\",\"df\",\"in_service\"],\"index\":[],\"data\":[]}", + "dtype": { + "name": "object", + "std_type": "object", + "hv_bus": "uint32", + "lv_bus": "uint32", + "sn_mva": "float64", + "vn_hv_kv": "float64", + "vn_lv_kv": "float64", + "vk_percent": "float64", + "vkr_percent": "float64", + "pfe_kw": "float64", + "i0_percent": "float64", + "shift_degree": "float64", + "tap_side": "object", + "tap_neutral": "int32", + "tap_min": "int32", + "tap_max": "int32", + "tap_step_percent": "float64", + "tap_step_degree": "float64", + "tap_pos": "int32", + "tap_phase_shifter": "bool", + "parallel": "uint32", + "df": "float64", + "in_service": "bool" + }, + "orient": "split" + }, + "trafo3w": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"std_type\",\"hv_bus\",\"mv_bus\",\"lv_bus\",\"sn_hv_mva\",\"sn_mv_mva\",\"sn_lv_mva\",\"vn_hv_kv\",\"vn_mv_kv\",\"vn_lv_kv\",\"vk_hv_percent\",\"vk_mv_percent\",\"vk_lv_percent\",\"vkr_hv_percent\",\"vkr_mv_percent\",\"vkr_lv_percent\",\"pfe_kw\",\"i0_percent\",\"shift_mv_degree\",\"shift_lv_degree\",\"tap_side\",\"tap_neutral\",\"tap_min\",\"tap_max\",\"tap_step_percent\",\"tap_step_degree\",\"tap_pos\",\"tap_at_star_point\",\"in_service\"],\"index\":[],\"data\":[]}", + "dtype": { + "name": "object", + "std_type": "object", + "hv_bus": "uint32", + "mv_bus": "uint32", + "lv_bus": "uint32", + "sn_hv_mva": "float64", + "sn_mv_mva": "float64", + "sn_lv_mva": "float64", + "vn_hv_kv": "float64", + "vn_mv_kv": "float64", + "vn_lv_kv": "float64", + "vk_hv_percent": "float64", + "vk_mv_percent": "float64", + "vk_lv_percent": "float64", + "vkr_hv_percent": "float64", + "vkr_mv_percent": "float64", + "vkr_lv_percent": "float64", + "pfe_kw": "float64", + "i0_percent": "float64", + "shift_mv_degree": "float64", + "shift_lv_degree": "float64", + "tap_side": "object", + "tap_neutral": "int32", + "tap_min": "int32", + "tap_max": "int32", + "tap_step_percent": "float64", + "tap_step_degree": "float64", + "tap_pos": "int32", + "tap_at_star_point": "bool", + "in_service": "bool" + }, + "orient": "split" + }, + "impedance": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"from_bus\",\"to_bus\",\"rft_pu\",\"xft_pu\",\"rtf_pu\",\"xtf_pu\",\"sn_mva\",\"in_service\"],\"index\":[],\"data\":[]}", + "dtype": { + "name": "object", + "from_bus": "uint32", + "to_bus": "uint32", + "rft_pu": "float64", + "xft_pu": "float64", + "rtf_pu": "float64", + "xtf_pu": "float64", + "sn_mva": "float64", + "in_service": "bool" + }, + "orient": "split" + }, + "dcline": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"from_bus\",\"to_bus\",\"p_mw\",\"loss_percent\",\"loss_mw\",\"vm_from_pu\",\"vm_to_pu\",\"max_p_mw\",\"min_q_from_mvar\",\"min_q_to_mvar\",\"max_q_from_mvar\",\"max_q_to_mvar\",\"in_service\"],\"index\":[],\"data\":[]}", + "dtype": { + "name": "object", + "from_bus": "uint32", + "to_bus": "uint32", + "p_mw": "float64", + "loss_percent": "float64", + "loss_mw": "float64", + "vm_from_pu": "float64", + "vm_to_pu": "float64", + "max_p_mw": "float64", + "min_q_from_mvar": "float64", + "min_q_to_mvar": "float64", + "max_q_from_mvar": "float64", + "max_q_to_mvar": "float64", + "in_service": "bool" + }, + "orient": "split" + }, + "ward": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"ps_mw\",\"qs_mvar\",\"qz_mvar\",\"pz_mw\",\"in_service\"],\"index\":[],\"data\":[]}", + "dtype": { + "name": "object", + "bus": "uint32", + "ps_mw": "float64", + "qs_mvar": "float64", + "qz_mvar": "float64", + "pz_mw": "float64", + "in_service": "bool" + }, + "orient": "split" + }, + "xward": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"ps_mw\",\"qs_mvar\",\"qz_mvar\",\"pz_mw\",\"r_ohm\",\"x_ohm\",\"vm_pu\",\"in_service\"],\"index\":[],\"data\":[]}", + "dtype": { + "name": "object", + "bus": "uint32", + "ps_mw": "float64", + "qs_mvar": "float64", + "qz_mvar": "float64", + "pz_mw": "float64", + "r_ohm": "float64", + "x_ohm": "float64", + "vm_pu": "float64", + "in_service": "bool" + }, + "orient": "split" + }, + "measurement": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"measurement_type\",\"element_type\",\"element\",\"value\",\"std_dev\",\"side\"],\"index\":[],\"data\":[]}", + "dtype": { + "name": "object", + "measurement_type": "object", + "element_type": "object", + "element": "uint32", + "value": "float64", + "std_dev": "float64", + "side": "object" + }, + "orient": "split" + }, + "pwl_cost": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"power_type\",\"element\",\"et\",\"points\"],\"index\":[],\"data\":[]}", + "dtype": { + "power_type": "object", + "element": "object", + "et": "object", + "points": "object" + }, + "orient": "split" + }, + "poly_cost": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"element\",\"et\",\"cp0_eur\",\"cp1_eur_per_mw\",\"cp2_eur_per_mw2\",\"cq0_eur\",\"cq1_eur_per_mvar\",\"cq2_eur_per_mvar2\"],\"index\":[],\"data\":[]}", + "dtype": { + "element": "object", + "et": "object", + "cp0_eur": "float64", + "cp1_eur_per_mw": "float64", + "cp2_eur_per_mw2": "float64", + "cq0_eur": "float64", + "cq1_eur_per_mvar": "float64", + "cq2_eur_per_mvar2": "float64" + }, + "orient": "split" + }, + "line_geodata": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"coords\"],\"index\":[0,1,2,3,4,5,6,7],\"data\":[[[[0,0],[0,4]]],[[[0,0],[2,4]]],[[[0,0],[4,4]]],[[[0,0],[4,0]]],[[[0,4],[2,4]]],[[[2,4],[3,4.2],[4,4]]],[[[2,4],[3,3.8],[4,4]]],[[[4,4],[4,0]]]]}", + "dtype": { + "coords": "object" + }, + "orient": "split" + }, + "bus_geodata": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"x\",\"y\",\"coords\"],\"index\":[0,1,2,3,4],\"data\":[[0.0,0.0,null],[0.0,4.0,null],[2.0,4.0,null],[4.0,4.0,null],[4.0,0.0,null]]}", + "dtype": { + "x": "float64", + "y": "float64", + "coords": "object" + }, + "orient": "split" + }, + "version": "2.1.0", + "converged": true, + "name": "5bus", + "f_hz": 50.0, + "sn_mva": 1, + "std_types": { + "line": { + "NAYY 4x50 SE": { + "c_nf_per_km": 210, + "r_ohm_per_km": 0.642, + "x_ohm_per_km": 0.083, + "max_i_ka": 0.142, + "type": "cs", + "q_mm2": 50, + "alpha": 0.00403 + }, + "NAYY 4x120 SE": { + "c_nf_per_km": 264, + "r_ohm_per_km": 0.225, + "x_ohm_per_km": 0.08, + "max_i_ka": 0.242, + "type": "cs", + "q_mm2": 120, + "alpha": 0.00403 + }, + "NAYY 4x150 SE": { + "c_nf_per_km": 261, + "r_ohm_per_km": 0.208, + "x_ohm_per_km": 0.08, + "max_i_ka": 0.27, + "type": "cs", + "q_mm2": 150, + "alpha": 0.00403 + }, + "NA2XS2Y 1x95 RM/25 12/20 kV": { + "c_nf_per_km": 216, + "r_ohm_per_km": 0.313, + "x_ohm_per_km": 0.132, + "max_i_ka": 0.252, + "type": "cs", + "q_mm2": 95, + "alpha": 0.00403 + }, + "NA2XS2Y 1x185 RM/25 12/20 kV": { + "c_nf_per_km": 273, + "r_ohm_per_km": 0.161, + "x_ohm_per_km": 0.117, + "max_i_ka": 0.362, + "type": "cs", + "q_mm2": 185, + "alpha": 0.00403 + }, + "NA2XS2Y 1x240 RM/25 12/20 kV": { + "c_nf_per_km": 304, + "r_ohm_per_km": 0.122, + "x_ohm_per_km": 0.112, + "max_i_ka": 0.421, + "type": "cs", + "q_mm2": 240, + "alpha": 0.00403 + }, + "NA2XS2Y 1x95 RM/25 6/10 kV": { + "c_nf_per_km": 315, + "r_ohm_per_km": 0.313, + "x_ohm_per_km": 0.123, + "max_i_ka": 0.249, + "type": "cs", + "q_mm2": 95, + "alpha": 0.00403 + }, + "NA2XS2Y 1x185 RM/25 6/10 kV": { + "c_nf_per_km": 406, + "r_ohm_per_km": 0.161, + "x_ohm_per_km": 0.11, + "max_i_ka": 0.358, + "type": "cs", + "q_mm2": 185, + "alpha": 0.00403 + }, + "NA2XS2Y 1x240 RM/25 6/10 kV": { + "c_nf_per_km": 456, + "r_ohm_per_km": 0.122, + "x_ohm_per_km": 0.105, + "max_i_ka": 0.416, + "type": "cs", + "q_mm2": 240, + "alpha": 0.00403 + }, + "NA2XS2Y 1x150 RM/25 12/20 kV": { + "c_nf_per_km": 250, + "r_ohm_per_km": 0.206, + "x_ohm_per_km": 0.116, + "max_i_ka": 0.319, + "type": "cs", + "q_mm2": 150, + "alpha": 0.00403 + }, + "NA2XS2Y 1x120 RM/25 12/20 kV": { + "c_nf_per_km": 230, + "r_ohm_per_km": 0.253, + "x_ohm_per_km": 0.119, + "max_i_ka": 0.283, + "type": "cs", + "q_mm2": 120, + "alpha": 0.00403 + }, + "NA2XS2Y 1x70 RM/25 12/20 kV": { + "c_nf_per_km": 190, + "r_ohm_per_km": 0.443, + "x_ohm_per_km": 0.132, + "max_i_ka": 0.22, + "type": "cs", + "q_mm2": 70, + "alpha": 0.00403 + }, + "NA2XS2Y 1x150 RM/25 6/10 kV": { + "c_nf_per_km": 360, + "r_ohm_per_km": 0.206, + "x_ohm_per_km": 0.11, + "max_i_ka": 0.315, + "type": "cs", + "q_mm2": 150, + "alpha": 0.00403 + }, + "NA2XS2Y 1x120 RM/25 6/10 kV": { + "c_nf_per_km": 340, + "r_ohm_per_km": 0.253, + "x_ohm_per_km": 0.113, + "max_i_ka": 0.28, + "type": "cs", + "q_mm2": 120, + "alpha": 0.00403 + }, + "NA2XS2Y 1x70 RM/25 6/10 kV": { + "c_nf_per_km": 280, + "r_ohm_per_km": 0.443, + "x_ohm_per_km": 0.123, + "max_i_ka": 0.217, + "type": "cs", + "q_mm2": 70, + "alpha": 0.00403 + }, + "N2XS(FL)2Y 1x120 RM/35 64/110 kV": { + "c_nf_per_km": 112, + "r_ohm_per_km": 0.153, + "x_ohm_per_km": 0.166, + "max_i_ka": 0.366, + "type": "cs", + "q_mm2": 120, + "alpha": 0.00393 + }, + "N2XS(FL)2Y 1x185 RM/35 64/110 kV": { + "c_nf_per_km": 125, + "r_ohm_per_km": 0.099, + "x_ohm_per_km": 0.156, + "max_i_ka": 0.457, + "type": "cs", + "q_mm2": 185, + "alpha": 0.00393 + }, + "N2XS(FL)2Y 1x240 RM/35 64/110 kV": { + "c_nf_per_km": 135, + "r_ohm_per_km": 0.075, + "x_ohm_per_km": 0.149, + "max_i_ka": 0.526, + "type": "cs", + "q_mm2": 240, + "alpha": 0.00393 + }, + "N2XS(FL)2Y 1x300 RM/35 64/110 kV": { + "c_nf_per_km": 144, + "r_ohm_per_km": 0.06, + "x_ohm_per_km": 0.144, + "max_i_ka": 0.588, + "type": "cs", + "q_mm2": 300, + "alpha": 0.00393 + }, + "15-AL1/3-ST1A 0.4": { + "c_nf_per_km": 11, + "r_ohm_per_km": 1.8769, + "x_ohm_per_km": 0.35, + "max_i_ka": 0.105, + "type": "ol", + "q_mm2": 16, + "alpha": 0.00403 + }, + "24-AL1/4-ST1A 0.4": { + "c_nf_per_km": 11.25, + "r_ohm_per_km": 1.2012, + "x_ohm_per_km": 0.335, + "max_i_ka": 0.14, + "type": "ol", + "q_mm2": 24, + "alpha": 0.00403 + }, + "48-AL1/8-ST1A 0.4": { + "c_nf_per_km": 12.2, + "r_ohm_per_km": 0.5939, + "x_ohm_per_km": 0.3, + "max_i_ka": 0.21, + "type": "ol", + "q_mm2": 48, + "alpha": 0.00403 + }, + "94-AL1/15-ST1A 0.4": { + "c_nf_per_km": 13.2, + "r_ohm_per_km": 0.306, + "x_ohm_per_km": 0.29, + "max_i_ka": 0.35, + "type": "ol", + "q_mm2": 94, + "alpha": 0.00403 + }, + "34-AL1/6-ST1A 10.0": { + "c_nf_per_km": 9.7, + "r_ohm_per_km": 0.8342, + "x_ohm_per_km": 0.36, + "max_i_ka": 0.17, + "type": "ol", + "q_mm2": 34, + "alpha": 0.00403 + }, + "48-AL1/8-ST1A 10.0": { + "c_nf_per_km": 10.1, + "r_ohm_per_km": 0.5939, + "x_ohm_per_km": 0.35, + "max_i_ka": 0.21, + "type": "ol", + "q_mm2": 48, + "alpha": 0.00403 + }, + "70-AL1/11-ST1A 10.0": { + "c_nf_per_km": 10.4, + "r_ohm_per_km": 0.4132, + "x_ohm_per_km": 0.339, + "max_i_ka": 0.29, + "type": "ol", + "q_mm2": 70, + "alpha": 0.00403 + }, + "94-AL1/15-ST1A 10.0": { + "c_nf_per_km": 10.75, + "r_ohm_per_km": 0.306, + "x_ohm_per_km": 0.33, + "max_i_ka": 0.35, + "type": "ol", + "q_mm2": 94, + "alpha": 0.00403 + }, + "122-AL1/20-ST1A 10.0": { + "c_nf_per_km": 11.1, + "r_ohm_per_km": 0.2376, + "x_ohm_per_km": 0.323, + "max_i_ka": 0.41, + "type": "ol", + "q_mm2": 122, + "alpha": 0.00403 + }, + "149-AL1/24-ST1A 10.0": { + "c_nf_per_km": 11.25, + "r_ohm_per_km": 0.194, + "x_ohm_per_km": 0.315, + "max_i_ka": 0.47, + "type": "ol", + "q_mm2": 149, + "alpha": 0.00403 + }, + "34-AL1/6-ST1A 20.0": { + "c_nf_per_km": 9.15, + "r_ohm_per_km": 0.8342, + "x_ohm_per_km": 0.382, + "max_i_ka": 0.17, + "type": "ol", + "q_mm2": 34, + "alpha": 0.00403 + }, + "48-AL1/8-ST1A 20.0": { + "c_nf_per_km": 9.5, + "r_ohm_per_km": 0.5939, + "x_ohm_per_km": 0.372, + "max_i_ka": 0.21, + "type": "ol", + "q_mm2": 48, + "alpha": 0.00403 + }, + "70-AL1/11-ST1A 20.0": { + "c_nf_per_km": 9.7, + "r_ohm_per_km": 0.4132, + "x_ohm_per_km": 0.36, + "max_i_ka": 0.29, + "type": "ol", + "q_mm2": 70, + "alpha": 0.00403 + }, + "94-AL1/15-ST1A 20.0": { + "c_nf_per_km": 10, + "r_ohm_per_km": 0.306, + "x_ohm_per_km": 0.35, + "max_i_ka": 0.35, + "type": "ol", + "q_mm2": 94, + "alpha": 0.00403 + }, + "122-AL1/20-ST1A 20.0": { + "c_nf_per_km": 10.3, + "r_ohm_per_km": 0.2376, + "x_ohm_per_km": 0.344, + "max_i_ka": 0.41, + "type": "ol", + "q_mm2": 122, + "alpha": 0.00403 + }, + "149-AL1/24-ST1A 20.0": { + "c_nf_per_km": 10.5, + "r_ohm_per_km": 0.194, + "x_ohm_per_km": 0.337, + "max_i_ka": 0.47, + "type": "ol", + "q_mm2": 149, + "alpha": 0.00403 + }, + "184-AL1/30-ST1A 20.0": { + "c_nf_per_km": 10.75, + "r_ohm_per_km": 0.1571, + "x_ohm_per_km": 0.33, + "max_i_ka": 0.535, + "type": "ol", + "q_mm2": 184, + "alpha": 0.00403 + }, + "243-AL1/39-ST1A 20.0": { + "c_nf_per_km": 11, + "r_ohm_per_km": 0.1188, + "x_ohm_per_km": 0.32, + "max_i_ka": 0.645, + "type": "ol", + "q_mm2": 243, + "alpha": 0.00403 + }, + "48-AL1/8-ST1A 110.0": { + "c_nf_per_km": 8, + "r_ohm_per_km": 0.5939, + "x_ohm_per_km": 0.46, + "max_i_ka": 0.21, + "type": "ol", + "q_mm2": 48, + "alpha": 0.00403 + }, + "70-AL1/11-ST1A 110.0": { + "c_nf_per_km": 8.4, + "r_ohm_per_km": 0.4132, + "x_ohm_per_km": 0.45, + "max_i_ka": 0.29, + "type": "ol", + "q_mm2": 70, + "alpha": 0.00403 + }, + "94-AL1/15-ST1A 110.0": { + "c_nf_per_km": 8.65, + "r_ohm_per_km": 0.306, + "x_ohm_per_km": 0.44, + "max_i_ka": 0.35, + "type": "ol", + "q_mm2": 94, + "alpha": 0.00403 + }, + "122-AL1/20-ST1A 110.0": { + "c_nf_per_km": 8.5, + "r_ohm_per_km": 0.2376, + "x_ohm_per_km": 0.43, + "max_i_ka": 0.41, + "type": "ol", + "q_mm2": 122, + "alpha": 0.00403 + }, + "149-AL1/24-ST1A 110.0": { + "c_nf_per_km": 8.75, + "r_ohm_per_km": 0.194, + "x_ohm_per_km": 0.41, + "max_i_ka": 0.47, + "type": "ol", + "q_mm2": 149, + "alpha": 0.00403 + }, + "184-AL1/30-ST1A 110.0": { + "c_nf_per_km": 8.8, + "r_ohm_per_km": 0.1571, + "x_ohm_per_km": 0.4, + "max_i_ka": 0.535, + "type": "ol", + "q_mm2": 184, + "alpha": 0.00403 + }, + "243-AL1/39-ST1A 110.0": { + "c_nf_per_km": 9, + "r_ohm_per_km": 0.1188, + "x_ohm_per_km": 0.39, + "max_i_ka": 0.645, + "type": "ol", + "q_mm2": 243, + "alpha": 0.00403 + }, + "305-AL1/39-ST1A 110.0": { + "c_nf_per_km": 9.2, + "r_ohm_per_km": 0.0949, + "x_ohm_per_km": 0.38, + "max_i_ka": 0.74, + "type": "ol", + "q_mm2": 305, + "alpha": 0.00403 + }, + "490-AL1/64-ST1A 110.0": { + "c_nf_per_km": 9.75, + "r_ohm_per_km": 0.059, + "x_ohm_per_km": 0.37, + "max_i_ka": 0.96, + "type": "ol", + "q_mm2": 490, + "alpha": 0.00403 + }, + "679-AL1/86-ST1A 110.0": { + "c_nf_per_km": 9.95, + "r_ohm_per_km": 0.042, + "x_ohm_per_km": 0.36, + "max_i_ka": 0.115, + "type": "ol", + "q_mm2": 679, + "alpha": 0.00403 + }, + "490-AL1/64-ST1A 220.0": { + "c_nf_per_km": 10, + "r_ohm_per_km": 0.059, + "x_ohm_per_km": 0.285, + "max_i_ka": 0.96, + "type": "ol", + "q_mm2": 490, + "alpha": 0.00403 + }, + "679-AL1/86-ST1A 220.0": { + "c_nf_per_km": 11.7, + "r_ohm_per_km": 0.042, + "x_ohm_per_km": 0.275, + "max_i_ka": 0.115, + "type": "ol", + "q_mm2": 679, + "alpha": 0.00403 + }, + "490-AL1/64-ST1A 380.0": { + "c_nf_per_km": 11, + "r_ohm_per_km": 0.059, + "x_ohm_per_km": 0.253, + "max_i_ka": 0.96, + "type": "ol", + "q_mm2": 490, + "alpha": 0.00403 + }, + "679-AL1/86-ST1A 380.0": { + "c_nf_per_km": 14.6, + "r_ohm_per_km": 0.042, + "x_ohm_per_km": 0.25, + "max_i_ka": 0.115, + "type": "ol", + "q_mm2": 679, + "alpha": 0.00403 + } + }, + "trafo": { + "160 MVA 380/110 kV": { + "i0_percent": 0.06, + "pfe_kw": 60, + "vkr_percent": 0.25, + "sn_mva": 160, + "vn_lv_kv": 110.0, + "vn_hv_kv": 380.0, + "vk_percent": 12.2, + "shift_degree": 0, + "vector_group": "Yy0", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_phase_shifter": false + }, + "100 MVA 220/110 kV": { + "i0_percent": 0.06, + "pfe_kw": 55, + "vkr_percent": 0.26, + "sn_mva": 100, + "vn_lv_kv": 110.0, + "vn_hv_kv": 220.0, + "vk_percent": 12.0, + "shift_degree": 0, + "vector_group": "Yy0", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_phase_shifter": false + }, + "63 MVA 110/20 kV": { + "i0_percent": 0.04, + "pfe_kw": 22, + "vkr_percent": 0.32, + "sn_mva": 63, + "vn_lv_kv": 20.0, + "vn_hv_kv": 110.0, + "vk_percent": 18, + "shift_degree": 150, + "vector_group": "YNd5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_phase_shifter": false + }, + "40 MVA 110/20 kV": { + "i0_percent": 0.05, + "pfe_kw": 18, + "vkr_percent": 0.34, + "sn_mva": 40, + "vn_lv_kv": 20.0, + "vn_hv_kv": 110.0, + "vk_percent": 16.2, + "shift_degree": 150, + "vector_group": "YNd5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_phase_shifter": false + }, + "25 MVA 110/20 kV": { + "i0_percent": 0.07, + "pfe_kw": 14, + "vkr_percent": 0.41, + "sn_mva": 25, + "vn_lv_kv": 20.0, + "vn_hv_kv": 110.0, + "vk_percent": 12, + "shift_degree": 150, + "vector_group": "YNd5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_phase_shifter": false + }, + "63 MVA 110/10 kV": { + "sn_mva": 63, + "vn_hv_kv": 110, + "vn_lv_kv": 10, + "vk_percent": 18, + "vkr_percent": 0.32, + "pfe_kw": 22, + "i0_percent": 0.04, + "shift_degree": 150, + "vector_group": "YNd5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_phase_shifter": false + }, + "40 MVA 110/10 kV": { + "sn_mva": 40, + "vn_hv_kv": 110, + "vn_lv_kv": 10, + "vk_percent": 16.2, + "vkr_percent": 0.34, + "pfe_kw": 18, + "i0_percent": 0.05, + "shift_degree": 150, + "vector_group": "YNd5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_phase_shifter": false + }, + "25 MVA 110/10 kV": { + "sn_mva": 25, + "vn_hv_kv": 110, + "vn_lv_kv": 10, + "vk_percent": 12, + "vkr_percent": 0.41, + "pfe_kw": 14, + "i0_percent": 0.07, + "shift_degree": 150, + "vector_group": "YNd5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_phase_shifter": false + }, + "0.25 MVA 20/0.4 kV": { + "sn_mva": 0.25, + "vn_hv_kv": 20, + "vn_lv_kv": 0.4, + "vk_percent": 6, + "vkr_percent": 1.44, + "pfe_kw": 0.8, + "i0_percent": 0.32, + "shift_degree": 150, + "vector_group": "Yzn5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -2, + "tap_max": 2, + "tap_step_degree": 0, + "tap_step_percent": 2.5, + "tap_phase_shifter": false + }, + "0.4 MVA 20/0.4 kV": { + "sn_mva": 0.4, + "vn_hv_kv": 20, + "vn_lv_kv": 0.4, + "vk_percent": 6, + "vkr_percent": 1.425, + "pfe_kw": 1.35, + "i0_percent": 0.3375, + "shift_degree": 150, + "vector_group": "Dyn5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -2, + "tap_max": 2, + "tap_step_degree": 0, + "tap_step_percent": 2.5, + "tap_phase_shifter": false + }, + "0.63 MVA 20/0.4 kV": { + "sn_mva": 0.63, + "vn_hv_kv": 20, + "vn_lv_kv": 0.4, + "vk_percent": 6, + "vkr_percent": 1.206, + "pfe_kw": 1.65, + "i0_percent": 0.2619, + "shift_degree": 150, + "vector_group": "Dyn5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -2, + "tap_max": 2, + "tap_step_degree": 0, + "tap_step_percent": 2.5, + "tap_phase_shifter": false + }, + "0.25 MVA 10/0.4 kV": { + "sn_mva": 0.25, + "vn_hv_kv": 10, + "vn_lv_kv": 0.4, + "vk_percent": 4, + "vkr_percent": 1.2, + "pfe_kw": 0.6, + "i0_percent": 0.24, + "shift_degree": 150, + "vector_group": "Dyn5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -2, + "tap_max": 2, + "tap_step_degree": 0, + "tap_step_percent": 2.5, + "tap_phase_shifter": false + }, + "0.4 MVA 10/0.4 kV": { + "sn_mva": 0.4, + "vn_hv_kv": 10, + "vn_lv_kv": 0.4, + "vk_percent": 4, + "vkr_percent": 1.325, + "pfe_kw": 0.95, + "i0_percent": 0.2375, + "shift_degree": 150, + "vector_group": "Dyn5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -2, + "tap_max": 2, + "tap_step_degree": 0, + "tap_step_percent": 2.5, + "tap_phase_shifter": false + }, + "0.63 MVA 10/0.4 kV": { + "sn_mva": 0.63, + "vn_hv_kv": 10, + "vn_lv_kv": 0.4, + "vk_percent": 4, + "vkr_percent": 1.0794, + "pfe_kw": 1.18, + "i0_percent": 0.1873, + "shift_degree": 150, + "vector_group": "Dyn5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -2, + "tap_max": 2, + "tap_step_degree": 0, + "tap_step_percent": 2.5, + "tap_phase_shifter": false + } + }, + "trafo3w": { + "63/25/38 MVA 110/20/10 kV": { + "sn_hv_mva": 63, + "sn_mv_mva": 25, + "sn_lv_mva": 38, + "vn_hv_kv": 110, + "vn_mv_kv": 20, + "vn_lv_kv": 10, + "vk_hv_percent": 10.4, + "vk_mv_percent": 10.4, + "vk_lv_percent": 10.4, + "vkr_hv_percent": 0.28, + "vkr_mv_percent": 0.32, + "vkr_lv_percent": 0.35, + "pfe_kw": 35, + "i0_percent": 0.89, + "shift_mv_degree": 0, + "shift_lv_degree": 0, + "vector_group": "YN0yn0yn0", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -10, + "tap_max": 10, + "tap_step_percent": 1.2 + }, + "63/25/38 MVA 110/10/10 kV": { + "sn_hv_mva": 63, + "sn_mv_mva": 25, + "sn_lv_mva": 38, + "vn_hv_kv": 110, + "vn_mv_kv": 10, + "vn_lv_kv": 10, + "vk_hv_percent": 10.4, + "vk_mv_percent": 10.4, + "vk_lv_percent": 10.4, + "vkr_hv_percent": 0.28, + "vkr_mv_percent": 0.32, + "vkr_lv_percent": 0.35, + "pfe_kw": 35, + "i0_percent": 0.89, + "shift_mv_degree": 0, + "shift_lv_degree": 0, + "vector_group": "YN0yn0yn0", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -10, + "tap_max": 10, + "tap_step_percent": 1.2 + } + } + }, + "res_bus": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"vm_pu\",\"va_degree\",\"p_mw\",\"q_mvar\"],\"index\":[0,1,2,3,4],\"data\":[[1.02,-0.845445168673926,0.0,-111.791243672370911],[1.02,0.0,-21.729831330858325,116.839935541152954],[1.019214100496144,-0.409103297622625,0.0,0.0],[1.018637116919488,-0.503470352662766,10.0,7.0],[1.017983079721402,-0.653497665026562,10.0,7.0]]}", + "dtype": { + "vm_pu": "float64", + "va_degree": "float64", + "p_mw": "float64", + "q_mvar": "float64" + }, + "orient": "split" + }, + "res_line": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_from_mw\",\"q_from_mvar\",\"p_to_mw\",\"q_to_mvar\",\"pl_mw\",\"ql_mvar\",\"i_from_ka\",\"i_to_ka\",\"i_ka\",\"vm_from_pu\",\"va_from_degree\",\"vm_to_pu\",\"va_to_degree\",\"loading_percent\"],\"index\":[0,1,2,3,4,5,6,7],\"data\":[[-7.167647147657727,57.480079867900443,8.03525639977348,-60.113463233922118,0.867609252115754,-2.633383366021676,0.327874112511858,0.343286326507116,0.343286326507116,1.02,-0.845445168673926,1.02,0.0,57.214387751185988],[-0.657313913963437,25.969126903729045,0.866078469150186,-29.007927174007612,0.208764555186749,-3.038800270278568,0.147040043868819,0.164393305610081,0.164393305610081,1.02,-0.845445168673926,1.019214100496144,-0.409103297622625,74.724229822763931],[1.64566972119938,15.370129751576128,-1.540268914180618,-19.229415550834709,0.105400807018762,-3.859285799258581,0.087496748884432,0.109338903896103,0.109338903896103,1.02,-0.845445168673926,1.018637116919488,-0.503470352662766,68.336814935064211],[6.179291340421495,12.971907266349552,-6.119076735247816,-15.70424981919658,0.060214605173678,-2.732342552847028,0.081330018729726,0.095589209712924,0.095589209712924,1.02,-0.845445168673926,1.017983079721402,-0.653497665026562,59.743256070577175],[13.694574931085771,-56.726472302863066,-13.283848894885464,55.407854241119566,0.410726036200307,-1.3186180617435,0.330312825878128,0.322760996590474,0.330312825878128,1.02,0.0,1.019214100496144,-0.409103297622625,55.052137646354595],[6.208885212872048,-13.199963533555254,-6.184761786109662,11.833197159642042,0.024123426762386,-1.366766373913212,0.082632108556076,0.075677384410291,0.082632108556076,1.019214100496144,-0.409103297622625,1.018637116919488,-0.503470352662766,27.544036185358689],[6.208885212872048,-13.199963533555254,-6.184761786109662,11.833197159642042,0.024123426762386,-1.366766373913212,0.082632108556076,0.075677384410291,0.082632108556076,1.019214100496144,-0.409103297622625,1.018637116919488,-0.503470352662766,27.544036185358689],[3.909792486391969,-11.436978768449999,-3.88092326475316,8.704249819196738,0.028869221638809,-2.732728949253261,0.068506463438984,0.054050881891821,0.068506463438984,1.018637116919488,-0.503470352662766,1.017983079721402,-0.653497665026562,42.816539649365005]]}", + "dtype": { + "p_from_mw": "float64", + "q_from_mvar": "float64", + "p_to_mw": "float64", + "q_to_mvar": "float64", + "pl_mw": "float64", + "ql_mvar": "float64", + "i_from_ka": "float64", + "i_to_ka": "float64", + "i_ka": "float64", + "vm_from_pu": "float64", + "va_from_degree": "float64", + "vm_to_pu": "float64", + "va_to_degree": "float64", + "loading_percent": "float64" + }, + "orient": "split" + }, + "res_trafo": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_hv_mw\",\"q_hv_mvar\",\"p_lv_mw\",\"q_lv_mvar\",\"pl_mw\",\"ql_mvar\",\"i_hv_ka\",\"i_lv_ka\",\"vm_hv_pu\",\"va_hv_degree\",\"vm_lv_pu\",\"va_lv_degree\",\"loading_percent\"],\"index\":[],\"data\":[]}", + "dtype": { + "p_hv_mw": "float64", + "q_hv_mvar": "float64", + "p_lv_mw": "float64", + "q_lv_mvar": "float64", + "pl_mw": "float64", + "ql_mvar": "float64", + "i_hv_ka": "float64", + "i_lv_ka": "float64", + "vm_hv_pu": "float64", + "va_hv_degree": "float64", + "vm_lv_pu": "float64", + "va_lv_degree": "float64", + "loading_percent": "float64" + }, + "orient": "split" + }, + "res_trafo3w": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_hv_mw\",\"q_hv_mvar\",\"p_mv_mw\",\"q_mv_mvar\",\"p_lv_mw\",\"q_lv_mvar\",\"pl_mw\",\"ql_mvar\",\"i_hv_ka\",\"i_mv_ka\",\"i_lv_ka\",\"vm_hv_pu\",\"va_hv_degree\",\"vm_mv_pu\",\"va_mv_degree\",\"vm_lv_pu\",\"va_lv_degree\",\"va_internal_degree\",\"vm_internal_pu\",\"loading_percent\"],\"index\":[],\"data\":[]}", + "dtype": { + "p_hv_mw": "float64", + "q_hv_mvar": "float64", + "p_mv_mw": "float64", + "q_mv_mvar": "float64", + "p_lv_mw": "float64", + "q_lv_mvar": "float64", + "pl_mw": "float64", + "ql_mvar": "float64", + "i_hv_ka": "float64", + "i_mv_ka": "float64", + "i_lv_ka": "float64", + "vm_hv_pu": "float64", + "va_hv_degree": "float64", + "vm_mv_pu": "float64", + "va_mv_degree": "float64", + "vm_lv_pu": "float64", + "va_lv_degree": "float64", + "va_internal_degree": "float64", + "vm_internal_pu": "float64", + "loading_percent": "float64" + }, + "orient": "split" + }, + "res_impedance": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_from_mw\",\"q_from_mvar\",\"p_to_mw\",\"q_to_mvar\",\"pl_mw\",\"ql_mvar\",\"i_from_ka\",\"i_to_ka\"],\"index\":[],\"data\":[]}", + "dtype": { + "p_from_mw": "float64", + "q_from_mvar": "float64", + "p_to_mw": "float64", + "q_to_mvar": "float64", + "pl_mw": "float64", + "ql_mvar": "float64", + "i_from_ka": "float64", + "i_to_ka": "float64" + }, + "orient": "split" + }, + "res_ext_grid": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64" + }, + "orient": "split" + }, + "res_load": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[0,1,2],\"data\":[[10.0,7.0],[10.0,7.0],[10.0,7.0]]}", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64" + }, + "orient": "split" + }, + "res_sgen": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64" + }, + "orient": "split" + }, + "res_storage": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64" + }, + "orient": "split" + }, + "res_shunt": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"vm_pu\"],\"index\":[],\"data\":[]}", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64", + "vm_pu": "float64" + }, + "orient": "split" + }, + "res_gen": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"va_degree\",\"vm_pu\"],\"index\":[0,1],\"data\":[[10.0,118.791243672370911,-0.845445168673926,1.02],[21.729831330858325,-116.839935541152954,0.0,1.02]]}", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64", + "va_degree": "float64", + "vm_pu": "float64" + }, + "orient": "split" + }, + "res_ward": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"vm_pu\"],\"index\":[],\"data\":[]}", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64", + "vm_pu": "float64" + }, + "orient": "split" + }, + "res_xward": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"vm_pu\",\"va_internal_degree\",\"vm_internal_pu\"],\"index\":[],\"data\":[]}", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64", + "vm_pu": "float64", + "va_internal_degree": "float64", + "vm_internal_pu": "float64" + }, + "orient": "split" + }, + "res_dcline": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_from_mw\",\"q_from_mvar\",\"p_to_mw\",\"q_to_mvar\",\"pl_mw\",\"vm_from_pu\",\"va_from_degree\",\"vm_to_pu\",\"va_to_degree\"],\"index\":[],\"data\":[]}", + "dtype": { + "p_from_mw": "float64", + "q_from_mvar": "float64", + "p_to_mw": "float64", + "q_to_mvar": "float64", + "pl_mw": "float64", + "vm_from_pu": "float64", + "va_from_degree": "float64", + "vm_to_pu": "float64", + "va_to_degree": "float64" + }, + "orient": "split" + }, + "user_pf_options": {}, + "OPF_converged": false + } +} diff --git a/grid2op/data_test/5bus_example_with_flexibility/grid_layout.json b/grid2op/data_test/5bus_example_with_flexibility/grid_layout.json new file mode 100644 index 00000000..05780856 --- /dev/null +++ b/grid2op/data_test/5bus_example_with_flexibility/grid_layout.json @@ -0,0 +1,22 @@ +{ + "sub_0": [ + 0.0, + 0.0 + ], + "sub_1": [ + 0.0, + 400.0 + ], + "sub_2": [ + 200.0, + 400.0 + ], + "sub_3": [ + 400.0, + 400.0 + ], + "sub_4": [ + 400.0, + 0.0 + ] +} \ No newline at end of file diff --git a/grid2op/data_test/5bus_example_with_flexibility/params.json b/grid2op/data_test/5bus_example_with_flexibility/params.json new file mode 100644 index 00000000..124f5f23 --- /dev/null +++ b/grid2op/data_test/5bus_example_with_flexibility/params.json @@ -0,0 +1 @@ +{"NB_TIMESTEP_TOPOLOGY_REMODIF": 19} \ No newline at end of file diff --git a/grid2op/data_test/5bus_example_with_flexibility/prods_charac.csv b/grid2op/data_test/5bus_example_with_flexibility/prods_charac.csv new file mode 100644 index 00000000..d82fbbad --- /dev/null +++ b/grid2op/data_test/5bus_example_with_flexibility/prods_charac.csv @@ -0,0 +1,3 @@ +Pmax,Pmin,name,type,bus,max_ramp_up,max_ramp_down,min_up_time,min_down_time,marginal_cost,shut_down_cost,start_cost,x,y,V +10,0.0,gen_0_0,wind,5,0,0,0,0,0,0,0,0,0,102. +30,0.0,gen_1_1,thermal,0,10,10,4,4,70,1,2,0,400,102. \ No newline at end of file diff --git a/grid2op/tests/test_flexibility.py b/grid2op/tests/test_flexibility.py new file mode 100644 index 00000000..95d19e5d --- /dev/null +++ b/grid2op/tests/test_flexibility.py @@ -0,0 +1,96 @@ +# Copyright (c) 2019-2022, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + +import warnings +import os +import unittest +import numpy as np + +from grid2op.tests.helper_path_test import PATH_DATA_TEST +import grid2op + +class TestFlexibility(unittest.TestCase): + def setUp(self) -> None: + self.env_name = os.path.join(PATH_DATA_TEST, "5bus_example_with_flexibility") + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make( + self.env_name, + test=True, + _add_to_name=type(self).__name__ + ) + self.env.set_id(0) + _ = self.env.reset() + self.ref_obs, *_ = self.env.step(self.env.action_space()) + + self.env.set_id(0) + _ = self.env.reset() + + self.flex_max_ramp_up = self.env.action_space( + {"flexibility": [(el, self.env.load_max_ramp_up[el]) for el in np.nonzero(self.env.load_flexible)[0]]} + ) + self.flex_max_ramp_down = self.env.action_space( + {"flexibility": [(el, -self.env.load_max_ramp_down[el]) for el in np.nonzero(self.env.load_flexible)[0]]} + ) + self.flex_all_zero = self.env.action_space( + {"flexibility": [(el, 0.0) for el in np.nonzero(self.env.load_flexible)[0]]} + ) + self.flex_small_up = self.env.action_space( + {"flexibility": [(el, 0.01) for el in np.nonzero(self.env.load_flexible)[0]]} + ) + self.flex_small_down = self.env.action_space( + {"flexibility": [(el, 0.01) for el in np.nonzero(self.env.load_flexible)[0]]} + ) + + def test_zero_flex(self): + flex_obs, *_ = self.env.step(self.flex_all_zero) + flex_mask = self.env.load_flexible + # Change in load relative to DoNothing scenario (i.e. normal Chronics) + change_in_load = self.ref_obs.load_p[flex_mask] - flex_obs.load_p[flex_mask] + assert np.isclose(change_in_load, np.zeros(flex_mask.sum()), atol=0.001).all() + + def test_flex_small_up(self): + flex_obs, *_ = self.env.step(self.flex_small_up) + flex_mask = self.env.load_flexible + # Change in load relative to DoNothing scenario (i.e. normal Chronics) + change_in_load = flex_obs.load_p[flex_mask] - self.ref_obs.load_p[flex_mask] + assert np.isclose(change_in_load, self.flex_small_up.flexibility[flex_mask], atol=0.001).all() + + def test_flex_small_down(self): + flex_obs, *_ = self.env.step(self.flex_small_down) + flex_mask = self.env.load_flexible + + # Change in load relative to DoNothing scenario (i.e. normal Chronics) + change_in_load = flex_obs.load_p[flex_mask] - self.ref_obs.load_p[flex_mask] + assert np.isclose(change_in_load, self.flex_small_down.flexibility[flex_mask], atol=0.001).all() + + def test_flex_max_ramp_up(self): + flex_obs, *_ = self.env.step(self.flex_max_ramp_up) + flex_mask = self.env.load_flexible + # Load meets max ramp up, or the max size of the load + ref_load = self.ref_obs.load_p[flex_mask] + expected_load = ref_load + self.flex_max_ramp_up.flexibility[flex_mask] + maximum_feasible_load = np.minimum(self.env.load_size[flex_mask], expected_load) + assert np.isclose(flex_obs.load_p[flex_mask], maximum_feasible_load, atol=0.001).all() + + def test_flex_max_ramp_down(self): + flex_obs, *_ = self.env.step(self.flex_max_ramp_down) + flex_mask = self.env.load_flexible + # Load meets max ramp down, or the minimum load (of 0) + ref_load = self.ref_obs.load_p[flex_mask] + expected_load = ref_load + self.flex_max_ramp_down.flexibility[flex_mask] + minimum_feasible_load = np.maximum(np.zeros(flex_mask.sum()), expected_load) + assert np.isclose(flex_obs.load_p[flex_mask], minimum_feasible_load, atol=0.001).all() + + def test_flex_in_obs(self): + flex_obs, *_ = self.env.step(self.flex_max_ramp_up) + assert np.isclose(flex_obs.target_flex, self.flex_max_ramp_up.flexibility).all() + +if __name__ == "__main__": + unittest.main() From 1aba6b5ecf69978a2ecc59f3bd2ce9f2b0f1d67c Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Tue, 16 Sep 2025 10:11:19 +0200 Subject: [PATCH 06/38] Add: flexibility does not affect topology Signed-off-by: Xavier Weiss --- docs/grid2op_dev/action.rst | 1 + grid2op/Action/baseAction.py | 1 + grid2op/Space/GridObjects.py | 32 ++++++++++++++++++++++++++++++-- grid2op/Space/default_var.py | 3 +++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/docs/grid2op_dev/action.rst b/docs/grid2op_dev/action.rst index 27c40395..3f4eea6f 100644 --- a/docs/grid2op_dev/action.rst +++ b/docs/grid2op_dev/action.rst @@ -133,6 +133,7 @@ TODO add the `cls.attr_list_vect` to baseAction: * Add a check to `impact_on_objects()` for your action * Add a boolean for your action in `get_types()` * Modify `_aux_effect_on_XXX()` as appropriate +* If you action does NOT modify topology, add it to the `_dont_affect_topology()` method Worry about the backward compatibility **************************************** diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 90ea7d02..8ad5ac22 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -1841,6 +1841,7 @@ def _dont_affect_topology(self) -> bool: and (not self._modif_detach_load) and (not self._modif_detach_gen) and (not self._modif_detach_storage) + and (not self._modif_flexibility) # new in 1.12.x ) def _aux_get_topo_impact_notopo(self, _store_in_cache: bool): diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 7febfd34..a2f6e3f7 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -31,7 +31,8 @@ from grid2op.typing_variables import CLS_AS_DICT_TYPING, N_BUSBAR_PER_SUB_TYPING from grid2op.Exceptions import * from grid2op.Space.space_utils import extract_from_dict, save_to_dict, ElTypeInfo -from grid2op.Space.default_var import (DEFAULT_ALLOW_DETACHMENT, +from grid2op.Space.default_var import (DEFAULT_FLEXIBILITY_IS_AVAILABLE, + DEFAULT_ALLOW_DETACHMENT, DEFAULT_N_BUSBAR_PER_SUB, GRID2OP_CLASSES_ENV_FOLDER, GRID2OP_CURRENT_VERSION_STR, @@ -622,7 +623,7 @@ class GridObjects: shunt_to_subid : ClassVar[Optional[np.ndarray]] = None # flexibility, not available in every environment - flexibility_is_available: ClassVar[bool] = False + flexibility_is_available: ClassVar[bool] = DEFAULT_FLEXIBILITY_IS_AVAILABLE # alarm / alert assistant_warning_type = None @@ -861,6 +862,9 @@ def _clear_grid_dependant_class_attributes(cls) -> None: cls.alertable_line_names = [] cls.alertable_line_ids = [] + # flexibility + cls.flexibility_is_available = DEFAULT_FLEXIBILITY_IS_AVAILABLE + @classmethod def _update_value_set(cls) -> None: """ @@ -3011,6 +3015,9 @@ def init_grid(cls, gridobj, force=False, extra_name=None, force_module=None, _lo if gridobj.detachment_is_allowed != DEFAULT_ALLOW_DETACHMENT: name_res += "_allowDetach" + + if gridobj.flexibility_is_available != DEFAULT_FLEXIBILITY_IS_AVAILABLE: + name_res += "_allowFlexibility" if _local_dir_cls is not None and gridobj._PATH_GRID_CLASSES is not None: # new in grid2op 1.10.3: @@ -4363,6 +4370,25 @@ class res(GridObjects): dict_, nm_attr, lambda x: np.array(x).astype(type_attr) ), ) + + # Demand Response / Flexibility + if dict_.get("load_size", None) is None: + cls.flexible_load_available = False + else: + cls.flexible_load_available = True + type_attr_flex_load = [ + dt_float, + dt_bool, + dt_float, + dt_float, + dt_float, + ] + for nm_attr, type_attr in zip(cls._li_attr_flex_load, type_attr_flex_load): + setattr(cls, nm_attr, + extract_from_dict( + dict_, nm_attr, + lambda x, type_attr=type_attr: np.array(x).astype(type_attr) + )) cls.grid_layout = extract_from_dict(dict_, "grid_layout", lambda x: x) @@ -4437,6 +4463,8 @@ class res(GridObjects): cls.process_detachment() + cls.process_flexiblity() # new in 1.12.x + if "assistant_warning_type" in dict_: cls.assistant_warning_type = dict_["assistant_warning_type"] else: diff --git a/grid2op/Space/default_var.py b/grid2op/Space/default_var.py index b5b22b3c..53777ed8 100644 --- a/grid2op/Space/default_var.py +++ b/grid2op/Space/default_var.py @@ -18,6 +18,9 @@ #: from the grid DEFAULT_ALLOW_DETACHMENT = False +#: whether or not grid2op will process flexiblity actions +DEFAULT_FLEXIBILITY_IS_AVAILABLE = False + #: in which subfolder (of the environment) the grid2op classes #: will be stored GRID2OP_CLASSES_ENV_FOLDER = "_grid2op_classes" From 02a75405b8160895e74ad81b77bc526f010ac246 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Tue, 16 Sep 2025 10:16:52 +0200 Subject: [PATCH 07/38] Add: Flexibility to BackendAction Signed-off-by: Xavier Weiss --- grid2op/Action/_backendAction.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/grid2op/Action/_backendAction.py b/grid2op/Action/_backendAction.py index 2ea206de..bf631599 100644 --- a/grid2op/Action/_backendAction.py +++ b/grid2op/Action/_backendAction.py @@ -684,6 +684,14 @@ def set_redispatch(self, new_redispatching): """ self.prod_p.change_val(new_redispatching) + def set_flexibility(self, new_flexibility): + """ + .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ + + This is called by the environment, do not alter. + """ + self.load_p.change_val(new_flexibility) + def set_storage(self, new_storage): """ .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ @@ -882,10 +890,17 @@ def __iadd__(self, other : BaseAction) -> Self: if modif_inj: self._aux_iadd_inj(other._dict_inj) - # Ib change the injection aka redispatching + # Ib change the (generator) injection aka redispatching if other._modif_redispatch: self.prod_p.change_val(redispatching) self._is_cached = False + + # Ib2 change the (load) injection aka flexibility + if cls.flexibility_is_available: + flexibility = other._private_flexibility + if other._modif_flexibility: + self.load_p.change_val(flexibility) + self._is_cached = False # Ic storage unit if other._modif_storage: From e6dea22c5ea7bf8d98b043c5d09aba0ac26f1d6d Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Tue, 16 Sep 2025 10:41:56 +0200 Subject: [PATCH 08/38] Add: Flexibility Attributes to GridObejcts Signed-off-by: Xavier Weiss --- docs/grid2op_dev/action.rst | 1 + docs/grid2op_dev/observation.rst | 16 +++ grid2op/Exceptions/__init__.py | 4 +- .../Exceptions/ambiguousActionExceptions.py | 8 ++ grid2op/Space/GridObjects.py | 120 +++++++++++++++++- 5 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 docs/grid2op_dev/observation.rst diff --git a/docs/grid2op_dev/action.rst b/docs/grid2op_dev/action.rst index 3f4eea6f..b8122e07 100644 --- a/docs/grid2op_dev/action.rst +++ b/docs/grid2op_dev/action.rst @@ -192,6 +192,7 @@ Backend "private" API Then you need to modify the `__iadd__` method of the `BackendAction` class to handle the modification you performed and pass it to the backend. +* Add a `set_YOURACTION()` if your action modifies setpoints / injections. Add tests diff --git a/docs/grid2op_dev/observation.rst b/docs/grid2op_dev/observation.rst new file mode 100644 index 00000000..0ec2eea1 --- /dev/null +++ b/docs/grid2op_dev/observation.rst @@ -0,0 +1,16 @@ +How to add a new type of observation attribute +=================================== + +Work in progress ! + +GridObjects +=================================== +* Add your attribute(s) to the end of `_clear_grid_dependant_class_attributes()`, assume they are None / False by default. +For tidiness you could define a new DEFAULT_XXXX in default_var, but this is not required. +* Add your attributes to the appropriate `_li_attr_COMPONENT`, or define a new one in the class attributes. Do the same for `_type_attr_XXXX`. +* Ensure your new class attributes are also defined in `_clear_class_attributes(cls)` +* Add your attribute(s) to `_check_convert_to_numpy_array()` method, under the appropriate data type(s) +* Add your attribute flag(s), if any, to `assert_grid_correct_cls()`. You could for instance use a flag for checking if your attributes should exist. +If the logic gets complicated define a new `_check_validity_XXXX()` method. +* Add your attribute(s) to the `__str__()` method, note that this can break some unit tests - so would recommend doing this last (after you are passing all tests). +.. include:: final.rst \ No newline at end of file diff --git a/grid2op/Exceptions/__init__.py b/grid2op/Exceptions/__init__.py index 03614f93..f4cef18d 100644 --- a/grid2op/Exceptions/__init__.py +++ b/grid2op/Exceptions/__init__.py @@ -27,6 +27,7 @@ "GeneratorTurnedOffTooSoon", "GeneratorTurnedOnTooSoon", "InvalidRedispatching", + "InvalidFlexibility", # new in 1.12.x "InvalidBusStatus", "InvalidNumberOfObjectEnds", "InvalidNumberOfLines", @@ -118,7 +119,8 @@ AmbiguousAction, NonFiniteElement, AmbiguousActionRaiseAlert, - InvalidBackendCallback) + InvalidBackendCallback, + InvalidFlexibility) from grid2op.Exceptions.observationExceptions import (BaseObservationError, NoForecastAvailable, diff --git a/grid2op/Exceptions/ambiguousActionExceptions.py b/grid2op/Exceptions/ambiguousActionExceptions.py index 5eb9def2..24c10eb9 100644 --- a/grid2op/Exceptions/ambiguousActionExceptions.py +++ b/grid2op/Exceptions/ambiguousActionExceptions.py @@ -114,6 +114,14 @@ class InvalidRedispatching(AmbiguousAction): pass +class InvalidFlexibility(AmbiguousAction): + """ + This is a more precise exception than :class:`AmbiguousAction` indicating that + the :class:`grid2op.BaseAction.BaseAction` + try to apply an invalid flexibility strategy. + """ + + pass class InvalidCurtailment(AmbiguousAction): """ diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index a2f6e3f7..1de58ce8 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -586,6 +586,23 @@ class GridObjects: float, bool, ] + + # for flexibility / demand response + _li_attr_flex_load : ClassVar[List[str]] = [ + "load_size", + "load_flexible", + "load_max_ramp_up", + "load_max_ramp_down", + "load_cost_per_MW", + ] + + _type_attr_flex_load : ClassVar[List] = [ + float, + bool, + float, + float, + float + ] # redispatch data, not available in all environment redispatching_unit_commitment_availble : ClassVar[bool] = False @@ -601,6 +618,14 @@ class GridObjects: gen_startup_cost : ClassVar[Optional[np.ndarray]] = None # start cost (in currency) gen_shutdown_cost : ClassVar[Optional[np.ndarray]] = None # shutdown cost (in currency) gen_renewable : ClassVar[Optional[np.ndarray]] = None + + # Flexible load data, not available in all Environments, new in 1.12.x + flexible_load_available: ClassVar[bool] = False + load_size: ClassVar[Optional[np.ndarray]] = None + load_flexible: ClassVar[Optional[np.ndarray]] = None + load_max_ramp_up: ClassVar[Optional[np.ndarray]] = None + load_max_ramp_down: ClassVar[Optional[np.ndarray]] = None + load_cost_per_MW: ClassVar[Optional[np.ndarray]] = None # storage unit static data storage_type : ClassVar[Optional[np.ndarray]] = None @@ -723,6 +748,15 @@ def _clear_class_attribute(cls) -> None: "gen_shutdown_cost", "gen_renewable", ] + + + cls._li_attr_flex_load = [ + "load_size", + "load_flexible", + "load_max_ramp_up", + "load_max_ramp_down", + "load_cost_per_MW", + ] cls._type_attr_disp = [ str, @@ -738,6 +772,14 @@ def _clear_class_attribute(cls) -> None: float, bool, ] + + cls._type_attr_flex_load = [ + float, + bool, + float, + float, + float + ] cls.SUB_COL = 0 cls.LOA_COL = 1 @@ -828,7 +870,7 @@ def _clear_grid_dependant_class_attributes(cls) -> None: cls.gen_startup_cost = None # start cost (in currency) cls.gen_shutdown_cost = None # shutdown cost (in currency) cls.gen_renewable = None - + # storage unit static data cls.storage_type = None cls.storage_Emax = None @@ -862,8 +904,14 @@ def _clear_grid_dependant_class_attributes(cls) -> None: cls.alertable_line_names = [] cls.alertable_line_ids = [] - # flexibility + # Flexible load data, not available in all environments cls.flexibility_is_available = DEFAULT_FLEXIBILITY_IS_AVAILABLE + cls.load_size = None + cls.load_flexible = None + cls.load_max_ramp_up = None + cls.load_max_ramp_down = None + cls.load_cost_per_MW = None + @classmethod def _update_value_set(cls) -> None: @@ -1518,7 +1566,7 @@ def _check_sub_id(cls): if len(cls.load_to_subid) != cls.n_load: raise IncorrectNumberOfLoads() if np.min(cls.load_to_subid) < 0: - raise EnvError("Some shunt is connected to a negative substation id.") + raise EnvError("Some load is connected to a negative substation id.") if np.max(cls.load_to_subid) > cls.n_sub: raise EnvError( "Some load is supposed to be connected to substations with id {} which" @@ -1980,6 +2028,12 @@ def _assign_attr(cls, attrs_list, tp, tp_nm, raise_if_none=False): @classmethod def _check_convert_to_np_array(cls, raise_if_none=True): + # convert bool to array of bools + attrs_bool = [] + if cls.flexibility_is_available: # new in 1.12.x + attrs_bool.append("load_flexible") + cls._assign_attr(attrs_bool, dt_bool, "bool", raise_if_none) + # convert int to array of ints attrs_int = ["load_pos_topo_vect", "load_to_subid", @@ -2032,6 +2086,11 @@ def _check_convert_to_np_array(cls, raise_if_none=True): "gen_cost_per_MW", "gen_startup_cost", "gen_shutdown_cost"] + if cls.flexibility_is_available: + attrs_float += ["load_size", + "load_max_ramp_up", + "load_max_ramp_down", + "load_cost_per_MW"] cls._assign_attr(attrs_float, dt_float, "float", raise_if_none) @classmethod @@ -2077,6 +2136,13 @@ def assert_grid_correct_cls(cls): raise EnvError("Grid2op cannot handle disconnection of loads / generators " "at the moment (make sure `detachment_is_allowed` " "is a bool)") + + if isinstance(cls.flexibility_is_available, (bool, dt_bool)): + cls.flexibility_is_available = dt_bool(cls.flexibility_is_available) + else: + raise EnvError("Grid2op cannot handle flexibility of loads " + "at the moment (make sure `flexibility_is_available` " + "is a bool)") if (cls.n_busbar_per_sub < 1).any(): raise EnvError(f"`n_busbar_per_sub` should be >= 1 found {cls.n_busbar_per_sub}") @@ -2354,6 +2420,10 @@ def assert_grid_correct_cls(cls): # redispatching / unit commitment if cls.redispatching_unit_commitment_availble: cls._check_validity_dispathcing_data() + + # flexibility / demand response + if cls.flexibility_is_available: + cls._check_validity_flexibility_data() # shunt data if cls.shunts_data_available: @@ -2882,6 +2952,46 @@ def _check_validity_dispathcing_data(cls): raise InvalidRedispatching( "Invalid maximum ramp for some generator (above pmax)" ) + + @classmethod + def _check_validity_flexibility_data(cls): + for attr_name in cls._li_attr_flex_load: + attr = getattr(cls, attr_name, None) + if attr is None: + raise InvalidFlexibility( + f"Impossible to recognize the ({attr_name}) of loads when " + "flexibility is supposed to be available." + ) + elif len(attr) != cls.n_load: + raise InvalidFlexibility( + f"Invalid length for the ({attr_name}) of loads when " + "flexibility is supposed to be available." + ) + elif attr.dtype is not np.dtype(bool) and (attr < 0).any(): + raise InvalidFlexibility( + f"One of the ({attr_name}) is negative" + ) + + for el, prim_type in zip(cls._li_attr_flex_load, cls._type_attr_flex_load): + type_ = {float:dt_float, bool:dt_bool, int:dt_int, str:str}[prim_type] + if not isinstance(getattr(cls, el), np.ndarray): + try: + setattr(cls, el, getattr(cls, el).astype(type_)) + except Exception as exc_: + raise InvalidFlexibility( + '{} should be convertible to a numpy array with error:\n "{}"' + "".format(el, exc_) + ) + if not np.issubdtype(getattr(cls, el).dtype, np.dtype(type_).type): + try: + setattr(cls, el, getattr(cls, el).astype(type_)) + except Exception as exc_: + raise InvalidFlexibility( + "{} should be convertible data should be convertible to " + '{} with error: \n"{}"'.format(el, type_, exc_) + ) + if (cls.load_max_ramp_up[cls.load_flexible] > cls.load_size[cls.load_flexible]).any(): + raise InvalidFlexibility("Invalid maximum ramp for some loads (above size of load)") @classmethod def attach_layout(cls, grid_layout): @@ -4373,9 +4483,9 @@ class res(GridObjects): # Demand Response / Flexibility if dict_.get("load_size", None) is None: - cls.flexible_load_available = False + cls.flexibility_is_available = False else: - cls.flexible_load_available = True + cls.flexibility_is_available = True type_attr_flex_load = [ dt_float, dt_bool, From 238703665c9ad862d1f62275558976f0e529f397 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Tue, 16 Sep 2025 10:47:17 +0200 Subject: [PATCH 09/38] Add: Missing Flexibility Attribute Processing in GridObjects Signed-off-by: Xavier Weiss --- grid2op/Space/GridObjects.py | 45 ++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 1de58ce8..721f276d 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -193,6 +193,17 @@ class GridObjects: - :attr:`GridObjects.gen_renewable` These information are loaded using the :func:`grid2op.Backend.Backend.load_redispacthing_data` method. + + Note that if you want to model an environment with flexibility capabilities, you also need + to provide the following attributes: + + - :attr:`GridObjects.load_size` + - :attr:`GridObjects.load_flexible` + - :attr:`GridObjects.load_max_ramp_up` + - :attr:`GridObjects.load_max_ramp_down` + - :attr:`GridObjects.load_cost_per_MW` + + This information is loaded using the :func:`grid2op.Backend.Backend.load_flexibility_data` method. **NB** it does not store any information about the current state of the powergrid. It stores information that cannot be modified by the BaseAgent, the Environment or any other entity. @@ -308,7 +319,7 @@ class GridObjects: to/from it. This parameter is also used to compute automatically :func:`GridObjects.dtype` and :func:`GridObjects.shape` as well as :func:`GridObjects.size`. If this class is derived, then it's really important that this vector is properly set. All the attributes with the name on this vector should have - consistently the same size and shape, otherwise, some methods will not behave as expected. [*class attribute*] + consistently the same size and shape, othercwise, some methods will not behave as expected. [*class attribute*] _vectorized: :class:`numpy.ndarray`, dtype:float The representation of the GridObject as a vector. See the help of :func:`GridObjects.to_vect` and @@ -393,6 +404,17 @@ class GridObjects: - :attr:`GridObjects.gen_startup_cost` - :attr:`GridObjects.gen_shutdown_cost` - :attr:`GridObjects.gen_renewable` + + flexibility_is_available: ``bool`` + Does the current grid allow for flexible loads. If not, any attempt to use it + will raise a :class:`grid2op.Exceptions.FlexibilityNotAvailable` error. [*class attribute*] + For an environment to be compatible with this feature, you need to set up, when loading the backend: + + - :attr:`GridObjects.load_size` + - :attr:`GridObjects.load_flexible` + - :attr:`GridObjects.load_max_ramp_up` + - :attr:`GridObjects.load_max_ramp_down` + - :attr:`GridObjects.load_cost_per_MW` grid_layout: ``dict`` or ``None`` The layout of the powergrid in a form of a dictionnary with keys the substation name, and value a tuple of @@ -620,7 +642,7 @@ class GridObjects: gen_renewable : ClassVar[Optional[np.ndarray]] = None # Flexible load data, not available in all Environments, new in 1.12.x - flexible_load_available: ClassVar[bool] = False + flexibility_is_available: ClassVar[bool] = False load_size: ClassVar[Optional[np.ndarray]] = None load_flexible: ClassVar[Optional[np.ndarray]] = None load_max_ramp_up: ClassVar[Optional[np.ndarray]] = None @@ -4106,6 +4128,22 @@ def _make_cls_dict(cls, res, as_list=True, copy_=True, _topo_vect_only=False): else: for nm_attr in cls._li_attr_disp: res[nm_attr] = None + + # Flexibility, new in 1.12.x + if cls.flexibility_is_available: + for nm_attr, type_attr in zip(cls._li_attr_flex_load, cls._type_attr_flex_load): + if nm_attr not in res and hasattr(cls, nm_attr) is False: + # Note: Need default values here for flex to work together + # correctly with redispatch + res[nm_attr] = np.zeros(shape=cls.n_load, dtype=type_attr) + if getattr(cls, nm_attr, None) is not None: + save_to_dict( + res, + cls, + nm_attr, + (lambda li, type_attr=type_attr: [type_attr(el) for el in li]) if as_list else None, + copy_, + ) # layout (position of substation on a map of the grid) if cls.grid_layout is not None: @@ -4281,6 +4319,9 @@ def _make_cls_dict_extended(cls, res: CLS_AS_DICT_TYPING, as_list=True, copy_=Tr "redispatching_unit_commitment_availble" ] = cls.redispatching_unit_commitment_availble + # flexibility / deamnd response + res["flexibility_is_available"] = cls.flexibility_is_available + # n_busbar_per_sub res["n_busbar_per_sub"] = cls.n_busbar_per_sub From f0564ff6ad793d0465addb56b4226fb4632845d6 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Tue, 16 Sep 2025 11:20:51 +0200 Subject: [PATCH 10/38] Add: Flexibility Attributes to Observation Signed-off-by: Xavier Weiss --- docs/grid2op_dev/observation.rst | 22 ++++++- grid2op/Observation/baseObservation.py | 76 +++++++++++++++++++++- grid2op/Observation/completeObservation.py | 7 ++ grid2op/Space/GridObjects.py | 8 ++- 4 files changed, 108 insertions(+), 5 deletions(-) diff --git a/docs/grid2op_dev/observation.rst b/docs/grid2op_dev/observation.rst index 0ec2eea1..cb6f961b 100644 --- a/docs/grid2op_dev/observation.rst +++ b/docs/grid2op_dev/observation.rst @@ -12,5 +12,23 @@ For tidiness you could define a new DEFAULT_XXXX in default_var, but this is not * Add your attribute(s) to `_check_convert_to_numpy_array()` method, under the appropriate data type(s) * Add your attribute flag(s), if any, to `assert_grid_correct_cls()`. You could for instance use a flag for checking if your attributes should exist. If the logic gets complicated define a new `_check_validity_XXXX()` method. -* Add your attribute(s) to the `__str__()` method, note that this can break some unit tests - so would recommend doing this last (after you are passing all tests). -.. include:: final.rst \ No newline at end of file +* Add your attribute(s) to `_make_cls_dict()` and `_make_cls_dict_extended()` methods +* Add your attribute(s) to `from_dict()` method, with appropriate defaults. +* Add your attribute(s) to the `__str__()` method, note that this can break some unit tests - so would recommend doing this last (after you are passing all tests). + + +BaseObservation +=================================== +* Add your attribute(s) to the class attribute: `attr_list_vect` +* Add empty array(s) for your attribute(s) in the `__init__()` method. +* Add your attribute(s) to the list of Literals in `state_of()` signature and add it/them to the appropriate res dictionary inside the method. +* Include your attribute(s) in the `reset()` method +* Append your attribute(s) to the appropriate `_aux_add_XXX()` method +* Add your attribute(s) to `_update_obs_complete()` method + +CompleteObservation +=================================== +* Append your attribute(s) to the class attribute: `attr_list_vect`. + +.. include:: final.rst + diff --git a/grid2op/Observation/baseObservation.py b/grid2op/Observation/baseObservation.py index 3a85942a..397d8b22 100644 --- a/grid2op/Observation/baseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -217,12 +217,27 @@ class BaseObservation(GridObjects): dispatchable. actual_dispatch: :class:`numpy.ndarray`, dtype:float + .. versionadded:: 1.12.x For **each** generators, it gives the redispatching currently implemented by the environment. Indeed, the environment tries to implement at best the :attr:`BaseObservation.target_dispatch`, but sometimes, due to physical limitation (pmin, pmax, ramp min and ramp max) it cannot. In this case, only the best possible redispatching is implemented at the current time step, and this is what this vector stores. Note that there is information about all generators there, even the one that are not dispatchable. + + target_flex: :class:`numpy.ndarray`, dtype:float + .. versionadded:: 1.12.x + For **each** load, it gives the target flexibility, asked by the agent. This is the sum of all + flexibility asked by the agent during the episode. For each load it is a number between: + 0 and `load_size`. Note that there is information about all loads here, even the one that are not + flexible. + + actual_flex: :class:`numpy.ndarray`, dtype:float + For **each** load, it gives the flexibility currently implemented by the environment. + Indeed, the environment tries to implement at best the :attr:`BaseObservation.target_flex`, but sometimes, + due to physical limitations (e.g. size) it cannot. In this case, only the best possible + flexibility is implemented at the current time step, and this is what this vector stores. Note that there is + information about all loads here, even the one that are not flexible. storage_charge: :class:`numpy.ndarray`, dtype:float The actual 'state of charge' of each storage unit, expressed in MWh. @@ -572,6 +587,9 @@ class BaseObservation(GridObjects): "storage_p_detached", # better handling of soft_overflow_threshold (>= 1.11.0) "timestep_protection_engaged", + # flexibility / demand response, new in 1.12.x + "target_flex", + "actual_flex" ] attr_list_vect = None @@ -734,6 +752,11 @@ def __init__(self, # redispatching self.target_dispatch = np.empty(shape=cls.n_gen, dtype=dt_float) self.actual_dispatch = np.empty(shape=cls.n_gen, dtype=dt_float) + + # flexibility / demand response + if cls.flexibility_is_available: + self.target_flex = np.empty(shape=cls.n_load, dtype=dt_float) + self.actual_flex = np.empty(shape=cls.n_load, dtype=dt_float) # storage unit self.storage_charge = np.empty(shape=cls.n_storage, dtype=dt_float) # in MWh @@ -902,8 +925,8 @@ def state_of( storage_id=None, substation_id=None, ) -> Dict[Literal["p", "q", "v", "theta", "bus", "sub_id", "actual_dispatch", "target_dispatch", - "maintenance", "cooldown_time", "storage_power", "storage_charge", - "storage_power_target", "storage_theta", + "actual_flex", "target_flex", "maintenance", "cooldown_time", "storage_power", + "storage_charge", "storage_power_target", "storage_theta", "topo_vect", "nb_bus", "origin", "extremity"], Union[int, float, Dict[Literal["p", "q", "v", "a", "sub_id", "bus", "theta"], Union[int, float]]] ]: @@ -956,6 +979,9 @@ def state_of( the detachment of this load (if detachement is allowed in the environment) - "q_detached" (>= 1.11.0) amount of MVAr detached from the grid cause by the detachment of this load (if detachement is allowed in the environment) + - "actual_flex" (new in 1.12.x) the actual flexibility implemented for this load + - "target_flex" (new in 1.12.x) the target flexibility (cumulation of all previously asked + flexibility by the agent) for this load - if a generator is inspected, then the keys are: @@ -1072,6 +1098,8 @@ def state_of( "p": self.load_p[load_id], "q": self.load_q[load_id], "v": self.load_v[load_id], + "target_flex": self.target_flex[load_id], + "actual_flex": self.actual_flex[load_id], "bus": self.topo_vect[self.load_pos_topo_vect[load_id]], "sub_id": cls.load_to_subid[load_id], } @@ -1363,6 +1391,26 @@ def _aux_process_grid2op_compat_1_11_0(cls): # this attribute was not there in the first place pass + @classmethod + def _aux_process_grid2op_compat_1_12_x(cls): + cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) + + for el in [ + # flexibility, new in 1.12.x + "load_size", + "load_flexible", + "load_max_ramp_up", + "load_max_ramp_down", + "load_cost_per_MW", + "target_flex", + "actual_flex", + ]: + try: + cls.attr_list_vect.remove(el) + except ValueError as exc_: + # this attribute was not there in the first place + pass + @classmethod def process_grid2op_compat(cls) -> None: super().process_grid2op_compat() @@ -1396,6 +1444,10 @@ def process_grid2op_compat(cls) -> None: # detachment has been added in grid2op 1.11 cls._aux_process_grid2op_compat_1_11_0() + if glop_ver < cls.MIN_VERSION_FLEX: + # flexibility / demand response has been added in grid2op 1.12.x + cls._aux_process_grid2op_compat_1_12_x() + cls.attr_list_set = copy.deepcopy(cls.attr_list_set) cls.attr_list_set = set(cls.attr_list_vect) @@ -1471,6 +1523,11 @@ def reset(self) -> None: # redispatching self.target_dispatch[:] = np.nan self.actual_dispatch[:] = np.nan + + # flexibility, new in 1.12.x + if type(self).flexibility_is_available: + self.target_flex[:] = np.nan + self.actual_flex[:] = np.nan # storage units self.storage_charge[:] = np.nan @@ -2941,6 +2998,14 @@ def _aux_add_loads(self, graph, cls, first_id): ("q_detached", self.load_q_detached), ] else: + nodes_prop = [] + + if type(self).flexibility_is_available: + nodes_prop.extend([ + ("target_flex", self.target_flex), + ("actual_flcex", self.actual_flex) + ]) + if nodes_prop == []: nodes_prop = None edges_prop=[ @@ -3936,6 +4001,9 @@ def to_dict(self): self._dictionnarized["loads"]["p"] = self.load_p self._dictionnarized["loads"]["q"] = self.load_q self._dictionnarized["loads"]["v"] = self.load_v + if type(self).flexibility_is_available: # new in 1.12.x + self._dictionnarized["loads"]["target_flex"] = self.target_flex + self._dictionnarized["loads"]["actual_flex"] = self.actual_flex self._dictionnarized[ "prods" ] = {} # TODO will be removed in future versions @@ -4503,6 +4571,10 @@ def _update_obs_complete(self, env: "grid2op.Environment.BaseEnv", with_forecast # redispatching self.target_dispatch[:] = env._target_dispatch self.actual_dispatch[:] = env._actual_dispatch + + if type(self).flexibility_is_available: + self.target_flex[:] = env._target_flex + self.actual_flex[:] = env._actual_flex self._thermal_limit[:] = env.get_thermal_limit() diff --git a/grid2op/Observation/completeObservation.py b/grid2op/Observation/completeObservation.py index b7acb3dd..df83a5c2 100644 --- a/grid2op/Observation/completeObservation.py +++ b/grid2op/Observation/completeObservation.py @@ -76,6 +76,10 @@ class CompleteObservation(BaseObservation): [:attr:`grid2op.Space.GridObjects.n_gen` elements] #. :attr:`BaseObservation.actual_dispatch` the actual dispatch for each generator [:attr:`grid2op.Space.GridObjects.n_gen` elements] + #. :attr:`BaseObservation.target_flex` the target flexibility for each load + [:attr:`grid2op.Space.GridObjects.n_load` elements] + #. :attr:`BaseObservation.actual_flex` the actual flexibility for each load + [:attr:`grid2op.Space.GridObjects.n_load` elements] #. :attr:`BaseObservation.storage_charge` the actual state of charge of each storage unit [:attr:`grid2op.Space.GridObjects.n_storage` elements] #. :attr:`BaseObservation.storage_power_target` the production / consumption of setpoint of each storage unit @@ -209,6 +213,9 @@ class CompleteObservation(BaseObservation): "storage_p_detached", # protections (>= 1.11.0) "timestep_protection_engaged" + # flexibility / demand response, new in 1.12.x + "target_flex", + "actual_flex", ] attr_list_json = [ "_thermal_limit", diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 721f276d..9afa0852 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -3274,6 +3274,12 @@ def process_grid2op_compat(cls): cls.detachment_is_allowed = DEFAULT_ALLOW_DETACHMENT res = True + if glop_ver < cls.MIN_VERSION_FLEX: + # Flexibility / demand response did not exist, default value + # should have no effect + cls.flexibility_is_available = DEFAULT_FLEXIBILITY_IS_AVAILABLE + res = True + if res: cls._reset_cls_dict() # forget the previous class (stored as dict) return res @@ -4524,7 +4530,7 @@ class res(GridObjects): # Demand Response / Flexibility if dict_.get("load_size", None) is None: - cls.flexibility_is_available = False + cls.flexibility_is_available = DEFAULT_FLEXIBILITY_IS_AVAILABLE else: cls.flexibility_is_available = True type_attr_flex_load = [ From facaf6a53012954dc4a32be8b6ad60b8c46c0f02 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Tue, 16 Sep 2025 17:21:32 +0200 Subject: [PATCH 11/38] Add: Flex in parameters Signed-off-by: Xavier Weiss --- grid2op/Environment/baseEnv.py | 108 ++++++++++++++++++++++++------ grid2op/Parameters.py | 3 + grid2op/tests/test_flexibility.py | 9 ++- 3 files changed, 99 insertions(+), 21 deletions(-) diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index abf0e252..6adc24a0 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -65,18 +65,21 @@ DETAILED_REDISP_ERR_MSG = ( "\nThis is an attempt to explain why the dispatch did not succeed and caused a game over.\n" "To compensate the {increase} of loads and / or {decrease} of " - "renewable energy (due to naturl causes but also through curtailment) and / or variation in the storage units, " - "the generators should {increase} their total production of {sum_move:.2f}MW (in total).\n" - "But, if you take into account the generator constraints ({pmax} and {max_ramp_up}) you " - "can have at most {avail_up_sum:.2f}MW.\n" - "Indeed at time t, generators are in state:\n\t{gen_setpoint}\ntheir ramp max is:" - "\n\t{ramp_up}\n and pmax is:\n\t{gen_pmax}\n" - "Wrapping up, each generator can {increase} at {maximum} of:\n\t{avail_up}\n" - "NB: if you did not do any dispatch during this episode, it would have been possible to " - "meet these constraints. This situation is caused by not having enough degree of freedom " - 'to "compensate" the variation of the load due to (most likely) an "over usage" of ' - "redispatching feature (some generators stuck at {pmax} as a consequence of your " - "redispatching. They can't increase their productions to meet the {increase} in demand or " + "renewable energy (due to natural causes but also through curtailment) and / or variation in the storage units, " + "the generators & flexible loads should {increase} their total production of {sum_move:.2f}MW (in total).\n" + "But, if you take into account the generator constraints ('{pmax}' and '{max_ramp_up}'), as well as the flexible\n" + "load constraints ('load_size' and '{max_ramp_up}') you can have at most {avail_up_sum:.2f}MW.\n" + "Indeed at time t, generators have setpoints of:\n\t{gen_setpoint}\ntheir ramp max is:" + "\n\t{gen_ramp_up}\nand pmax is:\n\t{gen_pmax}\n" + "The flexible loads have setpoints of:\n\t{load_setpoint}\ntheir max ramp is:" + "\n\t{load_ramp_up}\nand their size is:\n\t{load_size}\n" + "Wrapping up, each generator can {increase} at {maximum} of:\n\t{avail_gen_up}\n" + "And each flexible load can {increase} at {maximum} of:\n\t{avail_load_up}\n" + "NB: if you did not do any dispatch during this episode, it would have been possible to\n" + "meet these constraints. This situation is caused by not having enough degree of freedom\n" + 'to "compensate" the variation of the load due to (most likely) an "over usage" of\n' + "redispatching feature (some generators stuck at {pmax} as a consequence of your\n" + "redispatching. They can't increase their productions to meet the {increase} in demand or\n" "{decrease} of renewables)" ) @@ -465,6 +468,8 @@ def __init__( self._forbid_dispatch_off: bool = ( not self._parameters.ALLOW_DISPATCH_GEN_SWITCH_OFF ) + + # type of power flow to play # if True, then it will not disconnect lines above their thermal limits @@ -699,6 +704,15 @@ def __init__( # 1.12.1 self._needs_active_bus = False + # flexibility / demand response, new in 1.12.x + if type(self).flexibility_is_available: + self._forbid_flex_off:bool = (not self._parameters.ALLOW_FLEX_LOAD_SWITCH_OFF) + self._target_flex: np.ndarray = None + self._already_modified_load: np.ndarray = None + self._actual_flex: np.ndarray = None + self._load_demand_t: np.ndarray = None + self._load_demand_t_flex: np.ndarray = None + @property def highres_sim_counter(self) -> int: return self._highres_sim_counter @@ -1029,6 +1043,15 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): new_obj._called_from_reset = self._called_from_reset new_obj._needs_active_bus = self._needs_active_bus + # flexibility / demand response, new in 1.12.x + if type(self).flexibility_is_available: + new_obj._forbid_flex_off = self._forbid_flex_off + new_obj._target_flex = copy.deepcopy(self._target_flex) + new_obj._already_modified_load = copy.deepcopy(self._already_modified_load) + new_obj._actual_flex = copy.deepcopy(self._actual_flex) + new_obj._load_demand_t = copy.deepcopy(self._load_demand_t) + new_obj._load_demand_t_flex = copy.deepcopy(self._load_demand_t_flex) + def get_path_env(self): """ Get the path that allows to create this environment. @@ -1501,6 +1524,15 @@ def _has_been_initialized(self): # slack (1.11.0) self._delta_gen_p = np.zeros(bk_type.n_gen, dtype=dt_float) + # flexibility / demand response (1.12.x) + if type(self).flexibility_is_available: + self._target_flex = np.zeros(self.n_load, dtype=dt_float) + self._already_modified_load = np.zeros(self.n_load, dtype=dt_bool) + self._actual_flex = np.zeros(self.n_load, dtype=dt_float) + self._load_demand_t = np.zeros(self.n_load, dtype=dt_float) + self._load_demand_t_flex = np.zeros(self.n_load, dtype=dt_float) + self._reset_flexibility() + # previous state (complete) n_shunt = bk_type.n_shunt if bk_type.shunts_data_available else 0 self._previous_conn_state = _EnvPreviousState(bk_type, @@ -1544,6 +1576,8 @@ def _update_parameters(self): self._parameters = self.__new_param self._ignore_min_up_down_times = self._parameters.IGNORE_MIN_UP_DOWN_TIME self._forbid_dispatch_off = not self._parameters.ALLOW_DISPATCH_GEN_SWITCH_OFF + if type(self).flexibility_is_available: # flexibility / demand response, new in 1.12.x + self._forbid_flex_off = not self._parameters.ALLOW_FLEX_LOAD_SWITCH_OFF # type of power flow to play # if True, then it will not disconnect lines above their thermal limits @@ -2038,6 +2072,14 @@ def _reset_redispatching(self): self._gen_downtime[:] = 0 self._gen_activeprod_t[:] = 0.0 self._gen_activeprod_t_redisp[:] = 0.0 + + def _reset_flexibility(self): + # flexibility / demand response, new in 1.12.x + self._target_flex[:] = 0.0 + self._already_modified_load[:] = False + self._actual_flex[:] = 0.0 + self._load_demand_t[:] = 0.0 + self._load_demand_t_flex[:] = 0.0 def _feed_data_for_detachment(self, new_p_th): """feed the attribute for the detachment""" @@ -2085,8 +2127,23 @@ def _get_already_modified_gen(self, action: BaseAction): ) self._already_modified_gen[is_redisped] = True return self._already_modified_gen + + def _get_already_modified_load(self, action: BaseAction): + if not action._modif_flexibility: + # nothing changes if the action does + # not affect flexibility + return self._already_modified_load + flex_act_orig = action._flexibility + is_flexed = np.abs(flex_act_orig) > 1e-7 + self._target_flex[self._already_modified_load] += flex_act_orig[self._already_modified_load] + first_modified = (~self._already_modified_load) & is_flexed + self._target_flex[first_modified] = ( + self._actual_flex[first_modified] + flex_act_orig[first_modified] + ) + self._already_modified_load[is_flexed] = True + return self._already_modified_load - def _prepare_redisp(self, action: BaseAction, new_p, already_modified_gen): + def _prepare_redisp_and_flex(self, action: BaseAction, new_gen_p:np.ndarray, new_load_p:np.ndarray): cls = type(self) # trying with an optimization method except_ = None @@ -2099,11 +2156,22 @@ def _prepare_redisp(self, action: BaseAction, new_p, already_modified_gen): else: redisp_act_orig = None - if ( - (redisp_act_orig is not None and (np.abs(redisp_act_orig) <= 1e-7).all()) + # get the flexibility action (if any) + if action._modif_flexibility and cls.flexibility_is_available: + flex_act_orig = action._flexibility.copy() + else: + flex_act_orig = None + + disp_cond = redisp_act_orig is not None and ((np.abs(redisp_act_orig) <= 1e-7).all() and (np.abs(self._target_dispatch) <= 1e-7).all() - and (np.abs(self._actual_dispatch) <= 1e-7).all() - ): + and (np.abs(self._actual_dispatch) <= 1e-7).all()) + flex_cond = flex_act_orig is not None and ((np.abs(flex_act_orig) <= 1e-7).all() + and (np.abs(self._target_flex) <= 1e-7).all() + and (np.abs(self._actual_flex) <= 1e-7).all()) + # If no redispatching / flexibility is active, return + if disp_cond and not cls.flexibility_is_available: + return valid, except_, info_ + if disp_cond and flex_cond and cls.flexibility_is_available: return valid, except_, info_ if redisp_act_orig is None: @@ -2136,7 +2204,7 @@ def _prepare_redisp(self, action: BaseAction, new_p, already_modified_gen): return valid, except_, info_ # i can't redispatch turned off generators [turned off generators need to be turned on before redispatching] - if (redisp_act_orig[np.abs(new_p) <= 1e-7]).any() and self._forbid_dispatch_off: + if (redisp_act_orig[np.abs(new_gen_p) <= 1e-7]).any() and self._forbid_dispatch_off: # action is invalid, a generator has been redispatched, but it's turned off except_ = IllegalRedispatching( "Impossible to dispatch a turned off generator" @@ -2146,7 +2214,7 @@ def _prepare_redisp(self, action: BaseAction, new_p, already_modified_gen): if self._forbid_dispatch_off: redisp_act_orig_cut = redisp_act_orig.copy() - redisp_act_orig_cut[np.abs(new_p) <= 1e-7] = 0.0 + redisp_act_orig_cut[np.abs(new_gen_p) <= 1e-7] = 0.0 if (redisp_act_orig_cut != redisp_act_orig).any(): info_.append( { @@ -3152,7 +3220,7 @@ def _aux_apply_redisp(self, # compute the redispatching and the new productions active setpoint already_modified_gen = self._get_already_modified_gen(action) - valid_disp, except_tmp, info_ = self._prepare_redisp( + valid_disp, except_tmp, info_ = self._prepare_redisp_and_flex( action, new_p, already_modified_gen ) diff --git a/grid2op/Parameters.py b/grid2op/Parameters.py index ea908dcf..c3281517 100644 --- a/grid2op/Parameters.py +++ b/grid2op/Parameters.py @@ -250,6 +250,9 @@ def __init__(self, parameters_path=None): # allow dispatch on turned off generator (if ``True`` you can actually dispatch a turned on geenrator) self.ALLOW_DISPATCH_GEN_SWITCH_OFF = True + + # allow flexibility on turned off load (if ``True`` you can actually apply flexibility to a turned on load) + self.ALLOW_FLEX_LOAD_SWITCH_OFF = True # if a curtailment action is "too strong" it will limit it to the "maximum feasible" # not to break the whole system diff --git a/grid2op/tests/test_flexibility.py b/grid2op/tests/test_flexibility.py index 95d19e5d..3c58d0db 100644 --- a/grid2op/tests/test_flexibility.py +++ b/grid2op/tests/test_flexibility.py @@ -47,7 +47,14 @@ def setUp(self) -> None: self.flex_small_down = self.env.action_space( {"flexibility": [(el, 0.01) for el in np.nonzero(self.env.load_flexible)[0]]} ) - + + def test_create_flex_action(self): + try: + flex_act = self.env.action_space({"flexibiity":[(el, 0.01) for el in np.nonzero(self.env.load_flexible)[0]]}) + assert True + except: + assert False + def test_zero_flex(self): flex_obs, *_ = self.env.step(self.flex_all_zero) flex_mask = self.env.load_flexible From f451d08f205a6c8db68b2d92925bed54c3983bf8 Mon Sep 17 00:00:00 2001 From: DEUCE1957 <2246306W@student.gla.ac.uk> Date: Tue, 16 Sep 2025 22:04:12 +0200 Subject: [PATCH 12/38] Add: Flexibility in redispatch optimization Signed-off-by: DEUCE1957 <2246306W@student.gla.ac.uk> --- grid2op/Environment/baseEnv.py | 609 +++++++++++------- grid2op/Exceptions/__init__.py | 2 + grid2op/Exceptions/illegalActionExceptions.py | 8 + 3 files changed, 382 insertions(+), 237 deletions(-) diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 6adc24a0..ff4a1182 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -38,6 +38,7 @@ from grid2op.Exceptions import (Grid2OpException, EnvError, IllegalRedispatching, + IllegalFlexibility, ImpossibleRedispatching, GeneratorTurnedOffTooSoon, GeneratorTurnedOnTooSoon, @@ -2176,290 +2177,407 @@ def _prepare_redisp_and_flex(self, action: BaseAction, new_gen_p:np.ndarray, new if redisp_act_orig is None: redisp_act_orig = type(action)._build_attr("_redispatch") - - # check that everything is consistent with pmin, pmax: - if (self._target_dispatch > cls.gen_pmax - cls.gen_pmin).any(): - # action is invalid, the target redispatching would be above pmax for at least a generator - cond_invalid = self._target_dispatch > cls.gen_pmax - cls.gen_pmin - except_ = IllegalRedispatching( - "You cannot ask for a dispatch higher than pmax - pmin [it would be always " - "invalid because, even if the sepoint is pmin, this dispatch would set it " - "to a number higher than pmax, which is impossible]. Invalid dispatch for " - "generator(s): " - "{}".format((cond_invalid).nonzero()[0]) - ) - self._target_dispatch -= redisp_act_orig - return valid, except_, info_ - if (self._target_dispatch < cls.gen_pmin - cls.gen_pmax).any(): - # action is invalid, the target redispatching would be below pmin for at least a generator - cond_invalid = self._target_dispatch < cls.gen_pmin - cls.gen_pmax - except_ = IllegalRedispatching( - "You cannot ask for a dispatch lower than pmin - pmax [it would be always " - "invalid because, even if the sepoint is pmax, this dispatch would set it " - "to a number bellow pmin, which is impossible]. Invalid dispatch for " - "generator(s): " - "{}".format((cond_invalid).nonzero()[0]) - ) - self._target_dispatch -= redisp_act_orig - return valid, except_, info_ - - # i can't redispatch turned off generators [turned off generators need to be turned on before redispatching] - if (redisp_act_orig[np.abs(new_gen_p) <= 1e-7]).any() and self._forbid_dispatch_off: - # action is invalid, a generator has been redispatched, but it's turned off - except_ = IllegalRedispatching( - "Impossible to dispatch a turned off generator" - ) - self._target_dispatch -= redisp_act_orig - return valid, except_, info_ - if self._forbid_dispatch_off: - redisp_act_orig_cut = redisp_act_orig.copy() - redisp_act_orig_cut[np.abs(new_gen_p) <= 1e-7] = 0.0 - if (redisp_act_orig_cut != redisp_act_orig).any(): - info_.append( - { - "INFO: redispatching cut because generator will be turned_off": ( - redisp_act_orig_cut != redisp_act_orig - ).nonzero()[0] - } + if flex_act_orig is None: + flex_act_orig = type(action)._build_attr("_flexibility") + + + if self.redispatching_unit_commitment_availble: + # Check that generator redispatching is consistent with pmin, pmax: + if (self._target_dispatch > cls.gen_pmax - cls.gen_pmin).any(): + # action is invalid, the target redispatching would be above pmax for at least a generator + cond_invalid = self._target_dispatch > cls.gen_pmax - cls.gen_pmin + except_ = IllegalRedispatching( + "You cannot ask for a dispatch higher than pmax - pmin [it would be always " + "invalid because, even if the sepoint is pmin, this dispatch would set it " + "to a number higher than pmax, which is impossible]. Invalid dispatch for " + "generator(s): " + "{}".format((cond_invalid).nonzero()[0]) + ) + self._target_dispatch -= redisp_act_orig + return valid, except_, info_ + if (self._target_dispatch < cls.gen_pmin - cls.gen_pmax).any(): + # action is invalid, the target redispatching would be below pmin for at least a generator + cond_invalid = self._target_dispatch < cls.gen_pmin - cls.gen_pmax + except_ = IllegalRedispatching( + "You cannot ask for a dispatch lower than pmin - pmax [it would be always " + "invalid because, even if the sepoint is pmax, this dispatch would set it " + "to a number bellow pmin, which is impossible]. Invalid dispatch for " + "generator(s): " + "{}".format((cond_invalid).nonzero()[0]) ) - return valid, except_, info_ + self._target_dispatch -= redisp_act_orig + return valid, except_, info_ + + # Can't redispatch turned off generators [turned off generators need to be turned on before redispatching + if (redisp_act_orig[np.abs(new_gen_p) <= 1e-7]).any() and self._forbid_dispatch_off: + # action is invalid, a generator has been redispatched, but it's turned off + except_ = IllegalRedispatching( + "Impossible to dispatch a turned off generator" + ) + self._target_dispatch -= redisp_act_orig + return valid, except_, info_ + + if self._forbid_dispatch_off: + redisp_act_orig_cut = redisp_act_orig.copy() + redisp_act_orig_cut[np.abs(new_gen_p) <= 1e-7] = 0.0 + if (redisp_act_orig_cut != redisp_act_orig).any(): + info_.append( + { + "INFO: redispatching cut because generator will be turned_off": ( + redisp_act_orig_cut != redisp_act_orig + ).nonzero()[0] + } + ) + if self.flexibility_is_available: + if (self._target_flex > self.load_size).any(): + # Action is invalid, the target flexibility would be above the size of at least one flexible load + cond_invalid = self._target_flex > self.load_size + except_ = IllegalFlexibility( + "You cannot ask for flexibility greater than load_size [it would be always " + "invalid because, even if the setpoint is 0, this dispatch would set it " + "to a number higher than the size of the load, which is impossible]. Invalid flexibility for " + "loads(s): " + "{}".format((cond_invalid).nonzero()[0]) + ) + self._target_flex -= flex_act_orig + return valid, except_, info_ + + # Check that flexible load adjustments are consistent with their size (max consumption): + if (self._target_flex < -self.load_size).any(): + # Action is invalid, the target redispatching would be above pmax for at least a generator + cond_invalid = self._target_flex < -self.load_size + except_ = IllegalFlexibility( + "You cannot ask for flexibility greater than load_size [it would be always " + "invalid because, even if the setpoint is load_size, this dispatch would set it " + "to a number lower than 0, which is impossible]. Invalid flexibility for " + "loads(s): " + "{}".format((cond_invalid).nonzero()[0]) + ) + self._target_flex -= flex_act_orig + return valid, except_, info_ + + # Can't adjust turned off loads [turned off loads need to be turned on before using their flexibility] + if (flex_act_orig[np.abs(new_load_p) <= 1e-7]).any() and self._forbid_flex_off: + # Action is invalid, a flexible load has been adjusted, but it's turned off + except_ = IllegalFlexibility("Impossible to change a turned off flexible load") + self._target_flex -= flex_act_orig + return valid, except_, info_ + + if self._forbid_flex_off is True: + flex_act_orig_cut = 1.0 * flex_act_orig + flex_act_orig_cut[np.abs(new_load_p) <= 1e-7] = 0.0 + if (flex_act_orig_cut != flex_act_orig).any(): + info_.append({ + "INFO: Use of Flexibility in load cancelled because it is off": + (flex_act_orig_cut != flex_act_orig).nonzero()[0] + }) + return valid, except_, info_ - def _make_redisp(self, already_modified_gen, new_p): - """this computes the redispaching vector, taking into account the storage units""" + def _make_redisp_and_flex(self, already_modified_gen:np.ndarray, new_gen_p:np.ndarray, + already_modified_load:np.ndarray, new_load_p:np.ndarray): + """ + Compute the Redispatching vector. Taking into account: + 1. Storage Units + 2. Flexible Loads + 3. Curtailment + """ except_ = None valid = True - mismatch = self._actual_dispatch - self._target_dispatch - mismatch = np.abs(mismatch) - if ( - np.abs((self._actual_dispatch).sum()) >= self._tol_poly - or np.max(mismatch) >= self._tol_poly - or np.abs(self._amount_storage) >= self._tol_poly - or np.abs(self._sum_curtailment_mw) >= self._tol_poly - or np.abs(self._detached_elements_mw) >= self._tol_poly - ): - except_ = self._compute_dispatch_vect(already_modified_gen, new_p) + redisp_mismatch = np.abs(self._actual_dispatch - self._target_dispatch) + flex_mismatch = np.abs(self._actual_flex - self._target_flex) + + disp_cond = (np.abs((self._actual_dispatch).sum()) >= self._tol_poly or + np.max(redisp_mismatch) >= self._tol_poly or + np.abs(self._amount_storage) >= self._tol_poly or + np.abs(self._sum_curtailment_mw) >= self._tol_poly or + np.abs(self._detached_elements_mw) >= self._tol_poly) + flex_cond = self.flexible_load_available and \ + (np.abs((self._actual_flex).sum()) >= self._tol_poly or + np.max(flex_mismatch) >= self._tol_poly) + + # If either redispatch or flexibility is active, we need to compute the dispatch/flex vector + if (disp_cond or flex_cond): + except_ = self._compute_dispatch_and_flex_vect(already_modified_gen, new_gen_p, + already_modified_load, new_load_p) valid = except_ is None return valid, except_ - def _compute_dispatch_vect(self, already_modified_gen, new_p): + def _compute_dispatch_and_flex_vect(self, already_modified_gen:np.ndarray, new_gen_p:np.ndarray, + already_modified_load:np.ndarray, new_load_p:np.ndarray): except_ = None cls = type(self) this_dt_float = float # to be compliant with scipy 1.16 (removes np.float32) - # handle the case where there are storage or redispatching - # action or curtailment action on the "init state" - # of the grid + # Handle the case where there are storage, flexibility, or redispatching + # actions or curtailment actions on the "init state" of the grid if self.nb_time_step == 0: - self._gen_activeprod_t_redisp[:] = new_p + self._gen_activeprod_t_redisp[:] = new_gen_p + if cls.flexibility_is_available: + self._load_demand_t_flex[:] = new_load_p - # first i define the participating generators - # these are the generators that will be adjusted for redispatching - gen_participating = ( - (new_p > 0.0) + # First define the involved generators / flexible loads + # these are the generators / loads that will be adjust + gen_involved = ( + (new_gen_p > 0.0) | (np.abs(self._actual_dispatch) >= 1e-7) | (self._target_dispatch != self._actual_dispatch) ) - gen_participating[~cls.gen_redispatchable] = False + gen_involved[~cls.gen_redispatchable] = False if cls.detachment_is_allowed: - gen_participating[self._backend_action.get_gen_detached()] = False - incr_in_chronics = new_p - ( - self._gen_activeprod_t_redisp - self._actual_dispatch - ) + gen_involved[self._backend_action.get_gen_detached()] = False + incr_in_gen_chronics = new_gen_p - (self._gen_activeprod_t_redisp - self._actual_dispatch) - # check if the constraints are violated - ## total available "juice" to go down (incl ramp and pmin / pmax) - p_min_down = ( - cls.gen_pmin[gen_participating] - - self._gen_activeprod_t_redisp[gen_participating] - ) - avail_down = np.maximum(p_min_down, -cls.gen_max_ramp_down[gen_participating]) - ## total available "juice" to go up (incl. ramp and pmin / pmax) - p_max_up = ( - cls.gen_pmax[gen_participating] - - self._gen_activeprod_t_redisp[gen_participating] + load_involved = ( + (new_load_p > 0.0) + | (np.abs(self._actual_flex) >= 1e-7) + | (self._target_flex != self._actual_flex) ) - avail_up = np.minimum(p_max_up, cls.gen_max_ramp_up[gen_participating]) + load_involved[~self.load_flexible] = False + if cls.detachment_is_allowed: + load_involved[self._backend_action.get_load_detached()] = False + incr_in_load_chronics = new_load_p - (self._load_demand_t_flex - self._actual_flex) + + # Check if the constraints are violated + ## Total available "juice" to go down (incl ramping rates, pmin, pmax, and load size) + p_min_gen_down = (cls.gen_pmin[gen_involved] - + self._gen_activeprod_t_redisp[gen_involved]) + avail_gen_down = np.maximum(p_min_gen_down, -cls.gen_max_ramp_down[gen_involved]) + p_min_load_down = (0.0 - self._load_demand_t_flex[load_involved]) + avail_load_down = np.maximum(p_min_load_down, -cls.load_max_ramp_down[load_involved]) + + ## Total available "juice" to go up (incl ramping rates, pmin, pmax, and load size) + p_max_gen_up = (cls.gen_pmax[gen_involved] - + self._gen_activeprod_t_redisp[gen_involved]) + avail_gen_up = np.minimum(p_max_gen_up, cls.gen_max_ramp_up[gen_involved]) + p_max_load_up = (cls.load_size[load_involved] - + self._load_demand_t_flex[load_involved]) + avail_load_up = np.minimum(p_max_load_up, cls.load_max_ramp_up[load_involved]) + except_ = self._detect_infeasible_dispatch( - incr_in_chronics[gen_participating], avail_down, avail_up + incr_in_gen_chronics[gen_involved], avail_gen_down, avail_gen_up, + incr_in_load_chronics[load_involved], avail_load_down, avail_load_up ) if except_ is not None: # try to force the turn on of turned off generators (if parameters allow it) - if ( - self._parameters.IGNORE_MIN_UP_DOWN_TIME - and self._parameters.ALLOW_DISPATCH_GEN_SWITCH_OFF - ): - gen_participating_tmp = self.gen_redispatchable + if (self._parameters.IGNORE_MIN_UP_DOWN_TIME and self._parameters.ALLOW_DISPATCH_GEN_SWITCH_OFF): + gen_involved_tmp = self.gen_redispatchable if cls.detachment_is_allowed: - gen_participating_tmp[self._backend_action.get_gen_detached()] = False - p_min_down_tmp = ( - cls.gen_pmin[gen_participating_tmp] - - self._gen_activeprod_t_redisp[gen_participating_tmp] - ) - avail_down_tmp = np.maximum( - p_min_down_tmp, -cls.gen_max_ramp_down[gen_participating_tmp] - ) - p_max_up_tmp = ( - cls.gen_pmax[gen_participating_tmp] - - self._gen_activeprod_t_redisp[gen_participating_tmp] - ) - avail_up_tmp = np.minimum( - p_max_up_tmp, cls.gen_max_ramp_up[gen_participating_tmp] - ) + gen_involved_tmp[self._backend_action.get_gen_detached()] = False + p_min_gen_down_tmp = (cls.gen_pmin[gen_involved_tmp] - + self._gen_activeprod_t_redisp[gen_involved_tmp]) + avail_gen_down_tmp = np.maximum(p_min_gen_down_tmp, + -cls.gen_max_ramp_down[gen_involved_tmp]) + p_max_gen_up_tmp = (cls.gen_pmax[gen_involved_tmp] - + self._gen_activeprod_t_redisp[gen_involved_tmp]) + avail_gen_up_tmp = np.minimum(p_max_gen_up_tmp, + cls.gen_max_ramp_up[gen_involved_tmp]) + if cls.flexibility_is_available and self._parameters.ALLOW_FLEX_LOAD_SWITCH_OFF: + load_involved_tmp = self.load_flexible + if cls.detachment_is_allowed: + load_involved_tmp[self._backend_action.get_load_detached()] = False + p_min_load_down_tmp = (0.0 - self._load_demand_t_flex[load_involved_tmp]) + avail_load_down_tmp = np.maximum(p_min_load_down_tmp, + -cls.load_max_ramp_down[load_involved_tmp]) + p_max_load_up_tmp = (cls.load_size[load_involved_tmp] - + self._load_demand_t_flex[load_involved_tmp]) + avail_load_up_tmp = np.minimum(p_max_load_up_tmp, + cls.load_max_ramp_up[load_involved_tmp]) + else: + avail_load_down_tmp = np.zeros([], dtype=dt_float) + avail_load_up_tmp = np.zeros([], dtype=dt_float) except_tmp = self._detect_infeasible_dispatch( - incr_in_chronics[gen_participating_tmp], - avail_down_tmp, - avail_up_tmp, + incr_in_gen_chronics[gen_involved_tmp], avail_gen_down_tmp, avail_gen_up_tmp, + incr_in_load_chronics[load_involved_tmp], avail_load_down_tmp, avail_load_up_tmp ) if except_tmp is None: - # I can "save" the situation by turning on all generators, I do it - # TODO logger here - gen_participating = gen_participating_tmp + # If it is possible to "save" the situation by turning on all + # generators / flexible loads, do so. + gen_involved = gen_involved_tmp + load_involved = self.load_flexible except_ = None else: return except_tmp else: return except_ - # define the objective value - target_vals = ( - self._target_dispatch[gen_participating] - - self._actual_dispatch[gen_participating] - ) - - already_modified_gen_me = already_modified_gen[gen_participating] - target_vals_me = target_vals[already_modified_gen_me] - nb_dispatchable = gen_participating.sum() - tmp_zeros = np.zeros((1, nb_dispatchable), dtype=this_dt_float) - coeffs = 1.0 / ( - self.gen_max_ramp_up + self.gen_max_ramp_down + self._epsilon_poly - ) - weights = np.ones(nb_dispatchable) * coeffs[gen_participating] + # Define the objective value + target_gen_vals = (self._target_dispatch[gen_involved] - + self._actual_dispatch[gen_involved]) + modded_involved_gens = already_modified_gen[gen_involved] + target_gen_vals_mi = target_gen_vals[modded_involved_gens] + target_load_vals = (self._target_flex[load_involved] - + self._actual_flex[load_involved]) + modded_involved_loads = already_modified_load[load_involved] + target_load_vals_mi = target_load_vals[modded_involved_loads] + + nb_dispatchable = gen_involved.sum() + nb_flexible = load_involved.sum() + + tmp_zeros = np.zeros((1, nb_dispatchable + nb_flexible), dtype=this_dt_float) + gen_coeffs = 1.0 / (self.gen_max_ramp_up + + self.gen_max_ramp_down + self._epsilon_poly) + load_coeffs = 1.0 / (self.load_max_ramp_up + + self.load_max_ramp_down + self._epsilon_poly) + coeffs = np.concatenate((gen_coeffs[gen_involved], load_coeffs[load_involved])) + + involved = np.concatenate((gen_involved, load_involved)) + weights = np.ones(nb_dispatchable + nb_flexible) * coeffs[involved] weights /= weights.sum() - if target_vals_me.shape[0] == 0: - # no dispatch means all dispatchable, otherwise i will never get to 0 - already_modified_gen_me[:] = True - target_vals_me = target_vals[already_modified_gen_me] - - # for numeric stability - # to scale the input also: - # see https://stackoverflow.com/questions/11155721/positive-directional-derivative-for-linesearch - scale_x = max(np.max(np.abs(self._actual_dispatch)), 1.0) - scale_x = this_dt_float(scale_x) - target_vals_me_optim = 1.0 * (target_vals_me / scale_x) - target_vals_me_optim = target_vals_me_optim.astype(this_dt_float) - - # see https://stackoverflow.com/questions/11155721/positive-directional-derivative-for-linesearch - # where they advised to scale the function - scale_objective = max(0.5 * np.abs(target_vals_me_optim).sum() ** 2, 1.0) + if target_gen_vals_mi.shape[0] == 0 and target_load_vals_mi.shape[0] == 0: + # No dispatch and flex means all dispatchable and flexible + # Otherwise will never get to 0 + modded_involved_gens[:] = True + modded_involved_loads[:] = True + target_gen_vals_mi = target_gen_vals[modded_involved_gens] + target_load_vals_mi = target_load_vals[modded_involved_loads] + + # For numeric stability + # To also scale the input see: + # https://stackoverflow.com/questions/11155721/positive-directional-derivative-for-linesearchh + scale_gen_x = max(np.max(np.abs(self._actual_dispatch)), 1.0) + scale_load_x = max(np.max(np.abs(self._actual_flex)), 1.0) + scale_x = this_dt_float(max(scale_gen_x, scale_load_x)) + target_vals_mi = np.concatenate((target_gen_vals_mi, target_load_vals_mi)) + target_vals_mi_optim = (1.0 * (target_vals_mi / scale_x)) + target_vals_mi_optim = target_vals_mi_optim.astype(this_dt_float) + + # See https://stackoverflow.com/questions/11155721/positive-directional-derivative-for-linesearch + # where it is advised to scale the function + scale_objective = max(0.5 * np.abs(target_vals_mi_optim).sum() ** 2, 1.0) scale_objective = np.round(scale_objective, decimals=4) scale_objective = this_dt_float(scale_objective) - # add the "sum to 0" - mat_sum_0_no_turn_on = np.ones((1, nb_dispatchable), dtype=this_dt_float) - # this is where the storage is taken into account - # storages are "load convention" this means that i need to sum the amount of production to sum of storage + # Add the "sum to 0" + mat_sum_0_no_turn_on = np.ones((1, nb_dispatchable+nb_flexible), dtype=this_dt_float) + + # Take into account Energy Storage Systems (ESSs) + # Storages follow the "load convention" so we need to sum the amount of production to sum of storage # hence the "+ self._amount_storage" below # self._sum_curtailment_mw is "generator convention" hence the "-" there - const_sum_0_no_turn_on = ( - np.zeros(1, dtype=this_dt_float) - + self._amount_storage - - self._sum_curtailment_mw - + self._detached_elements_mw - ) - - # gen increase in the chronics - new_p_th = new_p[gen_participating] + self._actual_dispatch[gen_participating] - - # minimum value available for disp - ## first limit delta because of pmin - p_min_const = self.gen_pmin[gen_participating] - new_p_th - ## second limit delta because of ramps - ramp_down_const = ( - -self.gen_max_ramp_down[gen_participating] - - incr_in_chronics[gen_participating] - ) - ## take max of the 2 + const_sum_0_no_turn_on = (np.zeros(1, dtype=this_dt_float) + + self._amount_storage - + self._sum_curtailment_mw + + self._detached_elements_mw) + + # Gen increase in the chronics + new_gen_p_th = new_gen_p[gen_involved] + self._actual_dispatch[gen_involved] + + # Load increase in the chronics + new_load_p_th = new_load_p[load_involved] + self._actual_flex[load_involved] + + # Minimum value available for dispatch + ## 1st limit delta because of pmin + p_min_gen_const = self.gen_pmin[gen_involved] - new_gen_p_th + p_min_load_const = 0 - new_load_p_th + p_min_const = np.concatenate((p_min_gen_const, p_min_load_const)) + + ## 2nd limit delta because of ramping rates + ramp_gen_down_const = (-self.gen_max_ramp_down[gen_involved] - + incr_in_gen_chronics[gen_involved]) + ramp_load_down_const = (-self.load_max_ramp_down[load_involved] - + incr_in_load_chronics[load_involved]) + ramp_down_const = np.concatenate((ramp_gen_down_const, ramp_load_down_const)) + + ## Take the maximum of the 2 min_disp = np.maximum(p_min_const, ramp_down_const) min_disp = min_disp.astype(this_dt_float) - # maximum value available for disp - ## first limit delta because of pmin - p_max_const = self.gen_pmax[gen_participating] - new_p_th - ## second limit delta because of ramps - ramp_up_const = ( - self.gen_max_ramp_up[gen_participating] - - incr_in_chronics[gen_participating] - ) - ## take min of the 2 + # Maximum value available for dispatch + ## 1st limit delta because of pmax + p_max_gen_const = self.gen_pmax[gen_involved] - new_gen_p_th + p_max_load_const = self.load_size[load_involved] - new_load_p_th + p_max_const = np.concatenate((p_max_gen_const, p_max_load_const)) + + ## 2nd limit delta because of ramps + ramp_gen_up_const = (self.gen_max_ramp_up[gen_involved] - + incr_in_gen_chronics[gen_involved]) + ramp_load_up_const = (self.load_max_ramp_up[load_involved] - + incr_in_load_chronics[load_involved]) + ramp_up_const = np.concatenate((ramp_gen_up_const, ramp_load_up_const)) + + ## Take minimum of the 2 max_disp = np.minimum(p_max_const, ramp_up_const) max_disp = max_disp.astype(this_dt_float) - # add everything into a linear constraint object - # equality + # Add everything into a linear constraint (object equality) added = 0.5 * self._epsilon_poly equality_const = LinearConstraint( - mat_sum_0_no_turn_on, # do the sum - (const_sum_0_no_turn_on) / scale_x, # lower bound - (const_sum_0_no_turn_on) / scale_x, # upper bound + mat_sum_0_no_turn_on, # Sum + (const_sum_0_no_turn_on) / scale_x, # Lower Bound + (const_sum_0_no_turn_on) / scale_x, # Upper Bound ) - mat_pmin_max_ramps = np.eye(nb_dispatchable) + mat_pmin_max_ramps = np.eye(nb_dispatchable + nb_flexible) ineq_const = LinearConstraint( mat_pmin_max_ramps, (min_disp - added) / scale_x, (max_disp + added) / scale_x, ) - # choose a good initial point (close to the solution) - # the idea here is to chose a initial point that would be close to the - # desired solution (split the (sum of the) dispatch to the available generators) - x0 = np.zeros(gen_participating.sum(), dtype=this_dt_float) - if (np.abs(self._target_dispatch) >= 1e-7).any() or already_modified_gen.any(): - gen_for_x0 = np.abs(self._target_dispatch[gen_participating]) >= 1e-7 - gen_for_x0 |= already_modified_gen[gen_participating] - x0[gen_for_x0] = ( - self._target_dispatch[gen_participating][gen_for_x0] - - self._actual_dispatch[gen_participating][gen_for_x0] - ) / scale_x - # at this point x0 is made of the difference between the target and the - # actual dispatch for all generators that have a - # target dispatch non 0. - - # in this "if" block I set the other component of x0 to - # their "right" value + # Choose a good initial point x0 (close to the solution) + # Desired solution is one where the (sum of the) dispatch is split among + # the available generators / flexible loads) + x0 = np.zeros(nb_dispatchable + nb_flexible, dtype=this_dt_float) + disp_cond = ((np.abs(self._target_dispatch) >= 1e-7).any() or already_modified_gen.any()) + flex_cond = ((np.abs(self._target_flex) >= 1e-7).any() or already_modified_load.any()) + + if disp_cond or flex_cond: + # Find all dispatched generators or those which have already been modified + gen_for_x0 = np.abs(self._target_dispatch[gen_involved]) >= 1e-7 + gen_for_x0 |= already_modified_gen[gen_involved] + # Find all flexible loads or those which have already been modified + load_for_x0 = np.abs(self._target_flex[load_involved]) >= 1e-7 + load_for_x0 |= already_modified_load[load_involved] + elts_for_x0 = np.concatenate((gen_for_x0, load_for_x0)) + + # x0 will be made of the difference between the target and the + # actual dispatch/flex for all generators/flexible loads that have a + # non-zero target dispatch/flex + gen_mismatch = self._target_dispatch[gen_involved][gen_for_x0] - \ + self._actual_dispatch[gen_involved][gen_for_x0] + load_mismatch = self._target_flex[load_involved][load_for_x0] - \ + self._actual_flex[load_involved][load_for_x0] + mismatch = np.concatenate((gen_mismatch, load_mismatch)) + + # Subtitute in initial points for each controllable variable + x0[elts_for_x0] = mismatch / scale_x + + # Set the other component of x0 to their "right" value can_adjust = (np.abs(x0) <= 1e-7) if can_adjust.any(): init_sum = x0.sum() denom_adjust = (1.0 / weights[can_adjust]).sum() if denom_adjust <= 1e-2: - # i don't want to divide by something too cloose to 0. + # Don't divide by something too close to 0. denom_adjust = 1.0 x0[can_adjust] = -init_sum / (weights[can_adjust] * denom_adjust) else: - # to "force" the exact reset to 0.0 for all components - x0 -= self._actual_dispatch[gen_participating] / scale_x + # "force" the exact reset to 0.0 for all components + dispatch = np.concatenate((self._actual_dispatch[gen_involved], + self._actual_flex[load_involved])) + x0 -= dispatch / scale_x + + modded_involved = np.concatenate((modded_involved_gens, modded_involved_loads)) def target(actual_dispatchable): - # define my real objective - quad_ = ( - actual_dispatchable[already_modified_gen_me] - target_vals_me_optim - ) ** 2 - coeffs_quads = weights[already_modified_gen_me] * quad_ + # Define objective (quadratic) + quad_ = (actual_dispatchable[modded_involved_gens] - target_vals_mi_optim) ** 2 + coeffs_quads = weights[modded_involved_gens] * quad_ coeffs_quads_const = coeffs_quads.sum() coeffs_quads_const /= scale_objective # scaling the function return coeffs_quads_const def jac(actual_dispatchable): res_jac = 1.0 * tmp_zeros - res_jac[0, already_modified_gen_me] = ( + res_jac[0, modded_involved_gens] = ( 2.0 - * weights[already_modified_gen_me] - * (actual_dispatchable[already_modified_gen_me] - target_vals_me_optim) + * weights[modded_involved_gens] + * (actual_dispatchable[modded_involved_gens] - target_vals_mi_optim) ) res_jac /= scale_objective # scaling the function return res_jac.reshape(-1) - # objective function + # Objective function def f(init): this_res = minimize( target, @@ -2472,56 +2590,69 @@ def f(init): "disp": False, }, jac=jac - # hess=hess # not used for SLSQP ) return this_res res = f(x0) if res.success: - self._actual_dispatch[gen_participating] += res.x * scale_x + optim_gen_dispatch = res.x[0:nb_dispatchable] + self._actual_dispatch[gen_involved] += optim_gen_dispatch * scale_x + optim_load_flex = res.x[nb_dispatchable:] + self._actual_flex[load_involved] += optim_load_flex * scale_x else: - # check if constraints are "approximately" met + # Check if constraints are "approximately" met mat_const = np.concatenate((mat_sum_0_no_turn_on, mat_pmin_max_ramps)) - downs = np.concatenate( - (const_sum_0_no_turn_on / scale_x, (min_disp - added) / scale_x) - ) - ups = np.concatenate( - (const_sum_0_no_turn_on / scale_x, (max_disp + added) / scale_x) - ) + downs = np.concatenate((const_sum_0_no_turn_on / scale_x, + (min_disp - added) / scale_x)) + ups = np.concatenate((const_sum_0_no_turn_on / scale_x, + (max_disp + added) / scale_x)) vals = np.matmul(mat_const, res.x) - ok_down = np.all( - vals - downs >= -self._tol_poly - ) # i don't violate "down" constraints + + # Check: Don't violate "down" constraints + ok_down = np.all(vals - downs >= -self._tol_poly) + # Check: Don't violate "up" constraints ok_up = np.all(vals - ups <= self._tol_poly) if ok_up and ok_down: - # it's ok i can tolerate "small" perturbations - self._actual_dispatch[gen_participating] += res.x * scale_x + # Can tolerate "small" perturbations + optim_gen_dispatch = res.x[0:nb_dispatchable] + self._actual_dispatch[gen_involved] += optim_gen_dispatch * scale_x + optim_load_flex = res.x[nb_dispatchable:] + self._actual_flex[load_involved] += optim_load_flex * scale_x else: # TODO try with another method here, maybe error_dispatch = ( - "Redispatching automaton terminated with error (no more information available " + "Redispatching / Flexibility automaton terminated with error (no more information available " 'at this point):\n"{}"'.format(res.message) ) except_ = ImpossibleRedispatching(error_dispatch) return except_ - def _detect_infeasible_dispatch(self, incr_in_chronics, avail_down, avail_up): - """This function is an attempt to give more detailed log by detecting infeasible dispatch""" + def _detect_infeasible_dispatch(self, incr_in_gen_chronics:np.ndarray, avail_gen_down:np.ndarray, avail_gen_up:np.ndarray, + incr_in_load_chronics:np.ndarray, avail_load_down:np.ndarray, avail_load_up:np.ndarray): + """ + This function is an attempt to give more detailed logging information by detecting + infeasible dispatch / flexibility. + """ except_ = None - sum_move = ( - incr_in_chronics.sum() + self._amount_storage - self._sum_curtailment_mw + self._detached_elements_mw - ) - avail_down_sum = avail_down.sum() - avail_up_sum = avail_up.sum() + sum_move = (incr_in_gen_chronics.sum() + incr_in_load_chronics.sum() + + self._amount_storage - self._sum_curtailment_mw + self._detached_elements_mw) + avail_down_sum = avail_gen_down.sum() + avail_load_down.sum() + avail_up_sum = avail_gen_up.sum() + avail_load_up.sum() gen_setpoint = self._gen_activeprod_t_redisp[self.gen_redispatchable] + load_setpoint = self._load_demand_t_flex[self.load_flexible] + if sum_move > avail_up_sum: - # infeasible because too much is asked + # Infeasible because too much is asked msg = DETAILED_REDISP_ERR_MSG.format( sum_move=sum_move, avail_up_sum=avail_up_sum, gen_setpoint=np.round(gen_setpoint, decimals=2), - ramp_up=self.gen_max_ramp_up[self.gen_redispatchable], + load_setpoint=np.round(load_setpoint, decimals=2), + gen_ramp_up=self.gen_max_ramp_up[self.gen_redispatchable], + load_ramp_up=self.load_max_ramp_up[self.load_flexible], gen_pmax=self.gen_pmax[self.gen_redispatchable], - avail_up=np.round(avail_up, decimals=2), + load_size=self.load_size[self.load_flexible], + avail_gen_up=np.round(avail_gen_up, decimals=2), + avail_load_up=np.round(avail_load_up, decimals=2), increase="increase", decrease="decrease", maximum="maximum", @@ -2530,14 +2661,18 @@ def _detect_infeasible_dispatch(self, incr_in_chronics, avail_down, avail_up): ) except_ = ImpossibleRedispatching(msg) elif sum_move < avail_down_sum: - # infeasible because not enough is asked + # Infeasible because not enough is asked msg = DETAILED_REDISP_ERR_MSG.format( sum_move=sum_move, avail_up_sum=avail_down_sum, gen_setpoint=np.round(gen_setpoint, decimals=2), - ramp_up=self.gen_max_ramp_down[self.gen_redispatchable], + load_setpoint=np.round(load_setpoint, decimals=2), + gen_ramp_up=self.gen_max_ramp_down[self.gen_redispatchable], + load_ramp_up=self.load_max_ramp_down[self.load_flexible], gen_pmax=self.gen_pmin[self.gen_redispatchable], - avail_up=np.round(avail_up, decimals=2), + load_size=self.load_size[self.load_flexible], + avail_gen_up=np.round(avail_gen_up, decimals=2), + avail_load_up=np.round(avail_load_up, decimals=2), increase="decrease", decrease="increase", maximum="minimum", @@ -3256,7 +3391,7 @@ def _aux_apply_redisp(self, self._storage_power_prev[:] = self._storage_power # case where the action modifies load (TODO maybe make a different env for that...) self._aux_handle_act_inj(action) - valid_disp, except_tmp = self._make_redisp(already_modified_gen, new_p) + valid_disp, except_tmp = self._make_redisp_and_flex(already_modified_gen, new_p) if not valid_disp or except_tmp is not None: # game over case (divergence of the scipy routine to compute redispatching) diff --git a/grid2op/Exceptions/__init__.py b/grid2op/Exceptions/__init__.py index f4cef18d..9f458c12 100644 --- a/grid2op/Exceptions/__init__.py +++ b/grid2op/Exceptions/__init__.py @@ -15,6 +15,7 @@ "MultiEnvException", "IllegalAction", "IllegalRedispatching", + "IllegalFlexibility", # new in 1.12.x "OnProduction", "VSetpointModified", "ActiveSetPointAbovePmax", @@ -101,6 +102,7 @@ InvalidReconnection, UnitCommitorRedispachingNotAvailable, IllegalRedispatching, + IllegalFlexibility, ) from grid2op.Exceptions.ambiguousActionExceptions import (NotEnoughGenerators, diff --git a/grid2op/Exceptions/illegalActionExceptions.py b/grid2op/Exceptions/illegalActionExceptions.py index b550e7b4..ea864595 100644 --- a/grid2op/Exceptions/illegalActionExceptions.py +++ b/grid2op/Exceptions/illegalActionExceptions.py @@ -98,6 +98,14 @@ class IllegalRedispatching(IllegalAction): pass +class IllegalFlexibility(IllegalAction): + """ + This is a more precise exception than :class:`IllegalAction` indicating that + the :class:`grid2op.BaseAction.BaseAction` + try to apply an invalid flexibility / demand response strategy. + """ + + pass # attempt to use redispatching or unit commit method in an environment not set up. class UnitCommitorRedispachingNotAvailable(IllegalAction): From fee5fbe31554197366cc3fa2426cdc800fa804dd Mon Sep 17 00:00:00 2001 From: DEUCE1957 <2246306W@student.gla.ac.uk> Date: Wed, 17 Sep 2025 11:12:04 +0200 Subject: [PATCH 13/38] Add: BackendAction updates include flexibility Signed-off-by: DEUCE1957 <2246306W@student.gla.ac.uk> --- grid2op/Action/baseAction.py | 15 +- grid2op/Environment/baseEnv.py | 340 ++++++++++++++++++--------------- 2 files changed, 205 insertions(+), 150 deletions(-) diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 8ad5ac22..985fd219 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -38,7 +38,7 @@ IncorrectNumberOfElements, InvalidStorage, InvalidCurtailment, - ) + InvalidFlexibility) from grid2op.Space import GridObjects, GRID2OP_CURRENT_VERSION_STR # TODO time delay somewhere (eg action is implemented after xxx timestep, and not at the time where it's proposed) @@ -2465,6 +2465,7 @@ def _aux_iadd_modif_flags(self, other: Self): self._modif_inj = self._modif_inj or other._modif_inj self._modif_shunt = self._modif_shunt or other._modif_shunt self._modif_redispatch = self._modif_redispatch or other._modif_redispatch + self._modif_flexibility = self._modif_flexibility or other._modif_flexibility # new in 1.12.x self._modif_storage = self._modif_storage or other._modif_storage self._modif_curtailment = self._modif_curtailment or other._modif_curtailment self._modif_alarm = self._modif_alarm or other._modif_alarm @@ -3644,6 +3645,18 @@ def _check_for_ambiguity(self): - TODO + - For flexibility, Ambiguous actions can happen when: + + - A flexibility action is active, but + :attr:`grid2op.Space.GridObjects.flexibility_available` is set to ``False`` + - The length of the flexibility vector :attr:`BaseAction._flexibility` is not compatible with the number + of loads. + - Some of the asked for flexibility is above the maximum ramp up :attr:`grid2op.Space.GridObjects.load_max_ramp_up` + - some of the asked for flexibility is below the maximum ramp down :attr:`grid2op.Space.GridObjects.load_max_ramp_down` + - The flexibility action affects a non-flexible load + - The flexibility and consumption values, when added, are above load_size for at least one load + - The flexibility and consumption values, when added, are below 0 for at least one load + In case of need to overload this method, it is advise to still call this one from the base :class:`BaseAction` with ":code:`super()._check_for_ambiguity()`" or ":code:`BaseAction._check_for_ambiguity(self)`". diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index ff4a1182..9b69edab 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -2082,10 +2082,11 @@ def _reset_flexibility(self): self._load_demand_t[:] = 0.0 self._load_demand_t_flex[:] = 0.0 - def _feed_data_for_detachment(self, new_p_th): + def _feed_data_for_detachment(self, new_gen_p_th, new_load_p_th): """feed the attribute for the detachment""" - self._prev_gen_p[:] = new_p_th + self._prev_gen_p[:] = new_gen_p_th + self._prev_load_p[:] = new_load_p_th self._aux_retrieve_modif_act(self._prev_load_p, self._env_modification, "load_p") self._aux_retrieve_modif_act(self._prev_load_q, self._env_modification, "load_q") @@ -2105,13 +2106,27 @@ def _get_new_prod_setpoint(self, action): instead """ # get the modification of generator active setpoint from the action - new_p = 1.0 * self._gen_activeprod_t - self._aux_retrieve_modif_act(new_p, action, "prod_p") + new_gen_p = 1.0 * self._gen_activeprod_t + self._aux_retrieve_modif_act(new_gen_p, action, "prod_p") # modification of the environment always override the modification of the agents (if any) # TODO have a flag there if this is the case. - self._aux_retrieve_modif_act(new_p, self._env_modification, "prod_p") - return new_p + self._aux_retrieve_modif_act(new_gen_p, self._env_modification, "prod_p") + return new_gen_p + + def _get_new_load_setpoint(self, action): + """ + NB this is overidden in _ObsEnv where the data are read from the action to set this environment + instead + """ + # get the modification of generator active setpoint from the action + new_load_p = 1.0 * self._load_demand_t + self._aux_retrieve_modif_act(new_load_p, action, "load_p") + + # modification of the environment always override the modification of the agents (if any) + # TODO have a flag there if this is the case. + self._aux_retrieve_modif_act(new_load_p, self._env_modification, "load_p") + return new_load_p def _get_already_modified_gen(self, action: BaseAction): if not action._modif_redispatch: @@ -3222,80 +3237,85 @@ def _aux_readjust_storage_after_limiting(self, total_storage): self._amount_storage -= total_storage self._amount_storage_prev -= total_storage - def _aux_limit_curtail_storage_if_needed(self, new_p, new_p_th, gen_curtailed): + def _aux_limit_curtail_storage_if_needed(self, new_gen_p:np.ndarray, new_gen_p_th:np.ndarray, + new_load_p:np.ndarray, new_load_p_th:np.ndarray, + gen_curtailed:np.ndarray): gen_redisp = self.gen_redispatchable + load_flex = self.load_flexible - normal_increase = new_p - ( - self._gen_activeprod_t_redisp - self._actual_dispatch - ) - normal_increase = normal_increase[gen_redisp] - p_min_down = ( - self.gen_pmin[gen_redisp] - self._gen_activeprod_t_redisp[gen_redisp] - ) - avail_down = np.maximum(p_min_down, -self.gen_max_ramp_down[gen_redisp]) - p_max_up = self.gen_pmax[gen_redisp] - self._gen_activeprod_t_redisp[gen_redisp] - avail_up = np.minimum(p_max_up, self.gen_max_ramp_up[gen_redisp]) + # Expected increase in generation (after redispatch) + normal_gen_increase = new_gen_p - (self._gen_activeprod_t_redisp - \ + self._actual_dispatch) + normal_gen_increase = normal_gen_increase[gen_redisp] - sum_move = ( - normal_increase.sum() + self._amount_storage - self._sum_curtailment_mw - ) + # Expected increase in consumption (after flexibility) + normal_load_increase = new_load_p - (self._load_demand_t_flex - \ + self._actual_flex) + normal_load_increase = normal_load_increase[load_flex] + + # Available decrease ("down") and increase ("up") of generation from current timestep + p_min_gen_down = (self.gen_pmin[gen_redisp] - self._gen_activeprod_t_redisp[gen_redisp]) + avail_gen_down = np.maximum(p_min_gen_down, -self.gen_max_ramp_down[gen_redisp]) + + p_max_gen_up = self.gen_pmax[gen_redisp] - self._gen_activeprod_t_redisp[gen_redisp] + avail_gen_up = np.minimum(p_max_gen_up, self.gen_max_ramp_up[gen_redisp]) + + # Available decrease ("down") and increase ("up") of fleixble loads from current timestep + p_min_load_down = (0.0 - self._load_demand_t_flex[load_flex]) + avail_load_down = np.maximum(p_min_load_down, -self.load_max_ramp_down[load_flex]) + + p_max_load_up = self.load_size[load_flex] - self._load_demand_t_flex[load_flex] + avail_load_up = np.minimum(p_max_load_up, self.load_max_ramp_up[load_flex]) + + # Net change in generation (note: flexibility acts in same direction as redispatch, hence the '+') + sum_move = (normal_gen_increase.sum() + normal_load_increase.sum() + \ + self._amount_storage - self._sum_curtailment_mw) total_storage_curtail = self._amount_storage - self._sum_curtailment_mw update_env_act = False if abs(total_storage_curtail) >= self._tol_poly: - # if there is an impact on the curtailment / storage (otherwise I cannot fix anything) + # If there is an impact on the curtailment / storage (otherwise cannot fix anything) too_much = 0.0 - if sum_move > avail_up.sum(): - # I need to limit curtailment (not enough ramps up available) - too_much = dt_float(sum_move - avail_up.sum() + self._tol_poly) + if sum_move > (avail_gen_up.sum() + avail_load_up.sum()): + # Need to limit curtailment (not enough ramps up available) + too_much = dt_float(sum_move - avail_gen_up.sum() - avail_load_up.sum() + self._tol_poly) self._limited_before = too_much - elif sum_move < avail_down.sum(): - # I need to limit storage unit (not enough ramps down available) - too_much = dt_float(sum_move - avail_down.sum() - self._tol_poly) + elif sum_move < (avail_gen_down.sum() + avail_load_down.sum()): + # Need to limit storage unit (not enough ramps down available) + too_much = dt_float(sum_move - avail_gen_down.sum() - avail_load_down.sum() - self._tol_poly) self._limited_before = too_much elif np.abs(self._limited_before) >= self._tol_poly: - # adjust the "mess" I did before by not curtailing enough + # Adjust the "mess" made before by not curtailing enough # max_action = self.gen_pmax[gen_curtailed] * self._limit_curtailment[gen_curtailed] update_env_act = True - too_much = min(avail_up.sum() - self._tol_poly, self._limited_before) + too_much = min(avail_gen_up.sum() + avail_load_up.sum() - self._tol_poly, self._limited_before) self._limited_before -= too_much too_much = self._limited_before if abs(too_much) > self._tol_poly: - total_curtailment = ( - -self._sum_curtailment_mw / total_storage_curtail * too_much - ) - total_storage = ( - self._amount_storage / total_storage_curtail * too_much - ) # TODO !!! + total_curtailment = (-self._sum_curtailment_mw / total_storage_curtail * too_much) + total_storage = (self._amount_storage / total_storage_curtail * too_much) # TODO !!! update_env_act = True # TODO "log" the total_curtailment and total_storage somewhere (in the info part of the step function) if np.sign(total_curtailment) != np.sign(total_storage): - # curtailment goes up, storage down, i only "limit" the one that - # has the same sign as too much - total_curtailment = ( - too_much - if np.sign(total_curtailment) == np.sign(too_much) - else 0.0 - ) - total_storage = ( - too_much if np.sign(total_storage) == np.sign(too_much) else 0.0 - ) - # NB i can directly assign all the "curtailment" to the maximum because in this case, too_much will - # necessarily be > than total_curtail (or total_storage) because the other - # one is of opposite sign - - # fix curtailment + # Curtailment goes up, storage down, only "limit" the one that + # has the same sign as 'too_much' + total_curtailment = (too_much if np.sign(total_curtailment) == np.sign(too_much) else 0.0) + total_storage = (too_much if np.sign(total_storage) == np.sign(too_much) else 0.0) + # Note: Can directly assign all the "curtailment" to the maximum because in this case, 'too_much' will + # necessarily be > than 'total_curtail' (or 'total_storage') because the other one is of opposite sign + + # Fix Curtailment self._aux_readjust_curtailment_after_limiting( - total_curtailment, new_p_th, new_p + total_curtailment, new_gen_p_th, new_gen_p ) - # fix storage + # Fix storage self._aux_readjust_storage_after_limiting(total_storage) if update_env_act: - self._aux_update_curtail_env_act(new_p) + self._aux_update_curtail_env_act(new_gen_p) def _aux_handle_act_inj(self, action: BaseAction): for inj_key in ["load_p", "prod_p", "load_q"]: @@ -3338,25 +3358,24 @@ def _aux_handle_attack(self, action: BaseAction): self._backend_action += attack return lines_attacked, subs_attacked, attack_duration - def _aux_apply_redisp(self, - action: BaseAction, - new_p: np.ndarray, - new_p_th: np.ndarray, - gen_curtailed: np.ndarray, - except_: List[Exception], - powerline_status): + def _aux_apply_redisp_and_flex(self, action: BaseAction, new_gen_p: np.ndarray, new_gen_pth: np.ndarray, + new_load_p:np.ndarray, new_load_pth:np.ndarray, gen_curtailed: np.ndarray, + except_: List[Exception], powerline_status) -> Tuple[BaseAction, np.ndarray, np.ndarray, bool]: cls = type(self) failed_redisp = False is_done = False is_illegal_reco = False - # remember generator that were "up" before the action + # Remember generators / loads that were "up" before the action gen_up_before = np.abs(self._gen_activeprod_t) > 1e-7 + load_up_before = np.abs(self._load_demand_t) > 1e-7 - # compute the redispatching and the new productions active setpoint + # Compute the redispatching and the new productions active already_modified_gen = self._get_already_modified_gen(action) + already_modified_load = self._get_already_modified_load(action) + # Checks that Redispatch / Flexibility in action is valid valid_disp, except_tmp, info_ = self._prepare_redisp_and_flex( - action, new_p, already_modified_gen + action, new_gen_p, new_load_p, ) if except_tmp is not None: @@ -3380,21 +3399,27 @@ def _aux_apply_redisp(self, self._withdraw_storage_losses() # end storage - # fix redispatching for curtailment storage + # Fix redispatching for curtailment storage if ( self.redispatching_unit_commitment_availble and self._parameters.LIMIT_INFEASIBLE_CURTAILMENT_STORAGE_ACTION ): - # limit the curtailment / storage in case of infeasible redispatching - self._aux_limit_curtail_storage_if_needed(new_p, new_p_th, gen_curtailed) + # Limit the curtailment / storage in case of infeasible redispatching + flexibility + self._aux_limit_curtail_storage_if_needed(new_gen_p, new_gen_pth, + new_load_p, new_load_pth, gen_curtailed) self._storage_power_prev[:] = self._storage_power - # case where the action modifies load (TODO maybe make a different env for that...) + # Case where the action directly injects power at a load + # Note: Not the same as flexibility (change in load for certain price), since this can + # affect any load and can represent other actions + # TODO: maybe make a different env for that self._aux_handle_act_inj(action) - valid_disp, except_tmp = self._make_redisp_and_flex(already_modified_gen, new_p) + valid_disp, except_tmp = self._make_redisp_and_flex(already_modified_gen, new_gen_p, + already_modified_load, new_load_p) if not valid_disp or except_tmp is not None: - # game over case (divergence of the scipy routine to compute redispatching) + # GAMEOVER case + # Reason: Divergence of the Scipy (optimization) routine to compute redispatching and flexibility action.reset_cache_topological_impact() res_action = self._action_space({}) _ = res_action.get_topological_impact(powerline_status, _store_in_cache=True, _read_from_cache=False) @@ -3404,19 +3429,17 @@ def _aux_apply_redisp(self, except_.append(except_tmp) is_done = True - except_.append( - ImpossibleRedispatching( - "Game over due to infeasible redispatching state. " - 'The routine used to compute the "next state" has diverged. ' - "This means that there is no way to compute a physically valid generator state " - "(one that meets all pmin / pmax - ramp min / ramp max with the information " - "provided. As one of the physical constraints would be violated, this means that " - "a generator would be damaged in real life. This is a game over." - ) - ) + except_.append(ImpossibleRedispatching( + "Game over due to infeasible redispatching / flexibility state. " + 'The routine used to compute the "next state" has diverged. ' + "This means that there is no way to compute a physically valid generator/flexible load " + "state (one that meets all pmin / pmax - ramp min / ramp max with the information " + "provided. As one of the physical constraints would be violated, this means that " + "a generator would be damaged in real life. This is a game over." + )) return res_action, failed_redisp, is_illegal_reco, is_done - # check the validity of min downtime and max uptime + # Check the validity of min downtime and max uptime (for loads and generators) except_tmp = self._handle_updown_times(gen_up_before, self._actual_dispatch) if except_tmp is not None: is_illegal_reco = True @@ -3434,7 +3457,7 @@ def _aux_apply_redisp(self, def _aux_update_backend_action(self, action: BaseAction, action_storage_power: np.ndarray, - init_disp: np.ndarray): + init_disp: np.ndarray, init_flex:np.ndarray): """updates the backend action with the agent action""" # make sure the dispatching action is not implemented "as is" by the backend. # the environment must make sure it's a zero-sum action. @@ -3442,12 +3465,16 @@ def _aux_update_backend_action(self, res_exc_ = None # cancel the redisp and storage tags (set later in the code) tag_redisp = action._modif_redispatch + tag_flex = action._modif_flexibility tag_storage = action._modif_storage action._modif_redispatch = False + action._modif_flexibility = False action._modif_storage = False # cancel the values if tag_redisp: action._redispatch[:] = 0.0 # redispatch is added after everything in the code (even after the opponent) + if tag_flex: + action._flexibility[:] = 0.0 # flexibility, new in 1.12.x if tag_storage: action._storage_power[:] = 0.0 # storage is also added after everything # add the action @@ -3455,10 +3482,13 @@ def _aux_update_backend_action(self, # put initial value if tag_storage: action._storage_power[:] = action_storage_power + if tag_flex: + action._flexibility[:] = init_flex if tag_redisp: action._redispatch[:] = init_disp # put back the tags action._modif_redispatch = tag_redisp + action._modif_flexibility = tag_flex action._modif_storage = tag_storage return res_exc_ @@ -3504,52 +3534,48 @@ def _update_alert_properties(self, # TODO after alert budget will be implemented ! # self._is_alert_illegal - def _aux_register_env_converged(self, - disc_lines, - action: BaseAction, - init_line_status, - new_p) -> Optional[Grid2OpException]: + def _aux_register_env_converged(self, disc_lines:np.ndarray, action: BaseAction, + init_line_status:np.ndarray, new_gen_p:np.ndarray, new_load_p:np.ndarray) -> Optional[Grid2OpException]: cls = type(self) beg_res = time.perf_counter() - # update the thermal limit, for DLR for example + # Update the thermal limit, for DLR for example self.backend.update_thermal_limit(self) overflow_lines = self.backend.get_line_overflow() current_flows = self.backend.get_line_flow() - # save the current topology as "last" topology (for connected powerlines) + # Save the current topology as "last" topology (for connected powerlines) # and update the state of the disconnected powerline due to cascading failure self._backend_action.update_state(disc_lines) - # one timestep passed, i can maybe reconnect some lines + + # One timestep passed, can possibly reconnect some lines self._times_before_line_status_actionable[ self._times_before_line_status_actionable > 0 ] -= 1 - # update the vector for lines that have been disconnected + # Update the vector for lines that have been disconnected self._times_before_line_status_actionable[disc_lines >= 0] = int( self._nb_ts_reco ) self._update_time_reconnection_hazards_maintenance() - # for the powerline that are on overflow, increase this time step + # Increase the time step of lines on overflow (too much current) self._timestep_overflow[overflow_lines] += 1 - # set to 0 the number of timestep for lines that are not on overflow + # Set to 0 the no. of timesteps for lines that are not on overflow self._timestep_overflow[~overflow_lines] = 0 - # update protection counter + # Update protection counter engaged_protection = current_flows > self.backend.get_thermal_limit() * self._parameters.SOFT_OVERFLOW_THRESHOLD self._protection_counter[engaged_protection] += 1 self._protection_counter[~engaged_protection] = 0 - # build the topological action "cooldown" + # Build the topological action "cooldown" aff_lines, aff_subs = action.get_topological_impact(_read_from_cache=True) if self._max_timestep_line_status_deactivated > 0: - # i update the cooldown only when this does not impact the line disconnected for the + # Update the cooldown only when this does not impact the line disconnected for the # opponent or by maintenance for example - cond = aff_lines # powerlines i modified - # powerlines that are not affected by any other "forced disconnection" - cond &= ( - self._times_before_line_status_actionable - < self._max_timestep_line_status_deactivated - ) + cond = aff_lines # Modified powerlines + # Powerlines that are not affected by any other "forced disconnection" + cond &= (self._times_before_line_status_actionable + < self._max_timestep_line_status_deactivated) self._times_before_line_status_actionable[ cond ] = self._max_timestep_line_status_deactivated @@ -3562,13 +3588,13 @@ def _aux_register_env_converged(self, aff_subs ] = self._max_timestep_topology_deactivated - # extract production active value at this time step (should be independent of action class) + # Extract production active value at this time step (should be independent of action class) tmp_gen_p, *_ = self.backend.generators_info() if not self._parameters.STOP_EP_IF_GEN_BREAK_CONSTRAINTS: - # default behaviour, no check performed + # Default behaviour, no check performed self._gen_activeprod_t[:] = tmp_gen_p else: - # I need to check whether all generators meet the constraints + # Check whether all generators meet the constraints if tmp_gen_p[cls.gen_redispatchable] > cls.gen_pmax[cls.gen_redispatchable] + self._tol_poly: gen_ko = (tmp_gen_p[cls.gen_redispatchable] > cls.gen_pmax[cls.gen_redispatchable]).nonzero()[0] gen_ko_nms = cls.name_gen[cls.gen_redispatchable][gen_ko] @@ -3590,7 +3616,7 @@ def _aux_register_env_converged(self, self._gen_activeprod_t[:] = tmp_gen_p - # set the status of the other elements (if the backend + # Set the status of the other elements (if the backend # disconnect them) topo_ = self.backend.get_topo_vect() if cls.detachment_is_allowed: @@ -3603,25 +3629,29 @@ def _aux_register_env_converged(self, topo_[cls.gen_pos_topo_vect], topo_[cls.storage_pos_topo_vect]) - # problem with the gen_activeprod_t above, is that the slack bus absorbs alone all the losses + # A problem with the gen_activeprod_t above is that the slack bus absorbs alone all the losses # of the system. So basically, when it's too high (higher than the ramp) it can # mess up the rest of the environment - self._gen_activeprod_t_redisp[:] = new_p + self._actual_dispatch + self._gen_activeprod_t_redisp[:] = new_gen_p + self._actual_dispatch + + # Extract consumption active value at this time step (should be independent of action class) + self._load_demand_t[:], *_ = self.backend.loads_info() + self._load_demand_t_flex[:] = new_load_p + self._actual_flex - # set the line status + # Set the line status self._line_status[:] = self.backend.get_line_status() - # for detachment remember previous loads and generation + # For detachment remember previous loads and generation self._prev_load_p[:], self._prev_load_q[:], *_ = self.backend.loads_info() self._delta_gen_p[:] = self._gen_activeprod_t - self._gen_activeprod_t_redisp - self._delta_gen_p[gen_detached_user] = 0. # when the backend disconnect it it should not be set to 0. + self._delta_gen_p[gen_detached_user] = 0. # When the backend disconnect it it should not be set to 0. self._prev_gen_p[:] = self._gen_activeprod_t - - # finally, build the observation (it's a different one at each step, we cannot reuse the same one) + + # Finally, build the observation (it's a different one at each step, we cannot reuse the same one) # THIS SHOULD BE DONE AFTER EVERYTHING IS INITIALIZED ! self.current_obs = self.get_obs(_do_copy=False) - # update the previous state + # Update the previous state self._previous_conn_state.update_from_backend(self.backend) self._time_extract_obs += time.perf_counter() - beg_res @@ -3664,26 +3694,21 @@ def _aux_update_detachment_info(self): self._storage_power[self._storages_detached] = 0. - def _aux_run_pf_after_state_properly_set( - self, - action: BaseAction, - init_line_status, - new_p, - except_ : List[Exception] - ): + def _aux_run_pf_after_state_properly_set(self, action: BaseAction, init_line_status:np.ndarray, + new_gen_p:np.ndarray, new_load_p:np.ndarray, except_ : List[Exception]): has_error = True detailed_info = None try: - # compute the next _grid state + # Compute the next _grid state beg_pf = time.perf_counter() disc_lines, detailed_info, conv_ = self._backend_next_grid_state() self._disc_lines[:] = disc_lines self._time_powerflow += time.perf_counter() - beg_pf if conv_ is None: - # everything went well, so i register what is needed + # Everything went well, so register what is needed maybe_error = self._aux_register_env_converged( - disc_lines, action, init_line_status, new_p + disc_lines, action, init_line_status, new_gen_p, new_load_p, ) if maybe_error is None: has_error = False @@ -3700,12 +3725,13 @@ def _aux_run_pf_after_state_properly_set( ) return detailed_info, has_error - def _aux_apply_detachment(self, new_p, new_p_th): + def _aux_apply_detachment(self, new_gen_p:np.ndarray, new_gen_pth:np.ndarray, + new_load_p:np.ndarray, new_load_pth:np.ndarray): gen_detached_user = self._backend_action.get_gen_detached() load_detached_user = self._backend_action.get_load_detached() # handle gen - mw_gen_lost_this = new_p[gen_detached_user].sum() + mw_gen_lost_this = new_gen_p[gen_detached_user].sum() # handle loads mw_load_lost_this = self._prev_load_p[load_detached_user].sum() @@ -3713,15 +3739,20 @@ def _aux_apply_detachment(self, new_p, new_p_th): # put everything together total_power_lost = -mw_gen_lost_this + mw_load_lost_this self._detached_elements_mw = (-total_power_lost + - self._actual_dispatch[gen_detached_user].sum() - + self._actual_dispatch[gen_detached_user].sum() + + self._actual_flex[load_detached_user].sum() - self._detached_elements_mw_prev) self._detached_elements_mw_prev = -total_power_lost # and now modifies the vectors - new_p[gen_detached_user] = 0. - new_p_th[gen_detached_user] = 0. + new_gen_p[gen_detached_user] = 0. + new_gen_pth[gen_detached_user] = 0. self._actual_dispatch[gen_detached_user] = 0. - return new_p, new_p_th + if type(self).flexibility_is_available: + new_load_p[load_detached_user] = 0. + new_load_pth[load_detached_user] = 0. + self._actual_flex[load_detached_user] = 0. + return new_gen_p, new_gen_pth def _aux_step_reset_action(self): action = self._action_space({}) @@ -3854,6 +3885,10 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, init_disp = action._redispatch.copy() # dispatching action else: init_disp = type(action)._build_attr("_redispatch") + if action._modif_flexibility: + init_flex = action._flexibility.copy() # flexibility action + else: + init_flex = type(action)._build_attr("_flexibility") init_alert = None if cls.dim_alerts > 0: @@ -3945,9 +3980,11 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, self._env_modification._single_act = ( False # because it absorbs all redispatching actions ) - new_p = self._get_new_prod_setpoint(action) - new_p_th = 1.0 * new_p - self._feed_data_for_detachment(new_p_th) # should be called before _axu_apply_detachment + new_gen_p = self._get_new_prod_setpoint(action) + new_load_p = self._get_new_load_setpoint(action) + new_gen_pth = 1.0 * new_gen_p + new_load_pth = 1.0 * new_load_p + self._feed_data_for_detachment(new_gen_pth, new_load_pth) # should be called before _axu_apply_detachment # storage unit if cls.n_storage > 0: @@ -3957,20 +3994,24 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, # curtailment (does not attempt to "limit" the curtailment to make sure # it is feasible) - self._gen_before_curtailment[cls.gen_renewable] = new_p[cls.gen_renewable] - gen_curtailed = self._aux_handle_curtailment_without_limit(action, new_p) + self._gen_before_curtailment[cls.gen_renewable] = new_gen_p[cls.gen_renewable] + gen_curtailed = self._aux_handle_curtailment_without_limit(action, new_gen_p) # TODO detachment - self._aux_update_backend_action(action, action_storage_power, init_disp) - new_p, new_p_th = self._aux_apply_detachment(new_p, new_p_th) + self._aux_update_backend_action(action, action_storage_power, init_disp, init_flex) + (new_gen_p, new_gen_pth, new_load_p, + new_load_pth) = self._aux_apply_detachment(new_gen_p, new_gen_pth, + new_load_p, new_load_pth) + # Redispatch + Flexibility beg__redisp = time.perf_counter() - if (cls.redispatching_unit_commitment_availble or cls.n_storage > 0) and self._parameters.ENV_DOES_REDISPATCHING: - # this computes the "optimal" redispatching - # and it is also in this function that the limiting of the curtailment / storage actions - # is perform to make the state "feasible" - res_disp = self._aux_apply_redisp( - action, new_p, new_p_th, gen_curtailed, except_, powerline_status + if (cls.redispatching_unit_commitment_availble or cls.n_storage > 0 or cls.flexibility_is_available) and self._parameters.ENV_DOES_REDISPATCHING: + # This computes the "optimal" redispatching of generators and flexibility + # adjustment of loads + # Note: It is in this function that the limiting of the curtailment / storage actions + # is performed to make the state " + res_disp = self._aux_apply_redisp_and_flex( + action, new_gen_p, new_gen_pth, new_load_p, new_load_pth, gen_curtailed, except_, powerline_status ) action, failed_redisp, is_illegal_reco, is_done = res_disp else: @@ -3989,18 +4030,19 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, if not is_done: # TODO ? - # self._aux_update_backend_action(action, action_storage_power, init_disp) + # self._aux_update_backend_action(action, action_storage_power, init_disp, init_flex) # TODO storage: check the original action, even when replaced by do nothing is not modified self._backend_action += self._env_modification self._backend_action.set_redispatch(self._actual_dispatch) + self._backend_action.set_flexibility(self._actual_flex) self._backend_action.set_storage(self._storage_power) - # now get the new generator voltage setpoint + # Now get the new generator voltage setpoint voltage_control_act = self._voltage_control(action, prod_v_chronics) self._backend_action += voltage_control_act - # handle the opponent here + # Handle the opponent here tick = time.perf_counter() lines_attacked, subs_attacked, attack_duration = self._aux_handle_attack( action @@ -4014,7 +4056,7 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, has_error = True except_.append(exc_) is_done = True - # TODO in this case: cancel the topological action of the agent + # TODO: Cancel the topological action of the agent # and continue instead of "game over" except BackendError as exc_: has_error = True @@ -4022,12 +4064,12 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, is_done = True self._time_apply_act += time.perf_counter() - beg_ - # now it's time to run the powerflow properly - # and to update the time dependant properties + # Run the PowerFlow + # + Update the time-dependent properties if not is_done: self._update_alert_properties(action, lines_attacked, subs_attacked) detailed_info, has_error = self._aux_run_pf_after_state_properly_set( - action, init_line_status, new_p, except_ + action, init_line_status, new_gen_p, new_load_p, except_ ) else: has_error = True From d2a50e17612c7fb0eb20c55785487d1b58f15f06 Mon Sep 17 00:00:00 2001 From: DEUCE1957 <2246306W@student.gla.ac.uk> Date: Wed, 17 Sep 2025 14:36:50 +0200 Subject: [PATCH 14/38] Add: load_flexibility_data to Backend Signed-off-by: DEUCE1957 <2246306W@student.gla.ac.uk> --- getting_started/13_DemandResponse.ipynb | 155 ++++++++++++++++++ grid2op/Action/baseAction.py | 63 +++---- grid2op/Backend/backend.py | 112 ++++++++++++- grid2op/Converter/BackendConverter.py | 21 +++ grid2op/Environment/baseEnv.py | 46 +++--- grid2op/Environment/environment.py | 9 + grid2op/MakeEnv/UpdateEnv.py | 1 + grid2op/MakeEnv/_aux_var.py | 1 + grid2op/Observation/baseObservation.py | 36 +++- grid2op/Observation/completeObservation.py | 2 +- grid2op/Space/GridObjects.py | 9 +- .../chronics/0/load_p.csv.bz2 | Bin .../chronics/0/load_p_forecasted.csv.bz2 | Bin .../chronics/0/prod_p.csv.bz2 | Bin .../chronics/0/prod_p_forecasted.csv.bz2 | Bin .../chronics/0/prod_v_forecasted.csv.bz2 | Bin .../rte_case5_flexibility}/config.py | 0 .../difficulty_levels.json | 0 .../rte_case5_flexibility}/grid.json | 0 .../rte_case5_flexibility}/grid_layout.json | 0 .../rte_case5_flexibility}/params.json | 0 .../rte_case5_flexibility}/prods_charac.csv | 0 .../flex_loads_charac.csv | 4 - 23 files changed, 391 insertions(+), 68 deletions(-) create mode 100644 getting_started/13_DemandResponse.ipynb rename grid2op/{data_test/5bus_example_with_flexibility => data/rte_case5_flexibility}/chronics/0/load_p.csv.bz2 (100%) rename grid2op/{data_test/5bus_example_with_flexibility => data/rte_case5_flexibility}/chronics/0/load_p_forecasted.csv.bz2 (100%) rename grid2op/{data_test/5bus_example_with_flexibility => data/rte_case5_flexibility}/chronics/0/prod_p.csv.bz2 (100%) rename grid2op/{data_test/5bus_example_with_flexibility => data/rte_case5_flexibility}/chronics/0/prod_p_forecasted.csv.bz2 (100%) rename grid2op/{data_test/5bus_example_with_flexibility => data/rte_case5_flexibility}/chronics/0/prod_v_forecasted.csv.bz2 (100%) rename grid2op/{data_test/5bus_example_with_flexibility => data/rte_case5_flexibility}/config.py (100%) rename grid2op/{data_test/5bus_example_with_flexibility => data/rte_case5_flexibility}/difficulty_levels.json (100%) rename grid2op/{data_test/5bus_example_with_flexibility => data/rte_case5_flexibility}/grid.json (100%) rename grid2op/{data_test/5bus_example_with_flexibility => data/rte_case5_flexibility}/grid_layout.json (100%) rename grid2op/{data_test/5bus_example_with_flexibility => data/rte_case5_flexibility}/params.json (100%) rename grid2op/{data_test/5bus_example_with_flexibility => data/rte_case5_flexibility}/prods_charac.csv (100%) delete mode 100644 grid2op/data_test/5bus_example_with_flexibility/flex_loads_charac.csv diff --git a/getting_started/13_DemandResponse.ipynb b/getting_started/13_DemandResponse.ipynb new file mode 100644 index 00000000..9313d879 --- /dev/null +++ b/getting_started/13_DemandResponse.ipynb @@ -0,0 +1,155 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7a14d20a", + "metadata": {}, + "source": [ + "# Demand Response in Grid2OP.\n", + "Try me out interactively with: [![Binder](./img/badge_logo.svg)](https://mybinder.org/v2/gh/Grid2Op/grid2op/master)\n", + " \n", + "**Objectives**\n", + "\n", + "In the previous notebooks, we presented actions in a discrete action space. However, more actions are available in Grid2Op. Redispatching and curtailment are kind of continuous action that will be described here.\n", + "\n", + "This notebook will:\n", + "\n", + "- [I) Redispatching](#redispatching) details redispatching\n", + " - present what redispatching is\n", + " - show how it can be used in grid2op\n", + " - detail the redispatching actions\n", + " - show an example of a redispatching Agent\n", + "- [II) Curtailment](#curtailment) explains the curtailment\n", + " - show the behaviour of the curtailment: how to perform it, what is does etc.\n", + " - expose some limits of this curtailment: it can break the grid easily !\n", + "\n", + "\n", + "Execute the cell below by removing the # character if you use google colab !\n", + "\n", + "\n", + "\n", + "Cell will look like:\n", + "```python\n", + "!pip install grid2op[optional] # for use with google colab (grid2Op is not installed by default)\n", + "```\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f11b6b78", + "metadata": {}, + "outputs": [], + "source": [ + "# !pip install grid2op[optional] # for use with google colab (grid2Op is not installed by default)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0029e770", + "metadata": {}, + "outputs": [], + "source": [ + "import grid2op\n", + "from grid2op.Agent import DoNothingAgent, BaseAgent\n", + "from tqdm.notebook import tqdm # for easy progress bar\n", + "from pathlib import Path\n", + "display_tqdm = False # this is set to False for ease with the unitt test, feel free to set it to True\n", + "import numpy as np\n", + "max_iter = 100 # to make computation much faster we will only consider 100 time steps\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "id": "80045672", + "metadata": {}, + "source": [ + "By default, some environments do not specify the cost of generators, their maximum and minimum production values, etc. In this case it is not possible to redispatch in grid2op.\n", + "\n", + "To know more about what is needed for using redispatching, it is advised to look at this help online : https://grid2op.readthedocs.io/en/latest/space.html#grid2op.Space.GridObjects.redispatching_unit_commitment_availble for the most recent documentation. When this notebook was created, the following were needed:\n", + "\n", + "- \"load_size\" : the maximum value a load can consume\n", + "- \"load_flexible\": whether this load is capable of providing demand response \n", + "- \"load_max_ramp_up\": the maximum increase of power a generator can have between two consecutive time steps\n", + "- \"load_max_ramp_down\": the maximum decrease of power a generator can have between two consecutive time steps\n", + "- \"load_cost_per_MW\": the cost of activating 1MW of demand response. \n", + "\n", + "We made available a dedicated environment, based on the IEEE case5 powergrid that has all this features. It is advised to use this small environment for testing and getting familiar with demand response." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc448ed2", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " from lightsim2grid import LightSimBackend\n", + " bk_cls = LightSimBackend\n", + "except ImportError as exc:\n", + " print(f\"Error: {exc} when importing faster LightSimBackend\")\n", + " from grid2op.Backend import PandaPowerBackend\n", + " bk_cls = PandaPowerBackend\n", + "\n", + "env_name = \"rte_case5_flexibility\"\n", + "env = grid2op.make(env_name, test=True, backend=bk_cls())\n", + "\n", + "print(f\"Is this environment suitable for redispatching: {env.redispatching_unit_commitment_availble}\")\n", + "print(f\"Is this environment suitable for flexibility: {env.flexibility_is_available}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30b9f0f6", + "metadata": {}, + "outputs": [], + "source": [ + "env.load_size" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4fca80e5", + "metadata": {}, + "outputs": [], + "source": [ + "act = env.action_space({\"flexibility\": [(2, +1)]})\n", + "act.is_ambiguous()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37738883", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv_grid2op", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 985fd219..255388f1 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -595,7 +595,10 @@ def __init__(self, _names_chronics_to_backend: Optional[Dict[Literal["loads", "p self._private_redispatch : Optional[np.ndarray] = None # np.full(shape=cls.n_gen, fill_value=0.0, dtype=dt_float) # demand response / flexibility vector - self._private_flexibility : Optional[np.ndarray] = None + if cls.flexibility_is_available: + self._private_flexibility : Optional[np.ndarray] = None + else: + self._private_flexibility = None # storage unit vector self._private_storage_power : Optional[np.ndarray] = None # np.full( @@ -843,6 +846,8 @@ def _build_dict_attr_if_needed(cls): "_set_switch_status": None, "_change_switch_status": None, + + "_flexibility": None, # new in 1.12.x } # shunts @@ -858,7 +863,7 @@ def _build_dict_attr_if_needed(cls): # new in 1.12.x if cls.flexibility_is_available: - cls.DICT_ATTR_["_flexibility"] = np.full(shape=cls.n_load, fill_value=0.0, dtype=dt_float), + cls.DICT_ATTR_["_flexibility"] = np.full(shape=cls.n_load, dtype=dt_float, fill_value=0.0) @classmethod def _build_attr(cls, attr_nm: str): @@ -1159,16 +1164,6 @@ def as_serializable_dict(self) -> dict: ] if not res["curtail"]: del res["curtail"] - - if self.flexibility_is_available: # new in 1.12.x - if self._modif_flexibility: - res["flexibility"] = [ - (str(cls.name_Load[id_]), float(val)) - for id_, val in enumerate(self._private_flexibility) - if np.abs(val) >= 1e-7 - ] - if not res["flexibility"]: - del res["flexibility"] # more advanced options if self._modif_inj: @@ -1212,6 +1207,16 @@ def as_serializable_dict(self) -> dict: res[attr_key] = [str(xxx_name[el]) for el in vect_.nonzero()[0]] if not res[attr_key]: del res[attr_key] + + if self.flexibility_is_available: # new in 1.12.x + if self._modif_flexibility: + res["flexibility"] = [ + (str(cls.name_Load[id_]), float(val)) + for id_, val in enumerate(self._private_flexibility) + if np.abs(val) >= 1e-7 + ] + if not res["flexibility"]: + del res["flexibility"] return res @@ -4019,7 +4024,7 @@ def _is_flexibility_ambiguous(self): 'Action of type "flexibility" are not supported by this action type' ) - if (np.abs(self._private_flex[~type(self).load_flexible]) >= 1e-7).any(): + if (np.abs(self._private_flexibility[~type(self).load_flexible]) >= 1e-7).any(): raise InvalidFlexibility( "Trying to apply a flexibility action on a non redispatchable generator" ) @@ -4366,22 +4371,22 @@ def __str__(self) -> str: # flexibility, new in 1.12.x - if self.flexibility_is_available: - if self._modif_flexibility: - res.append( - "\t - Modify the loads with flexibility in the following way:" - ) - for load_idx in range(self.n_load): - if np.abs(self._private_flexibility[load_idx]) >= 1e-7: - load_name = self.name_load[load_idx] - f_amount = self._private_flexibility[load_idx] - res.append( - '\t \t - Flexibility "{}" of {:.2f} MW'.format( - load_name, f_amount - ) - ) - else: - res.append("\t - NOT perform any flexibility action") + # if self.flexibility_is_available: + # if self._modif_flexibility: + # res.append( + # "\t - Modify the loads with flexibility in the following way:" + # ) + # for load_idx in range(self.n_load): + # if np.abs(self._private_flexibility[load_idx]) >= 1e-7: + # load_name = self.name_load[load_idx] + # f_amount = self._private_flexibility[load_idx] + # res.append( + # '\t \t - Flexibility "{}" of {:.2f} MW'.format( + # load_name, f_amount + # ) + # ) + # else: + # res.append("\t - NOT perform any flexibility action") return "\n".join(res) diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index 6bb872cb..e2d2907c 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -1929,6 +1929,116 @@ def load_redispacthing_data(self, self.redispatching_unit_commitment_availble = True + def load_flexibility_data(self, + path : Union[os.PathLike, str], + name : Optional[str]="flex_loads_charac.csv") -> None: + """ + INTERNAL + + .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ + + This method will load everything needed for the environment to support + demand response / flexible loads. + + We don't recommend at all to modify this function. + + Notes + ----- + Before you use this function, make sure the names of the loads are properly set. + + For example you can either read them from the grid (setting self.name_load) or call + self._fill_names_obj() beforehand (the later is done in the environment.) + + Parameters + ---------- + path: ``str`` + Location of the dataframe containing the flexibility data. This dataframe (csv, comma separated) + should have at least the columns (other columns are ignored, order of the colums do not matter): + + - "name": identifying the name of the load (should match the names in self.name_load) + - "size": the maximum size of the load (in MW) + - "max_ramp_up": maximum value the load can increase its production between two consecutive + steps + - "max_ramp_down": maximum value the load can decrease its production between two consecutive + steps (is positive) + - "marginal_cost": "average" marginal cost of the flexible load. For now we don't allow it to vary across + different steps or episode in $/(MW.time step duration) and NOT $/MWh. This is needed to compute the + economic cost of different interventions in the grid. + + name: ``str`` + Name of the dataframe containing the flexibility data. Defaults to 'flex_load_charac.csv', we don't advise + to change it. + + """ + self.flexibility_is_available = False + + self.load_size = np.full(self.n_load, fill_value=0.0, dtype=dt_float) + self.load_flexible = np.full(self.n_load, fill_value=False, dtype=dt_bool) + self.load_max_ramp_up = np.full(self.n_load, fill_value=0.0, dtype=dt_float) + self.load_max_ramp_down = np.full(self.n_load, fill_value=0.0, dtype=dt_float) + self.load_cost_per_MW = np.full(self.n_load, fill_value=0.0, dtype=dt_float) + + # For flexibility + fullpath = os.path.join(path, name) + if not os.path.exists(fullpath): + return + try: + df = pd.read_csv(fullpath, sep=",") + except Exception as exc_: + warnings.warn( + f'Impossible to load the flexibility data for this environment with error:\n"{exc_}"\n' + f"Flexibility will be unavailable.\n" + f"Please make sure \"{name}\" file is a csv (comma ',') separated file." + ) + return + + mandatory_columns = [ + "size", + "is_flexible", + "max_ramp_up", + "max_ramp_down", + "marginal_cost", + ] + for el in mandatory_columns: + if el not in df.columns: + warnings.warn( + f"Impossible to load the flexibility data for this environment because" + f"one of the mandatory column is not present ({el}). Please check the file " + f'"{name}" contains all the mandatory columns: {mandatory_columns}' + ) + return + + load_info = {} + for _, row in df.iterrows(): + load_info[row["name"]] = { + "size": row["size"], + "flexible": row["is_flexible"], + "max_ramp_up": row["max_ramp_up"], + "max_ramp_down": row["max_ramp_down"], + "marginal_cost": row["marginal_cost"], + } + + for i, load_nm in enumerate(self.name_load): + try: + tmp_load = load_info[load_nm] + except KeyError: + raise BackendError( + f"Impossible to load the flexibility data. The load {i} with name {load_nm} " + f'could not be located on the description file "{name}".' + ) + self.load_size[i] = self._aux_check_finite_float( + tmp_load["size"], f' for load. "{load_nm}" and column "size"' + ) + self.load_flexible[i] = dt_bool(tmp_load["flexible"]) + tmp = dt_float(tmp_load["max_ramp_up"]) + if np.isfinite(tmp): + self.load_max_ramp_up[i] = tmp + tmp = dt_float(tmp_load["max_ramp_down"]) + if np.isfinite(tmp): + self.load_max_ramp_down[i] = tmp + self.load_cost_per_MW[i] = dt_float(tmp_load["marginal_cost"]) + self.flexibility_is_available = True + def load_storage_data(self, path : Union[os.PathLike, str], name: Optional[str] ="storage_units_charac.csv") -> None: @@ -1976,7 +2086,7 @@ def load_storage_data(self, state of charge of 3MWh. name: ``str`` - Name of the dataframe containing the redispatching data. Defaults to 'prods_charac.csv', we don't advise + Name of the dataframe containing the storage data. Defaults to 'storage_units_charac.csv', we don't advise to change it. Notes diff --git a/grid2op/Converter/BackendConverter.py b/grid2op/Converter/BackendConverter.py index 83c1ea8d..4d8e968f 100644 --- a/grid2op/Converter/BackendConverter.py +++ b/grid2op/Converter/BackendConverter.py @@ -165,6 +165,10 @@ def __init__( # TODO storage check all this class ! + the doc of the backend + # flexibiltiy, new in 1.12.x + self.path_flexibility = None + self.name_flexibility = None + def load_grid(self, path=None, filename=None): # register the "n_busbar_per_sub" (set for the backend class) # TODO in case source supports the "more than 2" feature but not target @@ -502,6 +506,18 @@ def assert_grid_correct(self, _local_dir_cls=None) -> None: warnings.warn(f"Impossible to load redispatching data. This is not an error but you will not be able " f"to use all grid2op functionalities. " f"The error was: \"{exc_}\"") + if self.path_flexibility is not None: + # Flexibility / demand response data is available, new in 1.12.x + try: + super().load_flexibility_data(self.path_flex, name=self.name_flex) + self.source_backend.load_flexibility_data( + self.path_flex, name=self.name_flex + ) + except BackendError as exc_: + self.flexibility_is_available = False + warnings.warn(f"Impossible to load flexibility data. This is not an error but you will not be able " + f"to use all grid2op functionalities. " + f"The error was: \"{exc_}\"") if self.path_storage_data is not None: super().load_storage_data(self.path_storage_data, self.name_storage_data) self.source_backend.load_storage_data( @@ -767,6 +783,11 @@ def load_redispacthing_data(self, path, name="prods_charac.csv"): self.path_redisp = path self.name_redisp = name + def load_flexibility_data(self, path, name="flex_loads_charac.csv"): + # data are loaded with the name of the source backend, i need to map it to the target backend too + self.path_redisp = path + self.name_redisp = name + def load_storage_data(self, path, name="storage_units_charac.csv"): # data are loaded with the name of the source backend, i need to map it to the target backend too self.path_storage_data = path diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 9b69edab..044c42ea 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -706,13 +706,13 @@ def __init__( self._needs_active_bus = False # flexibility / demand response, new in 1.12.x - if type(self).flexibility_is_available: - self._forbid_flex_off:bool = (not self._parameters.ALLOW_FLEX_LOAD_SWITCH_OFF) - self._target_flex: np.ndarray = None - self._already_modified_load: np.ndarray = None - self._actual_flex: np.ndarray = None - self._load_demand_t: np.ndarray = None - self._load_demand_t_flex: np.ndarray = None + # if type(self).flexibility_is_available: + self._forbid_flex_off:bool = (not self._parameters.ALLOW_FLEX_LOAD_SWITCH_OFF) + self._target_flex: np.ndarray = None + self._already_modified_load: np.ndarray = None + self._actual_flex: np.ndarray = None + self._load_demand_t: np.ndarray = None + self._load_demand_t_flex: np.ndarray = None @property def highres_sim_counter(self) -> int: @@ -1045,13 +1045,13 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): new_obj._needs_active_bus = self._needs_active_bus # flexibility / demand response, new in 1.12.x - if type(self).flexibility_is_available: - new_obj._forbid_flex_off = self._forbid_flex_off - new_obj._target_flex = copy.deepcopy(self._target_flex) - new_obj._already_modified_load = copy.deepcopy(self._already_modified_load) - new_obj._actual_flex = copy.deepcopy(self._actual_flex) - new_obj._load_demand_t = copy.deepcopy(self._load_demand_t) - new_obj._load_demand_t_flex = copy.deepcopy(self._load_demand_t_flex) + # if type(self).flexibility_is_available: + new_obj._forbid_flex_off = self._forbid_flex_off + new_obj._target_flex = copy.deepcopy(self._target_flex) + new_obj._already_modified_load = copy.deepcopy(self._already_modified_load) + new_obj._actual_flex = copy.deepcopy(self._actual_flex) + new_obj._load_demand_t = copy.deepcopy(self._load_demand_t) + new_obj._load_demand_t_flex = copy.deepcopy(self._load_demand_t_flex) def get_path_env(self): """ @@ -1526,13 +1526,13 @@ def _has_been_initialized(self): self._delta_gen_p = np.zeros(bk_type.n_gen, dtype=dt_float) # flexibility / demand response (1.12.x) - if type(self).flexibility_is_available: - self._target_flex = np.zeros(self.n_load, dtype=dt_float) - self._already_modified_load = np.zeros(self.n_load, dtype=dt_bool) - self._actual_flex = np.zeros(self.n_load, dtype=dt_float) - self._load_demand_t = np.zeros(self.n_load, dtype=dt_float) - self._load_demand_t_flex = np.zeros(self.n_load, dtype=dt_float) - self._reset_flexibility() + # if type(self).flexibility_is_available: + self._target_flex = np.zeros(self.n_load, dtype=dt_float) + self._already_modified_load = np.zeros(self.n_load, dtype=dt_bool) + self._actual_flex = np.zeros(self.n_load, dtype=dt_float) + self._load_demand_t = np.zeros(self.n_load, dtype=dt_float) + self._load_demand_t_flex = np.zeros(self.n_load, dtype=dt_float) + self._reset_flexibility() # previous state (complete) n_shunt = bk_type.n_shunt if bk_type.shunts_data_available else 0 @@ -2307,7 +2307,7 @@ def _make_redisp_and_flex(self, already_modified_gen:np.ndarray, new_gen_p:np.nd np.abs(self._amount_storage) >= self._tol_poly or np.abs(self._sum_curtailment_mw) >= self._tol_poly or np.abs(self._detached_elements_mw) >= self._tol_poly) - flex_cond = self.flexible_load_available and \ + flex_cond = self.flexibility_is_available and \ (np.abs((self._actual_flex).sum()) >= self._tol_poly or np.max(flex_mismatch) >= self._tol_poly) @@ -3752,7 +3752,7 @@ def _aux_apply_detachment(self, new_gen_p:np.ndarray, new_gen_pth:np.ndarray, new_load_p[load_detached_user] = 0. new_load_pth[load_detached_user] = 0. self._actual_flex[load_detached_user] = 0. - return new_gen_p, new_gen_pth + return new_gen_p, new_gen_pth, new_load_p, new_load_pth def _aux_step_reset_action(self): action = self._action_space({}) diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 44342481..8a0ee3fd 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -316,6 +316,15 @@ def _init_backend( warnings.warn(f"Impossible to load redispatching data. This is not an error but you will not be able " f"to use all grid2op functionalities. " f"The error was: \"{exc_}\"") + + # Flexibility / demand response, new in 1.12.x + try: + self.backend.load_flexibility_data(self.get_path_env()) + except BackendError as exc_: + self.backend.flexibility_is_available = False + warnings.warn(f"Impossible to load flexibility data. This is not an error but you will not be able " + f"to use all grid2op functionalities. " + f"The error was: \"{exc_}\"") exc_ = self.backend.load_grid_layout(self.get_path_env()) if exc_ is not None: warnings.warn( diff --git a/grid2op/MakeEnv/UpdateEnv.py b/grid2op/MakeEnv/UpdateEnv.py index 2b7674b8..ca6f8c4b 100644 --- a/grid2op/MakeEnv/UpdateEnv.py +++ b/grid2op/MakeEnv/UpdateEnv.py @@ -216,6 +216,7 @@ def _hash_env( "grid_layout.json", "prods_charac.csv", "storage_units_charac.csv", + "flex_loads_charac.csv", # chronix2grid files, if any "loads_charac.csv", "params.json", diff --git a/grid2op/MakeEnv/_aux_var.py b/grid2op/MakeEnv/_aux_var.py index e149dd7a..950b7160 100644 --- a/grid2op/MakeEnv/_aux_var.py +++ b/grid2op/MakeEnv/_aux_var.py @@ -22,6 +22,7 @@ "rte_case14_redisp": DEV_DATASET.format("rte_case14_redisp"), "rte_case14_test": DEV_DATASET.format("rte_case14_test"), "rte_case5_example": DEV_DATASET.format("rte_case5_example"), + "rte_case5_flexibility": DEV_DATASET.format("rte_case5_flexibility"), "rte_case118_example": DEV_DATASET.format("rte_case118_example"), "rte_case14_opponent": DEV_DATASET.format("rte_case14_opponent"), "l2rpn_wcci_2020": DEV_DATASET.format("l2rpn_wcci_2020"), diff --git a/grid2op/Observation/baseObservation.py b/grid2op/Observation/baseObservation.py index 397d8b22..aa2c16da 100644 --- a/grid2op/Observation/baseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -677,7 +677,10 @@ class BaseObservation(GridObjects): "gen_p_detached", "storage_p_detached", # soft_overflow_threshold - "timestep_protection_engaged" + "timestep_protection_engaged", + # flexibility / demand response, new in 1.12.x + "target_flex", + "actual_flex" ] def __init__(self, @@ -1523,11 +1526,6 @@ def reset(self) -> None: # redispatching self.target_dispatch[:] = np.nan self.actual_dispatch[:] = np.nan - - # flexibility, new in 1.12.x - if type(self).flexibility_is_available: - self.target_flex[:] = np.nan - self.actual_flex[:] = np.nan # storage units self.storage_charge[:] = np.nan @@ -1591,6 +1589,11 @@ def reset(self) -> None: self.load_q_detached[:] = 0. self.gen_p_detached[:] = 0. self.storage_p_detached[:] = 0. + + # flexibility, new in 1.12.x + if type(self).flexibility_is_available: + self.target_flex[:] = np.nan + self.actual_flex[:] = np.nan def set_game_over(self, env: Optional["grid2op.Environment.Environment"]=None) -> None: @@ -1742,6 +1745,11 @@ def set_game_over(self, self.load_q_detached[:] = 0. self.gen_p_detached[:] = 0. self.storage_p_detached[:] = 0. + + # flexibility, new in 1.12.x + if type(self).flexibility_is_available: + self.target_flex[:] = 0.0 + self.actual_flex[:] = 0.0 def __compare_stats(self, other: Self, name: str) -> bool: attr_me = getattr(self, name) @@ -5365,3 +5373,19 @@ def process_detachment(cls): pass cls._update_value_set() return super().process_detachment() + + @classmethod + def process_flexibility(cls): + if not cls.flexibility_is_available: + # this is really important, otherwise things from grid2op base types will be affected + cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) + # remove the detachment from the list to vector + for el in ["target_flex", + "actual_flex"]: + if el in cls.attr_list_vect: + try: + cls.attr_list_vect.remove(el) + except ValueError: + pass + cls._update_value_set() + return super().process_detachment() diff --git a/grid2op/Observation/completeObservation.py b/grid2op/Observation/completeObservation.py index df83a5c2..0d87e822 100644 --- a/grid2op/Observation/completeObservation.py +++ b/grid2op/Observation/completeObservation.py @@ -212,7 +212,7 @@ class CompleteObservation(BaseObservation): "gen_p_detached", "storage_p_detached", # protections (>= 1.11.0) - "timestep_protection_engaged" + "timestep_protection_engaged", # flexibility / demand response, new in 1.12.x "target_flex", "actual_flex", diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 9afa0852..fb2533c1 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -3058,7 +3058,7 @@ def _aux_finish_init_grid_from_file(cls): cls._compute_pos_big_topo_cls() cls.process_shunt_static_data() cls.process_detachment() - cls.process_flexiblity() + cls.process_flexibility() @classmethod def _aux_init_grid_from_cls(cls, gridobj, name_res): @@ -3197,6 +3197,7 @@ def init_grid(cls, gridobj, force=False, extra_name=None, force_module=None, _lo compat_mode = res_cls.process_grid2op_compat() res_cls.process_detachment() res_cls.process_shunt_static_data() + res_cls.process_flexibility() # new in 1.12.x res_cls._check_convert_to_np_array() # convert everything to numpy array if force_module is not None: res_cls.__module__ = force_module # hack because otherwise it says "abc" which is not the case @@ -4620,7 +4621,7 @@ class res(GridObjects): cls.process_detachment() - cls.process_flexiblity() # new in 1.12.x + cls.process_flexibility() # new in 1.12.x if "assistant_warning_type" in dict_: cls.assistant_warning_type = dict_["assistant_warning_type"] @@ -4673,7 +4674,7 @@ def process_detachment(cls): pass @classmethod - def process_flexiblity(cls): + def process_flexibility(cls): """process demand response / flexibility that is applied to loads, is overloaded for :class:`grid2op.Action.BaseAction` or :class:`grid2op.Observation.BaseObservation` """ @@ -4775,7 +4776,7 @@ def _build_cls_from_import(name_cls, path_env): my_class.process_grid2op_compat() my_class.process_detachment() my_class.process_shunt_static_data() - my_class.process_flexiblity() + my_class.process_flexibility() return my_class @staticmethod diff --git a/grid2op/data_test/5bus_example_with_flexibility/chronics/0/load_p.csv.bz2 b/grid2op/data/rte_case5_flexibility/chronics/0/load_p.csv.bz2 similarity index 100% rename from grid2op/data_test/5bus_example_with_flexibility/chronics/0/load_p.csv.bz2 rename to grid2op/data/rte_case5_flexibility/chronics/0/load_p.csv.bz2 diff --git a/grid2op/data_test/5bus_example_with_flexibility/chronics/0/load_p_forecasted.csv.bz2 b/grid2op/data/rte_case5_flexibility/chronics/0/load_p_forecasted.csv.bz2 similarity index 100% rename from grid2op/data_test/5bus_example_with_flexibility/chronics/0/load_p_forecasted.csv.bz2 rename to grid2op/data/rte_case5_flexibility/chronics/0/load_p_forecasted.csv.bz2 diff --git a/grid2op/data_test/5bus_example_with_flexibility/chronics/0/prod_p.csv.bz2 b/grid2op/data/rte_case5_flexibility/chronics/0/prod_p.csv.bz2 similarity index 100% rename from grid2op/data_test/5bus_example_with_flexibility/chronics/0/prod_p.csv.bz2 rename to grid2op/data/rte_case5_flexibility/chronics/0/prod_p.csv.bz2 diff --git a/grid2op/data_test/5bus_example_with_flexibility/chronics/0/prod_p_forecasted.csv.bz2 b/grid2op/data/rte_case5_flexibility/chronics/0/prod_p_forecasted.csv.bz2 similarity index 100% rename from grid2op/data_test/5bus_example_with_flexibility/chronics/0/prod_p_forecasted.csv.bz2 rename to grid2op/data/rte_case5_flexibility/chronics/0/prod_p_forecasted.csv.bz2 diff --git a/grid2op/data_test/5bus_example_with_flexibility/chronics/0/prod_v_forecasted.csv.bz2 b/grid2op/data/rte_case5_flexibility/chronics/0/prod_v_forecasted.csv.bz2 similarity index 100% rename from grid2op/data_test/5bus_example_with_flexibility/chronics/0/prod_v_forecasted.csv.bz2 rename to grid2op/data/rte_case5_flexibility/chronics/0/prod_v_forecasted.csv.bz2 diff --git a/grid2op/data_test/5bus_example_with_flexibility/config.py b/grid2op/data/rte_case5_flexibility/config.py similarity index 100% rename from grid2op/data_test/5bus_example_with_flexibility/config.py rename to grid2op/data/rte_case5_flexibility/config.py diff --git a/grid2op/data_test/5bus_example_with_flexibility/difficulty_levels.json b/grid2op/data/rte_case5_flexibility/difficulty_levels.json similarity index 100% rename from grid2op/data_test/5bus_example_with_flexibility/difficulty_levels.json rename to grid2op/data/rte_case5_flexibility/difficulty_levels.json diff --git a/grid2op/data_test/5bus_example_with_flexibility/grid.json b/grid2op/data/rte_case5_flexibility/grid.json similarity index 100% rename from grid2op/data_test/5bus_example_with_flexibility/grid.json rename to grid2op/data/rte_case5_flexibility/grid.json diff --git a/grid2op/data_test/5bus_example_with_flexibility/grid_layout.json b/grid2op/data/rte_case5_flexibility/grid_layout.json similarity index 100% rename from grid2op/data_test/5bus_example_with_flexibility/grid_layout.json rename to grid2op/data/rte_case5_flexibility/grid_layout.json diff --git a/grid2op/data_test/5bus_example_with_flexibility/params.json b/grid2op/data/rte_case5_flexibility/params.json similarity index 100% rename from grid2op/data_test/5bus_example_with_flexibility/params.json rename to grid2op/data/rte_case5_flexibility/params.json diff --git a/grid2op/data_test/5bus_example_with_flexibility/prods_charac.csv b/grid2op/data/rte_case5_flexibility/prods_charac.csv similarity index 100% rename from grid2op/data_test/5bus_example_with_flexibility/prods_charac.csv rename to grid2op/data/rte_case5_flexibility/prods_charac.csv diff --git a/grid2op/data_test/5bus_example_with_flexibility/flex_loads_charac.csv b/grid2op/data_test/5bus_example_with_flexibility/flex_loads_charac.csv deleted file mode 100644 index 9d9f763e..00000000 --- a/grid2op/data_test/5bus_example_with_flexibility/flex_loads_charac.csv +++ /dev/null @@ -1,4 +0,0 @@ -size,name,bus,is_flexible,max_ramp_up,max_ramp_down,min_up_time,min_down_time,marginal_cost,x,y,V -10.7,load_0_0,5,false,0,0,0,0,99999,0,0,102. -9.7,load_3_1,5,True,2.0,2.0,4,140,0,0,0,102. -10.0,load_4_2,0,True,3.0,3.0,4,4,140,0,400,102. \ No newline at end of file From 008c8f5c4d7522715c17a3fba6c6a32337dab045 Mon Sep 17 00:00:00 2001 From: DEUCE1957 <2246306W@student.gla.ac.uk> Date: Wed, 17 Sep 2025 15:43:23 +0200 Subject: [PATCH 15/38] Add: Backend Converter Support for Flexibility Signed-off-by: DEUCE1957 <2246306W@student.gla.ac.uk> --- getting_started/13_DemandResponse.ipynb | 141 ++++++++++++++++++++++-- grid2op/Action/baseAction.py | 36 +++--- grid2op/Converter/BackendConverter.py | 4 +- grid2op/Environment/baseEnv.py | 16 +-- 4 files changed, 159 insertions(+), 38 deletions(-) diff --git a/getting_started/13_DemandResponse.ipynb b/getting_started/13_DemandResponse.ipynb index 9313d879..88b7888c 100644 --- a/getting_started/13_DemandResponse.ipynb +++ b/getting_started/13_DemandResponse.ipynb @@ -36,7 +36,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "f11b6b78", "metadata": {}, "outputs": [], @@ -46,10 +46,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "0029e770", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\Users\\Deuce\\Documents\\projects\\Grid2Op\\venv_grid2op\\Lib\\site-packages\\numba\\core\\config.py:168: UserWarning: CUDA Python bindings requested (the environment variable NUMBA_CUDA_USE_NVIDIA_BINDING is set), but they are not importable: No module named 'cuda'.\n", + " warnings.warn(msg)\n" + ] + } + ], "source": [ "import grid2op\n", "from grid2op.Agent import DoNothingAgent, BaseAgent\n", @@ -81,10 +90,35 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "cc448ed2", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\Users\\Deuce\\Documents\\projects\\Grid2Op\\grid2op\\MakeEnv\\Make.py:453: UserWarning: You are using a development environment. This environment is not intended for training agents. It might not be up to date and its primary use if for tests (hence the \"test=True\" you passed as argument). Use at your own risk.\n", + " warnings.warn(_MAKE_DEV_ENV_WARN)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Is this environment suitable for redispatching: True\n", + "Is this environment suitable for flexibility: True\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\Users\\Deuce\\Documents\\projects\\Grid2Op\\venv_grid2op\\Lib\\site-packages\\lightsim2grid\\gridmodel\\from_pandapower\\_aux_add_slack.py:114: UserWarning: We found either some slack coefficient to be < 0. or they were all 0.We set them all to 1.0 to avoid such issues\n", + " warnings.warn(\"We found either some slack coefficient to be < 0. or they were all 0.\"\n" + ] + } + ], "source": [ "try:\n", " from lightsim2grid import LightSimBackend\n", @@ -103,22 +137,45 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "30b9f0f6", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "array([10.7, 9.7, 10. ], dtype=float32)" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "env.load_size" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "4fca80e5", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "(True,\n", + " Grid2OpException AmbiguousAction InvalidFlexibility InvalidFlexibility('Trying to apply a flexibility action on a non-flexible load'))" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "act = env.action_space({\"flexibility\": [(2, +1)]})\n", + "act = env.action_space({\"flexibility\": [(0, +1)]})\n", "act.is_ambiguous()" ] }, @@ -127,6 +184,70 @@ "execution_count": null, "id": "37738883", "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "This action will:\n", + "\t - NOT change anything to the injections\n", + "\t - Modify the generators with redispatching in the following way:\n", + "\t \t - Redispatch \"gen_1_1\" of 2.00 MW\n", + "\t - NOT modify any storage capacity\n", + "\t - NOT perform any curtailment\n", + "\t - NOT force any line status\n", + "\t - NOT switch any line status\n", + "\t - NOT switch anything in the topology\n", + "\t - NOT force any particular bus configuration\n", + "\t - NOT perform any flexibility action\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABPIAAALDCAYAAACfCC4HAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAOxAAADsQBlSsOGwAA/AhJREFUeJzs3Qd0FPXXxvFvGr333nvvIr03AUFF/Ss2RESlCBZQQUWxYgEBUVQEFVFAEUVAAaUKSO+9995ryr7nzrxB0AAJJJlk83zOWVN2d+YmmJ2dO/d3b4DP5/MhIiIiIiIiIiIiCVqg1wGIiIiIiIiIiIjI9SmRJyIiIiIiIiIikggokSciIiIiIiIiIpIIKJEnIiIiIiIiIiKSCCiRJyIiIiIiIiIikggokSciIiIiIiIiIpIIKJEnIiIiIiIiIiKSCCiRJyIiIiIiIiIikggokSciIiIiIiIiIpIIJPhE3vnz52nTpg3FihWjfPnyNG7cmM2bN1+6/5FHHrl0X82aNVm0aNFVtxUQEEDZsmWpUKGCc5szZ0607rvcq6++6jx2woQJl77n8/koWLAgGTJkcL7u168fHTt2vHT/3LlznefMnDnz0vc6d+5M3759b+I3IyIiIiIiIiIiSUmCT+SZTp06sWHDBlasWMHtt99+RZKsbdu2rF271rnvhRdeoF27dtfcliXoli9f7txq164d7fsuV7lyZUaMGHHp6xkzZpAlS5ZLX9evX/+KpN2ff/7JLbfc8p/vNWjQIAa/BRERERERERERScoSfCIvRYoUtGjRwqloM9WrV2f79u2X7m/dujXBwcGX7tuzZw9hYWFxGlOtWrXYsmUL+/fvd762pF6HDh0u3W9x7N27l927dztfWwLv5ZdfvpTI27dvHzt37uTWW2+N0zhFRERERERERMR/JPhE3r8NGjTIqcq72n2W9ItM7EWlYcOGzjLcnj17cubMmWjf92/t27dn1KhRHD9+3FnO27Rp00v3JUuWjBo1ajhVdxcuXGDbtm1OXJbYs6XC9n1L4lmSUkRERERERERExO8SeW+++abTH++tt976z33ffPMNY8eOZfjw4Vd9/o4dO1iyZAl//fUXhw4d4rnnnovWfVF56KGHnETe6NGjufvuuwkMvPJXGbm8duHChVSrVu1Spd78+fOd79v9IiIiIiIiIiIifpfIe++99/jxxx+ZMmUKqVKluuK+77//3hkwMW3aNLJnz37VbeTLl8/5mDp1ap588skrBlpc676o5M6dm/z58zv7tYEb/2aJOqu8s1u9evWc79WtW/fS926kP96pC6cYvHAwlT6tRI+pPdhydEuMtyEiIiIiIiIiIolTokjkffDBB4wZM8ZJ1EVOho1kVXh9+vRh+vTpl5JxUTl27Bhnz551Po+IiHCSfxUrVrzufdfy+uuv079/f4oUKfKf+6pWrcrBgwedir3LE3nfffed0yMvskovOixh9/TUp8n9QW56Te9F8SzF+WHdDxQdXJTWY1ozY+sMZ3KuiIiIiIiIiEhSdeHCBbp06ULRokUpW7as0xYtKpb7sbZqpUqVoly5ck4xlq0AjTRp0iRKlCjhbOeOO+7g5MmTUW7n4YcfdmY6LFu27NL3Tp06RZo0aahQoYLztRV/We7o8hWl9pzL5z80a9aML774wj8SedZX7plnnnF60dkv1n4RNgE20v333+/0nbO+eXaf3Y4cOeLc98knnzhDJsz69eudpa3WA8/+Me0xAwcOvO5911KlShVnom5UQkJCnKEY9g9o//imWLFiztf2fbv/WiwxN33rdCdRZwm7H9f9SJ86fdjdczdj7hzD1u5bGdtuLMfOH6PR140oO6wsw5cM52yom5AUEREREREREUlKevfu7STJNm7cyKpVq5zVnVH5+eefmTdvHitWrGDlypXOzIQXX3zRue/06dM8+uij/PTTT2zatIlcuXI5hVxXU7lyZWcIaiQrDitZsuR/Wq9FslWalteK/J4NbJ07d260V24G+FTKlaBYIu6bld/w0cKPWHNoDbXz1abbLd1oU6INwYFRD/FYum+p8/gxq8eQOiQ1j1V6jKeqPUW+9FevUBQRERERERER8RdnzpwhZ86cTkFYunTprvnYiRMn8sorrzht1ax6rlevXk5CzVaEjhs3zqmOmzp1qvPYtWvX0qRJE2e7UVXkWdHWZ5995hSJJU+enJo1azqVgJ9++inLly9n165dToGXrQa14ajFixfn3XffZcKECYwcOdKZpfC///3vigq9RF2Rl1TsPLGTXtN6keeDPHSd0pUquaqwpNMSZj8ym7tK3XXVJJ6plLMSI9uMZOfTO+lRvQdfrfyKQoMK0W5cO+bsmKNltyIiIiIiIiLi17Zs2UKmTJmcQam2grJ27drMmDEjyse2atXKaYOWI0cOJ/lnj3vttdec+3bu3OnMRIhUoEABp0WaJfqiYnMcGjdu7FTwWTLPcjCXV+TlzZvX2YcNQ7VtW4yWGLRhqyamcxSUyPOQ/eNaou2usXdRcFBBJwFniThLyFlizhJ0MZE9TXb61u3Ljqd3MKrNKHYc30GdkXWoPLwyo5aP4nzY+Tj7WUREREREREREou38Ydj+HYTHTq4iLCyMHTt2OH3vFi9ezEcffcQ999zDgQMH/vNYu3/16tXs2bOHvXv3OktrO3fufMP77tChg1PFZ7erDUS1pbR2s/kJKVOmJFu2bGzbts35nt0fXUrkecASaiOXj3QSbJZos2q8r9p85STgLBFnCbmbkSwoGfeXu5+/H/ub+Y/Od4ZjdPylI/k+zMfLf77MvlP7Yu1nERERERERERGJkQtH4Y9G8Nf/YM5dEH4xxpv46quvLs1K+PLLL50BqIGBgc4sBWNDTAsWLOj0yovquVYFZwNV7TkPPfSQUxlnbDuWEIxkS16toi44+OorJW3ugiUEbcDpvffe+5/7LVFn27fb5QNRf//9d6dXnyryErAvln7hJNQe++UxJ8FmiTZLuFnizRJw12OloVmyZOHVV1+N1v6q56nuDMfY3n07nSp3YtjiYeQbmI8HJzxIhC8iFn4iEREREREREZFoungC/mwKF49Cze/g4CyYdy9EhMZoMw8++KDTg85uVgVnuRKrrPvtt9+c+63azW6XL3ONVKhQIf744w8uXrx4aUptmTJlLk2QXbp0qbNM1nz88cdRJuf+bdCgQc5wjbRp00aZyFuwYAGzZs1yBqBGJvLef/99cufO7dyi6+rpRIkTny75lEwpM7Hs8WXkThf9f6hI3377rbN2O7pNECPZvvo36M9LtV+i25RufL7sc95s+CZ50uWJcQwiIiIiIiIiIjEWegpmNodze6DRbEhbBFLmhD+bw18PQI3REBh0w5v/5JNPnImzNrzCKu1s4ERkkqxjx460bt3auT311FOsW7eO8uXLExIS4vTKs+caS8R9/vnntGnTxlmuawm+UaNGXXfflkS8Gqvos0o/qwC04RqmRo0aTqLR4o0JTa2NZ9+v/p57f7iXpZ2WUjFnxRvahk01sURedKvyLhcaHkqpj0s5wzSsUk9EREREREREJM6FnXWTeCfXQ8NZkL7EP/ftnwEzb4P890D1LyFAC0ivRr+ZeNaudDuq5qrKc9Oe82Sa7PAlw50hGG80eCPe9y0iIiIiIiIiSZANtJh9O5xYAw2mX5nEMzkaQp0JsOM7+LszqBXYVSmRF88CAwJ5t/G7zNg2g9+3/B6v+z554ST9ZvXjqapPUShjoXjdt4iIiIiIiIgkQeEXYM6dcGQx1P8dMpSN+nG5mkOtsbD1S1jSHbSANEpK5HmgXoF6tCzWkl7TexEeER5v+33vr/e4GH6RPnX6xNs+RURERERERCSJsgEW8+6Bg3Og/lTIVOnaj89zO9T8FjZ9DMueUzIvCkrkeeTthm+z6uAqRq8aHaPn2UjkAQMGOKOSGzVqFO3n7T21l/fnv88LtV4gc6rMNxCxiIiIiIiIiEg0RYTBX/fD/ulQbzJkueXSXbVr13amzEbZ+z9fO6j+Faz/AFb2jd+YEwENu/DQYz8/xm9bfmNDlw2kDEkZp/vq9EsnpmyewsYuG+N8XyIiIiIiIiKShNnqwwUPw67xbhIve/0r7t61axczZsy49iDPLSNg4aNQ7nUoo5WFkVSR56F+9ftx+OxhBv89OE73s/bQWr5Y9gWv139dSTwRERERERERiTs2qGLR47BzLNT+6T9JPJM3b97rb6dwB6j6sVuVt3ZA3MSaCCmR56FcaXPR89aevDnnTY6cPRJn++k9vTels5bmgXIPxNk+RERERERERCSJs0Wfi7vC1lFQazzkanpz2yv6BFT6EJY/Dxs+iq0oEzUl8jz2fM3nCQkKcZJ5cWH2jtn8svEXsizLwrat2+JkHyIiIiIiIiKSxFkSb+kzsPlTqPkd5GkVO9st8TRUeNudZLvpU5I6JfI8li55Ol6p+wpDFg1h+/Htsbpta3/4/LTnKZu6LLtn7aZEiRI8/vjj7N69O1b3IyIiIiIiIiJJPIm34kXYOAhu/Rry3Rm72y/VC8q+Cos6u9V+SZiGXSQAF8MvUvrj0qQOSU2prKVibbsnL5zk102/sqTTEsplLedMuu3Xrx8HDhzgiSee4IUXXiBbtmyxtj8RERERERERSYJW9YNVr0L1kVDooes+/KGHHmLx4sWcO3eOQoUKMX369GgmC1+Cde/Ard9Agf+RFCmRl0BM2TSF4UuHx/p2y2cvz6v1/pkAc+HCBYYPH84bb7zB6dOn6d69O88++ywZM2aM9X2LiIiIiIiIiJ9b8zaseAGqfQpFOsXP8t2NH0HN72O/8i8RUCIviTpz5gxDhgzhnXfecZbgPvfcc3Tr1o00adJ4HZqIiIiIiIiIJAa7JsCcOyBlTsh5k4Mtosvng53jIOIiNFsCGcuRlCiRl8SdOHGC999/nw8//JBUqVLx4osvOn30UqRI4XVoIiIiIiIiIpJAhYWFMWPiZ2Ta+gpHjx4hZcpU5M2bl9y5c5MsJCTuAwgMdodqBKciKVEiTxyHDh1yqvOsSi9r1qy8/PLLPPzww4TExx+fiIiIiIiIiCQKBw8e5PPPP+eTTz5h165dNGvWjC5dujgfg4KCvA7P7ymRJ1fYs2cP/fv3d/4oCxQo4AzHuPfeewkM1IBjERERERERkaTIUkcLFixg6NChjBs3zlnR16FDB2eQZpEiRbwOL0lRdkauYCWww4YNY8OGDdSoUYP27dtTvnx5Jk6c6PzhioiIiIiIiEjSYFNlR4wYQZUqVZwcwZo1a5xknhUBWZsuJfHinxJ5EiUb/zxq1ChWr15N8eLFadOmDdWrV2fatGlK6ImIiIiIiIj4sa1btzpDMfPkyUPnzp2dvMDcuXNZunQpHTt2dCryxBtK5Mk1lSpVivHjx7N48WIyZcpEkyZNqF+/PvPmzfM6NBERERERERGJJREREUyZMoXbbrvNqbQbM2YMPXr0YOfOnXz77bfUrFmTgIAAr8NM8pTIk2ipXLmy8wc9Z84c54+7Vq1azh/3smXLvA5NRERERERERG7Q0aNHnWWyxYoVo0WLFpw9e9bpg7dt2zb69OlDjhw5vA5RLqNEnsSIJfBmzZrF1KlTOXDgAJUqVaJdu3asW7fO69BEREREREREJJqsMMeWydry2VdffZWmTZs67bX+/PNP7rzzTkJCQrwOUaKgRJ7EmJXS2h/4okWL+PHHH50kXpkyZXj44YedjL2IiIiIiIiIJDwXL168tEzWCnOsbda7777rDK+wIRalS5f2OkS5DiXy5KYSem3btmXFihXOYAxbdmsNMJ988kn27t3rdXgiIiIiIiIiAuzatctZJps3b14eeOABsmfPzowZM1i7di1dunQhXbp0Xoco0RTg0whSiSWhoaF8+eWXvPbaaxw5csR5MejVqxdZsmTxOjQRERERERGRJMXSPTNnzmTIkCFMnDjRGWDZqVMnHn/8cSehJ4mTEnkS686fP8+wYcN46623nM9tyk3Pnj1Jnz6916GJiIiIiIiI+LVTp07x1Vdf8fHHHzsVd7feeitPPfUUd911F8mTJ/c6PLlJSuRJnDl9+jSDBg1iwIABBAUFOdV5VqWXKlUqr0MTERERERER8SuWtLPknbW+CgsL47777nMSeNYLT/yHEnkS544dO+Yk8yypZ+vuX3rpJR577DFdCRARERERERG5CZaw+/nnn53lszZttlChQjzxxBN06NDBWUor/keJPIk3Bw4c4M033+STTz4hZ86cvPLKK06TzeDg4Os/OSIC9u+HM2fg3DmwMdhW2Zc5M6RJEx/hi4iIiIiIiCSY8+vPPvuMTz/91Jk427x5c6f6rlmzZgQGaq6pP1MiT+Ldzp07ef31153BGEWKFHGGY9ha/StebMLDYelSmDXLvc2dC8eP/3dj9pzy5aFuXfdWpw5E46qD5QJ37nRvO3a4H3fvdr8fGureAgLcfKHdLFeYLx/kz//Px9y53ftEREQSOjuu2XEu8rhnt127rA3GP8c9e0cYedyza2V58lx57LNbihRe/yQiIiLXZ6eTVgcSea4X+dFOKSOPe1YrYjUldtyzxWK5cl15vmcf06YlQbH0zfz58xk6dCjjxo0jTZo0TuWdVeAVLlzY6/AkniiRJ57ZtGmTU5X33XffUa5cOfr3789ttWoR8P77MHgwnDgR841a9q1RI+jfH6pVc7514QIsWvRPTnDFCjh48J+n2MmKvVDbCYt9HnkSY38ZkS/yJ0+6L/x20mNfR+YQLZlnu7H8oeURy5Z1vy8iIuLlycuqVf8c9+wYuGePe1wzyZKBDaqLPEGJPO7ZITTyuHf2rHvMsxMfu8gVKXt2qFDhn+tnVaq42xMREfGKHd82b/7nuDd/vnvuFnneFhTknrfZcc9qPiKTd3beFhbmPu78edi71z3uXX4amjEjlC79z/lejRrRXBB26BDMnu0GZgdV24EdMO2EM2dOqF0bYpB4O3v2LGPGjHESeMuWLaNixYpO9d3//vc/9aBPgpTIE8+tWrWKvn37km7iRAYHB5PeXk1v0nLKM7FMH2ZlvJ35i0Kc1027wmIvvpZ4K1Dgnyst9mJuJy/REbnCN/KqztatMG8ezJnjJvvshd5ek+vVgzvvdPchIiIS17Zvhx9+gJkz/yliz5DBPSbZSUehQv9UF1gyLroXnexd4pEj/xz3bD8LF7onSnY8TJnS3b4dX9u0cS9oifirJk2asH//fmcVSdq0afnoo4+ck2nTrVs3p0fVjh07nJPsCpbxvoovvviCt99+m4iICBo0aOA0pg8JCXFWq1hP6Ui7d++mTp06/Pjjj//Zhq1usYvhFsf3339PfvsDBx5++GGnF3XNmjXj5HcgklDYscmOe3/+6R6T9u1zj0m33gq1akGxYv+c79l5YHS6OUWyRN7lq7ciF4pZTs62U7mye9xr0cJN8DnnklY9MnGieyC2B69de/0dWXYx8spY27aQNet/HrJlyxaGDRvGiBEjnGGS7dq1cwZIVq9enYDonsSK31EiT7xn/wv27g3vvntTmzlFGr7jXoYHP8nisIrkyX6R+o2DqVsv0HlttAsecfVaZ9UPVukXeRXIbpbYa9YMOnWC226L2cFDRETkeqyC4JdfYPhw+P1392JS5PlAZJW4VSHE1aF706Z/jnl2ImWVDLfc4h737rkHUqeOm32LeOX48eNksAw5MGHCBF599VVW2BtArPBmttNgvlatWvz0009XTeRt27bNSbItXbqU7Nmzc/vtt9O0aVOnsubfypQpQ79+/bjTrg5f5uTJk1StWtWZTjl69GhWrlzJe++9x7Rp05y4LDEo4o/s2GPHnM8+g/Hj3WOcXbCKPO5VrRq3VeJW3R553LN83caNljD00bHs3zw0vzPZ9i6/8Y3bQbN7d+jVi4g0aZgyZYpTfTd16lRy585N586d6dixo/O6IaJEnnjvvffguedu+OlLqcinPM63QQ8QGpjcqYR77PEg58Xcq4sUFy+6F2Ts5Gr6dLd6ukMHeOwx96qQiIjIjdq2zT2+fPml2yqiSRM3edaqlXe9W61i3ZJ5dnJlxUPWS+/++6FzZ7eVrYi/GTlyJAMHDmT58itP3AsUKHDNRN6AAQOcChsb/mYmT57sDIOba6W0l1m4cCGtWrVyGthbtd7lzpw5Q/ny5Vm9erVTxbdr1y769OnjNLifNGkS6dKli/WfV8Tr6rsRI+Dzz93kmSXs7Lzq3nu97WG3bBl8NtzH6K/COXce2vh+4nHfMBrwBzd6GnoqfXoeTp2aH/fudSp2LcnfunXr6A2IlCRDiTzxltUqFy/uliLH0BYK0TvgXcb77qRkkYt0eioZDzzgDrJNSLZssSUU7gmXHYS6doU+fdzKCRERkeiyY8hrr4EV22TL5l4gevRRt11EQmJtgb76yk022gmXnWi9+SYULOh1ZCI378EHH+RPy1r/fxKu7L/Wk18vkde1a1dy5crFCy+84HxtVXWWgLNhcJfr1KmTs2z2fesdHQWruhs+fDg5cuRg1KhRToKwdu3aToWfiL+w9ki22tyOIaZ9ezeBd42V6544cwbGjoXPPg5l/uIQ6gTN4/3w7lRhyQ1t72JwMPuGDiW/XaUTiYLa8ou3vvsuxkm8c6TgRd6gZOAGVhVs5VS+rdmYjKefTnhJPGNLeu3gY+/PPvgARo2CIkXc5J7S6JKYXbhwwenRUbRoUedEpr29u7qsj5ANsbETGTuxsH5BUbH+QD179qRUqVLO4+vXr89ma0Dy//0zrTdQiRIlnOVFNpHr3OVd9y9jfUNsG9aryJYrRXr55Zf59ttvY/1nF4nvajcr3rFjx5gx7jwo61X3+usJL4lnrMXPM8/A+vVu/6LFi6FECXjllRu6bieSoHz11VdOBZwNaevVq1ec7MMq7qz/3aOWqb+KJ5980qkGtGV31kvPbpYQtOod66Fl/ftEErPJk91jR79+7opTm7w+dGjCS+JFrop95BH4a1EIf/0FoZWqUZXFdAgYwREyxXh7ycLCyG/JfutZIRIFJfLEW1c5ub+audSkXPA6Bqd8ngEfBLNqfTJat/ZuCW1M2KqILl3cJqlWOWgXWBo0cJdIiSRGvXv3dprsbty40Um6WX+eSGPHjnV69thJhiXqrPl2VKwx+Lx585weQ/b4hg0b8uKLLzr3pUiRgiFDhrB+/Xrnfjuxeeedd6LcjjX9XrRokdMk3JYomTVr1jjbvO++++Lk5xeJD3bMsFYRdvzo2NH92parerWENibs2HzHHfa36F7QspcIOwFbsMDryERu3kMPPeRU5h2xUtkYyJcvnzMQI9L27dud711u3LhxlC5d2rlAdT1hYWE8++yzzjLfb775hqxZszrPt155W20qm0gic/Qo/O9/bo9x67tqld1Wje7lEtqYsGEb8xaGOPUqUzK3p2TwJsZzZZ/LaP8i3norLkIUP6BEnngrBq/IY7iXBoEzKdYwD2s3BDtXZhLDicy/WY/kgQPdExlbflS9Oiy5saprEc9YUs2m7r3xxhuXJmbZ8p5Ikc3AzYkTJ646Vcu+b5V958+fxzo9WAPvPHnyOPdZpZ9V6ZmgoCCnsbed8ETF7rdtWFzJkiVzKv169OhxxfQ/kcTGjhN2jDh1ChYtst5akBhbX1njcavQs4Re3rzuZHer1Iuywaz9sCrbkwQ66GLvZdUxtnw2c+bMZMoUs2obG1xhF7Fs+q0d96xX3r22/vwydny9VjXe5WzprV2wsmOwHQMjj7f20b4WSXBsSuDp0xDFKgvLcdvE2dmzYdIk+P57+P+3hYmK/Rna0Kd1m0K4/cEMtGM8b/ICMV6MFcXEahGH9cgT8cyPP9rq0mveIsA3gGecL3s8HeELD/f5jZMnfb7GjX2+1Kl9vilTvI5GJPpWrFjhy58/v69Xr16+ypUr+2rVquWbPn36FY954IEHfHny5HFuK1eujHI74eHhvu7du/tSpUrly549u69SpUq+U6dO/edxp0+f9hUvXtz3o71mROGHH37wVaxY0VenTh3funXrfIMHD/YNHDgwln5akfg3caLPlzKlz3fbbfb/v89vhIX5fE8+6fMFBET4Pmq/0Ofr0MHnK1LE5wsOvvL4b1/nz28vJD7f55/7fJs23dR+Dx70+WbP9vlCQ2PtR5EkZvv27b6qVav6ypQp4ytXrpyvYcOGvmXLll26v1OnTr7cuXP7goKCfNmyZfMVLlz40n2PPvqob6L9Uf+/4cOH+woVKuTcOnTo4Lt48eKl+9avX+9LkyaN76S9SbyOzZs3+5o1a+aLiIhwvj5y5IhzHCxdurQTj4jnjh51D2g9e/p8lSv7fMmTX/laHxDg82XO7PO1auVb3nOUL2fWi74yZSJ8u3b5/Mrgwe5x7wk+9oUReN3z30u3wECvQ5cESok88Za9calQ4aovXuEE+J7mA+fL99/3+aULF3y+9u3dc5aRI72ORiR6lixZYhcVfaNGjXK+Xrp0qS9z5sy+/fv3/+exI0eO9DVv3jzK7SxcuNA5GTp27JiT1Hvuued8999//xWPuXDhgu+2227zde3aNVqx7dy509eoUSNfWFiY74UXXvDdfffdvhdffPGGfk4RL3z6qfve/dFH/TPxZDmHN990D/W9eMs51kfrhCZXLp/vpZd8vmPHYrzPKlXcTaRN6/Pde6/P9+237vmlSEw0btzYV7ZsWV/58uWdC1h27Itkxyi7wGXHxssTfFH5/PPPfUWKFHESeR07dryUyBsxYoSz7cibHVfbtm0b5TZee+01X6lSpXy33HKLk2SM9NBDD/nmzp0baz+zyA29yH/3nZu4s4NZNF7fZ1DflzbotK9erdAbeYlPFH74wedLHhLmaxP4k+8sKaJ33Cta1OuwJYFSIk+8t2aNeyUmihevfvT1hQSF+caM8fn98a5XL/fHnjzZ62hEru/QoUO+wMBAJ1kWqUqVKr5p06ZF+fgUKVL4Dh8+/J/vP/XUU7433njj0terV6/25bKT9f9nJzdt2rRxTnQiKw6ux056rALQKgTthMY8+OCDvj/++CNGP6OIFyZMcI8FL7/sHhv8mV28Cg4M873N89GvTrBbxow+39ChMdpX8eL/PD0oyC0CsfPLGjV8vnff9fnWrfP/37fcPLvoFMkqxK0yL9KsWbN8u3btcpJ510rkbd261ZczZ07fvn37nONaq1atfEOGDInysVZZN378+P98/8SJE75ixYo5x2C7oPbMM8843//99999TzzxxE3+lCI3YdEin69ixRi9pq+hpC9F4Hnf3XeF+c6f9/m1OXN8vgxpLvr+FzAmer+fPn28DlkSKPXIE+9ZI99586BQoSu+vYgqvBbwCgPeD+JfrUP8jvVRePttt7Frhw5w5FCE2yfImpyePavxtpLgZMmSxRlM8dtvvzlfb9u2zbmVLFkyRn2EChUqxB9//MFF642F9UOZ5EyojWzgbX2D7HnDhw+/ap+9y33//fdODDZF9/JeQYGBgZy2fiwiCYm9tttrvL3Wnz7NgX0RPPaYO/nOpvQlhkFON+Ohh6D/m0H0DXyDFbj9MKPl2DF46imwITphYTfUnsl+9TYN2KYL2nydkiUhf353MuL06W67PpF/u1b/V5uyHtnj9VrGjx9P69atnZ529vzOnTszxsZR/8vChQs5ePCg89io+sKGh4cTGhp6qTfs2bNnncFPb9sbShEvjB3rNriLwTDDi4TwQPAYylUI5Jtvg0ieHL9mv55vx4Ywxncv33HPtR9cuDDE0WRsSfwCLJvndRAiDjvJfuMN+Ogjzp71UTF4FXlr5uf3P4IJTCIpZzs3KVsylOoHf2Gc704uvT20N4r2Ym6jC61LuH20juE36sR6CDsNmSpBQBL55Uqss2l41oz78OHDTqLs5Zdfdpp42zS+du3ace7cOef7NkHPJtpWsHGV2OTNjs6Jid1s0EWXLl2YO3cuISEhzomNNf62BN/o0aNp3769M/Ai8mSpZs2aDB06NMp4jh07xu23387vv//uTLy15OBdd93lDMgoWLCgc/Jk+xDxjHXxnjXrn5tNlPz/t2H239aBv7I6V2NWrAlJlEMtboQl1erVCuX44i0sCqtACmI46MIGAnz++XUfVqIEbNhw/c3ZS0RoKKRKBU2bguVQWrSAbNliFpb4rwcffNCZVmsmT57sXDi6XIECBZwLWJHHvH/r2rUruXLl4oUXXnC+Xrt2Lc2aNWPnzp1XPK5Tp06kTZvWGWYRlY8//ti5yGXHzVGjRjFgwABq167tHAdF4p1d2G3ePMbFB314nQ+SvcDyVUEUK0aS8eQTEYz57Ayrw0uQm38ufl+SK5f7O/3/i9si/6ZEniQ8+/fTrc0Ovl5bmVVrgxPlpKKbYZUAjRvD17SnPaOv/kCrYOzaFZ54ghhfvvouBURcgORZIM/tkLsV5GgEwalvOn4REbnM+fNgyechQ+AqU5fNF3TgsYDPmTUrgNq1SVK2bYNypcPofO5DBvB8zDcwZQo0axYribzLBQX9s76pUiVo2xZatgQbpu3v1ZJyfZY8sypwS+bFdiLPquxy5szJggULKGUrV65jyZIlTiLPYurZs6dTyWdJvW7dut30zylyXVZZXqQI7NsXo6ct4BZqBvzFkKGBzulMUmIDpSuWuUiBXXP4PbzRlXfWqAFWpZsvn1fhSSKgUhxJcE6mysHw5bfQ/62kl8QzjRrBA+0j+CDkOqXUVsnRowcULQoTJ8ZsJ5bEMxcOw9ZRMLsNjMsIfzSGjUPhzI4b/wFERMQ1frx7cvPss9dM4tkV1fdDetOxY9JL4pmCBeHV14MZFtSFs6SM+QY++yzOqgVt+a0l8pYscZc7W24mZ07o3NmqseDcuTjZtSQCDz30kFOZd+TIkRg9L1++fE7leiSrGrfvXW7cuHGULl06Wkk8a0Px7LPPMnDgQL755hunCt6eP2HCBKdyXiTO/fJLjJN4ZlDA01SpGO68niY1qVPDx58lY1p4Q1ZT2v2mndN9+y3MnasknlxX8PUfIhL/xwJreeNFX7zvvvvOWba3YsUKTp065fQeCQ6O/z+TBx4MpMk3ZdlEEYqy+doP3rUL2rSB11+HPn1ivjPf//cX8oXC/hlw4E9Y3AXSFoe8d0DulpD5FggMurEfRkQkKerbF/r3j9ZD11CadaFF+fQB4l3v3r359ddfncRC6tSpqVevHu+++y55b6Z9ww247z547rkUTKE5d/JjjJ4bOmc+G9dc+zEXYrhiN8r9hLofDxxwV/N++qm7FHfQIOuPdvPbl4Tt5MnjnD9/lmzZcjlfz5jxE+nTZ2bfvky2mOSK/0+2bHH/34hK2bJ3MmBALf73v1fJnDk7AwZ8Qt2697Lmsv+HBw/+gtatH73ie1fzxRfvU6/efRw5koPNm89w8mSA87yzZwNYseKMks0S53L8uoDMMXyOXbT5JfB23n4kJN4rnPv168dXX33ltIaxliuVK1fmnXfeuWoVbVxp0AByZgtjbOlPKNM/EKpVAw/OOyVx0tJaSXCstYe94Z46Nf73bY37jx496vT2st5fXiXyLJGZI0soPU+8wou8Ff0nfvCBW6V3Pd9G84gZEOIm+ELSuwm93K0hZ1NIlj76MYmIJDXWbP7/l81Fx8v04/Msvdl9IFm894S15X3WS9L6fFmz/CeffNJZ6rd8+fL4DQS3V172v37ie9/dMXredvJTkKtXPIrEDquiawdYZsz+ULMC7wGRJ/+PA79ajxhw0hpp4dLF2I7WBfP/b8aqSCOHUtQDPrEOjf//ta0BrwJO3yzbxrVsAbpYtz570wYcBdoCViVYE/g0Dn4PIld6h+d5ngExes4P3EG7gPHs2RPgVDnHpw0bNpAtWzYyZszo9FMePHiwcwHLBrXZIJn4ZAOWrBXeunVq2SAxo0SeJCg2qDVLFhg2zJ3e6pWZM2dSv359zxJ55vFOPv4euYZloVc2Ub4m65Vnl2FtMEZsJPIuFxAMvnB3OEaWmpC3DeRqCemKxnxbIiL+av16t4laZPlWNJQM2USTJ4o4lV1eswRexYoVnYtadpITnz7+GJ7rdp7D4RlJyfloP+/YHY+y97VrD7ywHnc3M4nW3grYRTY7zN56K9Sv71bhZbVcjohIEpZ63u8UeLxpjJ5zb8B37K9xBzPnejuEzIauDRs2jB49eji9JW1penyaN8+dZLtypVXrxuuuJZFT7aYkKLZK1N5oV63qdSTeq1otgG9HFIrZk6yU8Ztv4JVXYj+gS0tww+HQHDj8FyztCakLQJ62kKc1ZK0JgZoKKiJJ2KhRMUri2dXUzWEFeCmBHPds6nP+/PnjPYln7Nh/NjwF+8hJIbZF70khIWTs24WM/99iKDZFJu9seKANumjVyh0anyJF7O9LRCTRKtEQhlWwK0HRfsrmkJLUv9W7cwZrKXH//fdz4sQJAgICnERefCfxTOQ57+bNSuRJzCiRJwnKyZPux3TpvI7Ee+nTw+nwVIQTSBAR0X/inGlw/K64DM099YxM7J3ZDhs/gg0fQlAqqP0DpIrf3koiIgnG3Bkxevh5UhDmC04Qx73p06c7vYN++OEHz4575gQxaN/w2mvuBIpYVrky3HWXO6W2dGkteRIRuSpbjmqFBFWquJPao+Ek6Tw97t12220cP37cqT63ac95PJqwmCyZe3HoxAlPdi+JmBJ5kqBEtiWwSXFJnVUBBBBBYEySeObYPJhchnhlVXom/CzMbB6/+xYRSUj+/4JUdEW+xnt93Js0aRLt27d3pl42a9bMs+OeCeb/P4mO6EwDiOH7EPu3WLXKbfVhyUU72dQAQRGRa7AJydFM4pkgwj0/7plMmTLRvXt3pwq9WLFilC9fPl73b03O7NinGRcSU/pfRhKUyCszR49CoRiuKvU3x45BuuCzBMTgfMZx70vQ4n/XfszNJvqcfnlh7hCM7A0gW13IUgOCU93cdkVEErutI2GlNcCPnuRcJHngRY4eTYZXRo8e7Qy5GDt2LE2bxqzPUWwf90y6mGRDrQrknnvc0rlYEHliaW0+pk2zKkV48kkoWRLuuMPdjS2Fiud+6CIiCdfp09DRBrpEXzrfCed8LyGIiIhw+qJv2rQp3hN5Z864ibyEUJUviYsSeZKgFCwI1pbnjz/c6uz4Fh4e7ryQ2wSjyAaoYWFhJEtmkwTjd5TgjN/DqeJbHLMn5cgBHZ+BDLHd2yjQHYbmi4D0ZSFvW3eKbabK7vALERFxPf48fDQKDh2K9lOqBC7lj+nVePTR+H89HTJkCH379nUq8mrXro2XZsyAbMFHyRO2O2ZP/Pzz6ybyQkJiPuwi4rKCeJso+O678MYbkCEDtG7t9sxr0kQnYCKSxP38Mxw8GKOnVAmdzx+/WWFB/F/EGjRoEPfeey/Zs2fn0KFDvPTSS865Xs2aNuk5/o97kQOZRGJCU2slwXn0UVixAhbHMIcVG0aOHMkjjzzyn+//+eef1KtXL16n92bLEs6gi0/Qic+i9yQ7S/nlF4hONUV0ptYGhIAvFAKTQY5GkOd2yHUbpModvXhERJIqey228q3ItaLX8RFdeSnlBxw8EkzKlMQra/Jt09mT2zjWy0yZMiXeE3tlil+k7sbPGEqXGD0vNEsONs7cd83HWJ5v+3ZifQmufRw40J1iKyKSFOV4+2kyfxOzseuzqEM9ZrF6tduHND61bNmSRYsWcfr0adKlS0fVqlV5+eWXqeJBFcn998OOHTB3brzvWhI5JfIkwfntN7D2PDa9p3BhkqQxY+CB+8PZ58tBVg5f/wlp04I1J2/cOHo7uFoiL3LJbIockKcN5G4F2etDcDyfWYqIJHaTJ8Pdd7vrZq5jD7nIyy5++DHQmY6aFFmruzJlYCZ1qcvsGD13GwWiP+VWRERi1dv0ohfvxug5NswvT/ABHn8pC6++SpJ07hxky+ZWenfr5nU0ktgokScJTmgo5MwJHTq4y1iSGvuLbFA3jJC/ZvF7eKPrP8HO+j74AAoUiP5OIhN5AUHuDm0KbcZK/yyZzVBOI/pERG7Wli3Qo4dboXcddYLnkbLuLUydFpQkX367d/MxdthhdofliNmkduur264T+175NE4r8qwRuRVYpkoFdeq4FXi1av0zaVdEJKlKvWAGBTpG45zlX7ryEZNyP86ajcmc19akZsQIt7Xgrl2QWwueJIaUyJMEaehQ98qElRnfeitJypAhdkITwTxfDaqzMOoH2fg8W+rbteuNNROcWBDOH4SczSBPa8jVHFJku+nYRUQkCgsWuAe2WbPcd+xXWWZUnz8Z/llgTHuGJ3r2a6lf38dnvo48yoiYPdmWBFsvDivnu4YSJWDDhuhv1pKp1hrXls/a8K0773STgTVsrpM6TIuI/MNeKKtXj3FfpJ3kpVzQGh7onJrBQ5JWz+2dO6FcOXjwQfjoI6+jkcRIiTxJkOz/yubN3eW1y5dDmjQkCevXQ8UKETyXdRSvZR7kdua2hkl2mapoUahb173FpPouKmFn3WW0Qd5NSRQRSZK2bXMzV3azg9zZs+76muTJef7gs3x87F5WrApKMq0lTpyAciVDqXBgKj9FtHbmKsXI4MHQ5fo99aKTyLMEnZ2PWgLP2gO2aQO33QZFisQ0KBGRJMZeYK36InL8eDSN5j7aM5qpU6PX5tsf2CClhg1h/35YssQ9zROJKSXyJMHau9e9wG5XwYcP9/+VnhcuQOSwpPnz3dkVIiKSdNhxoGpV9+KV5fn8/Thg70AfbB/Ob2NPsDqsBNmI/qRfZ8rExx9Dp07RevjVEnn2O7aWHjaJ9vbb3Um01m5Wk2hFRGJo7Vq3EsPKzaLJEhH3BoxlTpY2rFwbQpYs+L333oMXXnDP9zyYryF+ImnVsEqikisXfPYZfPGFu8zWrpL7c0WCHfc2boRvvvH/kzcREYl6lejo0e7kdksonT6N37J+c48/FsG3YwIYEfZgzJJ4Vi7399/RTuJdzi4KWg7QlCwJzz/vnkwdPmyT692Lh0riiYjcgFKl3AOYVUlH82TG6jSG+R4n5NhB6twaGpMcYKJkU86few5ef11JPLk5qsiTBG/sWHjgAWjdGr7+GlKkwO8qDy2Jd/AgTJkCFSp4HZGIiHhp0SJ3Sae1Q/31V8ieHb9iq4nvbRfOtKlhfBdxN7fz8/WflCOH21rikUduaP3VHXfApEnukAqbEWW/37x5byx+ERG5DpsuNGgQzJgBq1f//3C9q9tFHpoHT+N4psJMmR5C2bL43XLaXr3cajwb5vjss/6/2kzilhJ5kijYEiNb8mJNQSdOhIwZ8Qvr1kGzZm4bPOsNcbOt70RExD9Y+zw7Pti7NDs+WJtUf2CVb1ZtaBXov3yyhxqnf3cP8nbSZxm+8+fd0sTUqSFPHndErCXwihe/qf1aVb8tXVYvIhGReHbkCMyeDT//DF995Wa1onCc9NweNInlKaozcVKwM9fPH1jLc7sGZcUpX34J7dt7HZH4AyXyJNGwizl2UmNNqIcNc6+mJ1Z2QmEDDF96CeeK0y+/QObMXkclIiIJiVVq27Fu40Yfb78dwOOPu8fAxOqnn+Cpp9wVV5actL51IiKSRPTpA2+8cc2HnCc5DwZ+w4SAO3j2uUBeftkteEjMFfaPPQZbtsCPP7o9WEViQyJ+OyhJjQ2+sMk+NhCiZUto184d/pfYLFgANWrAM89A9+7wxx9K4omIyH9lywaz+k6n87mBdH0qnNq3hrJ4MYmOncDYcla72aQ++xmUxBMRSWLmzLnuQ1JwwWm5MDC8K0MHnKFsiYtOwUNiKz2y6vOuXaF6dXclmZ3DKoknsUmJPElUrE/QmDFuz6Dly90TAWtUbeO7E8My2nvucSez26qhpUuhf3//6/knIiKxZONGUv3vdt4J7cliX2XnwGFTbdvfFx7lBNaE2AO2Rw93qIQdA3/7zV1VlRSmEoqIyL9Es6Q8EB9P8TFrw4tTcdcvTp/0hnVD+euvhJ/QswGGAwZAkSLuUlob3GhFG8WKeR2Z+Bsl8iRRatEC1qxxXyhHjHAbVtukOVuqk5Cm254755602IA9G+RkyccJE9x2QP7WxFVERGLZk0+6feOACqxgblh1xnEXC8btdi5k1asVyrffum3lEgo7BtvFNutra8M6bAqvTelbtQqaNPE6OhER8YxNG4qBPOxhnO8u5lKTc/OXO6uyypW8yODBcOwYCYYlF23FVYcOkCsXvPKKO7h30yb3expqIXFBPfIk0bNk2fjx7hUPq9jOnx8efdSddOvF8Ajr32qT162ZqU3ZPX3anbjbqRM0agRBQfEfk4iIJDJWzmbDHqJ4mxZOIL/RlOGBTzDJ14L0aSN4sEOI00zbLhJ5cdKwdat7zPviC9i1C6dJufUFsmmxqjwXERHn4FC6NJw6FeOn2pFwIbfwGZ34Lug+IoJCaNcugI6dAp2WRcHBeNLH9vvvYfhwt5e7HX/tfO/++/1nMKMkXErkiV+xpTuW0Bs1Co4edRN5dev6qFs3wDmpsK9j+wTHEndWaWBVdpE3G85UqJB7EvPww5AjR+zuU0RE/JytxbGGctexl5x8ySN8FvwkO8JykzXDReo2CKZu/UBn2KudM8X2gAx752iJu5kz/znu7dzpLpm1Y17Hjjc9ZFZERPyRXfF58MGb2sQJ0jGG//FpcBeWh5Uhbaowp1rPOfbVhSpV3KFKsc1aOV1+vrd2rTsJ/d573XO+W25R9Z3EHyXyxC/ZMiPro2AvsjN/OsbC1am5EJGMvNkvUq1mMAUKBjpLfuxmFXz2MVOmq7/4WrLuwAHYscM9WbGbfb51Uzjz/4rg2KkQ0gefpnaDZNRtnMw5iFSunLinC4qIiIeWLYNKlaL98AgCWERVZlGXWYENmBtQi5PhacicPpTqNQIpVCTo0vEu8thnwzSudpyyd4d2UeryY5593L4dFi6EPXvcSjtr5G3HPLtYFtkDVkRE5Kqs4sKu+ISF3dRmLImxjpLMpB6zAuoxK7gRB0IzkTpFGNVvgaIlg/9zvmdLX69VvXfy5JXHPOfz7T6WLrzIhm3JCQoIp1L+o9S9K6tz7KtTB9Klu6kfQ+SGKJEn/u/ttzn/wqtOObad4CynIjuTFWanLy+HQv+pe06ZPJxUycMJCbarOD7nJCY0LIDQ0ABOnQsiNMw92wkgglwhh8nPdvKHbqYaf9tpE+VYSdCMadCggYc/rIiI+AU7CNmZhy1FugG2/HY5FZzjniX4doQUZSf52BuaBd//t0hOFhxOmpQRhKQKJiQkwLmYFRrq3s6cubL3niX9Ik+IKlZ0k3fVqilxJyIiMXR4AXz7CAxeD5tjYXu2DOrFF/F17MgGijvHvfncyvbgouwILMDu0OyE+dzsXVBgBOlS2/mez7lZyyM75l0MDeBCaCCnz/2T5csQfIr8gbvIF7qVUr7V1GMmNZlH2ofuhJEjYyFwkRunRJ74P+uI2q1blHedJSW7yOuc3OwmD+dISSghzi0A3/9/FkoaTpOPneRnB7nZQwhXuYJkZQp2ZiMiInKz7ETBGt/FoouEsIfc7CC/c+w7U7MpoXff75zI2DtCW45kt5Qp3UFSlrizj/a1iIjIDTt/EJa/AFtHQLZ6UGUIzNkKQ4fC3LnuFaTosgOVjXG3pujWHN2ea8urrtJXdh85nWOeHftOkP7S+V44QZfO95JzgVzsdc757JaOq/Tye+opGDLkJn4RIjdPiTzxf5s3Q9Gicb8fK1ew5uSaZiEiIrHFTlBsPHtcsEZC1ugudeq42b6IiEhEGGwaBiv7QnAaqPQ+5Lv7yp5Gtsx2yZJ/GtBt2eJObbfS8GTJ3GZ0OXO6a1kjeznY9y5nUy/mz4/7n2fSJLjttrjfj8g1KJEnScM998DYsXG7j/feg2eeidt9iIhI0mJv0/r0gbfeirLS4Ia1bAnffacknoiIxJ2Dc2BxFzi5Dko8A6VfgpA0cZdga9WKOGVN0Bcs8GZMrshllMiTpMFG2FpH7k2b4mb7dlXm55813UJEROKGTXB6/nmYN+/mtlO4MLz2Gtx3X2xFJiIicqVz+2DZc7B9NORoAlU+gnTxMM68Rw8YODButp05s7sEuESJuNm+SAwokSdJx+HD7lUau4oSmx56CD77LG7mnIuIiFxu2jQY+TLMXQC7AqJXpVeggDudonFjuPtuHa9ERCRuRITCho9g1auQPDNU+hDytLlyGW1c69fPvcVmmsMGakyZAsWKxd42RW6CEnmStFj/hUGD4M033Sq9m2F992yp0513xlZ0IiIi17ZzHMy9GyoPhqz3wZw57gWq48fdfkIREW7foDRpoEIFN4FnEytERETi0v4ZsLgrnN4KpXq5t+B/9bGLL3ZstJZHixbd3HZs0lPXrtC3r3tcFUkglMiTpOnkSfj0U7eywZYrRXdKUtasULu2W9nXvr36I4iISPw5sgim14VCj0DVoV5HIyIiAmd2wbJn3AtNuVtB5YGQphAJgrU+GjfOHaCxa1f0nmPDNapVg/r14Ykn3CEbIgmMEnkioaH/TElavhxOn4aTh+DoQshbH7LmdfvrWVVDyZLxWxouIiJizu6G36pB+jJQbzIE6kKSiIh4KPwCrH8fVr8BKXNC5UGQOwFPc922zT3fs16zhw9z+sAZ/p4fRtXaKUmbI7V7nmfnezYR1yrxRBIwJfJEonJ8DUwuAy1WQ4bSXkcjIiJJWdgZmFYbws9Bk/mQLIPXEYmISFK2dwos7gbn9riTaEs+A0EpSEzWrIEyZWD1aiit0z1JZHQ5V0RERCSh8kXAX+3hzA5oulBJPBER8Y71v1vSA/b8DHnvgoYzILX6sIrENyXyRERERBKqFS/B3l+h/jRIW8TraEREJCkKOwdr34G1b0OaglD/d8jZ2OuoRJIsJfJEREREEqKto9yTplu+gOx1vY5GRESSGuvCtXsiLO0BFw5D+f5QrBsEJfM6MpEkTYk8ERERkYTm4Fz4+zEo+RwU7uB1NCIiktSc3AhLusO+qZD/Pqj4LqTKTUJ27hw88wyEh1//sceOuR/79YOMGa//+IYN4e67bz5GkdigRJ6IiIhIQutBNKct5GwO5d/yOhoREUlqA5ZW93cn0qYrAQ1nJpqq8EWLYNgwCAiAoKBrPzZy5OePP7qPv9bjLDG4fLkSeZJwKJEnIiIiklBcPAGzWkHK3FBjNARe50xEREQkNljGauc4WPYMhJ6Eiu9B0SchMPGkDOrUgW7dYMgQCAuL3nOuV71nSb40aeDrr2MlRJFYkXj+KkVERET8WUQYzLsHLhyBpn9DSBqvIxIRkaTgxFpY3BUO/AGFHobyb0PK7CRGH3wA69fDH39EP5l3vUTeTz9BsWKxEZ1I7AiMpe2IiIiIyM1Y2hMOzIQ6EyF1Pq+jERERf2eVd0ufgcnl4eJxaPwXVP8y0SbxjC2pHTcOChWC4FgoW/r4Y7c/nkhCooo8EREREa9tGgYbB0ONMZDlFq+jERERf19Gu300LHsOIi5ClSFQuKPftHNIlw6mToVKleDkSYiIiPk2AgOhSxd4/PG4iFDk5qgiT0RERMRL+6a5S5rKvAIF7vU6GhER8WfHVsD0OjD/QchzO7TaCEUf95skXqSCBWHSJDchF1NWydeokbtMVyQhUiJPRERExCsn1sPcdpD3Lij7itfRiIiIv7p4zL1oNLUSRIS6vVirfQLJM+OvataEESNinsSzZbm2PPd6k29FvKKltSIiIiJesKEWs1pCuuJuTyLrqC0iIhKbfBGw9UtY3ttGN0C1z6HQQxCQNGp6HngA1q2Dt992VxRfi1XvpU3rLsu15bkiCZUSeSIiIiLxLfwizLnT7U1kwy2CU3odkYiI+Jsji2HxU3B0MRR9Csr1g2QZSWr693eTebbU9lqTbC2R98sv7rJckYQsaaThRURERBIKKwlY9IR7YlX3F0iZw+uIRETEn5w/DAs7wW/VIDA5NFsGVT5Kkkm8yATdN99AyZLXnmT75ZfuclyRhE4VeSIiIiLxaf377jKnOj9BxvJeRyMiIv4iIhy2DIcVL7kJvFu/hgL3qXUDkDo1TJkCFSvCkSNXTrK1X0/v3tC+vZcRikSfKvJERERE4svun2HZ81DhHcjT2utoRETEXxz6C36rAou7QaEO0GoDFLxfSbzL5M7tJvMur8qzgRa33+4uvxVJLJTIExEREYkPx1bAX/dBoYeh5LNeRyMiIv7g3AGY/zBMqwnJMkOLFVDpPQjRtIaoVK4Mo0f/83Xhwu6yW1t+K5JY6H9XERERkbh2bj/MagWZKkPVT1QhISIiNyciDNYPgknF4MAfUGscNJgG6Ut5HVmCd9dd8Oz/X08bNsxddiuSmCiRJyIiIhKXws7B7DYQGAK1foCgZF5HJCIiidmBmTClIix/Hop1gZbrIN9dukgUAw8/7H7Mnt3rSERiTsMuREREROJyQu3CR+HkemgyH1Jk8ToiERFJrM7ugWXPwo7vIGdzqP0jpCvqdVQiEs+UyBMRERGJK6tfh51jod4USF/S62hERCQxCr8IGwbC6tcgeTaoMxFyt1IFnkgSpUSeiIiISFzY8T2segWqDIWcjb2ORkREEqN902BJVzizA0r1hpLPQ3BKr6MSEQ8pkSciIiIS2w7/DQsednsXFXvS62hERCSxscTd0p6w60fI08at7E5T0OuoRCQBUCJPREREJDad2QWzW0O2ulDpQ6+jERGRxCT8PKwdAGvfglR53ARermZeRyUiCYgSeSIiIiKxJfQ0zGoFyTJBze8hUG+1REQkmvZMgiXd4dx+KPMylOgBQcm9jkpEEhi9uxQRERGJDb4ImN8ezu2GJgshWXqvIxIRkcTg1BZY8jTsnQT57oaK70HqvF5HJSIJlBJ5IiIiIrFh+QuwdzI0mAFpC3sdjYiIJHRhZ2HNW7BugHvcsONHjgZeRyUiCZwSeSIiIiI3a+tIWPcuVP8SstX2OhoREUnIfD7YPQGW9ICLx6D8m1C8KwSGeB2ZiCQCSuSJiIiI3IyDs+HvTlCqFxR62OtoREQkITu5ARZ3hf3ToMADUPEdSJnT66hEJBFRIk9ERETkZvoazbkDct3mVlSIiIhEJfQUrO4PGz6EdCWh0RzIVsvrqEQkEVIiT0RERORGXDwOs1pCqrxw69cQEOh1RCIikhCX0e74HpY94/bEq/QhFHlcU81F5Ibp1UNEREQkpiLCYO49bjKv2SIISeN1RCIiktAcXw2Lu8DBWVD4UbdyO0U2r6MSkUROiTwRERGRmFryNByaDY1mQ6o8XkcjIiIJycUTsOoV2DgEMlaEJgshSzWvoxIRP6FEnoiIiEhMbBwKm4ZCze8hc1WvoxERkYTCFwHbvoblz4MvHKoOg0IdIDDI68hExI8okSciIiISXft+hyXdoWw/yH+319GIiEhCcXSZu4z28Hwo2hnK9YfkmbyOSkT8kBJ5IiIiItFxYh3MbQf57oYyfb2ORkREEoILR2FlH9j0CWSpDs0WQ6ZKXkclIn5MiTwRERGR6zl/2J1Qm64UVB8BAQFeRyQiIl6KCIetI2DFCxAQDNVHQsH2mmAuInFOiTwRERGRawm/AHPucCfV1vkJglJ4HZGIiHjp8N+w+Ck4tgyKdYWyr0Ky9F5HJSJJhBJ5IiIiIlfj88Gizu7JWuN5kDK71xGJiIhXzh9yK/C2fAHZ6kLzZZChrNdRiUgSo0SeiIiIyNWsGwBbR0GdiZCxnNfRiIiIF6wi23rgrewLwamgxhjIf4/aLIiIJ5TIExEREYnKrp9geW+oOADytPI6GhER8cLBue402pNroXgPd9hRSBqvoxKRJEyJPBEREZF/O7Yc/rofCneAEj29jkZEROLbuX2w7HnY/g3kaAw1v4P0JbyOSkREiTwRERGR/5y8zWoFmatBlY+1dEpEJCmJCIUNg2GVDbDICLV/gDxtdSwQkQRDiTwRERGRSGHnYHYbCEzhnrwFJfM6IhERiS/7/3CX0Z7eCqWeh1K93Z54IiIJiBJ5IiIiIsYXAQsehpMboekCSJ7J64hERCQ+nNkFy56FnWMhV0uo+wukLex1VCIiUVIiT0RERMSs6ge7foD6UyFdca+jERGRuBZ+AdZ/AKv7Q8ocbgIvd0uvoxIRuSYl8kRERES2j4HVr0HVYZCjkdfRiIhIXNs7FZZ0g7O7ofSLUPJZCErhdVQiItelRJ6IiIgkbYcXwIJHoFg3KNrZ62hERCQund4GS3vA7omQ905oMA1S5/c6KhGRaFMiT0RERJKuMzvd4RbZ60Ol972ORkRE4nKY0bp3Ye3bbuKu/m+Qs4nXUYmIxJgSeSIiIpI0hZ6CWa0geWao+R0E6m2RiIjf8flgzy+w5Gm4cBDK9oPiT2squYgkWnrHKiIiIklPRDj8dT+c2wtN/4Zk6b2OSEREYtvJTbCkO+ybAvn/BxUHQKrcXkclInJTlMgTERGRpGdFb9j3GzT8A9IU9DoaERGJTWFnYM2bsO49SFsMGv4J2et5HZWISKxQIk9ERESSli1fuCd3t34FWWt6HY2IiMTmMtpd42FpTwg9CRXehWJPQmCI15GJiMQaJfJEREQk6TgwE/7uDKVegIIPeB2NiIjElhNrYXE3ODADCj4EFd6BlNm9jkpEJNYpkSciIiJJw6nNMOdOyNMayvf3OhoREYkNVnm36jXYMAgylIXG8yBrDa+jEhGJM0rkiYiIiP+7eAxmtYTUBdwltQGBXkckIiI3u4x2+7ew/DkIPw9VPoLCnSAwyOvIRETilBJ5IiIi4t8iQmHu3W7VRoMZEJza64hERORmHFsJi7vAoblQ5DEo9wakyOJ1VCIi8UKJPBEREfHvio0l3eHQPGg0G1Ll9joiERG5URePw8qXYdNQyFQFmi6EzFW9jkpEJF4pkSciIiL+a+MQ2DQMao2DzFW8jkZERG6ELwK2joTlve0LqDYcCj2iNgkikiQpkSciIiL+ae9UWPo0lOsP+e7yOhoREbkRRxa7y2iPLoKiT0K51yBZRq+jEhHxjBJ5IiIi4n+Or4F590D++6D0i15HIyIiMXXhCKx4ETZ/BllrQrOlkLG811GJiHhOiTwRERHxL+cPwaxWkL4M3PIZBAR4HZGIiERXRDhs+QxWvASBydxJ4wXu12u5iMj/UyJPRERE/Ef4BZjT1s4Eoc4ECErhdUQiIhJdh+a7y2iPr4Di3aHsKxCSzuuoREQSFCXyRERExH8m1P7dCY6tgCZ/QYpsXkckIiLRce4ArOjtDrTI3gCar4AMpb2OSkQkQVIiT0RERPzD2ndg+zdQ52fIUNbraERE5HoiwmDTx7DyZQhJC7XGQt67tIxWROQalMgTERGRxG/XBFjxAlT6AHLf5nU0IiJyPQdmwZKucHI9lHjWHUwUksbrqEREEjwl8kRERCRxO7oM/moPRTpB8ae9jkZERK7l7F5Y9izsGAM5m0Kt8ZCumNdRiYgkGkrkiYiISOI+IbQJtVmqQ5UhWo4lIpJQhV+EDYNg9WuQPAvU+Qlyt9brtohIDCmRJyIiIolT2FmYfTsEp4Ja4yAwxOuIREQkKvunw+KucHoblOoNpXpBcEqvoxIRSZSUyBMREZHExxcB8x+C01ugyQJInsnriERE5N/O7ISlPWHXD271Xb1fIU0hr6MSEUnUlMgTERGRxGflK7D7J6j/m3oriYgkNOHnYd37sOYNSJkb6v4KuVt4HZWIiF9QIk9EREQSl22jYU1/qPYp5GjgdTQiInK5Pb/Cku5wbh+U6QslekJQcq+jEhHxG0rkiYiISOJxaD4sfNSdTmtTakVEJGE4vRWWPA17foF87aDie5A6n9dRiYj4HSXyREREJHE4swPmtIEcDd0TRBERSRiDh9a+496s/12D6e7rtIiIxAkl8kRERCThCz0FM1tC8mxQcwwEBnkdkYhI0ubzub1Kl/aAC0eg/BtQrCsEJfM6MhERv6ZEnoiIiCRsEeEw739w/gA0/RtC0nkdkYhI0nZyAyzuBvt/hwLtoeK7kDKn11GJiCQJSuSJiIhIwrb8edg/DRr+CWkKeB2NiEjSFXraHTa0/gNIVxIazYZstb2OSkQkSVEiT0RERBKuzZ+5J4y3fgNZa3gdjYhI0l1Gu3MsLH0Gwk5Dxfeh6BMQqNNJEZH4pldeERERSZgO/AmLnoTSL0HB+72ORkQkaTq+GhZ3hYMzoVAHqPAWpMjmdVQiIkmWEnkiIiKS8JzcCHPuhDxtoNxrXkcjIpL0XDwBq/rBxo8gYwVosgCy3OJ1VCIiSZ4SeSIiIpKwXDwGs1pBmkJw6ygICPQ6IhGRpMMXAdu+cfuTRoRC1Y+h0KOaFi4ikkAokSciIiIJh500zrkLws64wy2CU3kdkYhI0nFsOSx6Cg7PhyKPQ/n+kDyz11GJiMhllMgTERGRhNNM3fowHV4AjedAqlxeRyQikjRcOAor+8LmTyBTNWi2GDJV8joqERGJghJ5IiIikjBs+Ag2fwq1f9AJpIhIfC2j3TICVrzgtjG45Qso+KBaGoiIJGBK5ImIiIj39kyGZT2h/JuQ9w6voxER8X+H/4bFXeDYUijWBcq+CskyeB2ViIhchxJ5IiIi4q3jq2HevVCgPZTq7XU0IiL+7fwhWPEibPkCstWGZkshYzmvoxIRkWhSIk9ERES8c/4gzGrpnkRWGw4BAV5HJCLinyLC3R54K/q4g4RqjIb89+p1V0QkkVEiT0RERLwRfh5mtwUCoPYECErudUQiIv7p0Dx3Ga1VQJfoAWX6Qkhar6MSEZEboESeiIiIeDOhduFjcHwVNJkPKbJ6HZGIiP85tx+W94JtX0GORtBiFaQv4XVUIiJyE5TIExERkfi39i3Y8S3UnQQZSnsdjYiIf4kIhY1DYOUr7gCLWuPdQUJaRisikugpkSciIiLxa+cPsOIlqDwIcjX3OhoREf9y4E9Y3BVObYKSz0Pp3hCc2uuoREQkliiRJyIiIvHn6BKY/wAU6QzFunodjYiI/zi7G5Y+Czu/h1wtoM5PkLaI11GJiEgsUyJPRERE4sfZPTCrNWSpAVU+0hIvEZHYEH4BNgyE1a9D8mxQ52fI08rrqEREJI4okSciIiJxL+yMm8QLTgO1x0FgiNcRiYgkfnt/gyXd4OxOKPUilHoOglJ4HZWIiMQhJfJEREQkbvkiYP6DcGYbNFkIyTJ6HZGISOJ2ejss7QG7f4I8baH+b5CmgNdRiYhIPFAiT0REROLWyr6w+2doMA3SFfU6GhGRxCv8PKx91538nSof1JsKuZp6HZWIiMQjJfJEREQk7mz7Gta8CdU+g+z1vI5GRCRx8vlgzyRY+jScPwBlX4XiPSAomdeRiYhIPFMiT0REROLGoXmwsCOUeAaKdPQ6GhGRxOnUZljSHfZOhnz3QKX3IFUer6MSERGPKJEnIiIicdO/aXZbyNEEKrzjdTQiIolzSNCat2DdAEhbFBr+Adnrex2ViIh4TIk8ERERiV2hJ2FWS0iZE2p+C4FBXkckIpK4ltHu+tEdZhF6wr0YUuwpTfsWERGHEnkiIiISeyLCYO69cOEQNP0bQtJ6HZGISOJxYh0s6Qb7p0PBB90kXsocXkclIiIJiBJ5IiIiEnuWPQsH/oBGMyF1fq+jERFJHEJPwerXYP1AyFAGGs+FrDW9jkpERBIgJfJEREQkdmz6FDYMghqjIUt1r6MREUkcy2h3jHEvgoSdg8qDoMjjakkgIiJXpUSeiIiI3Lz9M2DxU1CmLxS4z+toREQSvuOrYHEXODgHCneE8m9AiqxeRyUiIgmcEnkiIiJyc05ugDl3Qd47oOyrXkcjIpKwXTwOK1+BTUMhYyVosgCyVPM6KhERSSSUyBMREZEbd+EozGoFaYtC9ZEQEOh1RCIiCZMvAraOguW9bDIQVP0ECnfQ66aIiMSIEnkiIiJyYyJCYe5dEH4OGs2C4FReRyQikjAdXQKLusDRv6HIE1DuNUieyeuoREQkEVIiT0RERG6sQfuip+DwQne6YsqcXkckIpLwXDgCK/rA5k8haw1otgQyVvA6KhERScSUyBMREZGY2zAQtnwOtX+ETBW9jkZEJGGJCHdfI1e8CIEhcOsoKNAeAgK8jkxERBI5JfJEREQkZvZMgqXPQIW3IG8br6MREUlYDi9wp9EeWw7Fu0OZlyFZeq+jEhERP6FEnoiIiETfsZUw739Q8EEo+bzX0YiIJBznD8Ly3rD1S8heH5qvgAylvY5KRET8jBJ5IiIiEj3nDrgTajNWhGqfaomYiIiJCINNw2BlXwhOAzW/g3x36zVSRETihBJ5IiIicn3h52FOWwgIcvviBSX3OiIREe8dnO0uoz25Hko8A6VfgpA0XkclIiJ+TIk8ERERuf6E2gWPwok10GQ+pMjidUQiIt46uxeWPw/bR0OOJlBrHKQr7nVUIiKSBCiRJyIiIte25g3Y+R3UnQzpS3kdjYiIdyJCYcMgWNUPkmeG2hMgz+1aRisiIvFGiTwRERG5up3j3L5PlQdDrqZeRyMi4p39M2BxVzi9FUr1cm/BqbyOSkREkhgl8kRERCRqRxbB/Aeh6JNQvIvX0YiIeOPMTlj2rHthI3crqDcJ0hTyOioREUmilMgTERGR/zq7G2bfDllrQ+VBXkcjIhL/wi/A+vdh9RuQMifUnQS5b/M6KhERSeKUyBMREZErhZ2BWa0hJD3UGguBersgIknM3imwuBuc2wNl+kCJnhCUwuuoRERElMgTERGRy/gi4K/2cGYHNF0IyTJ4HZGISPyx/ndLesCenyHvXdBwBqTO53VUIiIilyiRJyIiIv9Y8RLs/RXqT4O0RbyORkQkfoSdg7Vvw9p3IE1BqP875GzsdVQiIiL/oUSeiIiIuLaOck9kbxkB2et6HY2ISNzz+WD3RFjaAy4chvL9oVg3CErmdWQiIiJRUiJPRERE4OBc+PsxKPkcFH7E62hEROLeyY2wpDvsmwr574OKAyBVLq+jEhERuSYl8kRERJI66wk1py3kbA7l3/I6GhGRuBV6Gta84U6kTVcCGs2CbHW8jkpERCRalMgTERFJyi6egJktIWVuqDEaAoO8jkhEJO6W0e4cB8uegdCTUPE9KPqkJnOLiEiioqOWiIhIUhURBvPugYtHoenfEJLG64hEROLG8TWwpCsc+BMKPeJWH6fM7nVUIiIiMaZEnoiISFK1tCccmOkuK0udz+toRERin1XereoHGz6CDOWgyXzIUt3rqERERG6YEnkiIiJJ0aZhsHEw1BgDWW7xOhoRkdhfRrv9G1j2PERchCpDoHBHtQ8QEZFET4k8ERGRpGbfNFjcFcq+CgXu9ToaEZHYdWw5LO4Ch/6CIp2g/BuQPLPXUYmIiMQKJfJERESSkhPrYW47yNcOyrzsdTQiIrHn4jFY0Rc2D4NMVaHZIshU2euoREREYpUSeSIiIknFhSMwqyWkKwG3jICAAK8jEhG5eb4I2PolLO8NBEC1z6HQQxAQ6HVkIiIisU6JPBERkaQg/CLMudPtFVXnJwhO6XVEIiI378gidxnt0cVQ9Cko9xoky+B1VCIiInFGiTwREZGk0PR90RPuiW7jeZAyh9cRiYjcnPOHYcWLsOVzyFoLmi2DjOW8jkpERCTOKZEnIiLi79a/7y47s0q8jOW9jkZE5MZFhMOW4bDiJQhKATW+gfz/U6sAERFJMpTIExER8We7f4Zlz0OFdyBPa6+jERG5cTaFdvFTcHw1lHgayvSFkHReRyUiIhKvlMgTERHxV8dWwF/3QaGHoeSzXkcjInJjzu13B1lsGwXZG0KLlZC+pNdRiYiIeEKJPBEREX898Z3VCjJVhqqfaNmZiCQ+EaGwcSisegVC0kOtcZD3Tr2eiYhIkqZEnoiIiL8JOwez20BgMqj9IwQl8zoiEZGYOTATFneFUxvdiuLSL0Jwaq+jEhER8ZwSeSIiIv42oXbho3ByPTRZAMkzex2RiEj0nd0Dy56FHd9BzubuxYh0Rb2OSkREJMFQIk9ERMSfrH4ddo6FelMgfQmvoxERiZ7wi7BhIKx+DZJngzoTIXcrLaMVERH5FyXyRERE/MWO791eUlWGQs7GXkcjIhI9+353l9Ge3QmlekPJ5yE4pddRiYiIJEhK5ImIiPiDw3/DgoehWBco9qTX0YiIXN+ZHbC0J+z6EfK0gfpTIU1Br6MSERFJ0JTIExERSezO7ILZrSFbXaj0odfRiIhcW/h5WDsA1r4FqfK4rQByNfM6KhERkURBiTwREZHELPQ0zGoFyTJBze8hUId2EUnA9kyCJd3h3H4o8zKU6AFByb2OSkREJNHQu30REZHEyhcB89vDud3Q9G9Ilt7riEREonZqi5vA2/sr5LsbKr4HqfN6HZWIiEiio0SeiIhIYrX8Bdg7GRrMgDSFvI5GROS/ws7Cmrdg3buQtoj7epWjgddRiYiIJFpK5ImIiCRGW0e6J8bVR0K22l5HIyJyJZ8Pdk+AJT3g4jGo8LY7jCcwxOvIREREEjUl8kRERBKbg7Ph705QqhcUesjraERErnRiPSzpBvunQYEHoOI7kDKn11GJiIj4BSXyREREElufqdltIddtUP5Nr6MREflH6ClY/Tqs/xDSl4ZGcyBbLa+jEhER8StK5ImIiCQWF4/DrJaQOh/c+jUEBHodkYiIu4x2x3ew7Fm3J17lgVDkcU3RFhERiQM6uoqIiCQGEWEw9x43mddsEYSk8ToiERE4vhoWd4GDs6Dwo1D+LUiR1euoRERE/JYSeSIiIonBkqfh0GxoNBtS5fE6GhFJ6uyiwqpXYeMQyFgRmiyELNW8jkpERMTvKZEnIiKS0G0cCpuGQs3vIXNVr6MRkaTMFwHbvoblz4MvHKoOcyvxtNRfREQkXiiRJyIikpDt+x2WdIeyr0H+u72ORkSSsqNL3WW0RxZCkc5Q7nVInsnrqERERJIUJfJEREQSqhPrYG47yHcPlOnjdTQiklRdOAor+8CmTyBLdWi6GDJV9DoqERGRJEmJPBERkYTo/GF3Qm26UlD9CwgI8DoiEUlqIsJh6xew4kUICIbqI6Fgey2jFRER8ZASeSIiIglN+AWYc4c7qbbOTxCUwuuIRCSpObzQXUZ7bBkU6wplX4Vk6b2OSkREJMlTIk9ERCQh8flgUWf35LnxPEiZ3euIRCQpOX8IlveGrSMgW11ovhwylPE6KhEREfl/SuSJiIgkJOsGwNZRUGciZCzndTQiklRYBbD1wFvZF4JTQY0xkP8eLesXkSTD5/Nx8eJFr8OQWBASEkJgoP+2gVAiT0REJKHY9ZNbCVPxPcjTyutoRCSpODjHXUZ7ch0U7wFl+kJIGq+jEhGJN6GhoWzbto3w8HCvQ5FYkilTJrJly0aAH16QUiJPREQkITi2HP66Hwo/CiV6eB2NiCQF5/bBsudh+zeQozHU/B7Sl/A6KhGReK/E27dvH0FBQeTNm9evK7mSyr/n2bNnOXDggPN19uz+16ZGiTwREZGEcDI9qxVkrgZVhmopm4jErYhQ2DAYVtkAi4xQ+0fI00avPSKSJFkV3pkzZ8iTJw8pU6b0OhyJBSlSuIPiLJmXNWtWv0vOKpEnIiLipbBzMLsNBKaA2j9AUDKvIxIRf7Z/BizuCqe3QqnnoVRvtyeeiEgSFbmc1vqqif9IlSrVpWXTyZMnx58okSciIuIVXwQseBhOboSmCyB5Jq8jEhF/dWYXLHsGdo6DXC2h7i+QtrDXUYmIJBj+2EstKQvw439PJfJERES8sqof7PoB6k+FdMW9jkZE/FH4BVj/AazuDylzugm83C29jkpERERukBJ5IiIiXtg+Bla/BlWHQY5GXkcjIv5o71RY0g3O7obSL0LJZyHI7RskIiIiiZN/dfwTERFJDA4vgAWPQLFuULSz19GIiL85vc3tvTmzOWQoBy3XQZk+SuKJiMTT1NRXXnmFXLlykTp1aurUqcPq1asv3X/u3DnatWtH0aJFnSEMffr0ueq2atasydixY53PV65c6WzLtmnbfvXVV519Rfruu++oXbs26dKlc5aVhoWFXTfWY8eOcf/995M+fXoyZMjgfH78+PErHjN+/HhKlCjhDAIpWbIkP/744zW3uWLFCpo3b06OHDmcOKZPn35D+5WrUyJPREQkPp3Z6Z5gZ28Ald73OhoR8bfhOStfhV9Lwcn1UP83qD0eUuf3OjIRkSTjvffeY8SIEfz2228cPnzYScY1bdqU06dPO/dbcqtGjRoMHz6catWqXXU7e/fuZfny5bRo0YJTp04527Bt2TZt259//jkDBw689PiMGTPy5JNPXvG962nfvr0z2XXLli1s3rzZ+fyhhx66dP/ChQudx7zxxhucPHmS/v37O0m3xYsXX3WbyZIl44477mDSpEk3vF+5tgDf5SlcEXEdXwOTy0CL1ZChtNfRiIi/CD0F02qBLxya/AUh6byOSET8gb2d3/MzLHkaLhyCMq9A8e6agi0ichVr1kCZMmCFckWKXGDr1q0UKlQoVqabFixYkKeffpru3bs7X1tlXM6cOfnggw944IEHrnhsvXr1qFWrlpMg+7ehQ4fy+++/M3HiREaNGsVzzz3nJPeCg90OaYMGDeKjjz5ykmGXmzlzJvXr13emtUY+Nio7duygQIECTrKwfPnyl6rpKlSo4NyXL18+HnnkEadSbsKECZee17ZtWzJlysQXX3xx3d+FJS2nTZtGo0aNYrTf2HDhQuz+uyYkqsgTERGJDxHh8Nf9cG6v22xeSTwRiQ0nN8HM29xK3yy3QssNUOo5JfFERDxw4sQJtm/ffkWlnSXTKlasyLJly2K0LVvCeueddzqfW9LLtnF5Yq5q1apOosoq5W6EbdMSXJHJNGOfW0Wd3Rf5mH9XDdp+L/9ZrAqwZcuWsbpfuTYNuxAREYkPK3rDvt+g4R+QpqDX0YhIYhd2Bla/Aevfh7TFoOFMyF7X66hERJK0yKSa9X27nC17jUnCzZbPzp8/nx9++OHSdqPaZuR91hfvRmK1HnX/ZvuJjPVq+738Z/n4449jfb9ybUrkiYiIxLUtX8C69+DWryBrTa+jEZHEvox213hY2hNCT0KFd6HYkxAY4nVkIiJJXmRC7d+DG2y4Q+7cuaO9HVtOa4MrIpNott3du3f/Z5uX7/NGYrUKwn+z2CO3aR+j+lludJ/R3a9cm5bWioiIxKUDM+HvzlDqBSh4ZV8UEZEYObEW/mgMc++GHI2g5UYo0V1JPBGRBMIqzaz/26JFiy59z3rkRS6NvZFltcb6x9ly1ssn0drACev/dqPJL9um9ZGzabiR7POLFy8690U+5vKfJXK/MflZbmS/cm1K5ImIiMSVU5thzp2QpzWU/28TYxGRaLHKu6XPwuTycPEoNJ4H1b+ElNm9jkxERP7FesbZ5NrVq1dz7tw5XnnlFUJCQpwhEZEskXX+/HkiIiIIDw93PrdElrFqtT///JM2bdpcerxNgQ0KCnK2Zdu0bds+nnrqqUuP+fd2Lt9HVPLnz+9MxH322Wedpbx2s89btWp1aeDE448/zuTJk51hFzY8wz5OmTKFzp07X/Xnt3mqtl+7GXuefR6ZhIzOfuXalMgTERGJCxePwayWkLqAu6Q2QIdcEbmBZbTbRsOkErB1BFQZDE0XQdYaXkcmIiJXYUmphx9+2JnUmjlzZubMmcPUqVNJkybNpccUL16clClTOve9/fbbzudNmjRx7ps0aZIzUCJbtmyXHp82bVp+++03Zs+e7WzTtt2hQwd69Ohx6TFff/21s52mTZs6X9v+7Gt7ztXYc7JkyULhwoWdW9asWfnqq68u3V+9enXnMS+88IITg3385ptvnPgiWVKvefPml762ybO2X7sZS9rZ55dP5r3efuXaAnyWLhWRKx1fA5PLQIvVkKG019GISGITEQozW7jL4Jr+Dami3xNFRMRxbAUs7gKH5kGRx6DcG5Aii9dRiYj4hTVroEwZWL0aihS54Ex/tWWqNk3Va7aktm7dunTr1s3rUBK1CxcS1r9rbNKwCxERkdhk18eWdHdPvhvPURJPRGLm4nFY2Rc2fQyZqroXAzJX8ToqERGJJ1YFd/fdd3sdhiRgSuSJiIjEpo1DYNMwqDUOMlX2OhoRSSx8EbB1JCzvbV9Atc+g0MNali8iksQ899xzXocgCZwSeSIiIrFl71RY+jSU6w/57vI6GhFJLI4sdpfRHl0ERZ+Ecq9BsoxeRyUiIiIJkBJ5IiIisdVbc949kP8+KP2i19GISGJw/jCsfAk2fwZZa0KzpZCxvNdRiYiISAKmRJ6IiMjNOn8IZrWC9GXgls8gIMDriEQkIYsIhy2fwYqXIDCZO9m6wP167RAREZHrUiJPRETkZoRfgDlt7cwc6kyAoBReRyQiCdmh+e4y2uMroXh3KPsyhKTzOioRERFJJJTIExERuZkJtX93gmMroMlfkCKb1xGJSEJ17gCs6O0OtMjeAFqsgPSlvI5KREREEhkl8kRERG7U2ndg+zdQ52fIUNbraEQkIYoIg41DYdX/V97VGgt579IyWhEREbkhSuSJiIjciF0TYMULUOlDyH2b19GISEJ0YJa7jPbUBijxrDsIJySN11GJiIhIIhbodQAiIiKJztFl8Fd7KNLJ7XElInK5s3th3n0wox6kygMtVkOFN5XEExERv7d9+3YCAgLYvHmz8/Xo0aMpXry412H5FSXyREREYnqCbhNqs1SHKkO0PE5E/hF+EdYOgEnF4fB8qPMT1JsM6Yp5HZmIiIgn7r//fjZs2OB1GH5FS2tFRESiK+wszG4Nwamg1jgIDPE6IhFJKPZNgyVd4cwOKNUbSj4PwSm9jkpERCTeXLx40esQkgQl8kRERKLDFwHzH4LTW6HJAkieyeuIRCQhOLMTlvaEXT9Antuh3hRIU9DrqERE5CacP3+eLVu2xOs+CxcuTIoUKa77uHr16lGmTBn27t3LtGnTyJ49Oy+//DIPPvigc/+vv/7qfG1LW+2+Tp060bNnTwIDA3nmmWec540ZM8Z57COPPMJXX33F4cOHyZgxIwsXLqRBgwYcPXqU5MmTs2fPHp577jlmzZpFaGgoDRs25KOPPiJr1qxXxGLP/+2337jnnnvo3bv3FfGOHDmSPn36sHv3bufrhx9+2Pn9ZsmSxYkjODjYifH111+/9Jz169fz7LPPsmjRIuf+22+/nQEDBpA6depY/Z0nVkrkiYiIRMfKV2D3T1D/Ny2TExEIPw/r3oc1b0DK3O4S2lzNvY5KRERigSXxLEEVn1avXk3p0qWj9dgvvviCsWPHOrfff/+dNm3aOInAZMmS0bZtW7755hvuuOMOVqxYQatWrZxk2NNPP03jxo154IEH8Pl8Th87e649b/r06bRr185JDNapU8dJ4l24cMFJ3LVs2ZKNGzc6z3niiSe47777nMdF+vLLLxk3bhzffvutk6A7ePDgdeOfMGECo0aNYtCgQfz999/OPi2BWL9+fScpWLt2bV588UV++OEHTp06xb333uvE/9lnn93U79hfKJEnIiJyPdtGw5r+UO1TyNHA62hExGt7foUl3eHcPijTF0r0hKDkXkclIiKxxJJblliL731GV4sWLZwEXeTnlrwbMWKEk7C77bbbuPvuu537Kleu7FTUffLJJ04irG7duk5ibOnSpU6yLiQkxEnOWTWdJfIssWfbiqzss8daJZwl/czbb79Nnjx5nOo6+2hat27txGBSpUoVrfhr1qzpJOfMrbfeSoUKFZyEniXyrEKwSJEi9OjRw7nf4uzXr5+T6LOfIygoiKROiTwREZFrOTQfFj4KxXu4U2pFJOk6tQWWPA17J0G+u6Hie5A6r9dRiYhILLMlrtGtjvNCwYIF//O1JecskVeqVKkr7rOk2M6dO53PU6ZMSa1atZyEnSXImjRp4tw++OADJ2m3YMEChg0b5jx206ZNHDhwwFlyezl7nm0vMpH371iiI1euXFd8bUtmbf+R+12yZAkZMmS4dH9kBeH+/fvJnTs3SZ0SeSIiIldjTevntIEcjaDiAK+jEREvB92sfRvWvgtpCkGD6ZCjoddRiYhIErV9+/b/fG2JNauw+3dvP/s6X758l7625bVTp051EnKPPfaYk7C0RNnHH3/s9L6LTGDmyJGD/PnzX7dXoPXei022X0s2/vHHH7G6XX8Su79xERERfxF6Cma2hOTZoOa3EKgyfpEkx+eDXRPg11Kw/kMo/wa0WKEknoiIeGry5MnO0tfw8HAnKWc952xwRYcOHZzvW285u2/ZsmXO0lgbJhHJKvD++usv5s2b5/TAi/zeW2+95ST5IlmPPRtw0bdvX06cOOF8z/rfff/993H6s9nPYXFbYvHs2bNOknHXrl389NNPcbrfxESJPBERkX+LCId5/4PzB6DuLxCSzuuIRCS+ndwAfzaDOXdA1trQaiOUfAYCQ7yOTEREkjhL2NnAC1t++tRTTzm942xAxC233ML48eN54403nCWx1veuW7dudO/e/dJzrR+dPc+GeUQuX23atKmTrLOEXqS0adMyf/58Zxlt2bJlSZcuHTVq1GD27Nlx+rNZ9aDt1wZqWN9Ai9HiW7VqVZzuNzEJ8Fl6U0SudHwNTC4DLVZDhoTbG0FE4siSnrBpKDT8E7LW8DoaEYlPoafd4TbrP4B0JaHKEMhW2+uoREQkFq1ZAzaU1uZZFClyga1bt1KoUCFnuWlCV69ePWfpaf/+/b0OJUG7cCFx/bvGhHrkiYiIXG7zZ7DhQ7j1GyXxRJISu7a943tY9iyEnYaKH0DRzhCot8siIiKScOidiYiISKQDf8KiJ6H0S1Dwfq+jEZH4cnw1LO4KB2dCoQ5Q4S1Ikc3rqERERET+Q4k8ERERc3IjzLkT8rSBcq95HY2IxIeLJ2DVq7BxMGSsAE0WQJZbvI5KRETkqmbOnOl1COIxJfJEREQuHoNZrSBNYbh1FARoFpSIX/NFwLZvYPnzEBEKVT+GQo9qOrWIiIgkeErkiYhI0mYn8XPugrAz7nCL4FReRyQicenYclj0FBye7/bAK/c6JM/sdVQiIiIi0aJEnoiIJO3m9tYX6/ACaDwHUuXyOiIRiSsXjsLKvrD5E8hUDZothkyVvI5KREREJEaUyBMRkaRrw0ew+VOo/YNO6EX8eRntli9gxQsQEAS3fAEFH9QSehEREUmUlMgTEZGkac9kWNYTyr8Jee/wOhoRiQuH/4bFXeDYUijWBcq+CskyeB2ViIiIyA3TpUgREUl6jq+GefdCgfZQqrfX0YhIbDt/CBY+Br9Xh+CU0HwZVB6oJJ6IiCRpI0eOJE+ePCQEadKk0QTeG6REnoiIJC3nD8KslpCxHFQbDgEBXkckIrElIgw2DoVfisHeyVBjNDScCRnKeh2ZiIhIohcQEMD06dNjJXl4+vRp6tWrF4vRJR1aWisiIklH+HmY3da9jlV7AgQl9zoiEYkth+a502hPrIESPaBMXwhJ63VUIiIiIrFKFXkiIpJ0JtTaUrsTq6HuL5Aiq9cRiUhsOLcP/noQptVy/65brIKK7yqJJyIiN8QWa3h1i64hQ4ZQuHBh0qZNS/bs2Xn44YejrJjbvn27873Nmzdf8fxBgwY5VXKZM2emQ4cOTnWc8fl8vPzyy859tm37+OKLLzr3lS5d2vnYqlUrZ1ls8+bNna/HjRtH5cqVyZgxI1myZKF169Zs27bNuW/OnDl07tyZvXv3Os+x2+jRo6OM9ddff3W2kz59eooVK8Z7771HRETEZf8uAQwePJiaNWs62ylbtixz584lKVIiT0REkoa1b8GOb6Hm95DBfSMiIolYRCis/xB+KQ4HZ7nTp+v/DulLeB2ZiIhInNm0aRPPP/88EydO5NSpU2zZssVJxkXX/v37Wb58ORs2bGDlypWsWrWKHj16OPdZYm3EiBH89ddfzrbtfkvcmTVr1jgff/nlFyfxN2XKFOdrS/jZcw4fPsz69eudZOB9993n3Fe7dm0++eQTcuXK5TzHbvfff/9/Ylq0aBFt27alV69eHDlyhDFjxvDBBx/w0UcfXfG4zz//nFGjRnH8+HEaNmwY5baSAiXyRETE/+38AVa8BJU+hFzNvI5GRG7WgT9hSgVY3huKd4eW69zp0+p5KSIifi44ONhJllli7eTJk051Wp06daL9fHvuwIEDSZ06Nblz5+a1115zkmPh4eEkS5aM8+fPO9s+d+4cmTJl4tZbb73m9po1a0b58uUJCgpyKvJsewsWLHASgdFlCbrbbruNu+++2/n5rDLvueeec5KAl3vmmWcoUqSI85jHHnuMnTt3cuDAAZIaJfJERMS/HV0C8x+AIp2hWFevoxGRm3F2N8y9B2Y0gNQF4LY1UP51CE7ldWQiIiLxomDBgnz33Xd8+eWX5MuXj6pVqzoVbNFlS2Bt+erl2wsNDXUSYnXr1uXdd9/l7bffdpbsWoJw2rRp19zerFmznOq4nDlzki5dOmcb5uDBg9GOadeuXc5S4ctZws4SdZezyr5Ilog0MUkY+gsl8kRExH+d3QOzWkOWGlDlI1XriCRW4RdgzdvuMtqji90+l/V+hbRFvI5MRET8sK2yV7fouv3225k6daqznNUq12yJ6caNG53qvDNnzlx6nPWm+7djx45x4sSJK/rohYSEOIk7Y8t0LTl36NAh2rRp4yytjUyWWZ+6y128eJGWLVs6VXm2f6sQtOe6v0f3BwoMvH7aKW/evM4S4cvZ15aolP9SIk9ERPxT2Bk3iRecBmqPg8AQryMSkRux9zeYXA5W94NSvd0qvNwtvY5KRETEE9bbbvLkyU6/OVtiGlldZ0tbq1SpwsiRI53lsVZh169fv/8835JxtkTVEn6W6HvllVd44IEHnOf//fffzJ4921lWa8tsrf+dPd7uMzly5HD2f3kizx5rVX72WNtenz59rtifPccSjtb77moseWjDLn744Qdnie+yZcsYMGAAnTp1isXfnP9QIk9ERPyPLwLmPwhntkHdSZAso9cRiUhMnd4Os9vCzGaQvjTctg7K9oWgFF5HJiIi4hlLnr3xxhtOfztbympJua+++spZmjp06FBnmIX1qmvcuLGToPs3S6zZxFebDFumTBlKlizp9Mwzlhzs2bMn2bJlI0OGDAwfPpwJEyaQKpXbwuKtt97inXfece6zSjyrALT+dv379780ybZdu3ZX7K9BgwZOBaHtz5737bff/iemW265hfHjxzs/lyUFbRvdunWje/fucfZ7TMwCfJH1jiLyj+NrYHIZaLFa0y1FEiMbbLH2XWgwDbLX8zoaEYmJsHOwboA7aTpVPqj8EeRq6nVUIiLiR2wAa5kysHq19WK7wNatWylUqBDJkyf3OjSJJRcu+O+/a7DXAYiIiMSqbV/Dmjfhls+VxBNJTOza8p5fYMnTcOEglO0HxZ+GoGReRyYiIiKSYCiRJyIi/uPQPFjYEUo8A4Uf9ToaEYmuU5thSXfYOxny3wsVB0CqPF5HJSIiIpLgKJEnIiL+1U8rRxOo8I7X0YhIdIfSrHnLXUqbtig0/FOVtCIiIiLXoESeiIgkfqEnYVZLSJkTan4Lge5kLRFJwMtod/0AS3tC6Ak3+V7sKU2XFhEREbkOTa0VEZHELSIM5t4LFw5B3Z8hJK3XEYnItZxYB382gbntIHsDaLkBSjytJJ6IiCRui7vB5PLuBWaROKREnoiIJG7LnoUDf0CdiZA6v9fRiMjVhJ6CZc/B5HJw4TA0ngu3joSUObyOTERE5OYcWQwbB8OJtbC6v9fRiJ/T0loREUm8Nn0KGwZBjdGQpbrX0YjI1ZbR7hjjJt3DzkHlQVDkcS2BFxER/znO2cCmrDUh3z2wzIauPQbpinodmfgpVeSJiEjitH8GLH4KyrwMBe7zOhoRicqxlTC9LvzVHnK1hFYbodiTSuKJiIj/2PEdHJ7vXqgq+oQ7vMkuXonEESXyREQk8Tm5AebcBXnvhLKveB2NJGEXLlygS5cuFC1alLJly9K+ffv/PObLL78kICCAn376KcptbN++naCgICpUqHDptmXLlkv3DxgwgDJlylCqVCnatm3L8ePHo9zOiBEjnMdUrFiRpUuXXvr+yy+/zLfffku8ungcFneHqZUg/Dw0WQC3DIcUWeM3DhERkbievr78eSj0MGSqDIHBUGkg7PkZ9v3udXTip5TIExFJpI4cOXLFiX+xYsUIDg7m6NGjzv2LFi2iZs2alC9f3rn/jz/+iHI7e/fupWnTphQvXpxy5cpx5513cujQoUv3N2nSxPm+baN27dosW7Ysyu1MnjzZSTbY7bfffrsiufDmm2/G3g9+4SjMauVe7aw+EgJ0KBPv9O7d20nSbdy4kVWrVvHee+/9J0n32WefUb36tZd+p02bluXLl1+6FS5c2Pn+tGnTnETg/PnzWbt2LZUrV+all16Kchuvv/6683c/aNCgS39za9asYeXKldx3XzxVrfoiYMuX8Esx2PEtVPsUmi6ALNXiZ/8iIiLxae277sWr8pe9183ZGHK3hqU9ICI01ndZr149+vTpQ1ypVasWr776apxtX26eeuSJiCRSmTNndk74I1kCYdasWWTKlAmfz+dU7owcOZJGjRo5SQb7uGHDBlKmTHnFdqwSqG/fvs5B2zz33HPOzZ5rxo4dS4YMGZzPJ0yYwMMPP8yKFSv+E49V/Vgyz9xxxx1OcvDAgQN8/fXXTjIiVtibobl3Qfg5aDQLgq/8WUTi05kzZ/jiiy/YvXu3k8wzOXL8M7ghIiKCjh07MnjwYJ555pkb2of9rdnfpiX6TIsWLZw38EOHDv3PY+1v+fz5805cyZIlc/bfo0cPJ5EYL44ugUVd4OjfUOQJKPcaJM8UP/sWERGJb2d2wLp3oWy//w5uqvQ+/FoKNn0Cxbt6FaH4KZUxiIj4CUsoPProo5eq9ayqzpJ3xqr1LBk3ZcqU/zwve/bsl5J45pZbbnGqiCJFJvHMiRMnLiUs/i0kJISzZ89eSiIYSyK88847TqVgrDQSXvQkHF4IdX6GlDlvfpsiN8GWv1ri3KrfqlSp4lSszpgx49L9H3zwgVMVa1V012N/N1WrVqVSpUq89tprhIeHO9+3506fPp39+/c7CfrRo0dz6tSpS5W3l3v33Xdp3Lgxb7/9tpNY//jjj7ntttvInz+OpzlfOAJ/d4apVd3ed82WQNUhSuKJiIh/W9YLUuaG4t3/e1/aIlD8aVj5Mpw/7EV04seUyBMR8QN//fUXx44do2XLls7XWbJkIWfOnE41nbHldlaNd3mCLiqWPBgyZAi33377Fd9/8MEHyZs3r1O5ZxV2UbEkwkMPPeRU7Fl14KRJk5wkYbVqsbSkbv2HsOULd0Jtpoqxs02RmxAWFsaOHTucvnSLFy/mo48+4p577nEqUVevXs0PP/wQraUv9re6Z88e5+/UknZz5szh/fffd+6rX78+zz77rPO3bctzs2Z1e8xFlRy3SljrjWeVualTp2bixIlO/74XX3zRietqS3JvWES4OznaltHu/gluHQWN5kDGCrG7HxERkYTm4BzY+T1U+gCCkkf9mDJ9ICgZrIq7fs7r1q2jefPmznv/PHny8PjjjzsX3iNZBb+1vUmXLp2zauCBBx7g8OHDV7yXef7555377D3GCy+8EGexSuxRIk9ExE+q8SzZdvnJvZ3EW386a3xvPbOs6u5alXFW7fPkk0+SMWNGune/8sriV199xa5du+jfvz+9evWK8vlWjbRw4ULnZk3/rRLPHj9w4EDatWvnJBQuXrx4Yz/gnknu9K8Kb0HeNje2DZFYli9fPgIDA7n//vudr+1vrWDBgk6vPEvGWeLchmAUKFCABQsW0KlTJ4YNG/af7SRPnpxs2bI5n1uFX4cOHZznR7K/S0sU2t+WLau1N+r2hvxa7G/YKgJnzpzp9MH8/vvvnSXAf/75Z+z88IcXwO+3uJOjrcG3TaMt+ABcpWJXRETEb9iFrCXdIYf1wmt19ceFpIPyb8HmT+D4qlgPwyr0bfWNXVDcuXOnc0Fw/fr1zoX1SJag+/HHH51BWfY+wtrtdO3a9YoL8Xbh33pp2/sEO1ewx0nCpkSeiEgid/r0aecAbCf/l7MhF1OnTnWGU3zzzTfOyXzp0qWvup1u3bo5yTo74bfkRFTsjYElAmzp7rVYBZBV/9hyQEsojhs3zunpZ8sCY+zYSpj3Pyj4IJR8PubPF4kjdvW7YcOGl4a7bNu2zbmVLFmSJ554gn379jnJPLtZNd3w4cOd7//bwYMHCQ0NvTQF195wW1Iwkm3H2NJ1WzJrV86vxf6GLQZLqNuS3cjl8PZ3ba8XN+X8QVjQAX6/1T1Bab7C7QNkn4uIiCQFW7+E4yuh0ofXv4BlF7syVoQlT7ttYmKRrX6xi+R28TxVqlROhb9dQLf33vYe3NgQO2uxY+8BrNWGDen6/fd/punaQC3r42vJQLuwaEMu7KK+JGxK5ImIJHJ20m5JuxIlSlzx/ciTf2PN7m2pXYMGDa6axNu8ebMzzCKyv52xq3eWAIz0008/OQk5qxq6Gqs8spL+Zs2a3XwS4dwBd0KtvQGy6Zeq9pEE5pNPPmHAgAFO0qxNmzZ8+umn5M6d+7rPs4ScPdfMnTvXSdzZ37H1yLOr55cvg7XJ0ZaEt/utstaqW6/GltjbMhpbBm/s79AS7zZ52vrq2dc3JCIMNnzkLqPd9zvU/B4azIAMV784ICIi4ncunoAVL0LRJ6J3DAwIhEoD4cAfsHtirIZiF+AtOXf5ipsiRYo4H61Cz9jFwRo1ajiV/1bNb0tr7f1AZC9eq8Kz1QSXD86yFQeSsGlqrYiIHyyrfeyxx/7zfav+sQo4WzJr1TmWpItMqlkCwRJ01lR/3rx5zlRNSwTaoAtjB3R7vCXkbFnsuXPnnESc9c6wq39XG3hhVUW29Hb8+PHO15Y8KFSokNObw6qXbJvRFn4e5rSFwGCo/ePV+4+IeMj+/47OclVb4no5+9u7vLed3a7GlupGl11Fnz179qWvLTH/888/c1MOzobFXeDkeijxDJR+CULS3Nw2RUREEqM1/cEX7k6qja5stSD/vbDsGcjVDIJSxEoo1r/aEnbW5y4ymWeDuIwl4yxJZ+/jbWVO27ZtSZEihfNe3N5z2PmBsXYdl/fQtgSfJQglYVMiT0TEDwZdROWVV15xblHp3Lnzpc9tqmbkwfzf7Crf33//He1YbHKtNdq/3Oeff06MWTwLHoUTa6DJfEiRJebbEJGbc3YvLH8eto+GnE2h1nhIV8zrqERERLxxciNsGASFH4Mz29xbdOW7G3Z8D+sHQunesRKOTaa3ZbHW0qZfv37OBfgePXrQqlUrp7rf+uVFREQ4F9Mtibdp0ybeeuut/7TNsQFb1mvPLuS/+eabTsWeJGxK5ImISMKz5g3Y+R3UnQzpS3kdjUjSEn4RNn4Eq/pB8sxQewLkuV1L20VEJGk7uxMiQmHTx+7tRpzaFGvh2FLZadOm0bNnT6eyzpJ1LVq0cAZYGFttY4k7G4hn7W2sDUj79u2doRiRbCWNteWoW7euk/Tr2LHjpRU6knAF+K5WhiGSlB1fA5PLQIvV6v8jEt92joO5d0PlwVD86r3ARCQO7J8Oi7vC6W1QqjeUeh6CU3kdlYiISKxaswbKlIHVq62v3AW2bt3qtMuwgQ/XZMdHbiKFkiI7BKe+8edLtNkAsWj/uyYyqsgTEZGE48gimP8gFH1SSTyR+HRmJyx9BnaNh9ytod6vkKaQ11GJiIgkLGn+GQwh4hUl8kREJGE4uxtm3w5Za0PlQV5HI5I0hF+A9e/D6jcgZU6o+yvkbuF1VCIiIiJyFYFXu0MkJiWrXbp0oWjRopfW3f/bl19+6Uy5/Omnn646ka9OnTrOOn6bbtmhQwdnSmakUaNGOduuUKECFStWZPLkyVFux75vz7fbb7/9dun7I0aMcBp3ikgCFXYGZrWGkPRQa6w7qVZE4taeyfCrrSvqD2VegttWK4knIiJyFYcPH2bx4sVOT7lIZ86ccYZKrFmzxrmdPHnyqs+3PnX2GDv33bBhAxcvXnS+bx83btzI6tWrnfs3b95MaGholNuwgRaR+7LPL49t3759sfrzSsKlMyW5ab1793aSdPbiYx/3799/xf02zvqzzz6jevXqV92GNeYcMmQI5cqVc0Ze33fffbzzzju8+uqrztScrl27Otu36Ttz5851RmYfPHjwP9t5+eWXLyX57DFNmzblwIEDfP31104jUBFJgHwR8Fd7t4Fwk4WQLIPXEYn4t9NbYcnTsOcXyHsXNJwBqfN5HZWIiEiCLl45dOgQqVP/09/Oxg1Y0s2mvdrgiZlbZjJq+ijevP1NgoOuTLXYY7dt20b+/Pmdx9o5865duyhcuLBzDp0zZ07Spk3rPNa+v3v3bme7/7Znzx6KFCnifL5lyxbSp0/vJP2OHDlCsWKaLJ9UKJEnN8WuQHzxxRfOC429ABlLtkWKnHwzePBgZzT21Vg1X6SgoCCqVq3qXJGI3Ia98J06dcrZ9vHjx52pPFEJCQnh7NmzzuOTJUvmfM9GcFtSMDhY/7uLJEgrXoK9v0KD6ZC2sNfRiPivsHOw9m1Y+47b46fBNMjRyOuoREREEjQ7t7TilHz58jnnvZHCwsKcW+o0qXlzzpu8/OfLhPvCWXR8EWPuHkOONP+cF9s5qp0vWxLPZM2a1UnK2bmuncPaLZIlCy1pGBXbhj0n8vPIxJ+dH0d+Lf5PmQ25KXYVIFOmTM6y1enTp5MyZUqniq5hw4bO/R988AE1a9akcuXKMUoOfv75586obJMlSxY++eQTKlWq5OzLltzavqJio7Yfeugh5/MPP/yQSZMmkT17dqpVqxYrP6+IxLKto9zEwi0jIFsdr6MR8U8+H+yeCEt7wIXDUL4/FOsGQe4FLxEREbk6W+GVJk2aK6rxjCXfjoYdpd6X9Vi4dyHvNn6XGnlrcN8P91H+k/KMvH0kzYs2v7R8NrLQJLJ4xW5WTXf5RFVLGloSL0OGqFeoWMLOkoomb968TpGLxfHv2MS/KZEnN8WuQOzYsYNSpUrx9ttvs2zZMho3buys2bcXoB9++IHZs2dHe3v2AnfPPffQpEkT2rZt63zP1v4PGjSIv//+m5IlS/LLL784961bt+6KF0NTu3ZtFi5c6HxuFXwtWrRg6tSpDBw4kHnz5jlJPUsu/vt5IuKBg3Ph78eg5HNQ+BGvoxHxTyc3wpJusO83yH8fVBwAqXJ5HZWIiEiiYEUk1hOvePHi/7nvlw2/8MisR8iQIgPzH51P5Vxu8cqyx5fxxK9P0OLbFvSs3pM3G0avV7sl8Xbu3Okk+LJlyxblY2z5rZ0TG2tJtWnTJmd1myUbrQefJfUs2RcYqHEI/kz/unJTrLzYXiTuv/9+52sbRGFr+a2B55w5c5yrBfbCUqBAARYsWECnTp0YNmxYlNuyqxGWxLP+AJa4i2S97eyKROQLVqtWrZwmopZAvJYXX3yRl156yek/MHHiRMaNG0fmzJkZPXp0rP4OROQGe3TNaQs5m0N5t/pWRGJR6GlY/gJMLgPn9kKjWVBztJJ4IiIiMWDFIVZsYm2fVq5c6STLNm7dSMcfO9L6u9a0LNbSSdxFJvFM+hTpGX3HaL68/Us+XfIpNUbUYMfpHZeGW0Qm4ex2+ZJaWyJrjylUqFC0lsna0lw7d7bzaKvMs357lgS0HvPi35TIk5tiy15tGW3khFhr4Gk3S7o98cQTzuQcS+bZzYZdDB8+3Pl+VJV99957r7N01h5z+QuXvZAtX7780hCN+fPnO4+3UuKrsaShVfI1a9bMWaobuT1LOtqLr4h46OIJmNkSUuaGGqMhMMjriET8axntjrHwa0nYNAwqvg/NlmrpuoiIyA2wyrjy5cs7QxntdiD8AI8ueJTvN3zPN22/YWSbkaRN7g6puJydfz5c4WGWdFriVNrV+roWP23/6dKk2cjls5GVc1aJd/78eScZF51qOjuntUSgDbuI7JkXuV/7vvg3La2Vm2b96x599FF69erlvOh8+umn5M6d+7rPswmzuXLlonPnznz//ff8+OOPzoujVfUZ6603dOhQpzeeVdY1aNDAuWJhQyvGjh3rTLqNil2RsFjGjx/vfG3btGRgmTJlnMTjhAkTYvk3ICLRFhEG8+6Bi0eh6d8QksbriET8x/E1sKQrHPgTCj3iVrumzO51VCIiIomeJeO+WPYFXX/vStnsZVn++HIKZ7r+kLbiWYo7y25fmPECfRf0ZcHBBbxU4SUypc50aSqtJeUOHjzonN9a+yhjffMip9P+myXubOiGJf1MqlSpnMdbeys7V478vvivAJ/9Hyki/z0ZsuVILVZDhtJeRyPiPxZ3gy2fQcOZkOUWr6MR8Q+hJ2Hlq7DxI8hQHqoOhSzVvY5KREQkwVqzBsqUgdWroUiRC2zdutUp/rh88MTlvl/9Pff+cK/T8+6tRm+R7AYGRk3eNNkZhFG3QF0m3juRpGjkyJH06dPnium/ceXChev/uyZWqsgTEZH4Ycv8Ng6Gmt8piScSG+xa7PZvYNlzEBEKVYZC4Y5ari4iIhLLbBqtqV+w/g0l8UyjQo2cpa+189XG39hyXvvZ4mvIxsV/TQFOatQjT0RE4t6+abC4K5R9FfLf43U0IonfseUwvTbMfwjytIFWG6Ho40riiYiIxIG86fNyS+5b+GHdDze8jT+3/cnx88e5s+SdNx2PTalt06aN02fPKs5soKMl0mbOnOncv3DhQurVq+cMe8yfPz99+/Z1+sxHsscOHjzYaWeVJk0aypYty9y5c6/Yx1dffeX0B7Q+fKVLl+a77767dJ/tx7Zh3ytWrJizvNeWB9uAycqVK5MxY0anrVXr1q2dHvrGhmFaW629e/c6+7Rb5CBKW1LcvHlz5zk2dffxxx+/1E/Q2M/SpUsXp69+xowZ6datG0mZEnkiIhK3TqyHue0gXzso87LX0YgkbhePwaIuMLWy23Oy2SKo9gkkz+x1ZCIiIn7NEnAT108kNDz0hp5vScCKOSpSMKPbG+9m3HfffU4VnCXJlixZ4vSQj7RhwwZnIKUlzSzhN3v2bH7++WfeeeedK7bx+eefM2rUKGfirT3+/vvv/88S2C+++IJjx445ffA7der0n2Sf9bq3YZQnT54ka9aspE2blhEjRnD48GHWr1/v9Ba0WE3t2rWd/vrWJ9/6AtrN9mmTgRs1akSpUqWcoR+LFi1ynvvQQw9dsa8vv/ySBx98kCNHjvDBBx+QlCmRJyIicefCEZjVEtKVgFtG2OU/ryMSSZx8EbD5c/ilGOwcC9U+hyZ/QabKXkcmIiKSJNxZ6k6OnT/GzO1u1VtMhEeE89P6n2KlGs/6y/3xxx9OYs6q0+z2xhtvXLrfBka2atXKqV6z4RdWkff88887ibDLPfPMM85ADXvMY4895iTRLPFnLFFmAyerVKniLJetVasW99xzj5Pgu9zbb7/tVP1ZD7qgoCCaNWvmVPHZ51Zd99prr7FgwQInWXc1kyZNcpbK2s9jlX05c+Zk4MCBTJw4kf379196nFX3tWjRwonHHpeUqUeeiIjEjfCLMOdOiLgIdX6C4JReRySSOB1ZBIuegmNLoGgXKNcPkmXwOioREZEkpVDGQlTIUcGprGtcuHGMnjtn5xwOnT3EXaXuuuk49uzZ43y0BF2kAgUKXPp806ZN/Pnnn86y28sn3drtclYZFyl16tTOR0u4Zc+e3dmGJfp69ep16TG2NLdOnTpXbCNy8m6kWbNmOcm7tWvXcubMmUvft2W3Vq0XlV27djk/iyUUI0VO7LXkYo4cOaLcV1KmijwREYmbJvyLnoCji6HuL5DSPQCLSAycPwwLO8Fvt0BQCmi2DKoMUhJPRETEI1ZRN2H9BKfCLibGrx1P6aylKZ6l+E3HkDt3bufjjh07Ln3v8s8t8WXLWW3JbOTNlr7aUtbosm18/PHHV2zDnj958uQrHnf5cAurqmvZsqVTlbdx40Znn5bYM7bE9t+Pj5Q3b14nYXd5D78tW7Y4H/PlyxflvpI6/SZERCT2rX8ftn4JNb6FjOW9jkYkcbGTg40fw6RisHcS1PgGGs2CjOW8jkxERISknsg7eOYgc3de2SvuWiJ8Efy47sdYWVZrbBiEDX944YUXLiXZrJ9dpCeffJLx48c7gycsuWa99DZv3szUqVOjvY//a+8+wKMq0z6M3yk06R2kSBcEVFRApNpQEQS7K3ZdK1bEvnZd21o+de3o2nsXKwoi0puCSJUmCiK9k/Jd7xlBUZAEkpyU+7fXbDIzZ848EzBh/nne97nkkku45ZZbov3qQiffunXros/DfnxbE55rzZo10VLf0H0Xhlr8sa6NAWHYPy/sc7fR4YcfHnXjXXPNNdHjw3LaSy+9NFoevLEbT5szyJMk5ax578K4K6DVXVD7iLirkQqWX76Gj/eBMRdDwzOh+/dQ70T3l5QkKR9oVrUZzao0y9b02uHzhvPTyp+iPfZyyosvvhh1uYUlqa1atYr2jwtKlixJ69at+fTTT3niiSei7r2wh90xxxyzWdfetlx88cXceOON0cCMSpUqRefp16/fZstl/yxMoQ0DNG699dbo8zCF9thjj93smAMOOICePXtGk27D0t/wOsqVKxfVO2HChCikDFNvw9LaMIhDW5aUubHHUdLvlk6CAS2g20So0DzuaqSCY8kE+LQ91D0e2j5p+CBl1ZqfYfyV8MOzUP1A2OdBKN8s7qokSSqUJk2CFi1g4sSwH9s6Zs6cSYMGDaKhDdty/RfX039cf+ZcOofkpG33RvX9uC/vTn2XqX2mkpRL/zYeP358FOiFLrgwLEJEXYTZ+XMtSBx2IUnKuSBicA+otA+0fsQQT8qKjA0w9WH49gYoVh46vAZ1jva/H0mS8qmwRPaWL2/hyFeOpHSxxJCIv/PJjE/4517/zNEQb+LEidFS1j333DMK7y677DL2339/Q7wiwiBPkrTj0tbAl70guTh0fANSisddkZT/LRgEoy+EFVOhWT9ofjWkbvsNgSRJis/u1XfntD1PY+napawJ/wbeho67dOTElifmaA3Lli3j9NNPjybYhmWsnTt35oEHHsjR51D+ZZAnSdoxYYeGEWfC8u+h63AoUTnuiqT8bfU8GNcPZr8MNQ+Djm9CucZxVyVJkrIgdNY93fPpWGto3759NBlWRZNBniRpx0y8Bea8Cl0+hPJN465Gyr/S18OU+xL/zZSoBp3ehVrdXUYrSVIhMOLBESyYsIBD7juEEmUL155syl8M8iRJ22/2K4m9vfZ5GGoeHHc1Uv710yeJZbSr58BuVyeW0qaWirsqSZKUA4b8ewifX/M5JcqXYNHkRfT+qLdhnnLNtkesSJK0JYtGwvDToEkfaHJ+3NVI+dOq2TDkaPjiECi/Gxw+GVpeb4gnSVIhC/EOe/Awzvz6TBbPWMwLh77AuhXr4i5NhZRBniQp+1bNhS+PgGqdYa/74q5Gyn/S18K3t8D7TWHpt4ml553egjL14q5MkiTlQojXpk8bqu5WlVM/P9UwT7nKIE+SlD0bVsLgHomhFu1fgWR3aZA28+P78EFz+O4OaHEDdPsWdj407qokSVIuhngbGeYptxnkSZKyLjMDhp0Ea36Ezu9B8fJxVyTlHytmwKDuiaC7UmvoMQWaXwUp7pEjSVJRCPE2MsxTbjLIkyRl3firYf4A6PgmlGkQdzVS/pC2Gib8Cz7YDVb9AAcMhA4vw061465MkiTlcYgXZ5hXr149nnzyyVx/HsXLIE+SlDUznobJd0GbJ6Bax7irkeKXmQlz3oD3m8GUB2DPO+Cw8VDjgLgrkyRJMYZ4BbkzLz09nYyMjLjL0N8wyJMkbduCwTDqHNjtSmhwatzVSPFb9n1iEu1XxySGvvSYCk0vheRicVcmSZLyQYiXm2HeQw89RMOGDSlbtizVq1fntNNO47DDDmPOnDn06dOHMmXK0Lx5803B3N13302TJk0oX748++yzDx9++OGmcw0aNIikpCRefvnl6JiddtqJhQsX8tprr7H33ntTsWJFqlSpwhFHHMEPP/yw6XGZmZnccccd1K1blwoVKnDWWWdx3HHHRbVstHTpUs477zx22WUXKleuTLdu3Zg5c+YOv/6iziBPkrTtfb+GHAU7Hw573B53NVK8NqyAcVfAgJawdiEcNAT2exZK1Yi7MkmSlEtG/XfUdoV4WwrzXuz24g7VMm3aNK644greeecdVqxYwYwZMzjjjDOicC6EaiHkW7lyJZMmTYqOv//++3nggQeioO7XX3/l8ssvp2fPnowdO3az877yyisMGzaM5cuXU7Vq1Sgk7N+/P4sWLeL777+PgrsTTzxx0/HPPfdcFBCGwC8c065dO956661N94fjjzzyyOh848aNY/78+bRs2ZLu3buzYcOGHfoaFHUGeZKkrVu/FAZ3h9J1od1zkOSPDRXhZbSzXoL3m8L0J2Dv++HQ0VCtQ9yVSZKkXLZ4+mJSS6Wyc+udt/sc5euWp2KDitG5dkRqamoUkoWgLoRkofuuU6dOWz3+8ccfp1+/fuy1117RY0844YSoey/c/kehuy50zZUoUYKUlBQOPfRQ9thjj+jz0JF38803M3z48Cg8DJ599lnOPPNM2rZtG503fB6O3yiEd0OHDuWxxx6jUqVK0Xlvv/32qKtvxIgRO/Q1KOp8RyZJ2rKMNPjqeNiwLDGhtliZuCuS4rH0WxjYBb7uDTt3SyyjbXIBJKfGXZkkScoDB95+IPX3r8/zXZ9n3oh52X78+pXreaHbC1GId/KnJ+9QLfXr14+6655++umoA69169a89NJLWz1+7ty50TLcP2rUqFG0DPfP5/2jwYMHc+CBB1KzZk3KlStH586do9vDstvgxx9/jJbM/nnYxh87B9PS0qhdu3a09DZcQlC4sSZtP4M8SdKWjbkEfvkSOr3j9E0V3Y7U8N/Bh60Sk2m7Doe2T0DJqnFXJkmS8lBqyVSOe+M46naom+0wb2OIt+j7RdHy2motqu1wPWFp7EcffRQtaQ3ddr1792bq1KkkJ/814qlTp060/PaPwvUQAv7RHx+7fv36aAls6MoL5w2dfyHYC0I3YFCrVi1mz5692Tn+eL1GjRoUL16cX375Jdorb+NlzZo1/OMf/9jhr0FRZpAnSfqrqQ/DtIeh3bNQuXXc1Uh5KzMDZv4P3t8VZj0PrR+FQ0ZAlezviSNJkopumJcbId6UKVMYMGBAtA9eWNIaBlgEYQlsCM/C/X8UhlDcc889jB8/PuqQe/XVV6PHh9u3Wvf69VHgFgZdhL3ywv5211133WbHnHzyydEeeqNGjYrOGzoEw3Ns1KFDB1q0aBENu9jYxbdkyRLeeOMNVq9evcNfh6LMIE+StLmfPoExF0PLm6HusXFXI+WtxWPh0w4w4gyocwx0nwqNznJ/SEmSlK0wLzdCvOi869dz2223RR1xYclr3759o/3qwvLZ66+/PhqCEZax7r777tHxl112GRdccAHHHHNMtFfdnXfeyZtvvhlNr92asO/ek08+ya233hp9HvbUO/bYzd8XnHLKKVx66aUcddRR0R56X331VdTFV7JkyU3B4qeffhpNwQ376IVAMOyhFwZihCm52n5JmRv7IiX9bukkGNACuk2EComx3VKRsOw7+KQd7Nwd9nse/CGromLdYphwLUx/DKq0g30egkqt4q5KkiTlgjDQtUULmDgx7Be3jpkzZ9KgQYNoIENWpK1N49WjX2XOV3M46ZOTqN22dp6EePndnnvuyfHHH8/VV18ddymsW5f9P9eCwl8vS5IS1i6CwT2g3G6w71OGeCoaMtJh+uPwfhOY9xbs+wwc/JUhniRJ2q7OvKIU4r3yyivREty1a9dy33338d133/2lc085zyBPkgTp62DIUYlJtZ3ehpRES7xUqC0aAZ/sC6POh3onQ/cp0OAUQ2xJkrRdYV5RCvGCJ554ItqXr2rVqjz//PPRst4wEVe5KzWXzy9Jyu/CDgujzoUl4+DgoVCqetwVSblr7UIYfzXM7A/VOsNh46FCi7irkiRJBTTMC8tsQ5hXedfKLJ21tEiEeMFnn30WdwlFkh15klTUTb47MaGz/UtQMbEprlQohY7TKQ/Be7vCTx9D+5fhwC8M8SRJ0g6HefW61GPVwlVFJsRTfOzIk6SibO7bMP4qaHUP1OoedzVS7lk4BEb3geWToell0Pw6KFYm7qokSVIhCfNOeOeEuMtQEWGQJ0lF1ZLx8HVvaHgmNL007mqk3LHmJxh3Bcx6Hmp0hQ6vQrld465KkiRJ2i4GeZJUVMONMKG2chvY52E391fhk7EBpvwffHsjFK8EHd+E2r38uy5JkqQCzSBPkoqatDUwuCckl4SOb0BK8bgrknLWzwNh9IWwcibsdgXsdhWk7hR3VZIkSdIOM8iTpKIkMwOGnwYrpsEhw6FEpbgrknLOqrkwri/MeQ1q9YDO70HZhnFXJUmSJOUYp9ZKUlHy7U0w9w3o+Jr7hKnwSF8Hk/4N7zeFxWOh8/vQ+V1DPEmSlGeWLFnClClToo8FTZcuXbjuuuviLkNZZJAnSUXFrJdg4s2wz0NQ46C4q5FyxvwP4YMWMPEWaH41HD4Rah0ed1WSJKmImD59OkcddRRVqlShadOm0cejjz46ul3KDS6tlaSiYNFwGH467HoxND437mqkHbfyBxh7Kcx7B+ocDQd+BqV3ibsqSZJUhISwrnXr1qxcuZKMjIzotvDx3Xff5fPPP2fUqFE0atQo7jJVyNiRJ0mF3ao58GUvqH4AtPpP3NVIOz6s5Zsb4YPdYPn3sP8n0PF1QzxJkpTnrrzyyijES0tL2+z2cD3cHu7PjWWwF198MSeeeCLly5enTp06PPLII5sd88EHH7D33ntH9zdp0oR77rlnU9C4NUuXLo06C8uWLRuFj88+++ym+5555hlq16692fE33ngjHTp02HT9oYceomHDhtHjq1evzmmnnZZjr1mbsyNPkgqzDStgcA8oUQU6vAzJKXFXJG2fzEz48V0Ycwms+wVa3pzoMHXqsiRJymFJSUk7fI4Q5r355ptZPldm+LdOFoVg7a233uL555+PPh533HEcfPDBUQAXugCPPPLI6L4QzE2YMIEePXqQmprKJZdcstVzPvXUU7z66qvR5ZNPPqFXr15RMNe+fftt1jNt2jSuuOIKRo4cSYsWLaIQc+zYsVl+PcoeO/IkqbDKSIeve8Oa+YnpncXKxV2RtH2WT4NB3RKdpVXaQfcpsFs/QzxJklQkhT34DjjgAJKTk6PPK1WqxJgxY6L7nnzySQ4//PAo3AvhXejM69evH48++ujfnrNbt26bAr/weQgD+/fvn6V6wmNCEDlp0iSWL19OmTJl6NSpU468Vv2VQZ4kFVYTroKfPoZOb0OZ+nFXI2Vf2ioYfw0MaAGr58GBg6D9i7BTrbgrkyRJhVgIpbZ1Wbx4cRSk/Z2UlJTouKycLzt23nnnza6XLl2aFStWRJ/PnTs36qT7o9CpN2fOnL89Z/369f9yPZwrK8KxL7/8Mk8//TR169aN9g186aWXsvhqlF0GeZJUGM14CibfA22fhKrbboeX8pXwj9k5r8H7TWHaw9DqbjhsHFTvHHdlkiRJkYoVK0bLT0M32paE23v27Bkdl5fCnnkzZszY7LZwPQRsf2fWrFl/ub5xX7yw792qVas2u3/+/PmbXQ+v9aOPPmLRokVRB2Dv3r2ZOnXqDr4abYlBniQVNgsGwchzYberof7JcVcjZc+y7+Dzg+Cr46DGQdB9Kux6ESS7ra8kScpf7rzzzmgZ6Z/DvHA93B7uz2tnnHFGNOzijTfeID09nXHjxnH33Xdz9tln/+3jBgwYED0uPCYEcmHvvdNPPz26r1WrVlHH3yuvvBINzRg0aBCvvfbapsdOmTIlenzYGy+89jBkY2NHonKeQZ4kFSYrpsOQo6H2EbDHrXFXI2XdhuUwti8M2APWL4GDh8K+T0Op6nFXJkmStEUbh0scccQRm5bZhvAqXA+3h/vzWtu2bXn99de57bbbom7AY489losuuiiadLutADAMvKhQoQIXXHBBtKdex44do/saNGgQTaW9/PLLo/sfe+yxTSFfsH79+uj5atWqRbly5ejbt2809fbPS3yVM5Iys7sYWyoKlk5K7MnUbSJUaB53NVLWhPDjk3aQUhoO/hJSS8ddkbRt4Z8hs16Acf0gYx3scTs0/KcTliVJUq6ZNAlatICJE0MYt46ZM2dGYVWJEiW2+5xLlixh4cKFVKtWLc+X0+qv1q3LmT/X/Mh1KpJUGGRsSCxF3LACDhhoiKeCYckEGN0HfhkKjf4Ju98GJavEXZUkSVK2hfDOAE95wSBPkgpDR9OYixNhyMFDnOipgtE9+s31MO2/UKk1HDISKu8Td1WSJElSvmeQJ0kF3dSHYNoj0OE1qLR33NVIW5eZATOfgfFXhSvQ5glocBokuWWvJEmSlBUGeZJUkM3/CMZeArvfCnWPibsaaet+HQ2jL4DFo6HxBbD7TVDc5SeSJElSdhjkSVJBHsoS9sXb5URofk3c1UhbtnYRfHMtTH8CqraHQ8dCxT3irkqSJEkqkAzyJKkgWvsLDO4BFVpC2ycgKSnuiqTNZaTDjCdgwrWQXBzaPQf1TvTvqiRJkrQDDPIkqaBJXwdDjkzsMdbpLUgpGXdF0uZ+GZZYRrv0W9j1Ymh5PRQrF3dVkiRJUoFnkCdJBW1C7cizYck30PVrKFkt7oqk361ZAOOvhB/+B9UPgG4ToPxucVclSZIkFRoGeZJUkHx3J8x6Hjq9BxVaxF2NlJCRBlMfhm9/67zr8CrUOcZltJIkqehYsgQWLoRq1aCiA72Ue5Jz8dySpJw09y2YcDW0+g/U6hZ3NVLCgsHwYSsY3y8xjbb791D3WEM8SZJUNEyfDkcdBVWqQNOmiY9HH524PRd06dKF6667LlfOrYLBIE+SCoLF4+Drk6DR2Yk9x6S4rf4Rhp4IA7vATrWh20TY83ZILR13ZZIkSXkjhHWtW8N770FGRuK28PHddxO351KYl5cGDhzIgQceSOXKlUlKSmJ6IXhNBZ1BniTld6vnJybUVtkX9nnITifFK309fHc3vN8UFg2DTu9AlwFQrknclUmSJOWtK6+ElSshLW3z28P1cHu4v4ArXbo0p5xyCs8++2zcpeg3BnmSlJ+lrYYvj4DUnaDDa5BcLO6KVJT99Cl8uHtiL7xml8Ph30HtIwyXJUlS4RL+bZOVy5tv/jXE2yjcHu7P6rmyYenSpRx11FGULVuWRo0abRayPfPMM9SuXXuz42+88UY6dOiw6fpDDz1Ew4YNo8dXr16d0047bavPte+++3LqqafSvHnzbNWo3OOwC0nKrzIzYNipsHImdB0OJSrFXZGKqlWzYWxfmPsG1O4JXT6EMvXjrkqSJKlIeuqpp3j11VejyyeffEKvXr2iYK59+/bbfOy0adO44oorGDlyJC1atGDlypWMHTs2T+pWzrAjT5Lyq29ugHlvQ4fXXbaoeKSvhYm3wvvNYOk3iSW0nd42xJMkSYVbZua2L4sXQ/I2IpWUlMRxWTlfNnTr1o0ePXqQmpoafX7kkUfSv3//LD02PCYzM5NJkyaxfPlyypQpQ6dOnbL1/IqXQZ4k5Uc/vACTboXW/4UaB8RdjYqiHz+AD1rApH9Di39Bt29h58PirkqSJCl/qFgRevUKydiW7w+39+yZOC6H1a9f/y/X586dm+XHvvzyyzz99NPUrVuX1q1b89JLL+V4jco9BnmSlN/8MgxGnAm7XgqN/hl3NSpqVsyAQT1gcHeotDd0/x6aXw0pJeKuTJIkKX+5804oU+avYV64Hm4P9+eCWbNm/eX6xn3xwr53q1at2uz++fPnb3a9Z8+efPTRRyxatIh+/frRu3dvpk6dmiu1KucZ5ElSftuLbEgvqHEQtLo77mpU1AarfHM9fNAcVs6AAz6DDq9A6TpxVyZJkpQ/NWoEo0bBEUf8vsw2LKcN18Pt4f5cMGDAAD744APS09OjQO6tt97i9NNPj+5r1aoVK1as4JVXXiEjI4NBgwbx2muvbXrslClToseHvfHCMtvy5cv/VnbKFp8rnGPt2rWsW7cuur5+/froenhuxcMgT5Lyiw0rYFB3KFEN2r8IyVv+YSrlqLAny9w3E/vgfX8/7HE7dJsANQ6MuzJJkqT8L4R1b7wBixbB99/DL78krudSiBecccYZ0cCLChUqcMEFF/Doo4/SsWPH6L4GDRpEU2kvv/zy6P7HHntsU8i3MYi77bbbqFWrFuXKlaNv377R1NswLGNLvvzyS0qVKkXTpk2j62F6bbj+3HPP5drr099Lygy7HEra3NJJMKAFdJsIFRyzrTyQkQ5f9oRfR8IhI6FMvbgrUlGwfAqMvgh+/gTqnQSt7oJSNeOuSpIkKVdNmgQtWsDEiSFvW8fMmTOjAKxECbcSKSzWrSu8f65b2ZVRkpSnxvWDnz+FA78wxFPu27ASJt4CU+6Dcs3goC+hWuK3uJIkSZLyL4M8SYrb9CcSgUq756HqfnFXo8IsNOHPfgXG9YW0VdDqXmh8LiT7zwFJkiSpIPBf7pIUpwVfwKjzofl1UL933NWoMFs6EUZfCAsHQcMzE3vhlawWd1WSJEmSssEgT5LisnwqDDkaaveC3W+KuxoVVuuXwbc3wtQHoeKe0HU4VGkbd1WSJEmStoNBniTFYf0SGNwDyjSEdv+DJIeIK4dlZsAPz8P4KyBjA7T+LzQ402nIkiRJUgFmkCdJeS2EKkOOSexRFoZbpO4Ud0UqbBaPg9F9YNGwxB54u98CJSrHXZUkSZKkHWSQJ0l5PWwg7FO2aDgcPAR22jnuilSYrFsM31wH0x+Dym3h0NFQaa+4q5IkSZKUQwzyJCkvTfm/RMjS8Q0DFuXsMtoZT8GEqyEpBdr2h/onu2RbkiRJKmT8F74k5ZUfB8C4yxLTQuscFXc1KiwWjYSP94VR50G9k6D7VGhwqiGeJElSHlrCEqYwJfqYm7p06cJ1112Xq8+h/M1/5UtSXlg6EYaekAhadrsq7mpUGKz9BUacBZ+0hdRScNg42Pt+KF4+7sokSZKKjOlM5yiOogpVaErT6OPRHB3dXhjce++97L333pQvX55q1arRvXt3Jk2aFHdZRZpBniTltrULYXB3qLgHtHkckpLirkgFWUYaTH0Y3msC8z+E/V6EAwdBhZZxVyZJklSkhLCuNa15j/fIICO6LXx8l3ej2wtDmLd27Vruv/9+fv75Z2bPnk3Tpk056KCDWLNmTdylFVkGeZKUm9LXwpdHJr7ddnwTUkrEXZEKsl+Gwkf7wJhLoNHZ0P17qPcPw2FJkqQYXMmVrGQlaaRtdnu4Hm4P9+eGpUuXctRRR1G2bFkaNWrEs88+u+m+Z555htq1a292/I033kiHDh02XX/ooYdo2LBh9Pjq1atz2mmnbfW5rrnmGjp27EipUqWiS1jWG0K977//Pldem7bNYReSlJsTakf8E5ZNhIO/hpJV465IBdWan2DclTDrOahxELT/Fso3jbsqSZKkQimJHf8laQjz3uTNLJ8rk8wsn/upp57i1VdfjS6ffPIJvXr1ioK59u3bb/Ox06ZN44orrmDkyJG0aNGClStXMnbs2Cw/d3i+0qVL06RJkyw/RjnLjjxJyi3f/RtmvwjtX4EKzeOuRgVRxgaYfC+8tyssHJyYdrz/J4Z4kiRJRVi3bt3o0aMHqamp0edHHnkk/fv3z9Jjw2MyMzOjfe6WL19OmTJl6NSpU5Ye+80333DuuedGS21DmKd4GORJUm6Y8wZMuBb2ug92PjTualQQLfgCPtwTJlwNu14M3Scnph27jFaSJClXZWbhf4tZTPI2IpUUUqLjsnK+7Khfv/5frs+dOzfLj3355Zd5+umnqVu3Lq1bt+all17a5uNCB9+BBx7IzTffzFlnnZWtepWzDPIkKactHgPDToZG50KTC+OuRgXN6nnw1fEw8AAoXR8OnwR73AKpO8VdmSRJkn5TkYr0ohepW9mxLNzek57RcTlt1qxZf7m+cV+8sO/dqlWrNrt//vz5m13v2bMnH330EYsWLaJfv3707t2bqVOnbvX5Bg4cSNeuXbn77rvp06dPjr4WZZ9BniTlpNU/wuAjoMp+sM//2T2lrEtfB5PuSCyjXTwaOr8HXd6Hso3irkySJElbcCd3UoYyfwnzwvVwe7g/NwwYMIAPPviA9PT0KJB76623OP3006P7WrVqxYoVK3jllVfIyMhg0KBBvPbaa5seO2XKlOjxYW+8sMy2fPny0e0pKSlbfK5w7rB098knn/zboRjKOwZ5kpRT0lYlQrxiZaHja5BcLO6KVFDM/wgGtISJN8FuVyW68Gp1j7sqSZIk/Y1GNGIUoziCIzYtsw3LacP1cHu4PzecccYZ0cCLChUqcMEFF/Doo49Gk2WDBg0aRFNpL7/88uj+xx57bFPIF6xfv57bbruNWrVqUa5cOfr27RtNvQ3DMrbksssuizr8QogX9tPbeHnhhRdy5bVp25Iywy6Hkja3dBIMaAHdJjqkQFmTmQFfHQsLBsEhI+yiUtasnAVjL4V5byf2v2v1HyhTL+6qJEmSCrVJk6BFC5g4ERo1WsfMmTOjAKxEiRLbfc4lLGEhC6lGtVxZTqvsWbcuZ/5c86MtL+aWJGXPN/+Cee/CAZ8a4mnb0tbA5LsTk41L7wL7fww1u8ZdlSRJkrZTCO8M8JQXDPIkaUf98BxMuh3aPgnVu8RdjfKz0AT/43sw5hJYtxBa3gS7XgIpxeOuTJIkSVIBYJAnSTvil6Ew4ixo2hcanhl3NcrPlk+DMRfDTx/CLidAq7thp8R0MUmSJEnKCoM8SdqR/c2+PBJqdIU9c2cilQrJEJTQsTn5HijbGA78ws5NSZIkSdvFIE+StseG5TC4O5SqCe1fhOQtj2tXEV9GO/cNGHsZbFiWCHubXOA0Y0mSpHzIOaCFS2Yh/vM0yJOk7MpIg69OgHW/wCEjoVjZuCtSfrNsMoy+EBYMhPqnwp53QKkacVclSZKkP0lJSfxCfsOGDZQsWTLucpRDVq9eHX0sVqzw/RLdIE+Ssmvc5bDgczhoUGLiqLTRhhUw8Wb4/n6o0AIO/gqqto+7KkmSJP1NkFe6dGkWLlxIamoqycnJcZekHezEW716NQsWLKBSpUqF8s/TIE+SsmPaYzDlAdjvRaiyb9zVKL8IrfuzXoTx/SB9Lezzf9DwbJdcS5Ik5XNJSUnUrFmTH374gVmzZsVdjnJIpUqVqFatGoWRQZ4kZdXPA2H0BdDieqj3j7irUX6x5BsY3Qd++QoangV73A4lq8RdlSRJkrIoLL9s3LhxtLy2MO+tVpT+PJMLYSfeRgZ5kpQVy6fAkGOgztHQ8oa4q1F+sH4pfHMDTHsYKu0Nh4yAyq3jrkqSJEnb2ZlXvHjxuMuQtskgT5K2Zd1iGNwDyjaGfZ+BpML72x1lQWYGzPwfjL8yXIE2j0GD0/17IUmSJCnXGeRJ0t9JXw9fHQPpa+CgwZBaKu6KFKfFY2BUH1g8EhqfD7vfDMUrxl2VJEmSpCLCIE+StibsjxH2xFs0IjF9tFTNuCtSXNb9ChOuhemPQ9X94NAxUHHPuKuSJEmSVMQY5EnS1nx/H8x4Cjq+CZVaxV2N4pCRDjOehAnXQHIxaPc/qHdS2EQl7sokSZIkFUEGeZK0JT++D+Muhz3vgDq94q5GcfhlWGIa7dIJsOvFiSEnxcrFXZUkSZKkIswgT5L+bMk3MPQf0OA0aNYv7mqU19YuhPFXwcynofr+cNgEqNA87qokSZIkySBPkjazZkFiQm3FVtD6UZdQFiUZaTDtv/DN9ZBaBtq/AnWP9e+AJEmSpHzDIE+SNkpfC0OOhOTUxL54KcXjrkh5ZeGXiWW0y7+Hpn2h+bVQrEzcVUmSJEnSZgzyJGnjhNrhZ8KySdB1GJSsEndFygur58O4fjD7Rah5CHR4Hco1ibsqSZIkSdoigzxJCibdBnNehs4DoPxucVej3Ja+Hqb+H3x7E5SoAp3ehlpHuIxWkiRJUr5mkCdJs1+Fb/4Fez8IOx8SdzXKbT9/BqMvhJU/wG5XwW5XQmqpuKuSJEmSpG0yyJNUtP06CoafCo0vgF37xF2NctOqOTD2Mpj7RqL7rssHUKZB3FVJkiRJUpYZ5EkqulbPgy97QtVOsPf9cVej3JK+Dibfk1g+XaoWdP4AanWLuypJkiRJyjaDPElFU9oqGHwEFCsPHV5JTKpV4fPjABhzMaz5EVpcl5hIm1Ii7qokSZIkabv4zlVS0ZOZAV+fBKvnQNcRULxC3BUpp62cCWMugR/fgzrHwIEDoXTduKuSJEmSpB1ikCep6JlwLcz/AA74DMo2jLsa5aS01fDdnYlLmfpwwKdQ46C4q5IkSZKkHGGQJ6lomfk/+O4OaNsfqnWKuxrllMxMmPcOjL0E1v0Ke9wGTS6ElOJxVyZJkiRJOcYgT1LRsXAIjPwnNOsHDU+PuxrllOVTYcxF8NPHUK837HkX7LRz3FVJkiRJUo4zyJNUdPZMG3Ik1DwM9vh33NUoJ2xYmZhE+/1/oFxTOGiwXZaSJEmSCjWDPEmF3/plMKg7lKoN+70AySlxV6QdXUY751UY2xfSVkKr/0Dj85w8LEmSJKnQ812PpMItIw2GHg/rl8AhI6FYmbgr0o5YOgnGXAgLvoAGp8Oed0DJanFXJUmSJEl5wiBPUuE29jJYOBgOHAyl68RdjbbXhuXwzY0w9f+gwh7QdRhU2TfuqiRJkiQpTxnkSSq8pj0CUx+E9i9DlTZxV6PtXUY763kY1w8yNsA+D0PDs1weLUmSJKlIMsiTVDj99CmMvhBa3gi7HB93NdoeS8bD6D7wy9fQ6GzY4zYoUTnuqiRJkiQpNgZ5kgqfZd/DV8dC3WOhxfVxV6PsCvsZTvgXTH8EKrWBQ0dBpb3jrkqSJEmSYmeQJ6lwWfcrDO4O5ZpC2/6QlBR3RcqqzAyY0R8mXA0kQdunoP4pkJQcd2WSJEmSlC8Y5EkqPNLXw5CjIGM9dHobUkvFXZGy6tdRMOoCWDIGGveB3W+C4hXirkqSJEmS8hWDPEmFQxiKMOo8WDwGDh4KpWrEXZGyYu2iRAfejKegagc4dBxU3D3uqiRJkiQpXzLIk1Q4fP8fmPk0dHoHKu4RdzXalox0mP4YfHMdpJSE/Z6HXf7hUmhJkiRJ+hsGeZIKvnnvwrgroNVdULtH3NVoW8IU2tEXwNKJ0PRSaPEvKFY27qokSZIkKd8zyJNUsC2ZAF+fCA1Oh6Z9465Gf2fNzzD+SvjhWah+IHT7Bso3i7sqSZIkSSowDPIkFexgaHAPqLQPtH7EZZn5VcYGmPoQfHNDYoBFh9egztH+eUmSJElSNhnkSSqY0tbAl70guTh0fANSisddkbZkwSAY3QdWTINm/aD51ZBaOu6qJEmSJKlAMsiTVDAn1I44A5Z/D12HQ4nKcVekP1s9D8b1g9kvw87doNPbULZR3FVJkiRJUoFmkCep4Jl4M8x5Dbp8COWbxl2N/ih9PUy5DybeAiWqQad3oVZ3l9FKkiRJUg4wyJNUsMx+Bb69EVr/F2oeHHc1+qOfPoHRF8LqObDb1YmltKml4q5KkiRJkgoNgzxJBceikTD8NGhyITQ+L+5qtNHKWTD2Mpj3FtQ+Evb/GMrUi7sqSZIkSSp0DPIkFQyr5sKXR0C1zrDXvXFXoyB9LXx3N3x3O+xUB7p8BDsfEndVkiRJklRoGeRJyv82rITBPRJDLdq/Asl+64rdvPdg7CWwdgG0vBF2vQRSSsRdlSRJkiQVar4blpS/ZWbAsJNgzY9wyAgoXj7uioq2FdNhzCUw/wOoezzsdQ/sVDvuqiRJkiSpSDDIk5S/jb8a5g+AAwZCmQZxV1N0pa2GSbfD5LuhbCM48HOovn/cVUmSJElSkWKQJyn/mvE0TL4L9n0GqnWMu5qiKTMT5r6ZGGaxfgnseQc06QPJxeKuTJIkSZKKHIM8SfnTgsEw6hzY7UpocGrc1RRNy76HMRfBz59CvZOh1V1QqkbcVUmSJElSkWWQJyn/WTEDhhwFOx8Oe9wedzVFz4YVMPEW+P4+KN8cDhoC1TrEXZUkSZIkFXkGeZLyl/VLYXB3KL0L7Pc8JCXHXVHRWkY7+2UYd3liT7y9H4BG50ByStyVSZIkSZIM8iTlKxlp8NXxsGEZHPAppJaOu6KiY+m3MLoPLBwCDc9MdEKWrBp3VZIkSZKkPzDIk5R/jLkEfvkSDvoSdqoddzVFpwPy2xth6kNQsRV0HQ5V2sRdlSRJkiRpCwzyJOUPUx+GaQ9Dh1ehcuu4qyn8MjPgh2dh/JWQmQ6tH4WGZ7iUWZIkSZLyMYM8SfH76RMYczG0vBnqHht3NYXf4rGJZbS/joBG58Lut0CJSnFXJUmSJEnaBoM8SfFa9h18dSzUPR5aXBd3NYXbul9hwnUw/TGo0g4OGQ2VWsVdlSRJkiQpiwzyJMVn7SIY3APKN4d9n4KkpLgrKpwy0mHGkzDhGkguBvs+A/VP9ustSZIkSQWMQZ6keKSvgyFHJSbVdnwLUkrGXVHhtGh4YhntkvHQ5CJoeQMULx93VZIkSZKk7WCQJynvZWbCqHNhyTjo+jWUqh53RYXP2oUw/mqY2R+qdYHDJkCF5nFXJUmSJEnaAQZ5kvLe5Lth5v+g87tQoWXc1RQuocNx2iPwzb8gtQy0fxnqHucyWkmSJEkqBAzyJOWtuW/D+Kug1T1Qq3vc1RQuC4ckltEunwxNL4Pm10GxMnFXJUmSJEnKIQZ5kvJO2Kft697Q8Exoemnc1RQea36Ccf1g1gtQoyt0eBXK7Rp3VZIkSZKkHGaQJynvwqYwobZyG9jnYZd65oSMDTDl/+DbG6FEZej4JtTu5ddWkiRJkgopgzxJuS9tDQzuCckloeMbkFI87ooKvp8HwugLYeVM2O3KxCV1p7irkiRJkiTlIoM8SbkrMwOGnwYrpsEhw6FEpbgrKthWzYVxfWHOa1CrB3R+D8o2jLsqSZIkSVIeMMiTlLu+vQnmvgH7f+y+bTsifR18/x+YeBuUqgmd34dah8ddlSRJkiQpDxnkSco9s16CiTdD60ehxoFxV1Nwzf8QRl8Ea36E5tdCs76QUjLuqiRJkiRJecwgT1LuWDQchp8Ou14Mjc+Ju5qCaeUPMPZSmPcO1DkaDhwIpevGXZUkSZIkKSYGeZJy3qo58GUvqH4AtPpP3NUUzOEg390J390BZerB/p9AzYPjrkqSJEmSFDODPEk5a8MKGNwDSlSBDi9DckrcFRUcmZnw47sw5hJYtwh2vyXR0eiUX0mSJEmSQZ6kHJWRDl/3hjXz4ZCRUKxc3BUVHMunwZiL4KePYJcTodVdsFOtuKuSJEmSJOUjBnmScs6Eq+Cnj+HAz6FM/birKRjSViUm0YaJtGWbwIGDoHrnuKuSJEmSJOVDBnmScsaMp2DyPdDuWajaPu5qCsYy2jmvwbi+sGE5tLobGp8PyX5bliRJkiRtme8YJe24BYNg5LnQ/Bqof3Lc1eR/y76D0RfCgs+hwWmwxx1QqnrcVUmSJEmS8jmDPEk7ZsV0GHI01O6ZGM6grQudd9/eBFP+DyrsDgd/DVXbxV2VJEmSJKmAMMiTtP3WL4HB3aF0PWj3P0hKjrui/LuMdtYLMK4fZKyDfR6Ehv90oq8kSZIkKVsM8iRtn4wN8NVxsGEFHDAQUkvHXVH+tGQCjO4DvwyFRv+E3W+DklXirkqSJEmSVAAZ5Enavg6zMRcnwqmDh8BOteKuKH92K35zPUz7L1RqDYeMhMr7xF2VJEmSJKkAM8iTlH1TH4Jpj0CH16DS3nFXk79kZsDMZ2D8VYnrbZ6EBqe67FiSJEmStMMM8iRlz/wPYewlsPutUPeYuKvJX34dDaMvgMWjofEFsPtNULxi3FVJkiRJkgoJgzxJWbd0Enx1POxyIjS/Ju5q8o+1i2DCNTDjSajaHg4dCxX3iLsqSZIkSVIhY5AnKWvW/gKDe0CFltD2SUhKirui+GWkw4zHYcK1kFwC2j0H9U70ayNJkiRJyhUGeZK2LX0dDDkybAAHnd6ClBJxVxS/X4YlltEu/RZ2vRhaXg/FysVdlSRJkiSpEDPIk7TtCbUjz4Yl30DXr6FkNYq0NQtg/JXww/+g+gHQbQKU3y3uqiRJkiRJRYBBnqS/992dMOt56PQeVGhBkZWRBlMfhm9D5135xMTeOke7jFaSJEmSlGcM8iRt3dy3YMLVsNd9UKsbRdaCwTC6D6yYCs0uTwz6SC0dd1WSJEmSpCLGIE/Sli0eC1+fBI3OTuwBVxSt/hHG9YPZL0HNw6Djm1CucdxVSZIkSZKKKIM8SX+1en5iQm2VfWGfh4re8tH09TDlfph4M5SoCp3egVo9it7XQZIkSZKUrxjkSdpc2mr48ghILQMdX4fkYhQpP30KYy6EVbNht6ug2RWQWiruqiRJkiRJMsiT9AeZGTDsVFg5E7qOgOIVKTJCcDf2Mpj7JtTuBV0+hDL1465KkiRJkqRNDPIk/e6bG2De23DAJ0VnL7j0tTD5Hph0O+xUOxHg7Xxo3FVJkiRJkvQXBnmSEn54ASbdCm0eh+r7UyT8+AGMuRjW/AQt/gVNL4OUEnFXJUmSJEnSFhnkSYJfhsGIM2HXS6HRPyn0VsyAMZfA/Peh7nHQ6h4oXSfuqiRJkiRJ+lsGeVJRF/aGG9ILahwEre6m0A/y+O4O+O4uKNsQDhgINQ6IuypJkiRJkrLEIE8qyjYsh0HdoUQ1aP8iJKdQKGVmwry3YMylsH4J7HE77Hph0ZvIK0mSJEkq0AzypKIqIx2GngjrFiYm1BYrR6G0fAqMvhB+/hTqnQSt7oJSNeOuSpIkSZKkbDPIk4qqcf3g58/gwC+gTD0KnQ0rYOKtMOU+KNcMDvoSqnWMuypJkiRJkrabQZ5UFE1/IhFwtXseqraj0C2jnf0KjOub2BNvr/ug0TmQ7Lc7SZIkSVLB5jtbqahZ8AWMOh+aXwf1e1OoLJ0Io/vAwsHQ8MzEXnglq8VdlSRJkiRJOcIgTypKlk+FIUdD7V6w+00UGuuXwbc3wNSHoOKe0HU4VGkbd1WSJEmSJOUogzypqAjTWgf3gDINod3/ICmZAi8zA354DsZfAZnp0PoRaHBG4Z2+K0mSJEkq0gzypKIgYwMMOQbSViWGW6TuRIG3eFxiGe2iYdD4XNj9VihRKe6qJEmSJEnKNQZ5UmEXhj9EgddwOHgI7LQzBdq6xfDNdTD9MajcFg4dDZX2irsqSZIkSZJynUGeVNhNeQCmPw4d3yzYgVdGOszsDxOuhqQUaNsf6p9cOJYIS5IkSZKUBQZ5UmH24wAY1xf2+DfUOZICa9FIGH0BLBkHTS6EljdC8fJxVyVJkiRJUp4yyJMKq6UTYegJUO8k2O1KCqS1vyQ68GY8BdU6w2HjoELLuKuSJEmSJCkWBnlSYbR2IQzuDhX3gDaPQ1ISBUpGWmIPvAnXJQZz7PcS7HJ8wXsdkiRJkiTlIIM8qbBJXwtfhmW0yYl98VJKUKAs/CoxnGPZJGh6GbS4DoqVjbsqSZIkSZJiZ5AnFbYJtSP+CcsmwsFfQ8mqFBhrfoJxV8Cs56HGwdD+ZSjfNO6qJEmSJEnKNwzypMLku3/D7Beh8wdQoTkFQsYGmPIgfBsGWFSEjm9A7SNdRitJkiRJ0p8Y5EmFxZw3YMK1sPcDsPOhFAg/fw5jLoQV06HZFdD86sSeeJIkSZIk6S8M8qTC4NfRMOxkaHweNLmQfG/VXBh3Ocx5FXY+HDq9A2UbxV2VJEmSJEn5mkGeVNCt/hG+7AlV2ye68fLzktT0dfD9fTDxFihVAzq/B7W6x12VJEmSJEkFgkGeVJClrYLBRySmunZ4DZKLkW/N/wjGXASr50Hza6DZ5ZBSMu6qJEmSJEkqMAzypIIqMwOGnQKrZsEhI6B4BfKllT/A2Mtg3ttQ5yg44FMovUvcVUmSJEmSVOAY5EkF1Tf/gnnvJoKx/Li/XNoamHwXfHdHIrjb/2Oo2TXuqiRJkiRJKrAM8qSC6IfnYNLt0PZJqN6FfCUzE358D8ZcAusWQsubYNdLIKV43JVJkiRJklSgGeSp6FjzEywel7VjV81OfFz45e+f/53q+0NqKfLEL0NhxFnQtC80PJN8Zfk0GHMx/PQh7PIPaHU37FQr7qokSZIkSSoUDPJUdIy/Bn54JnuPGX1+1o5r/Rg0Pps82W/uyyOhRlfY807y1dCN0CE4+R4o2xgO/CL/dQpKkiRJklTAJcddgJRnGp0NSTk81TUpBUrWgNpHZP+x65fCkgmJpahZsWE5DO4BpWpC+xchOYXYhdrnvAbvN4WpDyXCxcPGGeJJkiRJkpQL7MhT0VG1Hez7NAw7KefOGYLB/T+CUjWy/9gvusKvo6BkdajdC2r1gOoHbHmJbkYafHUCrFsEh4yEYmWJ3bLJMPpCWDAQ6p8Ke96xfV8HSZIkSZKUJQZ5Klrq94blkxPLQMliJ9xWJUGHV6DiHtv38HVLEh/XLoAZ/WH6Y5BcHGocBLV7ws6H/76/3LjLYcHncNAgKF2XWG1YARNvhu/vhwot4eChUHW/eGuSJEmSJKkIMMhT0bP7zbDsu8Rk1cy07T9P6EDbniW1W5K5IfExYz3M/wh++ggyM6B8i0RwN38AtHsequybM8+3XTVmwqwXYXw/SF8L+/wfNDw7fyzxlSRJkiSpCHCPPBU9Scmw3/OJkCwpdfv2xQtLSZv1y43qQpqXCPGCZRMTIV4w5iIYfgbMfQs2rCRPLfkGPusMw05OLAHuPhUan2eIJ0mSJElSHjLIU9GUuhN0+QBKVEoEc1kVgr/KbaHN45CURJ5avxh+eA6GHAWvV4SBB8KUhxKTbHPtOZfC6Ivgo1aQsQ4OGQFtHoOSVXLvOSVJkiRJ0hYZ5Kno2mln6PJh1rvyQuAXJsZ2egdSihOLjUuBw8cFX8DYS+DdBvBeExh/ddYn4G7zeTJgxtOJ885+KRFcdh0GlVvnzPklSZIkSVK2GeSpaKu0F7R/MQsHJkFKSdj/43zUjZYJmemJT1dMg+/uSnTN7ahfR8Mn+8HIs2CX46HHVGh4ZmJJsiRJkiRJio3vzKU6R8HutyXCuq1Kgo5vQvlm5CuhSzBlJ9j1EjhiRiJs3F7rfoWR58DHbSC5GBw6FvZ5EIpXzMmKJUmSJEnSdnJqrRQ0vxqWT04sI93Y5fZHIdCq2ZX84bfAsWR1aHY5NDwLipff/tNlpMOMJ2DCtZBcHNo9C/V65/0egJIkSZIk6W8Z5ElBCK3aPgkrpsPi0b/vRReaVsN01ibnx1zgb4M2Ql0V94DdroI6R0PyDv4n/MswGN0Hlk6AXS+GljdAsXI5VbEkSZIkScpBBnnSRikloPO78NFesGZ+YuBD5Taw9/35IMBLh527QbN+ULX9jnfLrVkAE66Cmc9A9f3hsAlQoXlOVSxJkiRJknKBQZ70RyWrQpeP4OPWkL4G9rpvx7vedmT/u6Ri0OisxB54ZRtu/7ky0mDZd/DrSPjhWVg8FkpUhPavQN1jXUYrSZIkSVIBYJAn/VnoTDvgC/h0XyhWNo+f/LdArUQVaNYXGp2d/WETGRtg2SRYPCZxWTQ8cT1j/e/HVG4HB3wCxcrkbPmSJEmSJCnXGORJW5LXAVfovgvLZ8s3T+x/F7rkUopv+3Hp62HZxN9Cu9G/hXbf/bbHX/Jv592w+WNSSkHndwzxJEmSJEkqYAzypLisX/z75zUPSex/V63z1pe5pq+Dpd9uHtot/z4R2oXALnTzbRrSEWQk9vn7o6RkaHljYgmxJEmSJEkqUAzypDjMfjUR5FXtkJiWW27Xze9PXwtLvoElY+DX0fDriN9Cu/Qth3bh9m1KgpI1YNeLcvzlSJIkSZKk3GeQJ23Jhg1RQ1uu+HUUDD8VGl8ArR/a8jGfdkx03W13aLclmbDXvZBScjsfL0mSJEmFQEYGxQlbEJWIuxIp25IyMzMzs/8wqZBYswaGD4fBg2HQIBg/HlatgrTfgrNiqVCxErRtC507Jy6tWkFKCNi2w+p58HEbKN8Sunyw9Ym4k+6Eb69PLI3dbLnsdgqBYMU94ZBRTqiVJEmSVHR8913i/V64DB0Kv/wC69ZFd2UmJ5O0007QrBl06ZJ4v9ehA5QvH3fV0lYZ5KloGjgQ/v1vGDIE1v9hmmtWlCsH3brB9dcnvuFnVdqqRKdd+hroOgyKV/j741fOgjEXw4/vJgZX7GiL4MFfQ9V2O3YOSZIkScrv5s+H226D115LBHfZEZo29toLLrsMjj/eRgjlOwZ5KlqmT4fzz4dPP93xcyUnw5FHQrt2MGoUTJgAK1cmuvzCfeE3O5Uqwb77QqeOUOJZyBgFXUdA2YZZf575H8Oo82DV7O0L85JSoXZP6Ph69h8rSZIkSQVFaNK45Ra45x5Yu3bHz7fnnvDoo4kVWlI+YZCnoiO0UvfqBUuXxldDwzpw9D+gXz+oUiXrj0tfD1Puh29uSCy1zc5y2xDk9ZgKZepvV8mSJEmSlO+F93nh/V5435eTihWDp5+G3r1z9rzSdjLIU9EQ9r4LnXG/7YUQuzJl4Mor4YoroHjxrD0m/Kc646nEctv01YkhGGGAxbZCvF0vgb3uzpGyJUmSJCnfSU+Hjh1h2LDcOX9YXvvmm4mgUIqZQZ4Kv/BXfPfdYeJE8p3WreHtt2Hnnf/+uOVTYPRF8PMnUK831OoF31wLK6b//XLbYuWh56xt78cnSZIkSQXVnXfCVVfl7nOEFVVTp0LFirn7PNI2hB30pcItTCbKjyFeEPbWq1UrsedC6NAbMABWrPj9/g0rYfxVMKAlrP0ZDhoM+z0PuxwDh0+Evf4DKTslOu/+Ihn2uM0QT5IkSVLh9vjjuf8cixYluvKkmNmRp8LvrrsSIdlvwl/4X6nMHOpGl9nsEn2cR23WJJVmQ3JxNiQVJ4lMimWsp1jmespkLo+O3oXZvz0q8XlZVuZ8vWFK0lFHwTn7wvJ7IW0l7H4LND4PkrcQ2K35GcZdAbOeg6QUyExPhHhl6kH37yG5WM7XKEmSJEn5wc8/Q82am920hpJ/eb8XLkuTKrIhqUT0fi8jKZnUzA3R+70SGWvYmfmbvecLH6uxkOQ/bmd0+unQv3/ev0bpD7bUxiMVKivSSjGUQxhMZwanHsSEzJasTi+56f5qFdezS91MatcvRrnSydFepuESIu4NGxKX5UvTGTAjjTk/JrN05e/BWMXU5bTJHEHn9M/D2dmH0RRnw47v7xDGpL/xGhy9GzwxDMrX2frxpWrAfs9C43Ng5Dmw7LvEctu97jfEkyRJklSopSel8g17Jt7vJe3PsNQOLNhQedP9pUumsUutNOo2SKVS1VRSUxPv95KTIS0t8X5v7ZpMxszewJuzM5m/qDiZmWE/ciievIHdUqbSecOn0fu9Tmnl+f3MUjzsyFOhE/5Gf/01vPNOYmDRmDGZpKcn0aTeOjofXII2baBePahbF+rUgVKlsnf+5cthzhyYPRtmzoShQzIY/HkaP/9anFLJa9kvaThd0j/jOF6lCdN2/AV16pR4MRWysEQ2Ix1mPAFpq6HppYlNWSVJkiSpEJk/H155Bb74AoYMSQysrVh2Ax07JdOhcwpNmiTe7+2yS2JLu+y8LQrB3rx5v7/nGzsWBn+6jgmTEwFfixbQuTN06waHHJJYUCXlJYM8FRq//grPPgtPPAGTJ0PTprD//olvsiEL+1O3dY4K/xVNm5YIDgcPyuDzT9L4aVFxOqd8xT/TH+Fo3qAkOzAx9+CD4eOPDeYkSZIkFUlh4dJHHyW2w/vgAyhbFg44IPF+L1xatkx02eWWJUsSoWF4zzdoUCLgC40hZ5yRuITgUMoLBnkq8MI300cfhddfh+LF4cQT4eyzYe+94/0h88kn8Pgj6bz3QRLlk1dwctrTnMcj7MrU7Tvp//4Hp5yS06VKkiRJUr7uvgvh3VNPJTrlQrPGP/8JRx4JJX/fMSnPhUaOJ5+Ep59OzME47LDE+9AePXI3UJQM8lRghUG0/folfivTunXim+YJJ0CZMuQrP/2U+Ob+xH/XM2d+KmdlPsnN/IvqLMzeicKvmwYOzK0yJUmSJCnfWLEiMbfwP/9JvMcLcybOOgsaNyZfWb8+sRNSWBn26aeJhpJQc+gSlHKDObEK5Df0iy6CPfZIDCgK2dbIkYlv6vktxAvCkt5rroHps4vzVP9k3q96Oo1SfuABLiI9O/8JTpiQm2VKkgqIevXqseuuu7LnnntGl1fCJkF/8vTTT5OUlMTbb7+9xXN8++23dOrUiaZNm9KiRQvOOOMM1qxZs+m+jecOl/B8lSpV2uJ5BgwYED0+XD4OW0D8pn///tx+++059polSUVHaDV69VWife7+7//gxhsT+9XdeWf+C/GCsCrs2GMTK7LGj0/sydelCxx3XKKpQ8ppduSpQAm/4TjzTFi5Eu6+G047reBtLrpqFfz733DnHRnslTSO/6X1pilTtv3A+vUT0zUkSUVaCNZCQBdCti2ZNWsWJ554IuGfeFdeeSW9evX6yzHTpk2Lgrvdd9+d9PT06PhmzZpxY3i39Cd9+vSJQsEHH3zwL/fts88+vPnmm9HnRx11FKNHj2bBggWccMIJfPrpp6SG0YCSJGXRggWJBo3330/sO3fHHVC1KgVKSFjCHn59+iSGcNx/f+J9q5RT7MhTgRH2Hzj0UNh3X/j++0SgV9BCvKB0abj1Vhg3PpmMFruzX+pIvqL9th94+OF5UZ4kqQDLyMjgrLPOikK3EiVKbPW4xo0bRyFekJKSQuvWraMA8M/Wrl3LCy+8wJnhh+4WFCtWjNWrV7Nq1SqKh5YE4NJLL+XOO+80xJMkZXvPuXbtYNKkxDTasCdeQQvxgjCfsHv3xOsIy4HDJazQsoVKOcUgT/le+IZ3002JDU2vvTYxZrxaNQq8MLb8y6+L0enQ0hycPJC3+GvHxCZhzfCll+ZleZKkfOyUU06hZcuWUcD2yy+/bLr93nvvpX379uydjYlPIYR78skn6dmz51/uC912DRo02Gr331133cWpp57Kaaedxj333MP7779P9erVadOmzXa+MklSURS2StpvP6hcGYYPTyxNLehCA8d99yX2Sw97/YWuvA0b4q5KhYG/Ks2G9Ix0lq9bTsVSFeMupchIS4Pzz0/8NiZMpj3nHAqVUqXg9bdS6HNBEkc//gYP0YfzeWTzg8LIo/DiGzSIq0xJUj7y5ZdfUrduXTZs2MB1110XBWlhr7qJEyfyxhtvRPdn1fr16zn++OPp2rUrR4bxf3/y1FNPbbUbL+jYsSMjRoyIPl+xYgXdunXjo48+4v7772fo0KFRqBfCxY3depIk/VlYhhr2kwvDIcLeePlx3/MdEQK86tUT++iFPd5ffx3Klo27KhVkduRlwZI1S/jP1/+h0YONqPmfmpzxzhmM/3l83GUVCddfD889FzoCCl+It1FYefTIo8ncfEsyF/Bf3qbn5r/GeeMN6N07zhIlSflICPE2Lmu95JJLGDJkSHQ9fAzLY8Oy2bCP3vDhwzn77LN55JE//YLoNyEIDCFezZo1eeCBB/5y/w8//BCdI+yflxXXXHMN1157LT///DPvvPMOr732GpUrV46W5kqStLV5fuH3SCHIC5NfC1uIt9FhhyWWC48bB6ee6jJb7RiDvL8x+ZfJnP/B+dS+rzY3Dr6R7o27c0/Xexg+bzitHmtF52c68+bkN0nLSIu71ELpq68Sk4lCO/IWVvsUKmEfheuug9NOyeCfqU+zgGrQo0di9FH79okJGZKkIi8sg10ads7+zUsvvUSrVq2iz8877zx++umnKMwLl3333ZfHH388uv3P0tLSooEUYRptOCYMs/izMHk2dOlVqFBhm3WFwG/ZsmUceuihUY0bz5ecnMzKMKFKkqQwHX3RIpg7F+bPZ+3PSzmpdyZt2yb2Qy9WjEKtdevwcxveeguefTbualSQObX2TzIyM/ho+kc8MOIBPpnxCQ0qNuDCNhdy+p6nU75k+eiY8CX7bOZn0TEfTPuAuuXr0qd1H87a6yyX3WZX6C0ePDisE4J582D16rDOh+UpFdlz9JM0a1+R9wekREFXUbB8Oeyx2waa//QZ72V0Y7OXXbNmot9846VZs+1/osXjYMMyqNoekgv5T0xJKkRmzpzJ0UcfHU2aDf8eCfvXhW660IH3Z126dIk69jZOrb3++uvZeeedOffcc6MuuZNOOikaeLExdAt76z388MObhmbssssuPPvss+y///5/W1Po7DvooIN4/fXXqfrbruRh4EYI96pUqcJbb71FxYr++0iSipzZs2HQoMT7vXCZOXOzu/tyD4+XuphvJqVSvz5FxmWXJYLL0I1YlF63co5B3m9WrFvBM+Of4cGRDzJt8TQOrH8gF7e9mG6Nu5GSvPXRqNN+ncZDIx/i6fFPk56Zzsm7n8xFbS9it6q75Wn9BcrXX8MzzyS+mU+dusVDzk56nDfLncbE74tRowZFSlgh1blzJk9knsWZ9N/6gWGjhTDT/JJLst+D/lIxyEyD1DKwczeodQTsfCiUqLzD9UuSJEkqosI0h/794e67YcaMrR72JR3pzJfRoWGqa1Gydi2EmVRVqiRyzqLStKKcU+SDvBmLZ0ThXf9x/aMlsiGIu7DthbSo1iJb5wlDMJ4e93R0rhlLZnBQg4M2BYHJSa5gjnz/PfTtCwMG/O1hSylPtaRfeOjRYpx9NkXSWWdmMvK57/lmQxYC4TDC9957s7eP3ot/+GmRlAKZGYnPK7eBOkdCrR5Qrpk/VSRJkiRlzeefJzY2nz59m4cem/Q6P7XpyZBhqUXyLUdYkBYWWYU987YyGF7aqiKZMG1cGtvjpR40frBxtM/ddZ2uY+6lc3msx2PZDvGCciXKcfG+FzP1wqm894/3SCIpOn+TB5vwwPAHoqCvSAsbXYfvUNsI8YJ3wrCH5ORoqk9cfz9uuOGGaPlR6dKl6dSpUzQJMC/1PimJbzc0YzJNt33wwoVw0klw1VXbt2tqZnr4v8Tl1xEw4Tr4oDm8XQdGXwg/fQLp67brdUiSJEkqAp54Arp2zVKIt5LSfJB0OCedFn+IF/aCDVtMfPbZZ3n6vB06QJ06iSm9UnYVqSBv9YbVPDb6MVo80oKDnzuYZWuX8eqxrzLz4plc0f4KKu+048sKQ/dd9ybd+eTkT5h0/qSoM+/qgVdT+97aXPzhxdFS3CInbABw8smwLmth0KvJ/6DrwZnEtZ3OPffcE23w/fHHH7No0aJoz6BDDjkkTzfr7tQJqlXcwGtkI80Mk0HuuGPHnzwsuQ3W/AjTHoMvDoHXKsDgnjCjP6xZsOPPIUmSJKlwePNNoqVU6aFBYNs+4HDWZRbnqKOIVdgHdnXYoz0GyclEjSshyCvaayS1PYrM0tpZS2ex12N7sWrDKv7R4h/RPnZ71dwrW+d4/vnnN20Cfeutt3LggQdm6XGL1yzmqbFP8dCoh5i7bC53H3w3fffrS5EweTLstVdiI4AsWEY5qib9yhNPp0ZjueNQv379aHPwiy++eNNkv5o1a3Lvvfdycggk88gFF8CQJyZnbXntRmHU07ffwq67Zn1pbbZz/wyo2ApqhyW43aHini7BlSRJkoqiFSsSExt+/TXLDwnLahd3PIKBg+Mbujdv3jz2228/vvrqq2i406effhoNbspLI0bAvvu6vFbZV2Q68pasWcKStUt49PBHeabXM9kO8ZYuXcpdd93FF198wXvvvRcFPWFiXFZUKlWJfu37MfG8iZQtUTYKFYuM++7LcogXzKM2GzJTo80/47Bs2TJmzZpFmzZtNt2WmppKq1atGBe+w+ahkH/OzPjrFMJtbi77/PO5VFHYR++3vfSWjIOJt8BHe8GbNWDE2TDvPUiL5zdakiRJkmLwzjvZCvGCmcWasHfb+EK80Mt0xhlncN1111G3bt3Y6mjVKvHxT8N8pW1KpYhoVbMVxzc/njuG3sFJu59EsZTsfeMYMWIEHTt2pGTJktGlTp06zJgxgyZNmmT5HGG6bXpGOtd2upY4vlmF4HHjJSMjI1vXt/cxh775Jjtlo87llIs+li2ba1+Kv3/+5Ym9DCtUqLDZ7RUrVtx0X14pVw5WpZcinWRSNgZoWTHsC1g6iVyXuSHxcd1CmPEUzHgCkopBp3egdHw/ECVJkiTlkaEfZ/sh4T1fXO/3gkceeSR6f3x2zJMVixeHkiXDe9BYy1ABVGSCvOC2A26j2cPNeGrcU5y7z7nZeuyvv/4ahTkbhc/DbVn1y6pfuO3L26gzrw7n9D4nx8KzrAZsca2gDl+h7AR5xUiEQ2m/bdOW18qF9Oy3Dsw/WrJkCbVq1crTWkJzXXIU42UjxAuWDYUB2R/YsmMyfg/3BnfL4+eWJEmSFIsfs/+QYqTF9n4vNOPccsstDB8+nLiFt+jhPV/YHUnKjiIV5DWs1JDz9jmPGwbdQO+WvaNlrllVuXLlKMzZKAQ94basuuXLWwgZ1W7LdqNkmZKkpKREl+Tk5E2fb+l6Vo7Jq8ds13kPPRQGDszy16kciV9HLF4MDRuS58qXL0+9evUYNWoU7dq127RH3vjx4/N0f7yNX4NyqatJyu4PuROvh27H/f0xOxr0JaUkpt2WrA7VD4JqnaHSPpBSfMfOK0mSJKngKDcS3jsjew/JXBq914nDkCFDooacvf+0l9PRRx/N8ccfz+OPP56n2wuG3bp+6yWRcmfYxUUXXcS7777L7Nmzo/3C9vybHRm//fZbLrzwQhYsSEy4vO222zjqt7E0Tz31FHfccUfUKXbAAQfw3//+l2JbiKG7dOnC119/HW1EWa1atei2mTNn0qhRI4444gjefvtt9t9/f84880xOOumkTUMowiUEbWEJbNC0adNoSEUYThE64xo92IhL972UG7vcmOUvVDhf586dGTlyJKtWrYo+D+FOCKu2Zfri6VEn4IOHPZjtTsAC74UX4Lc/m6xII4UaqYu47KYKXHMNsbj77rt58MEHGTBgAA0bNoz+Pj3zzDNMmTKFMmXK5FkdPbuns+6jz/kovWvWHxRmmE+cuO2fBtkddhEFd7913VXZ9/dBF+WaOuhCkiRJKqoyMqBDBxg2LMsPuZj7+bj+eUyeUTzP30qEKbWL/5Qihm2zXnrpJbp27UqlSpXyrJY33khMrv3xR6hZM8+eVkVt2MUxxxyzaarLtv7j6NmzZxSATJ48mYkTJ0b7ywU//PAD//rXv6IkfPr06VHQ93ep9+67785zzz236Xr//v03S89DkDdo0KBN18MwihAwbmyV/emnn6LhBe3bt4+uVy1dlavaX8U9X9/Dzyt/zvJrD3um9e3bNwoXDz/88GiCaVZCvODaz6+lYcWGnNnqTIqc3r3h8MOzfHgq6Ryd9gqvvrCeuFx++eWcdtpp0dSi0HUZ/q5+9NFHeRriLVsGH30Mx6W/lPUHheA6/LeSU7/SSf4tXE8tA3WOgf2eh6MXQdevYbd+UL6ZIZ4kSZJUlCUnw7PPQpUqWX7IcbzKlB+K8+235LmddtqJ2rVrb3YJqlSpkqchXvDqq9CpkyGecjnI69Sp06a/6H/nxRdfZN9996VDSOYhCryqVq0aff76669H3XQ1atQgKSmJc889N0q/t+bUU0/lf//7X/R56OB75ZVXOPHEE7cY5K1fvz4KCv/5z39uui18DLVs7M4LLt73YiqUrMBNg27KzsvnlFNOYdiwYdHl4IMPztJjRswbwauTXuWOg+7I9oCNQiP8+Wbx67XxG/uE74ozZQqxCH8vb775Zn7++ecolP7yyy9p2bJlntbw7ruQkZ5JL97O2gPCD51PP4XOnXfgWZMSnXdBmQaw6yVw0GA4Zgl0eBnqnQgl8vaHmyRJkqR8rlEj+PpraNAgS4e3Yxi1ii2Igqz8ICxSDE0ceWnVKnj/fThuGzsiSTsc5GXVd999R4kSJejevXvUHRcCsF9++SW6b86cOZt19IX9yMJtWxPaXEPoF6bGfvLJJ+yzzz6bDZ1o27Yt8+fPZ+7cuVEXXps2baKuudCZF4SPYfnuH+1UbCdu3v9mnhj7BN8v+p7c/IZwxWdXsF+d/ei5a0+KrDCS6IMP4KqrsrSTZ2cGUy31V/77cDwDOuIW9kl49KE0Dk4aSCV+35dxq78BC3v3heW0vwXn2ZKU+nuAF/a5a/Uf6DENjpgBre6Cap0guUhtpSlJkiQpuxo3hvHjoW/fxEqhv5FMJsdueJH/Pbk+2ieuKApNjGvXZvLb7mNS/EFeGA7w2Wef8dhjj0V76YVpn+edd952n++MM86I9tULl/D5HxUvXjxaNhs678IlhHhhX7Owr97atWuj20LX3p+dusepNK3SlKsHXk1u+WDaB3w5+0vuPvjuqMurSAsB3r//TdRmd+qpUKLE3y6vvT3tCh58KHRUUuTcdx+MHAW3ZvzN383ddoPzzw+bUSZ+CmS3HzvsbVesAtTrDR1eg2MWw0FfQNOLoWyjHX4NkiRJkopgA8c998D06XDllbBPGIa35e2oruAuVv+6hssu+W0f7iJkxgzod1kafTPupsYRbeDjj+MuSQVMrrTa1K1bNwrPQoAXhEEUhxxyyKb7wsjnjcL+deG2v9OrVy+uvPLKqMsvDKx4NgQXfxCeK3TehWW1jzzySHRbWE772muv8eOPP0Zde3+WkpzCXQffxeEvHs7QOUNpXzexh15OSctI48rPruSoZkdFHXn6Tf368Mwz8OijMGIEDB6cuITNUdes2XTYGfTn3aRenNr7UL75rhjly1MkfPMNXHtNBjc1fI69qu8E61tDqVJhM4fEb7nC0tmwkcJvS9W322ETEl14yVnb51GSJEmSsiTkAHfckfh8+XIYOjTxni+sIgprSlevpmZqKo+vuJ9j+t9Aj55wxBEUCWlpcPI/NtAgbRq38C8YtR4OPRRCXvLUU4mvnRRHkHfcccdF3XPLly+nXLly0fTPPfbYY9NY57B33o033kj16tV59NFHOeGEE/72fGF/u/vuuy/amDI5LCX8kxDkhfOkpqZGE2qDMFX2lltuibr1QtfelhzW6DD2r7c//T7tx9AzhuZo19wz459hyqIpvHX8Wzl2zkIltFuHUGrjnm4bNsCSJdE3ddatI6lkSZ5YXYaWXVK54ILEDIfC3tQYXnoY8LtP62SuGHw6pJ6ee0+WsuX/JiRJkiQpx4RBfIcdlrj8ydFhH/pT4ayzYMKEojH0ISxSGzMGRmccTwn+MOAxdOW1awcffZRYfSXlVJB3zjnn8MEHH0RDAEKHXdmyZaPJs8FZZ50VDbEIl9Bhd80117DffvtFwVvozNs4mbZBgwbcdNNNm6bIhqWw4bzbctTfLB5v3bo1S5YsiabJbhSCvHDePy/F/aMQ3IWuvNZPtKbaPdVITsq5lcZL1y7lnL3PoUnlJjl2zkK/9LZatc1uCtf694eePcMSanjssSxtsVcg/for9OgB8+bB6NGQ6rZ0kiRJkgq5//u/8H4eOnZMZFhhbkZhlJkJt90G118PD9CXlkz860Fz5ya688IyrQoV4ihTBURSZpjIUMS9PPFlFq1elOPnPa75cVQrvXk4pewLczLCNJ/QvBcmG5UpQ6Eya1bi+3VYWRx+eDVrFndFkiRJkpQ3FiyA7t1h9uzEJNc2bSh0wwz7XJARNaY8RB/OJ7Ed2FaFvdAffjivylMBZJCnAmHkSAgNl/XqJYK9PzXvFVhhsFPoMg+v58MPYeed465IkiRJkvLWypVwzDEwZAi89hp060ah2T7pxOPT+WhAOi9lHM+RvL3tB1WsmEg3C+tyNOXPqbVSTgu/lQl7pIYlqC1bwiuvJNqTC6r16+H228NQlkQH3pdfGuJJkiRJKprCqqv33oNjj0105114IaxYQYEW5nu0arGeLz9cxWcZB2QtxAvC3vE//pjb5akAM8hTgdGkSWJj0DDRKMxHCR16kydT4AwcCHvvDbfcAjfemNjXtKhM5ZUkSZKkLQkNaE8/nbi8+GJi5sNLL0FGBgXK/Plw+qkZdOkCjWcPZHx6CzowNOsnCFMey5bNzRJVwBnkqUAJXcZPPAGDBiUGQ4TuvLCFwA8/kK+F7sFRoxLh40EHQZ068O23cNVVdkxLkiRJ0sYM69RTEw0b4X1T796JVUyffJL/A72FCxPDLBo3SGPgiwt4heN4L6MbdZmbvROF6bWVK+dWmSoEDPJUIIXBF+PGJSbZvvMONGwIhxwCr7+eWLaaXyxbBo88AnvtlVgeHDqkww+hAQMK70QmSZIkSdoRYQ/x0JkXVmSF5rTwXq9xY/j3v+Hnn8k3Qrj42Wdw3DHp1N45nQduX8l16/7FlLQGHMdrJG1PkhnSQOlvOOxCBd6GDYnpRo8/nlimWrUqnHYanHwyNG+e+F6Y1/UMHw5P98/klVeTom/uYeruP/8J7dvnfT2SJEmSVJCF1UxhZdZzzyUGY/ToAWedBfvvD6VK5X09YUXYyy/Dk4+sZ+bc4uybOoqz0/7LcbxKaVZv/4mvuQZuuy0nS1UhZJCnQiWMLH/qKejfP9H9FkK9Tp0SHXxhj4IQ7CXncB9q6AAMy2bDZqaDB6Yx9GtYtTaVlnWWcPYVFaN28LAkWJIkSZK0/dasSazCCqFemHBbvHhi5VN4vxcu++0HpUvn7HOGxGTGjN/e70WXTObMSaJC6gpOSevPWTxJSybu0HOkAe+2a0ebV1+ldu3aOVa7CieDPBVKaWkwduzv32zDN/nly6FSJWi9dzr1GqRQty7RZZddEh9r1YLU1C2fb+1amDs3ERTOmZO4hM9nTtnAqDHJrFmfQs1iv9B5w0A6M4jODKZp5xokDfoir1+6JEmSJBV64T1Z2Dt943u+ELaF93NhsGBYhvvn93vhEqbjbklYRRWW7G58n/fH93zhfWVoEilZMrF9XRQa7reBfUc8QMl7b09Mmd0BGV268Ga7dlz63HMsXLiQc845h6uuuoqdd955h86rwssgT0VCejqMH5/4Bj/+xreZs6Yqc5J3Ye6GGqRl/p7elSiWTrHUzOgS/svYkJYUXdanpWw6plTyWuqmzmeX9JnRpQ0jo+CuEdM33wMhfON1bLgkSZIk5bowDDG83xs2DGbNSoRw4bJixe/HhEGDGy8pKYltkcIKq3DZmIyEFVzhrdzGADCs6grhXevWUKLEFjZFD8vBPv880T0Srm9L2Gtp40mPOSaxdAxYt24dTz75JLfffjuLFy/eFOjVqFEjJ79MKgQM8lT0dOgAQxPjv9NJ5mdqMIe6zKM2ayjFBopFlyQyf/tsA2VYSV3msAuzqcyvWdu0dI89EumhJEmSJCkWS5f+3l0XcrYQ3oVLaPbYGOqFgC6EdxtXaoXbsi209U2YkEgTJ02C1asTa4FDm+BOOyUm0YZN08PeT1WqbPU0a9eu5fHHH+ff//43y5Yt47zzzuOKK66gevXqO/R1UOFhkKei5557oF+/3H+eG26AG2/M/eeRJEmSJBUqa9as4dFHH+WOO+5g5cqVXHDBBfTr14+qYSN4FWkGeSp6Qm916JYLo4ZyS/h1zsSJTrmQJEmSJG23VatW8cgjj3DXXXexevVqLrzwQi6//HIqhw4/FUkGeSqaRo6EQw5J9FnntLCD6gcfJFqmJUmSJEnaQaEr7+GHH+buu++O9tO7+OKLueyyy6gUJjqqSDHIU9EV9i044giYOTPnzlmnDrz9Nuy1V86dU5IkSZKkaIHZCh588EHuuece0tPTueSSS7j00kupUKFC3KUpjxjkqWgLG5D+5z+JS1YmDP1dF94ll8CVV259prkkSZIkSTlg+fLlPPDAA9x7772EWCd054UuvfLly8ddmnKZQZ4ULF4Mr76amDAULj/9tO3HVKuWGBkeLscdB246KkmSJEnKQ0uXLt0U6KWkpNC3b18uuugiypYtG3dpyiUGedKWTJuWCPTCjPLQtbd2LW8MGEBy6dIceeGFifCuadO4q5QkSZIkiSVLlnDfffdx//33U6xYsWjCbZ8+fSjjirFCxyBPyqKrrrqK999/n4lhGq0kSZIkSfnMr7/+GnXnhS69UqVKccUVV3D++edTunTpuEtTDknOqRNJhd0ee+zB999/z9q1a+MuRZIkSZKkv6hcuTK33XYbs2bN4swzz+TGG2+kQYMGUbi3Oqw2U4FnkCdlI8gLU4G+++67uEuRJEmSJGmrqlSpwh133MEPP/zAKaecwnXXXUfDhg2jTr01a9bEXZ52gEGelEVNmjShRIkSTJgwIe5SJEmSJEnapmrVqnH33Xczc+ZM/vGPf0RbRoVA76GHHnK1WQFlkCdlUWpqKi1atDDIkyRJkiQVKDVq1IiW14ZA79hjj+Xyyy+nUaNGPPLII6xbty7u8pQNBnlSNpfXGuRJkiRJkgqimjVrRstrZ8yYQa9evbjkkkto3Lgxjz/+OOvXr8+5J8rMgFVzwfmqOc4gT8qG3XffPQryHPYsSZIkSSqoatWqFS2vnT59Oocffjh9+vSJtpN66qmn2LBhw46dPLxfHn0RvFMXxlxsmJfDDPKkbHbkLVmyhHnz5sVdiiRJkiRJO6ROnTrR8tpp06bRtWtXzj33XJo2bcozzzxDWlpa9k8YQrtxl8P0R6DJRTDtYRh/hWFeDjLIk7IZ5AUur5UkSZIkFRa77LJLtLx26tSpdOnShbPOOotmzZrx3HPPZT3QC2HdhGvh+/ug3XOwzwOw77Mw+T/wzfW5/RKKDIM8KRsqVqwY/cbCIE+SJEmSVNjUr18/Wl47ZcoU2rdvz2mnnUbz5s154YUXSE9P//sHT7wFvvs3tH0K6p342wl7Q9snYdKtMPHWPHkNhZ1BnrQdXXnffPNN3GVIkiRJkpQrGjZsGC2vnTx5Mm3atOGUU06hRYsWvPzyy2RkZPz1Ad/dCd/eAK0fgYan/+lkZ0Dr/8I3/4Lv7s6z11BYGeRJ2eTkWkmSJElSURAGYITltZMmTWKvvfbixBNPjIZAvvbaa78Het8/AOOvgr3uh8bnbvlEjc+Dve5L7Jc35cE8fQ2FjUGetB1BXtgIdPXq1XGXIkmSJElSrgsDMMLy2okTJ0adeccffzx77rkn4149B8ZeAnveCU0v3sZJwnF3wJiLYPrjeVV6oWOQJ21HkBd+8xC+gUmSJEmSVFTstttu0fLasN3UuV2L0yrtcWh5M+x2xaZjQvdehw4d6NixY/Rx1KhRfzjBldDyRhh5Dsz8XzwvooBLjbsAqSDuFbDTTjtFy2vDXgGSJEmSJBUlLUpPoMVeY6H5NdDius3uq1q1Ku+//z4VKlTgu+++iybgfv3113948PWQvg5GnAHJxaHeP/L+BRRgBnlSNqWkpNCyZUv3yZMkSZIkFT1zXoPhp0DTS2H3WyEpabO7q1WrtunzEiVKRO+hNxOO3+M2SF8Lw05OhHl1j86r6gs8gzxpOzjwQpIkSZJU5Mx7B4aeCI3Og1b3/CXE+6O0tDQuuOACrrtu8469SHjcXv+BjHUw9ARIfhNq98jd2gsJ98iTtjPIC3sCZGZmxl2KJEmSJEm5b/6H8NWx0OA02Of//jbEC/vKn3zyyfTs2ZNDDjlkyweFx+/zIDQ4Fb46BuZ/nHu1FyIGedJ2BnnLly9n1qxZcZciSZIkSVLu+nkgfHkk1D0B2jwGSVuPk0LDS9gXL7xvPu+88/7+vOE8rR+DusfBkF7w8+c5X3shY5AnbYfdd989+ujyWkmSJElSobbwSxjcA2r3gn37/22IF3zwwQe8+OKLfPTRR3Tp0oWjjjrq78+fnAL7Pg21eiSeZ+FXOVt/IZOU6dpAabun155yyinccMMNcZciSZIkSVLO+2UYfNEVahwMHV6B5GK591wZGxJLd0NX3gGfQpW2ufdcBZjDLqTt5MALSZIkSVKhtfRbGHRoYqpszYPhh2dz/zlrdE10AH5xCBz8FVRokfvPWcAY5Ek7EOQ9+2wefCOTJEmSJCmvhQAvLOLM3ADjrozn+fUXBnnSDuyTN3PmzGjoRbly5eIuR5IkSZKknFNuVzhuedxV6E8cdiHtQEde8O2338ZdiiRJkiRJhda6devo06cPjRs3pmXLlpx00klbPO7zzz+nTZs27LbbbjRv3pwrrriCjIyMTfe///77NG3aNDpPGMIRGnO25LTTTiMpKYlx48Ztum3FihWUKVOGPffcM7p++umnc+utt266//nnn48eM2vWrE23HXrooTz11FPkJIM8aTvVq1ePsmXLuk+eJEmSJEm56KqrropCsqlTp0bNNPfcc88Wj6tYsSIvv/wy3333HWPGjOHrr7/etCXWypUrOfPMM3n77beZNm0aO++8M7fccstWn3Pvvfemf//+m66/8sorNGvWbNP1/fffn0GDBm26/sUXX9C2bdtNt6WlpfHVV19xwAEHkJMM8qTtlJycHC2vNciTJEmSJCl3rFq1Kupqu+2226IwL6hRo8YWj23VqhUNGjSIPi9ZsmTUPbexQ+7DDz+M7g8decH555/PSy+9tNXnDR17oYMvdAMGTz/9NGecccZmQd6wYcNYv359dD2EdldfffWmIG/UqFFUqVKF+vXrk5MM8qQdXF77zTffxF2GJEmSJEmF0owZM6hUqRK33347++yzDx07dmTgwIHbfNzPP//M66+/Tvfu3aPrc+bMYZdddtlsld1PP/0Udc5tyU477cTBBx8cdfB9//33ZGZmbtaRV6dOHWrWrMmIESOic4cau3btGnUBbuzQy+luvMAgT9rBIC+09f5xzb0kSZIkScoZIWibPXt2tO/d6NGj+b//+z+OP/54FixYsNXHhL3vevToEe2RF8K/7RU68EI3YLiEPfH+bOPy2nDp3LkzpUqVolq1avzwww/RbeH+nGaQJ+1gkBfafMNvCCRJkiRJ0o4Je9qFJbHhEpaz1q1bN9raqnfv3tH9YXlsWK66tcGTYShFGDLRs2dPLrvssk23h/OEQHCjsOQ2dNSlpqZutZZ9992X+fPnR/vunXDCCX+5PwR1ofMuXLp06RLdFgK9Tz75hKFDh9qRJ+U3LVq0iNbou0+eJEmSJEk77pRTTmH8+PHRJXTBhX3mDjzwQD7++OPo/tDtFi5/XOa6URhoEUK8cLnuuus2uy/cNnbs2GiZbPDf//53i+Hcnz3wwAPRcI0w7HJLQd7w4cMZPHgwHTp02BTk/ec//6FWrVrRJacZ5Ek7oHTp0tHYaoM8SZIkSZJyx6OPPsrdd99Ny5Yt6dWrF4899timkOyss87i3Xff3RS6jRw5kjfffHNTV18YkhGEIO7JJ5+MHt+oUSPmzZvHv/71r20+dwgRw1LeLQkdfaHTLyynLVOmTHTbfvvtFwWNudGNFyRlht36JG234447jrVr1276xiFJkiRJkpQb7MiTcmCfvKgjb+0iWDQy7nIkSZIkSVIhZZAn5UCQt2bpHNI/6QCftIVJt8ddkiRJkiRJKoQM8qQd1Gq3ugz6VypJGeug5U0w4VqYfG/cZUmSJEmSpEJm6zN2JW3b+qXsPOV0qtWrRvJBX0CZepBaBsb1hZQS0OSCuCuUJEmSJEmFhEGetL02LIcvDiVp7c8UO2RwIsQLml0GoTtvdB9ILg6N/hl3pZIkSZIkqRAwyJO2R9oqGHQ4rJoFBw6Cso02v7/51ZC+FkaeA8kloMEpcVUqSZIkSZIKCYM8KbvS1sDgI2D55ESIV77plo9reWMizBtxemKZ7S7H53WlkiRJkiSpEDHIk7IjfR0MOQoWj4UDP4cKLbZ+bFIS7HlHYpnt170Ty2zrHJmX1UqSJEmSpELEqbVSVqWvh6+OhV+GwgGfQKVWm939/PPP065du+gycODA38O8ve6Dhv+EocfDjx/EU7skSZIkSSrwkjIzMzPjLkLK9zLSYOgJ8NNHsP/HULX9ZncvXbqUTp06MXLkSFauXMn+++/P+PHjSUlJSRyQmQEjzoJZL0Lnd6Fm13hehyRJkiRJKrDsyJO2JSMdhp0K8wdA5/f/EuIFI0aMoGPHjpQsWZIqVapQp04dZsyY8fsBScnQ5gmoczR82QsWDMrb1yBJkiRJkgo8gzzp74ROupFnwdw3oNM7UL3LFg/79ddfqVix4qbr4fNw22aSU6Dd/2Dnw2Fw98QSXUmSJEmSpCwyyJO2Jqw6H3U+zHoBOr4BNQ/e6qGVK1dmyZIlmy21Dbf9RXIqtH8Rqh8IXxwGi0bmVvWSJEmSJKmQMciTthbijbkEZjwJ7V+BWof/7eFt27blq6++Yt26dSxevJg5c+bQsGHDLR+cXAw6vJpYovvFIbB4XO68BkmSJEmSVKikxl2AlC9DvPFXwrSHYL8Xoc6R23xIhQoV6Nu3L126JJbe3nvvvb8PutiSlBLQ8c3EEtsvDoYDB0GFFjn5KiRJkiRJUiHj1Frpz765HibemtjPrv7Juftcaavgi0NhxVQ4cDCUb5q7zydJkiRJkgosgzzpj6Y9ktgXr3wLqN0rb54zbQVMfRBKVoduE6FEpbx5XkmSJEmSVKC4tFb6o512gQotE5//+E7ePW/55lCiChQrn3fPKUmSJEmSChQ78iRJkiRJkqQCwKm1kiRJkiRJUgFgkCdJkiRJkiQVAAZ5kiRJkiRJUgFgkCdJkiRJkiQVAAZ5Uh4ZMGAAe+21F3vuuSctWrTgf//731aPvfPOO9ltt92iY/fdd19Gjhy56b4RI0awxx570KRJEw444AB+/PHHLZ7jxhtvJCkpibfeemvTbWG2Tf369alQoUJ0/aabbuKss87adP9XX30VPWbQoEGbbjv33HP517/+tcOvX5IkSZIk7RiDPCkPhADtpJNO4plnnmH8+PG8//77nHPOOaxYseIvx4b7//vf/0bhXfi8T58+0SXIyMigd+/e3H///UydOpVu3bpxySWXbPV59957b/r377/p+sCBA6lSpcqm6/vvv/9mod0XX3xB27Zt/3JbCAwlSZIkSVK8DPKkPBI63ZYuXRp9vnz5cipXrkyJEiW2eNyGDRtYtWpVdD08pnbt2tHnY8aMITU1NQrgghAGvvfee6xdu3aLz9mhQwdmzJjBzz//HF0Pod4ZZ5yx6f7Q7Td//nzmzZsXXQ8B3vXXX78pyPvpp5+YM2cO7dq1y+GvhiRJkiRJyi6DPCkPhHDulVde4aijjmKXXXaJArawtLZ48eJ/OTYsm7300kujJbAhwLvvvvt48MEHo/tCqBYev1HZsmUpV65cFMZtTegEDM8VAsFRo0ZxyCGHbLovPP9+++0Xdd2tW7eOH374IeryC8FeCAfD7SHEK1myZI5/TSRJkiRJUvakZvN4SdshLS2NW2+9lTfffJNOnTpFgdoRRxzBt99+u9lS1yCEaeG46dOns/POO/PQQw9x/PHHR/vXbY9TTz2Vgw8+mDJlynDccceRnLx5fr9xeW0ICNu0abOpU2/YsGHR7Ru7/yRJkiRJUrzsyJNywbPPPhsNqgiXp59+OtrrLnTNhRAvaN26ddRtN27cuL889o033qBly5ZRiBecfvrpDB06lPXr11O3bl1mz5696diwx96yZcs2HbsltWrVikK6MNginOvPQlAXOu/CpUuXLtFtnTt33nSb++NJkiRJkpQ/GORJueCUU06JwrtwCeFZnTp1ov3mJk+eHN0fuu3C3nW77rrrXx7boEGDKLhbuXJldD0MxggTasMy2DC8IuyfFwK24LHHHqNHjx7bXPp6yy23RB2BjRo1+st9IVRcuHAhL7zwwmZB3ssvvxzVvLFLT5IkSZIkxcultVIeqF69Oo8//vimpa1h+mxYMhs67IIwYCJ01Z177rkceeSR0dLbffbZJxqGUbp0aV588cXouPDY559/PhpyEfawC4957rnntvn84VzhsiXFihWL9uybMGECTZs2jW4LwWHo9gu3h/slSZIkSVL8kjIzMzPjLkKSJEmSJEnS33NprSRJkiRJklQAGORJkiRJkiRJBYBBniRJkiRJklQAGORJkiRJkiRJBYBBniRJkiRJklQAGORJkiRJkiRJBYBBniRJkiRJklQAGORJkiRJkiRJBYBBniRJkiRJklQAGORJkiRJkiRJ5H//D1Ewx3snn53nAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Target Dispatch: [0. 0.00029647]\n", + "Actual Dispatch: [0. 0.00029655]\n", + "Target Flex: [0. 0. 0.]\n", + "Actual Flex: [ 0. -1.1950618 -1.7995671]\n", + "Total Load: 21.81 :: Total Gen: 24.15 :: Losses 2.21\n" + ] + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "from grid2op.PlotGrid import PlotMatplot\n", + "act = env.action_space({# \"flexibility\": [(1, +1)],\n", + " \"redispatch\": [(1, 2)]})\n", + "env.set_id(0) # make sure to use the same environment input data.\n", + "obs_init = env.reset()\n", + "plotter = PlotMatplot(env.observation_space)\n", + "print(act)\n", + "obs, reward, done, info = env.step(act)\n", + "plotter.plot_obs(obs)\n", + "plt.show()\n", + "print(f\"Target Dispatch: {obs.target_dispatch}\")\n", + "print(f\"Actual Dispatch: {obs.actual_dispatch}\")\n", + "print(f\"Target Flex: {obs.target_flex}\")\n", + "print(f\"Actual Flex: {obs.actual_flex}\")\n", + "print(f\"Total Load: {obs.load_p.sum():.2f} :: Total Gen: {obs.gen_p.sum():.2f} :: Losses {np.abs(np.abs(obs.p_or)-np.abs(obs.p_ex)).sum():.2f}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b29c0e8", + "metadata": {}, "outputs": [], "source": [] } diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 255388f1..9db9ab44 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -4026,7 +4026,7 @@ def _is_flexibility_ambiguous(self): if (np.abs(self._private_flexibility[~type(self).load_flexible]) >= 1e-7).any(): raise InvalidFlexibility( - "Trying to apply a flexibility action on a non redispatchable generator" + "Trying to apply a flexibility action on a non-flexible load" ) if self._single_act: @@ -4371,22 +4371,22 @@ def __str__(self) -> str: # flexibility, new in 1.12.x - # if self.flexibility_is_available: - # if self._modif_flexibility: - # res.append( - # "\t - Modify the loads with flexibility in the following way:" - # ) - # for load_idx in range(self.n_load): - # if np.abs(self._private_flexibility[load_idx]) >= 1e-7: - # load_name = self.name_load[load_idx] - # f_amount = self._private_flexibility[load_idx] - # res.append( - # '\t \t - Flexibility "{}" of {:.2f} MW'.format( - # load_name, f_amount - # ) - # ) - # else: - # res.append("\t - NOT perform any flexibility action") + if self.flexibility_is_available: + if self._modif_flexibility: + res.append( + "\t - Modify the loads with flexibility in the following way:" + ) + for load_idx in range(self.n_load): + if np.abs(self._private_flexibility[load_idx]) >= 1e-7: + load_name = self.name_load[load_idx] + f_amount = self._private_flexibility[load_idx] + res.append( + '\t \t - Flexibility "{}" of {:.2f} MW'.format( + load_name, f_amount + ) + ) + else: + res.append("\t - NOT perform any flexibility action") return "\n".join(res) @@ -4555,7 +4555,7 @@ def impact_on_objects(self) -> dict: if np.abs(self._private_flexibility[load_idx]) >= 1e-7: load_name = self.name_load[load_idx] f_amount = self._private_flexibility[load_idx] - redispatch["loads"].append( + flexibility["loads"].append( {"load_id": load_idx, "gen_name": load_name, "amount": f_amount} ) flexibility["changed"] = True diff --git a/grid2op/Converter/BackendConverter.py b/grid2op/Converter/BackendConverter.py index 4d8e968f..d78cb096 100644 --- a/grid2op/Converter/BackendConverter.py +++ b/grid2op/Converter/BackendConverter.py @@ -509,9 +509,9 @@ def assert_grid_correct(self, _local_dir_cls=None) -> None: if self.path_flexibility is not None: # Flexibility / demand response data is available, new in 1.12.x try: - super().load_flexibility_data(self.path_flex, name=self.name_flex) + super().load_flexibility_data(self.path_flexibility, name=self.name_flexibility) self.source_backend.load_flexibility_data( - self.path_flex, name=self.name_flex + self.path_flexibility, name=self.name_flexibility ) except BackendError as exc_: self.flexibility_is_available = False diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 044c42ea..f5f6a41d 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -2347,7 +2347,7 @@ def _compute_dispatch_and_flex_vect(self, already_modified_gen:np.ndarray, new_g | (np.abs(self._actual_flex) >= 1e-7) | (self._target_flex != self._actual_flex) ) - load_involved[~self.load_flexible] = False + load_involved[~cls.load_flexible] = False if cls.detachment_is_allowed: load_involved[self._backend_action.get_load_detached()] = False incr_in_load_chronics = new_load_p - (self._load_demand_t_flex - self._actual_flex) @@ -2435,8 +2435,8 @@ def _compute_dispatch_and_flex_vect(self, already_modified_gen:np.ndarray, new_g self.load_max_ramp_down + self._epsilon_poly) coeffs = np.concatenate((gen_coeffs[gen_involved], load_coeffs[load_involved])) - involved = np.concatenate((gen_involved, load_involved)) - weights = np.ones(nb_dispatchable + nb_flexible) * coeffs[involved] + # involved = np.concatenate((gen_involved, load_involved)) + weights = np.ones(nb_dispatchable + nb_flexible) * coeffs # [involved] weights /= weights.sum() if target_gen_vals_mi.shape[0] == 0 and target_load_vals_mi.shape[0] == 0: @@ -2576,18 +2576,18 @@ def _compute_dispatch_and_flex_vect(self, already_modified_gen:np.ndarray, new_g def target(actual_dispatchable): # Define objective (quadratic) - quad_ = (actual_dispatchable[modded_involved_gens] - target_vals_mi_optim) ** 2 - coeffs_quads = weights[modded_involved_gens] * quad_ + quad_ = (actual_dispatchable[modded_involved] - target_vals_mi_optim) ** 2 + coeffs_quads = weights[modded_involved] * quad_ coeffs_quads_const = coeffs_quads.sum() coeffs_quads_const /= scale_objective # scaling the function return coeffs_quads_const def jac(actual_dispatchable): res_jac = 1.0 * tmp_zeros - res_jac[0, modded_involved_gens] = ( + res_jac[0, modded_involved] = ( 2.0 - * weights[modded_involved_gens] - * (actual_dispatchable[modded_involved_gens] - target_vals_mi_optim) + * weights[modded_involved] + * (actual_dispatchable[modded_involved] - target_vals_mi_optim) ) res_jac /= scale_objective # scaling the function return res_jac.reshape(-1) From f313f23b8007413ca638b7a17cb25d265a31634c Mon Sep 17 00:00:00 2001 From: DEUCE1957 <2246306W@student.gla.ac.uk> Date: Wed, 17 Sep 2025 16:01:52 +0200 Subject: [PATCH 16/38] Add: load_flexibility_data in test backend Signed-off-by: DEUCE1957 <2246306W@student.gla.ac.uk> --- grid2op/tests/aaa_test_backend_interface.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/grid2op/tests/aaa_test_backend_interface.py b/grid2op/tests/aaa_test_backend_interface.py index c11f86f3..be347328 100644 --- a/grid2op/tests/aaa_test_backend_interface.py +++ b/grid2op/tests/aaa_test_backend_interface.py @@ -53,6 +53,7 @@ def aux_make_backend(self, extra_name=extra_name) backend.load_grid_public(self.get_path(), self.get_casefile()) backend.load_redispacthing_data("tmp") # pretend there is no generator + backend.load_flexibility_data("tmp") backend.load_storage_data(self.get_path()) backend.assert_grid_correct() try: @@ -96,6 +97,7 @@ def test_01load_grid(self): backend = self.make_backend() backend.load_grid_public(self.get_path(), self.get_casefile()) # both argument filled backend.load_redispacthing_data(self.get_path()) + backend.load_flexibility_data(self.get_path()) backend.load_storage_data(self.get_path()) env_name = "BasicTest_load_grid0_" + type(self).__name__ backend.env_name = env_name @@ -116,6 +118,7 @@ def test_01load_grid(self): backend.env_name = "BasicTest_load_grid2_" + type(self).__name__ backend.load_grid(os.path.join(self.get_path(), self.get_casefile())) # first argument filled, second None backend.load_redispacthing_data(self.get_path()) + backend.load_flexibility_data(self.get_path()) backend.load_storage_data(self.get_path()) backend.assert_grid_correct() backend.close() From 180037f245f15ce62815d5b4f140c1f2bc67714b Mon Sep 17 00:00:00 2001 From: DEUCE1957 <2246306W@student.gla.ac.uk> Date: Wed, 17 Sep 2025 19:09:07 +0200 Subject: [PATCH 17/38] Add: Flex Defaults in GridObjects and Environment Signed-off-by: DEUCE1957 <2246306W@student.gla.ac.uk> --- grid2op/Environment/environment.py | 2 ++ grid2op/Space/GridObjects.py | 27 +++++++++++++-------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 8a0ee3fd..03591d37 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -504,6 +504,7 @@ def _init_backend( # first injections given) self._reset_maintenance() self._reset_redispatching() + self._reset_flexibility() self._reward_to_obs = {} do_nothing = self._helper_action_env({}) @@ -1422,6 +1423,7 @@ def reset(self, self._env_modification = None self._reset_maintenance() self._reset_redispatching() + self._reset_flexibility() self._reset_vectors_and_timings() # it need to be done BEFORE to prevent cascading failure when there has been if options is not None and "init datetime" in options: diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index fb2533c1..72d8c670 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -4137,20 +4137,19 @@ def _make_cls_dict(cls, res, as_list=True, copy_=True, _topo_vect_only=False): res[nm_attr] = None # Flexibility, new in 1.12.x - if cls.flexibility_is_available: - for nm_attr, type_attr in zip(cls._li_attr_flex_load, cls._type_attr_flex_load): - if nm_attr not in res and hasattr(cls, nm_attr) is False: - # Note: Need default values here for flex to work together - # correctly with redispatch - res[nm_attr] = np.zeros(shape=cls.n_load, dtype=type_attr) - if getattr(cls, nm_attr, None) is not None: - save_to_dict( - res, - cls, - nm_attr, - (lambda li, type_attr=type_attr: [type_attr(el) for el in li]) if as_list else None, - copy_, - ) + for nm_attr, type_attr in zip(cls._li_attr_flex_load, cls._type_attr_flex_load): + if nm_attr not in res and hasattr(cls, nm_attr) is False: + # Note: Need default values here for flex to work together + # correctly with redispatch + res[nm_attr] = np.zeros(shape=cls.n_load, dtype=type_attr) + if getattr(cls, nm_attr, None) is not None: + save_to_dict( + res, + cls, + nm_attr, + (lambda li, type_attr=type_attr: [type_attr(el) for el in li]) if as_list else None, + copy_, + ) # layout (position of substation on a map of the grid) if cls.grid_layout is not None: From a45e8d979ac1352838a4428313618869c2354c22 Mon Sep 17 00:00:00 2001 From: DEUCE1957 <2246306W@student.gla.ac.uk> Date: Wed, 17 Sep 2025 20:08:58 +0200 Subject: [PATCH 18/38] Add: Update flexibility test environment Signed-off-by: DEUCE1957 <2246306W@student.gla.ac.uk> --- grid2op/tests/test_flexibility.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grid2op/tests/test_flexibility.py b/grid2op/tests/test_flexibility.py index 3c58d0db..0adc5084 100644 --- a/grid2op/tests/test_flexibility.py +++ b/grid2op/tests/test_flexibility.py @@ -16,7 +16,7 @@ class TestFlexibility(unittest.TestCase): def setUp(self) -> None: - self.env_name = os.path.join(PATH_DATA_TEST, "5bus_example_with_flexibility") + self.env_name = "rte_case5_flexibility" with warnings.catch_warnings(): warnings.filterwarnings("ignore") @@ -50,7 +50,7 @@ def setUp(self) -> None: def test_create_flex_action(self): try: - flex_act = self.env.action_space({"flexibiity":[(el, 0.01) for el in np.nonzero(self.env.load_flexible)[0]]}) + flex_act = self.env.action_space({"flexibility":[(el, 0.01) for el in np.nonzero(self.env.load_flexible)[0]]}) assert True except: assert False From c886a75ac0c71cf3ca99851c6475b0c40f537170 Mon Sep 17 00:00:00 2001 From: DEUCE1957 <2246306W@student.gla.ac.uk> Date: Wed, 17 Sep 2025 22:34:09 +0200 Subject: [PATCH 19/38] Add: Demand Response Notebook Signed-off-by: DEUCE1957 <2246306W@student.gla.ac.uk> --- getting_started/13_DemandResponse.ipynb | 174 +++++++++--------------- 1 file changed, 65 insertions(+), 109 deletions(-) diff --git a/getting_started/13_DemandResponse.ipynb b/getting_started/13_DemandResponse.ipynb index 88b7888c..0ac4810e 100644 --- a/getting_started/13_DemandResponse.ipynb +++ b/getting_started/13_DemandResponse.ipynb @@ -36,7 +36,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "f11b6b78", "metadata": {}, "outputs": [], @@ -46,19 +46,10 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "0029e770", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "c:\\Users\\Deuce\\Documents\\projects\\Grid2Op\\venv_grid2op\\Lib\\site-packages\\numba\\core\\config.py:168: UserWarning: CUDA Python bindings requested (the environment variable NUMBA_CUDA_USE_NVIDIA_BINDING is set), but they are not importable: No module named 'cuda'.\n", - " warnings.warn(msg)\n" - ] - } - ], + "outputs": [], "source": [ "import grid2op\n", "from grid2op.Agent import DoNothingAgent, BaseAgent\n", @@ -90,35 +81,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "cc448ed2", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "c:\\Users\\Deuce\\Documents\\projects\\Grid2Op\\grid2op\\MakeEnv\\Make.py:453: UserWarning: You are using a development environment. This environment is not intended for training agents. It might not be up to date and its primary use if for tests (hence the \"test=True\" you passed as argument). Use at your own risk.\n", - " warnings.warn(_MAKE_DEV_ENV_WARN)\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Is this environment suitable for redispatching: True\n", - "Is this environment suitable for flexibility: True\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "c:\\Users\\Deuce\\Documents\\projects\\Grid2Op\\venv_grid2op\\Lib\\site-packages\\lightsim2grid\\gridmodel\\from_pandapower\\_aux_add_slack.py:114: UserWarning: We found either some slack coefficient to be < 0. or they were all 0.We set them all to 1.0 to avoid such issues\n", - " warnings.warn(\"We found either some slack coefficient to be < 0. or they were all 0.\"\n" - ] - } - ], + "outputs": [], "source": [ "try:\n", " from lightsim2grid import LightSimBackend\n", @@ -137,43 +103,20 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "30b9f0f6", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([10.7, 9.7, 10. ], dtype=float32)" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "env.load_size" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "4fca80e5", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(True,\n", - " Grid2OpException AmbiguousAction InvalidFlexibility InvalidFlexibility('Trying to apply a flexibility action on a non-flexible load'))" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "act = env.action_space({\"flexibility\": [(0, +1)]})\n", "act.is_ambiguous()" @@ -184,51 +127,13 @@ "execution_count": null, "id": "37738883", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "This action will:\n", - "\t - NOT change anything to the injections\n", - "\t - Modify the generators with redispatching in the following way:\n", - "\t \t - Redispatch \"gen_1_1\" of 2.00 MW\n", - "\t - NOT modify any storage capacity\n", - "\t - NOT perform any curtailment\n", - "\t - NOT force any line status\n", - "\t - NOT switch any line status\n", - "\t - NOT switch anything in the topology\n", - "\t - NOT force any particular bus configuration\n", - "\t - NOT perform any flexibility action\n" - ] - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABPIAAALDCAYAAACfCC4HAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAOxAAADsQBlSsOGwAA/AhJREFUeJzs3Qd0FPXXxvFvGr333nvvIr03AUFF/Ss2RESlCBZQQUWxYgEBUVQEFVFAEUVAAaUKSO+9995ryr7nzrxB0AAJJJlk83zOWVN2d+YmmJ2dO/d3b4DP5/MhIiIiIiIiIiIiCVqg1wGIiIiIiIiIiIjI9SmRJyIiIiIiIiIikggokSciIiIiIiIiIpIIKJEnIiIiIiIiIiKSCCiRJyIiIiIiIiIikggokSciIiIiIiIiIpIIKJEnIiIiIiIiIiKSCCiRJyIiIiIiIiIikggokSciIiIiIiIiIpIIJPhE3vnz52nTpg3FihWjfPnyNG7cmM2bN1+6/5FHHrl0X82aNVm0aNFVtxUQEEDZsmWpUKGCc5szZ0607rvcq6++6jx2woQJl77n8/koWLAgGTJkcL7u168fHTt2vHT/3LlznefMnDnz0vc6d+5M3759b+I3IyIiIiIiIiIiSUmCT+SZTp06sWHDBlasWMHtt99+RZKsbdu2rF271rnvhRdeoF27dtfcliXoli9f7txq164d7fsuV7lyZUaMGHHp6xkzZpAlS5ZLX9evX/+KpN2ff/7JLbfc8p/vNWjQIAa/BRERERERERERScoSfCIvRYoUtGjRwqloM9WrV2f79u2X7m/dujXBwcGX7tuzZw9hYWFxGlOtWrXYsmUL+/fvd762pF6HDh0u3W9x7N27l927dztfWwLv5ZdfvpTI27dvHzt37uTWW2+N0zhFRERERERERMR/JPhE3r8NGjTIqcq72n2W9ItM7EWlYcOGzjLcnj17cubMmWjf92/t27dn1KhRHD9+3FnO27Rp00v3JUuWjBo1ajhVdxcuXGDbtm1OXJbYs6XC9n1L4lmSUkRERERERERExO8SeW+++abTH++tt976z33ffPMNY8eOZfjw4Vd9/o4dO1iyZAl//fUXhw4d4rnnnovWfVF56KGHnETe6NGjufvuuwkMvPJXGbm8duHChVSrVu1Spd78+fOd79v9IiIiIiIiIiIifpfIe++99/jxxx+ZMmUKqVKluuK+77//3hkwMW3aNLJnz37VbeTLl8/5mDp1ap588skrBlpc676o5M6dm/z58zv7tYEb/2aJOqu8s1u9evWc79WtW/fS926kP96pC6cYvHAwlT6tRI+pPdhydEuMtyEiIiIiIiIiIolTokjkffDBB4wZM8ZJ1EVOho1kVXh9+vRh+vTpl5JxUTl27Bhnz551Po+IiHCSfxUrVrzufdfy+uuv079/f4oUKfKf+6pWrcrBgwedir3LE3nfffed0yMvskovOixh9/TUp8n9QW56Te9F8SzF+WHdDxQdXJTWY1ozY+sMZ3KuiIiIiIiIiEhSdeHCBbp06ULRokUpW7as0xYtKpb7sbZqpUqVoly5ck4xlq0AjTRp0iRKlCjhbOeOO+7g5MmTUW7n4YcfdmY6LFu27NL3Tp06RZo0aahQoYLztRV/We7o8hWl9pzL5z80a9aML774wj8SedZX7plnnnF60dkv1n4RNgE20v333+/0nbO+eXaf3Y4cOeLc98knnzhDJsz69eudpa3WA8/+Me0xAwcOvO5911KlShVnom5UQkJCnKEY9g9o//imWLFiztf2fbv/WiwxN33rdCdRZwm7H9f9SJ86fdjdczdj7hzD1u5bGdtuLMfOH6PR140oO6wsw5cM52yom5AUEREREREREUlKevfu7STJNm7cyKpVq5zVnVH5+eefmTdvHitWrGDlypXOzIQXX3zRue/06dM8+uij/PTTT2zatIlcuXI5hVxXU7lyZWcIaiQrDitZsuR/Wq9FslWalteK/J4NbJ07d260V24G+FTKlaBYIu6bld/w0cKPWHNoDbXz1abbLd1oU6INwYFRD/FYum+p8/gxq8eQOiQ1j1V6jKeqPUW+9FevUBQRERERERER8RdnzpwhZ86cTkFYunTprvnYiRMn8sorrzht1ax6rlevXk5CzVaEjhs3zqmOmzp1qvPYtWvX0qRJE2e7UVXkWdHWZ5995hSJJU+enJo1azqVgJ9++inLly9n165dToGXrQa14ajFixfn3XffZcKECYwcOdKZpfC///3vigq9RF2Rl1TsPLGTXtN6keeDPHSd0pUquaqwpNMSZj8ym7tK3XXVJJ6plLMSI9uMZOfTO+lRvQdfrfyKQoMK0W5cO+bsmKNltyIiIiIiIiLi17Zs2UKmTJmcQam2grJ27drM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7Td//nzmzZsXXQ8B3vXXX78pyPvpp5+YM2cO7dq1y+GvhiRJkiRJyi6DPCkPhHDulVde4aijjmKXXXaJArawtLZ48eJ/OTYsm7300kujJbAhwLvvvvt48MEHo/tCqBYev1HZsmUpV65cFMZtTegEDM8VAsFRo0ZxyCGHbLovPP9+++0Xdd2tW7eOH374IeryC8FeCAfD7SHEK1myZI5/TSRJkiRJUvakZvN4SdshLS2NW2+9lTfffJNOnTpFgdoRRxzBt99+u9lS1yCEaeG46dOns/POO/PQQw9x/PHHR/vXbY9TTz2Vgw8+mDJlynDccceRnLx5fr9xeW0ICNu0abOpU2/YsGHR7Ru7/yRJkiRJUrzsyJNywbPPPhsNqgiXp59+OtrrLnTNhRAvaN26ddRtN27cuL889o033qBly5ZRiBecfvrpDB06lPXr11O3bl1mz5696diwx96yZcs2HbsltWrVikK6MNginOvPQlAXOu/CpUuXLtFtnTt33nSb++NJkiRJkpQ/GORJueCUU06JwrtwCeFZnTp1ov3mJk+eHN0fuu3C3nW77rrrXx7boEGDKLhbuXJldD0MxggTasMy2DC8IuyfFwK24LHHHqNHjx7bXPp6yy23RB2BjRo1+st9IVRcuHAhL7zwwmZB3ssvvxzVvLFLT5IkSZIkxcultVIeqF69Oo8//vimpa1h+mxYMhs67IIwYCJ01Z177rkceeSR0dLbffbZJxqGUbp0aV588cXouPDY559/PhpyEfawC4957rnntvn84VzhsiXFihWL9uybMGECTZs2jW4LwWHo9gu3h/slSZIkSVL8kjIzMzPjLkKSJEmSJEnS33NprSRJkiRJklQAGORJkiRJkiRJBYBBniRJkiRJklQAGORJkiRJkiRJBYBBniRJkiRJklQAGORJkiRJkiRJBYBBniRJkiRJklQAGORJkiRJkiRJBYBBniRJkiRJklQAGORJkiRJkiRJ5H//D1Ewx3snn53nAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Target Dispatch: [0. 0.00029647]\n", - "Actual Dispatch: [0. 0.00029655]\n", - "Target Flex: [0. 0. 0.]\n", - "Actual Flex: [ 0. -1.1950618 -1.7995671]\n", - "Total Load: 21.81 :: Total Gen: 24.15 :: Losses 2.21\n" - ] - } - ], + "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "from grid2op.PlotGrid import PlotMatplot\n", - "act = env.action_space({# \"flexibility\": [(1, +1)],\n", - " \"redispatch\": [(1, 2)]})\n", + "act = env.action_space({\"flexibility\": [(1, -1)],\n", + " \"redispatch\": [(1, 0.5)]\n", + " })\n", "env.set_id(0) # make sure to use the same environment input data.\n", "obs_init = env.reset()\n", "plotter = PlotMatplot(env.observation_space)\n", @@ -240,7 +145,9 @@ "print(f\"Actual Dispatch: {obs.actual_dispatch}\")\n", "print(f\"Target Flex: {obs.target_flex}\")\n", "print(f\"Actual Flex: {obs.actual_flex}\")\n", - "print(f\"Total Load: {obs.load_p.sum():.2f} :: Total Gen: {obs.gen_p.sum():.2f} :: Losses {np.abs(np.abs(obs.p_or)-np.abs(obs.p_ex)).sum():.2f}\")" + "losses = np.abs(np.abs(obs.p_or)-np.abs(obs.p_ex)).sum()\n", + "print(f\"Total Load: {obs.load_p.sum():.2f} :: Total Gen: {obs.gen_p.sum():.2f} :: Losses {losses:.2f}\")\n", + "print(f\"Balance: {obs.load_p.sum()+losses:.2f} == {obs.gen_p.sum():.2f}\")" ] }, { @@ -249,6 +156,55 @@ "id": "9b29c0e8", "metadata": {}, "outputs": [], + "source": [ + "# -1 Flex: 1.6 , 25.357508\n", + "# 0 Flex: 1.6 , 26.275875\n", + "obs.gen_p" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4ce6d1ba", + "metadata": {}, + "outputs": [], + "source": [ + "# -1 Flex: 8.8 , 7. , 8.230771\n", + "# 0 Flex: 8.8, 8. , 8.\n", + "obs.load_p" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "68fabbab", + "metadata": {}, + "outputs": [], + "source": [ + "env = grid2op.make(\"cigre_enhanced_large_train\", allow_detachment=True)\n", + "obs = env.reset(options={\"time serie id\": \"Scenario_4077\"})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "203a5b88", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "from grid2op.PlotGrid import PlotMatplot\n", + "plotter = PlotMatplot(env.observation_space)\n", + "plotter.plot_obs(obs)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ea179894", + "metadata": {}, + "outputs": [], "source": [] } ], From fecef7f8083464bbbaf7fc579576e868c12c5cc2 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Thu, 18 Sep 2025 15:47:27 +0200 Subject: [PATCH 20/38] Add: Test on Linux Signed-off-by: Xavier Weiss --- getting_started/13_DemandResponse.ipynb | 41 ++++++++++++++++--------- grid2op/Observation/baseObservation.py | 2 +- grid2op/tests/test_flexibility.py | 1 + 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/getting_started/13_DemandResponse.ipynb b/getting_started/13_DemandResponse.ipynb index 0ac4810e..ac70be0c 100644 --- a/getting_started/13_DemandResponse.ipynb +++ b/getting_started/13_DemandResponse.ipynb @@ -95,7 +95,8 @@ " bk_cls = PandaPowerBackend\n", "\n", "env_name = \"rte_case5_flexibility\"\n", - "env = grid2op.make(env_name, test=True, backend=bk_cls())\n", + "backend = bk_cls()\n", + "env = grid2op.make(env_name, test=True, backend=backend)\n", "\n", "print(f\"Is this environment suitable for redispatching: {env.redispatching_unit_commitment_availble}\")\n", "print(f\"Is this environment suitable for flexibility: {env.flexibility_is_available}\")" @@ -181,8 +182,14 @@ "metadata": {}, "outputs": [], "source": [ - "env = grid2op.make(\"cigre_enhanced_large_train\", allow_detachment=True)\n", - "obs = env.reset(options={\"time serie id\": \"Scenario_4077\"})" + "from grid2op.Parameters import Parameters\n", + "p = Parameters()\n", + "p.NB_TIMESTEP_OVERFLOW_ALLOWED = 1\n", + "p.MAX_LINE_STATUS_CHANGED = 17\n", + "p.MAX_SUB_CHANGED = 14\n", + "env = grid2op.make(\"cigre_enhanced_large_train\", allow_detachment=True, param=p)\n", + "\n", + "obs = env.reset(options={\"time serie id\": \"Scenario_2\"})\n" ] }, { @@ -194,23 +201,27 @@ "source": [ "import matplotlib.pyplot as plt\n", "from grid2op.PlotGrid import PlotMatplot\n", - "plotter = PlotMatplot(env.observation_space)\n", + "\n", + "obs = env.reset(options={\"time serie id\": \"Scenario_3068\"})\n", + "\n", + "plotter = PlotMatplot(env.observation_space, load_name=True, gen_name=True)\n", + "\n", + "LOAD = \"Load CI7\"\n", + "act = env.action_space({\"flexibility\":[(LOAD, obs.load_max_ramp_up[np.nonzero(obs.name_load == LOAD)[0]])],\n", + " \"curtail\": [(\"gen_7_8\", 0.0)]})\n", + "obs, reward, done, info = env.step(act)\n", "plotter.plot_obs(obs)\n", - "plt.show()" + "plt.show()\n", + "\n", + "losses = np.abs(np.abs(obs.p_or)-np.abs(obs.p_ex)).sum()\n", + "print(f\"Total Load: {obs.load_p.sum():.2f} :: Total Gen: {obs.gen_p.sum():.2f} :: Losses {losses:.2f}\")\n", + "print(f\"Balance: {obs.load_p.sum()+losses:.2f} == {obs.gen_p.sum():.2f}\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ea179894", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { "kernelspec": { - "display_name": "venv_grid2op", + "display_name": "venv_test", "language": "python", "name": "python3" }, @@ -224,7 +235,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.10.15" } }, "nbformat": 4, diff --git a/grid2op/Observation/baseObservation.py b/grid2op/Observation/baseObservation.py index aa2c16da..62778db9 100644 --- a/grid2op/Observation/baseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -5388,4 +5388,4 @@ def process_flexibility(cls): except ValueError: pass cls._update_value_set() - return super().process_detachment() + return super().process_flexibility() diff --git a/grid2op/tests/test_flexibility.py b/grid2op/tests/test_flexibility.py index 0adc5084..968c3dcd 100644 --- a/grid2op/tests/test_flexibility.py +++ b/grid2op/tests/test_flexibility.py @@ -14,6 +14,7 @@ from grid2op.tests.helper_path_test import PATH_DATA_TEST import grid2op + class TestFlexibility(unittest.TestCase): def setUp(self) -> None: self.env_name = "rte_case5_flexibility" From 4cf0d6498d4cd0b2a437ea4f579e0bda6243c257 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Fri, 19 Sep 2025 10:37:00 +0200 Subject: [PATCH 21/38] Test: Update observation to pass tests Signed-off-by: Xavier Weiss --- grid2op/Observation/baseObservation.py | 65 ++++++++++++++------------ grid2op/tests/test_Observation.py | 26 +++++++++-- 2 files changed, 57 insertions(+), 34 deletions(-) diff --git a/grid2op/Observation/baseObservation.py b/grid2op/Observation/baseObservation.py index 62778db9..ab0e5d26 100644 --- a/grid2op/Observation/baseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -756,10 +756,10 @@ def __init__(self, self.target_dispatch = np.empty(shape=cls.n_gen, dtype=dt_float) self.actual_dispatch = np.empty(shape=cls.n_gen, dtype=dt_float) - # flexibility / demand response - if cls.flexibility_is_available: - self.target_flex = np.empty(shape=cls.n_load, dtype=dt_float) - self.actual_flex = np.empty(shape=cls.n_load, dtype=dt_float) + # Flexibility / demand response, new in 1.12.x + # if cls.flexibility_is_available: + self.target_flex = np.empty(shape=cls.n_load, dtype=dt_float) + self.actual_flex = np.empty(shape=cls.n_load, dtype=dt_float) # storage unit self.storage_charge = np.empty(shape=cls.n_storage, dtype=dt_float) # in MWh @@ -1591,9 +1591,9 @@ def reset(self) -> None: self.storage_p_detached[:] = 0. # flexibility, new in 1.12.x - if type(self).flexibility_is_available: - self.target_flex[:] = np.nan - self.actual_flex[:] = np.nan + # if type(self).flexibility_is_available: + self.target_flex[:] = 0.0 + self.actual_flex[:] = 0.0 def set_game_over(self, env: Optional["grid2op.Environment.Environment"]=None) -> None: @@ -1747,9 +1747,9 @@ def set_game_over(self, self.storage_p_detached[:] = 0. # flexibility, new in 1.12.x - if type(self).flexibility_is_available: - self.target_flex[:] = 0.0 - self.actual_flex[:] = 0.0 + # if type(self).flexibility_is_available: + self.target_flex[:] = 0.0 + self.actual_flex[:] = 0.0 def __compare_stats(self, other: Self, name: str) -> bool: attr_me = getattr(self, name) @@ -4009,9 +4009,6 @@ def to_dict(self): self._dictionnarized["loads"]["p"] = self.load_p self._dictionnarized["loads"]["q"] = self.load_q self._dictionnarized["loads"]["v"] = self.load_v - if type(self).flexibility_is_available: # new in 1.12.x - self._dictionnarized["loads"]["target_flex"] = self.target_flex - self._dictionnarized["loads"]["actual_flex"] = self.actual_flex self._dictionnarized[ "prods" ] = {} # TODO will be removed in future versions @@ -4104,6 +4101,11 @@ def to_dict(self): self._dictionnarized["max_step"] = self.max_step # TODO shedding: add relevant attributes + + # Flexibility, new in 1.12.x + self._dictionnarized["flexibility"] = {} + self._dictionnarized["flexibility"]["target_flex"] = self.target_flex + self._dictionnarized["flexibility"]["actual_flex"] = self.actual_flex return self._dictionnarized @@ -4580,9 +4582,10 @@ def _update_obs_complete(self, env: "grid2op.Environment.BaseEnv", with_forecast self.target_dispatch[:] = env._target_dispatch self.actual_dispatch[:] = env._actual_dispatch - if type(self).flexibility_is_available: - self.target_flex[:] = env._target_flex - self.actual_flex[:] = env._actual_flex + # Flexibility, new in 1.12.x + # if type(self).flexibility_is_available: + self.target_flex[:] = env._target_flex + self.actual_flex[:] = env._actual_flex self._thermal_limit[:] = env.get_thermal_limit() @@ -5374,18 +5377,18 @@ def process_detachment(cls): cls._update_value_set() return super().process_detachment() - @classmethod - def process_flexibility(cls): - if not cls.flexibility_is_available: - # this is really important, otherwise things from grid2op base types will be affected - cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) - # remove the detachment from the list to vector - for el in ["target_flex", - "actual_flex"]: - if el in cls.attr_list_vect: - try: - cls.attr_list_vect.remove(el) - except ValueError: - pass - cls._update_value_set() - return super().process_flexibility() + # @classmethod + # def process_flexibility(cls): + # if not cls.flexibility_is_available: + # # this is really important, otherwise things from grid2op base types will be affected + # cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) + # # remove the detachment from the list to vector + # for el in ["target_flex", + # "actual_flex"]: + # if el in cls.attr_list_vect: + # try: + # cls.attr_list_vect.remove(el) + # except ValueError: + # pass + # cls._update_value_set() + # return super().process_flexibility() diff --git a/grid2op/tests/test_Observation.py b/grid2op/tests/test_Observation.py index 488ee90a..52cda851 100644 --- a/grid2op/tests/test_Observation.py +++ b/grid2op/tests/test_Observation.py @@ -301,6 +301,12 @@ def setUp(self): "alertable_line_ids": [], "assistant_warning_type": None, "_PATH_GRID_CLASSES": None, + # Flexibility, new in 1.12.x + "load_size":[0.0]*self.env.n_load, + "load_flexible":[False]*self.env.n_load, + "load_max_ramp_up":[0.0]*self.env.n_load, + "load_max_ramp_down":[0.0]*self.env.n_load, + "load_cost_per_MW":[0.0]*self.env.n_load, } self.json_ref = { @@ -755,6 +761,10 @@ def setUp(self): ], "target_dispatch": [0.0, 0.0, 0.0, 0.0, 0.0], "actual_dispatch": [0.0, 0.0, 0.0, 0.0, 0.0], + # Flexibility, new in 1.12.x + "target_flex": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + "actual_flex": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + "_shunt_p": [0.0], "_shunt_q": [-17.923625946044922], "_shunt_v": [0.20202238857746124], @@ -910,8 +920,10 @@ def setUp(self): dt_int, dt_int, dt_int, + # Redispatch dt_float, dt_float, + # Storage dt_float, dt_float, dt_float, @@ -958,7 +970,10 @@ def setUp(self): # dt_float, # dt_float, # timestep_protection_engaged - dt_int + dt_int, + # Flexibility, new in 1.12.x + dt_float, + dt_float, ], dtype=object, ) @@ -995,8 +1010,10 @@ def setUp(self): 14, 20, 20, + # redispatch 5, 5, + # storage 0, 0, 0, @@ -1038,10 +1055,13 @@ def setUp(self): # 5, # 0, # timestep_protection_engaged - 20 + 20, + # flexibility, new in 1.12.x + 11, + 11, ] ) - self.size_obs = 429 + 4 + 4 + 2 + 1 + 10 + 5 + 0 + 5 + 20 + self.size_obs = 429 + 4 + 4 + 2 + 1 + 10 + 5 + 0 + 5 + 20 + 11 + 11 def tearDown(self): self.env.close() From fea484faebb335edf90c6aecf06ea7d607098c5f Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Fri, 19 Sep 2025 10:39:43 +0200 Subject: [PATCH 22/38] Test: Minor change to test_Action to include flexibility Signed-off-by: Xavier Weiss --- grid2op/tests/test_Action.py | 1 + 1 file changed, 1 insertion(+) diff --git a/grid2op/tests/test_Action.py b/grid2op/tests/test_Action.py index 16e6c331..3b04cc51 100644 --- a/grid2op/tests/test_Action.py +++ b/grid2op/tests/test_Action.py @@ -459,6 +459,7 @@ def test_call(self): redispatching, storage, shunts, + flexibility, # new in 1.12.x ) = action() def test_compare(self): From 9501a0184eb837eae37b2c5d94105bd9eb849335 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Fri, 19 Sep 2025 10:55:16 +0200 Subject: [PATCH 23/38] Test: Fix test_attached_envs for flexibility Signed-off-by: Xavier Weiss --- grid2op/Action/baseAction.py | 2 +- grid2op/tests/test_attached_envs.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 9db9ab44..8623d258 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -1512,7 +1512,7 @@ def _post_process_from_vect(self): self._modif_detach_storage = self._private_detach_storage is not None and (self._private_detach_storage).any() if cls.flexibility_is_available: - self._modif_flexibility = self._modif_flexibility is not None and ( + self._modif_flexibility = self._private_flexibility is not None and ( np.isfinite(self._private_flexibility) & (np.abs(self._private_flexibility) >= 1e-7) ).any() diff --git a/grid2op/tests/test_attached_envs.py b/grid2op/tests/test_attached_envs.py index 56414d6c..c0785718 100644 --- a/grid2op/tests/test_attached_envs.py +++ b/grid2op/tests/test_attached_envs.py @@ -184,7 +184,8 @@ def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) # size_th = 467 # size_th = 473 # gen_delta - size_th = 473 + 20 # n_line added for timestep_protection_engaged + # size_th = 473 + 20 # n_line added for timestep_protection_engaged + size_th = 473 + 20 + 11 + 11 # flexibility added, new in 1.12.x assert self.env.observation_space.n == size_th, ( f"obs space size is {self.env.observation_space.n}," f"should be {size_th}" ) @@ -229,7 +230,8 @@ def test_action_space(self): def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) - size_th = 518 + 20 # n_line added for timestep_protection_engaged + # size_th = 518 + 20 # n_line added for timestep_protection_engaged + size_th = 518 + 20 + 11 + 11 # flexibility added, new in 1.12.x assert self.env.observation_space.n == size_th, ( f"obs space size is {self.env.observation_space.n}," f"should be {size_th}" ) @@ -274,7 +276,8 @@ def test_action_space(self): def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) # size_th = 467 - size_th = 473 + 20 # n_line added for timestep_protection_engaged + # size_th = 473 + 20 # n_line added for timestep_protection_engaged + size_th = 473 + 20 + 11 + 11 # flexibility, new in 1.12.x assert self.env.observation_space.n == size_th, ( f"obs space size is {self.env.observation_space.n}," f"should be {size_th}" ) @@ -319,7 +322,8 @@ def test_action_space(self): def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) # size_th = 475 - size_th = 481 + 20 # n_line added for timestep_protection_engaged + # size_th = 481 + 20 # n_line added for timestep_protection_engaged + size_th = 481 + 20 + 11 + 11 # flexibility added, new in 1.12.x assert self.env.observation_space.n == size_th, ( f"obs space size is {self.env.observation_space.n}," f"should be {size_th}" ) From 49fbe35b280d35fba20a1ff84a2adceb3a043716 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Fri, 19 Sep 2025 11:13:46 +0200 Subject: [PATCH 24/38] Test: Fix flexibility_is_available check in GridObjects Signed-off-by: Xavier Weiss --- grid2op/Space/GridObjects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index 72d8c670..b30cf468 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -4528,8 +4528,8 @@ class res(GridObjects): ), ) - # Demand Response / Flexibility - if dict_.get("load_size", None) is None: + # Demand Response / Flexibility, new in 1.12.x + if np.isclose(dict_.get("load_size", np.zeros(1)), 0.0).all(): cls.flexibility_is_available = DEFAULT_FLEXIBILITY_IS_AVAILABLE else: cls.flexibility_is_available = True From 18fd51962fc733384439eb2d86d2364b131cefc5 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Fri, 19 Sep 2025 11:20:46 +0200 Subject: [PATCH 25/38] Test: Remove comma after flexiblity, in test_call to work in older python version Signed-off-by: Xavier Weiss --- grid2op/tests/test_Action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grid2op/tests/test_Action.py b/grid2op/tests/test_Action.py index 3b04cc51..23dfba35 100644 --- a/grid2op/tests/test_Action.py +++ b/grid2op/tests/test_Action.py @@ -459,7 +459,7 @@ def test_call(self): redispatching, storage, shunts, - flexibility, # new in 1.12.x + flexibility # new in 1.12.x ) = action() def test_compare(self): From eadebf63af1fd6caf37f2c4e37d75f21f7f59c51 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Fri, 19 Sep 2025 11:34:01 +0200 Subject: [PATCH 26/38] Add: Flexibility to PlayableAction Signed-off-by: Xavier Weiss --- grid2op/Action/baseAction.py | 10 +++++----- grid2op/Action/playableAction.py | 1 + grid2op/tests/test_Action.py | 5 +++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 8623d258..678fe724 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -2774,9 +2774,9 @@ def __call__(self) -> Tuple[dict, np.ndarray, np.ndarray, np.ndarray, np.ndarray set_topo_vect, change_bus_vect, redispatch, - flexibility, + flexibility, # new in 1.12.x storage_power, - shunts, + shunts ) def _aux_digest_shunt_issue_warning_if_needed(self, ddict_): @@ -4813,12 +4813,12 @@ def get_types(self) -> Tuple[bool, bool, bool, bool, bool, bool, bool, bool]: Does it affect the line status (line status change / switch are **NOT** counted as topology) redispatching: ``bool`` Does it performs (explicitly) any redispatching + flexibility: ``bool`` + Does it performs (explicitly) any flexibility / demand response storage: ``bool`` Does it performs (explicitly) any action on the storage production / consumption curtailment: ``bool`` Does it performs (explicitly) any action on renewable generator - flexibility: ``bool`` - Does it performs (explicitly) any flexibility / demand response """ injection = "load_p" in self._dict_inj or "prod_p" in self._dict_inj voltage = "prod_v" in self._dict_inj @@ -4838,7 +4838,7 @@ def get_types(self) -> Tuple[bool, bool, bool, bool, bool, bool, bool, bool]: flexibility = self._private_flexibility is not None and (np.abs(self._private_flexibility) >= 1e-7).any() else: flexibility = False - return injection, voltage, topology, line, redispatching, storage, curtailment, flexibility + return injection, voltage, topology, line, redispatching, flexibility, storage, curtailment def _aux_effect_on_load(self, load_id): if load_id >= self.n_load: diff --git a/grid2op/Action/playableAction.py b/grid2op/Action/playableAction.py index 4b4b637a..d2b08df4 100644 --- a/grid2op/Action/playableAction.py +++ b/grid2op/Action/playableAction.py @@ -110,6 +110,7 @@ def __call__(self): self._set_topo_vect, self._change_bus_vect, self._redispatch, + self._flexibility, self._storage_power, {}, ) diff --git a/grid2op/tests/test_Action.py b/grid2op/tests/test_Action.py index 23dfba35..2f21ec15 100644 --- a/grid2op/tests/test_Action.py +++ b/grid2op/tests/test_Action.py @@ -450,6 +450,7 @@ def compare_vect(self, pred, true): def test_call(self): action = self.helper_action() + # injection, voltage, topology, line, redispatching, storage, curtailment, flexibility ( dict_injection, set_status, @@ -457,9 +458,9 @@ def test_call(self): set_topo_vect, switcth_topo_vect, redispatching, + flexibility, # new in 1.12.x storage, - shunts, - flexibility # new in 1.12.x + shunts ) = action() def test_compare(self): From f7e9b91f896773f2e80612589ebfcfdea905bc28 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Fri, 19 Sep 2025 12:36:56 +0200 Subject: [PATCH 27/38] Test: Fix Obs Size in aux_text_gym, fix missing default inside _prepare_redisp_and_flex Signed-off-by: Xavier Weiss --- grid2op/Environment/baseEnv.py | 1 + grid2op/tests/_aux_test_gym_compat.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index f5f6a41d..03b582ba 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -2400,6 +2400,7 @@ def _compute_dispatch_and_flex_vect(self, already_modified_gen:np.ndarray, new_g else: avail_load_down_tmp = np.zeros([], dtype=dt_float) avail_load_up_tmp = np.zeros([], dtype=dt_float) + load_involved_tmp = np.zeros([], dtype=dt_bool) except_tmp = self._detect_infeasible_dispatch( incr_in_gen_chronics[gen_involved_tmp], avail_gen_down_tmp, avail_gen_up_tmp, incr_in_load_chronics[load_involved_tmp], avail_load_down_tmp, avail_load_up_tmp diff --git a/grid2op/tests/_aux_test_gym_compat.py b/grid2op/tests/_aux_test_gym_compat.py index ea55d7a3..63d6493f 100644 --- a/grid2op/tests/_aux_test_gym_compat.py +++ b/grid2op/tests/_aux_test_gym_compat.py @@ -150,7 +150,8 @@ def test_convert_togym(self): for el in env_gym.observation_space.spaces ] ) - size_th = 562 # as of grid2Op 1.11.0 (with obs.gen_p_delta + timestep_protection_engaged) + # size_th = 562 # as of grid2Op 1.11.0 (with obs.gen_p_delta + timestep_protection_engaged) + size_th = 562 + 11 + 11 # added flexibility, new in 1.12.x assert ( dim_obs_space == size_th ), f"Size should be {size_th} but is {dim_obs_space}" From b47fbcf9aadcc4a26717c96e8d0fa8ee3029f327 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 6 Oct 2025 09:52:08 +0200 Subject: [PATCH 28/38] fix issue spotted in discussion grid2op#728 Signed-off-by: DONNOT Benjamin --- grid2op/MakeEnv/MakeFromPath.py | 2 +- grid2op/tests/test_issue_728.py | 76 +++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 grid2op/tests/test_issue_728.py diff --git a/grid2op/MakeEnv/MakeFromPath.py b/grid2op/MakeEnv/MakeFromPath.py index 158b3c9e..97fe1135 100644 --- a/grid2op/MakeEnv/MakeFromPath.py +++ b/grid2op/MakeEnv/MakeFromPath.py @@ -557,7 +557,7 @@ def make_from_dataset_path( voltage_class_cfg = config_data["voltage_class"] ### Create controler for voltages volagecontroler_class = _get_default_aux( - "volagecontroler_class", + "voltagecontroler_class", kwargs, defaultClassApp=voltage_class_cfg, defaultClass=ControlVoltageFromFile, diff --git a/grid2op/tests/test_issue_728.py b/grid2op/tests/test_issue_728.py new file mode 100644 index 00000000..92b71cd4 --- /dev/null +++ b/grid2op/tests/test_issue_728.py @@ -0,0 +1,76 @@ +# Copyright (c) 2025, RTE (https://www.rte-france.com) +# See AUTHORS.txt and https://github.com/Grid2Op/grid2op/pull/319 +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + +import numpy as np +import warnings +import grid2op +import unittest + +from grid2op.Action import BaseAction +from grid2op.VoltageControler import ControlVoltageFromFile + + +class _TEST_VC_AgentOverride(ControlVoltageFromFile): + def fix_voltage(self, observation, agent_action, env_action, prod_v_chronics): + vect_ = None + if "prod_v" in agent_action._dict_inj and np.isfinite(agent_action._dict_inj["prod_v"]).any(): + # agent decision + vect_ = 1. * agent_action._dict_inj["prod_v"] + if prod_v_chronics is not None: + # default values (in the time series) + if vect_ is None: + # agent did not change anything + vect_ = prod_v_chronics + else: + # keep the agent choice and put the default values + # (from the time series) for the generator not modified + # by the agent + mask_default = ~np.isfinite(vect_) + vect_[mask_default] = prod_v_chronics[mask_default] + + # now build the action + if vect_ is not None: + res = self.action_space({"injection": {"prod_v": vect_}}) + else: + res = self.action_space() + + if observation is not None: + # cache the get_topological_impact to avoid useless computations later + # this is a speed optimization + _ = res.get_topological_impact(observation.line_status, _store_in_cache=True, _read_from_cache=False) + return res + return super().fix_voltage(observation, agent_action, env_action, prod_v_chronics) + + +class TestIssue728(unittest.TestCase): + def setUp(self) -> None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make("educ_case14_storage", + test=True, + action_class=BaseAction, + voltagecontroler_class=_TEST_VC_AgentOverride, + _add_to_name=type(self).__name__) + assert isinstance(self.env._voltage_controler, _TEST_VC_AgentOverride) + assert issubclass(self.env._voltagecontrolerClass, _TEST_VC_AgentOverride) + self.init_obs = self.env.reset(seed=0, options={"time serie id":0}) + return super().setUp() + + def test_can_modify_gen_v(self): + new_prod_v = 1.0 * self.init_obs.gen_v + new_prod_v[0] = 150. + modify_prod_v_value = self.env.action_space({"injection": {"prod_v": new_prod_v}}) + obs, _, done, info = self.env.step(modify_prod_v_value) + assert not done + assert not info["is_ambiguous"] + assert not info["is_illegal"] + assert abs(obs.gen_v[0] - 150.) <= 1e-6 + + +if __name__ == "__main__": + unittest.main() From c07a42ac2843464a7b99b789e87d8f7f9995c247 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 6 Oct 2025 10:10:46 +0200 Subject: [PATCH 29/38] add a convient class to allow agent to modify the gen_v setpoints Signed-off-by: DONNOT Benjamin --- CHANGELOG.rst | 7 ++ .../ControlVoltageFromFile.py | 22 ++++ grid2op/VoltageControler/__init__.py | 3 +- .../vcFromFileAgentOverrides.py | 119 ++++++++++++++++++ .../tests/test_vcfromfileagentoverrides.py | 50 ++++++++ 5 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 grid2op/VoltageControler/vcFromFileAgentOverrides.py create mode 100644 grid2op/tests/test_vcfromfileagentoverrides.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2d8e9a3f..d329bc49 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -100,6 +100,13 @@ Native multi agents support: - add detachment - add change_bus / set_bus +[1.12.2] - 2025-10-xx +---------------------- +- [FIXED] an issue preventing to change the way + the voltages are set +- [ADDED] a convenience class to allow agent to override + voltage setpoint from provided time seies (`VCFromFileAgentOverrides`) + [1.12.1] - 2025-08-28 ---------------------- - [BREAKING] (small impact) action "property" `shunt_p`, `shunt_q` and `shunt_bus` diff --git a/grid2op/VoltageControler/ControlVoltageFromFile.py b/grid2op/VoltageControler/ControlVoltageFromFile.py index 3322eafe..d811874d 100644 --- a/grid2op/VoltageControler/ControlVoltageFromFile.py +++ b/grid2op/VoltageControler/ControlVoltageFromFile.py @@ -17,6 +17,28 @@ class ControlVoltageFromFile(BaseVoltageController): chronics. If the voltages are not on the chronics (missing files), it will not change the voltage setpoint at all. + + .. warning:: + This function does not take into account in any way any setpoint given by the Agent. + + .. versionadded: 1.12.2 + + If you want your agent to be able to modify the voltage, you need to use the + :class:`grid2op.VoltageControler.VCFromFileAgentOverrides`, + for example like this: + + .. code-block:: python + + import grid2op + from grid2op.VoltageControler import VCFromFileAgentOverrides + + env_name = ... + + env = grid2op.make(env_name, + ..., + voltagecontroler_class=VCFromFileAgentOverrides) + + """ def __init__(self, diff --git a/grid2op/VoltageControler/__init__.py b/grid2op/VoltageControler/__init__.py index 1334099d..6801533c 100644 --- a/grid2op/VoltageControler/__init__.py +++ b/grid2op/VoltageControler/__init__.py @@ -1,4 +1,5 @@ -__all__ = ["BaseVoltageController", "ControlVoltageFromFile"] +__all__ = ["BaseVoltageController", "ControlVoltageFromFile", "VCFromFileAgentOverrides"] from grid2op.VoltageControler.BaseVoltageController import BaseVoltageController from grid2op.VoltageControler.ControlVoltageFromFile import ControlVoltageFromFile +from grid2op.VoltageControler.vcFromFileAgentOverrides import VCFromFileAgentOverrides diff --git a/grid2op/VoltageControler/vcFromFileAgentOverrides.py b/grid2op/VoltageControler/vcFromFileAgentOverrides.py new file mode 100644 index 00000000..77399582 --- /dev/null +++ b/grid2op/VoltageControler/vcFromFileAgentOverrides.py @@ -0,0 +1,119 @@ +# Copyright (c) 2019-2020, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + +import numpy as np +from typing import TYPE_CHECKING + +from grid2op.VoltageControler.ControlVoltageFromFile import ControlVoltageFromFile + +if TYPE_CHECKING: + from grid2op.Observation import BaseObservation + from grid2op.Action import BaseAction + + +class VCFromFileAgentOverrides(ControlVoltageFromFile): + """ + + .. versionadded: 1.12.2 + + This class allows the agent to override the voltage setpoint + of the generators. + + It allows the agent to modifies the generator voltages. If a generator + is not modified by the agent, then the value (for this generator) is + read from the time series (same behaviour for this generator as + the default :class:`grid2op.VoltageControler.ControlVoltageFromFile` class.) + + To use it, you can do: + + .. code-block:: python + + import grid2op + from grid2op.VoltageControler import VCFromFileAgentOverrides + + env_name = ... + + env = grid2op.make(env_name, + ..., + voltagecontroler_class=VCFromFileAgentOverrides) + + And if you want your agent to perform action on generator setpoint you can + specify it like this. + + If you want to modify all the generator at the same time: + + .. code-block:: python + + obs = env.reset() + + new_v_setpoint = 1. * obs.gen_v + + # specify the new setpoints + new_v_setpoint *= 1.01 # increase it by 1%, for example... + ####### + + modify_prod_v_value = env.action_space({"injection": {"prod_v": new_prod_v}}) + + obs, reward, done, info = env.step(modify_prod_v_value) + print(obs.gen_v) + + If you want to modify the setpoint of only one generator: + + + .. code-block:: python + + obs = env.reset() + + new_v_setpoint = 1. * obs.gen_v + + # specify the new setpoints + # for example say we want to tell the generator 0 + # to have a setpoint of 145 kV + + new_v_setpoint[0] = 145. + ####### + + modify_prod_v_value = env.action_space({"injection": {"prod_v": new_prod_v}}) + + obs, reward, done, info = env.step(modify_prod_v_value) + print(obs.gen_v) + + """ + def fix_voltage(self, + observation: "BaseObservation", + agent_action: "BaseAction", + env_action: "BaseAction", + prod_v_chronics: np.ndarray): + vect_ = None + if "prod_v" in agent_action._dict_inj and np.isfinite(agent_action._dict_inj["prod_v"]).any(): + # agent decision + vect_ = 1. * agent_action._dict_inj["prod_v"] + if prod_v_chronics is not None: + # default values (in the time series) + if vect_ is None: + # agent did not change anything + vect_ = prod_v_chronics + else: + # keep the agent choice and put the default values + # (from the time series) for the generator not modified + # by the agent + mask_default = ~np.isfinite(vect_) + vect_[mask_default] = prod_v_chronics[mask_default] + + # now build the action + if vect_ is not None: + res = self.action_space({"injection": {"prod_v": vect_}}) + else: + res = self.action_space() + + if observation is not None: + # cache the get_topological_impact to avoid useless computations later + # this is a speed optimization + _ = res.get_topological_impact(observation.line_status, _store_in_cache=True, _read_from_cache=False) + return res + return super().fix_voltage(observation, agent_action, env_action, prod_v_chronics) diff --git a/grid2op/tests/test_vcfromfileagentoverrides.py b/grid2op/tests/test_vcfromfileagentoverrides.py new file mode 100644 index 00000000..4e7b5810 --- /dev/null +++ b/grid2op/tests/test_vcfromfileagentoverrides.py @@ -0,0 +1,50 @@ +# Copyright (c) 2025, RTE (https://www.rte-france.com) +# See AUTHORS.txt +# This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. +# If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, +# you can obtain one at http://mozilla.org/MPL/2.0/. +# SPDX-License-Identifier: MPL-2.0 +# This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. + +import numpy as np +import warnings +import grid2op +import unittest + +from grid2op.Action import BaseAction +from grid2op.VoltageControler import VCFromFileAgentOverrides + + +class TestVCFromFileAO(unittest.TestCase): + def setUp(self) -> None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make("educ_case14_storage", + test=True, + action_class=BaseAction, + voltagecontroler_class=VCFromFileAgentOverrides, + _add_to_name=type(self).__name__) + assert isinstance(self.env._voltage_controler, VCFromFileAgentOverrides) + assert issubclass(self.env._voltagecontrolerClass, VCFromFileAgentOverrides) + self.init_obs = self.env.reset(seed=0, options={"time serie id":0}) + return super().setUp() + + def test_can_modify_one_gen_v(self): + new_prod_v = 1.0 * self.init_obs.gen_v + new_prod_v[0] = 150. + modify_prod_v_value = self.env.action_space({"injection": {"prod_v": new_prod_v}}) + obs, _, done, info = self.env.step(modify_prod_v_value) + assert not done + assert not info["is_ambiguous"] + assert not info["is_illegal"] + assert abs(obs.gen_v[0] - 150.) <= 1e-6 + assert abs(obs.gen_v[1:] - self.init_obs.gen_v[1:]).max() <= 1e-6 + + def test_can_modify_all_gen_v(self): + new_prod_v = 1.05 * self.init_obs.gen_v + modify_prod_v_value = self.env.action_space({"injection": {"prod_v": new_prod_v}}) + obs, _, done, info = self.env.step(modify_prod_v_value) + assert not done + assert not info["is_ambiguous"] + assert not info["is_illegal"] + assert abs(obs.gen_v - 1.05 * self.init_obs.gen_v).max() <= 1e-6 From bc5b06b46766d62021bcafca1368ad0cf7f5b045 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 6 Oct 2025 11:15:22 +0200 Subject: [PATCH 30/38] fix a test not passing for gymnasium 1.2.1 - asynvectenv Signed-off-by: DONNOT Benjamin --- CHANGELOG.rst | 3 ++- docs/user/rules.rst | 4 +--- grid2op/tests/test_gym_asynch_env.py | 15 ++++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d329bc49..c334b0fe 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -104,9 +104,10 @@ Native multi agents support: ---------------------- - [FIXED] an issue preventing to change the way the voltages are set +- [FIXED] a test when using gymnasium>= 1.2.1 (AsynchVectEnv) - [ADDED] a convenience class to allow agent to override voltage setpoint from provided time seies (`VCFromFileAgentOverrides`) - + [1.12.1] - 2025-08-28 ---------------------- - [BREAKING] (small impact) action "property" `shunt_p`, `shunt_q` and `shunt_bus` diff --git a/docs/user/rules.rst b/docs/user/rules.rst index 40ef5ac4..a6967fcc 100644 --- a/docs/user/rules.rst +++ b/docs/user/rules.rst @@ -51,8 +51,6 @@ To illustrate this, let's create an environment that we'll use for an example an import grid2op env_name = "l2rpn_case14_sandbox" env = grid2op.make(env_name) - env.set_id(0) - env.seed(0) # for this example, we enforce that we need to wait 3 steps # before being able to change again a line or a substation @@ -61,7 +59,7 @@ To illustrate this, let's create an environment that we'll use for an example an param.NB_TIMESTEP_COOLDOWN_LINE = 3 env.change_parameters(param) - obs = env.reset() + obs = env.reset(seed=0, options={"time serie id": 0}) # in summary (see descriptions bellow for more information) : act_line_1 = env.action_space({"set_line_status": [(1, -1)]}) diff --git a/grid2op/tests/test_gym_asynch_env.py b/grid2op/tests/test_gym_asynch_env.py index 85f688fd..70c94f5e 100644 --- a/grid2op/tests/test_gym_asynch_env.py +++ b/grid2op/tests/test_gym_asynch_env.py @@ -11,8 +11,6 @@ from gymnasium.vector import AsyncVectorEnv import warnings import numpy as np -from multiprocessing import set_start_method - import grid2op from grid2op.Action import PlayableAction from grid2op.gym_compat import GymEnv, BoxGymActSpace, BoxGymObsSpace, DiscreteActSpace, MultiDiscreteActSpace @@ -31,7 +29,7 @@ def setUp(self) -> None: _add_to_name=type(self).__name__, action_class=PlayableAction, experimental_read_from_local_dir=False) - obs = self.env.reset(seed=0, options={"time serie id": 0}) + _ = self.env.reset(seed=0, options={"time serie id": 0}) return super().setUp() def test_default_space_obs_act(self): @@ -48,12 +46,15 @@ def test_default_space_obs_act(self): dn_act_single = template_env.action_space.sample() for k, v in dn_act_single.items(): v[:] = 0 - dn_acts = {k: np.tile(v, reps=[2, 1]) for k, v in dn_act_single.items()} - obs2 = async_vect_env.step(dn_acts) + dn_acts = {k: np.tile(v, reps=[2, 1]) for k, v in dn_act_single.items()} + _ = async_vect_env.step(dn_acts) rnd_acts_li = [template_env.action_space.sample(), template_env.action_space.sample()] - rnd_acts = {k: np.concatenate((rnd_acts_li[0][k], rnd_acts_li[1][k])) for k in rnd_acts_li[0].keys()} - obs3 = async_vect_env.step(rnd_acts) + rnd_acts = {k: np.concatenate((rnd_acts_li[0][k].reshape(1,-1), + rnd_acts_li[1][k].reshape(1,-1))) + for k in rnd_acts_li[0].keys()} + + _ = async_vect_env.step(rnd_acts) obs, infos = async_vect_env.reset(seed=[2, 3], options={"time serie id": 0}, From 835674281e126a8b709704da2738a2c8f25ab576 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 6 Oct 2025 13:24:59 +0200 Subject: [PATCH 31/38] bump to 1.12.2.dev1 [skip ci] Signed-off-by: DONNOT Benjamin --- grid2op/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grid2op/__init__.py b/grid2op/__init__.py index ef2e8325..74bdec2a 100644 --- a/grid2op/__init__.py +++ b/grid2op/__init__.py @@ -11,7 +11,7 @@ Grid2Op a testbed platform to model sequential decision making in power systems. """ -__version__ = '1.12.2.dev0' +__version__ = '1.12.2.dev1' __all__ = [ "Action", From c7dcb9a8b819ca75841a07511a0e8e952481bdf4 Mon Sep 17 00:00:00 2001 From: DONNOT Benjamin Date: Mon, 6 Oct 2025 15:42:37 +0200 Subject: [PATCH 32/38] renaming flexibility to 'load_flexibility' [skip ci] Signed-off-by: DONNOT Benjamin --- grid2op/Action/_backendAction.py | 9 +- grid2op/Action/baseAction.py | 275 ++++++++++++++++-------------- grid2op/Environment/baseEnv.py | 48 +++--- grid2op/Space/GridObjects.py | 47 +++-- grid2op/tests/test_flexibility.py | 59 ++++--- 5 files changed, 225 insertions(+), 213 deletions(-) diff --git a/grid2op/Action/_backendAction.py b/grid2op/Action/_backendAction.py index 57936a20..449af7dd 100644 --- a/grid2op/Action/_backendAction.py +++ b/grid2op/Action/_backendAction.py @@ -894,11 +894,10 @@ def __iadd__(self, other : BaseAction) -> Self: self._is_cached = False # Ib2 change the (load) injection aka flexibility - if cls.flexibility_is_available: - flexibility = other._private_flexibility - if other._modif_flexibility: - self.load_p.change_val(flexibility) - self._is_cached = False + if cls.load_flexibility_is_available and other._modif_load_flexibility: + flexibility = other._private_load_flexibility + self.load_p.change_val(flexibility) + self._is_cached = False # Ic storage unit if other._modif_storage: diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index 678fe724..ec1eb85a 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -462,7 +462,6 @@ def change_whatever(grid): "set_bus", "change_bus", "redispatch", - "flexibility", # new in 1.12.x "set_storage", "curtail", "raise_alarm", @@ -470,6 +469,7 @@ def change_whatever(grid): "detach_load", # new in 1.11.0 "detach_gen", # new in 1.11.0 "detach_storage", # new in 1.11.0 + "load_flexibility", # new in 1.12.2 } attr_list_vect = [ @@ -478,7 +478,6 @@ def change_whatever(grid): "load_p", "load_q", "_redispatch", - "_flexibility", # new in 1.12.x "_set_line_status", "_switch_line_status", "_set_topo_vect", @@ -492,6 +491,7 @@ def change_whatever(grid): "_detach_load", # new in 1.11.0 "_detach_gen", # new in 1.11.0 "_detach_storage", # new in 1.11.0 + "_load_flexibility", # new in 1.12.2 ] #: new in 1.12.1: mapping between code name and agent name @@ -501,7 +501,7 @@ def change_whatever(grid): "load_p": "injection", "load_q": "injection", "_redispatch": "redispatch", - "_flexibility": "flexibility", # new in 1.12.x + "_load_flexibility": "load_flexibility", # new in 1.12.2 "_set_line_status": "set_line_status", "_switch_line_status": "change_line_status", "_set_topo_vect": "set_bus", @@ -595,10 +595,10 @@ def __init__(self, _names_chronics_to_backend: Optional[Dict[Literal["loads", "p self._private_redispatch : Optional[np.ndarray] = None # np.full(shape=cls.n_gen, fill_value=0.0, dtype=dt_float) # demand response / flexibility vector - if cls.flexibility_is_available: - self._private_flexibility : Optional[np.ndarray] = None + if cls.load_flexibility_is_available: + self._private_load_flexibility : Optional[np.ndarray] = None else: - self._private_flexibility = None + self._private_load_flexibility = None # storage unit vector self._private_storage_power : Optional[np.ndarray] = None # np.full( @@ -660,7 +660,6 @@ def __init__(self, _names_chronics_to_backend: Optional[Dict[Literal["loads", "p self._modif_set_status = False self._modif_change_status = False self._modif_redispatch = False - self._modif_flexibility = False self._modif_storage = False self._modif_curtailment = False self._modif_alarm = False @@ -668,6 +667,7 @@ def __init__(self, _names_chronics_to_backend: Optional[Dict[Literal["loads", "p self._modif_detach_load = False self._modif_detach_gen = False self._modif_detach_storage = False + self._modif_load_flexibility = False #: .. versionadded:: 1.12.1 #: cache not to recompute the `is_ambiguous` for the same action @@ -795,11 +795,11 @@ def _detach_storage(self) -> Optional[np.ndarray]: return self._private_detach_storage @property - def _flexibility(self) -> np.ndarray: + def _load_flexibility(self) -> np.ndarray: cls = type(self) - if self._private_flexibility is None and cls.flexibility_is_available: - self._private_flexibility = cls._build_attr("_flexibility") - return self._private_flexibility + if cls.load_flexibility_is_available and self._private_load_flexibility is None: + self._private_load_flexibility = cls._build_attr("_load_flexibility") + return self._private_load_flexibility @classmethod def _build_dict_attr_if_needed(cls): @@ -847,7 +847,7 @@ def _build_dict_attr_if_needed(cls): "_set_switch_status": None, "_change_switch_status": None, - "_flexibility": None, # new in 1.12.x + "_load_flexibility": None, # new in 1.12.2 } # shunts @@ -861,9 +861,9 @@ def _build_dict_attr_if_needed(cls): cls.DICT_ATTR_["_detach_gen"] = np.full(cls.n_gen, dtype=dt_bool, fill_value=False) cls.DICT_ATTR_["_detach_storage"] = np.full(cls.n_storage, dtype=dt_bool, fill_value=False) - # new in 1.12.x - if cls.flexibility_is_available: - cls.DICT_ATTR_["_flexibility"] = np.full(shape=cls.n_load, dtype=dt_float, fill_value=0.0) + # new in 1.12.2 + if cls.load_flexibility_is_available: + cls.DICT_ATTR_["_load_flexibility"] = np.full(shape=cls.n_load, dtype=dt_float, fill_value=0.0) @classmethod def _build_attr(cls, attr_nm: str): @@ -916,19 +916,20 @@ def process_detachment(cls): @classmethod def process_flexibility(cls): - if not cls.flexibility_is_available: + if not cls.load_flexibility_is_available: # this is really important, otherwise things from grid2op base types will be affected cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) cls.authorized_keys = copy.deepcopy(cls.authorized_keys) # remove the flexibility from the list to vector - for el in ["_flexibility"]: + for el in ["_load_flexibility"]: if el in cls.attr_list_vect: try: cls.attr_list_vect.remove(el) except ValueError: pass + # remove the flexibility from the allowed action - for el in ["flexibility"]: + for el in ["load_flexibility"]: if el in cls.authorized_keys: cls.authorized_keys.remove(el) cls._update_value_set() @@ -953,7 +954,6 @@ def _aux_copy(self, other: Self) -> None: "_modif_set_status", "_modif_change_status", "_modif_redispatch", - "_modif_flexibility", # new in 1.12.x "_modif_storage", "_modif_curtailment", "_modif_alarm", @@ -963,6 +963,7 @@ def _aux_copy(self, other: Self) -> None: "_modif_detach_storage", "_single_act", "_cached_is_not_ambiguous", + "_modif_load_flexibility", # new in 1.12.2 ] attr_vect = [ @@ -987,7 +988,7 @@ def _aux_copy(self, other: Self) -> None: attr_vect += ["_private_detach_load", "_private_detach_gen", "_private_detach_storage"] if cls.flexibility_is_available: - attr_vect += ["_private_flexibility"] + attr_vect += ["_private_load_flexibility"] for attr_nm in attr_simple: setattr(other, attr_nm, getattr(self, attr_nm)) @@ -1208,15 +1209,15 @@ def as_serializable_dict(self) -> dict: if not res[attr_key]: del res[attr_key] - if self.flexibility_is_available: # new in 1.12.x - if self._modif_flexibility: - res["flexibility"] = [ + if cls.load_flexibility_is_available: # new in 1.12.x + if self._modif_load_flexibility: + res["load_flexibility"] = [ (str(cls.name_Load[id_]), float(val)) - for id_, val in enumerate(self._private_flexibility) + for id_, val in enumerate(self._private_load_flexibility) if np.abs(val) >= 1e-7 ] - if not res["flexibility"]: - del res["flexibility"] + if not res["load_flexibility"]: + del res["load_flexibility"] return res @@ -1350,10 +1351,10 @@ def process_grid2op_compat(cls): # this feature did not exist before. cls.authorized_keys = copy.deepcopy(cls.authorized_keys) cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) - attr_key = "flexibility" + attr_key = "load_flexibility" if attr_key in cls.authorized_keys: cls.authorized_keys.remove(attr_key) - attr_vect = "_private_flexibility" + attr_vect = "_private_load_flexibility" if attr_vect in cls.attr_list_vect: cls.attr_list_vect.remove(attr_vect) @@ -1385,7 +1386,7 @@ def _reset_modified_flags(self): self._modif_detach_gen = False self._modif_detach_storage = False # flexibility - self._modif_flexibility = False + self._modif_load_flexibility = False def can_affect_something(self) -> bool: """ @@ -1403,7 +1404,7 @@ def can_affect_something(self) -> bool: or self._modif_set_status or self._modif_change_status or self._modif_redispatch - or self._modif_flexibility + or self._modif_load_flexibility or self._modif_storage or self._modif_curtailment or self._modif_alarm @@ -1511,10 +1512,10 @@ def _post_process_from_vect(self): self._modif_detach_gen = self._private_detach_gen is not None and (self._private_detach_gen).any() self._modif_detach_storage = self._private_detach_storage is not None and (self._private_detach_storage).any() - if cls.flexibility_is_available: - self._modif_flexibility = self._private_flexibility is not None and ( - np.isfinite(self._private_flexibility) & (np.abs(self._private_flexibility) >= 1e-7) - ).any() + if cls.load_flexibility_is_available: + self._modif_load_flexibility = self._private_load_flexibility is not None and ( + (np.abs(self._private_load_flexibility) > 1e-7).any() + ) if self.can_affect_something(): # reset the ambiguous cache @@ -1846,7 +1847,7 @@ def _dont_affect_topology(self) -> bool: and (not self._modif_detach_load) and (not self._modif_detach_gen) and (not self._modif_detach_storage) - and (not self._modif_flexibility) # new in 1.12.x + # and (not self._modif_load_flexibility) # new in 1.12.2 => flexibility does not affect topology ) def _aux_get_topo_impact_notopo(self, _store_in_cache: bool): @@ -2332,48 +2333,62 @@ def reset(self): Reset the action to the "do nothing" state. """ + cls = type(self) # False(line is disconnected) / True(line is connected) - self._private_set_line_status[:] = 0 - self._private_switch_line_status[:] = False + if self._private_set_line_status is not None: + self._private_set_line_status[:] = cls._build_attr("_set_line_status") + if self._private_switch_line_status is not None: + self._private_switch_line_status[:] = cls._build_attr("_switch_line_status") # injection change self._dict_inj = {} # topology changed - self._private_set_topo_vect[:] = 0 - self._private_change_bus_vect[:] = False + if self._private_set_topo_vect is not None: + self._private_set_topo_vect[:] = cls._build_attr("_set_topo_vect") + + if self._private_change_bus_vect is not None: + self._private_change_bus_vect[:] = cls._build_attr("_change_bus_vect") # add the hazards and maintenance usefull for saving. - self._private_hazards[:] = False - self._private_maintenance[:] = False + if self._private_hazards is not None: + self._private_hazards[:] = cls._build_attr("_hazards") + if self._private_maintenance is not None: + self._private_maintenance[:] = cls._build_attr("_maintenance") # redispatching vector - self._private_redispatch[:] = 0.0 + if self._private_redispatch is not None: + self._private_redispatch[:] = cls._build_attr("_redispatch") # flexibility vector - self._private_flexibility[:] = 0.0 + if self._private_load_flexibility is not None: + self._private_load_flexibility[:] = cls._build_attr("_load_flexibility") # storage - self._private_storage_power[:] = 0.0 + if self._private_storage_power is not None: + self._private_storage_power[:] = cls._build_attr("_storage_power") # storage - self._private_curtail[:] = -1.0 + if self._private_curtail is not None: + self._private_curtail[:] = cls._build_attr("_curtail") self._vectorized = None self._lines_impacted = None self._subs_impacted = None # shunts - if type(self).shunts_data_available: + if cls.shunts_data_available: self._private_shunt_p[:] = np.nan self._private_shunt_q[:] = np.nan self._private_shunt_bus[:] = 0 # alarm - self._private_raise_alarm[:] = False + if self._private_raise_alarm is not None: + self._private_raise_alarm[:] = cls._build_attr("_raise_alarm") # alert - self._private_raise_alert[:] = False + if self._private_raise_alert is not None: + self._private_raise_alert[:] = cls._build_attr("_raise_alert") self._reset_modified_flags() @@ -2421,19 +2436,19 @@ def _aux_iadd_redisp(self, other: "BaseAction"): self._redispatch[ok_ind] += redispatching[ok_ind] def _aux_iadd_flexibility(self, other: "BaseAction"): - if other._private_flexibility is None: + if other._private_load_flexibility is None: return - flexibility = other._flexibility - if (np.abs(flexibility) >= 1e-7).any(): - if "_flexibility" not in self.attr_list_set: + flex = other._load_flexibility + if (np.isfinite(flex)).any(): + if "_load_flexibility" not in self.attr_list_set: warnings.warn( - type(self).ERR_ACTION_CUT.format("_flexibility") + type(self).ERR_ACTION_CUT.format("_load_flexibility") ) else: - ok_ind = np.isfinite(flexibility) - self._flexibility[ok_ind] += flexibility[ok_ind] + ok_ind = np.isfinite(flex) + self._load_flexibility[ok_ind] += flex[ok_ind] - def _aux_iadd_curtail(self, other): + def _aux_iadd_curtail(self, other: "BaseAction"): curtailment = other._curtail ok_ind = np.isfinite(curtailment) & (np.abs(curtailment + 1.0) >= 1e-7) if ok_ind.any(): @@ -2470,7 +2485,7 @@ def _aux_iadd_modif_flags(self, other: Self): self._modif_inj = self._modif_inj or other._modif_inj self._modif_shunt = self._modif_shunt or other._modif_shunt self._modif_redispatch = self._modif_redispatch or other._modif_redispatch - self._modif_flexibility = self._modif_flexibility or other._modif_flexibility # new in 1.12.x + self._modif_flexibility = self._modif_flexibility or other._modif_flexibility # new in 1.12.2 self._modif_storage = self._modif_storage or other._modif_storage self._modif_curtailment = self._modif_curtailment or other._modif_curtailment self._modif_alarm = self._modif_alarm or other._modif_alarm @@ -2634,7 +2649,7 @@ def __iadd__(self, other: Self): self._aux_iadd_redisp(other) # flexibility, new in 1.12.x - if type(self).flexibility_is_available: + if type(self).load_flexibility_is_available: self._aux_iadd_flexibility(other) # storage @@ -2757,7 +2772,7 @@ def __call__(self) -> Tuple[dict, np.ndarray, np.ndarray, np.ndarray, np.ndarray set_topo_vect = self._private_set_topo_vect change_bus_vect = self._private_change_bus_vect redispatch = self._private_redispatch - flexibility = self._private_flexibility # new in 1.12.x + flexibility = self._private_load_flexibility # new in 1.12.x storage_power = self._private_storage_power # remark: curtailment is handled by an algorithm in the environment, so don't need to be returned here shunts = {} @@ -3102,8 +3117,8 @@ def _digest_redispatching(self, dict_): self.redispatch = dict_["redispatch"] def _digest_flexibility(self, dict_): - if "flexibility" in dict_: - self.flexibility = dict_["flexibility"] + if "load_flexibility" in dict_: + self.load_flexibility = dict_["load_flexibility"] def _digest_storage(self, dict_): if "set_storage" in dict_: @@ -3176,7 +3191,7 @@ def _check_keys_exist(action_cls:GridObjects, act_dict): # with shunt in "init_state.json" with a backend that does not # handle shunt continue - if kk == "flexibility" and not action_cls.flexibility_is_available: + if kk == "load_flexibility" and not action_cls.load_flexibility_is_available: # no warnings are raised in this case because if a warning # were raised it could crash some environment # with flexibility in "init_state.json" with a backend that does not @@ -3267,9 +3282,9 @@ def update(self, tuple, each tuple being 2 elements: first the generator ID, second the amount of redispatching, for example `[(1, -23), (12, +17)]` - - "flexibility": the best use of this is to specify either the numpy array of the flexibility / demand response + - "load_flexibility": the best use of this is to specify either the numpy array of the flexibility / demand response vector you want to apply (that should have the size of the number of loads on the grid) or to specify a list - of tuples, each tuple being 2 elements: first the load ID, second the amount of flexibility, + of tuples, each tuple being 2 elements: first the load ID, second the amount of flexibility (in MW), for example `[(0, -12), (3, +7)]` - "set_storage": the best use of this is to specify either the numpy array of the storage units vector @@ -3400,11 +3415,11 @@ def update(self, storage_act = env.action_space({"curtail": [(renewable_energy_source, 0.8)]}) print(storage_act) - *Example 9*: apply flexibility of +7.21 MW at load with id 2 and -12.3 at load with id 1 + *Example 9*: apply flexibility of +7.21 MW at load with id 2 and -12.3MW at load with id 1 .. code-block:: python - flex_act = env.action_space({"flexibility": [(2, +7.21), (1, -12.3)]}) + flex_act = env.action_space({"load_flexibility": [(2, +7.21), (1, -12.3)]}) print(flex_act) Returns @@ -3422,7 +3437,7 @@ def update(self, if cls.shunts_data_available: # do not digest shunt when backend does not support it self._digest_shunt(dict_) - if cls.flexibility_is_available: + if cls.load_flexibility_is_available: self._digest_flexibility(dict_) self._digest_injection(dict_) @@ -3539,17 +3554,17 @@ def _check_for_correct_modif_flags(self): if "redispatch" not in cls.authorized_keys: raise IllegalAction("You illegally act on the redispatching") - if cls.flexibility_is_available: - if self._private_flexibility is not None and (np.abs(self._private_flexibility) >= 1e-7).any(): - if not self._modif_flexibility: + if cls.load_flexibility_is_available: + if self._private_load_flexibility is not None and (np.abs(self._private_load_flexibility) >= 1e-7).any(): + if not self._modif_load_flexibility: raise AmbiguousAction( - "A action of type flexibility is performed while the appropriate flag " + "A action of type load flexibility is performed while the appropriate flag " "is not " - "set. Please use the official grid2op action API to perform flexibility " + "set. Please use the official grid2op action API to perform load flexibility " "action." ) - if "flexibility" not in cls.authorized_keys: - raise IllegalAction("You illegally act on the flexibility") + if "load_flexibility" not in cls.authorized_keys: + raise IllegalAction("You illegally act on the load flexibility") if self._private_storage_power is not None and (np.abs(self._private_storage_power) >= 1e-7).any(): if not self._modif_storage: @@ -3650,7 +3665,7 @@ def _check_for_ambiguity(self): - TODO - - For flexibility, Ambiguous actions can happen when: + - For load flexibility, Ambiguous actions can happen when: - A flexibility action is active, but :attr:`grid2op.Space.GridObjects.flexibility_available` is set to ``False`` @@ -3739,10 +3754,10 @@ def _check_for_ambiguity(self): "there are {} in the grid".format(len(self._private_redispatch), cls.n_gen) ) - if self._private_flexibility is not None and len(self._private_flexibility) != cls.n_load: + if self._private_load_flexibility is not None and len(self._private_load_flexibility) != cls.n_load: raise InvalidNumberOfLoads( - "This action acts on {} loads (flexibility= while " - "there are {} in the grid".format(len(self._private_flexibility), cls.n_load) + "This action acts on {} loads flexibility while " + "there are {} in the grid".format(len(self._private_load_flexibility), cls.n_load) ) @@ -4017,24 +4032,25 @@ def _is_detachment_ambiguous(self): def _is_flexibility_ambiguous(self): "check if flexibility actions are ambiguous" - if self.flexibility_is_available: + cls = type(self) + if cls.load_flexibility_is_available: if self._modif_flexibility: # new in 1.12.x - if "flexibility" not in type(self).authorized_keys: + if "load_flexibility" not in cls.authorized_keys: raise AmbiguousAction( - 'Action of type "flexibility" are not supported by this action type' + 'Action of type "load_flexibility" are not supported by this action type' ) - if (np.abs(self._private_flexibility[~type(self).load_flexible]) >= 1e-7).any(): + if (np.abs(self._private_load_flexibility[~cls.load_flexible]) >= 1e-7).any(): raise InvalidFlexibility( "Trying to apply a flexibility action on a non-flexible load" ) if self._single_act: - if (self._private_flexibility > type(self).load_max_ramp_up).any(): + if (self._private_load_flexibility > cls.load_max_ramp_up).any(): raise InvalidFlexibility( "Some flexibility amount are above the maximum ramp up" ) - if (-self._private_flexibility > type(self).load_max_ramp_down).any(): + if (-self._private_load_flexibility > cls.load_max_ramp_down).any(): raise InvalidFlexibility( "Some flexibility amount are bellow the maximum ramp down" ) @@ -4371,22 +4387,22 @@ def __str__(self) -> str: # flexibility, new in 1.12.x - if self.flexibility_is_available: + if type(self).load_flexibility_is_available: if self._modif_flexibility: res.append( "\t - Modify the loads with flexibility in the following way:" ) for load_idx in range(self.n_load): - if np.abs(self._private_flexibility[load_idx]) >= 1e-7: + if np.abs(self._private_load_flexibility[load_idx]) >= 1e-7: load_name = self.name_load[load_idx] - f_amount = self._private_flexibility[load_idx] + f_amount = self._private_load_flexibility[load_idx] res.append( '\t \t - Flexibility "{}" of {:.2f} MW'.format( load_name, f_amount ) ) else: - res.append("\t - NOT perform any flexibility action") + res.append("\t - NOT perform any load flexibility action") return "\n".join(res) @@ -4398,7 +4414,7 @@ def impact_on_objects(self) -> dict: ------- dict: :class:`dict` The dictionary representation of an action impact on objects with keys, "has_impact", "injection", - "force_line", "switch_line", "topology", "redispatch", "flexibility", "storage", "curtailment". + "force_line", "switch_line", "topology", "redispatch", "load_flexibility", "storage", "curtailment". """ # handles actions on injections @@ -4548,13 +4564,13 @@ def impact_on_objects(self) -> dict: has_impact = True flexibility = {"changed": False, "loads": []} - if self.flexibility_is_available: # new in 1.12.x + if type(self).load_flexibility_is_available: # new in 1.12.x # handle flexibility - if self._private_flexibility is not None and (np.abs(self._private_flexibility) >= 1e-7).any(): + if self._private_load_flexibility is not None and (np.abs(self._private_load_flexibility) >= 1e-7).any(): for load_idx in range(cls.n_load): - if np.abs(self._private_flexibility[load_idx]) >= 1e-7: + if np.abs(self._private_load_flexibility[load_idx]) >= 1e-7: load_name = self.name_load[load_idx] - f_amount = self._private_flexibility[load_idx] + f_amount = self._private_load_flexibility[load_idx] flexibility["loads"].append( {"load_id": load_idx, "gen_name": load_name, "amount": f_amount} ) @@ -4568,7 +4584,7 @@ def impact_on_objects(self) -> dict: "switch_line": switch_line_status, "topology": topology, "redispatch": redispatch, - "flexibility": flexibility, + "load_flexibility": flexibility, "storage": storage, "curtailment": curtailment, } @@ -4678,7 +4694,7 @@ def _aux_as_dict_hazards_maintenance(self, res): def as_dict(self) -> Dict[Literal["load_p", "load_q", "prod_p", "prod_v", "change_line_status", "set_line_status", "change_bus_vect", "set_bus_vect", - "redispatch", "flexibility", + "redispatch", "load_flexibility", "storage_power", "curtailment"], Any]: """ @@ -4733,7 +4749,7 @@ def as_dict(self) -> Dict[Literal["load_p", "load_q", "prod_p", "prod_v", disconnected because of maintenance operations. * `redispatch` the redispatching action (if any). It gives, for each generator (all generator, not just the dispatchable one) the amount of power redispatched in this action. - * `flexibility` the flexibility action (if any). It gives, for each load (all loads, not just the + * `load flexibility` the flexibility action (if any). It gives, for each load (all loads, not just the flexible ones) the amount of demand flexibility in this action. * `storage_power`: the setpoint for production / consumption for all storage units * `curtailment`: the curtailment performed on all generator @@ -4768,9 +4784,9 @@ def as_dict(self) -> Dict[Literal["load_p", "load_q", "prod_p", "prod_v", if type(self).shunts_data_available: self._aux_as_dict_shunt(res) - if type(self).flexibility_is_available: - if self._private_flexibility is not None and (np.abs(self._private_flexibility) >= 1e-7).any(): - res["flexibility"] = 1.0 * self._private_flexibility + if type(self).load_flexibility_is_available: + if self._private_load_flexibility is not None and (np.abs(self._private_flexibility) >= 1e-7).any(): + res["load_flexibility"] = 1.0 * self._private_load_flexibility return res @@ -4783,7 +4799,7 @@ def get_types(self) -> Tuple[bool, bool, bool, bool, bool, bool, bool, bool]: - "topology": does this action modifies the topology of the grid (*ie* set or switch some buses) - "line": does this action modifies the line status - "redispatching" does this action modifies the redispatching - - "flexibility" does this action modify the flexibility + - "load_flexibility" does this action modify the flexibility - "storage" does this action impact the production / consumption of storage units - "curtailment" does this action impact the non renewable generators through curtailment @@ -4833,9 +4849,10 @@ def get_types(self) -> Tuple[bool, bool, bool, bool, bool, bool, bool, bool]: redispatching = self._private_redispatch is not None and (np.abs(self._private_redispatch) >= 1e-7).any() storage = self._modif_storage curtailment = self._modif_curtailment - # flexibility, new in 1.12.x - if type(self).flexibility_is_available: - flexibility = self._private_flexibility is not None and (np.abs(self._private_flexibility) >= 1e-7).any() + + # flexibility, new in 1.12.2 + if type(self).load_flexibility_is_available: + flexibility = self._private_load_flexibility is not None and (np.abs(self._private_load_flexibility) >= 1e-7).any() else: flexibility = False return injection, voltage, topology, line, redispatching, flexibility, storage, curtailment @@ -4857,8 +4874,8 @@ def _aux_effect_on_load(self, load_id): my_id = cls.load_pos_topo_vect[load_id] res["change_bus"] = self._private_change_bus_vect[my_id] if self._private_change_bus_vect is not None else False res["set_bus"] = self._private_set_topo_vect[my_id] if self._private_set_topo_vect is not None else 0 - if cls.flexibility_is_available: - res["flexibility"] = self._private_flexibility[load_id] if self._private_flexibility is not None else 0. + if cls.load_flexibility_is_available: + res["load_flexibility"] = self._private_load_flexibility[load_id] if self._private_load_flexibility is not None else 0. return res def _aux_effect_on_gen(self, gen_id): @@ -7199,31 +7216,31 @@ def redispatch(self, values): ) @property - def flexibility(self) -> np.ndarray: - res = 1.0 * self._flexibility + def load_flexibility(self) -> np.ndarray: + res = 1.0 * self._load_flexibility res.flags.writeable = False return res - @flexibility.setter - def flexibility(self, values): - if "flexibility" not in self.authorized_keys: + @load_flexibility.setter + def load_flexibility(self, values): + if "load_flexibility" not in self.authorized_keys: raise IllegalAction( - "Impossible to perform flexibility with this action type." + "Impossible to perform load flexibility with this action type." ) - orig_ = self.flexibility + orig_ = self.load_flexibility try: self._aux_affect_object_float( values, - "flexibility", + "load_flexibility", self.n_load, self.name_load, np.arange(self.n_load), - self._flexibility, + self._load_flexibility, _nm_ch_bk_key="loads", ) - self._modif_flexibility = True + self._modif_load_flexibility = True except Exception as exc_: - self._private_flexibility[:] = orig_ + self._private_load_flexibility[:] = orig_ raise AmbiguousAction( f"Impossible to modify the flexibility with your input. " f"Please consult the documentation. " @@ -7970,22 +7987,22 @@ def _aux_decompose_as_unary_actions_redisp(self, res["redispatch"].append(tmp) def _aux_decompose_as_unary_actions_flexibility(self, - cls: Type[Self], - group_flexibility: bool, - res): + cls: Type[Self], + group_flexibility: bool, + res): if group_flexibility: tmp = cls() tmp._modif_flexibility = True - tmp._flexibility[:] = self._private_flexibility - res["flexibility"] = [tmp] + tmp._load_flexibility[:] = self._private_load_flexibility + res["load_flexibility"] = [tmp] else: - load_changed = (np.abs(self._private_flexibility) >= 1e-7).nonzero()[0] - res["flexibility"] = [] + load_changed = (np.abs(self._private_load_flexibility) >= 1e-7).nonzero()[0] + res["load_flexibility"] = [] for load_id in load_changed: tmp = cls() tmp._modif_flexibility = True - tmp._redis_flexibilitypatch[load_id] = self._private_flexibility[load_id] - res["flexibility"].append(tmp) + tmp._load_flexibility[load_id] = self._private_load_flexibility[load_id] + res["load_flexibility"].append(tmp) def _aux_decompose_as_unary_actions_storage(self, cls: Type[Self], @@ -8034,7 +8051,7 @@ def decompose_as_unary_actions(self, "change_line_status", "set_line_status", "redispatch", - "flexibility", + "load_flexibility", "set_storage", "curtail"], List["BaseAction"]]: @@ -8064,7 +8081,7 @@ def decompose_as_unary_actions(self, - "redispatch" if the action affects the grid with `redispatch` In this case the value associated with this key is a list containing only action that performs `redispatch` - - "flexibility" if the action affects the grid with `flexibility` + - "load_flexibility" if the action affects the grid with `flexibility` In this case the value associated with this key is a list containing only action that performs `flexibility` - "set_storage" if the action affects the grid with `set_storage` @@ -8175,8 +8192,8 @@ def decompose_as_unary_actions(self, self._aux_decompose_as_unary_actions_set_ls(cls, group_line_status, res) if self._modif_redispatch: self._aux_decompose_as_unary_actions_redisp(cls, group_redispatch, res) - if cls.flexibility_is_available: # new in 1.12.x - if self._modif_flexibility: + if cls.load_flexibility_is_available: # new in 1.12.2 + if self._modif_load_flexibility: self._aux_decompose_as_unary_actions_flexibility(cls, group_flexibility, res) if self._modif_storage: self._aux_decompose_as_unary_actions_storage(cls, group_storage, res) diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 03b582ba..ef45520e 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -1045,7 +1045,7 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): new_obj._needs_active_bus = self._needs_active_bus # flexibility / demand response, new in 1.12.x - # if type(self).flexibility_is_available: + # if type(self).load_flexibility_is_available: new_obj._forbid_flex_off = self._forbid_flex_off new_obj._target_flex = copy.deepcopy(self._target_flex) new_obj._already_modified_load = copy.deepcopy(self._already_modified_load) @@ -1526,7 +1526,7 @@ def _has_been_initialized(self): self._delta_gen_p = np.zeros(bk_type.n_gen, dtype=dt_float) # flexibility / demand response (1.12.x) - # if type(self).flexibility_is_available: + # if type(self).load_flexibility_is_available: self._target_flex = np.zeros(self.n_load, dtype=dt_float) self._already_modified_load = np.zeros(self.n_load, dtype=dt_bool) self._actual_flex = np.zeros(self.n_load, dtype=dt_float) @@ -1577,7 +1577,7 @@ def _update_parameters(self): self._parameters = self.__new_param self._ignore_min_up_down_times = self._parameters.IGNORE_MIN_UP_DOWN_TIME self._forbid_dispatch_off = not self._parameters.ALLOW_DISPATCH_GEN_SWITCH_OFF - if type(self).flexibility_is_available: # flexibility / demand response, new in 1.12.x + if type(self).load_flexibility_is_available: # flexibility / demand response, new in 1.12.x self._forbid_flex_off = not self._parameters.ALLOW_FLEX_LOAD_SWITCH_OFF # type of power flow to play @@ -2145,11 +2145,11 @@ def _get_already_modified_gen(self, action: BaseAction): return self._already_modified_gen def _get_already_modified_load(self, action: BaseAction): - if not action._modif_flexibility: + if not action._modif_load_flexibility: # nothing changes if the action does # not affect flexibility return self._already_modified_load - flex_act_orig = action._flexibility + flex_act_orig = action._load_flexibility is_flexed = np.abs(flex_act_orig) > 1e-7 self._target_flex[self._already_modified_load] += flex_act_orig[self._already_modified_load] first_modified = (~self._already_modified_load) & is_flexed @@ -2173,8 +2173,8 @@ def _prepare_redisp_and_flex(self, action: BaseAction, new_gen_p:np.ndarray, new redisp_act_orig = None # get the flexibility action (if any) - if action._modif_flexibility and cls.flexibility_is_available: - flex_act_orig = action._flexibility.copy() + if action._modif_load_flexibility and cls.load_flexibility_is_available: + flex_act_orig = action._load_flexibility.copy() else: flex_act_orig = None @@ -2185,16 +2185,16 @@ def _prepare_redisp_and_flex(self, action: BaseAction, new_gen_p:np.ndarray, new and (np.abs(self._target_flex) <= 1e-7).all() and (np.abs(self._actual_flex) <= 1e-7).all()) # If no redispatching / flexibility is active, return - if disp_cond and not cls.flexibility_is_available: + if disp_cond and not cls.load_flexibility_is_available: return valid, except_, info_ - if disp_cond and flex_cond and cls.flexibility_is_available: + if disp_cond and flex_cond and cls.load_flexibility_is_available: return valid, except_, info_ if redisp_act_orig is None: redisp_act_orig = type(action)._build_attr("_redispatch") if flex_act_orig is None: - flex_act_orig = type(action)._build_attr("_flexibility") + flex_act_orig = type(action)._build_attr("_load_flexibility") if self.redispatching_unit_commitment_availble: @@ -2244,7 +2244,7 @@ def _prepare_redisp_and_flex(self, action: BaseAction, new_gen_p:np.ndarray, new ).nonzero()[0] } ) - if self.flexibility_is_available: + if type(self).load_flexibility_is_available: if (self._target_flex > self.load_size).any(): # Action is invalid, the target flexibility would be above the size of at least one flexible load cond_invalid = self._target_flex > self.load_size @@ -2307,7 +2307,7 @@ def _make_redisp_and_flex(self, already_modified_gen:np.ndarray, new_gen_p:np.nd np.abs(self._amount_storage) >= self._tol_poly or np.abs(self._sum_curtailment_mw) >= self._tol_poly or np.abs(self._detached_elements_mw) >= self._tol_poly) - flex_cond = self.flexibility_is_available and \ + flex_cond = type(self).load_flexibility_is_available and \ (np.abs((self._actual_flex).sum()) >= self._tol_poly or np.max(flex_mismatch) >= self._tol_poly) @@ -2327,7 +2327,7 @@ def _compute_dispatch_and_flex_vect(self, already_modified_gen:np.ndarray, new_g # actions or curtailment actions on the "init state" of the grid if self.nb_time_step == 0: self._gen_activeprod_t_redisp[:] = new_gen_p - if cls.flexibility_is_available: + if cls.load_flexibility_is_available: self._load_demand_t_flex[:] = new_load_p # First define the involved generators / flexible loads @@ -2386,7 +2386,7 @@ def _compute_dispatch_and_flex_vect(self, already_modified_gen:np.ndarray, new_g self._gen_activeprod_t_redisp[gen_involved_tmp]) avail_gen_up_tmp = np.minimum(p_max_gen_up_tmp, cls.gen_max_ramp_up[gen_involved_tmp]) - if cls.flexibility_is_available and self._parameters.ALLOW_FLEX_LOAD_SWITCH_OFF: + if cls.load_flexibility_is_available and self._parameters.ALLOW_FLEX_LOAD_SWITCH_OFF: load_involved_tmp = self.load_flexible if cls.detachment_is_allowed: load_involved_tmp[self._backend_action.get_load_detached()] = False @@ -3466,16 +3466,16 @@ def _aux_update_backend_action(self, res_exc_ = None # cancel the redisp and storage tags (set later in the code) tag_redisp = action._modif_redispatch - tag_flex = action._modif_flexibility + tag_flex = action._modif_load_flexibility tag_storage = action._modif_storage action._modif_redispatch = False - action._modif_flexibility = False + action._modif_load_flexibility = False action._modif_storage = False # cancel the values if tag_redisp: action._redispatch[:] = 0.0 # redispatch is added after everything in the code (even after the opponent) if tag_flex: - action._flexibility[:] = 0.0 # flexibility, new in 1.12.x + action._load_flexibility[:] = 0.0 # flexibility, new in 1.12.x if tag_storage: action._storage_power[:] = 0.0 # storage is also added after everything # add the action @@ -3484,12 +3484,12 @@ def _aux_update_backend_action(self, if tag_storage: action._storage_power[:] = action_storage_power if tag_flex: - action._flexibility[:] = init_flex + action._load_flexibility[:] = init_flex if tag_redisp: action._redispatch[:] = init_disp # put back the tags action._modif_redispatch = tag_redisp - action._modif_flexibility = tag_flex + action._modif_load_flexibility = tag_flex action._modif_storage = tag_storage return res_exc_ @@ -3749,7 +3749,7 @@ def _aux_apply_detachment(self, new_gen_p:np.ndarray, new_gen_pth:np.ndarray, new_gen_p[gen_detached_user] = 0. new_gen_pth[gen_detached_user] = 0. self._actual_dispatch[gen_detached_user] = 0. - if type(self).flexibility_is_available: + if type(self).load_flexibility_is_available: new_load_p[load_detached_user] = 0. new_load_pth[load_detached_user] = 0. self._actual_flex[load_detached_user] = 0. @@ -3886,10 +3886,10 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, init_disp = action._redispatch.copy() # dispatching action else: init_disp = type(action)._build_attr("_redispatch") - if action._modif_flexibility: - init_flex = action._flexibility.copy() # flexibility action + if action._modif_load_flexibility: + init_flex = action._load_flexibility.copy() # flexibility action else: - init_flex = type(action)._build_attr("_flexibility") + init_flex = type(action)._build_attr("_load_flexibility") init_alert = None if cls.dim_alerts > 0: @@ -4006,7 +4006,7 @@ def step(self, action: BaseAction) -> Tuple[BaseObservation, # Redispatch + Flexibility beg__redisp = time.perf_counter() - if (cls.redispatching_unit_commitment_availble or cls.n_storage > 0 or cls.flexibility_is_available) and self._parameters.ENV_DOES_REDISPATCHING: + if (cls.redispatching_unit_commitment_availble or cls.n_storage > 0 or cls.load_flexibility_is_available) and self._parameters.ENV_DOES_REDISPATCHING: # This computes the "optimal" redispatching of generators and flexibility # adjustment of loads # Note: It is in this function that the limiting of the curtailment / storage actions diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index b30cf468..02ab6cb6 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -194,7 +194,7 @@ class GridObjects: These information are loaded using the :func:`grid2op.Backend.Backend.load_redispacthing_data` method. - Note that if you want to model an environment with flexibility capabilities, you also need + Note that if you want to model an environment with load_flexibility capabilities, you also need to provide the following attributes: - :attr:`GridObjects.load_size` @@ -405,7 +405,7 @@ class GridObjects: - :attr:`GridObjects.gen_shutdown_cost` - :attr:`GridObjects.gen_renewable` - flexibility_is_available: ``bool`` + load_flexibility_is_available: ``bool`` Does the current grid allow for flexible loads. If not, any attempt to use it will raise a :class:`grid2op.Exceptions.FlexibilityNotAvailable` error. [*class attribute*] For an environment to be compatible with this feature, you need to set up, when loading the backend: @@ -422,10 +422,6 @@ class GridObjects: shunts_data_available: ``bool`` Whether or not the backend support the shunt data. [*class attribute*] - - flexibility_is_available: ``bool`` - Whether or not flexibility data is available in the environment [*class attribute*] - .. versionadded:: 1.12.x n_shunt: ``int`` or ``None`` Number of shunts on the grid. It might be ``None`` if the backend does not support shunts. [*class attribute*] @@ -642,7 +638,7 @@ class GridObjects: gen_renewable : ClassVar[Optional[np.ndarray]] = None # Flexible load data, not available in all Environments, new in 1.12.x - flexibility_is_available: ClassVar[bool] = False + load_flexibility_is_available: ClassVar[bool] = False load_size: ClassVar[Optional[np.ndarray]] = None load_flexible: ClassVar[Optional[np.ndarray]] = None load_max_ramp_up: ClassVar[Optional[np.ndarray]] = None @@ -670,7 +666,7 @@ class GridObjects: shunt_to_subid : ClassVar[Optional[np.ndarray]] = None # flexibility, not available in every environment - flexibility_is_available: ClassVar[bool] = DEFAULT_FLEXIBILITY_IS_AVAILABLE + load_flexibility_is_available: ClassVar[bool] = DEFAULT_FLEXIBILITY_IS_AVAILABLE # alarm / alert assistant_warning_type = None @@ -927,7 +923,7 @@ def _clear_grid_dependant_class_attributes(cls) -> None: cls.alertable_line_ids = [] # Flexible load data, not available in all environments - cls.flexibility_is_available = DEFAULT_FLEXIBILITY_IS_AVAILABLE + cls.load_flexibility_is_available = DEFAULT_FLEXIBILITY_IS_AVAILABLE cls.load_size = None cls.load_flexible = None cls.load_max_ramp_up = None @@ -2052,7 +2048,7 @@ def _assign_attr(cls, attrs_list, tp, tp_nm, raise_if_none=False): def _check_convert_to_np_array(cls, raise_if_none=True): # convert bool to array of bools attrs_bool = [] - if cls.flexibility_is_available: # new in 1.12.x + if cls.load_flexibility_is_available: # new in 1.12.2 attrs_bool.append("load_flexible") cls._assign_attr(attrs_bool, dt_bool, "bool", raise_if_none) @@ -2108,7 +2104,7 @@ def _check_convert_to_np_array(cls, raise_if_none=True): "gen_cost_per_MW", "gen_startup_cost", "gen_shutdown_cost"] - if cls.flexibility_is_available: + if cls.load_flexibility_is_available: attrs_float += ["load_size", "load_max_ramp_up", "load_max_ramp_down", @@ -2159,11 +2155,11 @@ def assert_grid_correct_cls(cls): "at the moment (make sure `detachment_is_allowed` " "is a bool)") - if isinstance(cls.flexibility_is_available, (bool, dt_bool)): - cls.flexibility_is_available = dt_bool(cls.flexibility_is_available) + if isinstance(cls.load_flexibility_is_available, (bool, dt_bool)): + cls.load_flexibility_is_available = dt_bool(cls.load_flexibility_is_available) else: raise EnvError("Grid2op cannot handle flexibility of loads " - "at the moment (make sure `flexibility_is_available` " + "at the moment (make sure `load_flexibility_is_available` " "is a bool)") if (cls.n_busbar_per_sub < 1).any(): @@ -2444,7 +2440,7 @@ def assert_grid_correct_cls(cls): cls._check_validity_dispathcing_data() # flexibility / demand response - if cls.flexibility_is_available: + if cls.load_flexibility_is_available: cls._check_validity_flexibility_data() # shunt data @@ -3148,8 +3144,8 @@ def init_grid(cls, gridobj, force=False, extra_name=None, force_module=None, _lo if gridobj.detachment_is_allowed != DEFAULT_ALLOW_DETACHMENT: name_res += "_allowDetach" - if gridobj.flexibility_is_available != DEFAULT_FLEXIBILITY_IS_AVAILABLE: - name_res += "_allowFlexibility" + if gridobj.load_flexibility_is_available != DEFAULT_FLEXIBILITY_IS_AVAILABLE: + name_res += "_allowLoadFlexibility" if _local_dir_cls is not None and gridobj._PATH_GRID_CLASSES is not None: # new in grid2op 1.10.3: @@ -3278,7 +3274,7 @@ def process_grid2op_compat(cls): if glop_ver < cls.MIN_VERSION_FLEX: # Flexibility / demand response did not exist, default value # should have no effect - cls.flexibility_is_available = DEFAULT_FLEXIBILITY_IS_AVAILABLE + cls.load_flexibility_is_available = DEFAULT_FLEXIBILITY_IS_AVAILABLE res = True if res: @@ -3904,7 +3900,7 @@ def topo_vect_element(cls, topo_vect_id: int) -> Dict[Literal["load_id", "gen_id raise Grid2OpException(f"Unknown element at position {topo_vect_id}") @staticmethod - def _make_cls_dict(cls, res, as_list=True, copy_=True, _topo_vect_only=False): + def _make_cls_dict(cls: "Type[GridObjects]", res, as_list=True, copy_=True, _topo_vect_only=False): """ INTERNAL @@ -4136,7 +4132,7 @@ def _make_cls_dict(cls, res, as_list=True, copy_=True, _topo_vect_only=False): for nm_attr in cls._li_attr_disp: res[nm_attr] = None - # Flexibility, new in 1.12.x + # Flexibility, new in 1.12.2 for nm_attr, type_attr in zip(cls._li_attr_flex_load, cls._type_attr_flex_load): if nm_attr not in res and hasattr(cls, nm_attr) is False: # Note: Need default values here for flex to work together @@ -4326,7 +4322,7 @@ def _make_cls_dict_extended(cls, res: CLS_AS_DICT_TYPING, as_list=True, copy_=Tr ] = cls.redispatching_unit_commitment_availble # flexibility / deamnd response - res["flexibility_is_available"] = cls.flexibility_is_available + res["load_flexibility_is_available"] = cls.load_flexibility_is_available # n_busbar_per_sub res["n_busbar_per_sub"] = cls.n_busbar_per_sub @@ -4528,11 +4524,11 @@ class res(GridObjects): ), ) - # Demand Response / Flexibility, new in 1.12.x + # Demand Response / Flexibility, new in 1.12.2 if np.isclose(dict_.get("load_size", np.zeros(1)), 0.0).all(): - cls.flexibility_is_available = DEFAULT_FLEXIBILITY_IS_AVAILABLE + cls.load_flexibility_is_available = DEFAULT_FLEXIBILITY_IS_AVAILABLE else: - cls.flexibility_is_available = True + cls.load_flexibility_is_available = True type_attr_flex_load = [ dt_float, dt_bool, @@ -4620,7 +4616,7 @@ class res(GridObjects): cls.process_detachment() - cls.process_flexibility() # new in 1.12.x + cls.process_flexibility() # new in 1.12.2 if "assistant_warning_type" in dict_: cls.assistant_warning_type = dict_["assistant_warning_type"] @@ -4771,6 +4767,7 @@ def _build_cls_from_import(name_cls, path_env): except (ModuleNotFoundError, ImportError) as exc_: # normal behaviour i don't do anything there # TODO explain why + # TODO log exc_ somewhere pass my_class.process_grid2op_compat() my_class.process_detachment() diff --git a/grid2op/tests/test_flexibility.py b/grid2op/tests/test_flexibility.py index 968c3dcd..37b8ebab 100644 --- a/grid2op/tests/test_flexibility.py +++ b/grid2op/tests/test_flexibility.py @@ -7,7 +7,6 @@ # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. import warnings -import os import unittest import numpy as np @@ -26,35 +25,35 @@ def setUp(self) -> None: test=True, _add_to_name=type(self).__name__ ) - self.env.set_id(0) - _ = self.env.reset() + assert type(self.env).load_flexibility_is_available + + _ = self.env.reset(seed=0, options={"time serie id": 0}) self.ref_obs, *_ = self.env.step(self.env.action_space()) + _ = self.env.reset(seed=0, options={"time serie id": 0}) - self.env.set_id(0) - _ = self.env.reset() + with warnings.catch_warnings(): + warnings.filterwarnings("error") - self.flex_max_ramp_up = self.env.action_space( - {"flexibility": [(el, self.env.load_max_ramp_up[el]) for el in np.nonzero(self.env.load_flexible)[0]]} - ) - self.flex_max_ramp_down = self.env.action_space( - {"flexibility": [(el, -self.env.load_max_ramp_down[el]) for el in np.nonzero(self.env.load_flexible)[0]]} - ) - self.flex_all_zero = self.env.action_space( - {"flexibility": [(el, 0.0) for el in np.nonzero(self.env.load_flexible)[0]]} - ) - self.flex_small_up = self.env.action_space( - {"flexibility": [(el, 0.01) for el in np.nonzero(self.env.load_flexible)[0]]} - ) - self.flex_small_down = self.env.action_space( - {"flexibility": [(el, 0.01) for el in np.nonzero(self.env.load_flexible)[0]]} - ) + self.flex_max_ramp_up = self.env.action_space( + {"load_flexibility": [(el, self.env.load_max_ramp_up[el]) for el in np.nonzero(self.env.load_flexible)[0]]} + ) + self.flex_max_ramp_down = self.env.action_space( + {"load_flexibility": [(el, -self.env.load_max_ramp_down[el]) for el in np.nonzero(self.env.load_flexible)[0]]} + ) + self.flex_all_zero = self.env.action_space( + {"load_flexibility": [(el, 0.0) for el in np.nonzero(self.env.load_flexible)[0]]} + ) + self.flex_small_up = self.env.action_space( + {"load_flexibility": [(el, 0.01) for el in np.nonzero(self.env.load_flexible)[0]]} + ) + self.flex_small_down = self.env.action_space( + {"load_flexibility": [(el, 0.01) for el in np.nonzero(self.env.load_flexible)[0]]} + ) def test_create_flex_action(self): - try: - flex_act = self.env.action_space({"flexibility":[(el, 0.01) for el in np.nonzero(self.env.load_flexible)[0]]}) - assert True - except: - assert False + with warnings.catch_warnings(): + warnings.filterwarnings("error") + _ = self.env.action_space({"load_flexibility":[(el, 0.01) for el in np.nonzero(self.env.load_flexible)[0]]}) def test_zero_flex(self): flex_obs, *_ = self.env.step(self.flex_all_zero) @@ -68,7 +67,7 @@ def test_flex_small_up(self): flex_mask = self.env.load_flexible # Change in load relative to DoNothing scenario (i.e. normal Chronics) change_in_load = flex_obs.load_p[flex_mask] - self.ref_obs.load_p[flex_mask] - assert np.isclose(change_in_load, self.flex_small_up.flexibility[flex_mask], atol=0.001).all() + assert np.isclose(change_in_load, self.flex_small_up.load_flexibility[flex_mask], atol=0.001).all() def test_flex_small_down(self): flex_obs, *_ = self.env.step(self.flex_small_down) @@ -76,14 +75,14 @@ def test_flex_small_down(self): # Change in load relative to DoNothing scenario (i.e. normal Chronics) change_in_load = flex_obs.load_p[flex_mask] - self.ref_obs.load_p[flex_mask] - assert np.isclose(change_in_load, self.flex_small_down.flexibility[flex_mask], atol=0.001).all() + assert np.isclose(change_in_load, self.flex_small_down.load_flexibility[flex_mask], atol=0.001).all() def test_flex_max_ramp_up(self): flex_obs, *_ = self.env.step(self.flex_max_ramp_up) flex_mask = self.env.load_flexible # Load meets max ramp up, or the max size of the load ref_load = self.ref_obs.load_p[flex_mask] - expected_load = ref_load + self.flex_max_ramp_up.flexibility[flex_mask] + expected_load = ref_load + self.flex_max_ramp_up.load_flexibility[flex_mask] maximum_feasible_load = np.minimum(self.env.load_size[flex_mask], expected_load) assert np.isclose(flex_obs.load_p[flex_mask], maximum_feasible_load, atol=0.001).all() @@ -92,13 +91,13 @@ def test_flex_max_ramp_down(self): flex_mask = self.env.load_flexible # Load meets max ramp down, or the minimum load (of 0) ref_load = self.ref_obs.load_p[flex_mask] - expected_load = ref_load + self.flex_max_ramp_down.flexibility[flex_mask] + expected_load = ref_load + self.flex_max_ramp_down.load_flexibility[flex_mask] minimum_feasible_load = np.maximum(np.zeros(flex_mask.sum()), expected_load) assert np.isclose(flex_obs.load_p[flex_mask], minimum_feasible_load, atol=0.001).all() def test_flex_in_obs(self): flex_obs, *_ = self.env.step(self.flex_max_ramp_up) - assert np.isclose(flex_obs.target_flex, self.flex_max_ramp_up.flexibility).all() + assert np.isclose(flex_obs.target_flex, self.flex_max_ramp_up.load_flexibility).all() if __name__ == "__main__": unittest.main() From 5e1426b4b318f16df65bf7472e0c5ef5e197dd7d Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Thu, 9 Oct 2025 14:22:41 +0200 Subject: [PATCH 33/38] Refact: 'load_flexibility' everywhere, Debug: Include 'rte_case5_flexibility' in data_test and data, Test: Add unit test for ambiguous flexibility action Signed-off-by: Xavier Weiss --- getting_started/13_DemandResponse.ipynb | 2 +- grid2op/Action/baseAction.py | 12 +- grid2op/Backend/backend.py | 4 +- grid2op/Converter/BackendConverter.py | 2 +- grid2op/Environment/baseEnv.py | 2 +- grid2op/Environment/environment.py | 2 +- grid2op/Observation/baseObservation.py | 12 +- .../chronics/0/load_p.csv.bz2 | Bin 87 -> 0 bytes .../chronics/0/load_p_forecasted.csv.bz2 | Bin 98 -> 0 bytes .../chronics/0/prod_p.csv.bz2 | Bin 83 -> 0 bytes .../chronics/0/prod_p_forecasted.csv.bz2 | Bin 83 -> 0 bytes .../chronics/0/prod_v_forecasted.csv.bz2 | Bin 83 -> 0 bytes grid2op/data/rte_case5_flexibility/config.py | 19 - .../difficulty_levels.json | 62 - grid2op/data/rte_case5_flexibility/grid.json | 1339 ----------------- .../rte_case5_flexibility/grid_layout.json | 22 - .../data/rte_case5_flexibility/params.json | 1 - .../rte_case5_flexibility/prods_charac.csv | 3 - .../__pycache__/config.cpython-310.pyc | Bin 0 -> 719 bytes .../__pycache__/config.cpython-312.pyc | Bin 0 -> 748 bytes grid2op/tests/test_flexibility.py | 24 +- 21 files changed, 41 insertions(+), 1465 deletions(-) delete mode 100644 grid2op/data/rte_case5_flexibility/chronics/0/load_p.csv.bz2 delete mode 100644 grid2op/data/rte_case5_flexibility/chronics/0/load_p_forecasted.csv.bz2 delete mode 100644 grid2op/data/rte_case5_flexibility/chronics/0/prod_p.csv.bz2 delete mode 100644 grid2op/data/rte_case5_flexibility/chronics/0/prod_p_forecasted.csv.bz2 delete mode 100644 grid2op/data/rte_case5_flexibility/chronics/0/prod_v_forecasted.csv.bz2 delete mode 100644 grid2op/data/rte_case5_flexibility/config.py delete mode 100644 grid2op/data/rte_case5_flexibility/difficulty_levels.json delete mode 100644 grid2op/data/rte_case5_flexibility/grid.json delete mode 100644 grid2op/data/rte_case5_flexibility/grid_layout.json delete mode 100644 grid2op/data/rte_case5_flexibility/params.json delete mode 100644 grid2op/data/rte_case5_flexibility/prods_charac.csv create mode 100644 grid2op/data_test/rte_case5_flexibility/__pycache__/config.cpython-310.pyc create mode 100644 grid2op/data_test/rte_case5_flexibility/__pycache__/config.cpython-312.pyc diff --git a/getting_started/13_DemandResponse.ipynb b/getting_started/13_DemandResponse.ipynb index ac70be0c..72c04c5a 100644 --- a/getting_started/13_DemandResponse.ipynb +++ b/getting_started/13_DemandResponse.ipynb @@ -99,7 +99,7 @@ "env = grid2op.make(env_name, test=True, backend=backend)\n", "\n", "print(f\"Is this environment suitable for redispatching: {env.redispatching_unit_commitment_availble}\")\n", - "print(f\"Is this environment suitable for flexibility: {env.flexibility_is_available}\")" + "print(f\"Is this environment suitable for flexibility: {env.load_flexibility_is_available}\")" ] }, { diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index ec1eb85a..baeea36d 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -987,7 +987,7 @@ def _aux_copy(self, other: Self) -> None: if cls.detachment_is_allowed: attr_vect += ["_private_detach_load", "_private_detach_gen", "_private_detach_storage"] - if cls.flexibility_is_available: + if cls.load_flexibility_is_available: attr_vect += ["_private_load_flexibility"] for attr_nm in attr_simple: @@ -2485,7 +2485,7 @@ def _aux_iadd_modif_flags(self, other: Self): self._modif_inj = self._modif_inj or other._modif_inj self._modif_shunt = self._modif_shunt or other._modif_shunt self._modif_redispatch = self._modif_redispatch or other._modif_redispatch - self._modif_flexibility = self._modif_flexibility or other._modif_flexibility # new in 1.12.2 + self._modif_load_flexibility = self._modif_load_flexibility or other._modif_load_flexibility # new in 1.12.2 self._modif_storage = self._modif_storage or other._modif_storage self._modif_curtailment = self._modif_curtailment or other._modif_curtailment self._modif_alarm = self._modif_alarm or other._modif_alarm @@ -4034,7 +4034,7 @@ def _is_flexibility_ambiguous(self): "check if flexibility actions are ambiguous" cls = type(self) if cls.load_flexibility_is_available: - if self._modif_flexibility: # new in 1.12.x + if self._modif_load_flexibility: # new in 1.12.x if "load_flexibility" not in cls.authorized_keys: raise AmbiguousAction( 'Action of type "load_flexibility" are not supported by this action type' @@ -4388,7 +4388,7 @@ def __str__(self) -> str: # flexibility, new in 1.12.x if type(self).load_flexibility_is_available: - if self._modif_flexibility: + if self._modif_load_flexibility: res.append( "\t - Modify the loads with flexibility in the following way:" ) @@ -7992,7 +7992,7 @@ def _aux_decompose_as_unary_actions_flexibility(self, res): if group_flexibility: tmp = cls() - tmp._modif_flexibility = True + tmp._modif_load_flexibility = True tmp._load_flexibility[:] = self._private_load_flexibility res["load_flexibility"] = [tmp] else: @@ -8000,7 +8000,7 @@ def _aux_decompose_as_unary_actions_flexibility(self, res["load_flexibility"] = [] for load_id in load_changed: tmp = cls() - tmp._modif_flexibility = True + tmp._modif_load_flexibility = True tmp._load_flexibility[load_id] = self._private_load_flexibility[load_id] res["load_flexibility"].append(tmp) diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index e2d2907c..15020221 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -1970,7 +1970,7 @@ def load_flexibility_data(self, to change it. """ - self.flexibility_is_available = False + self.load_flexibility_is_available = False self.load_size = np.full(self.n_load, fill_value=0.0, dtype=dt_float) self.load_flexible = np.full(self.n_load, fill_value=False, dtype=dt_bool) @@ -2037,7 +2037,7 @@ def load_flexibility_data(self, if np.isfinite(tmp): self.load_max_ramp_down[i] = tmp self.load_cost_per_MW[i] = dt_float(tmp_load["marginal_cost"]) - self.flexibility_is_available = True + self.load_flexibility_is_available = True def load_storage_data(self, path : Union[os.PathLike, str], diff --git a/grid2op/Converter/BackendConverter.py b/grid2op/Converter/BackendConverter.py index d78cb096..ba5279d7 100644 --- a/grid2op/Converter/BackendConverter.py +++ b/grid2op/Converter/BackendConverter.py @@ -514,7 +514,7 @@ def assert_grid_correct(self, _local_dir_cls=None) -> None: self.path_flexibility, name=self.name_flexibility ) except BackendError as exc_: - self.flexibility_is_available = False + self.load_flexibility_is_available = False warnings.warn(f"Impossible to load flexibility data. This is not an error but you will not be able " f"to use all grid2op functionalities. " f"The error was: \"{exc_}\"") diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index ef45520e..4b3616de 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -706,7 +706,7 @@ def __init__( self._needs_active_bus = False # flexibility / demand response, new in 1.12.x - # if type(self).flexibility_is_available: + # if type(self).load_flexibility_is_available: self._forbid_flex_off:bool = (not self._parameters.ALLOW_FLEX_LOAD_SWITCH_OFF) self._target_flex: np.ndarray = None self._already_modified_load: np.ndarray = None diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index 03591d37..021aca5c 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -321,7 +321,7 @@ def _init_backend( try: self.backend.load_flexibility_data(self.get_path_env()) except BackendError as exc_: - self.backend.flexibility_is_available = False + self.backend.load_flexibility_is_available = False warnings.warn(f"Impossible to load flexibility data. This is not an error but you will not be able " f"to use all grid2op functionalities. " f"The error was: \"{exc_}\"") diff --git a/grid2op/Observation/baseObservation.py b/grid2op/Observation/baseObservation.py index ab0e5d26..3c3c3e66 100644 --- a/grid2op/Observation/baseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -757,7 +757,7 @@ def __init__(self, self.actual_dispatch = np.empty(shape=cls.n_gen, dtype=dt_float) # Flexibility / demand response, new in 1.12.x - # if cls.flexibility_is_available: + # if cls.load_flexibility_is_available: self.target_flex = np.empty(shape=cls.n_load, dtype=dt_float) self.actual_flex = np.empty(shape=cls.n_load, dtype=dt_float) @@ -1591,7 +1591,7 @@ def reset(self) -> None: self.storage_p_detached[:] = 0. # flexibility, new in 1.12.x - # if type(self).flexibility_is_available: + # if type(self).load_flexibility_is_available: self.target_flex[:] = 0.0 self.actual_flex[:] = 0.0 @@ -1747,7 +1747,7 @@ def set_game_over(self, self.storage_p_detached[:] = 0. # flexibility, new in 1.12.x - # if type(self).flexibility_is_available: + # if type(self).load_flexibility_is_available: self.target_flex[:] = 0.0 self.actual_flex[:] = 0.0 @@ -3008,7 +3008,7 @@ def _aux_add_loads(self, graph, cls, first_id): else: nodes_prop = [] - if type(self).flexibility_is_available: + if type(self).load_flexibility_is_available: nodes_prop.extend([ ("target_flex", self.target_flex), ("actual_flcex", self.actual_flex) @@ -4583,7 +4583,7 @@ def _update_obs_complete(self, env: "grid2op.Environment.BaseEnv", with_forecast self.actual_dispatch[:] = env._actual_dispatch # Flexibility, new in 1.12.x - # if type(self).flexibility_is_available: + # if type(self).load_flexibility_is_available: self.target_flex[:] = env._target_flex self.actual_flex[:] = env._actual_flex @@ -5379,7 +5379,7 @@ def process_detachment(cls): # @classmethod # def process_flexibility(cls): - # if not cls.flexibility_is_available: + # if not cls.load_flexibility_is_available: # # this is really important, otherwise things from grid2op base types will be affected # cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) # # remove the detachment from the list to vector diff --git a/grid2op/data/rte_case5_flexibility/chronics/0/load_p.csv.bz2 b/grid2op/data/rte_case5_flexibility/chronics/0/load_p.csv.bz2 deleted file mode 100644 index 0b4140b9fef499f7f9f068a99db66cbc14bc33fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 87 zcmV-d0I2^$T4*^jL0KkKSv6-l`T#RQ+W-I(00DeR005)}pa60kQZx-T&`nhafDENT t#;TxjRD#5+2UDq34b?!=R17MCg(Ltr+GDm(yY%CIzaj8qGbEPXWBnn6ALHUonY17oEI14C1z0z;7`N9HTJ1+!K%WEg6(-PLMr m4X_Ml*1hU*EY#s`2;b5Gqg6uw%mIgP%ZnGeS_Di083F)9pd2m$ diff --git a/grid2op/data/rte_case5_flexibility/chronics/0/prod_p_forecasted.csv.bz2 b/grid2op/data/rte_case5_flexibility/chronics/0/prod_p_forecasted.csv.bz2 deleted file mode 100644 index 81e867b529a3ed5ff5192205533ec0dd75fe5843..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 83 zcmV-Z0IdH)T4*^jL0KkKSu6`C5&$P_+W-I(00DW3004r4AOLb15YyBif@-7zpv0&f pa;gU^plGTFTdIMKs)5?59ZH~OR3x5;p^hdjUC9*TLO`%Am`FKI9jpKV diff --git a/grid2op/data/rte_case5_flexibility/chronics/0/prod_v_forecasted.csv.bz2 b/grid2op/data/rte_case5_flexibility/chronics/0/prod_v_forecasted.csv.bz2 deleted file mode 100644 index c685c39c018b1ba892967015af0a2923fa8abcb0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 83 zcmV-Z0IdH)T4*^jL0KkKSq8~9X#g)uTL1tM00D3a004r4AOLVc3uTcBmuHbT0jyk9CrW! diff --git a/grid2op/data/rte_case5_flexibility/config.py b/grid2op/data/rte_case5_flexibility/config.py deleted file mode 100644 index cdc2758f..00000000 --- a/grid2op/data/rte_case5_flexibility/config.py +++ /dev/null @@ -1,19 +0,0 @@ -from grid2op.Action import CompleteAction -from grid2op.Reward import L2RPNReward -from grid2op.Rules import DefaultRules -from grid2op.Chronics import Multifolder -from grid2op.Chronics import GridStateFromFileWithForecasts -from grid2op.Backend import PandaPowerBackend - -config = { - "backend": PandaPowerBackend, - "action_class": CompleteAction, - "observation_class": None, - "reward_class": L2RPNReward, - "gamerules_class": DefaultRules, - "chronics_class": Multifolder, - "grid_value_class": GridStateFromFileWithForecasts, - "volagecontroler_class": None, - "thermal_limits": None, - "names_chronics_to_grid": None, -} diff --git a/grid2op/data/rte_case5_flexibility/difficulty_levels.json b/grid2op/data/rte_case5_flexibility/difficulty_levels.json deleted file mode 100644 index 581f9117..00000000 --- a/grid2op/data/rte_case5_flexibility/difficulty_levels.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "0": { - "NO_OVERFLOW_DISCONNECTION": true, - "NB_TIMESTEP_OVERFLOW_ALLOWED": 9999, - "NB_TIMESTEP_COOLDOWN_SUB": 0, - "NB_TIMESTEP_COOLDOWN_LINE": 0, - "HARD_OVERFLOW_THRESHOLD": 9999, - "NB_TIMESTEP_RECONNECTION": 0, - "IGNORE_MIN_UP_DOWN_TIME": true, - "ALLOW_FLEX_LOAD_SWITCH_OFF": true, - "ALLOW_DISPATCH_GEN_SWITCH_OFF": true, - "ENV_DC": false, - "FORECAST_DC": false, - "MAX_SUB_CHANGED": 1, - "MAX_LINE_STATUS_CHANGED": 1 - }, - "1": { - "NO_OVERFLOW_DISCONNECTION": false, - "NB_TIMESTEP_OVERFLOW_ALLOWED": 6, - "NB_TIMESTEP_COOLDOWN_SUB": 0, - "NB_TIMESTEP_COOLDOWN_LINE": 0, - "HARD_OVERFLOW_THRESHOLD": 300, - "NB_TIMESTEP_RECONNECTION": 1, - "IGNORE_MIN_UP_DOWN_TIME": true, - "ALLOW_FLEX_LOAD_SWITCH_OFF": true, - "ALLOW_DISPATCH_GEN_SWITCH_OFF": true, - "ENV_DC": false, - "FORECAST_DC": false, - "MAX_SUB_CHANGED": 1, - "MAX_LINE_STATUS_CHANGED": 1 - }, - "2": { - "NO_OVERFLOW_DISCONNECTION": false, - "NB_TIMESTEP_OVERFLOW_ALLOWED": 3, - "NB_TIMESTEP_COOLDOWN_SUB": 1, - "NB_TIMESTEP_COOLDOWN_LINE": 1, - "HARD_OVERFLOW_THRESHOLD": 250, - "NB_TIMESTEP_RECONNECTION": 6, - "IGNORE_MIN_UP_DOWN_TIME": true, - "ALLOW_FLEX_LOAD_SWITCH_OFF": true, - "ALLOW_DISPATCH_GEN_SWITCH_OFF": true, - "ENV_DC": false, - "FORECAST_DC": false, - "MAX_SUB_CHANGED": 1, - "MAX_LINE_STATUS_CHANGED": 1 - }, - "competition": { - "NO_OVERFLOW_DISCONNECTION": false, - "NB_TIMESTEP_OVERFLOW_ALLOWED": 3, - "NB_TIMESTEP_COOLDOWN_SUB": 3, - "NB_TIMESTEP_COOLDOWN_LINE": 3, - "HARD_OVERFLOW_THRESHOLD": 200, - "NB_TIMESTEP_RECONNECTION": 12, - "IGNORE_MIN_UP_DOWN_TIME": true, - "ALLOW_FLEX_LOAD_SWITCH_OFF": true, - "ALLOW_DISPATCH_GEN_SWITCH_OFF": true, - "ENV_DC": false, - "FORECAST_DC": false, - "MAX_SUB_CHANGED": 1, - "MAX_LINE_STATUS_CHANGED": 1 - } -} diff --git a/grid2op/data/rte_case5_flexibility/grid.json b/grid2op/data/rte_case5_flexibility/grid.json deleted file mode 100644 index 2e9dd0c7..00000000 --- a/grid2op/data/rte_case5_flexibility/grid.json +++ /dev/null @@ -1,1339 +0,0 @@ -{ - "_module": "pandapower.auxiliary", - "_class": "pandapowerNet", - "_object": { - "bus": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"vn_kv\",\"type\",\"zone\",\"in_service\"],\"index\":[0,1,2,3,4],\"data\":[[\"substation_1\",100.0,\"b\",null,true],[\"substation_2\",100.0,\"b\",null,true],[\"substation_3\",100.0,\"b\",null,true],[\"substation_4\",100.0,\"b\",null,true],[\"substation_5\",100.0,\"b\",null,true]]}", - "dtype": { - "name": "object", - "vn_kv": "float64", - "type": "object", - "zone": "object", - "in_service": "bool" - }, - "orient": "split" - }, - "load": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"bus\",\"p_mw\",\"q_mvar\",\"const_z_percent\",\"const_i_percent\",\"sn_mva\",\"scaling\",\"in_service\",\"type\"],\"index\":[0,1,2],\"data\":[[\"load_0_0\",0,10.0,7.0,0.0,0.0,null,1.0,true,null],[\"load_3_1\",3,10.0,7.0,0.0,0.0,null,1.0,true,null],[\"load_4_2\",4,10.0,7.0,0.0,0.0,null,1.0,true,null]]}", - "dtype": { - "name": "object", - "bus": "uint32", - "p_mw": "float64", - "q_mvar": "float64", - "const_z_percent": "float64", - "const_i_percent": "float64", - "sn_mva": "float64", - "scaling": "float64", - "in_service": "bool", - "type": "object" - }, - "orient": "split" - }, - "sgen": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"bus\",\"p_mw\",\"q_mvar\",\"sn_mva\",\"scaling\",\"in_service\",\"type\",\"current_source\"],\"index\":[],\"data\":[]}", - "dtype": { - "name": "object", - "bus": "int64", - "p_mw": "float64", - "q_mvar": "float64", - "sn_mva": "float64", - "scaling": "float64", - "in_service": "bool", - "type": "object", - "current_source": "bool" - }, - "orient": "split" - }, - "storage": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"bus\",\"p_mw\",\"q_mvar\",\"sn_mva\",\"soc_percent\",\"min_e_mwh\",\"max_e_mwh\",\"scaling\",\"in_service\",\"type\"],\"index\":[],\"data\":[]}", - "dtype": { - "name": "object", - "bus": "int64", - "p_mw": "float64", - "q_mvar": "float64", - "sn_mva": "float64", - "soc_percent": "float64", - "min_e_mwh": "float64", - "max_e_mwh": "float64", - "scaling": "float64", - "in_service": "bool", - "type": "object" - }, - "orient": "split" - }, - "gen": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"bus\",\"p_mw\",\"vm_pu\",\"sn_mva\",\"min_q_mvar\",\"max_q_mvar\",\"scaling\",\"slack\",\"in_service\",\"type\"],\"index\":[0,1],\"data\":[[\"gen_0_0\",0,10.0,1.02,null,null,null,1.0,false,true,null],[\"gen_1_1\",1,20.0,1.02,null,null,null,1.0,true,true,null]]}", - "dtype": { - "name": "object", - "bus": "uint32", - "p_mw": "float64", - "vm_pu": "float64", - "sn_mva": "float64", - "min_q_mvar": "float64", - "max_q_mvar": "float64", - "scaling": "float64", - "slack": "bool", - "in_service": "bool", - "type": "object" - }, - "orient": "split" - }, - "switch": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"bus\",\"element\",\"et\",\"type\",\"closed\",\"name\",\"z_ohm\"],\"index\":[],\"data\":[]}", - "dtype": { - "bus": "int64", - "element": "int64", - "et": "object", - "type": "object", - "closed": "bool", - "name": "object", - "z_ohm": "float64" - }, - "orient": "split" - }, - "shunt": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"bus\",\"name\",\"q_mvar\",\"p_mw\",\"vn_kv\",\"step\",\"max_step\",\"in_service\"],\"index\":[],\"data\":[]}", - "dtype": { - "bus": "uint32", - "name": "object", - "q_mvar": "float64", - "p_mw": "float64", - "vn_kv": "float64", - "step": "uint32", - "max_step": "uint32", - "in_service": "bool" - }, - "orient": "split" - }, - "ext_grid": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"bus\",\"vm_pu\",\"va_degree\",\"in_service\"],\"index\":[],\"data\":[]}", - "dtype": { - "name": "object", - "bus": "uint32", - "vm_pu": "float64", - "va_degree": "float64", - "in_service": "bool" - }, - "orient": "split" - }, - "line": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"std_type\",\"from_bus\",\"to_bus\",\"length_km\",\"r_ohm_per_km\",\"x_ohm_per_km\",\"c_nf_per_km\",\"g_us_per_km\",\"max_i_ka\",\"df\",\"parallel\",\"type\",\"in_service\"],\"index\":[0,1,2,3,4,5,6,7],\"data\":[[null,\"NAYY 4x50 SE\",0,1,4.0,0.642,0.083,210.0,0.0,0.6,1.0,1,\"cs\",true],[\"0_2_2\",\"NAYY 4x50 SE\",0,2,4.47,0.642,0.083,210.0,0.0,0.22,1.0,1,\"cs\",true],[\"0_3_3\",\"NAYY 4x50 SE\",0,3,5.65,0.642,0.083,210.0,0.0,0.16,1.0,1,\"cs\",true],[\"0_4_4\",\"NAYY 4x50 SE\",0,4,4.0,0.642,0.083,210.0,0.0,0.16,1.0,1,\"cs\",true],[\"1_2_5\",\"NAYY 4x50 SE\",1,2,2.0,0.642,0.083,210.0,0.0,0.6,1.0,1,\"cs\",true],[\"2_3_6\",\"NAYY 4x50 SE\",2,3,2.0,0.642,0.083,210.0,0.0,0.3,1.0,1,\"cs\",true],[\"2_3_7\",\"NAYY 4x50 SE\",2,3,2.0,0.642,0.083,210.0,0.0,0.3,1.0,1,\"cs\",true],[\"3_4_8\",\"NAYY 4x50 SE\",3,4,4.0,0.642,0.083,210.0,0.0,0.16,1.0,1,\"cs\",true]]}", - "dtype": { - "name": "object", - "std_type": "object", - "from_bus": "uint32", - "to_bus": "uint32", - "length_km": "float64", - "r_ohm_per_km": "float64", - "x_ohm_per_km": "float64", - "c_nf_per_km": "float64", - "g_us_per_km": "float64", - "max_i_ka": "float64", - "df": "float64", - "parallel": "uint32", - "type": "object", - "in_service": "bool" - }, - "orient": "split" - }, - "trafo": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"std_type\",\"hv_bus\",\"lv_bus\",\"sn_mva\",\"vn_hv_kv\",\"vn_lv_kv\",\"vk_percent\",\"vkr_percent\",\"pfe_kw\",\"i0_percent\",\"shift_degree\",\"tap_side\",\"tap_neutral\",\"tap_min\",\"tap_max\",\"tap_step_percent\",\"tap_step_degree\",\"tap_pos\",\"tap_phase_shifter\",\"parallel\",\"df\",\"in_service\"],\"index\":[],\"data\":[]}", - "dtype": { - "name": "object", - "std_type": "object", - "hv_bus": "uint32", - "lv_bus": "uint32", - "sn_mva": "float64", - "vn_hv_kv": "float64", - "vn_lv_kv": "float64", - "vk_percent": "float64", - "vkr_percent": "float64", - "pfe_kw": "float64", - "i0_percent": "float64", - "shift_degree": "float64", - "tap_side": "object", - "tap_neutral": "int32", - "tap_min": "int32", - "tap_max": "int32", - "tap_step_percent": "float64", - "tap_step_degree": "float64", - "tap_pos": "int32", - "tap_phase_shifter": "bool", - "parallel": "uint32", - "df": "float64", - "in_service": "bool" - }, - "orient": "split" - }, - "trafo3w": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"std_type\",\"hv_bus\",\"mv_bus\",\"lv_bus\",\"sn_hv_mva\",\"sn_mv_mva\",\"sn_lv_mva\",\"vn_hv_kv\",\"vn_mv_kv\",\"vn_lv_kv\",\"vk_hv_percent\",\"vk_mv_percent\",\"vk_lv_percent\",\"vkr_hv_percent\",\"vkr_mv_percent\",\"vkr_lv_percent\",\"pfe_kw\",\"i0_percent\",\"shift_mv_degree\",\"shift_lv_degree\",\"tap_side\",\"tap_neutral\",\"tap_min\",\"tap_max\",\"tap_step_percent\",\"tap_step_degree\",\"tap_pos\",\"tap_at_star_point\",\"in_service\"],\"index\":[],\"data\":[]}", - "dtype": { - "name": "object", - "std_type": "object", - "hv_bus": "uint32", - "mv_bus": "uint32", - "lv_bus": "uint32", - "sn_hv_mva": "float64", - "sn_mv_mva": "float64", - "sn_lv_mva": "float64", - "vn_hv_kv": "float64", - "vn_mv_kv": "float64", - "vn_lv_kv": "float64", - "vk_hv_percent": "float64", - "vk_mv_percent": "float64", - "vk_lv_percent": "float64", - "vkr_hv_percent": "float64", - "vkr_mv_percent": "float64", - "vkr_lv_percent": "float64", - "pfe_kw": "float64", - "i0_percent": "float64", - "shift_mv_degree": "float64", - "shift_lv_degree": "float64", - "tap_side": "object", - "tap_neutral": "int32", - "tap_min": "int32", - "tap_max": "int32", - "tap_step_percent": "float64", - "tap_step_degree": "float64", - "tap_pos": "int32", - "tap_at_star_point": "bool", - "in_service": "bool" - }, - "orient": "split" - }, - "impedance": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"from_bus\",\"to_bus\",\"rft_pu\",\"xft_pu\",\"rtf_pu\",\"xtf_pu\",\"sn_mva\",\"in_service\"],\"index\":[],\"data\":[]}", - "dtype": { - "name": "object", - "from_bus": "uint32", - "to_bus": "uint32", - "rft_pu": "float64", - "xft_pu": "float64", - "rtf_pu": "float64", - "xtf_pu": "float64", - "sn_mva": "float64", - "in_service": "bool" - }, - "orient": "split" - }, - "dcline": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"from_bus\",\"to_bus\",\"p_mw\",\"loss_percent\",\"loss_mw\",\"vm_from_pu\",\"vm_to_pu\",\"max_p_mw\",\"min_q_from_mvar\",\"min_q_to_mvar\",\"max_q_from_mvar\",\"max_q_to_mvar\",\"in_service\"],\"index\":[],\"data\":[]}", - "dtype": { - "name": "object", - "from_bus": "uint32", - "to_bus": "uint32", - "p_mw": "float64", - "loss_percent": "float64", - "loss_mw": "float64", - "vm_from_pu": "float64", - "vm_to_pu": "float64", - "max_p_mw": "float64", - "min_q_from_mvar": "float64", - "min_q_to_mvar": "float64", - "max_q_from_mvar": "float64", - "max_q_to_mvar": "float64", - "in_service": "bool" - }, - "orient": "split" - }, - "ward": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"bus\",\"ps_mw\",\"qs_mvar\",\"qz_mvar\",\"pz_mw\",\"in_service\"],\"index\":[],\"data\":[]}", - "dtype": { - "name": "object", - "bus": "uint32", - "ps_mw": "float64", - "qs_mvar": "float64", - "qz_mvar": "float64", - "pz_mw": "float64", - "in_service": "bool" - }, - "orient": "split" - }, - "xward": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"bus\",\"ps_mw\",\"qs_mvar\",\"qz_mvar\",\"pz_mw\",\"r_ohm\",\"x_ohm\",\"vm_pu\",\"in_service\"],\"index\":[],\"data\":[]}", - "dtype": { - "name": "object", - "bus": "uint32", - "ps_mw": "float64", - "qs_mvar": "float64", - "qz_mvar": "float64", - "pz_mw": "float64", - "r_ohm": "float64", - "x_ohm": "float64", - "vm_pu": "float64", - "in_service": "bool" - }, - "orient": "split" - }, - "measurement": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"name\",\"measurement_type\",\"element_type\",\"element\",\"value\",\"std_dev\",\"side\"],\"index\":[],\"data\":[]}", - "dtype": { - "name": "object", - "measurement_type": "object", - "element_type": "object", - "element": "uint32", - "value": "float64", - "std_dev": "float64", - "side": "object" - }, - "orient": "split" - }, - "pwl_cost": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"power_type\",\"element\",\"et\",\"points\"],\"index\":[],\"data\":[]}", - "dtype": { - "power_type": "object", - "element": "object", - "et": "object", - "points": "object" - }, - "orient": "split" - }, - "poly_cost": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"element\",\"et\",\"cp0_eur\",\"cp1_eur_per_mw\",\"cp2_eur_per_mw2\",\"cq0_eur\",\"cq1_eur_per_mvar\",\"cq2_eur_per_mvar2\"],\"index\":[],\"data\":[]}", - "dtype": { - "element": "object", - "et": "object", - "cp0_eur": "float64", - "cp1_eur_per_mw": "float64", - "cp2_eur_per_mw2": "float64", - "cq0_eur": "float64", - "cq1_eur_per_mvar": "float64", - "cq2_eur_per_mvar2": "float64" - }, - "orient": "split" - }, - "line_geodata": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"coords\"],\"index\":[0,1,2,3,4,5,6,7],\"data\":[[[[0,0],[0,4]]],[[[0,0],[2,4]]],[[[0,0],[4,4]]],[[[0,0],[4,0]]],[[[0,4],[2,4]]],[[[2,4],[3,4.2],[4,4]]],[[[2,4],[3,3.8],[4,4]]],[[[4,4],[4,0]]]]}", - "dtype": { - "coords": "object" - }, - "orient": "split" - }, - "bus_geodata": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"x\",\"y\",\"coords\"],\"index\":[0,1,2,3,4],\"data\":[[0.0,0.0,null],[0.0,4.0,null],[2.0,4.0,null],[4.0,4.0,null],[4.0,0.0,null]]}", - "dtype": { - "x": "float64", - "y": "float64", - "coords": "object" - }, - "orient": "split" - }, - "version": "2.1.0", - "converged": true, - "name": "5bus", - "f_hz": 50.0, - "sn_mva": 1, - "std_types": { - "line": { - "NAYY 4x50 SE": { - "c_nf_per_km": 210, - "r_ohm_per_km": 0.642, - "x_ohm_per_km": 0.083, - "max_i_ka": 0.142, - "type": "cs", - "q_mm2": 50, - "alpha": 0.00403 - }, - "NAYY 4x120 SE": { - "c_nf_per_km": 264, - "r_ohm_per_km": 0.225, - "x_ohm_per_km": 0.08, - "max_i_ka": 0.242, - "type": "cs", - "q_mm2": 120, - "alpha": 0.00403 - }, - "NAYY 4x150 SE": { - "c_nf_per_km": 261, - "r_ohm_per_km": 0.208, - "x_ohm_per_km": 0.08, - "max_i_ka": 0.27, - "type": "cs", - "q_mm2": 150, - "alpha": 0.00403 - }, - "NA2XS2Y 1x95 RM/25 12/20 kV": { - "c_nf_per_km": 216, - "r_ohm_per_km": 0.313, - "x_ohm_per_km": 0.132, - "max_i_ka": 0.252, - "type": "cs", - "q_mm2": 95, - "alpha": 0.00403 - }, - "NA2XS2Y 1x185 RM/25 12/20 kV": { - "c_nf_per_km": 273, - "r_ohm_per_km": 0.161, - "x_ohm_per_km": 0.117, - "max_i_ka": 0.362, - "type": "cs", - "q_mm2": 185, - "alpha": 0.00403 - }, - "NA2XS2Y 1x240 RM/25 12/20 kV": { - "c_nf_per_km": 304, - "r_ohm_per_km": 0.122, - "x_ohm_per_km": 0.112, - "max_i_ka": 0.421, - "type": "cs", - "q_mm2": 240, - "alpha": 0.00403 - }, - "NA2XS2Y 1x95 RM/25 6/10 kV": { - "c_nf_per_km": 315, - "r_ohm_per_km": 0.313, - "x_ohm_per_km": 0.123, - "max_i_ka": 0.249, - "type": "cs", - "q_mm2": 95, - "alpha": 0.00403 - }, - "NA2XS2Y 1x185 RM/25 6/10 kV": { - "c_nf_per_km": 406, - "r_ohm_per_km": 0.161, - "x_ohm_per_km": 0.11, - "max_i_ka": 0.358, - "type": "cs", - "q_mm2": 185, - "alpha": 0.00403 - }, - "NA2XS2Y 1x240 RM/25 6/10 kV": { - "c_nf_per_km": 456, - "r_ohm_per_km": 0.122, - "x_ohm_per_km": 0.105, - "max_i_ka": 0.416, - "type": "cs", - "q_mm2": 240, - "alpha": 0.00403 - }, - "NA2XS2Y 1x150 RM/25 12/20 kV": { - "c_nf_per_km": 250, - "r_ohm_per_km": 0.206, - "x_ohm_per_km": 0.116, - "max_i_ka": 0.319, - "type": "cs", - "q_mm2": 150, - "alpha": 0.00403 - }, - "NA2XS2Y 1x120 RM/25 12/20 kV": { - "c_nf_per_km": 230, - "r_ohm_per_km": 0.253, - "x_ohm_per_km": 0.119, - "max_i_ka": 0.283, - "type": "cs", - "q_mm2": 120, - "alpha": 0.00403 - }, - "NA2XS2Y 1x70 RM/25 12/20 kV": { - "c_nf_per_km": 190, - "r_ohm_per_km": 0.443, - "x_ohm_per_km": 0.132, - "max_i_ka": 0.22, - "type": "cs", - "q_mm2": 70, - "alpha": 0.00403 - }, - "NA2XS2Y 1x150 RM/25 6/10 kV": { - "c_nf_per_km": 360, - "r_ohm_per_km": 0.206, - "x_ohm_per_km": 0.11, - "max_i_ka": 0.315, - "type": "cs", - "q_mm2": 150, - "alpha": 0.00403 - }, - "NA2XS2Y 1x120 RM/25 6/10 kV": { - "c_nf_per_km": 340, - "r_ohm_per_km": 0.253, - "x_ohm_per_km": 0.113, - "max_i_ka": 0.28, - "type": "cs", - "q_mm2": 120, - "alpha": 0.00403 - }, - "NA2XS2Y 1x70 RM/25 6/10 kV": { - "c_nf_per_km": 280, - "r_ohm_per_km": 0.443, - "x_ohm_per_km": 0.123, - "max_i_ka": 0.217, - "type": "cs", - "q_mm2": 70, - "alpha": 0.00403 - }, - "N2XS(FL)2Y 1x120 RM/35 64/110 kV": { - "c_nf_per_km": 112, - "r_ohm_per_km": 0.153, - "x_ohm_per_km": 0.166, - "max_i_ka": 0.366, - "type": "cs", - "q_mm2": 120, - "alpha": 0.00393 - }, - "N2XS(FL)2Y 1x185 RM/35 64/110 kV": { - "c_nf_per_km": 125, - "r_ohm_per_km": 0.099, - "x_ohm_per_km": 0.156, - "max_i_ka": 0.457, - "type": "cs", - "q_mm2": 185, - "alpha": 0.00393 - }, - "N2XS(FL)2Y 1x240 RM/35 64/110 kV": { - "c_nf_per_km": 135, - "r_ohm_per_km": 0.075, - "x_ohm_per_km": 0.149, - "max_i_ka": 0.526, - "type": "cs", - "q_mm2": 240, - "alpha": 0.00393 - }, - "N2XS(FL)2Y 1x300 RM/35 64/110 kV": { - "c_nf_per_km": 144, - "r_ohm_per_km": 0.06, - "x_ohm_per_km": 0.144, - "max_i_ka": 0.588, - "type": "cs", - "q_mm2": 300, - "alpha": 0.00393 - }, - "15-AL1/3-ST1A 0.4": { - "c_nf_per_km": 11, - "r_ohm_per_km": 1.8769, - "x_ohm_per_km": 0.35, - "max_i_ka": 0.105, - "type": "ol", - "q_mm2": 16, - "alpha": 0.00403 - }, - "24-AL1/4-ST1A 0.4": { - "c_nf_per_km": 11.25, - "r_ohm_per_km": 1.2012, - "x_ohm_per_km": 0.335, - "max_i_ka": 0.14, - "type": "ol", - "q_mm2": 24, - "alpha": 0.00403 - }, - "48-AL1/8-ST1A 0.4": { - "c_nf_per_km": 12.2, - "r_ohm_per_km": 0.5939, - "x_ohm_per_km": 0.3, - "max_i_ka": 0.21, - "type": "ol", - "q_mm2": 48, - "alpha": 0.00403 - }, - "94-AL1/15-ST1A 0.4": { - "c_nf_per_km": 13.2, - "r_ohm_per_km": 0.306, - "x_ohm_per_km": 0.29, - "max_i_ka": 0.35, - "type": "ol", - "q_mm2": 94, - "alpha": 0.00403 - }, - "34-AL1/6-ST1A 10.0": { - "c_nf_per_km": 9.7, - "r_ohm_per_km": 0.8342, - "x_ohm_per_km": 0.36, - "max_i_ka": 0.17, - "type": "ol", - "q_mm2": 34, - "alpha": 0.00403 - }, - "48-AL1/8-ST1A 10.0": { - "c_nf_per_km": 10.1, - "r_ohm_per_km": 0.5939, - "x_ohm_per_km": 0.35, - "max_i_ka": 0.21, - "type": "ol", - "q_mm2": 48, - "alpha": 0.00403 - }, - "70-AL1/11-ST1A 10.0": { - "c_nf_per_km": 10.4, - "r_ohm_per_km": 0.4132, - "x_ohm_per_km": 0.339, - "max_i_ka": 0.29, - "type": "ol", - "q_mm2": 70, - "alpha": 0.00403 - }, - "94-AL1/15-ST1A 10.0": { - "c_nf_per_km": 10.75, - "r_ohm_per_km": 0.306, - "x_ohm_per_km": 0.33, - "max_i_ka": 0.35, - "type": "ol", - "q_mm2": 94, - "alpha": 0.00403 - }, - "122-AL1/20-ST1A 10.0": { - "c_nf_per_km": 11.1, - "r_ohm_per_km": 0.2376, - "x_ohm_per_km": 0.323, - "max_i_ka": 0.41, - "type": "ol", - "q_mm2": 122, - "alpha": 0.00403 - }, - "149-AL1/24-ST1A 10.0": { - "c_nf_per_km": 11.25, - "r_ohm_per_km": 0.194, - "x_ohm_per_km": 0.315, - "max_i_ka": 0.47, - "type": "ol", - "q_mm2": 149, - "alpha": 0.00403 - }, - "34-AL1/6-ST1A 20.0": { - "c_nf_per_km": 9.15, - "r_ohm_per_km": 0.8342, - "x_ohm_per_km": 0.382, - "max_i_ka": 0.17, - "type": "ol", - "q_mm2": 34, - "alpha": 0.00403 - }, - "48-AL1/8-ST1A 20.0": { - "c_nf_per_km": 9.5, - "r_ohm_per_km": 0.5939, - "x_ohm_per_km": 0.372, - "max_i_ka": 0.21, - "type": "ol", - "q_mm2": 48, - "alpha": 0.00403 - }, - "70-AL1/11-ST1A 20.0": { - "c_nf_per_km": 9.7, - "r_ohm_per_km": 0.4132, - "x_ohm_per_km": 0.36, - "max_i_ka": 0.29, - "type": "ol", - "q_mm2": 70, - "alpha": 0.00403 - }, - "94-AL1/15-ST1A 20.0": { - "c_nf_per_km": 10, - "r_ohm_per_km": 0.306, - "x_ohm_per_km": 0.35, - "max_i_ka": 0.35, - "type": "ol", - "q_mm2": 94, - "alpha": 0.00403 - }, - "122-AL1/20-ST1A 20.0": { - "c_nf_per_km": 10.3, - "r_ohm_per_km": 0.2376, - "x_ohm_per_km": 0.344, - "max_i_ka": 0.41, - "type": "ol", - "q_mm2": 122, - "alpha": 0.00403 - }, - "149-AL1/24-ST1A 20.0": { - "c_nf_per_km": 10.5, - "r_ohm_per_km": 0.194, - "x_ohm_per_km": 0.337, - "max_i_ka": 0.47, - "type": "ol", - "q_mm2": 149, - "alpha": 0.00403 - }, - "184-AL1/30-ST1A 20.0": { - "c_nf_per_km": 10.75, - "r_ohm_per_km": 0.1571, - "x_ohm_per_km": 0.33, - "max_i_ka": 0.535, - "type": "ol", - "q_mm2": 184, - "alpha": 0.00403 - }, - "243-AL1/39-ST1A 20.0": { - "c_nf_per_km": 11, - "r_ohm_per_km": 0.1188, - "x_ohm_per_km": 0.32, - "max_i_ka": 0.645, - "type": "ol", - "q_mm2": 243, - "alpha": 0.00403 - }, - "48-AL1/8-ST1A 110.0": { - "c_nf_per_km": 8, - "r_ohm_per_km": 0.5939, - "x_ohm_per_km": 0.46, - "max_i_ka": 0.21, - "type": "ol", - "q_mm2": 48, - "alpha": 0.00403 - }, - "70-AL1/11-ST1A 110.0": { - "c_nf_per_km": 8.4, - "r_ohm_per_km": 0.4132, - "x_ohm_per_km": 0.45, - "max_i_ka": 0.29, - "type": "ol", - "q_mm2": 70, - "alpha": 0.00403 - }, - "94-AL1/15-ST1A 110.0": { - "c_nf_per_km": 8.65, - "r_ohm_per_km": 0.306, - "x_ohm_per_km": 0.44, - "max_i_ka": 0.35, - "type": "ol", - "q_mm2": 94, - "alpha": 0.00403 - }, - "122-AL1/20-ST1A 110.0": { - "c_nf_per_km": 8.5, - "r_ohm_per_km": 0.2376, - "x_ohm_per_km": 0.43, - "max_i_ka": 0.41, - "type": "ol", - "q_mm2": 122, - "alpha": 0.00403 - }, - "149-AL1/24-ST1A 110.0": { - "c_nf_per_km": 8.75, - "r_ohm_per_km": 0.194, - "x_ohm_per_km": 0.41, - "max_i_ka": 0.47, - "type": "ol", - "q_mm2": 149, - "alpha": 0.00403 - }, - "184-AL1/30-ST1A 110.0": { - "c_nf_per_km": 8.8, - "r_ohm_per_km": 0.1571, - "x_ohm_per_km": 0.4, - "max_i_ka": 0.535, - "type": "ol", - "q_mm2": 184, - "alpha": 0.00403 - }, - "243-AL1/39-ST1A 110.0": { - "c_nf_per_km": 9, - "r_ohm_per_km": 0.1188, - "x_ohm_per_km": 0.39, - "max_i_ka": 0.645, - "type": "ol", - "q_mm2": 243, - "alpha": 0.00403 - }, - "305-AL1/39-ST1A 110.0": { - "c_nf_per_km": 9.2, - "r_ohm_per_km": 0.0949, - "x_ohm_per_km": 0.38, - "max_i_ka": 0.74, - "type": "ol", - "q_mm2": 305, - "alpha": 0.00403 - }, - "490-AL1/64-ST1A 110.0": { - "c_nf_per_km": 9.75, - "r_ohm_per_km": 0.059, - "x_ohm_per_km": 0.37, - "max_i_ka": 0.96, - "type": "ol", - "q_mm2": 490, - "alpha": 0.00403 - }, - "679-AL1/86-ST1A 110.0": { - "c_nf_per_km": 9.95, - "r_ohm_per_km": 0.042, - "x_ohm_per_km": 0.36, - "max_i_ka": 0.115, - "type": "ol", - "q_mm2": 679, - "alpha": 0.00403 - }, - "490-AL1/64-ST1A 220.0": { - "c_nf_per_km": 10, - "r_ohm_per_km": 0.059, - "x_ohm_per_km": 0.285, - "max_i_ka": 0.96, - "type": "ol", - "q_mm2": 490, - "alpha": 0.00403 - }, - "679-AL1/86-ST1A 220.0": { - "c_nf_per_km": 11.7, - "r_ohm_per_km": 0.042, - "x_ohm_per_km": 0.275, - "max_i_ka": 0.115, - "type": "ol", - "q_mm2": 679, - "alpha": 0.00403 - }, - "490-AL1/64-ST1A 380.0": { - "c_nf_per_km": 11, - "r_ohm_per_km": 0.059, - "x_ohm_per_km": 0.253, - "max_i_ka": 0.96, - "type": "ol", - "q_mm2": 490, - "alpha": 0.00403 - }, - "679-AL1/86-ST1A 380.0": { - "c_nf_per_km": 14.6, - "r_ohm_per_km": 0.042, - "x_ohm_per_km": 0.25, - "max_i_ka": 0.115, - "type": "ol", - "q_mm2": 679, - "alpha": 0.00403 - } - }, - "trafo": { - "160 MVA 380/110 kV": { - "i0_percent": 0.06, - "pfe_kw": 60, - "vkr_percent": 0.25, - "sn_mva": 160, - "vn_lv_kv": 110.0, - "vn_hv_kv": 380.0, - "vk_percent": 12.2, - "shift_degree": 0, - "vector_group": "Yy0", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -9, - "tap_max": 9, - "tap_step_degree": 0, - "tap_step_percent": 1.5, - "tap_phase_shifter": false - }, - "100 MVA 220/110 kV": { - "i0_percent": 0.06, - "pfe_kw": 55, - "vkr_percent": 0.26, - "sn_mva": 100, - "vn_lv_kv": 110.0, - "vn_hv_kv": 220.0, - "vk_percent": 12.0, - "shift_degree": 0, - "vector_group": "Yy0", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -9, - "tap_max": 9, - "tap_step_degree": 0, - "tap_step_percent": 1.5, - "tap_phase_shifter": false - }, - "63 MVA 110/20 kV": { - "i0_percent": 0.04, - "pfe_kw": 22, - "vkr_percent": 0.32, - "sn_mva": 63, - "vn_lv_kv": 20.0, - "vn_hv_kv": 110.0, - "vk_percent": 18, - "shift_degree": 150, - "vector_group": "YNd5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -9, - "tap_max": 9, - "tap_step_degree": 0, - "tap_step_percent": 1.5, - "tap_phase_shifter": false - }, - "40 MVA 110/20 kV": { - "i0_percent": 0.05, - "pfe_kw": 18, - "vkr_percent": 0.34, - "sn_mva": 40, - "vn_lv_kv": 20.0, - "vn_hv_kv": 110.0, - "vk_percent": 16.2, - "shift_degree": 150, - "vector_group": "YNd5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -9, - "tap_max": 9, - "tap_step_degree": 0, - "tap_step_percent": 1.5, - "tap_phase_shifter": false - }, - "25 MVA 110/20 kV": { - "i0_percent": 0.07, - "pfe_kw": 14, - "vkr_percent": 0.41, - "sn_mva": 25, - "vn_lv_kv": 20.0, - "vn_hv_kv": 110.0, - "vk_percent": 12, - "shift_degree": 150, - "vector_group": "YNd5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -9, - "tap_max": 9, - "tap_step_degree": 0, - "tap_step_percent": 1.5, - "tap_phase_shifter": false - }, - "63 MVA 110/10 kV": { - "sn_mva": 63, - "vn_hv_kv": 110, - "vn_lv_kv": 10, - "vk_percent": 18, - "vkr_percent": 0.32, - "pfe_kw": 22, - "i0_percent": 0.04, - "shift_degree": 150, - "vector_group": "YNd5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -9, - "tap_max": 9, - "tap_step_degree": 0, - "tap_step_percent": 1.5, - "tap_phase_shifter": false - }, - "40 MVA 110/10 kV": { - "sn_mva": 40, - "vn_hv_kv": 110, - "vn_lv_kv": 10, - "vk_percent": 16.2, - "vkr_percent": 0.34, - "pfe_kw": 18, - "i0_percent": 0.05, - "shift_degree": 150, - "vector_group": "YNd5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -9, - "tap_max": 9, - "tap_step_degree": 0, - "tap_step_percent": 1.5, - "tap_phase_shifter": false - }, - "25 MVA 110/10 kV": { - "sn_mva": 25, - "vn_hv_kv": 110, - "vn_lv_kv": 10, - "vk_percent": 12, - "vkr_percent": 0.41, - "pfe_kw": 14, - "i0_percent": 0.07, - "shift_degree": 150, - "vector_group": "YNd5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -9, - "tap_max": 9, - "tap_step_degree": 0, - "tap_step_percent": 1.5, - "tap_phase_shifter": false - }, - "0.25 MVA 20/0.4 kV": { - "sn_mva": 0.25, - "vn_hv_kv": 20, - "vn_lv_kv": 0.4, - "vk_percent": 6, - "vkr_percent": 1.44, - "pfe_kw": 0.8, - "i0_percent": 0.32, - "shift_degree": 150, - "vector_group": "Yzn5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -2, - "tap_max": 2, - "tap_step_degree": 0, - "tap_step_percent": 2.5, - "tap_phase_shifter": false - }, - "0.4 MVA 20/0.4 kV": { - "sn_mva": 0.4, - "vn_hv_kv": 20, - "vn_lv_kv": 0.4, - "vk_percent": 6, - "vkr_percent": 1.425, - "pfe_kw": 1.35, - "i0_percent": 0.3375, - "shift_degree": 150, - "vector_group": "Dyn5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -2, - "tap_max": 2, - "tap_step_degree": 0, - "tap_step_percent": 2.5, - "tap_phase_shifter": false - }, - "0.63 MVA 20/0.4 kV": { - "sn_mva": 0.63, - "vn_hv_kv": 20, - "vn_lv_kv": 0.4, - "vk_percent": 6, - "vkr_percent": 1.206, - "pfe_kw": 1.65, - "i0_percent": 0.2619, - "shift_degree": 150, - "vector_group": "Dyn5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -2, - "tap_max": 2, - "tap_step_degree": 0, - "tap_step_percent": 2.5, - "tap_phase_shifter": false - }, - "0.25 MVA 10/0.4 kV": { - "sn_mva": 0.25, - "vn_hv_kv": 10, - "vn_lv_kv": 0.4, - "vk_percent": 4, - "vkr_percent": 1.2, - "pfe_kw": 0.6, - "i0_percent": 0.24, - "shift_degree": 150, - "vector_group": "Dyn5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -2, - "tap_max": 2, - "tap_step_degree": 0, - "tap_step_percent": 2.5, - "tap_phase_shifter": false - }, - "0.4 MVA 10/0.4 kV": { - "sn_mva": 0.4, - "vn_hv_kv": 10, - "vn_lv_kv": 0.4, - "vk_percent": 4, - "vkr_percent": 1.325, - "pfe_kw": 0.95, - "i0_percent": 0.2375, - "shift_degree": 150, - "vector_group": "Dyn5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -2, - "tap_max": 2, - "tap_step_degree": 0, - "tap_step_percent": 2.5, - "tap_phase_shifter": false - }, - "0.63 MVA 10/0.4 kV": { - "sn_mva": 0.63, - "vn_hv_kv": 10, - "vn_lv_kv": 0.4, - "vk_percent": 4, - "vkr_percent": 1.0794, - "pfe_kw": 1.18, - "i0_percent": 0.1873, - "shift_degree": 150, - "vector_group": "Dyn5", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -2, - "tap_max": 2, - "tap_step_degree": 0, - "tap_step_percent": 2.5, - "tap_phase_shifter": false - } - }, - "trafo3w": { - "63/25/38 MVA 110/20/10 kV": { - "sn_hv_mva": 63, - "sn_mv_mva": 25, - "sn_lv_mva": 38, - "vn_hv_kv": 110, - "vn_mv_kv": 20, - "vn_lv_kv": 10, - "vk_hv_percent": 10.4, - "vk_mv_percent": 10.4, - "vk_lv_percent": 10.4, - "vkr_hv_percent": 0.28, - "vkr_mv_percent": 0.32, - "vkr_lv_percent": 0.35, - "pfe_kw": 35, - "i0_percent": 0.89, - "shift_mv_degree": 0, - "shift_lv_degree": 0, - "vector_group": "YN0yn0yn0", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -10, - "tap_max": 10, - "tap_step_percent": 1.2 - }, - "63/25/38 MVA 110/10/10 kV": { - "sn_hv_mva": 63, - "sn_mv_mva": 25, - "sn_lv_mva": 38, - "vn_hv_kv": 110, - "vn_mv_kv": 10, - "vn_lv_kv": 10, - "vk_hv_percent": 10.4, - "vk_mv_percent": 10.4, - "vk_lv_percent": 10.4, - "vkr_hv_percent": 0.28, - "vkr_mv_percent": 0.32, - "vkr_lv_percent": 0.35, - "pfe_kw": 35, - "i0_percent": 0.89, - "shift_mv_degree": 0, - "shift_lv_degree": 0, - "vector_group": "YN0yn0yn0", - "tap_side": "hv", - "tap_neutral": 0, - "tap_min": -10, - "tap_max": 10, - "tap_step_percent": 1.2 - } - } - }, - "res_bus": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"vm_pu\",\"va_degree\",\"p_mw\",\"q_mvar\"],\"index\":[0,1,2,3,4],\"data\":[[1.02,-0.845445168673926,0.0,-111.791243672370911],[1.02,0.0,-21.729831330858325,116.839935541152954],[1.019214100496144,-0.409103297622625,0.0,0.0],[1.018637116919488,-0.503470352662766,10.0,7.0],[1.017983079721402,-0.653497665026562,10.0,7.0]]}", - "dtype": { - "vm_pu": "float64", - "va_degree": "float64", - "p_mw": "float64", - "q_mvar": "float64" - }, - "orient": "split" - }, - "res_line": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_from_mw\",\"q_from_mvar\",\"p_to_mw\",\"q_to_mvar\",\"pl_mw\",\"ql_mvar\",\"i_from_ka\",\"i_to_ka\",\"i_ka\",\"vm_from_pu\",\"va_from_degree\",\"vm_to_pu\",\"va_to_degree\",\"loading_percent\"],\"index\":[0,1,2,3,4,5,6,7],\"data\":[[-7.167647147657727,57.480079867900443,8.03525639977348,-60.113463233922118,0.867609252115754,-2.633383366021676,0.327874112511858,0.343286326507116,0.343286326507116,1.02,-0.845445168673926,1.02,0.0,57.214387751185988],[-0.657313913963437,25.969126903729045,0.866078469150186,-29.007927174007612,0.208764555186749,-3.038800270278568,0.147040043868819,0.164393305610081,0.164393305610081,1.02,-0.845445168673926,1.019214100496144,-0.409103297622625,74.724229822763931],[1.64566972119938,15.370129751576128,-1.540268914180618,-19.229415550834709,0.105400807018762,-3.859285799258581,0.087496748884432,0.109338903896103,0.109338903896103,1.02,-0.845445168673926,1.018637116919488,-0.503470352662766,68.336814935064211],[6.179291340421495,12.971907266349552,-6.119076735247816,-15.70424981919658,0.060214605173678,-2.732342552847028,0.081330018729726,0.095589209712924,0.095589209712924,1.02,-0.845445168673926,1.017983079721402,-0.653497665026562,59.743256070577175],[13.694574931085771,-56.726472302863066,-13.283848894885464,55.407854241119566,0.410726036200307,-1.3186180617435,0.330312825878128,0.322760996590474,0.330312825878128,1.02,0.0,1.019214100496144,-0.409103297622625,55.052137646354595],[6.208885212872048,-13.199963533555254,-6.184761786109662,11.833197159642042,0.024123426762386,-1.366766373913212,0.082632108556076,0.075677384410291,0.082632108556076,1.019214100496144,-0.409103297622625,1.018637116919488,-0.503470352662766,27.544036185358689],[6.208885212872048,-13.199963533555254,-6.184761786109662,11.833197159642042,0.024123426762386,-1.366766373913212,0.082632108556076,0.075677384410291,0.082632108556076,1.019214100496144,-0.409103297622625,1.018637116919488,-0.503470352662766,27.544036185358689],[3.909792486391969,-11.436978768449999,-3.88092326475316,8.704249819196738,0.028869221638809,-2.732728949253261,0.068506463438984,0.054050881891821,0.068506463438984,1.018637116919488,-0.503470352662766,1.017983079721402,-0.653497665026562,42.816539649365005]]}", - "dtype": { - "p_from_mw": "float64", - "q_from_mvar": "float64", - "p_to_mw": "float64", - "q_to_mvar": "float64", - "pl_mw": "float64", - "ql_mvar": "float64", - "i_from_ka": "float64", - "i_to_ka": "float64", - "i_ka": "float64", - "vm_from_pu": "float64", - "va_from_degree": "float64", - "vm_to_pu": "float64", - "va_to_degree": "float64", - "loading_percent": "float64" - }, - "orient": "split" - }, - "res_trafo": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_hv_mw\",\"q_hv_mvar\",\"p_lv_mw\",\"q_lv_mvar\",\"pl_mw\",\"ql_mvar\",\"i_hv_ka\",\"i_lv_ka\",\"vm_hv_pu\",\"va_hv_degree\",\"vm_lv_pu\",\"va_lv_degree\",\"loading_percent\"],\"index\":[],\"data\":[]}", - "dtype": { - "p_hv_mw": "float64", - "q_hv_mvar": "float64", - "p_lv_mw": "float64", - "q_lv_mvar": "float64", - "pl_mw": "float64", - "ql_mvar": "float64", - "i_hv_ka": "float64", - "i_lv_ka": "float64", - "vm_hv_pu": "float64", - "va_hv_degree": "float64", - "vm_lv_pu": "float64", - "va_lv_degree": "float64", - "loading_percent": "float64" - }, - "orient": "split" - }, - "res_trafo3w": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_hv_mw\",\"q_hv_mvar\",\"p_mv_mw\",\"q_mv_mvar\",\"p_lv_mw\",\"q_lv_mvar\",\"pl_mw\",\"ql_mvar\",\"i_hv_ka\",\"i_mv_ka\",\"i_lv_ka\",\"vm_hv_pu\",\"va_hv_degree\",\"vm_mv_pu\",\"va_mv_degree\",\"vm_lv_pu\",\"va_lv_degree\",\"va_internal_degree\",\"vm_internal_pu\",\"loading_percent\"],\"index\":[],\"data\":[]}", - "dtype": { - "p_hv_mw": "float64", - "q_hv_mvar": "float64", - "p_mv_mw": "float64", - "q_mv_mvar": "float64", - "p_lv_mw": "float64", - "q_lv_mvar": "float64", - "pl_mw": "float64", - "ql_mvar": "float64", - "i_hv_ka": "float64", - "i_mv_ka": "float64", - "i_lv_ka": "float64", - "vm_hv_pu": "float64", - "va_hv_degree": "float64", - "vm_mv_pu": "float64", - "va_mv_degree": "float64", - "vm_lv_pu": "float64", - "va_lv_degree": "float64", - "va_internal_degree": "float64", - "vm_internal_pu": "float64", - "loading_percent": "float64" - }, - "orient": "split" - }, - "res_impedance": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_from_mw\",\"q_from_mvar\",\"p_to_mw\",\"q_to_mvar\",\"pl_mw\",\"ql_mvar\",\"i_from_ka\",\"i_to_ka\"],\"index\":[],\"data\":[]}", - "dtype": { - "p_from_mw": "float64", - "q_from_mvar": "float64", - "p_to_mw": "float64", - "q_to_mvar": "float64", - "pl_mw": "float64", - "ql_mvar": "float64", - "i_from_ka": "float64", - "i_to_ka": "float64" - }, - "orient": "split" - }, - "res_ext_grid": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", - "dtype": { - "p_mw": "float64", - "q_mvar": "float64" - }, - "orient": "split" - }, - "res_load": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[0,1,2],\"data\":[[10.0,7.0],[10.0,7.0],[10.0,7.0]]}", - "dtype": { - "p_mw": "float64", - "q_mvar": "float64" - }, - "orient": "split" - }, - "res_sgen": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", - "dtype": { - "p_mw": "float64", - "q_mvar": "float64" - }, - "orient": "split" - }, - "res_storage": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", - "dtype": { - "p_mw": "float64", - "q_mvar": "float64" - }, - "orient": "split" - }, - "res_shunt": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"vm_pu\"],\"index\":[],\"data\":[]}", - "dtype": { - "p_mw": "float64", - "q_mvar": "float64", - "vm_pu": "float64" - }, - "orient": "split" - }, - "res_gen": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"va_degree\",\"vm_pu\"],\"index\":[0,1],\"data\":[[10.0,118.791243672370911,-0.845445168673926,1.02],[21.729831330858325,-116.839935541152954,0.0,1.02]]}", - "dtype": { - "p_mw": "float64", - "q_mvar": "float64", - "va_degree": "float64", - "vm_pu": "float64" - }, - "orient": "split" - }, - "res_ward": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"vm_pu\"],\"index\":[],\"data\":[]}", - "dtype": { - "p_mw": "float64", - "q_mvar": "float64", - "vm_pu": "float64" - }, - "orient": "split" - }, - "res_xward": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"vm_pu\",\"va_internal_degree\",\"vm_internal_pu\"],\"index\":[],\"data\":[]}", - "dtype": { - "p_mw": "float64", - "q_mvar": "float64", - "vm_pu": "float64", - "va_internal_degree": "float64", - "vm_internal_pu": "float64" - }, - "orient": "split" - }, - "res_dcline": { - "_module": "pandas.core.frame", - "_class": "DataFrame", - "_object": "{\"columns\":[\"p_from_mw\",\"q_from_mvar\",\"p_to_mw\",\"q_to_mvar\",\"pl_mw\",\"vm_from_pu\",\"va_from_degree\",\"vm_to_pu\",\"va_to_degree\"],\"index\":[],\"data\":[]}", - "dtype": { - "p_from_mw": "float64", - "q_from_mvar": "float64", - "p_to_mw": "float64", - "q_to_mvar": "float64", - "pl_mw": "float64", - "vm_from_pu": "float64", - "va_from_degree": "float64", - "vm_to_pu": "float64", - "va_to_degree": "float64" - }, - "orient": "split" - }, - "user_pf_options": {}, - "OPF_converged": false - } -} diff --git a/grid2op/data/rte_case5_flexibility/grid_layout.json b/grid2op/data/rte_case5_flexibility/grid_layout.json deleted file mode 100644 index 05780856..00000000 --- a/grid2op/data/rte_case5_flexibility/grid_layout.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "sub_0": [ - 0.0, - 0.0 - ], - "sub_1": [ - 0.0, - 400.0 - ], - "sub_2": [ - 200.0, - 400.0 - ], - "sub_3": [ - 400.0, - 400.0 - ], - "sub_4": [ - 400.0, - 0.0 - ] -} \ No newline at end of file diff --git a/grid2op/data/rte_case5_flexibility/params.json b/grid2op/data/rte_case5_flexibility/params.json deleted file mode 100644 index 124f5f23..00000000 --- a/grid2op/data/rte_case5_flexibility/params.json +++ /dev/null @@ -1 +0,0 @@ -{"NB_TIMESTEP_TOPOLOGY_REMODIF": 19} \ No newline at end of file diff --git a/grid2op/data/rte_case5_flexibility/prods_charac.csv b/grid2op/data/rte_case5_flexibility/prods_charac.csv deleted file mode 100644 index d82fbbad..00000000 --- a/grid2op/data/rte_case5_flexibility/prods_charac.csv +++ /dev/null @@ -1,3 +0,0 @@ -Pmax,Pmin,name,type,bus,max_ramp_up,max_ramp_down,min_up_time,min_down_time,marginal_cost,shut_down_cost,start_cost,x,y,V -10,0.0,gen_0_0,wind,5,0,0,0,0,0,0,0,0,0,102. -30,0.0,gen_1_1,thermal,0,10,10,4,4,70,1,2,0,400,102. \ No newline at end of file diff --git a/grid2op/data_test/rte_case5_flexibility/__pycache__/config.cpython-310.pyc b/grid2op/data_test/rte_case5_flexibility/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..14f1f7494d035fd9e4d508e6275fef0320b8ea6e GIT binary patch literal 719 zcmZ8eL5|Zf6m^=m$)stAS%8p`Sg`A6B8|ijp@A87HZWA}0x6ryjelE%9arB@huLxt zF2EhQ0XN8&6&GN^itjWL2wQR9d;WfY`ZnWn!e~7C_I>xc$Jh@~{v9JCm-N{UErN+a zF&*SV;Bim&beM+?4pmudgi_p2lR8=-ah>w!;#Hxz zwe^`=cGx9CwvXx3KTECXhdU z)jH#VWE}dv)2_bl3`SlZQq+yyer5RBSNi4mGTRvqnSiQ*?AjEShSJ$=j^+at1a5r9 zcl)e0%mjBlLq|Q}7M`y*3O>oTRMOpNNB8sneI38hMo=$b26TmeMoY@lpqst07yLhy GUw;4_*V>o> literal 0 HcmV?d00001 diff --git a/grid2op/data_test/rte_case5_flexibility/__pycache__/config.cpython-312.pyc b/grid2op/data_test/rte_case5_flexibility/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ebdc7bd510efa68210d0064f07797e8cfc84b6b0 GIT binary patch literal 748 zcmYLF%Wl&^6dlKj?K+RPQX8PqqO=ks3u>tnY(SNuRl0!^r3)l)qKW4w8JO`z<4H=E z@DptK0setM=nq(yvSP)qs5@57IJOwS&N*}Mnd5uYZ2CyAhd*zoUn>axGNfGf0^E%? z_=X4~7DL<$Ez95vt8hECb8NFJuZ6W7SDC}z(9Lm8V=we_?63{)hrWft2aC91Lk(Q; zh?`&DcR#Ez-R{P3b7we{n IBlon$KUezh(*OVf literal 0 HcmV?d00001 diff --git a/grid2op/tests/test_flexibility.py b/grid2op/tests/test_flexibility.py index 37b8ebab..0dd7072a 100644 --- a/grid2op/tests/test_flexibility.py +++ b/grid2op/tests/test_flexibility.py @@ -49,6 +49,12 @@ def setUp(self) -> None: self.flex_small_down = self.env.action_space( {"load_flexibility": [(el, 0.01) for el in np.nonzero(self.env.load_flexible)[0]]} ) + self.flex_up_ambiguous = self.env.action_space( + {"load_flexibility": [(el, 1.1*self.env.load_max_ramp_up[el]) for el in np.nonzero(self.env.load_flexible)[0]]} + ) + self.flex_down_ambiguous = self.env.action_space( + {"load_flexibility": [(el, -1.1*self.env.load_max_ramp_down[el]) for el in np.nonzero(self.env.load_flexible)[0]]} + ) def test_create_flex_action(self): with warnings.catch_warnings(): @@ -94,7 +100,23 @@ def test_flex_max_ramp_down(self): expected_load = ref_load + self.flex_max_ramp_down.load_flexibility[flex_mask] minimum_feasible_load = np.maximum(np.zeros(flex_mask.sum()), expected_load) assert np.isclose(flex_obs.load_p[flex_mask], minimum_feasible_load, atol=0.001).all() - + + def test_flex_up_beyond_limits(self): + # Ambiguous action gets replaced with Do Nothing + flex_obs, _, _, info = self.env.step(self.flex_up_ambiguous) + flex_mask = self.env.load_flexible + ref_load = self.ref_obs.load_p[flex_mask] + assert np.isclose(flex_obs.load_p[flex_mask], ref_load, atol=0.001).all() + assert info["is_ambiguous"] + + def test_flex_down_beyond_limits(self): + # Ambiguous action gets replaced with Do Nothing + flex_obs, _, _, info = self.env.step(self.flex_down_ambiguous) + flex_mask = self.env.load_flexible + ref_load = self.ref_obs.load_p[flex_mask] + assert np.isclose(flex_obs.load_p[flex_mask], ref_load, atol=0.001).all() + assert info["is_ambiguous"] + def test_flex_in_obs(self): flex_obs, *_ = self.env.step(self.flex_max_ramp_up) assert np.isclose(flex_obs.target_flex, self.flex_max_ramp_up.load_flexibility).all() From 41c7b3ff535b668356443ea28ca657d989918605 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Thu, 9 Oct 2025 14:26:54 +0200 Subject: [PATCH 34/38] Debug: Add lxml to make pandapower happy Signed-off-by: Xavier Weiss --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 09e2d44b..efc7edcc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,7 +112,8 @@ test = [ "imageio", "plotly", "matplotlib", - "seaborn" + "seaborn", + "lxml" ] chronix2grid = [ "ChroniX2Grid>=1.2.0.post1", From 4ba64b31b39c6d87d6e3a728f0c6bdd604145444 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Thu, 9 Oct 2025 14:48:31 +0200 Subject: [PATCH 35/38] Debug: Undo lxml addition Signed-off-by: Xavier Weiss --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index efc7edcc..09e2d44b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,8 +112,7 @@ test = [ "imageio", "plotly", "matplotlib", - "seaborn", - "lxml" + "seaborn" ] chronix2grid = [ "ChroniX2Grid>=1.2.0.post1", From c65499a63386a726c4fda71074a4998ab8c71510 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Thu, 9 Oct 2025 15:10:26 +0200 Subject: [PATCH 36/38] Debug: Refactor _flexibility in PlayableAction to be '_load_flexibility' Signed-off-by: Xavier Weiss --- grid2op/Action/baseAction.py | 4 ++-- grid2op/Action/flexibilityAction.py | 2 +- grid2op/Action/playableAction.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/grid2op/Action/baseAction.py b/grid2op/Action/baseAction.py index baeea36d..a2e8c9e6 100644 --- a/grid2op/Action/baseAction.py +++ b/grid2op/Action/baseAction.py @@ -239,7 +239,7 @@ class BaseAction(GridObjects): on a generator, and on another you ask +10 MW then the total setpoint for this generator that the environment will try to implement is +20MW. - _flexibility: :class:`numpy.ndarray`, dtype:float + _load_flexibility: :class:`numpy.ndarray`, dtype:float Amount of demand response that this action will perform. Flexiblity will increase the load's active consumption value. This will be added to the value of the loads. The Environment will make sure that every physical constraint is met. This means that the agent provides a change in flexiblity, but there is no guarantee @@ -3669,7 +3669,7 @@ def _check_for_ambiguity(self): - A flexibility action is active, but :attr:`grid2op.Space.GridObjects.flexibility_available` is set to ``False`` - - The length of the flexibility vector :attr:`BaseAction._flexibility` is not compatible with the number + - The length of the flexibility vector :attr:`BaseAction._load_flexibility` is not compatible with the number of loads. - Some of the asked for flexibility is above the maximum ramp up :attr:`grid2op.Space.GridObjects.load_max_ramp_up` - some of the asked for flexibility is below the maximum ramp down :attr:`grid2op.Space.GridObjects.load_max_ramp_down` diff --git a/grid2op/Action/flexibilityAction.py b/grid2op/Action/flexibilityAction.py index c321ccdc..634717d0 100644 --- a/grid2op/Action/flexibilityAction.py +++ b/grid2op/Action/flexibilityAction.py @@ -19,7 +19,7 @@ class FlexibilityAction(PlayableAction): authorized_keys = {"flexibility"} - attr_list_vect = ["_flexibility"] + attr_list_vect = ["_load_flexibility"] attr_list_set = set(attr_list_vect) def __init__(self, _names_chronics_to_backend: Optional[Dict[Literal["loads", "prods", "lines"], Dict[str, str]]]=None): diff --git a/grid2op/Action/playableAction.py b/grid2op/Action/playableAction.py index d2b08df4..9a465389 100644 --- a/grid2op/Action/playableAction.py +++ b/grid2op/Action/playableAction.py @@ -110,7 +110,7 @@ def __call__(self): self._set_topo_vect, self._change_bus_vect, self._redispatch, - self._flexibility, + self._load_flexibility, self._storage_power, {}, ) From b491d58f803ece40a695ab59b88d89b1503484a6 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Thu, 9 Oct 2025 15:14:49 +0200 Subject: [PATCH 37/38] Debug: CI breaks without lxml as dependency Signed-off-by: Xavier Weiss --- pyproject.toml | 1 + pyproject_38.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 09e2d44b..42c42e7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ "scipy>=1.4.1", "pandas>=1.0.3,<3", # pandas 3 might break pandapower backend "pandapower>=3.1.1; python_version>='3.9'", + "lxml", "numpy", "scipy", "tqdm>=4.45.0", diff --git a/pyproject_38.toml b/pyproject_38.toml index 516aab26..2c6ab932 100644 --- a/pyproject_38.toml +++ b/pyproject_38.toml @@ -14,6 +14,7 @@ dependencies = [ "scipy>=1.4.1", "pandas>=1.0.3", "pandapower>=3.1.1; python_version>='3.9'", + "lxml", "numpy", "scipy", "tqdm>=4.45.0", From f1d66c2a6c68fe643d9359029df76f9310e78ad5 Mon Sep 17 00:00:00 2001 From: Xavier Weiss Date: Thu, 9 Oct 2025 16:28:28 +0200 Subject: [PATCH 38/38] Add: rte_case5_flexibility environment Signed-off-by: Xavier Weiss --- .../chronics/0/load_p.csv.bz2 | Bin 0 -> 87 bytes .../chronics/0/load_p_forecasted.csv.bz2 | Bin 0 -> 98 bytes .../chronics/0/prod_p.csv.bz2 | Bin 0 -> 83 bytes .../chronics/0/prod_p_forecasted.csv.bz2 | Bin 0 -> 83 bytes .../chronics/0/prod_v_forecasted.csv.bz2 | Bin 0 -> 83 bytes grid2op/data/rte_case5_flexibility/config.py | 19 + .../difficulty_levels.json | 62 + grid2op/data/rte_case5_flexibility/grid.json | 1339 +++++++++++++++++ .../rte_case5_flexibility/grid_layout.json | 22 + .../data/rte_case5_flexibility/params.json | 1 + .../rte_case5_flexibility/prods_charac.csv | 3 + .../__pycache__/config.cpython-310.pyc | Bin 719 -> 0 bytes .../__pycache__/config.cpython-312.pyc | Bin 748 -> 0 bytes 13 files changed, 1446 insertions(+) create mode 100644 grid2op/data/rte_case5_flexibility/chronics/0/load_p.csv.bz2 create mode 100644 grid2op/data/rte_case5_flexibility/chronics/0/load_p_forecasted.csv.bz2 create mode 100644 grid2op/data/rte_case5_flexibility/chronics/0/prod_p.csv.bz2 create mode 100644 grid2op/data/rte_case5_flexibility/chronics/0/prod_p_forecasted.csv.bz2 create mode 100644 grid2op/data/rte_case5_flexibility/chronics/0/prod_v_forecasted.csv.bz2 create mode 100644 grid2op/data/rte_case5_flexibility/config.py create mode 100644 grid2op/data/rte_case5_flexibility/difficulty_levels.json create mode 100644 grid2op/data/rte_case5_flexibility/grid.json create mode 100644 grid2op/data/rte_case5_flexibility/grid_layout.json create mode 100644 grid2op/data/rte_case5_flexibility/params.json create mode 100644 grid2op/data/rte_case5_flexibility/prods_charac.csv delete mode 100644 grid2op/data_test/rte_case5_flexibility/__pycache__/config.cpython-310.pyc delete mode 100644 grid2op/data_test/rte_case5_flexibility/__pycache__/config.cpython-312.pyc diff --git a/grid2op/data/rte_case5_flexibility/chronics/0/load_p.csv.bz2 b/grid2op/data/rte_case5_flexibility/chronics/0/load_p.csv.bz2 new file mode 100644 index 0000000000000000000000000000000000000000..0b4140b9fef499f7f9f068a99db66cbc14bc33fe GIT binary patch literal 87 zcmV-d0I2^$T4*^jL0KkKSv6-l`T#RQ+W-I(00DeR005)}pa60kQZx-T&`nhafDENT t#;TxjRD#5+2UDq34b?!=R17MCg(Ltr+GDm(yY%CIzaj8qGbEPXWBnn6ALHUonY17oEI14C1z0z;7`N9HTJ1+!K%WEg6(-PLMr m4X_Ml*1hU*EY#s`2;b5Gqg6uw%mIgP%ZnGeS_Di083F)9pd2m$ literal 0 HcmV?d00001 diff --git a/grid2op/data/rte_case5_flexibility/chronics/0/prod_p_forecasted.csv.bz2 b/grid2op/data/rte_case5_flexibility/chronics/0/prod_p_forecasted.csv.bz2 new file mode 100644 index 0000000000000000000000000000000000000000..81e867b529a3ed5ff5192205533ec0dd75fe5843 GIT binary patch literal 83 zcmV-Z0IdH)T4*^jL0KkKSu6`C5&$P_+W-I(00DW3004r4AOLb15YyBif@-7zpv0&f pa;gU^plGTFTdIMKs)5?59ZH~OR3x5;p^hdjUC9*TLO`%Am`FKI9jpKV literal 0 HcmV?d00001 diff --git a/grid2op/data/rte_case5_flexibility/chronics/0/prod_v_forecasted.csv.bz2 b/grid2op/data/rte_case5_flexibility/chronics/0/prod_v_forecasted.csv.bz2 new file mode 100644 index 0000000000000000000000000000000000000000..c685c39c018b1ba892967015af0a2923fa8abcb0 GIT binary patch literal 83 zcmV-Z0IdH)T4*^jL0KkKSq8~9X#g)uTL1tM00D3a004r4AOLVc3uTcBmuHbT0jyk9CrW! literal 0 HcmV?d00001 diff --git a/grid2op/data/rte_case5_flexibility/config.py b/grid2op/data/rte_case5_flexibility/config.py new file mode 100644 index 00000000..cdc2758f --- /dev/null +++ b/grid2op/data/rte_case5_flexibility/config.py @@ -0,0 +1,19 @@ +from grid2op.Action import CompleteAction +from grid2op.Reward import L2RPNReward +from grid2op.Rules import DefaultRules +from grid2op.Chronics import Multifolder +from grid2op.Chronics import GridStateFromFileWithForecasts +from grid2op.Backend import PandaPowerBackend + +config = { + "backend": PandaPowerBackend, + "action_class": CompleteAction, + "observation_class": None, + "reward_class": L2RPNReward, + "gamerules_class": DefaultRules, + "chronics_class": Multifolder, + "grid_value_class": GridStateFromFileWithForecasts, + "volagecontroler_class": None, + "thermal_limits": None, + "names_chronics_to_grid": None, +} diff --git a/grid2op/data/rte_case5_flexibility/difficulty_levels.json b/grid2op/data/rte_case5_flexibility/difficulty_levels.json new file mode 100644 index 00000000..581f9117 --- /dev/null +++ b/grid2op/data/rte_case5_flexibility/difficulty_levels.json @@ -0,0 +1,62 @@ +{ + "0": { + "NO_OVERFLOW_DISCONNECTION": true, + "NB_TIMESTEP_OVERFLOW_ALLOWED": 9999, + "NB_TIMESTEP_COOLDOWN_SUB": 0, + "NB_TIMESTEP_COOLDOWN_LINE": 0, + "HARD_OVERFLOW_THRESHOLD": 9999, + "NB_TIMESTEP_RECONNECTION": 0, + "IGNORE_MIN_UP_DOWN_TIME": true, + "ALLOW_FLEX_LOAD_SWITCH_OFF": true, + "ALLOW_DISPATCH_GEN_SWITCH_OFF": true, + "ENV_DC": false, + "FORECAST_DC": false, + "MAX_SUB_CHANGED": 1, + "MAX_LINE_STATUS_CHANGED": 1 + }, + "1": { + "NO_OVERFLOW_DISCONNECTION": false, + "NB_TIMESTEP_OVERFLOW_ALLOWED": 6, + "NB_TIMESTEP_COOLDOWN_SUB": 0, + "NB_TIMESTEP_COOLDOWN_LINE": 0, + "HARD_OVERFLOW_THRESHOLD": 300, + "NB_TIMESTEP_RECONNECTION": 1, + "IGNORE_MIN_UP_DOWN_TIME": true, + "ALLOW_FLEX_LOAD_SWITCH_OFF": true, + "ALLOW_DISPATCH_GEN_SWITCH_OFF": true, + "ENV_DC": false, + "FORECAST_DC": false, + "MAX_SUB_CHANGED": 1, + "MAX_LINE_STATUS_CHANGED": 1 + }, + "2": { + "NO_OVERFLOW_DISCONNECTION": false, + "NB_TIMESTEP_OVERFLOW_ALLOWED": 3, + "NB_TIMESTEP_COOLDOWN_SUB": 1, + "NB_TIMESTEP_COOLDOWN_LINE": 1, + "HARD_OVERFLOW_THRESHOLD": 250, + "NB_TIMESTEP_RECONNECTION": 6, + "IGNORE_MIN_UP_DOWN_TIME": true, + "ALLOW_FLEX_LOAD_SWITCH_OFF": true, + "ALLOW_DISPATCH_GEN_SWITCH_OFF": true, + "ENV_DC": false, + "FORECAST_DC": false, + "MAX_SUB_CHANGED": 1, + "MAX_LINE_STATUS_CHANGED": 1 + }, + "competition": { + "NO_OVERFLOW_DISCONNECTION": false, + "NB_TIMESTEP_OVERFLOW_ALLOWED": 3, + "NB_TIMESTEP_COOLDOWN_SUB": 3, + "NB_TIMESTEP_COOLDOWN_LINE": 3, + "HARD_OVERFLOW_THRESHOLD": 200, + "NB_TIMESTEP_RECONNECTION": 12, + "IGNORE_MIN_UP_DOWN_TIME": true, + "ALLOW_FLEX_LOAD_SWITCH_OFF": true, + "ALLOW_DISPATCH_GEN_SWITCH_OFF": true, + "ENV_DC": false, + "FORECAST_DC": false, + "MAX_SUB_CHANGED": 1, + "MAX_LINE_STATUS_CHANGED": 1 + } +} diff --git a/grid2op/data/rte_case5_flexibility/grid.json b/grid2op/data/rte_case5_flexibility/grid.json new file mode 100644 index 00000000..2e9dd0c7 --- /dev/null +++ b/grid2op/data/rte_case5_flexibility/grid.json @@ -0,0 +1,1339 @@ +{ + "_module": "pandapower.auxiliary", + "_class": "pandapowerNet", + "_object": { + "bus": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"vn_kv\",\"type\",\"zone\",\"in_service\"],\"index\":[0,1,2,3,4],\"data\":[[\"substation_1\",100.0,\"b\",null,true],[\"substation_2\",100.0,\"b\",null,true],[\"substation_3\",100.0,\"b\",null,true],[\"substation_4\",100.0,\"b\",null,true],[\"substation_5\",100.0,\"b\",null,true]]}", + "dtype": { + "name": "object", + "vn_kv": "float64", + "type": "object", + "zone": "object", + "in_service": "bool" + }, + "orient": "split" + }, + "load": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"p_mw\",\"q_mvar\",\"const_z_percent\",\"const_i_percent\",\"sn_mva\",\"scaling\",\"in_service\",\"type\"],\"index\":[0,1,2],\"data\":[[\"load_0_0\",0,10.0,7.0,0.0,0.0,null,1.0,true,null],[\"load_3_1\",3,10.0,7.0,0.0,0.0,null,1.0,true,null],[\"load_4_2\",4,10.0,7.0,0.0,0.0,null,1.0,true,null]]}", + "dtype": { + "name": "object", + "bus": "uint32", + "p_mw": "float64", + "q_mvar": "float64", + "const_z_percent": "float64", + "const_i_percent": "float64", + "sn_mva": "float64", + "scaling": "float64", + "in_service": "bool", + "type": "object" + }, + "orient": "split" + }, + "sgen": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"p_mw\",\"q_mvar\",\"sn_mva\",\"scaling\",\"in_service\",\"type\",\"current_source\"],\"index\":[],\"data\":[]}", + "dtype": { + "name": "object", + "bus": "int64", + "p_mw": "float64", + "q_mvar": "float64", + "sn_mva": "float64", + "scaling": "float64", + "in_service": "bool", + "type": "object", + "current_source": "bool" + }, + "orient": "split" + }, + "storage": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"p_mw\",\"q_mvar\",\"sn_mva\",\"soc_percent\",\"min_e_mwh\",\"max_e_mwh\",\"scaling\",\"in_service\",\"type\"],\"index\":[],\"data\":[]}", + "dtype": { + "name": "object", + "bus": "int64", + "p_mw": "float64", + "q_mvar": "float64", + "sn_mva": "float64", + "soc_percent": "float64", + "min_e_mwh": "float64", + "max_e_mwh": "float64", + "scaling": "float64", + "in_service": "bool", + "type": "object" + }, + "orient": "split" + }, + "gen": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"p_mw\",\"vm_pu\",\"sn_mva\",\"min_q_mvar\",\"max_q_mvar\",\"scaling\",\"slack\",\"in_service\",\"type\"],\"index\":[0,1],\"data\":[[\"gen_0_0\",0,10.0,1.02,null,null,null,1.0,false,true,null],[\"gen_1_1\",1,20.0,1.02,null,null,null,1.0,true,true,null]]}", + "dtype": { + "name": "object", + "bus": "uint32", + "p_mw": "float64", + "vm_pu": "float64", + "sn_mva": "float64", + "min_q_mvar": "float64", + "max_q_mvar": "float64", + "scaling": "float64", + "slack": "bool", + "in_service": "bool", + "type": "object" + }, + "orient": "split" + }, + "switch": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"bus\",\"element\",\"et\",\"type\",\"closed\",\"name\",\"z_ohm\"],\"index\":[],\"data\":[]}", + "dtype": { + "bus": "int64", + "element": "int64", + "et": "object", + "type": "object", + "closed": "bool", + "name": "object", + "z_ohm": "float64" + }, + "orient": "split" + }, + "shunt": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"bus\",\"name\",\"q_mvar\",\"p_mw\",\"vn_kv\",\"step\",\"max_step\",\"in_service\"],\"index\":[],\"data\":[]}", + "dtype": { + "bus": "uint32", + "name": "object", + "q_mvar": "float64", + "p_mw": "float64", + "vn_kv": "float64", + "step": "uint32", + "max_step": "uint32", + "in_service": "bool" + }, + "orient": "split" + }, + "ext_grid": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"vm_pu\",\"va_degree\",\"in_service\"],\"index\":[],\"data\":[]}", + "dtype": { + "name": "object", + "bus": "uint32", + "vm_pu": "float64", + "va_degree": "float64", + "in_service": "bool" + }, + "orient": "split" + }, + "line": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"std_type\",\"from_bus\",\"to_bus\",\"length_km\",\"r_ohm_per_km\",\"x_ohm_per_km\",\"c_nf_per_km\",\"g_us_per_km\",\"max_i_ka\",\"df\",\"parallel\",\"type\",\"in_service\"],\"index\":[0,1,2,3,4,5,6,7],\"data\":[[null,\"NAYY 4x50 SE\",0,1,4.0,0.642,0.083,210.0,0.0,0.6,1.0,1,\"cs\",true],[\"0_2_2\",\"NAYY 4x50 SE\",0,2,4.47,0.642,0.083,210.0,0.0,0.22,1.0,1,\"cs\",true],[\"0_3_3\",\"NAYY 4x50 SE\",0,3,5.65,0.642,0.083,210.0,0.0,0.16,1.0,1,\"cs\",true],[\"0_4_4\",\"NAYY 4x50 SE\",0,4,4.0,0.642,0.083,210.0,0.0,0.16,1.0,1,\"cs\",true],[\"1_2_5\",\"NAYY 4x50 SE\",1,2,2.0,0.642,0.083,210.0,0.0,0.6,1.0,1,\"cs\",true],[\"2_3_6\",\"NAYY 4x50 SE\",2,3,2.0,0.642,0.083,210.0,0.0,0.3,1.0,1,\"cs\",true],[\"2_3_7\",\"NAYY 4x50 SE\",2,3,2.0,0.642,0.083,210.0,0.0,0.3,1.0,1,\"cs\",true],[\"3_4_8\",\"NAYY 4x50 SE\",3,4,4.0,0.642,0.083,210.0,0.0,0.16,1.0,1,\"cs\",true]]}", + "dtype": { + "name": "object", + "std_type": "object", + "from_bus": "uint32", + "to_bus": "uint32", + "length_km": "float64", + "r_ohm_per_km": "float64", + "x_ohm_per_km": "float64", + "c_nf_per_km": "float64", + "g_us_per_km": "float64", + "max_i_ka": "float64", + "df": "float64", + "parallel": "uint32", + "type": "object", + "in_service": "bool" + }, + "orient": "split" + }, + "trafo": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"std_type\",\"hv_bus\",\"lv_bus\",\"sn_mva\",\"vn_hv_kv\",\"vn_lv_kv\",\"vk_percent\",\"vkr_percent\",\"pfe_kw\",\"i0_percent\",\"shift_degree\",\"tap_side\",\"tap_neutral\",\"tap_min\",\"tap_max\",\"tap_step_percent\",\"tap_step_degree\",\"tap_pos\",\"tap_phase_shifter\",\"parallel\",\"df\",\"in_service\"],\"index\":[],\"data\":[]}", + "dtype": { + "name": "object", + "std_type": "object", + "hv_bus": "uint32", + "lv_bus": "uint32", + "sn_mva": "float64", + "vn_hv_kv": "float64", + "vn_lv_kv": "float64", + "vk_percent": "float64", + "vkr_percent": "float64", + "pfe_kw": "float64", + "i0_percent": "float64", + "shift_degree": "float64", + "tap_side": "object", + "tap_neutral": "int32", + "tap_min": "int32", + "tap_max": "int32", + "tap_step_percent": "float64", + "tap_step_degree": "float64", + "tap_pos": "int32", + "tap_phase_shifter": "bool", + "parallel": "uint32", + "df": "float64", + "in_service": "bool" + }, + "orient": "split" + }, + "trafo3w": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"std_type\",\"hv_bus\",\"mv_bus\",\"lv_bus\",\"sn_hv_mva\",\"sn_mv_mva\",\"sn_lv_mva\",\"vn_hv_kv\",\"vn_mv_kv\",\"vn_lv_kv\",\"vk_hv_percent\",\"vk_mv_percent\",\"vk_lv_percent\",\"vkr_hv_percent\",\"vkr_mv_percent\",\"vkr_lv_percent\",\"pfe_kw\",\"i0_percent\",\"shift_mv_degree\",\"shift_lv_degree\",\"tap_side\",\"tap_neutral\",\"tap_min\",\"tap_max\",\"tap_step_percent\",\"tap_step_degree\",\"tap_pos\",\"tap_at_star_point\",\"in_service\"],\"index\":[],\"data\":[]}", + "dtype": { + "name": "object", + "std_type": "object", + "hv_bus": "uint32", + "mv_bus": "uint32", + "lv_bus": "uint32", + "sn_hv_mva": "float64", + "sn_mv_mva": "float64", + "sn_lv_mva": "float64", + "vn_hv_kv": "float64", + "vn_mv_kv": "float64", + "vn_lv_kv": "float64", + "vk_hv_percent": "float64", + "vk_mv_percent": "float64", + "vk_lv_percent": "float64", + "vkr_hv_percent": "float64", + "vkr_mv_percent": "float64", + "vkr_lv_percent": "float64", + "pfe_kw": "float64", + "i0_percent": "float64", + "shift_mv_degree": "float64", + "shift_lv_degree": "float64", + "tap_side": "object", + "tap_neutral": "int32", + "tap_min": "int32", + "tap_max": "int32", + "tap_step_percent": "float64", + "tap_step_degree": "float64", + "tap_pos": "int32", + "tap_at_star_point": "bool", + "in_service": "bool" + }, + "orient": "split" + }, + "impedance": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"from_bus\",\"to_bus\",\"rft_pu\",\"xft_pu\",\"rtf_pu\",\"xtf_pu\",\"sn_mva\",\"in_service\"],\"index\":[],\"data\":[]}", + "dtype": { + "name": "object", + "from_bus": "uint32", + "to_bus": "uint32", + "rft_pu": "float64", + "xft_pu": "float64", + "rtf_pu": "float64", + "xtf_pu": "float64", + "sn_mva": "float64", + "in_service": "bool" + }, + "orient": "split" + }, + "dcline": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"from_bus\",\"to_bus\",\"p_mw\",\"loss_percent\",\"loss_mw\",\"vm_from_pu\",\"vm_to_pu\",\"max_p_mw\",\"min_q_from_mvar\",\"min_q_to_mvar\",\"max_q_from_mvar\",\"max_q_to_mvar\",\"in_service\"],\"index\":[],\"data\":[]}", + "dtype": { + "name": "object", + "from_bus": "uint32", + "to_bus": "uint32", + "p_mw": "float64", + "loss_percent": "float64", + "loss_mw": "float64", + "vm_from_pu": "float64", + "vm_to_pu": "float64", + "max_p_mw": "float64", + "min_q_from_mvar": "float64", + "min_q_to_mvar": "float64", + "max_q_from_mvar": "float64", + "max_q_to_mvar": "float64", + "in_service": "bool" + }, + "orient": "split" + }, + "ward": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"ps_mw\",\"qs_mvar\",\"qz_mvar\",\"pz_mw\",\"in_service\"],\"index\":[],\"data\":[]}", + "dtype": { + "name": "object", + "bus": "uint32", + "ps_mw": "float64", + "qs_mvar": "float64", + "qz_mvar": "float64", + "pz_mw": "float64", + "in_service": "bool" + }, + "orient": "split" + }, + "xward": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"ps_mw\",\"qs_mvar\",\"qz_mvar\",\"pz_mw\",\"r_ohm\",\"x_ohm\",\"vm_pu\",\"in_service\"],\"index\":[],\"data\":[]}", + "dtype": { + "name": "object", + "bus": "uint32", + "ps_mw": "float64", + "qs_mvar": "float64", + "qz_mvar": "float64", + "pz_mw": "float64", + "r_ohm": "float64", + "x_ohm": "float64", + "vm_pu": "float64", + "in_service": "bool" + }, + "orient": "split" + }, + "measurement": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"measurement_type\",\"element_type\",\"element\",\"value\",\"std_dev\",\"side\"],\"index\":[],\"data\":[]}", + "dtype": { + "name": "object", + "measurement_type": "object", + "element_type": "object", + "element": "uint32", + "value": "float64", + "std_dev": "float64", + "side": "object" + }, + "orient": "split" + }, + "pwl_cost": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"power_type\",\"element\",\"et\",\"points\"],\"index\":[],\"data\":[]}", + "dtype": { + "power_type": "object", + "element": "object", + "et": "object", + "points": "object" + }, + "orient": "split" + }, + "poly_cost": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"element\",\"et\",\"cp0_eur\",\"cp1_eur_per_mw\",\"cp2_eur_per_mw2\",\"cq0_eur\",\"cq1_eur_per_mvar\",\"cq2_eur_per_mvar2\"],\"index\":[],\"data\":[]}", + "dtype": { + "element": "object", + "et": "object", + "cp0_eur": "float64", + "cp1_eur_per_mw": "float64", + "cp2_eur_per_mw2": "float64", + "cq0_eur": "float64", + "cq1_eur_per_mvar": "float64", + "cq2_eur_per_mvar2": "float64" + }, + "orient": "split" + }, + "line_geodata": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"coords\"],\"index\":[0,1,2,3,4,5,6,7],\"data\":[[[[0,0],[0,4]]],[[[0,0],[2,4]]],[[[0,0],[4,4]]],[[[0,0],[4,0]]],[[[0,4],[2,4]]],[[[2,4],[3,4.2],[4,4]]],[[[2,4],[3,3.8],[4,4]]],[[[4,4],[4,0]]]]}", + "dtype": { + "coords": "object" + }, + "orient": "split" + }, + "bus_geodata": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"x\",\"y\",\"coords\"],\"index\":[0,1,2,3,4],\"data\":[[0.0,0.0,null],[0.0,4.0,null],[2.0,4.0,null],[4.0,4.0,null],[4.0,0.0,null]]}", + "dtype": { + "x": "float64", + "y": "float64", + "coords": "object" + }, + "orient": "split" + }, + "version": "2.1.0", + "converged": true, + "name": "5bus", + "f_hz": 50.0, + "sn_mva": 1, + "std_types": { + "line": { + "NAYY 4x50 SE": { + "c_nf_per_km": 210, + "r_ohm_per_km": 0.642, + "x_ohm_per_km": 0.083, + "max_i_ka": 0.142, + "type": "cs", + "q_mm2": 50, + "alpha": 0.00403 + }, + "NAYY 4x120 SE": { + "c_nf_per_km": 264, + "r_ohm_per_km": 0.225, + "x_ohm_per_km": 0.08, + "max_i_ka": 0.242, + "type": "cs", + "q_mm2": 120, + "alpha": 0.00403 + }, + "NAYY 4x150 SE": { + "c_nf_per_km": 261, + "r_ohm_per_km": 0.208, + "x_ohm_per_km": 0.08, + "max_i_ka": 0.27, + "type": "cs", + "q_mm2": 150, + "alpha": 0.00403 + }, + "NA2XS2Y 1x95 RM/25 12/20 kV": { + "c_nf_per_km": 216, + "r_ohm_per_km": 0.313, + "x_ohm_per_km": 0.132, + "max_i_ka": 0.252, + "type": "cs", + "q_mm2": 95, + "alpha": 0.00403 + }, + "NA2XS2Y 1x185 RM/25 12/20 kV": { + "c_nf_per_km": 273, + "r_ohm_per_km": 0.161, + "x_ohm_per_km": 0.117, + "max_i_ka": 0.362, + "type": "cs", + "q_mm2": 185, + "alpha": 0.00403 + }, + "NA2XS2Y 1x240 RM/25 12/20 kV": { + "c_nf_per_km": 304, + "r_ohm_per_km": 0.122, + "x_ohm_per_km": 0.112, + "max_i_ka": 0.421, + "type": "cs", + "q_mm2": 240, + "alpha": 0.00403 + }, + "NA2XS2Y 1x95 RM/25 6/10 kV": { + "c_nf_per_km": 315, + "r_ohm_per_km": 0.313, + "x_ohm_per_km": 0.123, + "max_i_ka": 0.249, + "type": "cs", + "q_mm2": 95, + "alpha": 0.00403 + }, + "NA2XS2Y 1x185 RM/25 6/10 kV": { + "c_nf_per_km": 406, + "r_ohm_per_km": 0.161, + "x_ohm_per_km": 0.11, + "max_i_ka": 0.358, + "type": "cs", + "q_mm2": 185, + "alpha": 0.00403 + }, + "NA2XS2Y 1x240 RM/25 6/10 kV": { + "c_nf_per_km": 456, + "r_ohm_per_km": 0.122, + "x_ohm_per_km": 0.105, + "max_i_ka": 0.416, + "type": "cs", + "q_mm2": 240, + "alpha": 0.00403 + }, + "NA2XS2Y 1x150 RM/25 12/20 kV": { + "c_nf_per_km": 250, + "r_ohm_per_km": 0.206, + "x_ohm_per_km": 0.116, + "max_i_ka": 0.319, + "type": "cs", + "q_mm2": 150, + "alpha": 0.00403 + }, + "NA2XS2Y 1x120 RM/25 12/20 kV": { + "c_nf_per_km": 230, + "r_ohm_per_km": 0.253, + "x_ohm_per_km": 0.119, + "max_i_ka": 0.283, + "type": "cs", + "q_mm2": 120, + "alpha": 0.00403 + }, + "NA2XS2Y 1x70 RM/25 12/20 kV": { + "c_nf_per_km": 190, + "r_ohm_per_km": 0.443, + "x_ohm_per_km": 0.132, + "max_i_ka": 0.22, + "type": "cs", + "q_mm2": 70, + "alpha": 0.00403 + }, + "NA2XS2Y 1x150 RM/25 6/10 kV": { + "c_nf_per_km": 360, + "r_ohm_per_km": 0.206, + "x_ohm_per_km": 0.11, + "max_i_ka": 0.315, + "type": "cs", + "q_mm2": 150, + "alpha": 0.00403 + }, + "NA2XS2Y 1x120 RM/25 6/10 kV": { + "c_nf_per_km": 340, + "r_ohm_per_km": 0.253, + "x_ohm_per_km": 0.113, + "max_i_ka": 0.28, + "type": "cs", + "q_mm2": 120, + "alpha": 0.00403 + }, + "NA2XS2Y 1x70 RM/25 6/10 kV": { + "c_nf_per_km": 280, + "r_ohm_per_km": 0.443, + "x_ohm_per_km": 0.123, + "max_i_ka": 0.217, + "type": "cs", + "q_mm2": 70, + "alpha": 0.00403 + }, + "N2XS(FL)2Y 1x120 RM/35 64/110 kV": { + "c_nf_per_km": 112, + "r_ohm_per_km": 0.153, + "x_ohm_per_km": 0.166, + "max_i_ka": 0.366, + "type": "cs", + "q_mm2": 120, + "alpha": 0.00393 + }, + "N2XS(FL)2Y 1x185 RM/35 64/110 kV": { + "c_nf_per_km": 125, + "r_ohm_per_km": 0.099, + "x_ohm_per_km": 0.156, + "max_i_ka": 0.457, + "type": "cs", + "q_mm2": 185, + "alpha": 0.00393 + }, + "N2XS(FL)2Y 1x240 RM/35 64/110 kV": { + "c_nf_per_km": 135, + "r_ohm_per_km": 0.075, + "x_ohm_per_km": 0.149, + "max_i_ka": 0.526, + "type": "cs", + "q_mm2": 240, + "alpha": 0.00393 + }, + "N2XS(FL)2Y 1x300 RM/35 64/110 kV": { + "c_nf_per_km": 144, + "r_ohm_per_km": 0.06, + "x_ohm_per_km": 0.144, + "max_i_ka": 0.588, + "type": "cs", + "q_mm2": 300, + "alpha": 0.00393 + }, + "15-AL1/3-ST1A 0.4": { + "c_nf_per_km": 11, + "r_ohm_per_km": 1.8769, + "x_ohm_per_km": 0.35, + "max_i_ka": 0.105, + "type": "ol", + "q_mm2": 16, + "alpha": 0.00403 + }, + "24-AL1/4-ST1A 0.4": { + "c_nf_per_km": 11.25, + "r_ohm_per_km": 1.2012, + "x_ohm_per_km": 0.335, + "max_i_ka": 0.14, + "type": "ol", + "q_mm2": 24, + "alpha": 0.00403 + }, + "48-AL1/8-ST1A 0.4": { + "c_nf_per_km": 12.2, + "r_ohm_per_km": 0.5939, + "x_ohm_per_km": 0.3, + "max_i_ka": 0.21, + "type": "ol", + "q_mm2": 48, + "alpha": 0.00403 + }, + "94-AL1/15-ST1A 0.4": { + "c_nf_per_km": 13.2, + "r_ohm_per_km": 0.306, + "x_ohm_per_km": 0.29, + "max_i_ka": 0.35, + "type": "ol", + "q_mm2": 94, + "alpha": 0.00403 + }, + "34-AL1/6-ST1A 10.0": { + "c_nf_per_km": 9.7, + "r_ohm_per_km": 0.8342, + "x_ohm_per_km": 0.36, + "max_i_ka": 0.17, + "type": "ol", + "q_mm2": 34, + "alpha": 0.00403 + }, + "48-AL1/8-ST1A 10.0": { + "c_nf_per_km": 10.1, + "r_ohm_per_km": 0.5939, + "x_ohm_per_km": 0.35, + "max_i_ka": 0.21, + "type": "ol", + "q_mm2": 48, + "alpha": 0.00403 + }, + "70-AL1/11-ST1A 10.0": { + "c_nf_per_km": 10.4, + "r_ohm_per_km": 0.4132, + "x_ohm_per_km": 0.339, + "max_i_ka": 0.29, + "type": "ol", + "q_mm2": 70, + "alpha": 0.00403 + }, + "94-AL1/15-ST1A 10.0": { + "c_nf_per_km": 10.75, + "r_ohm_per_km": 0.306, + "x_ohm_per_km": 0.33, + "max_i_ka": 0.35, + "type": "ol", + "q_mm2": 94, + "alpha": 0.00403 + }, + "122-AL1/20-ST1A 10.0": { + "c_nf_per_km": 11.1, + "r_ohm_per_km": 0.2376, + "x_ohm_per_km": 0.323, + "max_i_ka": 0.41, + "type": "ol", + "q_mm2": 122, + "alpha": 0.00403 + }, + "149-AL1/24-ST1A 10.0": { + "c_nf_per_km": 11.25, + "r_ohm_per_km": 0.194, + "x_ohm_per_km": 0.315, + "max_i_ka": 0.47, + "type": "ol", + "q_mm2": 149, + "alpha": 0.00403 + }, + "34-AL1/6-ST1A 20.0": { + "c_nf_per_km": 9.15, + "r_ohm_per_km": 0.8342, + "x_ohm_per_km": 0.382, + "max_i_ka": 0.17, + "type": "ol", + "q_mm2": 34, + "alpha": 0.00403 + }, + "48-AL1/8-ST1A 20.0": { + "c_nf_per_km": 9.5, + "r_ohm_per_km": 0.5939, + "x_ohm_per_km": 0.372, + "max_i_ka": 0.21, + "type": "ol", + "q_mm2": 48, + "alpha": 0.00403 + }, + "70-AL1/11-ST1A 20.0": { + "c_nf_per_km": 9.7, + "r_ohm_per_km": 0.4132, + "x_ohm_per_km": 0.36, + "max_i_ka": 0.29, + "type": "ol", + "q_mm2": 70, + "alpha": 0.00403 + }, + "94-AL1/15-ST1A 20.0": { + "c_nf_per_km": 10, + "r_ohm_per_km": 0.306, + "x_ohm_per_km": 0.35, + "max_i_ka": 0.35, + "type": "ol", + "q_mm2": 94, + "alpha": 0.00403 + }, + "122-AL1/20-ST1A 20.0": { + "c_nf_per_km": 10.3, + "r_ohm_per_km": 0.2376, + "x_ohm_per_km": 0.344, + "max_i_ka": 0.41, + "type": "ol", + "q_mm2": 122, + "alpha": 0.00403 + }, + "149-AL1/24-ST1A 20.0": { + "c_nf_per_km": 10.5, + "r_ohm_per_km": 0.194, + "x_ohm_per_km": 0.337, + "max_i_ka": 0.47, + "type": "ol", + "q_mm2": 149, + "alpha": 0.00403 + }, + "184-AL1/30-ST1A 20.0": { + "c_nf_per_km": 10.75, + "r_ohm_per_km": 0.1571, + "x_ohm_per_km": 0.33, + "max_i_ka": 0.535, + "type": "ol", + "q_mm2": 184, + "alpha": 0.00403 + }, + "243-AL1/39-ST1A 20.0": { + "c_nf_per_km": 11, + "r_ohm_per_km": 0.1188, + "x_ohm_per_km": 0.32, + "max_i_ka": 0.645, + "type": "ol", + "q_mm2": 243, + "alpha": 0.00403 + }, + "48-AL1/8-ST1A 110.0": { + "c_nf_per_km": 8, + "r_ohm_per_km": 0.5939, + "x_ohm_per_km": 0.46, + "max_i_ka": 0.21, + "type": "ol", + "q_mm2": 48, + "alpha": 0.00403 + }, + "70-AL1/11-ST1A 110.0": { + "c_nf_per_km": 8.4, + "r_ohm_per_km": 0.4132, + "x_ohm_per_km": 0.45, + "max_i_ka": 0.29, + "type": "ol", + "q_mm2": 70, + "alpha": 0.00403 + }, + "94-AL1/15-ST1A 110.0": { + "c_nf_per_km": 8.65, + "r_ohm_per_km": 0.306, + "x_ohm_per_km": 0.44, + "max_i_ka": 0.35, + "type": "ol", + "q_mm2": 94, + "alpha": 0.00403 + }, + "122-AL1/20-ST1A 110.0": { + "c_nf_per_km": 8.5, + "r_ohm_per_km": 0.2376, + "x_ohm_per_km": 0.43, + "max_i_ka": 0.41, + "type": "ol", + "q_mm2": 122, + "alpha": 0.00403 + }, + "149-AL1/24-ST1A 110.0": { + "c_nf_per_km": 8.75, + "r_ohm_per_km": 0.194, + "x_ohm_per_km": 0.41, + "max_i_ka": 0.47, + "type": "ol", + "q_mm2": 149, + "alpha": 0.00403 + }, + "184-AL1/30-ST1A 110.0": { + "c_nf_per_km": 8.8, + "r_ohm_per_km": 0.1571, + "x_ohm_per_km": 0.4, + "max_i_ka": 0.535, + "type": "ol", + "q_mm2": 184, + "alpha": 0.00403 + }, + "243-AL1/39-ST1A 110.0": { + "c_nf_per_km": 9, + "r_ohm_per_km": 0.1188, + "x_ohm_per_km": 0.39, + "max_i_ka": 0.645, + "type": "ol", + "q_mm2": 243, + "alpha": 0.00403 + }, + "305-AL1/39-ST1A 110.0": { + "c_nf_per_km": 9.2, + "r_ohm_per_km": 0.0949, + "x_ohm_per_km": 0.38, + "max_i_ka": 0.74, + "type": "ol", + "q_mm2": 305, + "alpha": 0.00403 + }, + "490-AL1/64-ST1A 110.0": { + "c_nf_per_km": 9.75, + "r_ohm_per_km": 0.059, + "x_ohm_per_km": 0.37, + "max_i_ka": 0.96, + "type": "ol", + "q_mm2": 490, + "alpha": 0.00403 + }, + "679-AL1/86-ST1A 110.0": { + "c_nf_per_km": 9.95, + "r_ohm_per_km": 0.042, + "x_ohm_per_km": 0.36, + "max_i_ka": 0.115, + "type": "ol", + "q_mm2": 679, + "alpha": 0.00403 + }, + "490-AL1/64-ST1A 220.0": { + "c_nf_per_km": 10, + "r_ohm_per_km": 0.059, + "x_ohm_per_km": 0.285, + "max_i_ka": 0.96, + "type": "ol", + "q_mm2": 490, + "alpha": 0.00403 + }, + "679-AL1/86-ST1A 220.0": { + "c_nf_per_km": 11.7, + "r_ohm_per_km": 0.042, + "x_ohm_per_km": 0.275, + "max_i_ka": 0.115, + "type": "ol", + "q_mm2": 679, + "alpha": 0.00403 + }, + "490-AL1/64-ST1A 380.0": { + "c_nf_per_km": 11, + "r_ohm_per_km": 0.059, + "x_ohm_per_km": 0.253, + "max_i_ka": 0.96, + "type": "ol", + "q_mm2": 490, + "alpha": 0.00403 + }, + "679-AL1/86-ST1A 380.0": { + "c_nf_per_km": 14.6, + "r_ohm_per_km": 0.042, + "x_ohm_per_km": 0.25, + "max_i_ka": 0.115, + "type": "ol", + "q_mm2": 679, + "alpha": 0.00403 + } + }, + "trafo": { + "160 MVA 380/110 kV": { + "i0_percent": 0.06, + "pfe_kw": 60, + "vkr_percent": 0.25, + "sn_mva": 160, + "vn_lv_kv": 110.0, + "vn_hv_kv": 380.0, + "vk_percent": 12.2, + "shift_degree": 0, + "vector_group": "Yy0", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_phase_shifter": false + }, + "100 MVA 220/110 kV": { + "i0_percent": 0.06, + "pfe_kw": 55, + "vkr_percent": 0.26, + "sn_mva": 100, + "vn_lv_kv": 110.0, + "vn_hv_kv": 220.0, + "vk_percent": 12.0, + "shift_degree": 0, + "vector_group": "Yy0", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_phase_shifter": false + }, + "63 MVA 110/20 kV": { + "i0_percent": 0.04, + "pfe_kw": 22, + "vkr_percent": 0.32, + "sn_mva": 63, + "vn_lv_kv": 20.0, + "vn_hv_kv": 110.0, + "vk_percent": 18, + "shift_degree": 150, + "vector_group": "YNd5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_phase_shifter": false + }, + "40 MVA 110/20 kV": { + "i0_percent": 0.05, + "pfe_kw": 18, + "vkr_percent": 0.34, + "sn_mva": 40, + "vn_lv_kv": 20.0, + "vn_hv_kv": 110.0, + "vk_percent": 16.2, + "shift_degree": 150, + "vector_group": "YNd5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_phase_shifter": false + }, + "25 MVA 110/20 kV": { + "i0_percent": 0.07, + "pfe_kw": 14, + "vkr_percent": 0.41, + "sn_mva": 25, + "vn_lv_kv": 20.0, + "vn_hv_kv": 110.0, + "vk_percent": 12, + "shift_degree": 150, + "vector_group": "YNd5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_phase_shifter": false + }, + "63 MVA 110/10 kV": { + "sn_mva": 63, + "vn_hv_kv": 110, + "vn_lv_kv": 10, + "vk_percent": 18, + "vkr_percent": 0.32, + "pfe_kw": 22, + "i0_percent": 0.04, + "shift_degree": 150, + "vector_group": "YNd5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_phase_shifter": false + }, + "40 MVA 110/10 kV": { + "sn_mva": 40, + "vn_hv_kv": 110, + "vn_lv_kv": 10, + "vk_percent": 16.2, + "vkr_percent": 0.34, + "pfe_kw": 18, + "i0_percent": 0.05, + "shift_degree": 150, + "vector_group": "YNd5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_phase_shifter": false + }, + "25 MVA 110/10 kV": { + "sn_mva": 25, + "vn_hv_kv": 110, + "vn_lv_kv": 10, + "vk_percent": 12, + "vkr_percent": 0.41, + "pfe_kw": 14, + "i0_percent": 0.07, + "shift_degree": 150, + "vector_group": "YNd5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_phase_shifter": false + }, + "0.25 MVA 20/0.4 kV": { + "sn_mva": 0.25, + "vn_hv_kv": 20, + "vn_lv_kv": 0.4, + "vk_percent": 6, + "vkr_percent": 1.44, + "pfe_kw": 0.8, + "i0_percent": 0.32, + "shift_degree": 150, + "vector_group": "Yzn5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -2, + "tap_max": 2, + "tap_step_degree": 0, + "tap_step_percent": 2.5, + "tap_phase_shifter": false + }, + "0.4 MVA 20/0.4 kV": { + "sn_mva": 0.4, + "vn_hv_kv": 20, + "vn_lv_kv": 0.4, + "vk_percent": 6, + "vkr_percent": 1.425, + "pfe_kw": 1.35, + "i0_percent": 0.3375, + "shift_degree": 150, + "vector_group": "Dyn5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -2, + "tap_max": 2, + "tap_step_degree": 0, + "tap_step_percent": 2.5, + "tap_phase_shifter": false + }, + "0.63 MVA 20/0.4 kV": { + "sn_mva": 0.63, + "vn_hv_kv": 20, + "vn_lv_kv": 0.4, + "vk_percent": 6, + "vkr_percent": 1.206, + "pfe_kw": 1.65, + "i0_percent": 0.2619, + "shift_degree": 150, + "vector_group": "Dyn5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -2, + "tap_max": 2, + "tap_step_degree": 0, + "tap_step_percent": 2.5, + "tap_phase_shifter": false + }, + "0.25 MVA 10/0.4 kV": { + "sn_mva": 0.25, + "vn_hv_kv": 10, + "vn_lv_kv": 0.4, + "vk_percent": 4, + "vkr_percent": 1.2, + "pfe_kw": 0.6, + "i0_percent": 0.24, + "shift_degree": 150, + "vector_group": "Dyn5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -2, + "tap_max": 2, + "tap_step_degree": 0, + "tap_step_percent": 2.5, + "tap_phase_shifter": false + }, + "0.4 MVA 10/0.4 kV": { + "sn_mva": 0.4, + "vn_hv_kv": 10, + "vn_lv_kv": 0.4, + "vk_percent": 4, + "vkr_percent": 1.325, + "pfe_kw": 0.95, + "i0_percent": 0.2375, + "shift_degree": 150, + "vector_group": "Dyn5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -2, + "tap_max": 2, + "tap_step_degree": 0, + "tap_step_percent": 2.5, + "tap_phase_shifter": false + }, + "0.63 MVA 10/0.4 kV": { + "sn_mva": 0.63, + "vn_hv_kv": 10, + "vn_lv_kv": 0.4, + "vk_percent": 4, + "vkr_percent": 1.0794, + "pfe_kw": 1.18, + "i0_percent": 0.1873, + "shift_degree": 150, + "vector_group": "Dyn5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -2, + "tap_max": 2, + "tap_step_degree": 0, + "tap_step_percent": 2.5, + "tap_phase_shifter": false + } + }, + "trafo3w": { + "63/25/38 MVA 110/20/10 kV": { + "sn_hv_mva": 63, + "sn_mv_mva": 25, + "sn_lv_mva": 38, + "vn_hv_kv": 110, + "vn_mv_kv": 20, + "vn_lv_kv": 10, + "vk_hv_percent": 10.4, + "vk_mv_percent": 10.4, + "vk_lv_percent": 10.4, + "vkr_hv_percent": 0.28, + "vkr_mv_percent": 0.32, + "vkr_lv_percent": 0.35, + "pfe_kw": 35, + "i0_percent": 0.89, + "shift_mv_degree": 0, + "shift_lv_degree": 0, + "vector_group": "YN0yn0yn0", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -10, + "tap_max": 10, + "tap_step_percent": 1.2 + }, + "63/25/38 MVA 110/10/10 kV": { + "sn_hv_mva": 63, + "sn_mv_mva": 25, + "sn_lv_mva": 38, + "vn_hv_kv": 110, + "vn_mv_kv": 10, + "vn_lv_kv": 10, + "vk_hv_percent": 10.4, + "vk_mv_percent": 10.4, + "vk_lv_percent": 10.4, + "vkr_hv_percent": 0.28, + "vkr_mv_percent": 0.32, + "vkr_lv_percent": 0.35, + "pfe_kw": 35, + "i0_percent": 0.89, + "shift_mv_degree": 0, + "shift_lv_degree": 0, + "vector_group": "YN0yn0yn0", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -10, + "tap_max": 10, + "tap_step_percent": 1.2 + } + } + }, + "res_bus": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"vm_pu\",\"va_degree\",\"p_mw\",\"q_mvar\"],\"index\":[0,1,2,3,4],\"data\":[[1.02,-0.845445168673926,0.0,-111.791243672370911],[1.02,0.0,-21.729831330858325,116.839935541152954],[1.019214100496144,-0.409103297622625,0.0,0.0],[1.018637116919488,-0.503470352662766,10.0,7.0],[1.017983079721402,-0.653497665026562,10.0,7.0]]}", + "dtype": { + "vm_pu": "float64", + "va_degree": "float64", + "p_mw": "float64", + "q_mvar": "float64" + }, + "orient": "split" + }, + "res_line": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_from_mw\",\"q_from_mvar\",\"p_to_mw\",\"q_to_mvar\",\"pl_mw\",\"ql_mvar\",\"i_from_ka\",\"i_to_ka\",\"i_ka\",\"vm_from_pu\",\"va_from_degree\",\"vm_to_pu\",\"va_to_degree\",\"loading_percent\"],\"index\":[0,1,2,3,4,5,6,7],\"data\":[[-7.167647147657727,57.480079867900443,8.03525639977348,-60.113463233922118,0.867609252115754,-2.633383366021676,0.327874112511858,0.343286326507116,0.343286326507116,1.02,-0.845445168673926,1.02,0.0,57.214387751185988],[-0.657313913963437,25.969126903729045,0.866078469150186,-29.007927174007612,0.208764555186749,-3.038800270278568,0.147040043868819,0.164393305610081,0.164393305610081,1.02,-0.845445168673926,1.019214100496144,-0.409103297622625,74.724229822763931],[1.64566972119938,15.370129751576128,-1.540268914180618,-19.229415550834709,0.105400807018762,-3.859285799258581,0.087496748884432,0.109338903896103,0.109338903896103,1.02,-0.845445168673926,1.018637116919488,-0.503470352662766,68.336814935064211],[6.179291340421495,12.971907266349552,-6.119076735247816,-15.70424981919658,0.060214605173678,-2.732342552847028,0.081330018729726,0.095589209712924,0.095589209712924,1.02,-0.845445168673926,1.017983079721402,-0.653497665026562,59.743256070577175],[13.694574931085771,-56.726472302863066,-13.283848894885464,55.407854241119566,0.410726036200307,-1.3186180617435,0.330312825878128,0.322760996590474,0.330312825878128,1.02,0.0,1.019214100496144,-0.409103297622625,55.052137646354595],[6.208885212872048,-13.199963533555254,-6.184761786109662,11.833197159642042,0.024123426762386,-1.366766373913212,0.082632108556076,0.075677384410291,0.082632108556076,1.019214100496144,-0.409103297622625,1.018637116919488,-0.503470352662766,27.544036185358689],[6.208885212872048,-13.199963533555254,-6.184761786109662,11.833197159642042,0.024123426762386,-1.366766373913212,0.082632108556076,0.075677384410291,0.082632108556076,1.019214100496144,-0.409103297622625,1.018637116919488,-0.503470352662766,27.544036185358689],[3.909792486391969,-11.436978768449999,-3.88092326475316,8.704249819196738,0.028869221638809,-2.732728949253261,0.068506463438984,0.054050881891821,0.068506463438984,1.018637116919488,-0.503470352662766,1.017983079721402,-0.653497665026562,42.816539649365005]]}", + "dtype": { + "p_from_mw": "float64", + "q_from_mvar": "float64", + "p_to_mw": "float64", + "q_to_mvar": "float64", + "pl_mw": "float64", + "ql_mvar": "float64", + "i_from_ka": "float64", + "i_to_ka": "float64", + "i_ka": "float64", + "vm_from_pu": "float64", + "va_from_degree": "float64", + "vm_to_pu": "float64", + "va_to_degree": "float64", + "loading_percent": "float64" + }, + "orient": "split" + }, + "res_trafo": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_hv_mw\",\"q_hv_mvar\",\"p_lv_mw\",\"q_lv_mvar\",\"pl_mw\",\"ql_mvar\",\"i_hv_ka\",\"i_lv_ka\",\"vm_hv_pu\",\"va_hv_degree\",\"vm_lv_pu\",\"va_lv_degree\",\"loading_percent\"],\"index\":[],\"data\":[]}", + "dtype": { + "p_hv_mw": "float64", + "q_hv_mvar": "float64", + "p_lv_mw": "float64", + "q_lv_mvar": "float64", + "pl_mw": "float64", + "ql_mvar": "float64", + "i_hv_ka": "float64", + "i_lv_ka": "float64", + "vm_hv_pu": "float64", + "va_hv_degree": "float64", + "vm_lv_pu": "float64", + "va_lv_degree": "float64", + "loading_percent": "float64" + }, + "orient": "split" + }, + "res_trafo3w": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_hv_mw\",\"q_hv_mvar\",\"p_mv_mw\",\"q_mv_mvar\",\"p_lv_mw\",\"q_lv_mvar\",\"pl_mw\",\"ql_mvar\",\"i_hv_ka\",\"i_mv_ka\",\"i_lv_ka\",\"vm_hv_pu\",\"va_hv_degree\",\"vm_mv_pu\",\"va_mv_degree\",\"vm_lv_pu\",\"va_lv_degree\",\"va_internal_degree\",\"vm_internal_pu\",\"loading_percent\"],\"index\":[],\"data\":[]}", + "dtype": { + "p_hv_mw": "float64", + "q_hv_mvar": "float64", + "p_mv_mw": "float64", + "q_mv_mvar": "float64", + "p_lv_mw": "float64", + "q_lv_mvar": "float64", + "pl_mw": "float64", + "ql_mvar": "float64", + "i_hv_ka": "float64", + "i_mv_ka": "float64", + "i_lv_ka": "float64", + "vm_hv_pu": "float64", + "va_hv_degree": "float64", + "vm_mv_pu": "float64", + "va_mv_degree": "float64", + "vm_lv_pu": "float64", + "va_lv_degree": "float64", + "va_internal_degree": "float64", + "vm_internal_pu": "float64", + "loading_percent": "float64" + }, + "orient": "split" + }, + "res_impedance": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_from_mw\",\"q_from_mvar\",\"p_to_mw\",\"q_to_mvar\",\"pl_mw\",\"ql_mvar\",\"i_from_ka\",\"i_to_ka\"],\"index\":[],\"data\":[]}", + "dtype": { + "p_from_mw": "float64", + "q_from_mvar": "float64", + "p_to_mw": "float64", + "q_to_mvar": "float64", + "pl_mw": "float64", + "ql_mvar": "float64", + "i_from_ka": "float64", + "i_to_ka": "float64" + }, + "orient": "split" + }, + "res_ext_grid": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64" + }, + "orient": "split" + }, + "res_load": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[0,1,2],\"data\":[[10.0,7.0],[10.0,7.0],[10.0,7.0]]}", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64" + }, + "orient": "split" + }, + "res_sgen": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64" + }, + "orient": "split" + }, + "res_storage": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64" + }, + "orient": "split" + }, + "res_shunt": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"vm_pu\"],\"index\":[],\"data\":[]}", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64", + "vm_pu": "float64" + }, + "orient": "split" + }, + "res_gen": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"va_degree\",\"vm_pu\"],\"index\":[0,1],\"data\":[[10.0,118.791243672370911,-0.845445168673926,1.02],[21.729831330858325,-116.839935541152954,0.0,1.02]]}", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64", + "va_degree": "float64", + "vm_pu": "float64" + }, + "orient": "split" + }, + "res_ward": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"vm_pu\"],\"index\":[],\"data\":[]}", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64", + "vm_pu": "float64" + }, + "orient": "split" + }, + "res_xward": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"vm_pu\",\"va_internal_degree\",\"vm_internal_pu\"],\"index\":[],\"data\":[]}", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64", + "vm_pu": "float64", + "va_internal_degree": "float64", + "vm_internal_pu": "float64" + }, + "orient": "split" + }, + "res_dcline": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_from_mw\",\"q_from_mvar\",\"p_to_mw\",\"q_to_mvar\",\"pl_mw\",\"vm_from_pu\",\"va_from_degree\",\"vm_to_pu\",\"va_to_degree\"],\"index\":[],\"data\":[]}", + "dtype": { + "p_from_mw": "float64", + "q_from_mvar": "float64", + "p_to_mw": "float64", + "q_to_mvar": "float64", + "pl_mw": "float64", + "vm_from_pu": "float64", + "va_from_degree": "float64", + "vm_to_pu": "float64", + "va_to_degree": "float64" + }, + "orient": "split" + }, + "user_pf_options": {}, + "OPF_converged": false + } +} diff --git a/grid2op/data/rte_case5_flexibility/grid_layout.json b/grid2op/data/rte_case5_flexibility/grid_layout.json new file mode 100644 index 00000000..05780856 --- /dev/null +++ b/grid2op/data/rte_case5_flexibility/grid_layout.json @@ -0,0 +1,22 @@ +{ + "sub_0": [ + 0.0, + 0.0 + ], + "sub_1": [ + 0.0, + 400.0 + ], + "sub_2": [ + 200.0, + 400.0 + ], + "sub_3": [ + 400.0, + 400.0 + ], + "sub_4": [ + 400.0, + 0.0 + ] +} \ No newline at end of file diff --git a/grid2op/data/rte_case5_flexibility/params.json b/grid2op/data/rte_case5_flexibility/params.json new file mode 100644 index 00000000..124f5f23 --- /dev/null +++ b/grid2op/data/rte_case5_flexibility/params.json @@ -0,0 +1 @@ +{"NB_TIMESTEP_TOPOLOGY_REMODIF": 19} \ No newline at end of file diff --git a/grid2op/data/rte_case5_flexibility/prods_charac.csv b/grid2op/data/rte_case5_flexibility/prods_charac.csv new file mode 100644 index 00000000..d82fbbad --- /dev/null +++ b/grid2op/data/rte_case5_flexibility/prods_charac.csv @@ -0,0 +1,3 @@ +Pmax,Pmin,name,type,bus,max_ramp_up,max_ramp_down,min_up_time,min_down_time,marginal_cost,shut_down_cost,start_cost,x,y,V +10,0.0,gen_0_0,wind,5,0,0,0,0,0,0,0,0,0,102. +30,0.0,gen_1_1,thermal,0,10,10,4,4,70,1,2,0,400,102. \ No newline at end of file diff --git a/grid2op/data_test/rte_case5_flexibility/__pycache__/config.cpython-310.pyc b/grid2op/data_test/rte_case5_flexibility/__pycache__/config.cpython-310.pyc deleted file mode 100644 index 14f1f7494d035fd9e4d508e6275fef0320b8ea6e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 719 zcmZ8eL5|Zf6m^=m$)stAS%8p`Sg`A6B8|ijp@A87HZWA}0x6ryjelE%9arB@huLxt zF2EhQ0XN8&6&GN^itjWL2wQR9d;WfY`ZnWn!e~7C_I>xc$Jh@~{v9JCm-N{UErN+a zF&*SV;Bim&beM+?4pmudgi_p2lR8=-ah>w!;#Hxz zwe^`=cGx9CwvXx3KTECXhdU z)jH#VWE}dv)2_bl3`SlZQq+yyer5RBSNi4mGTRvqnSiQ*?AjEShSJ$=j^+at1a5r9 zcl)e0%mjBlLq|Q}7M`y*3O>oTRMOpNNB8sneI38hMo=$b26TmeMoY@lpqst07yLhy GUw;4_*V>o> diff --git a/grid2op/data_test/rte_case5_flexibility/__pycache__/config.cpython-312.pyc b/grid2op/data_test/rte_case5_flexibility/__pycache__/config.cpython-312.pyc deleted file mode 100644 index ebdc7bd510efa68210d0064f07797e8cfc84b6b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 748 zcmYLF%Wl&^6dlKj?K+RPQX8PqqO=ks3u>tnY(SNuRl0!^r3)l)qKW4w8JO`z<4H=E z@DptK0setM=nq(yvSP)qs5@57IJOwS&N*}Mnd5uYZ2CyAhd*zoUn>axGNfGf0^E%? z_=X4~7DL<$Ez95vt8hECb8NFJuZ6W7SDC}z(9Lm8V=we_?63{)hrWft2aC91Lk(Q; zh?`&DcR#Ez-R{P3b7we{n IBlon$KUezh(*OVf