diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3caea606e..f38e9b547 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,10 +3,11 @@ Change Log [TODO] -------------------- +- [???] example (and test) on how to corrupt the observation for the agent (without corrupting the environment) +- [???] use some kind of "env.get_state()" when simulating instead of recoding everything "by hand" - [???] use "backend.get_action_to_set()" in simulate - [???] use the prod_p_forecasted and co in the "next_chronics" of simulate -- [???] add the storage power in the backend.get_action_to_set()" -- [???] add a "_cst_" or something in the `const` member of all the class +- [???] add a "_cst_" or something in the `const` member of all the classes - [???] in deepcopy of env, make tests that the "pointers" are properly propagated in the attributes (for example `envcpy._game_rules.legal_action` should not be copied when building `envcpy._helper_action_env`) - [???] add multi agent @@ -29,12 +30,42 @@ Change Log - [???] "asynch" multienv - [???] properly model interconnecting powerlines +[1.6.5] - 2022-xx-yy +--------------------- +- [BREAKING] the name of the python files for the "Chronics" module are now lowercase (complient with PEP). If you + did things like `from grid2op.Chronics.ChangeNothing import ChangeNothing` you need to change it like + `from grid2op.Chronics.changeNothing import ChangeNothing` or even better, and this is the preferred way to include + them: `from grid2op.Chronics import ChangeNothing`. It should not affect lots of code (more refactoring of the kind + are to be expected in following versions). +- [BREAKING] same as above for the "Observation" module. It should not affect lots of code (more refactoring of the kind + are to be expected in following versions). +- [FIXED] an issue when copying the environment with the opponent (see issue https://github.com/rte-france/Grid2Op/issues/274) +- [FIXED] a bug leading to the wrong "backend.get_action_to_set()" when there were storage units on the grid. +- [FIXED] a bug in the "BackendConverter" when there are storage on the grid +- [FIXED] issue https://github.com/rte-france/Grid2Op/issues/265 +- [FIXED] issue https://github.com/rte-france/Grid2Op/issues/261 +- [ADDED] possibility to "env.set_id" by giving only the folder of the chronics and not the whole path. +- [ADDED] function "env.chronics_handler.available_chronics()" to return the list of available chronics + for a given environment +- [ADDED] possibility, through the `Parameters` class, to limit the number of possible calls to `obs.simulate(...)` + see `param.MAX_SIMULATE_PER_STEP` and `param.MAX_SIMULATE_PER_EPISODE` (see issue https://github.com/rte-france/Grid2Op/issues/273) +- [ADDED] a class to generate a "Chronics" readable by grid2op from numpy arrays (see https://github.com/rte-france/Grid2Op/issues/271) +- [ADDED] an attribute `delta_time` in the observation that tells the time (in minutes) between two consecutive steps. +- [ADDED] a method of the action space to show a list of actions to get back to the original topology + (see https://github.com/rte-france/Grid2Op/issues/275) + `env.action_space.get_back_to_ref_state(obs)` +- [ADDED] a method of the action to store it in a grid2op independant fashion (using json and dictionaries), see `act.as_serializable_dict()` +- [ADDED] possibility to generate a gym `DiscreteActSpace` from a given list of actions (see https://github.com/rte-france/Grid2Op/issues/277) +- [IMPROVED] observation now raises `Grid2OpException` instead of `RuntimeError` +- [IMRPOVED] docs (and notebooks) for the "split_train_val" https://github.com/rte-france/Grid2Op/issues/269 +- [IMRPOVED] the "split_train_val" function to also generate a test dataset see https://github.com/rte-france/Grid2Op/issues/276 + [1.6.4] - 2021-11-08 --------------------- -- [BREAKING] the name of the python file for the "agent" module are now lowercase (complient with PEP). If you +- [BREAKING] the name of the python files for the "agent" module are now lowercase (complient with PEP). If you did things like `from grid2op.Agent.BaseAgent import BaseAgent` you need to change it like `from grid2op.Agent.baseAgent import BaseAgent` or even better, and this is the preferred way to include - them: `from grid2op.Agent import BaseAgent` It should not affect lots of code. + them: `from grid2op.Agent import BaseAgent`. It should not affect lots of code. - [FIXED] a bug where the shunt had a voltage when disconnected using pandapower backend - [FIXED] a bug preventing to print the action space if some "part" of it had no size (empty action space) - [FIXED] a bug preventing to copy an action properly (especially for the alarm) diff --git a/_profiling/utils_benchmark.py b/_profiling/utils_benchmark.py index 4b3908a37..e8a732b83 100644 --- a/_profiling/utils_benchmark.py +++ b/_profiling/utils_benchmark.py @@ -189,7 +189,7 @@ def run_env(env, max_ts, agent): reward = env.reward_range[0] nb_ts = 0 prev_act = None - beg_ = time.time() + beg_ = time.perf_counter() with tqdm(total=nb_rows) as pbar: while not done: act = agent.act(obs, reward, done) @@ -206,7 +206,7 @@ def run_env(env, max_ts, agent): prev_act = act # if done: # print(act) - end_ = time.time() + end_ = time.perf_counter() total_time = end_ - beg_ return nb_ts, total_time, aor, gen_p, gen_q @@ -222,7 +222,7 @@ def run_env_with_reset(env, max_ts, agent, seed=None): done = False reward = env.reward_range[0] nb_ts = 0 - beg_ = time.time() + beg_ = time.perf_counter() reset_count = 0 with tqdm(total=nb_rows) as pbar: while not done: @@ -242,7 +242,7 @@ def run_env_with_reset(env, max_ts, agent, seed=None): obs = env.reset() reset_count += 1 done = False - end_ = time.time() + end_ = time.perf_counter() total_time = end_ - beg_ return nb_ts, total_time, aor, gen_p, gen_q, reset_count diff --git a/docs/action.rst b/docs/action.rst index 4ee1aab2b..2eea62359 100644 --- a/docs/action.rst +++ b/docs/action.rst @@ -359,6 +359,8 @@ An action can be incorrect because of two main factors: Ambiguous or Illegal, the action will be replaced by a "do nothing" without any other incidents on the game. +.. _action_powerline_status: + Note on powerline status ------------------------ As of grid2op version 1.2.0, we attempted to clean and rationalize the API concerning the change of diff --git a/docs/environment.rst b/docs/environment.rst index 7ab1d53a9..aa9d920e2 100644 --- a/docs/environment.rst +++ b/docs/environment.rst @@ -550,11 +550,12 @@ This can be done with: # extract 1% of the "chronics" to be used in the validation environment. The other 99% will # be used for test - nm_env_train, nm_env_val = env.train_val_split_random(pct_val=1.) + nm_env_train, nm_env_val, nm_env_test = env.train_val_split_random(pct_val=1., pct_test=1.) # and now you can use the training set only to train your agent: print(f"The name of the training environment is \\"{nm_env_train}\\"") print(f"The name of the validation environment is \\"{nm_env_val}\\"") + print(f"The name of the test environment is \\"{nm_env_test}\\"") env_train = grid2op.make(nm_env_train) You can then use, in the above case: @@ -577,67 +578,9 @@ And then, at time of validation: env_val = grid2op.make(env_name+"_val") # to only use the "validation chronics" # do whatever you want with env_val - -As of now, grid2op do not support "from the API" the possibility to split with convenient -names a environment a second times. If you want to do a "train / validation / test" split we recommend you to: - -1. make a training / test split (see below) -2. split again the training set into training / validation (see below) -3. you will have locally an environment named "trainval" on your computer. This directory will not weight - more than a few kilobytes. - -The example, not really convenient at the moment, please find a feature request if that is a problem for -you: - -.. code-block:: python - - import grid2op - import os - - env_name = "l2rpn_case14_sandbox" # or any other... - env = grid2op.make(env_name) - - # retrieve the names of the chronics: - full_path_data = env.chronics_handler.subpaths - chron_names = [os.path.split(el)[-1] for el in full_path_data] - - # splitting into training / test, keeping the "last" 10 chronics to the test set - nm_env_trainval, nm_env_test = env.train_val_split(val_scen_id=chron_names[-10:], - add_for_val="test", - add_for_train="trainval") - - # now splitting again the training set into training and validation, keeping the last 10 chronics - # of this environment for validation - env_trainval = grid2op.make(nm_env_trainval) # create the "trainval" environment - full_path_data = env_trainval.chronics_handler.subpaths - chron_names = [os.path.split(el)[-1] for el in full_path_data] - nm_env_train, nm_env_val = env_trainval.train_val_split(val_scen_id=chron_names[-10:], - remove_from_name="_trainval$") - -And later on, you can do, if you followed the names above: - -.. code-block:: python - - import grid2op - import os - - env_name = "l2rpn_case14_sandbox" # or any other... - env_train = grid2op.make(env_name+"_train") - env_val = grid2op.make(env_name+"_val") + # and of course env_test = grid2op.make(env_name+"_test") -And you can also, if you want, delete the folder "l2rpn_case14_sandbox_trainval" from your machine: - -.. code-block:: python - - import grid2op - import os - - env_name = "l2rpn_case14_sandbox" # or any other... - env_trainval = grid2op.make(env_name+"_trainval") - print(f"You can safely delete, if you want, the folder: \n\t\"{env_trainval.get_path_env()}\" \nnow useless.") - - Customization ------------- diff --git a/docs/gym.rst b/docs/gym.rst index 6e1c54cbb..f68a4f8cc 100644 --- a/docs/gym.rst +++ b/docs/gym.rst @@ -103,6 +103,7 @@ For example, an observation space will look like: - "v_ex": Box(`env.n_line`,) [type: float, low: 0, high: inf] - "v_or": Box(`env.n_line`,) [type: flaot, low: 0, high: inf] - "year": Discrete(2100) +- "delta_time": Box(0.0, inf, (1,), float32) Each keys correspond to an attribute of the observation. In this example `"line_status": MultiBinary(20)` represents the attribute `obs.line_status` which is a boolean vector (for each powerline @@ -320,6 +321,8 @@ Reinforcement learning frameworks TODO +Any contribution is welcome here + Other frameworks ********************** Any contribution is welcome here diff --git a/docs/observation.rst b/docs/observation.rst index e386adf3d..ef2700506 100644 --- a/docs/observation.rst +++ b/docs/observation.rst @@ -49,6 +49,7 @@ .. _attention_budget: ./observation.html#grid2op.Observation.BaseObservation.attention_budget .. _max_step: ./observation.html#grid2op.Observation.BaseObservation.max_step .. _current_step: ./observation.html#grid2op.Observation.BaseObservation.current_step +.. _delta_time: ./observation.html#grid2op.Observation.BaseObservation.delta_time .. _observation_module: @@ -131,6 +132,7 @@ Name(s) `last_alarm`_ int `dim_alarms`_ `attention_budget`_ int 1 `max_step`_ , `current_step`_ int 1 +`delta_time`_ float 1 ============================================================================= ========= ============ (*NB* for concision, if a coma ("*,*") is present in the "Name(s)" part of the column, it means multiple attributes diff --git a/getting_started/03_Action.ipynb b/getting_started/03_Action.ipynb index 42f4abbe8..ac29487e7 100644 --- a/getting_started/03_Action.ipynb +++ b/getting_started/03_Action.ipynb @@ -624,7 +624,7 @@ "outputs": [], "source": [ "curtail_act2 = action_space()\n", - "curtail_act2.curtail = [(gen_id, amount)]\n", + "curtail_act2.curtail = [(gen_id, ratio_curtailment)]\n", "print(curtail_act2)" ] }, @@ -1346,7 +1346,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.8.10" } }, "nbformat": 4, diff --git a/getting_started/04_TrainingAnAgent.ipynb b/getting_started/04_TrainingAnAgent.ipynb index d87d5340c..d43028d93 100644 --- a/getting_started/04_TrainingAnAgent.ipynb +++ b/getting_started/04_TrainingAnAgent.ipynb @@ -84,6 +84,50 @@ "res" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 0) Good practice\n", + "\n", + "As in other machine learning tasks, we highly recommend, before even trying to train an agent, to split the \"chronics\" (ie the episode data) into 3 datasets:\n", + "- \"train\" use to train the agent\n", + "- \"val\" use to validate the hyper parameters\n", + "- \"test\" at which you would look only once to report the agent performance in a scientific paper (for example)\n", + "\n", + "Grid2op lets you do that with relative ease:\n", + "\n", + "```python\n", + "import grid2op\n", + "env_name = \"l2rpn_case14_sandbox\" # or any other...\n", + "env = grid2op.make(env_name)\n", + "\n", + "# extract 1% of the \"chronics\" to be used in the validation environment. The other 99% will\n", + "# be used for test\n", + "nm_env_train, nm_env_val, nm_env_test = env.train_val_split_random(pct_val=1., pct_test=1.)\n", + "\n", + "# and now you can use the training set only to train your agent:\n", + "print(f\"The name of the training environment is \\\\\"{nm_env_train}\\\\\"\")\n", + "print(f\"The name of the validation environment is \\\\\"{nm_env_val}\\\\\"\")\n", + "print(f\"The name of the test environment is \\\\\"{nm_env_test}\\\\\"\")\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And now, you can use the training environment to train your agent:\n", + "\n", + "```python\n", + "import grid2op\n", + "env_name = \"l2rpn_case14_sandbox\"\n", + "env = grid2op.make(env_name+\"_train\")\n", + "```\n", + "\n", + "Be carefull, on windows you might run into issues. Don't hesitate to have a look at the documentation of this funciton if this the case (see https://grid2op.readthedocs.io/en/latest/environment.html#grid2op.Environment.Environment.train_val_split and https://grid2op.readthedocs.io/en/latest/environment.html#grid2op.Environment.Environment.train_val_split_random)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -1004,7 +1048,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.8.10" } }, "nbformat": 4, diff --git a/getting_started/11_IntegrationWithExistingRLFrameworks.ipynb b/getting_started/11_IntegrationWithExistingRLFrameworks.ipynb index e27aed5c4..ae9a28898 100644 --- a/getting_started/11_IntegrationWithExistingRLFrameworks.ipynb +++ b/getting_started/11_IntegrationWithExistingRLFrameworks.ipynb @@ -95,6 +95,41 @@ "source": [ "## 0) Recommended initial steps\n", "\n", + "\n", + "### Split the environment into training, validation and test\n", + "\n", + "As in other machine learning tasks, we highly recommend, before even trying to train an agent, to split the \"chronics\" (ie the episode data) into 3 datasets:\n", + "- \"train\" use to train the agent\n", + "- \"val\" use to validate the hyper parameters\n", + "- \"test\" at which you would look only once to report the agent performance in a scientific paper (for example)\n", + "\n", + "Grid2op lets you do that with relative ease:\n", + "\n", + "```python\n", + "import grid2op\n", + "env_name = \"l2rpn_case14_sandbox\" # or any other...\n", + "env = grid2op.make(env_name)\n", + "\n", + "# extract 1% of the \"chronics\" to be used in the validation environment. The other 99% will\n", + "# be used for test\n", + "nm_env_train, nm_env_val, nm_env_test = env.train_val_split_random(pct_val=1., pct_test=1.)\n", + "\n", + "# and now you can use the training set only to train your agent:\n", + "print(f\"The name of the training environment is \\\\\"{nm_env_train}\\\\\"\")\n", + "print(f\"The name of the validation environment is \\\\\"{nm_env_val}\\\\\"\")\n", + "print(f\"The name of the test environment is \\\\\"{nm_env_test}\\\\\"\")\n", + "```\n", + "\n", + "And now, you can use the training environment to train your agent:\n", + "\n", + "```python\n", + "import grid2op\n", + "env_name = \"l2rpn_case14_sandbox\"\n", + "env = grid2op.make(env_name+\"train\")\n", + "```\n", + "\n", + "Be carefull, on windows you might run into issues. Don't hesitate to have a look at the documentation of this funciton if this the case (see https://grid2op.readthedocs.io/en/latest/environment.html#grid2op.Environment.Environment.train_val_split and https://grid2op.readthedocs.io/en/latest/environment.html#grid2op.Environment.Environment.train_val_split_random)\n", + "\n", "### Create a grid2op environment\n", "\n", "This is a rather standard step, with lots of inspiration drawn from openAI gym framework, and there is absolutely no specificity here." @@ -108,7 +143,10 @@ "source": [ "import grid2op\n", "env_name = \"l2rpn_case14_sandbox\"\n", - "env_glop = grid2op.make(env_name, test=True) # NOTE: do not set the flag \"test=True\" for a real usage !\n", + "env_glop = grid2op.make(env_name, test=True)\n", + "# NOTE: do not set the flag \"test=True\" for a real usage !\n", + "# NOTE: use grid2op.make(env_name+\"_train\", test=True) for a real usage (see paragraph above !)\n", + "\n", "# This flag is here for testing purpose !!!\n", "obs_glop = env_glop.reset()\n", "obs_glop" @@ -219,6 +257,40 @@ "env_gym.action_space" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You also have the possibility to use other types of more common action space.\n", + "\n", + "#### More customization for the action space\n", + "\n", + "For example, just like in most Atari Games, you can encode each unary action by an integer (for example \"0\" might be \"turn left\", \"1\" \"turn right\" etc.) and have you argent predict the ID of the action instead of its complex form.\n", + "\n", + "This action space will \"automatically\" transform continuous actions into discrete by \"binning\" (more information on the official documentation for example here )\n", + "\n", + "This can be achieved with:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from grid2op.gym_compat import DiscreteActSpace\n", + "env_gym.action_space = DiscreteActSpace(env_gym.init_env.action_space)\n", + "print(f\"There are {env_gym.action_space.n} independant actions\")\n", + "env_gym.action_space" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can customize it even more, for example if you have at your disposal a list of grid2op actions you want to use (and not use the other one, this is explained in the documentation)." + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -459,6 +531,13 @@ " # 4. specific to rllib\n", " self.action_space = self.env_gym.action_space\n", " self.observation_space = self.env_gym.observation_space\n", + " \n", + " # 4. bis: to avoid other type of issues, we recommend to build the action space and observation\n", + " # space directly from the spaces class.\n", + " d = {k: v for k, v in self.env_gym.observation_space.spaces.items()}\n", + " self.observation_space = gym.spaces.Dict(d)\n", + " a = {k: v for k, v in self.env_gym.action_space.items()}\n", + " self.action_space = gym.spaces.Dict(a)\n", "\n", " def reset(self):\n", " obs = self.env_gym.reset()\n", @@ -493,6 +572,7 @@ "source": [ "if nb_step_train: # remember: don't forge to change this number to perform an actual training !\n", " from ray.rllib.agents import ppo # import the type of agents\n", + " # nb_step_train = 100 # Do not forget to turn on the actual training !\n", " # fist initialize ray\n", " ray.init()\n", " try:\n", diff --git a/grid2op/Action/BaseAction.py b/grid2op/Action/BaseAction.py index 2a6ae97cb..b1c38cac4 100644 --- a/grid2op/Action/BaseAction.py +++ b/grid2op/Action/BaseAction.py @@ -479,6 +479,75 @@ def __deepcopy__(self, memodict={}): return res + def as_serializable_dict(self): + """ + This method returns an action as a dictionnary, that can be serialized using the "json" module. + + It can be used to store the action into a grid2op indepependant format (the default action serialization, for speed, writes actions to numpy array. + The size of these arrays can change depending on grid2op versions, especially if some different types of actions are implemented). + + Once you have these dictionnary, you can use them to build back the action from the action space. + + Examples + --------- + + It can be used like: + + .. code-block:: python + + import grid2op + env_name = "l2rpn_case14_sandbox" # or anything else + env = grid2op.make(env_name) + + act = env.action_space(...) + + dict_ = act.as_serializable_dict() # you can save this dict with the json library + act2 = env.action_space(dict_) + act == act2 + + """ + res = {} + # bool elements + if self._modif_alarm: + res["raise_alarm"] = [int(id_) for id_, val in enumerate(self._raise_alarm) if val ] + if self._modif_change_bus: + res["change_bus"] = [int(id_) for id_, val in enumerate(self._change_bus_vect) if val ] + if self._modif_change_status: + res["change_line_status"] = [int(id_) for id_, val in enumerate(self._switch_line_status) if val ] + # int elements + if self._modif_set_bus: + res["set_bus"] = [(int(id_), int(val)) for id_, val in enumerate(self._set_topo_vect) if val != 0 ] + if self._modif_set_status: + res["set_line_status"] = [(int(id_), int(val)) for id_, val in enumerate(self._set_line_status) if val != 0 ] + # float elements + if self._modif_redispatch: + res["redispatch"] = [(int(id_), float(val)) for id_, val in enumerate(self._redispatch) if val != 0.] + if self._modif_storage: + res["set_storage"] = [(int(id_), float(val)) for id_, val in enumerate(self._storage_power) if val != 0.] + if self._modif_curtailment: + res["curtail"] = [(int(id_), float(val)) for id_, val in enumerate(self._curtail) if val != -1] + + # more advanced options + if self._modif_inj: + res["injection"] = {} + for ky in ["prod_p", "prod_v", "load_p", "load_q"]: + if ky in self._dict_inj: + res["injection"][ky] = [float(val) for val in self._dict_inj[ky]] + if not res["injection"]: + del res["injection"] + + if type(self).shunts_data_available: + res["shunt"] = {} + if np.any(np.isfinite(self.shunt_p)): + res["shunt"]["shunt_p"] = [(int(sh_id), float(val)) for sh_id, val in enumerate(self.shunt_p)] + if np.any(np.isfinite(self.shunt_q)): + res["shunt"]["shunt_q"] = [(int(sh_id), float(val)) for sh_id, val in enumerate(self.shunt_q)] + if np.any(self.shunt_bus != 0): + res["shunt"]["shunt_bus"] = [(int(sh_id), int(val)) for sh_id, val in enumerate(self.shunt_bus) if val != 0] + if not res["shunt"]: + del res["shunt"] + return res + @classmethod def _add_shunt_data(cls): if cls.shunt_added is False and cls.shunts_data_available: @@ -1080,7 +1149,7 @@ def __iadd__(self, other): me_change[other_set != 0 & me_change] = False # i set, but the other change, set to the opposite - inverted_set = other_change & (me_set != 0) + inverted_set = other_change & (me_set > 0) # so change +1 becomes +2 and +2 becomes +1 me_set[inverted_set] -= 1 # 1 becomes 0 and 2 becomes 1 me_set[inverted_set] *= -1 # 1 is 0 and 2 becomes -1 @@ -1244,7 +1313,6 @@ def _digest_shunt(self, dict_): warn = "The key {} is not recognized by BaseAction when trying to modify the shunt.".format(k) warn += " Recognized keys are {}".format(sorted(key_shunt_reco)) warnings.warn(warn) - for key_n, vect_self in zip(["shunt_bus", "shunt_p", "shunt_q", "set_bus"], [self.shunt_bus, self.shunt_p, self.shunt_q, self.shunt_bus]): if key_n in ddict_: @@ -1261,7 +1329,6 @@ def _digest_shunt(self, dict_): raise AmbiguousAction("Invalid shunt id {}. Shunt id should be less than the number " "of shunt {}".format(sh_id, self.n_shunt)) vect_self[sh_id] = new_bus - elif tmp is None: pass else: @@ -1277,6 +1344,7 @@ def _digest_injection(self, dict_): for k in tmp_d: if k in self.attr_list_set: self._dict_inj[k] = np.array(tmp_d[k]).astype(dt_float) + # TODO check the size based on the input data ! else: warn = "The key {} is not recognized by BaseAction when trying to modify the injections." \ "".format(k) @@ -3919,7 +3987,7 @@ def redispatch(self): Examples -------- - To retrieve the impact of the action on the storage unit, you can do: + To retrieve the impact of the action on the generator unit, you can do: .. code-block:: python @@ -3949,7 +4017,7 @@ def redispatch(self): # method 3: provide a list of the units you want to modify act.redispatch = [(1, 2.5), (0, -1.3)] - # method 4: change the storage unit by their name with a dictionary + # method 4: change the generators by their name with a dictionary act.redispatch = {"gen_1_0": 2.0} .. note:: The "rule of thumb" to perform redispatching is to provide always diff --git a/grid2op/Action/SerializableActionSpace.py b/grid2op/Action/SerializableActionSpace.py index e19f9bbf0..56f604870 100644 --- a/grid2op/Action/SerializableActionSpace.py +++ b/grid2op/Action/SerializableActionSpace.py @@ -6,8 +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. +import warnings import numpy as np import itertools +from typing import Dict, List from grid2op.dtypes import dt_int, dt_float, dt_bool from grid2op.Exceptions import AmbiguousAction, Grid2OpException @@ -154,9 +156,9 @@ def supports_type(self, action_type): f"You provided {action_type} which is not supported." if action_type == "storage_power": - return (self.n_storage > 0) and ("storage_power" in self.actionClass.authorized_keys) + return (self.n_storage > 0) and ("set_storage" in self.actionClass.authorized_keys) elif action_type == "set_storage": - return (self.n_storage > 0) and ("storage_power" in self.actionClass.authorized_keys) + return (self.n_storage > 0) and ("set_storage" in self.actionClass.authorized_keys) elif action_type == "curtail_mw": return "curtail" in self.actionClass.authorized_keys else: @@ -1126,3 +1128,226 @@ def _custom_deepcopy_for_copy(self, new_obj): # SerializableObservationSpace new_obj.actionClass = self.subtype new_obj._template_act = self.actionClass() + + def _aux_get_back_to_ref_state_curtail(self, res, obs): + is_curtailed = obs.curtailment_limit != 1.0 + if np.any(is_curtailed): + res["curtailment"] = [] + if not self.supports_type("curtail"): + warnings.warn("A generator is is curtailed, but you cannot perform curtailment action. Impossible to get back to the original topology.") + return + + curtail = np.full(obs.n_gen, fill_value=np.NaN) + curtail[is_curtailed] = 1.0 + act = self.actionClass() + act.curtail = curtail + res["curtailment"].append(act) + + def _aux_get_back_to_ref_state_line(self, res, obs): + disc_lines = ~obs.line_status + if np.any(disc_lines): + li_disc = np.where(disc_lines)[0] + res["powerline"] = [] + for el in li_disc: + act = self.actionClass() + if self.supports_type("set_line_status"): + act.set_line_status = [(el, +1)] + elif self.supports_type("change_line_status"): + act.change_line_status = [el] + else: + warnings.warn("A powerline is disconnected by you cannot reconnect it with your action space. Impossible to get back to the original topology") + break + res["powerline"].append(act) + + def _aux_get_back_to_ref_state_sub(self, res, obs): + not_on_bus_1 = obs.topo_vect > 1 # disconnected lines are handled above + if np.any(not_on_bus_1): + res["substation"] = [] + subs_changed = type(self).grid_objects_types[not_on_bus_1, type(self).SUB_COL] + for sub_id in set(subs_changed): + nb_el : int = type(self).sub_info[sub_id] + act = self.actionClass() + if self.supports_type("set_bus"): + act.sub_set_bus = [(sub_id, np.ones(nb_el, dtype=dt_int))] + elif self.supports_type("change_bus"): + arr_ = np.full(nb_el, fill_value=False, dtype=dt_bool) + changed = obs.state_of(substation_id=sub_id)["topo_vect"] >= 1 + arr_[changed] = True + act.sub_change_bus = [(sub_id, arr_)] + else: + warnings.warn("A substation is is not on its original topology (all at bus 1) and your action type does not allow to change it. " + "Impossible to get back to the original topology.") + break + res["substation"].append(act) + + def _aux_get_back_to_ref_state_redisp(self, res, obs, precision=1e-5): + # TODO this is ugly, probably slow and could definitely be optimized + notredisp_setpoint = obs.target_dispatch != 0.0 + if np.any(notredisp_setpoint): + need_redisp = np.where(notredisp_setpoint)[0] + res["redispatching"] = [] + # combine generators and do not exceed ramps (up or down) + rem = np.zeros(self.n_gen, dtype=dt_float) + nb_ = np.zeros(self.n_gen, dtype=dt_int) + for gen_id in need_redisp: + if obs.target_dispatch[gen_id] > 0.: + div_ = obs.target_dispatch[gen_id] / obs.gen_max_ramp_down[gen_id] + else: + div_ = -obs.target_dispatch[gen_id] / obs.gen_max_ramp_up[gen_id] + div_ = np.round(div_, precision) + nb_[gen_id] = int(div_) + if div_ != int(div_): + if obs.target_dispatch[gen_id] > 0.: + rem[gen_id] = obs.target_dispatch[gen_id] - obs.gen_max_ramp_down[gen_id] * nb_[gen_id] + else: + rem[gen_id] = - obs.target_dispatch[gen_id] - obs.gen_max_ramp_up[gen_id] * nb_[gen_id] + nb_[gen_id] += 1 + # now create the proper actions + for nb_act in range(np.max(nb_)): + act = self.actionClass() + if not self.supports_type("redispatch"): + warnings.warn("Some redispatching are set, but you cannot modify it with your action type. Impossible to get back to the original topology.") + break + reds = np.zeros(self.n_gen, dtype=dt_float) + for gen_id in need_redisp: + if nb_act >= nb_[gen_id]: + # nothing to add for this generator in this case + continue + if obs.target_dispatch[gen_id] > 0.: + if nb_act < nb_[gen_id] - 1 or (rem[gen_id] == 0. and nb_act == nb_[gen_id] - 1): + reds[gen_id] = - obs.gen_max_ramp_down[gen_id] + else: + reds[gen_id] = - rem[gen_id] + else: + if nb_act < nb_[gen_id] - 1 or (rem[gen_id] == 0. and nb_act == nb_[gen_id] - 1): + reds[gen_id] = obs.gen_max_ramp_up[gen_id] + else: + reds[gen_id] = rem[gen_id] + + act.redispatch = [(gen_id, red_) for gen_id, red_ in zip(need_redisp, reds)] + res["redispatching"].append(act) + + def _aux_get_back_to_ref_state_storage(self, res, obs, storage_setpoint, precision=5): + # TODO this is ugly, probably slow and could definitely be optimized + # TODO refacto with the redispatching + notredisp_setpoint = obs.storage_charge / obs.storage_Emax != storage_setpoint + delta_time_hour = dt_float(obs.delta_time / 60.) + if np.any(notredisp_setpoint): + need_ajust = np.where(notredisp_setpoint)[0] + res["storage"] = [] + # combine storage units and do not exceed maximum power + rem = np.zeros(self.n_storage, dtype=dt_float) + nb_ = np.zeros(self.n_storage, dtype=dt_int) + current_state = (obs.storage_charge - storage_setpoint * obs.storage_Emax) + for stor_id in need_ajust: + if current_state[stor_id] > 0.: + div_ = current_state[stor_id] / (obs.storage_max_p_prod[stor_id] * delta_time_hour) + else: + div_ = - current_state[stor_id] / (obs.storage_max_p_absorb[stor_id] * delta_time_hour) + div_ = np.round(div_, precision) + nb_[stor_id] = int(div_) + if div_ != int(div_): + if current_state[stor_id] > 0.: + rem[stor_id] = current_state[stor_id] / delta_time_hour - obs.storage_max_p_prod[stor_id] * nb_[stor_id] + else: + rem[stor_id] = - current_state[stor_id] / delta_time_hour - obs.storage_max_p_absorb[stor_id] * nb_[stor_id] + nb_[stor_id] += 1 + + # now create the proper actions + for nb_act in range(np.max(nb_)): + act = self.actionClass() + if not self.supports_type("set_storage"): + warnings.warn("Some storage are modififed, but you cannot modify them with your action type. Impossible to get back to the original topology.") + break + reds = np.zeros(self.n_storage, dtype=dt_float) + for stor_id in need_ajust: + if nb_act >= nb_[stor_id]: + # nothing to add in this case + continue + if current_state[stor_id] > 0.: + if nb_act < nb_[stor_id] - 1 or (rem[stor_id] == 0. and nb_act == nb_[stor_id] - 1): + reds[stor_id] = - obs.storage_max_p_prod[stor_id] + else: + reds[stor_id] = - rem[stor_id] + else: + if nb_act < nb_[stor_id] - 1 or (rem[stor_id] == 0. and nb_act == nb_[stor_id] - 1): + reds[stor_id] = obs.storage_max_p_absorb[stor_id] + else: + reds[stor_id] = rem[stor_id] + + act.storage_p = [(stor_id, red_) for stor_id, red_ in zip(need_ajust, reds)] + res["storage"].append(act) + + def get_back_to_ref_state(self, obs: "grid2op.Observation.BaseObservation", storage_setpoint=0.5, precision=5) -> Dict[str, List[BaseAction]]: + """ + This function returns the list of unary actions that you can perform in order to get back to the "fully meshed" / "initial" topology. + + Parameters + ---------- + observation: + The current observation (the one you want to know actions to set it back ot) + Notes + ----- + In this context a "unary" action, is (exclusive or): + + - an action that acts on a single powerline + - an action on a single substation + - a redispatching action + - a storage action + + 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: + + - "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 action (there can be multiple redispatching actions needed because of the ramps of the generator) + - "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) + + After receiving these lists, the agent has the choice for the order in which to apply these actions as well as how to best combine them (you can most + of the time combine action of different types in grid2op.) + + .. warning:: + + It does not presume anything on the availability of the objects. For examples, this funciton ignores completely the cooldowns on lines and substations. + + .. warning:: + + For the storage units, it tries to set their current setpoint to `storage_setpoint` % of their storage total capacity. Applying these actions + at different times might not fully set back the storage to this capacity in case of storage losses ! + + .. warning:: + + See section :ref:`action_powerline_status` for note on the powerline status. It might happen that you modify a powerline status using a "set_bus" (ie + tagged as "susbtation" by this function). + + .. warning:: + + It can raise warnings in case it's not possible, with your action space, to get back to the original / fully meshed topology + + Examples + -------- + + TODO + + """ + from grid2op.Observation.baseObservation import BaseObservation + if not isinstance(obs, BaseObservation): + raise AmbiguousAction("You need to provide a grid2op Observation for this function to work correctly.") + res = {} + + # powerline actions + self._aux_get_back_to_ref_state_line(res, obs) + # substations + self._aux_get_back_to_ref_state_sub(res, obs) + # redispatching + self._aux_get_back_to_ref_state_redisp(res, obs, precision=precision) + # storage + self._aux_get_back_to_ref_state_storage(res, obs, storage_setpoint, precision=precision) + # curtailment + self._aux_get_back_to_ref_state_curtail(res, obs) + + return res diff --git a/grid2op/Backend/Backend.py b/grid2op/Backend/Backend.py index e7f18a02b..b0d52edd5 100644 --- a/grid2op/Backend/Backend.py +++ b/grid2op/Backend/Backend.py @@ -1433,7 +1433,6 @@ def get_action_to_set(self): topo_vect = self.get_topo_vect() if np.all(topo_vect == -1): raise RuntimeError("The get_action_to_set should not be used after a divergence of the powerflow") - prod_p, _, prod_v = self.generators_info() load_p, load_q, _ = self.loads_info() set_me = self._complete_action_class() @@ -1445,6 +1444,7 @@ def get_action_to_set(self): "load_p": load_p, "load_q": load_q, }} + if self.shunts_data_available: p_s, q_s, sh_v, bus_s = self.shunt_info() dict_["shunt"] = {"shunt_bus": bus_s} @@ -1455,6 +1455,11 @@ def get_action_to_set(self): q_s[bus_s == -1] = np.NaN dict_["shunt"]["shunt_p"] = p_s dict_["shunt"]["shunt_q"] = q_s + + if self.n_storage > 0: + sto_p, *_ = self.storages_info() + dict_["set_storage"] = 1.0 * sto_p + set_me.update(dict_) return set_me diff --git a/grid2op/Backend/EducPandaPowerBackend.py b/grid2op/Backend/EducPandaPowerBackend.py index 1cbbd9586..d5e5ea5d0 100644 --- a/grid2op/Backend/EducPandaPowerBackend.py +++ b/grid2op/Backend/EducPandaPowerBackend.py @@ -348,7 +348,12 @@ def get_topo_vect(self): for bus_id in self._grid.load["bus"].values: res[self.load_pos_topo_vect[i]] = 1 if bus_id == self.load_to_subid[i] else 2 i += 1 - + + # do not forget storage units ! + i = 0 + for bus_id in self._grid.storage["bus"].values: + res[self.storage_pos_topo_vect[i]] = 1 if bus_id == self.storage_to_subid[i] else 2 + i += 1 return res def generators_info(self): diff --git a/grid2op/Chronics/GSFFWFWM.py b/grid2op/Chronics/GSFFWFWM.py index d085cce72..95e1615c3 100644 --- a/grid2op/Chronics/GSFFWFWM.py +++ b/grid2op/Chronics/GSFFWFWM.py @@ -14,7 +14,7 @@ from grid2op.dtypes import dt_bool, dt_int from grid2op.Exceptions import Grid2OpException -from grid2op.Chronics.GridStateFromFileWithForecasts import GridStateFromFileWithForecasts +from grid2op.Chronics.gridStateFromFileWithForecasts import GridStateFromFileWithForecasts class GridStateFromFileWithForecastsWithMaintenance(GridStateFromFileWithForecasts): @@ -41,6 +41,14 @@ class GridStateFromFileWithForecastsWithMaintenance(GridStateFromFileWithForecas """ + def __init__(self, path, sep=";", time_interval=timedelta(minutes=5), max_iter=-1, chunk_size=None): + super().__init__(path=path, sep=sep, time_interval=time_interval, max_iter=max_iter, chunk_size=chunk_size) + self.maintenance_starting_hour = None + self.maintenance_ending_hour = None + self.daily_proba_per_month_maintenance = None + self.max_daily_number_per_month_maintenance = None + self.line_to_maintenance = None + def initialize(self, order_backend_loads, order_backend_prods, order_backend_lines, order_backend_subs, names_chronics_to_backend=None): @@ -49,27 +57,34 @@ def initialize(self, order_backend_loads, order_backend_prods, order_backend_lin # properties of maintenance # self.maintenance_duration= 8*(self.time_interval.total_seconds()*60*60)#8h, 9am to 5pm # 8h furation, 9am to 5pm - with open(os.path.join(self.path, "maintenance_meta.json"), "r", encoding="utf-8") as f: - dict_ = json.load(f) - - self.maintenance_starting_hour = dict_["maintenance_starting_hour"] - # self.maintenance_duration= 8*(self.time_interval.total_seconds()*60*60) # not used for now, could be used later - self.maintenance_ending_hour = dict_["maintenance_ending_hour"] + if self.maintenance_starting_hour is None or \ + self.maintenance_ending_hour is None or \ + self.daily_proba_per_month_maintenance is None or \ + self.line_to_maintenance is None or \ + self.max_daily_number_per_month_maintenance is None: + # initialize the parameters from the json + with open(os.path.join(self.path, "maintenance_meta.json"), "r", encoding="utf-8") as f: + dict_ = json.load(f) - self.line_to_maintenance = set(dict_["line_to_maintenance"]) + self.maintenance_starting_hour = dict_["maintenance_starting_hour"] + # self.maintenance_duration= 8*(self.time_interval.total_seconds()*60*60) # not used for now, could be used later + self.maintenance_ending_hour = dict_["maintenance_ending_hour"] - # frequencies of maintenance - self.daily_proba_per_month_maintenance = dict_["daily_proba_per_month_maintenance"] + self.line_to_maintenance = set(dict_["line_to_maintenance"]) - self.max_daily_number_per_month_maintenance = dict_["max_daily_number_per_month_maintenance"] + # frequencies of maintenance + self.daily_proba_per_month_maintenance = dict_["daily_proba_per_month_maintenance"] + self.max_daily_number_per_month_maintenance = dict_["max_daily_number_per_month_maintenance"] super().initialize(order_backend_loads, order_backend_prods, order_backend_lines, order_backend_subs, names_chronics_to_backend) def _init_attrs(self, load_p, load_q, prod_p, prod_v, hazards=None, maintenance=None): super()._init_attrs(load_p, load_q, prod_p, prod_v, hazards=hazards, maintenance=None) # ignore the maitenance but keep hazards + self._sample_maintenance() + def _sample_maintenance(self): ######## # new method to introduce generated maintenance self.maintenance = self._generate_maintenance() # diff --git a/grid2op/Chronics/Settings_L2RPN2019.py b/grid2op/Chronics/Settings_L2RPN2019.py index 45aa67005..11bc82e27 100644 --- a/grid2op/Chronics/Settings_L2RPN2019.py +++ b/grid2op/Chronics/Settings_L2RPN2019.py @@ -19,7 +19,7 @@ from grid2op.Action import BaseAction from grid2op.Exceptions import AmbiguousAction, IncorrectNumberOfElements -from grid2op.Chronics.ReadPypowNetData import ReadPypowNetData # imported by another module +from grid2op.Chronics.readPypowNetData import ReadPypowNetData # imported by another module file_dir = Path(__file__).parent.absolute() grid2op_root = file_dir.parent.absolute() diff --git a/grid2op/Chronics/__init__.py b/grid2op/Chronics/__init__.py index a147ed73a..111d14653 100644 --- a/grid2op/Chronics/__init__.py +++ b/grid2op/Chronics/__init__.py @@ -8,16 +8,18 @@ "GridStateFromFileWithForecasts", "GridStateFromFileWithForecastsWithMaintenance", "GridStateFromFileWithForecastsWithoutMaintenance", - "ReadPypowNetData" + "ReadPypowNetData", + "FromNPY" ] -from grid2op.Chronics.ChronicsHandler import ChronicsHandler -from grid2op.Chronics.ChangeNothing import ChangeNothing -from grid2op.Chronics.GridValue import GridValue -from grid2op.Chronics.GridStateFromFile import GridStateFromFile -from grid2op.Chronics.GridStateFromFileWithForecasts import GridStateFromFileWithForecasts -from grid2op.Chronics.MultiFolder import Multifolder -from grid2op.Chronics.ReadPypowNetData import ReadPypowNetData +from grid2op.Chronics.chronicsHandler import ChronicsHandler +from grid2op.Chronics.changeNothing import ChangeNothing +from grid2op.Chronics.gridValue import GridValue +from grid2op.Chronics.gridStateFromFile import GridStateFromFile +from grid2op.Chronics.gridStateFromFileWithForecasts import GridStateFromFileWithForecasts +from grid2op.Chronics.multiFolder import Multifolder +from grid2op.Chronics.readPypowNetData import ReadPypowNetData from grid2op.Chronics.GSFFWFWM import GridStateFromFileWithForecastsWithMaintenance -from grid2op.Chronics.FromFileWithoutMaintenance import GridStateFromFileWithForecastsWithoutMaintenance -from grid2op.Chronics.MultifolderWithCache import MultifolderWithCache +from grid2op.Chronics.fromFileWithoutMaintenance import GridStateFromFileWithForecastsWithoutMaintenance +from grid2op.Chronics.multifolderWithCache import MultifolderWithCache +from grid2op.Chronics.fromNPY import FromNPY diff --git a/grid2op/Chronics/ChangeNothing.py b/grid2op/Chronics/changeNothing.py similarity index 93% rename from grid2op/Chronics/ChangeNothing.py rename to grid2op/Chronics/changeNothing.py index 121434800..9dcf72f1e 100644 --- a/grid2op/Chronics/ChangeNothing.py +++ b/grid2op/Chronics/changeNothing.py @@ -10,7 +10,7 @@ from datetime import datetime, timedelta from grid2op.dtypes import dt_int -from grid2op.Chronics.GridValue import GridValue +from grid2op.Chronics.gridValue import GridValue class ChangeNothing(GridValue): @@ -41,7 +41,8 @@ class ChangeNothing(GridValue): from grid2op.Chronics import ChangeNothing env_name = ... - env = grid2op.make(env_name, data_feeding_kwargs={"gridvalueClass": ChangeNothing}) + # env = grid2op.make(env_name, data_feeding_kwargs={"gridvalueClass": ChangeNothing}) + env = grid2op.make(env_name, chronics_class=ChangeNothing) It can also be used with the "blank" environment: diff --git a/grid2op/Chronics/ChronicsHandler.py b/grid2op/Chronics/chronicsHandler.py similarity index 98% rename from grid2op/Chronics/ChronicsHandler.py rename to grid2op/Chronics/chronicsHandler.py index ef1aa3425..3fbe85a1b 100644 --- a/grid2op/Chronics/ChronicsHandler.py +++ b/grid2op/Chronics/chronicsHandler.py @@ -13,8 +13,8 @@ from grid2op.dtypes import dt_int from grid2op.Exceptions import Grid2OpException, ChronicsError from grid2op.Space import RandomObject -from grid2op.Chronics.GridValue import GridValue -from grid2op.Chronics.ChangeNothing import ChangeNothing +from grid2op.Chronics.gridValue import GridValue +from grid2op.Chronics.changeNothing import ChangeNothing class ChronicsHandler(RandomObject): diff --git a/grid2op/Chronics/FromFileWithoutMaintenance.py b/grid2op/Chronics/fromFileWithoutMaintenance.py similarity index 98% rename from grid2op/Chronics/FromFileWithoutMaintenance.py rename to grid2op/Chronics/fromFileWithoutMaintenance.py index 7c30abbc4..eea10eec9 100644 --- a/grid2op/Chronics/FromFileWithoutMaintenance.py +++ b/grid2op/Chronics/fromFileWithoutMaintenance.py @@ -15,7 +15,7 @@ from grid2op.dtypes import dt_bool, dt_int from grid2op.Exceptions import Grid2OpException -from grid2op.Chronics.GridStateFromFileWithForecasts import GridStateFromFileWithForecasts +from grid2op.Chronics.gridStateFromFileWithForecasts import GridStateFromFileWithForecasts class GridStateFromFileWithForecastsWithoutMaintenance(GridStateFromFileWithForecasts): diff --git a/grid2op/Chronics/fromNPY.py b/grid2op/Chronics/fromNPY.py new file mode 100644 index 000000000..d9d8eaf54 --- /dev/null +++ b/grid2op/Chronics/fromNPY.py @@ -0,0 +1,622 @@ +# 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, Union +import numpy as np +import hashlib +from datetime import datetime, timedelta + +import grid2op +from grid2op.dtypes import dt_int +from grid2op.Chronics.gridValue import GridValue +from grid2op.Exceptions import ChronicsError + + +class FromNPY(GridValue): + """ + This class allows to generate some chronics compatible with grid2op if the data are provided in numpy format. + + It also enables the use of the starting the chronics at different time than the original time and to end it before the end + of the chronics. + + It is then much more flexible in its usage than the defaults chronics. But it is also much more error prone. For example, it does not check + the order of the loads / generators that you provide. + + .. warning:: + It assume the order of the elements are consistent with the powergrid backend ! It will not attempt to reorder the columns of the dataset + + .. note:: + + The effect if "i_start" and "i_end" are persistant. If you set it once, it affects the object even after "env.reset()" is called. + If you want to modify them, you need to use the :func:`FromNPY.chronics.change_i_start` and :func:`FromNPY.chronics.change_i_end` methods + (and call `env.reset()`!) + + TODO implement methods to change the loads / production "based on sampling" (online sampling instead of only reading data) + TODO implement the possibility to simulate maintenance / hazards "on the fly" + TODO implement hazards ! + + Examples + -------- + + Usage example, for what you don't really have to do: + + .. code-block:: python + + import grid2op + from grid2op.Chronics import FromNPY + + # first retrieve the data that you want, the easiest wayt is to create an environment and read the data from it. + env_name = "l2rpn_case14_sandbox" # for example + env_ref = grid2op.make(env_name) + # retrieve the data + load_p = 1.0 * env_ref.chronics_handler.real_data.data.load_p + load_q = 1.0 * env_ref.chronics_handler.real_data.data.load_q + prod_p = 1.0 * env_ref.chronics_handler.real_data.data.prod_p + prod_v = 1.0 * env_ref.chronics_handler.real_data.data.prod_v + + # now create an environment with these chronics: + env = grid2op.make(env_name, + chronics_class=FromNPY, + data_feeding_kwargs={"i_start": 5, # start at the "step" 5 NB first step is first observation, available with `obs = env.reset()` + "i_end": 18, # end index: data after that will not be considered (excluded as per python convention) + "load_p": load_p, + "load_q": load_q, + "prod_p": prod_p, + "prod_v": prod_v + # other parameters includes + # maintenance + # load_p_forecast + # load_q_forecast + # prod_p_forecast + # prod_v_forecast + }) + + # you can use env normally, including in runners + obs = env.reset() + # obs.load_p is load_p[5] (because you set "i_start" = 5, by default it's 0) + + You can, after creation, change the data with: + + .. code-block:: python + + # create env as above + + # retrieve some new values that you would like + new_load_p = ... + new_load_q = ... + new_prod_p = ... + new_prod_v = ... + + # change the values + env.chronics_handler.real_data.change_chronics(new_load_p, new_load_q, new_prod_p, new_prod_v) + obs = env.reset() # mandatory if you want the change to be taken into account + # obs.load_p is new_load_p[5] (or rather load_p[env.chronics_handler.real_data._i_start]) + + .. seealso:: + More usage examples in: + + - :func:`FromNPY.change_chronics` + - :func:`FromNPY.change_forecasts` + - :func:`FromNPY.change_i_start` + - :func:`FromNPY.change_i_end` + + Attributes + ---------- + TODO + """ + def __init__(self, + load_p : np.ndarray, + load_q : np.ndarray, + prod_p : np.ndarray, + prod_v : Optional[np.ndarray]=None, + hazards : Optional[np.ndarray]=None, + maintenance : Optional[np.ndarray]=None, + load_p_forecast : Optional[np.ndarray]=None, # TODO forecasts !! + load_q_forecast : Optional[np.ndarray]=None, + prod_p_forecast : Optional[np.ndarray]=None, + prod_v_forecast : Optional[np.ndarray]=None, + time_interval: timedelta=timedelta(minutes=5), + max_iter: int=-1, + start_datetime: datetime=datetime(year=2019, month=1, day=1), + chunk_size: Optional[int]=None, + i_start: Optional[int]=None, + i_end: Optional[int]=None, # excluded, as always in python + **kwargs): + GridValue.__init__(self, time_interval=time_interval, max_iter=max_iter, start_datetime=start_datetime, + chunk_size=chunk_size) + self._i_start : int = i_start if i_start is not None else 0 + self.__new_istart : Optional[int] = i_start + self.n_gen : int = prod_p.shape[1] + self.n_load : int = load_p.shape[1] + self.n_line : Union[int, None] = None + + self._load_p : np.ndarray = 1.0 * load_p + self._load_q : np.ndarray = 1.0 * load_q + self._prod_p : np.ndarray = 1.0 * prod_p + + self._prod_v = None + if prod_v is not None: + self._prod_v = 1.0 * prod_v + + self.__new_load_p : Optional[np.ndarray] = None + self.__new_prod_p : Optional[np.ndarray] = None + self.__new_prod_v : Optional[np.ndarray] = None + self.__new_load_q : Optional[np.ndarray] = None + + self._i_end : int = i_end if i_end is not None else load_p.shape[0] + self.__new_iend : Optional[int] = i_end + + self.has_maintenance = False + self.maintenance = None + self.maintenance_duration = None + self.maintenance_time = None + if maintenance is not None: + self.has_maintenance = True + self.n_line = maintenance.shape[1] + assert load_p.shape[0] == maintenance.shape[0] + self.maintenance = maintenance # TODO copy + + self.maintenance_time = np.zeros(shape=(self.maintenance.shape[0], self.n_line), dtype=dt_int) - 1 + self.maintenance_duration = np.zeros(shape=(self.maintenance.shape[0], self.n_line), dtype=dt_int) + for line_id in range(self.n_line): + self.maintenance_time[:, line_id] = self.get_maintenance_time_1d(self.maintenance[:, line_id]) + self.maintenance_duration[:, line_id] = self.get_maintenance_duration_1d(self.maintenance[:, line_id]) + + self.has_hazards = False + self.hazards = None + self.hazard_duration = None + if hazards is not None: + raise ChronicsError("This feature is not available at the moment. Fill a github issue at " + "https://github.com/rte-france/Grid2Op/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=") + # self.has_hazards = True + # if self.n_line is None: + # self.n_line = hazards.shape[1] + # else: + # assert self.n_line == hazards.shape[1] + # assert load_p.shape[0] == hazards.shape[0] + + # self.hazards = hazards # TODO copy ! + # self.hazard_duration = np.zeros(shape=(self.hazards.shape[0], self.n_line), dtype=dt_int) + # for line_id in range(self.n_line): + # self.hazard_duration[:, line_id] = self.get_hazard_duration_1d(self.hazards[:, line_id]) + + self._forecasts = None + if load_p_forecast is not None: + assert load_q_forecast is not None + assert prod_p_forecast is not None + self._forecasts = FromNPY(load_p=load_p_forecast, + load_q=load_q_forecast, + prod_p=prod_p_forecast, + prod_v=prod_v_forecast, + load_p_forecast=None, + load_q_forecast=None, + prod_p_forecast=None, + prod_v_forecast=None, + i_start=i_start, + i_end=i_end + ) + elif load_q_forecast is not None: + raise ChronicsError("if load_q_forecast is not None, then load_p_forecast should not be None") + elif prod_p_forecast is not None: + raise ChronicsError("if prod_p_forecast is not None, then load_p_forecast should not be None") + + def initialize(self, order_backend_loads, order_backend_prods, order_backend_lines, order_backend_subs, + names_chronics_to_backend=None): + assert len(order_backend_prods) == self.n_gen + assert len(order_backend_loads) == self.n_load + if self.n_line is None: + self.n_line = len(order_backend_lines) + else: + assert len(order_backend_lines) == self.n_line + + if self._forecasts is not None: + self._forecasts.initialize(order_backend_loads, + order_backend_prods, + order_backend_lines, + order_backend_subs, + names_chronics_to_backend) + self.maintenance_time_nomaint = np.zeros(shape=(self.n_line, ), dtype=dt_int) - 1 + self.maintenance_duration_nomaint = np.zeros(shape=(self.n_line, ), dtype=dt_int) + self.hazard_duration_nohaz = np.zeros(shape=(self.n_line, ), dtype=dt_int) + + self.curr_iter = 0 + self.current_index = self._i_start - 1 + + def _get_long_hash(self, hash_: hashlib.blake2b = None): + # get the "long hash" from blake2b + if hash_ is None: + hash_ = hashlib.blake2b() # should be faster than md5 ! (and safer, but we only care about speed here) + hash_.update(self._load_p.tobytes()) + hash_.update(self._load_q.tobytes()) + hash_.update(self._prod_p.tobytes()) + if self._prod_v is not None: + hash_.update(self._prod_v.tobytes()) + if self.maintenance is not None: + hash_.update(self.maintenance.tobytes()) + if self.hazards is not None: + hash_.update(self.hazards.tobytes()) + + if self._forecasts: + self._forecasts._get_long_hash(hash_) + return hash_.digest() + + def get_id(self) -> str: + """ + To return a unique ID of the chronics, we use a hash function (black2b), but it outputs a name too big (64 characters or so). + So we hash it again with md5 to get a reasonable length id (32 characters) + + Returns: + str: the hash of the arrays (load_p, load_q, etc.) in the chronics + """ + long_hash_byte = self._get_long_hash() + # now shorten it with md5 + short_hash = hashlib.md5(long_hash_byte) + return short_hash.hexdigest() + + def load_next(self): + self.current_index += 1 + + if self.current_index > self._i_end or self.current_index >= self._load_p.shape[0]: + raise StopIteration + + res = {} + dict_ = {} + prod_v = None + if self._load_p is not None: + dict_["load_p"] = 1.0 * self._load_p[self.current_index, :] + if self._load_q is not None: + dict_["load_q"] = 1.0 * self._load_q[self.current_index, :] + if self._prod_p is not None: + dict_["prod_p"] = 1.0 * self._prod_p[self.current_index, :] + if self._prod_v is not None: + prod_v = 1.0 * self._prod_v[self.current_index, :] + if dict_: + res["injection"] = dict_ + + if self.maintenance is not None and self.has_maintenance: + res["maintenance"] = self.maintenance[self.current_index, :] + if self.hazards is not None and self.has_hazards: + res["hazards"] = self.hazards[self.current_index, :] + + self.current_datetime += self.time_interval + self.curr_iter += 1 + + if self.maintenance_time is not None and self.maintenance_duration is not None and self.has_maintenance: + maintenance_time = dt_int(1 * self.maintenance_time[self.current_index, :]) + maintenance_duration = dt_int(1 * self.maintenance_duration[self.current_index, :]) + else: + maintenance_time = self.maintenance_time_nomaint + maintenance_duration = self.maintenance_duration_nomaint + + if self.hazard_duration is not None and self.has_hazards: + hazard_duration = 1 * self.hazard_duration[self.current_index, :] + else: + hazard_duration = self.hazard_duration_nohaz + + return self.current_datetime, res, maintenance_time, maintenance_duration, hazard_duration, prod_v + + def check_validity(self, backend: Optional["grid2op.Backend.backend.Backend"]) -> None: + # TODO raise the proper errors from ChronicsError here rather than AssertError + assert self._load_p.shape[0] == self._load_q.shape[0] + assert self._load_p.shape[0] == self._prod_p.shape[0] + if self._prod_v is not None: + assert self._load_p.shape[0] == self._prod_v.shape[0] + + if self.hazards is not None: + assert self.hazards.shape[1] == self.n_line + if self.maintenance is not None: + assert self.maintenance.shape[1] == self.n_line + if self.maintenance_duration is not None: + assert self.n_line == self.maintenance_duration.shape[1] + if self.maintenance_time is not None: + assert self.n_line == self.maintenance_time.shape[1] + + # TODO forecast + if self._forecasts is not None: + assert self._forecasts.n_line == self.n_line + assert self._forecasts.n_gen == self.n_gen + assert self._forecasts.n_load == self.n_load + assert self._load_p.shape[0] == self._forecasts._load_p.shape[0] + assert self._load_q.shape[0] == self._forecasts._load_q.shape[0] + assert self._prod_p.shape[0] == self._forecasts._prod_p.shape[0] + if self._prod_v is not None and self._forecasts._prod_v is not None: + assert self._prod_v.shape[0] == self._forecasts._prod_v.shape[0] + self._forecasts.check_validity(backend=backend) + + def next_chronics(self): + # restart the chronics: read it again ! + self.current_datetime = self.start_datetime + self.curr_iter = 0 + if self.__new_istart is not None: + self._i_start = self.__new_istart + else: + self._i_start = 0 + self.current_index = self._i_start + + if self.__new_load_p is not None: + self._load_p = self.__new_load_p + self.__new_load_p = None + if self.__new_load_q is not None: + self._load_q = self.__new_load_q + self.__new_load_q = None + if self.__new_prod_p is not None: + self._prod_p = self.__new_prod_p + self.__new_prod_p = None + if self.__new_prod_v is not None: + self._prod_v = self.__new_prod_v + self.__new_prod_v = None + + if self.__new_iend is None: + self._i_end = self._load_p.shape[0] + else: + self._i_end = self.__new_iend + + if self._forecasts is not None: + # update the forecast + self._forecasts.next_chronics() + self.check_validity(backend=None) + + def done(self): + """ + INTERNAL + + .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ + + Compare to :func:`GridValue.done` an episode can be over for 2 main reasons: + + - :attr:`GridValue.max_iter` has been reached + - There are no data in the numpy array. + - i_end has been reached + + The episode is done if one of the above condition is met. + + Returns + ------- + res: ``bool`` + Whether the episode has reached its end or not. + + """ + res = False + if self.current_index >= self._i_end or self.current_index >= self._load_p.shape[0]: + res = True + elif self.max_iter > 0: + if self.curr_iter > self.max_iter: + res = True + return res + + def forecasts(self): + """ + By default, forecasts are only made 1 step ahead. + + We could change that. Do not hesitate to make a feature request + (https://github.com/rte-france/Grid2Op/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=) if that is necessary for you. + """ + if self._forecasts is None: + return [] + self._forecasts.current_index = self.current_index - 1 + dt, dict_, *rest = self._forecasts.load_next() + return [(self.current_datetime + self.time_interval, dict_)] + + def change_chronics(self, + new_load_p: np.ndarray = None, + new_load_q: np.ndarray = None, + new_prod_p: np.ndarray = None, + new_prod_v: np.ndarray = None): + """ + Allows to change the data used by this class. + + .. warning:: + This has an effect only after "env.reset" has been called ! + + + Args: + new_load_p (np.ndarray, optional): change the load_p. Defaults to None (= do not change). + new_load_q (np.ndarray, optional): change the load_q. Defaults to None (= do not change). + new_prod_p (np.ndarray, optional): change the prod_p. Defaults to None (= do not change). + new_prod_v (np.ndarray, optional): change the prod_v. Defaults to None (= do not change). + + Examples + --------- + + .. code-block:: python + + import grid2op + from grid2op.Chronics import FromNPY + # create an environment as in this class description (in short: ) + + load_p = ... # find somehow a suitable "load_p" array: rows represent time, columns the individual load + load_q = ... + prod_p = ... + prod_v = ... + + # now create an environment with these chronics: + env = grid2op.make(env_name, + chronics_class=FromNPY, + data_feeding_kwargs={"load_p": load_p, + "load_q": load_q, + "prod_p": prod_p, + "prod_v": prod_v} + ) + obs = env.reset() # obs.load_p is load_p[0] (or rather load_p[env.chronics_handler.real_data._i_start]) + + new_load_p = ... # find somehow a new suitable "load_p" + new_load_q = ... + new_prod_p = ... + new_prod_v = ... + + env.chronics_handler.real_data.change_chronics(new_load_p, new_load_q, new_prod_p, new_prod_v) + # has no effect at this stage + + obs = env.reset() # now has some effect ! + # obs.load_p is new_load_p[0] (or rather load_p[env.chronics_handler.real_data._i_start]) + """ + if new_load_p is not None: + self.__new_load_p = 1.0 * new_load_p + if new_load_q is not None: + self.__new_load_q = 1.0 * new_load_q + if new_prod_p is not None: + self.__new_prod_p = 1.0 * new_prod_p + if new_prod_v is not None: + self.__new_prod_v = 1.0 * new_prod_v + + def change_forecasts(self, + new_load_p: np.ndarray = None, + new_load_q: np.ndarray = None, + new_prod_p: np.ndarray = None, + new_prod_v: np.ndarray = None): + """ + Allows to change the data used by this class in the "obs.simulate" function. + + .. warning:: + This has an effect only after "env.reset" has been called ! + + Args: + new_load_p (np.ndarray, optional): change the load_p_forecast. Defaults to None (= do not change). + new_load_q (np.ndarray, optional): change the load_q_forecast. Defaults to None (= do not change). + new_prod_p (np.ndarray, optional): change the prod_p_forecast. Defaults to None (= do not change). + new_prod_v (np.ndarray, optional): change the prod_v_forecast. Defaults to None (= do not change). + + Examples + --------- + + .. code-block:: python + + import grid2op + from grid2op.Chronics import FromNPY + # create an environment as in this class description (in short: ) + + load_p = ... # find somehow a suitable "load_p" array: rows represent time, columns the individual load + load_q = ... + prod_p = ... + prod_v = ... + load_p_forecast = ... + load_q_forecast = ... + prod_p_forecast = ... + prod_v_forecast = ... + + env = grid2op.make(env_name, + chronics_class=FromNPY, + data_feeding_kwargs={"load_p": load_p, + "load_q": load_q, + "prod_p": prod_p, + "prod_v": prod_v, + "load_p_forecast": load_p_forecast + "load_q_forecast": load_q_forecast + "prod_p_forecast": prod_p_forecast + "prod_v_forecast": prod_v_forecast + }) + + new_load_p_forecast = ... # find somehow a new suitable "load_p" + new_load_q_forecast = ... + new_prod_p_forecast = ... + new_prod_v_forecast = ... + + env.chronics_handler.real_data.change_forecasts(new_load_p_forecast, new_load_q_forecast, new_prod_p_forecast, new_prod_v_forecast) + # has no effect at this stage + + obs = env.reset() # now has some effect ! + sim_o, *_ = obs.simulate() # sim_o.load_p has the values of new_load_p_forecast[0] + """ + if self._forecasts is None: + raise ChronicsError("You cannot change the forecast for this chronics are there are no forecasts enabled") + self._forecasts.change_chronics(new_load_p=new_load_p, new_load_q=new_load_q, new_prod_p=new_prod_p, new_prod_v=new_prod_v) + + def max_timestep(self): + if self.max_iter >= 0: + return min(self.max_iter, self._load_p.shape[0], self._i_end) + return min(self._load_p.shape[0], self._i_end) + + def change_i_start(self, new_i_start: Union[int, None]): + """ + Allows to change the "i_start". + + .. warning:: + + It has only an affect after "env.reset()" is called. + + Examples + -------- + + .. code-block:: python + + import grid2op + from grid2op.Chronics import FromNPY + # create an environment as in this class description (in short: ) + + load_p = ... # find somehow a suitable "load_p" array: rows represent time, columns the individual load + load_q = ... + prod_p = ... + prod_v = ... + + # now create an environment with these chronics: + env = grid2op.make(env_name, + chronics_class=FromNPY, + data_feeding_kwargs={"load_p": load_p, + "load_q": load_q, + "prod_p": prod_p, + "prod_v": prod_v} + ) + obs = env.reset() # obs.load_p is load_p[0] (or rather load_p[env.chronics_handler.real_data._i_start]) + + env.chronics_handler.real_data.change_i_start(10) + obs = env.reset() # obs.load_p is load_p[10] + # indeed `env.chronics_handler.real_data._i_start` has been changed to 10. + + # to undo all changes (and use the defaults) you can: + # env.chronics_handler.real_data.change_i_start(None) + """ + if new_i_start is not None: + self.__new_istart = int(new_i_start) + else: + self.__new_istart = None + + + def change_i_end(self, new_i_end: Union[int, None]): + """ + Allows to change the "i_end". + + .. warning:: + + It has only an affect after "env.reset()" is called. + + Examples + -------- + + .. code-block:: python + + import grid2op + from grid2op.Chronics import FromNPY + # create an environment as in this class description (in short: ) + + load_p = ... # find somehow a suitable "load_p" array: rows represent time, columns the individual load + load_q = ... + prod_p = ... + prod_v = ... + + # now create an environment with these chronics: + env = grid2op.make(env_name, + chronics_class=FromNPY, + data_feeding_kwargs={"load_p": load_p, + "load_q": load_q, + "prod_p": prod_p, + "prod_v": prod_v} + ) + obs = env.reset() + + env.chronics_handler.real_data.change_i_end(150) + obs = env.reset() + # indeed `env.chronics_handler.real_data._i_end` has been changed to 10. + # scenario lenght will be at best 150 ! + + # to undo all changes (and use the defaults) you can: + # env.chronics_handler.real_data.change_i_end(None) + """ + if new_i_end is not None: + self.__new_iend = int(new_i_end) + else: + self.__new_iend = None diff --git a/grid2op/Chronics/GridStateFromFile.py b/grid2op/Chronics/gridStateFromFile.py similarity index 99% rename from grid2op/Chronics/GridStateFromFile.py rename to grid2op/Chronics/gridStateFromFile.py index ee0809ab8..100b380e7 100644 --- a/grid2op/Chronics/GridStateFromFile.py +++ b/grid2op/Chronics/gridStateFromFile.py @@ -17,7 +17,7 @@ from grid2op.Exceptions import IncorrectNumberOfElements, ChronicsError, ChronicsNotFoundError from grid2op.Exceptions import IncorrectNumberOfLoads, IncorrectNumberOfGenerators, IncorrectNumberOfLines from grid2op.Exceptions import EnvError, InsufficientData -from grid2op.Chronics.GridValue import GridValue +from grid2op.Chronics.gridValue import GridValue class GridStateFromFile(GridValue): diff --git a/grid2op/Chronics/GridStateFromFileWithForecasts.py b/grid2op/Chronics/gridStateFromFileWithForecasts.py similarity index 99% rename from grid2op/Chronics/GridStateFromFileWithForecasts.py rename to grid2op/Chronics/gridStateFromFileWithForecasts.py index edabbd957..e715ebdd8 100644 --- a/grid2op/Chronics/GridStateFromFileWithForecasts.py +++ b/grid2op/Chronics/gridStateFromFileWithForecasts.py @@ -15,7 +15,7 @@ from grid2op.dtypes import dt_float, dt_bool from grid2op.Exceptions import EnvError, IncorrectNumberOfLoads, IncorrectNumberOfLines, IncorrectNumberOfGenerators from grid2op.Exceptions import ChronicsError -from grid2op.Chronics.GridStateFromFile import GridStateFromFile +from grid2op.Chronics.gridStateFromFile import GridStateFromFile class GridStateFromFileWithForecasts(GridStateFromFile): diff --git a/grid2op/Chronics/GridValue.py b/grid2op/Chronics/gridValue.py similarity index 99% rename from grid2op/Chronics/GridValue.py rename to grid2op/Chronics/gridValue.py index ca7c798df..051cb9c4d 100644 --- a/grid2op/Chronics/GridValue.py +++ b/grid2op/Chronics/gridValue.py @@ -456,7 +456,7 @@ def load_next(self): The current timestamp for which the modifications have been generated. dict_: ``dict`` - Always empty, indicating i do nothing. + Always empty, indicating i do nothing (for this case) maintenance_time: ``numpy.ndarray``, dtype:``int`` Information about the next planned maintenance. See :attr:`GridValue.maintenance_time` for more information. diff --git a/grid2op/Chronics/MultiFolder.py b/grid2op/Chronics/multiFolder.py similarity index 97% rename from grid2op/Chronics/MultiFolder.py rename to grid2op/Chronics/multiFolder.py index e3a09cbc1..0233ccedd 100644 --- a/grid2op/Chronics/MultiFolder.py +++ b/grid2op/Chronics/multiFolder.py @@ -14,8 +14,8 @@ from grid2op.dtypes import dt_int from grid2op.Exceptions import * -from grid2op.Chronics.GridValue import GridValue -from grid2op.Chronics.GridStateFromFile import GridStateFromFile +from grid2op.Chronics.gridValue import GridValue +from grid2op.Chronics.gridStateFromFile import GridStateFromFile class Multifolder(GridValue): @@ -91,6 +91,16 @@ def __init__(self, path, self._prev_cache_id = 0 self._order = None + def available_chronics(self): + """return the list of available chronics. + + Examples + -------- + + # TODO + """ + return self.subpaths[self._order] + def _default_filter(self, x): """ default filter used at the initialization. It keeps only the first data encountered. @@ -336,11 +346,14 @@ def tell_id(self, id_num, previous=False): Do you want to set to the previous value of this one or not (note that in general you want to set to the previous value, as calling this function as an impact only after `env.reset()` is called) """ + import pdb if isinstance(id_num, str): # new accepted behaviour starting 1.6.4 + # new in version 1.6.5: you only need to specify the chronics folder id and not the full path found = False for internal_id_, number in enumerate(self._order): - if self.subpaths[number] == id_num: + if self.subpaths[number] == id_num or \ + os.path.join(self.path,id_num) == self.subpaths[number]: self._prev_cache_id = internal_id_ found = True diff --git a/grid2op/Chronics/MultifolderWithCache.py b/grid2op/Chronics/multifolderWithCache.py similarity index 97% rename from grid2op/Chronics/MultifolderWithCache.py rename to grid2op/Chronics/multifolderWithCache.py index db428ea92..0fdfbef8d 100644 --- a/grid2op/Chronics/MultifolderWithCache.py +++ b/grid2op/Chronics/multifolderWithCache.py @@ -9,8 +9,8 @@ from datetime import timedelta, datetime from grid2op.dtypes import dt_int -from grid2op.Chronics.MultiFolder import Multifolder -from grid2op.Chronics.GridStateFromFile import GridStateFromFile +from grid2op.Chronics.multiFolder import Multifolder +from grid2op.Chronics.gridStateFromFile import GridStateFromFile class MultifolderWithCache(Multifolder): @@ -46,7 +46,7 @@ class MultifolderWithCache(Multifolder): # assign a filter, use only chronics that have "december" in their name env.chronics_handler.real_data.set_filter(lambda x: re.match(".*december.*", x) is not None) # create the cache - env.chronics_handler.real_data.reset_cache() + env.chronics_handler.reset() # and now you can use it as you would do any gym environment: my_agent = ... diff --git a/grid2op/Chronics/ReadPypowNetData.py b/grid2op/Chronics/readPypowNetData.py similarity index 100% rename from grid2op/Chronics/ReadPypowNetData.py rename to grid2op/Chronics/readPypowNetData.py diff --git a/grid2op/Converter/BackendConverter.py b/grid2op/Converter/BackendConverter.py index 308de82fe..f879bce20 100644 --- a/grid2op/Converter/BackendConverter.py +++ b/grid2op/Converter/BackendConverter.py @@ -46,10 +46,10 @@ class BackendConverter(Backend): import grid2op from grid2op.Converter import BackendConverter from grid2op.Backend import PandaPowerBackend - from lightsim2grid.LightSimBackend import LightSimBackend + from lightsim2grid import LightSimBackend backend = BackendConverter(source_backend_class=PandaPowerBackend, - target_backend_class=LightSimBackend, - target_backend_grid_path=None) + target_backend_class=LightSimBackend, + target_backend_grid_path=None) # and now your environment behaves as if PandaPowerBackend did the computation (same load order, same generator order etc.) but real computation are made with LightSimBackend. @@ -320,8 +320,8 @@ def assert_grid_correct(self): # self.target_backend.load_redispacthing_data(self.path_redisp, name=self.name_redisp) 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(self.path_redisp, name=self.name_redisp) - # self.target_backend.load_storage_data(self.path_redisp, name=self.name_redisp) + self.source_backend.load_storage_data(self.path_storage_data, name=self.name_storage_data) + self.target_backend.load_storage_data(self.path_storage_data, name=self.name_storage_data) if self.path_grid_layout is not None: # grid layout data were available super().load_grid_layout(self.path_grid_layout, self.name_grid_layout) @@ -454,6 +454,11 @@ def lines_ex_info(self): return self.cst1*p_[self._line_tg2sr], self.cst1*q_[self._line_tg2sr], \ self.cst1*v_[self._line_tg2sr], self.cst1*a_[self._line_tg2sr] + def storages_info(self): + p_, q_, v_ = self.target_backend.storages_info() + return self.cst1*p_[self._storage_sr2tg], self.cst1*q_[self._storage_sr2tg], \ + self.cst1*v_[self._storage_sr2tg] + def shunt_info(self): if self._shunt_tg2sr is not None: # shunts are supported by both source and target backend @@ -534,5 +539,4 @@ def update_thermal_limit(self, env): # env has the powerline stored in the order of the source backend, but i need # to have them stored in the order of the target backend for such function pass - # TODO update_from_obs too, maybe ? diff --git a/grid2op/Converter/IdToAct.py b/grid2op/Converter/IdToAct.py index cf7724898..73bc6380f 100644 --- a/grid2op/Converter/IdToAct.py +++ b/grid2op/Converter/IdToAct.py @@ -10,6 +10,7 @@ from grid2op.Action import BaseAction from grid2op.Converter.Converters import Converter +from grid2op.Exceptions.Grid2OpException import Grid2OpException from grid2op.dtypes import dt_float, dt_int @@ -53,13 +54,11 @@ class IdToAct(Converter): Actions corresponding to all topologies are also used by default. See :func:`grid2op.BaseAction.ActionSpace.get_all_unitary_topologies_set` for more information. - In this converter: - `encoded_act` are positive integer, representing the index of the actions. - `transformed_obs` are regular observations. - **NB** The number of actions in this converter can be especially big. For example, if a substation counts N elements there are roughly 2^(N-1) possible actions in this substation. This means if there are a single substation with more than N = 15 or 16 elements, the amount of actions (for this substation alone) will be higher than 16.000 @@ -126,7 +125,14 @@ def init_converter(self, all_actions=None, **kwargs): Whether you want to include the "redispatch" in your action space (in case the original action space allows it) - + curtail: ``bool`` + Whether you want to include the "curtailment" in your action space + (in case the original action space allows it) + + storage: ``bool`` + Whether you want to include the "storage unit" in your action space + (in case the original action space allows it) + Examples -------- Here is an example of a code that will: make a converter by selecting some action. Save it, and then restore @@ -241,15 +247,20 @@ def init_converter(self, all_actions=None, **kwargs): # assign the action to my actions possible_act = all_actions[0] if isinstance(possible_act, BaseAction): + # list of grid2op action self.all_actions = np.array(all_actions) + elif isinstance(possible_act, dict): + # list of dictionnary (obtained with `act.as_serializable_dict()`) + self.all_actions = np.array([self.__call__(el) for el in all_actions]) else: + # should be an array ! try: self.all_actions = np.array([self.__call__() for _ in all_actions]) for i, el in enumerate(all_actions): self.all_actions[i].from_vect(el) - except Exception as e: - raise RuntimeError("Impossible to convert the data provided in \"all_actions\" into valid " - "grid2op action. The error was:\n{}".format(e)) + except Exception as exc_: + raise Grid2OpException("Impossible to convert the data provided in \"all_actions\" into valid " + "grid2op action. The error was:\n{}".format(e)) from exc_ else: raise RuntimeError("Impossible to load the action provided.") self.n = len(self.all_actions) diff --git a/grid2op/Environment/BaseEnv.py b/grid2op/Environment/BaseEnv.py index 21b2d94a1..b717a3f70 100644 --- a/grid2op/Environment/BaseEnv.py +++ b/grid2op/Environment/BaseEnv.py @@ -233,11 +233,12 @@ def __init__(self, has_attention_budget=False, attention_budget_cls=LinearAttentionBudget, kwargs_attention_budget={}, - logger=None, + logger=None, + _is_test=False, # TODO not implemented !! ): GridObjects.__init__(self) RandomObject.__init__(self) - + self._is_test = _is_test if logger is None: import logging self.logger = logging.getLogger(__name__) @@ -425,11 +426,13 @@ def __init__(self, self._is_alarm_used_in_reward = False self._kwargs_attention_budget = copy.deepcopy(kwargs_attention_budget) - def _custom_deepcopy_for_copy(self, new_obj): + def _custom_deepcopy_for_copy(self, new_obj, dict_=None): if self.__closed: raise RuntimeError("Impossible to make a copy of a closed environment !") RandomObject._custom_deepcopy_for_copy(self, new_obj) + if dict_ is None: + dict_ = {} new_obj._init_grid_path = copy.deepcopy(self._init_grid_path) new_obj._DEBUG = self._DEBUG @@ -541,6 +544,7 @@ def _custom_deepcopy_for_copy(self, new_obj): new_obj._observationClass = self._observationClass new_obj._legalActClass = self._legalActClass new_obj._observation_space = self._observation_space.copy(copy_backend=True) + new_obj._observation_space._legal_action = new_obj._game_rules.legal_action # TODO this does not respect SOLID principles at all ! new_obj._names_chronics_to_backend = self._names_chronics_to_backend new_obj._reward_helper = copy.deepcopy(self._reward_helper) @@ -566,7 +570,7 @@ def _custom_deepcopy_for_copy(self, new_obj): # init the opponent new_obj._opponent = new_obj._opponent_class.__new__(new_obj._opponent_class) - self._opponent._custom_deepcopy_for_copy(new_obj._opponent) + self._opponent._custom_deepcopy_for_copy(new_obj._opponent, {"partial_env": new_obj, **new_obj._kwargs_opponent}) new_obj._oppSpace = OpponentSpace(compute_budget=new_obj._compute_opp_budget, init_budget=new_obj._opponent_init_budget, @@ -575,8 +579,8 @@ def _custom_deepcopy_for_copy(self, new_obj): budget_per_timestep=new_obj._opponent_budget_per_ts, opponent=new_obj._opponent ) - new_obj._oppSpace.init_opponent(partial_env=new_obj, **new_obj._kwargs_opponent) - new_obj._oppSpace.reset() + state_me, state_opp = self._oppSpace._get_state() + new_obj._oppSpace._set_state(state_me) # voltage new_obj._voltagecontrolerClass = self._voltagecontrolerClass @@ -1019,8 +1023,8 @@ def seed(self, seed=None): # example from gym # self.np_random, seed = seeding.np_random(seed) # inspiration from @ https://github.com/openai/gym/tree/master/gym/utils - - super().seed(seed) + seed_init = seed + super().seed(seed_init) seed_chron = None seed_obs = None seed_action_space = None @@ -1046,7 +1050,7 @@ def seed(self, seed=None): if self._opponent is not None: seed = self.space_prng.randint(max_int) seed_opponent = self._opponent.seed(seed) - return seed, seed_chron, seed_obs, seed_action_space, seed_env_modif, seed_volt_cont, seed_opponent + return seed_init, seed_chron, seed_obs, seed_action_space, seed_env_modif, seed_volt_cont, seed_opponent def deactivate_forecast(self): """ @@ -1941,9 +1945,9 @@ def step(self, action): self.nb_time_step += 1 self._disc_lines[:] = -1 - beg_step = time.time() + beg_step = time.perf_counter() try: - beg_ = time.time() + beg_ = time.perf_counter() is_legal, reason = self._game_rules(action=action, env=self) if not is_legal: @@ -2030,7 +2034,7 @@ def step(self, action): gen_up_before = self._gen_activeprod_t > 0. # compute the redispatching and the new productions active setpoint - beg__redisp = time.time() + beg__redisp = time.perf_counter() already_modified_gen = self._get_already_modified_gen(action) valid_disp, except_tmp, info_ = self._prepare_redisp(action, new_p, already_modified_gen) @@ -2070,7 +2074,7 @@ def step(self, action): is_illegal_reco = True action = self._action_space({}) except_.append(except_tmp) - self._time_redisp += time.time() - beg__redisp + self._time_redisp += time.perf_counter() - beg__redisp # make sure the dispatching action is not implemented "as is" by the backend. # the environment must make sure it's a zero-sum action. @@ -2092,7 +2096,7 @@ def step(self, action): # have the opponent here # TODO code the opponent part here and split more the timings! here "opponent time" is # TODO included in time_apply_act - tick = time.time() + tick = time.perf_counter() attack, attack_duration = self._oppSpace.attack(observation=self.current_obs, agent_action=action, env_action=self._env_modification) @@ -2106,18 +2110,18 @@ def step(self, action): self._times_before_topology_actionable[subs_attacked] = \ np.maximum(attack_duration, self._times_before_topology_actionable[subs_attacked]) self._backend_action += attack - self._time_opponent += time.time() - tick + self._time_opponent += time.perf_counter() - tick self.backend.apply_action(self._backend_action) - self._time_apply_act += time.time() - beg_ + self._time_apply_act += time.perf_counter() - beg_ try: # compute the next _grid state - beg_pf = time.time() + beg_pf = time.perf_counter() disc_lines, detailed_info, conv_ = self.backend.next_grid_state(env=self, is_dc=self._env_dc) self._disc_lines[:] = disc_lines - self._time_powerflow += time.time() - beg_pf + self._time_powerflow += time.perf_counter() - beg_pf if conv_ is None: - beg_res = time.time() + beg_res = time.perf_counter() self.backend.update_thermal_limit(self) # update the thermal limit, for DLR for example overflow_lines = self.backend.get_line_overflow() # save the current topology as "last" topology (for connected powerlines) @@ -2165,7 +2169,7 @@ def step(self, action): # TODO storage: get back the result of the storage ! with the illegal action when a storage unit # TODO is non zero and disconnected, this should be ok. - self._time_extract_obs += time.time() - beg_res + self._time_extract_obs += time.perf_counter() - beg_res has_error = False except Grid2OpException as exc_: @@ -2176,7 +2180,7 @@ def step(self, action): except StopIteration: # episode is over is_done = True - end_step = time.time() + end_step = time.perf_counter() self._time_step += end_step - beg_step self._backend_action.reset() if conv_ is not None: @@ -2317,42 +2321,43 @@ def close(self): raise EnvError("This environment is closed already, you cannot close it a second time.") # todo there might be some side effect - if self.viewer is not None: + if hasattr(self, "viewer") and self.viewer is not None: self.viewer = None self.viewer_fig = None - if self.backend is not None: + if hasattr(self, "backend") and self.backend is not None: self.backend.close() - del self.backend + del self.backend self.backend = None - if self.observation_space is not None: + if hasattr(self, "observation_space") and self.observation_space is not None: # do not forget to close the backend of the observation (used for simulate) self.observation_space.close() - if self._voltage_controler is not None: + if hasattr(self, "_voltage_controler") and self._voltage_controler is not None: # in case there is a backend in the voltage controler self._voltage_controler.close() - if self._oppSpace is not None: + if hasattr(self, "_oppSpace") and self._oppSpace is not None: # in case there is a backend in the opponent space self._oppSpace.close() - if self._helper_action_env is not None: + if hasattr(self, "_helper_action_env") and self._helper_action_env is not None: # close the action helper self._helper_action_env.close() - if self.action_space is not None: + if hasattr(self, "action_space") and self.action_space is not None: # close the action space if needed self.action_space.close() - if self._reward_helper is not None: + if hasattr(self, "_reward_helper") and self._reward_helper is not None: # close the reward if needed self._reward_helper.close() - for el, rew in self.other_rewards.items(): - # close the "other rewards" - rew.close() + if hasattr(self, "other_rewards"): + for el, rew in self.other_rewards.items(): + # close the "other rewards" + rew.close() self.backend = None self.__is_init = False @@ -2383,7 +2388,8 @@ def close(self): "_storage_power", "_limit_curtailment", "_gen_before_curtailment", "_sum_curtailment_mw", "_sum_curtailment_mw_prev", "_has_attention_budget", "_attention_budget", "_attention_budget_cls", "_is_alarm_illegal", "_is_alarm_used_in_reward", "_kwargs_attention_budget"]: - delattr(self, attr_nm) + if hasattr(self, attr_nm): + delattr(self, attr_nm) setattr(self, attr_nm, None) def attach_layout(self, grid_layout): @@ -2538,7 +2544,7 @@ def get_current_line_status(self): @property def parameters(self): """ - return a deepcopy of the parameters used by the environment + Return a deepcopy of the parameters used by the environment It is a deepcopy, so modifying it will have absolutely no effect. diff --git a/grid2op/Environment/BaseMultiProcessEnv.py b/grid2op/Environment/BaseMultiProcessEnv.py index 55803669d..21ec99a47 100644 --- a/grid2op/Environment/BaseMultiProcessEnv.py +++ b/grid2op/Environment/BaseMultiProcessEnv.py @@ -140,7 +140,7 @@ def run(self): self.remote.send((self.env.observation_space, self.env.action_space)) elif cmd == 's': # perform a step - beg_ = time.time() + beg_ = time.perf_counter() if data is None: data = self.env.action_space() else: @@ -157,7 +157,7 @@ def run(self): if not self.return_info: info = None - end_ = time.time() + end_ = time.perf_counter() self._comp_time += end_ - beg_ self.remote.send((res_obs, reward, done, info)) elif cmd == 'r': diff --git a/grid2op/Environment/Environment.py b/grid2op/Environment/Environment.py index 3dc28b8ee..ec0a0234f 100644 --- a/grid2op/Environment/Environment.py +++ b/grid2op/Environment/Environment.py @@ -92,6 +92,7 @@ def __init__(self, _raw_backend_class=None, _compat_glop_version=None, _read_from_local_dir=True, # TODO runner and all here ! + _is_test=False ): BaseEnv.__init__(self, init_grid_path=init_grid_path, @@ -114,6 +115,7 @@ def __init__(self, attention_budget_cls=attention_budget_cls, kwargs_attention_budget=kwargs_attention_budget, logger=logger.getChild("grid2op_Environment") if logger is not None else None, + _is_test=_is_test, # is this created with "test=True" # TODO not implemented !! ) if name == "unknown": warnings.warn("It is NOT recommended to create an environment without \"make\" and EVEN LESS " @@ -155,9 +157,6 @@ def _init_backend(self, chronics_handler, backend, """ if isinstance(rewardClass, type): - # raise Grid2OpException("Parameter \"rewardClass\" used to build the Environment should be a type (a class) " - # "and not an object (an instance of a class). " - # "It is currently \"{}\"".format(type(rewardClass))) if not issubclass(rewardClass, BaseReward): raise Grid2OpException("Parameter \"rewardClass\" used to build the Environment should derived form " "the grid2op.BaseReward class, type provided is \"{}\"".format(type(rewardClass))) @@ -234,14 +233,13 @@ def _init_backend(self, chronics_handler, backend, type(actionClass))) if not isinstance(observationClass, type): - raise Grid2OpException("Parameter \"actionClass\" used to build the Environment should be a type (a class) " - "and not an object (an instance of a class). " - "It is currently \"{}\"".format(type(legalActClass))) + raise Grid2OpException(f"Parameter \"observationClass\" used to build the Environment should be a type (a class) " + f"and not an object (an instance of a class). " + f"It is currently : {observationClass} (type \"{type(observationClass)}\")") if not issubclass(observationClass, BaseObservation): raise Grid2OpException( - "Parameter \"observationClass\" used to build the Environment should derived form the " - "grid2op.BaseObservation class, type provided is \"{}\"".format( - type(observationClass))) + f"Parameter \"observationClass\" used to build the Environment should derived form the " + f"grid2op.BaseObservation class, type provided is \"{type(observationClass)}\"") # action affecting the grid that will be made by the agent bk_type = type(self.backend) # be careful here: you need to initialize from the class, and not from the object @@ -764,7 +762,6 @@ def reset(self): # reset the opponent self._oppSpace.reset() - # reset, if need, reward and other rewards self._reward_helper.reset(self) for extra_reward in self.other_rewards.values(): @@ -945,7 +942,10 @@ def train_val_split(self, val_scen_id, add_for_train="train", add_for_val="val", - remove_from_name=None): + add_for_test=None, + test_scen_id=None, + remove_from_name=None, + deep_copy=False): """ This function is used as :func:`Environment.train_val_split_random`. @@ -956,6 +956,13 @@ def train_val_split(self, ---------- val_scen_id: ``list`` List of the scenario names that will be placed in the validation set + + test_scen_id: ``list`` + + .. versionadded:: 2.6.5 + + List of the scenario names that will be placed in the test set (only used + if add_for_test is not None - and mandatory in this case) add_for_train: ``str`` See :func:`Environment.train_val_split_random` for more information @@ -963,9 +970,21 @@ def train_val_split(self, add_for_val: ``str`` See :func:`Environment.train_val_split_random` for more information + add_for_test: ``str`` + + .. versionadded:: 2.6.5 + + See :func:`Environment.train_val_split_random` for more information + remove_from_name: ``str`` See :func:`Environment.train_val_split_random` for more information + deep_copy: ``bool`` + + .. versionadded:: 2.6.5 + + See :func:`Environment.train_val_split_random` for more information + Returns ------- nm_train: ``str`` @@ -974,12 +993,75 @@ def train_val_split(self, nm_val: ``str`` See :func:`Environment.train_val_split_random` for more information + nm_test: ``str``, optionnal + + .. versionadded:: 2.6.5 + + See :func:`Environment.train_val_split_random` for more information + Examples -------- A full example on a training / validation / test split with explicit specification of which chronics goes in which scenarios is: + + .. code-block:: python + + import grid2op + import os + + env_name = "l2rpn_case14_sandbox" # or any other... + env = grid2op.make(env_name) + + # retrieve the names of the chronics: + full_path_data = env.chronics_handler.subpaths + chron_names = [os.path.split(el)[-1] for el in full_path_data] + + + # splitting into training / test, keeping the "last" 10 chronics to the test set + nm_env_train, m_env_val, nm_env_test = env.train_val_split(test_scen_id=chron_names[-10:], # last 10 in test set + add_for_test="test", + val_scen_id=chron_names[-20:-10], # last 20 to last 10 in val test + ) + + env_train = grid2op.make(env_name+"_train") + env_val = grid2op.make(env_name+"_val") + env_test = grid2op.make(env_name+"_test") + + For a more simple example, with less parametrization and with random assignment (recommended), + please refer to the help of :func:`Environment.train_val_split_random` + + **NB** read the "Notes" of this section for possible "unexpected" behaviour of the code snippet above. + + On Some windows based platform, if you don't have an admin account nor a + "developer" account (see https://docs.python.org/3/library/os.html#os.symlink) + you might need to do: + + .. code-block:: python + + import grid2op + import os + + env_name = "l2rpn_case14_sandbox" # or any other... + env = grid2op.make(env_name) + + # retrieve the names of the chronics: + full_path_data = env.chronics_handler.subpaths + chron_names = [os.path.split(el)[-1] for el in full_path_data] + + + # splitting into training / test, keeping the "last" 10 chronics to the test set + nm_env_train, m_env_val, nm_env_test = env.train_val_split(test_scen_id=chron_names[-10:], # last 10 in test set + add_for_test="test", + val_scen_id=chron_names[-20:-10], # last 20 to last 10 in val test + deep_copy=True) + + .. warning:: + The above code will use much more memory on your hard drive than the version using symbolic links. + It will also be significantly slower ! + As an "historical curiosity", this is what you needed to do in grid2op version < 1.6.5: + .. code-block:: python import grid2op @@ -1006,14 +1088,9 @@ def train_val_split(self, remove_from_name="_trainval$") # and now you can use the following code to load the environments: - env_train = grid2op.make(nm_env+"_train") - env_val = grid2op.make(nm_env+"_val") - env_test = grid2op.make(nm_env+"_test") - - For a more simple example, with less parametrization and with random assignment (recommended), - please refer to the help of :func:`Environment.train_val_split_random` - - **NB** read the "Notes" of this section for possible "unexpected" behaviour of the code snippet above. + env_train = grid2op.make(env_name+"_train") + env_val = grid2op.make(env_name+"_val") + env_test = grid2op.make(env_name+"_test") Notes ------ @@ -1036,7 +1113,17 @@ def train_val_split(self, if re.match(self.REGEX_SPLIT, add_for_val) is None: raise EnvError(f"The suffixes you can use for validation data (add_for_val)" f"should match the regex \"{self.REGEX_SPLIT}\"") + if add_for_test is not None: + if re.match(self.REGEX_SPLIT, add_for_test) is None: + raise EnvError(f"The suffixes you can use for test data (add_for_test)" + f"should match the regex \"{self.REGEX_SPLIT}\"") + if add_for_test is None and test_scen_id is not None: + raise EnvError(f"add_for_test is None and test_scen_id is not None.") + + if add_for_test is not None and test_scen_id is None: + raise EnvError(f"add_for_test is not None and test_scen_id is None.") + from grid2op.Chronics import MultifolderWithCache, Multifolder if not isinstance(self.chronics_handler.real_data, (MultifolderWithCache, Multifolder)): raise EnvError("It does not make sense to split a environment between training / validation " @@ -1050,12 +1137,20 @@ def train_val_split(self, raise EnvError("The suffixes you can remove from the name of the environment (remove_from_name)" "should match the regex \"^[a-zA-Z0-9^$_]*$\"") my_name = re.sub(remove_from_name, "", my_name) - nm_train = f'{my_name}_{add_for_train}' path_train = os.path.join(path_train[0], nm_train) + path_val = os.path.split(my_path) nm_val = f'{my_name}_{add_for_val}' path_val = os.path.join(path_val[0], nm_val) + + nm_test = None + path_test = None + if add_for_test is not None: + path_test = os.path.split(my_path) + nm_test = f'{my_name}_{add_for_test}' + path_test = os.path.join(path_test[0], nm_test) + chronics_dir = self._chronics_folder_name() # create the folder @@ -1071,39 +1166,73 @@ def train_val_split(self, f"continue either delete the folder \"{path_train}\" or name your training environment " f" differently " f"using the \"add_for_train\" keyword argument of this function.") + + if nm_test is not None and os.path.exists(path_test): + raise RuntimeError(f"Impossible to create the test environment that should have the name " + f"\"{nm_test}\" because an environment is already named this way. If you want to " + f"continue either delete the folder \"{path_test}\" or name your test environment " + f" differently " + f"using the \"add_for_test\" keyword argument of this function.") + os.mkdir(path_val) os.mkdir(path_train) + if nm_test is not None: + os.mkdir(path_test) # assign which chronics goes where chronics_path = os.path.join(my_path, chronics_dir) all_chron = sorted(os.listdir(chronics_path)) to_val = set(val_scen_id) - # copy the files + if nm_test is not None: + to_test = set(test_scen_id) + + if deep_copy: + import shutil + copy_file_fun = shutil.copy2 + copy_dir_fun = shutil.copytree + else: + copy_file_fun = os.symlink + copy_dir_fun = os.symlink + + # "copy" the files for el in os.listdir(my_path): tmp_path = os.path.join(my_path, el) if os.path.isfile(tmp_path): # this is a regular env file - os.symlink(tmp_path, os.path.join(path_train, el)) - os.symlink(tmp_path, os.path.join(path_val, el)) + copy_file_fun(tmp_path, os.path.join(path_train, el)) + copy_file_fun(tmp_path, os.path.join(path_val, el)) + if nm_test is not None: + copy_file_fun(tmp_path, os.path.join(path_test, el)) elif os.path.isdir(tmp_path): if el == chronics_dir: # this is the chronics folder os.mkdir(os.path.join(path_train, chronics_dir)) os.mkdir(os.path.join(path_val, chronics_dir)) + if nm_test is not None: + os.mkdir(os.path.join(path_test, chronics_dir)) for chron_name in all_chron: tmp_path_chron = os.path.join(tmp_path, chron_name) if chron_name in to_val: - os.symlink(tmp_path_chron, os.path.join(path_val, chronics_dir, chron_name)) + copy_dir_fun(tmp_path_chron, os.path.join(path_val, chronics_dir, chron_name)) + elif chron_name in to_test: + copy_dir_fun(tmp_path_chron, os.path.join(path_test, chronics_dir, chron_name)) else: - os.symlink(tmp_path_chron, os.path.join(path_train, chronics_dir, chron_name)) - return nm_train, nm_val + copy_dir_fun(tmp_path_chron, os.path.join(path_train, chronics_dir, chron_name)) + if add_for_test is None: + res = nm_train, nm_val + else: + res = nm_train, nm_val, nm_test + return res def train_val_split_random(self, pct_val=10., add_for_train="train", add_for_val="val", - remove_from_name=None): + add_for_test=None, + pct_test=None, + remove_from_name=None, + deep_copy=False): """ By default a grid2op environment contains multiple "scenarios" containing values for all the producers and consumers representing multiple days. In a "game like" environment, you can think of the scenarios as @@ -1140,11 +1269,51 @@ def train_val_split_random(self, Suffix that will be added to the name of the environment for the validation set. We don't recommend to modify the default value ("val") + add_for_test: ``str``, (optional) + + .. versionadded:: 2.6.5 + + Suffix that will be added to the name of the environment for the test set. By default, + it only splits into training and validation, so this is ignored. We recommend + to assign it to "test" if you want to split into training / validation and test. + If it is set, then the `pct_test` must also be set. + + pct_test: ``float``, (optional) + + .. versionadded:: 2.6.5 + + Percentage of chronics that will go to the test set. + For 10% of the chronics, set it to 10. and NOT to 0.1. + (If you set it, you need to set the `add_for_test` argument.) + remove_from_name: ``str`` If you "split" an environment multiple times, this allows you to keep "short" names (for example you will be able to call `grid2op.make(env_name+"_train")` instead of `grid2op.make(env_name+"_train_train")`) + deep_copy: ``bool`` + + .. versionadded:: 2.6.5 + + A function to specify to "copy" the elements of the original + environment to the created one. By default it will save as + much memory as possible using symbolic links (rather than performing + copies). By default it does use symbolic links (`deep_copy=False`). + + .. note:: + If set to ``True`` the new environment will take much more space + on the hard drive, and the execution of this function will + be much slower ! + + .. warning:: + On windows based system, you will most likely run into issues + if you don't set this parameters. + Indeed, Windows does not link symbolink links + (https://docs.python.org/3/library/os.html#os.symlink). + In this case, you can use the ``deep_copy=True`` and + it will work fine (examples in the function + :func:`Environment.train_val_split`) + Returns ------- nm_train: ``str`` @@ -1153,6 +1322,13 @@ def train_val_split_random(self, nm_val: ``str`` Complete name of the "validation" environment + nm_test: ``str``, optionnal + + .. versionadded:: 2.6.5 + + Complete name of the "test" environment. It is only returned if + `add_for_test` and `pct_test` are not `None`. + Examples -------- This function can be used like: @@ -1181,10 +1357,34 @@ def train_val_split_random(self, env_name_train = "l2rpn_case14_sandbox_train" # depending on the option you passed above env_train = grid2op.make(env_name_train) + .. versionadded:: 2.6.5 + Possibility to create a training, validation AND test set. + + If you have grid2op version >= 1.6.5, you can also use the following: + + .. code-block:: python + + import grid2op + env_name = "l2rpn_case14_sandbox" # or any other... + env = grid2op.make(env_name) + + # extract 1% of the "chronics" to be used in the validation environment. The other 99% will + # be used for test + nm_env_train, nm_env_val, nm_env_test = env.train_val_split_random(pct_val=1., pct_test=1.) + + # and now you can use the training set only to train your agent: + print(f"The name of the training environment is \\"{nm_env_train}\\"") + print(f"The name of the validation environment is \\"{nm_env_val}\\"") + print(f"The name of the test environment is \\"{nm_env_test}\\"") + env_train = grid2op.make(nm_env_train) + + .. warning:: + In this case this function returns 3 elements and not 2 ! + Notes ----- This function will fail if an environment already exists with one of the name that would be given - to the training environment or the validation environment. + to the training environment or the validation environment (or test environment). """ if re.match(self.REGEX_SPLIT, add_for_train) is None: @@ -1194,14 +1394,32 @@ def train_val_split_random(self, raise EnvError("The suffixes you can use for validation data (add_for_val)" "should match the regex \"{self.REGEX_SPLIT}\"") + if add_for_test is None and pct_test is not None: + raise EnvError(f"add_for_test is None and pct_test is not None.") + + if add_for_test is not None and pct_test is None: + raise EnvError(f"add_for_test is not None and pct_test is None.") + + my_path = self.get_path_env() chronics_path = os.path.join(my_path, self._chronics_folder_name()) all_chron = sorted(os.listdir(chronics_path)) - to_val = self.space_prng.choice(all_chron, int(len(all_chron) * pct_val * 0.01)) + nb_init = len(all_chron) + to_val = self.space_prng.choice(all_chron, int(nb_init * pct_val * 0.01), replace=False) + + test_scen_id = None + if pct_test is not None: + all_chron = set(all_chron) - set(to_val) + all_chron = list(all_chron) + test_scen_id = self.space_prng.choice(all_chron, int(nb_init * pct_test * 0.01), replace=False) + return self.train_val_split(to_val, add_for_train=add_for_train, add_for_val=add_for_val, - remove_from_name=remove_from_name) + remove_from_name=remove_from_name, + add_for_test=add_for_test, + test_scen_id=test_scen_id, + deep_copy=deep_copy) def get_params_for_runner(self): """ @@ -1265,4 +1483,5 @@ def get_params_for_runner(self): res["has_attention_budget"] = self._has_attention_budget res["_read_from_local_dir"] = self._read_from_local_dir res["logger"] = self.logger + res["_is_test"] = self._is_test # TODO not implemented !! return res diff --git a/grid2op/Episode/EpisodeReplay.py b/grid2op/Episode/EpisodeReplay.py index 729e2e778..9740a9cbd 100644 --- a/grid2op/Episode/EpisodeReplay.py +++ b/grid2op/Episode/EpisodeReplay.py @@ -132,7 +132,7 @@ def replay_episode(self, episode_id, fps=2.0, gif_name=None, if end_step > 0 and step >= end_step: break # Get a timestamp for current frame - start_time = time.time() + start_time = time.perf_counter() # Render the observation fig = plot_runner.plot_obs(observation=obs, @@ -153,7 +153,7 @@ def replay_episode(self, episode_id, fps=2.0, gif_name=None, frames.append(plot_runner.convert_figure_to_numpy_HWC(figure)) # Get the timestamp after frame is rendered - end_time = time.time() + end_time = time.perf_counter() delta_time = end_time - start_time # Cap fps for display mode diff --git a/grid2op/Exceptions/ObservationExceptions.py b/grid2op/Exceptions/ObservationExceptions.py index 9985dae15..df1368a62 100644 --- a/grid2op/Exceptions/ObservationExceptions.py +++ b/grid2op/Exceptions/ObservationExceptions.py @@ -20,3 +20,28 @@ class NoForecastAvailable(Grid2OpException): In that case it is not possible to use the :func:`grid2op.Observation.BaseObservation.forecasts` method. """ pass + +class SimulateUsedTooMuch(Grid2OpException): + pass + +class SimulateUsedTooMuchThisStep(SimulateUsedTooMuch): + """ + This exception is raised by the :class:`grid2op.Observation.BaseObservation` when using "obs.simulate(...)". + + It is raised when the total number of calls to `obs.simulate(...)` exceeds the maximum number of allowed + calls to it, for a given step. + + You can do more "obs.simulate" if you call "env.step". + """ + pass + +class SimulateUsedTooMuchThisEpisode(SimulateUsedTooMuch): + """ + This exception is raised by the :class:`grid2op.Observation.BaseObservation` when using "obs.simulate(...)". + + It is raised when the total number of calls to `obs.simulate(...)` exceeds the maximum number of allowed + calls to it for this episode. + + The only way to use "obs.simulate(...)" again is to call "env.reset(...)" + """ + pass diff --git a/grid2op/Exceptions/__init__.py b/grid2op/Exceptions/__init__.py index 857dab0c2..7dec9398c 100644 --- a/grid2op/Exceptions/__init__.py +++ b/grid2op/Exceptions/__init__.py @@ -39,6 +39,8 @@ "NonFiniteElement", "DivergingPowerFlow", "NoForecastAvailable", + "SimulateUsedTooMuchThisStep", + "SimulateUsedTooMuchThisEpisode", "ChronicsError", "ChronicsNotFoundError", "InsufficientData", @@ -95,6 +97,8 @@ from grid2op.Exceptions.PowerflowExceptions import DivergingPowerFlow from grid2op.Exceptions.ObservationExceptions import NoForecastAvailable +from grid2op.Exceptions.ObservationExceptions import SimulateUsedTooMuchThisStep +from grid2op.Exceptions.ObservationExceptions import SimulateUsedTooMuchThisEpisode from grid2op.Exceptions.ChronicsExceptions import ChronicsError from grid2op.Exceptions.ChronicsExceptions import ChronicsNotFoundError diff --git a/grid2op/MakeEnv/Make.py b/grid2op/MakeEnv/Make.py index 740ab0ffb..6d84145a2 100644 --- a/grid2op/MakeEnv/Make.py +++ b/grid2op/MakeEnv/Make.py @@ -295,19 +295,22 @@ def make(dataset="rte_case14_realistic", make_from_path_fn = _aux_make_multimix elif _aux_is_multimix(dataset) and test_tmp: def make_from_path_fn_(*args, **kwargs): + if not "logger" in kwargs: + kwargs["logger"] = logger + if not "experimental_read_from_local_dir" in kwargs: + kwargs["experimental_read_from_local_dir"] = experimental_read_from_local_dir return _aux_make_multimix(*args, test=True, - logger=logger, - experimental_read_from_local_dir=experimental_read_from_local_dir, **kwargs) make_from_path_fn = make_from_path_fn_ - + if not "logger" in kwargs: + kwargs["logger"] = logger + if not "experimental_read_from_local_dir" in kwargs: + kwargs["experimental_read_from_local_dir"] = experimental_read_from_local_dir return make_from_path_fn(dataset_path=dataset, - logger=logger, _add_to_name=_add_to_name_tmp, _compat_glop_version=_compat_glop_version_tmp, - experimental_read_from_local_dir=experimental_read_from_local_dir, **kwargs) # Not a path: get the dataset name and cache path diff --git a/grid2op/Observation/_ObsEnv.py b/grid2op/Observation/_ObsEnv.py index bbf57f85e..051d4be4e 100644 --- a/grid2op/Observation/_ObsEnv.py +++ b/grid2op/Observation/_ObsEnv.py @@ -63,6 +63,7 @@ def __init__(self, attention_budget_cls=LinearAttentionBudget, kwargs_attention_budget={}, _complete_action_cls=None, + _ptr_orig_obs_space=None ): BaseEnv.__init__(self, init_grid_path, @@ -110,6 +111,7 @@ def __init__(self, self._complete_action_cls = _complete_action_cls self._action_space = action_helper # obs env and env share the same action space self._observation_space = action_helper # not used here, so it's definitely a hack ! + self._ptr_orig_obs_space = _ptr_orig_obs_space #### self.no_overflow_disconnection = parameters.NO_OVERFLOW_DISCONNECTION @@ -461,6 +463,10 @@ def simulate(self, action): - "is_ambiguous" (``bool``) whether the action given as input was ambiguous. """ + self._ptr_orig_obs_space.simulate_called() + maybe_exc = self._ptr_orig_obs_space.can_use_simulate() + if maybe_exc is not None: + raise maybe_exc self._reset_to_orig_state() obs, reward, done, info = self.step(action) return obs, reward, done, info @@ -571,6 +577,6 @@ def close(self): "_storage_current_charge_init", "_storage_previous_charge_init", "_limit_curtailment_init", "_gen_before_curtailment_init", "_sum_curtailment_mw_init", "_sum_curtailment_mw_prev_init", "_nb_time_step_init", "_attention_budget_state_init", - "_max_episode_duration"]: + "_max_episode_duration", "_ptr_orig_obs_space"]: delattr(self, attr_nm) setattr(self, attr_nm, None) diff --git a/grid2op/Observation/__init__.py b/grid2op/Observation/__init__.py index 5626f9bb3..bcf0238e4 100644 --- a/grid2op/Observation/__init__.py +++ b/grid2op/Observation/__init__.py @@ -1,14 +1,23 @@ +# 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. + __all__ = [ # private "_ObsEnv", # real export "CompleteObservation", + # "NoisyObservation", "BaseObservation", "ObservationSpace" ] - -from grid2op.Observation.CompleteObservation import CompleteObservation +from grid2op.Observation.completeObservation import CompleteObservation +# from grid2op.Observation.noisyObservation import NoisyObservation from grid2op.Observation._ObsEnv import _ObsEnv -from grid2op.Observation.BaseObservation import BaseObservation -from grid2op.Observation.ObservationSpace import ObservationSpace +from grid2op.Observation.baseObservation import BaseObservation +from grid2op.Observation.observationSpace import ObservationSpace diff --git a/grid2op/Observation/BaseObservation.py b/grid2op/Observation/baseObservation.py similarity index 99% rename from grid2op/Observation/BaseObservation.py rename to grid2op/Observation/baseObservation.py index 2d7697d18..5419d061d 100644 --- a/grid2op/Observation/BaseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -10,13 +10,13 @@ import datetime import warnings import networkx -import numpy as np from abc import abstractmethod +import numpy as np +from scipy.sparse import csr_matrix from grid2op.dtypes import dt_int, dt_float, dt_bool -from grid2op.Exceptions import * +from grid2op.Exceptions import Grid2OpException, NoForecastAvailable, EnvError from grid2op.Space import GridObjects -from scipy.sparse import csr_matrix # TODO have a method that could do "forecast" by giving the _injection by the agent, # TODO if he wants to make custom forecasts @@ -195,6 +195,9 @@ class BaseObservation(GridObjects): max_step: ``int`` Maximum number of steps possible for this episode + delta_time: ``float`` + Time (in minutes) between the last step and the current step (usually constant in an episode, even in an environment) + is_alarm_illegal: ``bool`` whether the last alarm has been illegal (due to budget constraint). It can only be ``True`` if an alarm was raised by the agent on the previous step. Otherwise it is always ``False`` @@ -355,10 +358,11 @@ def __init__(self, # counter self.current_step = dt_int(0) self.max_step = dt_int(np.iinfo(dt_int).max) + self.delta_time = dt_float(5.) def _aux_copy(self, other): attr_simple = ["max_step", "current_step", "support_theta", "day_of_week", - "minute_of_hour", "hour_of_day", "day", "month", "year"] + "minute_of_hour", "hour_of_day", "day", "month", "year", "delta_time"] attr_vect = ["storage_theta", "gen_theta", "load_theta", "theta_ex", "theta_or", "curtailment_limit", "curtailment", "gen_p_before_curtail", "_thermal_limit", "is_alarm_illegal", @@ -711,6 +715,19 @@ def process_grid2op_compat(cls): pass cls.attr_list_set = set(cls.attr_list_vect) + if cls.glop_version < "1.6.5" or cls.glop_version == cls.BEFORE_COMPAT_VERSION: + # "current_step", "max_step" were added in grid2Op 1.6.5 + cls.attr_list_vect = copy.deepcopy(cls.attr_list_vect) + cls.attr_list_set = copy.deepcopy(cls.attr_list_set) + + for el in ["delta_time"]: + try: + cls.attr_list_vect.remove(el) + except ValueError as exc_: + # this attribute was not there in the first place + pass + cls.attr_list_set = set(cls.attr_list_vect) + def reset(self): """ INTERNAL @@ -805,6 +822,7 @@ def reset(self): self.current_step = dt_int(0) self.max_step = dt_int(np.iinfo(dt_int).max) + self.delta_time = dt_float(5.) def set_game_over(self, env=None): """ @@ -1015,7 +1033,7 @@ def __sub__(self, other): """ same_grid = type(self).same_grid_class(type(other)) if not same_grid: - raise RuntimeError("Cannot compare to observation not coming from the same powergrid.") + raise Grid2OpException("Cannot compare to observation not coming from the same powergrid.") tmp_obs_env = self._obs_env self._obs_env = None # keep aside the backend res = copy.deepcopy(self) diff --git a/grid2op/Observation/CompleteObservation.py b/grid2op/Observation/completeObservation.py similarity index 96% rename from grid2op/Observation/CompleteObservation.py rename to grid2op/Observation/completeObservation.py index 43f1971c8..46cb7bfe5 100644 --- a/grid2op/Observation/CompleteObservation.py +++ b/grid2op/Observation/completeObservation.py @@ -9,7 +9,7 @@ import numpy as np from grid2op.dtypes import dt_int, dt_float -from grid2op.Observation.BaseObservation import BaseObservation +from grid2op.Observation.baseObservation import BaseObservation class CompleteObservation(BaseObservation): @@ -123,11 +123,16 @@ class CompleteObservation(BaseObservation): "is_alarm_illegal", "time_since_last_alarm", "last_alarm", "attention_budget", "was_alarm_used_after_game_over", "_shunt_p", "_shunt_q", "_shunt_v", "_shunt_bus", # starting from grid2op version 1.6.0 - "current_step", "max_step" # starting from grid2op version 1.6.4 + "current_step", "max_step", # starting from grid2op version 1.6.4 + "delta_time" # starting grid2op version 1.6.5 ] attr_list_json = ["_thermal_limit", "support_theta", - "theta_or", "theta_ex", "load_theta", "gen_theta", "storage_theta"] + "theta_or", + "theta_ex", + "load_theta", + "gen_theta", + "storage_theta"] attr_list_set = set(attr_list_vect) def __init__(self, @@ -235,3 +240,5 @@ def update(self, env, with_forecast=True): self.time_since_last_alarm[:] = -1 self.last_alarm[:] = env._attention_budget.last_successful_alarm_raised self.attention_budget[:] = env._attention_budget.current_budget + + self.delta_time = dt_float(1.0 * env.delta_time_seconds / 60.) diff --git a/grid2op/Observation/ObservationSpace.py b/grid2op/Observation/observationSpace.py similarity index 85% rename from grid2op/Observation/ObservationSpace.py rename to grid2op/Observation/observationSpace.py index e5d695538..7258b9b7a 100644 --- a/grid2op/Observation/ObservationSpace.py +++ b/grid2op/Observation/observationSpace.py @@ -9,9 +9,9 @@ import sys import copy -from grid2op.Observation.SerializableObservationSpace import SerializableObservationSpace +from grid2op.Observation.serializableObservationSpace import SerializableObservationSpace from grid2op.Reward import RewardHelper -from grid2op.Observation.CompleteObservation import CompleteObservation +from grid2op.Observation.completeObservation import CompleteObservation from grid2op.Observation._ObsEnv import _ObsEnv @@ -76,6 +76,8 @@ def __init__(self, SerializableObservationSpace.__init__(self, gridobj, observationClass=observationClass) self.with_forecast = with_forecast self._simulate_parameters = copy.deepcopy(env.parameters) + self._legal_action = env._game_rules.legal_action + self._env_param = copy.deepcopy(env.parameters) if rewardClass is None: self._reward_func = env._reward_helper.template_reward @@ -112,12 +114,40 @@ def __init__(self, kwargs_attention_budget=env._kwargs_attention_budget, max_episode_duration=env.max_episode_duration(), _complete_action_cls=env._complete_action_cls, + _ptr_orig_obs_space=self, ) for k, v in self.obs_env.other_rewards.items(): v.initialize(env) self._empty_obs = self._template_obj self._update_env_time = 0. + self.__nb_simulate_called_this_step = 0 + self.__nb_simulate_called_this_episode = 0 + + def simulate_called(self): + """ + INTERNAL + + .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ + + Tells this class that the "obs.simulate" function has been called. + """ + self.__nb_simulate_called_this_step += 1 + self.__nb_simulate_called_this_episode += 1 + + @property + def nb_simulate_called_this_episode(self): + return self.__nb_simulate_called_this_episode + + @property + def nb_simulate_called_this_step(self): + return self.__nb_simulate_called_this_step + + def can_use_simulate(self) -> bool: + """ + This checks on the rules if the agent has not made too many calls to "obs.simulate" this step + """ + return self._legal_action.can_use_simulate(self.__nb_simulate_called_this_step, self.__nb_simulate_called_this_episode, self._env_param) def _change_parameters(self, new_param): """ @@ -187,7 +217,7 @@ def __call__(self, env, _update_state=True): res = self.observationClass(obs_env=self.obs_env, action_helper=self.action_helper_env) - + self.__nb_simulate_called_this_step = 0 if _update_state: # TODO how to make sure that whatever the number of time i call "simulate" i still get the same observations # TODO use self.obs_prng when updating actions @@ -207,14 +237,18 @@ def get_empty_observation(self): .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ - return an empty observation, for internal use only.""" + return an empty observation, for internal use only. + """ return copy.deepcopy(self._empty_obs) def reset(self, real_env): """reset the observation space with the new values of the environment""" self.obs_env._reward_helper.reset(real_env) + self.__nb_simulate_called_this_step = 0 + self.__nb_simulate_called_this_episode = 0 for k, v in self.obs_env.other_rewards.items(): v.reset(real_env) + self._env_param = copy.deepcopy(real_env.parameters) def _custom_deepcopy_for_copy(self, new_obj): """implements a faster "res = copy.deepcopy(self)" to use @@ -232,9 +266,12 @@ def _custom_deepcopy_for_copy(self, new_obj): new_obj.action_helper_env = self.action_helper_env # const new_obj.reward_helper = copy.deepcopy(self.reward_helper) new_obj._backend_obs = self._backend_obs # ptr to a backend for simulate - new_obj.obs_env = self.obs_env + new_obj.obs_env = self.obs_env # it is None anyway ! new_obj._update_env_time = self._update_env_time - + new_obj.__nb_simulate_called_this_step = self.__nb_simulate_called_this_step + new_obj.__nb_simulate_called_this_episode = self.__nb_simulate_called_this_episode + new_obj._env_param = copy.deepcopy(self._env_param) + def copy(self, copy_backend=False): """ INTERNAL @@ -264,6 +301,7 @@ def copy(self, copy_backend=False): res.obs_env = obs_env else: res.obs_env = obs_env.copy() + res.obs_env._ptr_orig_obs_space = res res._backend_obs = res.obs_env.backend res._empty_obs = obs_.copy() res._empty_obs._obs_env = res.obs_env diff --git a/grid2op/Observation/SerializableObservationSpace.py b/grid2op/Observation/serializableObservationSpace.py similarity index 98% rename from grid2op/Observation/SerializableObservationSpace.py rename to grid2op/Observation/serializableObservationSpace.py index 1d05af6ff..455f80322 100644 --- a/grid2op/Observation/SerializableObservationSpace.py +++ b/grid2op/Observation/serializableObservationSpace.py @@ -7,7 +7,7 @@ # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. from grid2op.Space import SerializableSpace -from grid2op.Observation.CompleteObservation import CompleteObservation +from grid2op.Observation.completeObservation import CompleteObservation class SerializableObservationSpace(SerializableSpace): diff --git a/grid2op/Opponent/BaseOpponent.py b/grid2op/Opponent/BaseOpponent.py index 936664622..e837a5d86 100644 --- a/grid2op/Opponent/BaseOpponent.py +++ b/grid2op/Opponent/BaseOpponent.py @@ -124,8 +124,10 @@ def set_state(self, my_state): """ pass - def _custom_deepcopy_for_copy(self, new_obj): + def _custom_deepcopy_for_copy(self, new_obj, dict_=None): super()._custom_deepcopy_for_copy(new_obj) + if dict_ is None: + dict_ = {} new_obj.action_space = self.action_space # const new_obj._do_nothing = new_obj.action_space() new_obj.set_state(self.get_state()) diff --git a/grid2op/Opponent/GeometricOpponent.py b/grid2op/Opponent/GeometricOpponent.py index 16f77c918..db59551ce 100644 --- a/grid2op/Opponent/GeometricOpponent.py +++ b/grid2op/Opponent/GeometricOpponent.py @@ -7,6 +7,7 @@ # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. import warnings +import copy import numpy as np from grid2op.dtypes import dt_int @@ -295,3 +296,19 @@ def set_state(self, my_state): self._attack_waiting_times = 1 * _attack_waiting_times self._attack_durations = 1 * _attack_durations self._number_of_attacks = 1 * _number_of_attacks + + def _custom_deepcopy_for_copy(self, new_obj, dict_=None): + super()._custom_deepcopy_for_copy(new_obj, dict_) + if dict_ is None: + dict_ = {} + + new_obj._attacks = copy.deepcopy(self._attacks) + new_obj._lines_ids = copy.deepcopy(self._lines_ids) + new_obj._next_attack_time = copy.deepcopy(self._next_attack_time) + new_obj._attack_hazard_rate = copy.deepcopy(self._attack_hazard_rate) + new_obj._recovery_minimum_duration = copy.deepcopy(self._recovery_minimum_duration) + new_obj._recovery_rate = copy.deepcopy(self._recovery_rate) + new_obj._pmax_pmin_ratio = copy.deepcopy(self._pmax_pmin_ratio) + new_obj._attack_counter = copy.deepcopy(self._attack_counter) + new_obj._episode_max_time = copy.deepcopy(self._episode_max_time) + new_obj._env = dict_["partial_env"] # I need to keep a pointer to the environment for computing the maximum length of the episode diff --git a/grid2op/Opponent/OpponentSpace.py b/grid2op/Opponent/OpponentSpace.py index f97dbebd2..754c881d2 100644 --- a/grid2op/Opponent/OpponentSpace.py +++ b/grid2op/Opponent/OpponentSpace.py @@ -104,9 +104,10 @@ def _get_state(self): state_opp = self.opponent.get_state() return state_me, state_opp - def _set_state(self, my_state, opp_state): - # used for simulate - self.opponent.set_state(opp_state) + def _set_state(self, my_state, opp_state=None): + # used for simulate (and for deep copy) + if opp_state is not None: + self.opponent.set_state(opp_state) budget, previous_fails, current_attack_duration, current_attack_cooldown, last_attack = my_state self.budget = budget self.previous_fails = previous_fails diff --git a/grid2op/Opponent/RandomLineOpponent.py b/grid2op/Opponent/RandomLineOpponent.py index b2ddcb964..e0a0f0aef 100644 --- a/grid2op/Opponent/RandomLineOpponent.py +++ b/grid2op/Opponent/RandomLineOpponent.py @@ -7,6 +7,7 @@ # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. import warnings import numpy as np +import copy from grid2op.Opponent import BaseOpponent from grid2op.Exceptions import OpponentError @@ -123,3 +124,11 @@ def attack(self, observation, agent_action, env_action, # Pick a line among the connected lines attack = self.space_prng.choice(self._attacks[status]) return attack, None + + def _custom_deepcopy_for_copy(self, new_obj, dict_=None): + super()._custom_deepcopy_for_copy(new_obj, dict_) + if dict_ is None: + dict_ = {} + + new_obj._attacks = copy.deepcopy(self._attacks) + new_obj._lines_ids = copy.deepcopy(self._lines_ids) diff --git a/grid2op/Opponent/WeightedRandomOpponent.py b/grid2op/Opponent/WeightedRandomOpponent.py index f87793c10..01934bc05 100644 --- a/grid2op/Opponent/WeightedRandomOpponent.py +++ b/grid2op/Opponent/WeightedRandomOpponent.py @@ -7,6 +7,7 @@ # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. import warnings import numpy as np +import copy from grid2op.Opponent import BaseOpponent from grid2op.Exceptions import OpponentError @@ -171,3 +172,14 @@ def attack(self, observation, agent_action, env_action, return None, 0 attack = self.space_prng.choice(available_attacks, p=rho / rho_sum) return attack, None + + def _custom_deepcopy_for_copy(self, new_obj, dict_=None): + super()._custom_deepcopy_for_copy(new_obj, dict_) + if dict_ is None: + dict_ = {} + + new_obj._attacks = copy.deepcopy(self._attacks) + new_obj._lines_ids = copy.deepcopy(self._lines_ids) + new_obj._next_attack_time = copy.deepcopy(self._next_attack_time) + new_obj._attack_period = copy.deepcopy(self._attack_period) + new_obj._rho_normalization = copy.deepcopy(self._rho_normalization) diff --git a/grid2op/Parameters.py b/grid2op/Parameters.py index 724579181..0da220864 100644 --- a/grid2op/Parameters.py +++ b/grid2op/Parameters.py @@ -106,6 +106,12 @@ class Parameters: Number of steps for which it's worth it to give an alarm (if an alarm is send outside of the window `[ALARM_BEST_TIME - ALARM_WINDOW_SIZE, ALARM_BEST_TIME + ALARM_WINDOW_SIZE]` then it does not grant anything + MAX_SIMULATE_PER_STEP: ``int`` + Maximum number of calls to `obs.simuate(...)` allowed per step (reset each "env.step(...)"). Defaults to -1 meaning "as much as you want". + + MAX_SIMULATE_PER_EPISODE: ``int`` + Maximum number of calls to `obs.simuate(...)` allowed per episode (reset each "env.simulate(...)"). Defaults to -1 meaning "as much as you want". + """ def __init__(self, parameters_path=None): """ @@ -161,6 +167,14 @@ def __init__(self, parameters_path=None): # do i take into account the storage loss in the step function self.ACTIVATE_STORAGE_LOSS = True + # alarms + self.ALARM_BEST_TIME = 12 + self.ALARM_WINDOW_SIZE = 12 + + # number of simulate + self.MAX_SIMULATE_PER_STEP = dt_int(-1) + self.MAX_SIMULATE_PER_EPISODE = dt_int(-1) + if parameters_path is not None: if os.path.isfile(parameters_path): self.init_from_json(parameters_path) @@ -168,8 +182,6 @@ def __init__(self, parameters_path=None): warn_msg = "Parameters: the file {} is not found. Continuing with default parameters." warnings.warn(warn_msg.format(parameters_path)) - self.ALARM_BEST_TIME = 12 - self.ALARM_WINDOW_SIZE = 12 @staticmethod def _isok_txt(arg): @@ -263,10 +275,17 @@ def init_from_dict(self, dict_): if "ALARM_WINDOW_SIZE" in dict_: self.ALARM_WINDOW_SIZE = dt_int(dict_["ALARM_WINDOW_SIZE"]) + if "MAX_SIMULATE_PER_STEP" in dict_: + self.MAX_SIMULATE_PER_STEP = dt_int(dict_["MAX_SIMULATE_PER_STEP"]) + + if "MAX_SIMULATE_PER_EPISODE" in dict_: + self.MAX_SIMULATE_PER_EPISODE = dt_int(dict_["MAX_SIMULATE_PER_EPISODE"]) + authorized_keys = set(self.__dict__.keys()) authorized_keys = authorized_keys | {'NB_TIMESTEP_POWERFLOW_ALLOWED', 'NB_TIMESTEP_TOPOLOGY_REMODIF', "NB_TIMESTEP_LINE_STATUS_REMODIF"} + ignored_keys = dict_.keys() - authorized_keys if len(ignored_keys): warnings.warn("Parameters: The _parameters \"{}\" used to build the Grid2Op.Parameters " @@ -299,6 +318,8 @@ def to_dict(self): res["ACTIVATE_STORAGE_LOSS"] = bool(self.ACTIVATE_STORAGE_LOSS) res["ALARM_BEST_TIME"] = int(self.ALARM_BEST_TIME) res["ALARM_WINDOW_SIZE"] = int(self.ALARM_WINDOW_SIZE) + res["MAX_SIMULATE_PER_STEP"] = int(self.MAX_SIMULATE_PER_STEP) + res["MAX_SIMULATE_PER_EPISODE"] = int(self.MAX_SIMULATE_PER_EPISODE) return res def init_from_json(self, json_path): @@ -463,3 +484,19 @@ def check_valid(self): raise RuntimeError("self.ALARM_WINDOW_SIZE should be a positive integer !") if self.ALARM_BEST_TIME <= 0: raise RuntimeError("self.ALARM_BEST_TIME should be a positive integer !") + + try: + self.MAX_SIMULATE_PER_STEP = int(self.MAX_SIMULATE_PER_STEP) # to raise if numpy array + self.MAX_SIMULATE_PER_STEP = dt_int(self.MAX_SIMULATE_PER_STEP) + except Exception as exc_: + raise RuntimeError(f"Impossible to convert MAX_SIMULATE_PER_STEP to int with error \n:\"{exc_}\"") + if self.MAX_SIMULATE_PER_STEP <= -2: + raise RuntimeError(f"self.MAX_SIMULATE_PER_STEP should be a positive integer or -1, we found {self.MAX_SIMULATE_PER_STEP}") + + try: + self.MAX_SIMULATE_PER_EPISODE = int(self.MAX_SIMULATE_PER_EPISODE) # to raise if numpy array + self.MAX_SIMULATE_PER_EPISODE = dt_int(self.MAX_SIMULATE_PER_EPISODE) + except Exception as exc_: + raise RuntimeError(f"Impossible to convert MAX_SIMULATE_PER_EPISODE to int with error \n:\"{exc_}\"") + if self.MAX_SIMULATE_PER_EPISODE <= -2: + raise RuntimeError(f"self.MAX_SIMULATE_PER_EPISODE should be a positive integer or -1, we found {self.MAX_SIMULATE_PER_EPISODE}") diff --git a/grid2op/Plot/PlotPyGame.py b/grid2op/Plot/PlotPyGame.py index 6e92131ef..d05bdba98 100644 --- a/grid2op/Plot/PlotPyGame.py +++ b/grid2op/Plot/PlotPyGame.py @@ -431,13 +431,13 @@ def init_fig(self, fig, reward, done, timestamp): # The game is not paused anymore (or never has been), I can render the next surface if self.time_last is not None and self._deactivate_display is False: - tmp = time.time() # in second + tmp = time.perf_counter() # in second if tmp - self.time_last < self.timestep_duration_seconds: nb_sec_wait = int(1000 * (self.timestep_duration_seconds - (tmp - self.time_last))) pygame.time.wait(nb_sec_wait) # it's in ms - self.time_last = time.time() + self.time_last = time.perf_counter() else: - self.time_last = time.time() + self.time_last = time.perf_counter() self.screen.fill(self.background_color) if done is not None: diff --git a/grid2op/PlotGrid/LayoutUtil.py b/grid2op/PlotGrid/LayoutUtil.py index 1731afdf5..4cf39fccc 100644 --- a/grid2op/PlotGrid/LayoutUtil.py +++ b/grid2op/PlotGrid/LayoutUtil.py @@ -11,6 +11,8 @@ import copy import math +from grid2op.PlotGrid.PlotUtil import PlotUtil as pltu + def layout_obs_sub_only(obs, scale=1000.0): n_sub = obs.n_sub @@ -51,7 +53,7 @@ def layout_obs_sub_only(obs, scale=1000.0): return improved_layout -def layout_obs_sub_load_and_gen(obs, scale=1000.0, use_initial=False): +def layout_obs_sub_load_and_gen(obs, scale=1000.0, use_initial=False, parallel_spacing=3.0): # Create a graph of substations vertices G = nx.Graph() diff --git a/grid2op/Rules/BaseRules.py b/grid2op/Rules/BaseRules.py index 7007f775d..01de55120 100644 --- a/grid2op/Rules/BaseRules.py +++ b/grid2op/Rules/BaseRules.py @@ -44,3 +44,12 @@ def __call__(self, action, env): The cause of the illegal part of the action (should be a grid2op exception) """ pass + + def can_use_simulate(self, nb_simulate_call_step, nb_simulate_call_episode, param): + """ + This function can be overriden. + + It is expected to return either SimulateUsedTooMuchThisStep or SimulateUsedTooMuchThisEpisode if the number of calls to `obs.simulate` + is too high in total or for the given step + """ + return None \ No newline at end of file diff --git a/grid2op/Rules/DefaultRules.py b/grid2op/Rules/DefaultRules.py index 5606732fe..ba82b6df5 100644 --- a/grid2op/Rules/DefaultRules.py +++ b/grid2op/Rules/DefaultRules.py @@ -36,3 +36,6 @@ def __call__(self, action, env): return False, reason return PreventReconnection.__call__(self, action, env) + + def can_use_simulate(self, nb_simulate_call_step, nb_simulate_call_episode, param): + return LookParam.can_use_simulate(self, nb_simulate_call_step, nb_simulate_call_episode, param) diff --git a/grid2op/Rules/LookParam.py b/grid2op/Rules/LookParam.py index 8305bd0c2..737a25364 100644 --- a/grid2op/Rules/LookParam.py +++ b/grid2op/Rules/LookParam.py @@ -7,7 +7,7 @@ # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. import numpy as np -from grid2op.Exceptions import IllegalAction +from grid2op.Exceptions import IllegalAction, SimulateUsedTooMuchThisStep, SimulateUsedTooMuchThisEpisode from grid2op.Rules.BaseRules import BaseRules @@ -38,3 +38,11 @@ def __call__(self, action, env): return False, IllegalAction("More than {} substation affected by the action: {}" "".format(env.parameters.MAX_SUB_CHANGED, ids)) return True, None + + def can_use_simulate(self, nb_simulate_call_step, nb_simulate_call_episode, param): + if param.MAX_SIMULATE_PER_STEP >= 0: + if nb_simulate_call_step > param.MAX_SIMULATE_PER_STEP: + return SimulateUsedTooMuchThisStep(f"attempt to use {nb_simulate_call_step} times `obs.simulate(...)` while the maximum allowed for this step is {param.MAX_SIMULATE_PER_STEP}") + if param.MAX_SIMULATE_PER_EPISODE >= 0: + if nb_simulate_call_episode > param.MAX_SIMULATE_PER_EPISODE: + return SimulateUsedTooMuchThisEpisode(f"attempt to use {nb_simulate_call_episode} times `obs.simulate(...)` while the maximum allowed for this episode is {param.MAX_SIMULATE_PER_EPISODE}") diff --git a/grid2op/Runner/aux_fun.py b/grid2op/Runner/aux_fun.py index fa14daa31..9233d48f2 100644 --- a/grid2op/Runner/aux_fun.py +++ b/grid2op/Runner/aux_fun.py @@ -153,7 +153,7 @@ def _aux_run_one_episode(env, other_rewards=[]) episode.set_parameters(env) - beg_ = time.time() + beg_ = time.perf_counter() reward = float(env.reward_range[0]) done = False @@ -161,9 +161,9 @@ def _aux_run_one_episode(env, next_pbar = [False] with _aux_make_progress_bar(pbar, nb_timestep_max, next_pbar) as pbar_: while not done: - beg__ = time.time() + beg__ = time.perf_counter() act = agent.act(obs, reward, done) - end__ = time.time() + end__ = time.perf_counter() time_act += end__ - beg__ obs, reward, done, info = env.step(act) # should load the first time stamp @@ -179,7 +179,7 @@ def _aux_run_one_episode(env, act, obs, opp_attack, info) - end_ = time.time() + end_ = time.perf_counter() episode.set_meta(env, time_step, float(cum_reward), env_seed, agent_seed) li_text = ["Env: {:.2f}s", "\t - apply act {:.2f}s", "\t - run pf: {:.2f}s", diff --git a/grid2op/Runner/runner.py b/grid2op/Runner/runner.py index 7ee9c069d..6d34486b5 100644 --- a/grid2op/Runner/runner.py +++ b/grid2op/Runner/runner.py @@ -258,7 +258,8 @@ def __init__(self, has_attention_budget=False, logger=None, # experimental: whether to read from local dir or generate the classes on the fly: - _read_from_local_dir=False + _read_from_local_dir=False, + _is_test=False # TODO not implemented !! ): """ Initialize the Runner. diff --git a/grid2op/Space/GridObjects.py b/grid2op/Space/GridObjects.py index f12c8391a..f7f0858c9 100644 --- a/grid2op/Space/GridObjects.py +++ b/grid2op/Space/GridObjects.py @@ -932,7 +932,7 @@ def _assign_attr_from_name(self, attr_nm, vect): TODO doc : documentation and example """ tmp = getattr(self, attr_nm) - if isinstance(tmp, (dt_bool, dt_int, dt_float)): + if isinstance(tmp, (dt_bool, dt_int, dt_float, float, int, bool)): if isinstance(vect, np.ndarray): setattr(self, attr_nm, vect[0]) else: @@ -2998,6 +2998,8 @@ def __reduce__(self): It here to avoid issue with pickle. But the problem is that it's also used by deepcopy... So its implementation is used a lot """ + # TODO this is not really a convenient use of that i'm sure ! + # Try to see if it can be better cls_attr_as_dict = {} GridObjects._make_cls_dict_extended(type(self), cls_attr_as_dict, as_list=False) if hasattr(self, "__getstate__"): @@ -3005,7 +3007,7 @@ def __reduce__(self): else: my_state = {} for k, v in self.__dict__.items(): - my_state[k] = copy.copy(v) + my_state[k] = v # copy.copy(v) my_cls = type(self) if hasattr(my_cls, "_INIT_GRID_CLS"): @@ -3165,7 +3167,7 @@ class {cls.__name__}({cls._INIT_GRID_CLS.__name__}): _PATH_ENV = {_PATH_ENV_str} _INIT_GRID_CLS = {cls._INIT_GRID_CLS.__name__} - SUB_COL = 1 + SUB_COL = 0 LOA_COL = 1 GEN_COL = 2 LOR_COL = 3 diff --git a/grid2op/Space/SerializableSpace.py b/grid2op/Space/SerializableSpace.py index 5c7f8c73d..f151a4631 100644 --- a/grid2op/Space/SerializableSpace.py +++ b/grid2op/Space/SerializableSpace.py @@ -171,15 +171,28 @@ def from_dict(dict_): if actionClass_li[-1] in globals(): subtype = globals()[actionClass_li[-1]] else: + try: + exec("from {} import {}".format(".".join(actionClass_li[:-1]), actionClass_li[-1])) + except ModuleNotFoundError as exc_: + # prior to grid2op 1.6.5 the Observation module was grid2op.Observation.completeObservation.CompleteObservation + # after its grid2op.Observation.completeObservation.CompleteObservation + # so I try here to make the python file lower case in order to import + # the class correctly + if len(actionClass_li) > 2: + test_str = actionClass_li[2] + actionClass_li[2] = test_str[0].lower() + test_str[1:] + exec("from {} import {}".format(".".join(actionClass_li[:-1]), actionClass_li[-1])) + else: + raise exc_ + # TODO make something better and recursive here - exec("from {} import {}".format(".".join(actionClass_li[:-1]), actionClass_li[-1])) try: subtype = eval(actionClass_li[-1]) except NameError: if len(actionClass_li) > 1: try: subtype = eval(".".join(actionClass_li[1:])) - except: + except Exception as exc_: msg_err_ = "Impossible to find the module \"{}\" to load back the space (ERROR 1). " \ "Try \"from {} import {}\"" raise Grid2OpException(msg_err_.format(actionClass_str, ".".join(actionClass_li[:-1]), @@ -192,7 +205,7 @@ def from_dict(dict_): except AttributeError: try: subtype = eval(actionClass_li[-1]) - except: + except Exception as exc_: if len(actionClass_li) > 1: msg_err_ = "Impossible to find the class named \"{}\" to load back the space (ERROR 3)" \ "(module is found but not the class in it) Please import it via " \ diff --git a/grid2op/data_test/test_action_json_educ_case14_storage.json b/grid2op/data_test/test_action_json_educ_case14_storage.json new file mode 100644 index 000000000..3c2a834d9 --- /dev/null +++ b/grid2op/data_test/test_action_json_educ_case14_storage.json @@ -0,0 +1 @@ +[{}, {"change_line_status": [0]}, {"change_line_status": [1]}, {"change_line_status": [2]}, {"change_line_status": [3]}, {"change_line_status": [4]}, {"change_line_status": [5]}, {"change_line_status": [6]}, {"change_line_status": [7]}, {"change_line_status": [8]}, {"change_line_status": [9]}, {"change_line_status": [10]}, {"change_line_status": [11]}, {"change_line_status": [12]}, {"change_line_status": [13]}, {"change_line_status": [14]}, {"change_line_status": [15]}, {"change_line_status": [16]}, {"change_line_status": [17]}, {"change_line_status": [18]}, {"change_line_status": [19]}, {"set_bus": [[0, 1], [1, 1], [2, 1]]}, {"set_bus": [[0, 2], [1, 1], [2, 2]]}, {"set_bus": [[0, 2], [1, 1], [2, 1]]}, {"set_bus": [[3, 1], [4, 1], [5, 1], [6, 1], [7, 1], [8, 1]]}, {"set_bus": [[3, 2], [4, 2], [5, 2], [6, 1], [7, 2], [8, 2]]}, {"set_bus": [[3, 2], [4, 2], [5, 2], [6, 1], [7, 2], [8, 1]]}, {"set_bus": [[3, 2], [4, 2], [5, 2], [6, 1], [7, 1], [8, 2]]}, {"set_bus": [[3, 2], [4, 2], [5, 2], [6, 1], [7, 1], [8, 1]]}, {"set_bus": [[3, 2], [4, 2], [5, 1], [6, 2], [7, 2], [8, 2]]}, {"set_bus": [[3, 2], [4, 2], [5, 1], [6, 2], [7, 2], [8, 1]]}, {"set_bus": [[3, 2], [4, 2], [5, 1], [6, 2], [7, 1], [8, 2]]}, {"set_bus": [[3, 2], [4, 2], [5, 1], [6, 2], [7, 1], [8, 1]]}, {"set_bus": [[3, 2], [4, 2], [5, 1], [6, 1], [7, 2], [8, 2]]}, {"set_bus": [[3, 2], [4, 2], [5, 1], [6, 1], [7, 2], [8, 1]]}, {"set_bus": [[3, 2], [4, 2], [5, 1], [6, 1], [7, 1], [8, 2]]}, {"set_bus": [[3, 2], [4, 2], [5, 1], [6, 1], [7, 1], [8, 1]]}, {"set_bus": [[3, 2], [4, 1], [5, 2], [6, 2], [7, 2], [8, 2]]}, {"set_bus": [[3, 2], [4, 1], [5, 2], [6, 2], [7, 2], [8, 1]]}, {"set_bus": [[3, 2], [4, 1], [5, 2], [6, 2], [7, 1], [8, 2]]}, {"set_bus": [[3, 2], [4, 1], [5, 2], [6, 2], [7, 1], [8, 1]]}, {"set_bus": [[3, 2], [4, 1], [5, 2], [6, 1], [7, 2], [8, 2]]}, {"set_bus": [[3, 2], [4, 1], [5, 2], [6, 1], [7, 2], [8, 1]]}, {"set_bus": [[3, 2], [4, 1], [5, 2], [6, 1], [7, 1], [8, 2]]}, {"set_bus": [[3, 2], [4, 1], [5, 2], [6, 1], [7, 1], [8, 1]]}, {"set_bus": [[3, 2], [4, 1], [5, 1], [6, 2], [7, 2], [8, 2]]}, {"set_bus": [[3, 2], [4, 1], [5, 1], [6, 2], [7, 2], [8, 1]]}, {"set_bus": [[3, 2], [4, 1], [5, 1], [6, 2], [7, 1], [8, 2]]}, {"set_bus": [[3, 2], [4, 1], [5, 1], [6, 2], [7, 1], [8, 1]]}, {"set_bus": [[3, 2], [4, 1], [5, 1], [6, 1], [7, 2], [8, 2]]}, {"set_bus": [[3, 2], [4, 1], [5, 1], [6, 1], [7, 2], [8, 1]]}, {"set_bus": [[3, 2], [4, 1], [5, 1], [6, 1], [7, 1], [8, 2]]}, {"set_bus": [[3, 2], [4, 1], [5, 1], [6, 1], [7, 1], [8, 1]]}, {"set_bus": [[9, 1], [10, 1], [11, 1], [12, 1]]}, {"set_bus": [[9, 2], [10, 1], [11, 2], [12, 2]]}, {"set_bus": [[9, 2], [10, 1], [11, 2], [12, 1]]}, {"set_bus": [[9, 2], [10, 1], [11, 1], [12, 2]]}, {"set_bus": [[9, 2], [10, 1], [11, 1], [12, 1]]}, {"set_bus": [[13, 1], [14, 1], [15, 1], [16, 1], [17, 1], [18, 1]]}, {"set_bus": [[13, 2], [14, 2], [15, 2], [16, 2], [17, 1], [18, 2]]}, {"set_bus": [[13, 2], [14, 2], [15, 2], [16, 2], [17, 1], [18, 1]]}, {"set_bus": [[13, 2], [14, 2], [15, 2], [16, 1], [17, 2], [18, 2]]}, {"set_bus": [[13, 2], [14, 2], [15, 2], [16, 1], [17, 2], [18, 1]]}, {"set_bus": [[13, 2], [14, 2], [15, 2], [16, 1], [17, 1], [18, 2]]}, {"set_bus": [[13, 2], [14, 2], [15, 2], [16, 1], [17, 1], [18, 1]]}, {"set_bus": [[13, 2], [14, 2], [15, 1], [16, 2], [17, 2], [18, 2]]}, {"set_bus": [[13, 2], [14, 2], [15, 1], [16, 2], [17, 2], [18, 1]]}, {"set_bus": [[13, 2], [14, 2], [15, 1], [16, 2], [17, 1], [18, 2]]}, {"set_bus": [[13, 2], [14, 2], [15, 1], [16, 2], [17, 1], [18, 1]]}, {"set_bus": [[13, 2], [14, 2], [15, 1], [16, 1], [17, 2], [18, 2]]}, {"set_bus": [[13, 2], [14, 2], [15, 1], [16, 1], [17, 2], [18, 1]]}, {"set_bus": [[13, 2], [14, 2], [15, 1], [16, 1], [17, 1], [18, 2]]}, {"set_bus": [[13, 2], [14, 2], [15, 1], [16, 1], [17, 1], [18, 1]]}, {"set_bus": [[13, 2], [14, 1], [15, 2], [16, 2], [17, 2], [18, 2]]}, {"set_bus": [[13, 2], [14, 1], [15, 2], [16, 2], [17, 2], [18, 1]]}, {"set_bus": [[13, 2], [14, 1], [15, 2], [16, 2], [17, 1], [18, 2]]}, {"set_bus": [[13, 2], [14, 1], [15, 2], [16, 2], [17, 1], [18, 1]]}, {"set_bus": [[13, 2], [14, 1], [15, 2], [16, 1], [17, 2], [18, 2]]}, {"set_bus": [[13, 2], [14, 1], [15, 2], [16, 1], [17, 2], [18, 1]]}, {"set_bus": [[13, 2], [14, 1], [15, 2], [16, 1], [17, 1], [18, 2]]}, {"set_bus": [[13, 2], [14, 1], [15, 2], [16, 1], [17, 1], [18, 1]]}, {"set_bus": [[13, 2], [14, 1], [15, 1], [16, 2], [17, 2], [18, 2]]}, {"set_bus": [[13, 2], [14, 1], [15, 1], [16, 2], [17, 2], [18, 1]]}, {"set_bus": [[13, 2], [14, 1], [15, 1], [16, 2], [17, 1], [18, 2]]}, {"set_bus": [[13, 2], [14, 1], [15, 1], [16, 2], [17, 1], [18, 1]]}, {"set_bus": [[13, 2], [14, 1], [15, 1], [16, 1], [17, 2], [18, 2]]}, {"set_bus": [[13, 2], [14, 1], [15, 1], [16, 1], [17, 2], [18, 1]]}, {"set_bus": [[13, 2], [14, 1], [15, 1], [16, 1], [17, 1], [18, 2]]}, {"set_bus": [[13, 2], [14, 1], [15, 1], [16, 1], [17, 1], [18, 1]]}, {"set_bus": [[19, 1], [20, 1], [21, 1], [22, 1], [23, 1]]}, {"set_bus": [[19, 2], [20, 2], [21, 2], [22, 1], [23, 2]]}, {"set_bus": [[19, 2], [20, 2], [21, 2], [22, 1], [23, 1]]}, {"set_bus": [[19, 2], [20, 2], [21, 1], [22, 2], [23, 2]]}, {"set_bus": [[19, 2], [20, 2], [21, 1], [22, 2], [23, 1]]}, {"set_bus": [[19, 2], [20, 2], [21, 1], [22, 1], [23, 2]]}, {"set_bus": [[19, 2], [20, 2], [21, 1], [22, 1], [23, 1]]}, {"set_bus": [[19, 2], [20, 1], [21, 2], [22, 2], [23, 2]]}, {"set_bus": [[19, 2], [20, 1], [21, 2], [22, 2], [23, 1]]}, {"set_bus": [[19, 2], [20, 1], [21, 2], [22, 1], [23, 2]]}, {"set_bus": [[19, 2], [20, 1], [21, 2], [22, 1], [23, 1]]}, {"set_bus": [[19, 2], [20, 1], [21, 1], [22, 2], [23, 2]]}, {"set_bus": [[19, 2], [20, 1], [21, 1], [22, 2], [23, 1]]}, {"set_bus": [[19, 2], [20, 1], [21, 1], [22, 1], [23, 2]]}, {"set_bus": [[19, 2], [20, 1], [21, 1], [22, 1], [23, 1]]}, {"set_bus": [[24, 1], [25, 1], [26, 1], [27, 1], [28, 1], [29, 1], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 2], [27, 1], [28, 2], [29, 2], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 2], [27, 1], [28, 2], [29, 2], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 2], [27, 1], [28, 2], [29, 2], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 2], [27, 1], [28, 2], [29, 2], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 2], [27, 1], [28, 2], [29, 1], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 2], [27, 1], [28, 2], [29, 1], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 2], [27, 1], [28, 2], [29, 1], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 2], [27, 1], [28, 2], [29, 1], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 2], [27, 1], [28, 1], [29, 2], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 2], [27, 1], [28, 1], [29, 2], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 2], [27, 1], [28, 1], [29, 2], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 2], [27, 1], [28, 1], [29, 2], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 2], [27, 1], [28, 1], [29, 1], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 2], [27, 1], [28, 1], [29, 1], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 2], [27, 1], [28, 1], [29, 1], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 2], [27, 1], [28, 1], [29, 1], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 2], [28, 2], [29, 2], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 2], [28, 2], [29, 2], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 2], [28, 2], [29, 2], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 2], [28, 2], [29, 2], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 2], [28, 2], [29, 1], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 2], [28, 2], [29, 1], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 2], [28, 2], [29, 1], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 2], [28, 2], [29, 1], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 2], [28, 1], [29, 2], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 2], [28, 1], [29, 2], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 2], [28, 1], [29, 2], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 2], [28, 1], [29, 2], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 2], [28, 1], [29, 1], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 2], [28, 1], [29, 1], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 2], [28, 1], [29, 1], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 2], [28, 1], [29, 1], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 1], [28, 2], [29, 2], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 1], [28, 2], [29, 2], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 1], [28, 2], [29, 2], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 1], [28, 2], [29, 2], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 1], [28, 2], [29, 1], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 1], [28, 2], [29, 1], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 1], [28, 2], [29, 1], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 1], [28, 2], [29, 1], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 1], [28, 1], [29, 2], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 1], [28, 1], [29, 2], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 1], [28, 1], [29, 2], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 1], [28, 1], [29, 2], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 1], [28, 1], [29, 1], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 1], [28, 1], [29, 1], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 1], [28, 1], [29, 1], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 2], [26, 1], [27, 1], [28, 1], [29, 1], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 2], [28, 2], [29, 2], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 2], [28, 2], [29, 2], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 2], [28, 2], [29, 2], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 2], [28, 2], [29, 2], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 2], [28, 2], [29, 1], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 2], [28, 2], [29, 1], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 2], [28, 2], [29, 1], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 2], [28, 2], [29, 1], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 2], [28, 1], [29, 2], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 2], [28, 1], [29, 2], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 2], [28, 1], [29, 2], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 2], [28, 1], [29, 2], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 2], [28, 1], [29, 1], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 2], [28, 1], [29, 1], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 2], [28, 1], [29, 1], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 2], [28, 1], [29, 1], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 1], [28, 2], [29, 2], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 1], [28, 2], [29, 2], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 1], [28, 2], [29, 2], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 1], [28, 2], [29, 2], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 1], [28, 2], [29, 1], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 1], [28, 2], [29, 1], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 1], [28, 2], [29, 1], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 1], [28, 2], [29, 1], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 1], [28, 1], [29, 2], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 1], [28, 1], [29, 2], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 1], [28, 1], [29, 2], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 1], [28, 1], [29, 2], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 1], [28, 1], [29, 1], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 1], [28, 1], [29, 1], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 1], [28, 1], [29, 1], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 2], [27, 1], [28, 1], [29, 1], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 2], [28, 2], [29, 2], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 2], [28, 2], [29, 2], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 2], [28, 2], [29, 2], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 2], [28, 2], [29, 2], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 2], [28, 2], [29, 1], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 2], [28, 2], [29, 1], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 2], [28, 2], [29, 1], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 2], [28, 2], [29, 1], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 2], [28, 1], [29, 2], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 2], [28, 1], [29, 2], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 2], [28, 1], [29, 2], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 2], [28, 1], [29, 2], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 2], [28, 1], [29, 1], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 2], [28, 1], [29, 1], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 2], [28, 1], [29, 1], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 2], [28, 1], [29, 1], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 1], [28, 2], [29, 2], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 1], [28, 2], [29, 2], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 1], [28, 2], [29, 2], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 1], [28, 2], [29, 2], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 1], [28, 2], [29, 1], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 1], [28, 2], [29, 1], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 1], [28, 2], [29, 1], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 1], [28, 2], [29, 1], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 1], [28, 1], [29, 2], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 1], [28, 1], [29, 2], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 1], [28, 1], [29, 2], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 1], [28, 1], [29, 2], [30, 1], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 1], [28, 1], [29, 1], [30, 2], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 1], [28, 1], [29, 1], [30, 2], [31, 1]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 1], [28, 1], [29, 1], [30, 1], [31, 2]]}, {"set_bus": [[24, 2], [25, 1], [26, 1], [27, 1], [28, 1], [29, 1], [30, 1], [31, 1]]}, {"set_bus": [[32, 1], [33, 1], [34, 1]]}, {"set_bus": [[32, 2], [33, 2], [34, 1]]}, {"set_bus": [[32, 2], [33, 1], [34, 2]]}, {"set_bus": [[32, 2], [33, 1], [34, 1]]}, {"set_bus": [[38, 1], [39, 1], [40, 1], [41, 1], [42, 1]]}, {"set_bus": [[38, 2], [39, 2], [40, 2], [41, 1], [42, 2]]}, {"set_bus": [[38, 2], [39, 2], [40, 2], [41, 1], [42, 1]]}, {"set_bus": [[38, 2], [39, 2], [40, 1], [41, 2], [42, 2]]}, {"set_bus": [[38, 2], [39, 2], [40, 1], [41, 2], [42, 1]]}, {"set_bus": [[38, 2], [39, 2], [40, 1], [41, 1], [42, 2]]}, {"set_bus": [[38, 2], [39, 2], [40, 1], [41, 1], [42, 1]]}, {"set_bus": [[38, 2], [39, 1], [40, 2], [41, 2], [42, 2]]}, {"set_bus": [[38, 2], [39, 1], [40, 2], [41, 2], [42, 1]]}, {"set_bus": [[38, 2], [39, 1], [40, 2], [41, 1], [42, 2]]}, {"set_bus": [[38, 2], [39, 1], [40, 2], [41, 1], [42, 1]]}, {"set_bus": [[38, 2], [39, 1], [40, 1], [41, 2], [42, 2]]}, {"set_bus": [[38, 2], [39, 1], [40, 1], [41, 2], [42, 1]]}, {"set_bus": [[38, 2], [39, 1], [40, 1], [41, 1], [42, 2]]}, {"set_bus": [[38, 2], [39, 1], [40, 1], [41, 1], [42, 1]]}, {"set_bus": [[43, 1], [44, 1], [45, 1]]}, {"set_bus": [[43, 2], [44, 1], [45, 2]]}, {"set_bus": [[43, 2], [44, 1], [45, 1]]}, {"set_bus": [[46, 1], [47, 1], [48, 1]]}, {"set_bus": [[46, 2], [47, 1], [48, 2]]}, {"set_bus": [[46, 2], [47, 1], [48, 1]]}, {"set_bus": [[49, 1], [50, 1], [51, 1]]}, {"set_bus": [[49, 2], [50, 1], [51, 2]]}, {"set_bus": [[49, 2], [50, 1], [51, 1]]}, {"set_bus": [[52, 1], [53, 1], [54, 1], [55, 1]]}, {"set_bus": [[52, 2], [53, 2], [54, 1], [55, 2]]}, {"set_bus": [[52, 2], [53, 2], [54, 1], [55, 1]]}, {"set_bus": [[52, 2], [53, 1], [54, 2], [55, 2]]}, {"set_bus": [[52, 2], [53, 1], [54, 2], [55, 1]]}, {"set_bus": [[52, 2], [53, 1], [54, 1], [55, 2]]}, {"set_bus": [[52, 2], [53, 1], [54, 1], [55, 1]]}, {"set_bus": [[56, 1], [57, 1], [58, 1]]}, {"set_bus": [[56, 2], [57, 1], [58, 2]]}, {"set_bus": [[56, 2], [57, 1], [58, 1]]}] \ No newline at end of file diff --git a/grid2op/gym_compat/box_gym_obsspace.py b/grid2op/gym_compat/box_gym_obsspace.py index cfb6cf523..691acc051 100644 --- a/grid2op/gym_compat/box_gym_obsspace.py +++ b/grid2op/gym_compat/box_gym_obsspace.py @@ -309,6 +309,10 @@ def __init__(self, np.full(shape=(1,), fill_value=True, dtype=dt_bool), (1,), dt_bool), + "delta_time": (np.full(shape=(1,), fill_value=0, dtype=dt_float), + np.full(shape=(1,), fill_value=np.inf, dtype=dt_float), + (1,), + dt_float) } self.dict_properties["prod_p"] = self.dict_properties["gen_p"] self.dict_properties["prod_q"] = self.dict_properties["gen_q"] diff --git a/grid2op/gym_compat/discrete_gym_actspace.py b/grid2op/gym_compat/discrete_gym_actspace.py index 967c2744a..e1d4956c9 100644 --- a/grid2op/gym_compat/discrete_gym_actspace.py +++ b/grid2op/gym_compat/discrete_gym_actspace.py @@ -10,7 +10,7 @@ import warnings from gym.spaces import Discrete - +from grid2op.Exceptions import Grid2OpException from grid2op.Action import ActionSpace from grid2op.Converter import IdToAct @@ -44,9 +44,8 @@ class DiscreteActSpace(Discrete): gym_env1.action_space = MultiDiscreteActSpace(env.action_space, attr_to_keep=['redispatch', "curtail", "one_sub_set"]) - gym_env2.action_space = MultiDiscreteActSpace(env.action_space, - attr_to_keep=['redispatch', "curtail", "set_bus"]) - + gym_env2.action_space = DiscreteActSpace(env.action_space, + attr_to_keep=['redispatch', "curtail", "set_bus"]) Then at each step, `gym_env1` will allow to perform a redispatching action (on any number of generators), a curtailment @@ -75,7 +74,7 @@ class DiscreteActSpace(Discrete): env_name = ... env = grid2op.make(env_name) - from grid2op.gym_compat import GymEnv, MultiDiscreteActSpace, DiscreteActSpace + from grid2op.gym_compat import GymEnv, DiscreteActSpace gym_env = GymEnv(env) gym_env.observation_space = DiscreteActSpace(env.observation_space, @@ -94,17 +93,46 @@ class DiscreteActSpace(Discrete): - "curtail" - "curtail_mw" (same effect as "curtail") + If you do not want (each time) to build all the actions from the action space, but would rather + save the actions you find the most interesting and then reload them, you can, for example: + + .. code-block:: python + + import grid2op + from grid2op.gym_compat import GymEnv, DiscreteActSpace + env_name = ... + env = grid2op.make(env_name) + + gym_env = GymEnv(env) + action_list = ... # a list of action, that can be processed by + # IdToAct.init_converter (all_actions): see + # https://grid2op.readthedocs.io/en/latest/converter.html#grid2op.Converter.IdToAct.init_converter + gym_env.observation_space = DiscreteActSpace(env.observation_space, + action_list=action_list) + + .. note:: + + This last version is much (much) safer and reproducible. Indeed, the + actions usable by your agent will be the same (and in the same order) + regardless of the grid2op version. + + It might not be consistent (between different grid2op versions) + if the actions are built from scratch (for example, depending on the + grid2op version other types of actions can be made, such as curtailment, + or actions on storage units). + """ def __init__(self, grid2op_action_space, attr_to_keep=ALL_ATTR, nb_bins=None, + action_list=None, ): if not isinstance(grid2op_action_space, ActionSpace): - raise RuntimeError(f"Impossible to create a BoxGymActSpace without providing a " - f"grid2op action_space. You provided {type(grid2op_action_space)}" - f"as the \"grid2op_action_space\" attribute.") + raise Grid2OpException(f"Impossible to create a BoxGymActSpace without providing a " + f"grid2op action_space. You provided {type(grid2op_action_space)}" + f"as the \"grid2op_action_space\" attribute.") if nb_bins is None: nb_bins = {"redispatch": 7, "set_storage": 7, "curtail": 7} @@ -117,9 +145,13 @@ def __init__(self, # i do not do that if the user specified specific attributes to keep. This is his responsibility in # in this case attr_to_keep = {el for el in attr_to_keep if grid2op_action_space.supports_type(el)} - + else: + if action_list is not None: + raise Grid2OpException("Impossible to specify a list of attributes " + "to keep (argument attr_to_keep) AND a list of " + "action to use (argument action_list).") for el in attr_to_keep: - if el not in ATTR_DISCRETE: + if el not in ATTR_DISCRETE and action_list is None: warnings.warn(f"The class \"DiscreteActSpace\" should mainly be used to consider only discrete " f"actions (eg. set_line_status, set_bus or change_bus). Though it is possible to use " f"\"{el}\" when building it, be aware that this continuous action will be treated " @@ -142,9 +174,14 @@ def __init__(self, "raise_alarm": act_sp.get_all_unitary_alarm, } - self.converter = None - n_act = self._get_info() - + if action_list is None: + self.converter = None + n_act = self._get_info() + else: + self.converter = IdToAct(self.action_space) + self.converter.init_converter(all_actions=action_list) + n_act = self.converter.n + # initialize the base container Discrete.__init__(self, n=n_act) diff --git a/grid2op/gym_compat/gym_obs_space.py b/grid2op/gym_compat/gym_obs_space.py index 7f752d9f9..48ca1344f 100644 --- a/grid2op/gym_compat/gym_obs_space.py +++ b/grid2op/gym_compat/gym_obs_space.py @@ -227,6 +227,9 @@ def _fill_dict_obs_space(self, dict_, observation_space, env_params, opponent_sp elif attr_nm == "attention_budget": low = 0. high = np.inf + elif attr_nm == "delta_time": + low = 0. + high = np.inf # curtailment, curtailment_limit, gen_p_before_curtail my_type = SpaceType(low=low, high=high, shape=shape, dtype=dt) diff --git a/grid2op/gym_compat/gymenv.py b/grid2op/gym_compat/gymenv.py index 76658b837..c0db2975a 100644 --- a/grid2op/gym_compat/gymenv.py +++ b/grid2op/gym_compat/gymenv.py @@ -6,7 +6,6 @@ # 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 os import initgroups import gym from grid2op.gym_compat.gym_obs_space import GymObservationSpace from grid2op.gym_compat.gym_act_space import GymActionSpace @@ -65,12 +64,16 @@ def render(self, mode='human'): self.init_env.render(mode=mode) def close(self): - if self.init_env is None: + if hasattr(self, "init_env") and self.init_env is None: self.init_env.close() - del self.init_env + del self.init_env self.init_env = None - self.action_space.close() - self.observation_space.close() + if hasattr(self, "action_space") and self.action_space is not None: + self.action_space.close() + self.action_space = None + if hasattr(self, "observation_space") and self.observation_space is not None: + self.observation_space.close() + self.observation_space = None def seed(self, seed=None): self.init_env.seed(seed) diff --git a/grid2op/rest_server/multi_env_server.py b/grid2op/rest_server/multi_env_server.py index 592c336b3..a1f370bc3 100644 --- a/grid2op/rest_server/multi_env_server.py +++ b/grid2op/rest_server/multi_env_server.py @@ -138,10 +138,10 @@ def close(self): if __name__ == "__main__": multi_env = MultiEnvServer() try: - beg = time.time() + beg = time.perf_counter() for _ in tqdm(range(NB_step)): obs, reward, done, info = multi_env.step([multi_env.action_space() for _ in range(multi_env.nb_env)]) - end = time.time() + end = time.perf_counter() finally: multi_env.close() print(f"Using {'synchronous' if SYNCH else 'asyncio'}, it took {end-beg:.2f}s to make {NB_step} steps " diff --git a/grid2op/rest_server/test_server.py b/grid2op/rest_server/test_server.py index c9c734d9a..fb62f6e98 100644 --- a/grid2op/rest_server/test_server.py +++ b/grid2op/rest_server/test_server.py @@ -253,9 +253,9 @@ def assert_rec_equal(li1, li2): with tqdm(desc="local env") as pbar: while True: act = real_env.action_space() - beg_step = time.time() + beg_step = time.perf_counter() obs, reward, done, info = env_perf.step(act) - time_for_step += time.time() - beg_step + time_for_step += time.perf_counter() - beg_step if done: break nb_step_local += 1 @@ -274,18 +274,18 @@ def assert_rec_equal(li1, li2): with tqdm(desc="remote env") as pbar: while True: act = real_env.action_space() - beg_step = time.time() + beg_step = time.perf_counter() act_as_json = act.to_json() resp_step = client.post(f"{URL}/step/{env_name}/{id_env_perf}", json={"action": act_as_json}) - after_step = time.time() + after_step = time.perf_counter() time_for_step_api += after_step - beg_step resp_step_json = resp_step.json() - time_get_json += time.time() - after_step + time_get_json += time.perf_counter() - after_step reic_obs_json = resp_step_json["obs"] - beg_convert = time.time() + beg_convert = time.perf_counter() obs.from_json(reic_obs_json) - time_convert += time.time() - beg_convert - time_for_all_api += time.time() - beg_step + time_convert += time.perf_counter() - beg_convert + time_for_all_api += time.perf_counter() - beg_step if resp_step_json["done"]: break pbar.update(1) diff --git a/grid2op/tests/BaseBackendTest.py b/grid2op/tests/BaseBackendTest.py index 291690ccd..44e1d50b2 100644 --- a/grid2op/tests/BaseBackendTest.py +++ b/grid2op/tests/BaseBackendTest.py @@ -978,6 +978,20 @@ def test_get_action_to_set(self): except AssertionError as exc_: raise AssertionError("Error for shunt: {}".format(exc_)) + def test_get_action_to_set_storage(self): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = make("educ_case14_storage", test=True, backend=self.make_backend(), _add_to_name="test_gats_storage") + env2 = make("educ_case14_storage", test=True, backend=self.make_backend(), _add_to_name="test_gats_storage") + obs, *_ = env.step(env.action_space({"set_storage": [-1.0, 1.0]})) + act = env.backend.get_action_to_set() + + bk_act2 = env2.backend.my_bk_act_class() + bk_act2 += act + env2.backend.apply_action(bk_act2) + env2.backend.runpf() + assert np.all(env2.backend.storages_info()[0] == env.backend.storages_info()[0]) + def _aux_test_back_orig_2(self, obs, prod_p, load_p, p_or, sh_q): self.backend.update_from_obs(obs) self._aux_aux_check_if_matches(prod_p, load_p, p_or, sh_q) diff --git a/grid2op/tests/notebooks_getting_started.py b/grid2op/tests/notebooks_getting_started.py index 1c76a6d47..3231303f2 100644 --- a/grid2op/tests/notebooks_getting_started.py +++ b/grid2op/tests/notebooks_getting_started.py @@ -96,12 +96,12 @@ class to have an approximation of the runtime of the notebook. used for another purpose. """ def __init__(self, str_=""): - self._time = time.time() + self._time = time.perf_counter() self.str_ = str_ def __del__(self): if VERBOSE_TIMER: - print(f"Execution time for {self.str_}: {time.time() - self._time:.3f} s") + print(f"Execution time for {self.str_}: {time.perf_counter() - self._time:.3f} s") class TestNotebook(unittest.TestCase): diff --git a/grid2op/tests/test_Agent.py b/grid2op/tests/test_Agent.py index 07e82bc08..b19a18a58 100644 --- a/grid2op/tests/test_Agent.py +++ b/grid2op/tests/test_Agent.py @@ -46,7 +46,7 @@ def tearDown(self): def _aux_test_agent(self, agent, i_max=30): done = False i = 0 - beg_ = time.time() + beg_ = time.perf_counter() cum_reward = dt_float(0.0) obs = self.env.get_obs() reward = 0. @@ -54,10 +54,10 @@ def _aux_test_agent(self, agent, i_max=30): all_acts = [] while not done: # print("_______________") - beg__ = time.time() + beg__ = time.perf_counter() act = agent.act(obs, reward, done) all_acts.append(act) - end__ = time.time() + end__ = time.perf_counter() obs, reward, done, info = self.env.step(act) # should load the first time stamp time_act += end__ - beg__ cum_reward += reward @@ -65,7 +65,7 @@ def _aux_test_agent(self, agent, i_max=30): if i > i_max: break - end_ = time.time() + end_ = time.perf_counter() if DEBUG: li_text = ["Env: {:.2f}s", "\t - apply act {:.2f}s", diff --git a/grid2op/tests/test_AgentsFast.py b/grid2op/tests/test_AgentsFast.py index 2399de53d..0929dc8e5 100644 --- a/grid2op/tests/test_AgentsFast.py +++ b/grid2op/tests/test_AgentsFast.py @@ -54,7 +54,7 @@ def tearDown(self): def _aux_test_agent(self, agent, i_max=30): done = False i = 0 - beg_ = time.time() + beg_ = time.perf_counter() cum_reward = dt_float(0.0) obs = self.env.get_obs() reward = 0. @@ -62,10 +62,10 @@ def _aux_test_agent(self, agent, i_max=30): all_acts = [] while not done: # print("_______________") - beg__ = time.time() + beg__ = time.perf_counter() act = agent.act(obs, reward, done) all_acts.append(act) - end__ = time.time() + end__ = time.perf_counter() obs, reward, done, info = self.env.step(act) # should load the first time stamp time_act += end__ - beg__ cum_reward += reward @@ -77,7 +77,7 @@ def _aux_test_agent(self, agent, i_max=30): if i > i_max: break - end_ = time.time() + end_ = time.perf_counter() if DEBUG: li_text = ["Env: {:.2f}s", "\t - apply act {:.2f}s", diff --git a/grid2op/tests/test_AlarmFeature.py b/grid2op/tests/test_AlarmFeature.py index 16933537d..edd9c60ae 100644 --- a/grid2op/tests/test_AlarmFeature.py +++ b/grid2op/tests/test_AlarmFeature.py @@ -42,7 +42,7 @@ def tearDown(self) -> None: self.env.close() def test_create_ok(self): - """test that the stuff is created with the right parameters""" + """TestAlarmFeature.test_create_ok test that the stuff is created with the right parameters""" assert self.env._has_attention_budget assert self.env._attention_budget is not None assert isinstance(self.env._attention_budget, LinearAttentionBudget) @@ -54,18 +54,19 @@ def test_create_ok(self): with self.assertRaises(Grid2OpException): # it raises because the default reward: AlarmReward can only be used # if there is an alarm budget - with make(self.env_nm, has_attention_budget=False) as env: + with make(self.env_nm, has_attention_budget=False, test=True) as env: assert env._has_attention_budget is False assert env._attention_budget is None - with make(self.env_nm, has_attention_budget=False, reward_class=RedispReward) as env: + with make(self.env_nm, has_attention_budget=False, reward_class=RedispReward, test=True) as env: assert env._has_attention_budget is False assert env._attention_budget is None - with make(self.env_nm, kwargs_attention_budget={"max_budget": 15, - "budget_per_ts": 1, - "alarm_cost": 12, - "init_budget": 0}) as env: + with make(self.env_nm, test=True, + kwargs_attention_budget={"max_budget": 15, + "budget_per_ts": 1, + "init_budget": 0, + "alarm_cost": 12}) as env: assert env._has_attention_budget assert env._attention_budget is not None assert isinstance(env._attention_budget, LinearAttentionBudget) @@ -87,7 +88,8 @@ def test_budget_increases_ok(self): with make(self.env_nm, kwargs_attention_budget={"max_budget": 5, "budget_per_ts": 1, "alarm_cost": 12, - "init_budget": 0}) as env: + "init_budget": 0}, + test=True) as env: env.step(self.env.action_space()) assert abs(env._attention_budget._current_budget - 1) <= 1e-6 env.step(self.env.action_space()) diff --git a/grid2op/tests/test_ChronicsHandler.py b/grid2op/tests/test_ChronicsHandler.py index 04309d645..61a11930f 100644 --- a/grid2op/tests/test_ChronicsHandler.py +++ b/grid2op/tests/test_ChronicsHandler.py @@ -10,6 +10,7 @@ import warnings import pandas as pd import tempfile +import re from grid2op.tests.helper_path_test import * from grid2op.dtypes import dt_int, dt_float @@ -631,7 +632,7 @@ def test_load(self): param.NO_OVERFLOW_DISCONNECTION = True with warnings.catch_warnings(): warnings.filterwarnings("ignore") - with make(os.path.join(PATH_DATA_TEST, "ieee118_R2subgrid_wcci_test_maintenance"), param=param) as env: + with make(os.path.join(PATH_DATA_TEST, "ieee118_R2subgrid_wcci_test_maintenance"), test=True, param=param) as env: env.seed(123456) # for reproducible tests ! obs = env.reset() #get input data, to check they were correctly applied in @@ -672,7 +673,7 @@ def test_maintenance_multiple_timesteps(self): param.NO_OVERFLOW_DISCONNECTION = True with warnings.catch_warnings(): warnings.filterwarnings("ignore") - with make(os.path.join(PATH_DATA_TEST, "ieee118_R2subgrid_wcci_test_maintenance"), + with make(os.path.join(PATH_DATA_TEST, "ieee118_R2subgrid_wcci_test_maintenance"), test=True, param=param) as env: env.seed(0) envLines = env.name_line @@ -712,7 +713,7 @@ def test_proba(self): param.NO_OVERFLOW_DISCONNECTION = True with warnings.catch_warnings(): warnings.filterwarnings("ignore") - with make(os.path.join(PATH_DATA_TEST, "ieee118_R2subgrid_wcci_test_maintenance_2"), + with make(os.path.join(PATH_DATA_TEST, "ieee118_R2subgrid_wcci_test_maintenance_2"), test=True, param=param) as env: env.seed(0) # input data @@ -739,7 +740,7 @@ def test_load_fake_january(self): param.NO_OVERFLOW_DISCONNECTION = True with warnings.catch_warnings(): warnings.filterwarnings("ignore") - with make(os.path.join(PATH_DATA_TEST, "ieee118_R2subgrid_wcci_test_maintenance_3"), + with make(os.path.join(PATH_DATA_TEST, "ieee118_R2subgrid_wcci_test_maintenance_3"), test=True, param=param) as env: env.seed(0) # get input data, to check they were correctly applied in @@ -760,7 +761,7 @@ def test_split_and_save(self): param.NO_OVERFLOW_DISCONNECTION = True with warnings.catch_warnings(): warnings.filterwarnings("ignore") - with make(os.path.join(PATH_DATA_TEST, "ieee118_R2subgrid_wcci_test_maintenance"), param=param) as env: + with make(os.path.join(PATH_DATA_TEST, "ieee118_R2subgrid_wcci_test_maintenance"), test=True, param=param) as env: env.seed(0) env.set_id(0) obs = env.reset() @@ -812,7 +813,7 @@ def test_split_and_save(self): assert np.all(maintenance_0_0 == maintenance_0_1) # make sure i can reload the environment - env2 = make(os.path.join(PATH_DATA_TEST, "ieee118_R2subgrid_wcci_test_maintenance"), + env2 = make(os.path.join(PATH_DATA_TEST, "ieee118_R2subgrid_wcci_test_maintenance"), test=True, param=param, data_feeding_kwargs={"gridvalueClass": GridStateFromFileWithForecasts}, chronics_path=chronics_outdir3) @@ -827,7 +828,7 @@ def test_seed(self): param.NO_OVERFLOW_DISCONNECTION = True with warnings.catch_warnings(): warnings.filterwarnings("ignore") - with make(os.path.join(PATH_DATA_TEST, "ieee118_R2subgrid_wcci_test_maintenance"), param=param) as env: + with make(os.path.join(PATH_DATA_TEST, "ieee118_R2subgrid_wcci_test_maintenance"), test=True, param=param) as env: nb_scenario = 10 nb_maintenance = np.zeros((nb_scenario, env.n_line), dtype=dt_float) nb_maintenance1 = np.zeros((nb_scenario, env.n_line), dtype=dt_float) @@ -848,7 +849,7 @@ def test_chunk_size(self): param.NO_OVERFLOW_DISCONNECTION = True with warnings.catch_warnings(): warnings.filterwarnings("ignore") - with make(os.path.join(PATH_DATA_TEST, "ieee118_R2subgrid_wcci_test_maintenance_3"), + with make(os.path.join(PATH_DATA_TEST, "ieee118_R2subgrid_wcci_test_maintenance_3"), test=True, param=param) as env: env.seed(0) obs = env.reset() @@ -867,7 +868,7 @@ def test_load(self): param.NO_OVERFLOW_DISCONNECTION = True with warnings.catch_warnings(): warnings.filterwarnings("ignore") - with make(os.path.join(PATH_DATA_TEST, "5bus_example_some_missing"), + with make(os.path.join(PATH_DATA_TEST, "5bus_example_some_missing"), test=True, param=param, chronics_class=MultifolderWithCache) as env: env.seed(123456) # for reproducible tests ! @@ -892,8 +893,7 @@ def test_withrealistic(self): param.NO_OVERFLOW_DISCONNECTION = True with warnings.catch_warnings(): warnings.filterwarnings("ignore") - with make(os.path.join(PATH_CHRONICS, "env_14_test_maintenance"), - test=True, + with make(os.path.join(PATH_CHRONICS, "env_14_test_maintenance"), test=True, param=param) as env: l_id = 11 obs = env.reset() @@ -949,8 +949,7 @@ def test_with_alwayslegal(self): with warnings.catch_warnings(): warnings.filterwarnings("ignore") - with make(os.path.join(PATH_CHRONICS, "env_14_test_maintenance"), - test=True, + with make(os.path.join(PATH_CHRONICS, "env_14_test_maintenance"), test=True, param=param, gamerules_class=AlwaysLegal) as env: l_id = 11 @@ -1160,6 +1159,79 @@ def __call__(self, path): assert id_ == 0 assert np.all(env.chronics_handler.real_data._order == [2*i for i in range(10)]) + def test_set_id_int(self): + """test the env.set_id method when used with int""" + chronics_class = self.get_multifolder_class() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = make("rte_case5_example", test=True, chronics_class=chronics_class) + + if issubclass(chronics_class, MultifolderWithCache): + env.chronics_handler.set_filter(lambda x: re.match(".*(01|04|05|07|09).*", x) is not None) + env.chronics_handler.reset() + + env.set_id(1) + env.reset() + id_str = os.path.split(env.chronics_handler.get_id())[-1] + if not issubclass(chronics_class, MultifolderWithCache): + assert id_str == "01" + else: + assert id_str == "04" + + env.set_id(4) + env.reset() + id_str = os.path.split(env.chronics_handler.get_id())[-1] + if not issubclass(chronics_class, MultifolderWithCache): + assert id_str == "04" + else: + assert id_str == "09" + + def test_set_id_full_path(self): + """test the env.set_id method when used with full path""" + chronics_class = self.get_multifolder_class() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = make("rte_case5_example", test=True, chronics_class=chronics_class) + if issubclass(chronics_class, MultifolderWithCache): + env.chronics_handler.set_filter(lambda x: re.match(".*(01|04|05).*", x) is not None) + env.chronics_handler.reset() + base_path = os.path.split(env.chronics_handler.get_id())[0] + env.set_id(os.path.join(base_path, "01")) + env.reset() + assert env.chronics_handler.get_id() == os.path.join(base_path, "01") + env.set_id(os.path.join(base_path, "04")) + env.reset() + assert env.chronics_handler.get_id() == os.path.join(base_path, "04") + + with self.assertRaises(ChronicsError): + env.set_id(os.path.join(base_path, "31")) + if issubclass(chronics_class, MultifolderWithCache): + with self.assertRaises(ChronicsError): + env.set_id(os.path.join(base_path, "00")) + + def test_set_id_chron_dir(self): + """test the env.set_id method when used with only folder name in the chronics""" + chronics_class = self.get_multifolder_class() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = make("rte_case5_example", test=True, chronics_class=chronics_class) + if issubclass(chronics_class, MultifolderWithCache): + env.chronics_handler.set_filter(lambda x: re.match(".*(01|04|05).*", x) is not None) + env.chronics_handler.reset() + base_path = os.path.split(env.chronics_handler.get_id())[0] + + env.set_id("01") + env.reset() + assert env.chronics_handler.get_id() == os.path.join(base_path, "01") + env.set_id("04") + env.reset() + assert env.chronics_handler.get_id() == os.path.join(base_path, "04") + + with self.assertRaises(ChronicsError): + env.set_id("31") + if issubclass(chronics_class, MultifolderWithCache): + with self.assertRaises(ChronicsError): + env.set_id("00") class TestMultiFolderWithCache(TestMultiFolder): def get_multifolder_class(self): @@ -1197,6 +1269,5 @@ def test_maintenance_deactivated(self): # all maintenance are deactivated assert np.all(obs.time_next_maintenance == -1) - if __name__ == "__main__": unittest.main() diff --git a/grid2op/tests/test_Converter.py b/grid2op/tests/test_Converter.py index 3bee05246..d0628f086 100644 --- a/grid2op/tests/test_Converter.py +++ b/grid2op/tests/test_Converter.py @@ -7,6 +7,9 @@ # This file is part of Grid2Op, Grid2Op a testbed platform to model sequential decision making in power systems. import warnings +import os +import json +from grid2op.Action.BaseAction import BaseAction from grid2op.tests.helper_path_test import * from grid2op.MakeEnv import make @@ -47,7 +50,7 @@ def test_ConnectivityConverter(self): 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 12, - 12, 12, 12, 12, 12]) + 12, 12, 12, 12, 12]) assert np.array_equal(converter.subs_ids, res) assert len(converter.obj_type) == converter.n @@ -263,6 +266,7 @@ def setUp(self): test=True, param=param, action_class=PlayableAction) np.random.seed(0) + self.filenamedict = "test_action_json_educ_case14_storage.json" def tearDown(self): self.env.close() @@ -310,7 +314,15 @@ def test_specific_attr(self): assert converter.n == dims[attr], f"dim for \"{attr}\" should be {dims[attr]} but is " \ f"{converter.n}" - + def test_init_from_list_of_dict(self): + path_input = os.path.join(PATH_DATA_TEST, self.filenamedict) + with open(path_input, "r") as f: + list_act = json.load(f) + converter = IdToAct(self.env.action_space) + converter.init_converter(all_actions=list_act) + assert converter.n == 255 + assert isinstance(converter.all_actions[-1], BaseAction) + assert isinstance(converter.all_actions[0], BaseAction) if __name__ == "__main__": unittest.main() diff --git a/grid2op/tests/test_Environment.py b/grid2op/tests/test_Environment.py index 53eae7345..5e1e36445 100644 --- a/grid2op/tests/test_Environment.py +++ b/grid2op/tests/test_Environment.py @@ -165,10 +165,10 @@ def test_load_env(self): cp.enable() import pandapower as pp nb_powerflow = 5000 - beg_ = time.time() + beg_ = time.perf_counter() for i in range(nb_powerflow): pp.runpp(self.backend._grid) - end_ = time.time() + end_ = time.perf_counter() print("Time to compute {} powerflows: {:.2f}".format(nb_powerflow, end_-beg_)) if PROFILE_CODE: cp.disable() @@ -228,14 +228,14 @@ def test_reward(self): if PROFILE_CODE: cp = cProfile.Profile() cp.enable() - beg_ = time.time() + beg_ = time.perf_counter() cum_reward = dt_float(0.0) do_nothing = self.env.action_space({}) while not done: obs, reward, done, info = self.env.step(do_nothing) # should load the first time stamp cum_reward += reward i += 1 - end_ = time.time() + end_ = time.perf_counter() if DEBUG: msg_ = "\nEnv: {:.2f}s\n\t - apply act {:.2f}s\n\t - run pf: {:.2f}s\n" \ "\t - env update + observation: {:.2f}s\nTotal time: {:.2f}\nCumulative reward: {:1f}" diff --git a/grid2op/tests/test_GymConverter.py b/grid2op/tests/test_GymConverter.py index ab619b400..3e8f10b98 100644 --- a/grid2op/tests/test_GymConverter.py +++ b/grid2op/tests/test_GymConverter.py @@ -9,7 +9,9 @@ # TODO test the json part but... https://github.com/openai/gym-http-api/issues/62 or https://github.com/openai/gym/issues/1841 import tempfile import json +from grid2op.gym_compat.discrete_gym_actspace import DiscreteActSpace from grid2op.tests.helper_path_test import * +from grid2op.Action import PlayableAction from grid2op.dtypes import dt_float, dt_bool, dt_int from grid2op.tests.helper_path_test import * @@ -358,6 +360,36 @@ class TestWithoutConverterStorage(TestWithoutConverterWCCI): def get_env_name(self): return "educ_case14_storage" +class TestDiscreteActSpace(unittest.TestCase): + def setUp(self) -> None: + self.filenamedict = "test_action_json_educ_case14_storage.json" + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.glop_env = make("educ_case14_storage", + test=True, + action_class=PlayableAction) + + def tearDown(self) -> None: + self.glop_env.close() + + def test_create(self): + gym_env = GymEnv(self.glop_env) + act_space = gym_env.action_space + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + act_space = DiscreteActSpace(self.glop_env.action_space) + assert act_space.n == 690, f"{act_space.n = } instead of {690}" + + def test_create_from_list(self): + path_input = os.path.join(PATH_DATA_TEST, self.filenamedict) + with open(path_input, "r") as f: + action_list = json.load(f) + gym_env = GymEnv(self.glop_env) + act_space = gym_env.action_space + + act_space = DiscreteActSpace(self.glop_env.action_space, + action_list=action_list) + assert act_space.n == 255, f"{act_space.n = } instead of {255}" if __name__ == "__main__": unittest.main() diff --git a/grid2op/tests/test_MakeEnv.py b/grid2op/tests/test_MakeEnv.py index a0da20a13..c1ace38f1 100644 --- a/grid2op/tests/test_MakeEnv.py +++ b/grid2op/tests/test_MakeEnv.py @@ -638,7 +638,7 @@ def test_create_dev_iterable(self): def test_create_from_path(self): with warnings.catch_warnings(): warnings.filterwarnings("ignore") - env = make(PATH_DATA_MULTIMIX) + env = make(PATH_DATA_MULTIMIX, test=True) assert env != None assert isinstance(env, MultiMixEnvironment) diff --git a/grid2op/tests/test_Observation.py b/grid2op/tests/test_Observation.py index b9db29f81..9f69db52c 100644 --- a/grid2op/tests/test_Observation.py +++ b/grid2op/tests/test_Observation.py @@ -105,7 +105,7 @@ def setUp(self): "storage_loss": [], 'storage_charging_efficiency': [], 'storage_discharging_efficiency': [], - '_init_subtype': 'grid2op.Observation.CompleteObservation.CompleteObservation', + '_init_subtype': 'grid2op.Observation.completeObservation.CompleteObservation', "dim_alarms": 0, "alarms_area_names": [], "alarms_lines_area": {}, @@ -229,6 +229,7 @@ def setUp(self): "was_alarm_used_after_game_over": [False], "current_step": [0], "max_step": [8064], + "delta_time": [5.] } self.dtypes = np.array([dt_int, dt_int, dt_int, dt_int, dt_int, dt_int, dt_float, dt_float, @@ -247,7 +248,9 @@ def setUp(self): # shunts dt_float, dt_float, dt_float, dt_int, # steps - dt_int, dt_int + dt_int, dt_int, + # delta_time + dt_float ], dtype=object) @@ -257,8 +260,8 @@ def setUp(self): 20, 20, 20, 20, 20, 20, 56, 20, 14, 20, 20, 5, 5, 0, 0, 0, 5, 5, 5, 1, 1, 0, 1, 1, 1, 1, 1, 1, - 1, 1]) - self.size_obs = 429 + 4 + 4 + 2 + 1, 1, 1]) + self.size_obs = 429 + 4 + 4 + 2 + 1 def tearDown(self): self.env.close() @@ -707,7 +710,6 @@ def test_4_to_from_vect(self): assert vect.shape[0] == obs.size() obs2.reset() obs2.from_vect(vect) - assert np.all(obs.dtype() == self.dtypes) assert np.all(obs.shape() == self.shapes) diff --git a/grid2op/tests/test_Opponent.py b/grid2op/tests/test_Opponent.py index b266ff6f9..5ee4d7d77 100644 --- a/grid2op/tests/test_Opponent.py +++ b/grid2op/tests/test_Opponent.py @@ -8,6 +8,7 @@ import tempfile import warnings +import grid2op from grid2op.tests.helper_path_test import * from grid2op.Chronics import ChangeNothing from grid2op.Opponent import BaseOpponent, RandomLineOpponent, WeightedRandomOpponent, GeometricOpponent @@ -1525,6 +1526,5 @@ def test_last_attack(self): obs, reward, done, info = env.step(dn) assert info["opponent_attack_line"] is not None - if __name__ == "__main__": unittest.main() diff --git a/grid2op/tests/test_PandaPowerBackendDefaultFunc.py b/grid2op/tests/test_PandaPowerBackendDefaultFunc.py index a8daa38ab..6996a068f 100644 --- a/grid2op/tests/test_PandaPowerBackendDefaultFunc.py +++ b/grid2op/tests/test_PandaPowerBackendDefaultFunc.py @@ -100,7 +100,12 @@ def get_topo_vect(self): for bus_id in self._grid.load["bus"].values: res[self.load_pos_topo_vect[i]] = 1 if bus_id == self.load_to_subid[i] else 2 i += 1 - + + # do not forget storage units ! + i = 0 + for bus_id in self._grid.storage["bus"].values: + res[self.storage_pos_topo_vect[i]] = 1 if bus_id == self.storage_to_subid[i] else 2 + i += 1 return res diff --git a/grid2op/tests/test_act_as_serializable_dict.py b/grid2op/tests/test_act_as_serializable_dict.py new file mode 100644 index 000000000..239e77d3a --- /dev/null +++ b/grid2op/tests/test_act_as_serializable_dict.py @@ -0,0 +1,277 @@ +# 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 unittest +import numpy as np + +import grid2op + +from grid2op.dtypes import dt_int +from grid2op.Exceptions import * +from grid2op.Action import * +from grid2op.Rules import RulesChecker +from grid2op.Space import GridObjects +import json +import tempfile + + +import pdb + + +def _get_action_grid_class(): + GridObjects.env_name = "test_action_serial_dict" + GridObjects.n_gen = 5 + GridObjects.name_gen = np.array(["gen_{}".format(i) for i in range(5)]) + GridObjects.n_load = 11 + GridObjects.name_load = np.array(["load_{}".format(i) for i in range(11)]) + GridObjects.n_line = 20 + GridObjects.name_line = np.array(["line_{}".format(i) for i in range(20)]) + GridObjects.n_sub = 14 + GridObjects.name_sub = np.array(["sub_{}".format(i) for i in range(14)]) + GridObjects.sub_info = np.array([3, 7, 5, 6, 5, 6, 3, 2, 5, 3, 3, 3, 4, 3], dtype=dt_int) + GridObjects.load_to_subid = np.array([1, 2, 3, 4, 5, 8, 9, 10, 11, 12, 13]) + GridObjects.gen_to_subid = np.array([0, 1, 2, 5, 7]) + GridObjects.line_or_to_subid = np.array([0, 0, 1, 1, 1, 2, 3, 3, 3, 4, 5, 5, + 5, 6, 6, 8, 8, 9, 11, 12]) + GridObjects.line_ex_to_subid = np.array([1, 4, 2, 3, 4, 3, 4, 6, 8, 5, 10, 11, + 12, 7, 8, 9, 13, 10, 12, 13]) + GridObjects.load_to_sub_pos = np.array([4, 2, 5, 4, 4, 4, 1, 1, 1, 2, 1]) + GridObjects.gen_to_sub_pos = np.array([2, 5, 3, 5, 1]) + GridObjects.line_or_to_sub_pos = np.array([0, 1, 1, 2, 3, 1, 2, 3, 4, 3, 1, 2, 3, 1, + 2, 2, 3, 0, 0, 1]) + GridObjects.line_ex_to_sub_pos = np.array([0, 0, 0, 0, 1, 1, 2, 0, 0, 0, 2, 2, 3, 0, + 1, 2, 2, 0, 0, 0]) + GridObjects.load_pos_topo_vect = np.array([7, 12, 20, 25, 30, + 41, 43, 46, 49, 53, 56]) + GridObjects.gen_pos_topo_vect = np.array([2, 8, 13, 31, 36]) + GridObjects.line_or_pos_topo_vect = np.array([0, 1, 4, 5, 6, 11, 17, 18, 19, + 24, 27, 28, 29, 33, 34, 39, 40, + 42, 48, 52]) + GridObjects.line_ex_pos_topo_vect = np.array([3, 21, 10, 15, 22, 16, 23, 32, 37, 26, + 47, 50, 54, 35, 38, 44, 57, + 45, 51, 55]) + + GridObjects.redispatching_unit_commitment_availble = True + GridObjects.gen_type = np.array(["thermal"] * 3 + ["wind"] * 2) + GridObjects.gen_pmin = np.array([0.0] * 5) + GridObjects.gen_pmax = np.array([100.0] * 5) + GridObjects.gen_min_uptime = np.array([0] * 5) + GridObjects.gen_min_downtime = np.array([0] * 5) + GridObjects.gen_cost_per_MW = np.array([70.0] * 5) + GridObjects.gen_startup_cost = np.array([0.0] * 5) + GridObjects.gen_shutdown_cost = np.array([0.0] * 5) + GridObjects.gen_redispatchable = np.array([True, True, True, False, False]) + GridObjects.gen_max_ramp_up = np.array([10., 5., 15., 7., 8.]) + GridObjects.gen_max_ramp_down = np.array([11., 6., 16., 8., 9.]) + GridObjects.gen_renewable = ~GridObjects.gen_redispatchable + + GridObjects.n_storage = 2 + GridObjects.name_storage = np.array(["storage_0", "storage_1"]) + GridObjects.storage_to_subid = np.array([1, 2]) + GridObjects.storage_to_sub_pos = np.array([6, 4]) + GridObjects.storage_pos_topo_vect = np.array([9, 14]) + GridObjects.storage_type = np.array(["battery"] * 2) + GridObjects.storage_Emax = np.array([100., 100.]) + GridObjects.storage_Emin = np.array([0., 0.]) + GridObjects.storage_max_p_prod = np.array([10., 10.]) + GridObjects.storage_max_p_absorb = np.array([15., 15.]) + GridObjects.storage_marginal_cost = np.array([0., 0.]) + GridObjects.storage_loss = np.array([0., 0.]) + GridObjects.storage_discharging_efficiency = np.array([1., 1.]) + GridObjects.storage_charging_efficiency = np.array([1., 1.]) + + GridObjects._topo_vect_to_sub = np.repeat(np.arange(GridObjects.n_sub), repeats=GridObjects.sub_info) + GridObjects.glop_version = grid2op.__version__ + GridObjects._PATH_ENV = None + + GridObjects.shunts_data_available = True + GridObjects.n_shunt = 2 + GridObjects.shunt_to_subid = np.array([0, 1]) + GridObjects.name_shunt = np.array(["shunt_1", "shunt_2"]) + + GridObjects.alarms_area_lines = [[el for el in GridObjects.name_line]] + GridObjects.alarms_area_names = ["all"] + GridObjects.alarms_lines_area = {el: ["all"] for el in GridObjects.name_line} + GridObjects.dim_alarms = 1 + my_cls = GridObjects.init_grid(GridObjects, force=True) + return my_cls + + +class TestActionSerialDict(unittest.TestCase): + def _action_setup(self): + # return self.ActionSpaceClass(self.gridobj, legal_action=self.game_rules.legal_action, actionClass=BaseAction) + return BaseAction + + def tearDown(self): + self.authorized_keys = {} + self.gridobj._clear_class_attribute() + + def setUp(self): + """ + The case file is a representation of the case14 as found in the ieee14 powergrid. + :return: + """ + self.tolvect = 1e-2 + self.tol_one = 1e-5 + self.game_rules = RulesChecker() + + GridObjects_cls = _get_action_grid_class() + self.gridobj = GridObjects_cls() + self.n_line = self.gridobj.n_line + + self.ActionSpaceClass = ActionSpace.init_grid(GridObjects_cls) + act_cls = self._action_setup() + self.helper_action = self.ActionSpaceClass(GridObjects_cls, + legal_action=self.game_rules.legal_action, + actionClass=act_cls) + self.helper_action.seed(42) + self.authorized_keys = self.helper_action().authorized_keys + self.size_act = self.helper_action.size() + + def test_set_line_status(self): + act = self.helper_action({"set_line_status": [(l_id, status) for l_id, status in zip([2, 4, 5], [1, -1, 1])]}) + dict_ = act.as_serializable_dict() + act2 = self.helper_action(dict_) + assert act == act2 + dict_2 = act.as_serializable_dict() + assert dict_ == dict_2 + with tempfile.TemporaryFile(mode="w") as f: + json.dump(fp=f, obj=dict_) + + def test_change_status(self): + act = self.helper_action({"change_line_status": [l_id for l_id in [2, 4, 5]]}) + dict_ = act.as_serializable_dict() + act2 = self.helper_action(dict_) + assert act == act2 + dict_2 = act.as_serializable_dict() + assert dict_ == dict_2 + with tempfile.TemporaryFile(mode="w") as f: + json.dump(fp=f, obj=dict_) + + def test_set_bus(self): + act = self.helper_action({"set_bus": [(el_id, status) for el_id, status in zip([2, 4, 5, 8, 9, 10], [1, -1, 1, 2, 2, 1])]}) + dict_ = act.as_serializable_dict() + act2 = self.helper_action(dict_) + assert act == act2 + dict_2 = act.as_serializable_dict() + assert dict_ == dict_2 + with tempfile.TemporaryFile(mode="w") as f: + json.dump(fp=f, obj=dict_) + + def test_change_bus(self): + act = self.helper_action({"change_bus": [el_id for el_id in [2, 4, 5, 8, 9, 10]]}) + dict_ = act.as_serializable_dict() + act2 = self.helper_action(dict_) + assert act == act2 + dict_2 = act.as_serializable_dict() + assert dict_ == dict_2 + with tempfile.TemporaryFile(mode="w") as f: + json.dump(fp=f, obj=dict_) + + def test_redispatch(self): + act = self.helper_action({"redispatch": [(el_id, amount) for el_id, amount in zip([0, 2], [-3., 28.9])]}) + dict_ = act.as_serializable_dict() + act2 = self.helper_action(dict_) + assert act == act2 + dict_2 = act.as_serializable_dict() + assert dict_ == dict_2 + with tempfile.TemporaryFile(mode="w") as f: + json.dump(fp=f, obj=dict_) + + def test_curtail(self): + act = self.helper_action({"curtail": [(el_id, amount) for el_id, amount in zip([3, 4], [0.5, 0.7])]}) + dict_ = act.as_serializable_dict() + act2 = self.helper_action(dict_) + assert act == act2 + dict_2 = act.as_serializable_dict() + assert dict_ == dict_2 + with tempfile.TemporaryFile(mode="w") as f: + json.dump(fp=f, obj=dict_) + + def test_set_storage(self): + act = self.helper_action({"set_storage": [(el_id, amount) for el_id, amount in zip([0, 1], [-0.5, 0.7])]}) + dict_ = act.as_serializable_dict() + act2 = self.helper_action(dict_) + assert act == act2 + dict_2 = act.as_serializable_dict() + assert dict_ == dict_2 + with tempfile.TemporaryFile(mode="w") as f: + json.dump(fp=f, obj=dict_) + + def test_raise_alarm(self): + act = self.helper_action({"raise_alarm": [0]}) + dict_ = act.as_serializable_dict() + act2 = self.helper_action(dict_) + assert act == act2 + dict_2 = act.as_serializable_dict() + assert dict_ == dict_2 + with tempfile.TemporaryFile(mode="w") as f: + json.dump(fp=f, obj=dict_) + + def test_injection(self): + np.random.seed(0) + act = self.helper_action({"injection": {"prod_p": np.random.uniform(size=self.helper_action.n_gen), + "prod_v": np.random.normal(size=self.helper_action.n_gen), + "load_p": np.random.lognormal(size=self.helper_action.n_load), + "load_q": np.random.logistic(size=self.helper_action.n_load), + }}) + dict_ = act.as_serializable_dict() + act2 = self.helper_action(dict_) + assert act == act2 + dict_2 = act.as_serializable_dict() + assert dict_ == dict_2 + with tempfile.TemporaryFile(mode="w") as f: + json.dump(fp=f, obj=dict_) + + def test_shunt(self): + np.random.seed(0) + act = self.helper_action({"shunt": {"shunt_p": np.random.uniform(size=self.helper_action.n_shunt), + "shunt_q": np.random.normal(size=self.helper_action.n_shunt), + "shunt_bus": [(0, 1), (1, 2)], + }}) + dict_ = act.as_serializable_dict() + act2 = self.helper_action(dict_) + assert act == act2 + dict_2 = act.as_serializable_dict() + assert dict_ == dict_2 + with tempfile.TemporaryFile(mode="w") as f: + json.dump(fp=f, obj=dict_) + + def test_iadd(self): + """I add a bug when += a change_bus after a set bus""" + act = self.helper_action({"set_bus": [(el_id, status) for el_id, status in zip([2, 4, 5, 8, 9, 10], [1, -1, 1, 2, 2, 1])]}) + act += self.helper_action({"change_bus": [el_id for el_id in [2, 4, 5, 8, 9, 10]]}) + assert np.all(act._set_topo_vect <= 4) + + def test_all_at_once(self): + np.random.seed(1) + act = self.helper_action({"set_line_status": [(l_id, status) for l_id, status in zip([2, 4, 5], [1, -1, 1])]}) + act += self.helper_action({"change_line_status": [l_id for l_id in [2, 4, 5]]}) + act += self.helper_action({"set_bus": [(el_id, status) for el_id, status in zip([2, 4, 5, 8, 9, 10], [1, -1, 1, 2, 2, 1])]}) + act += self.helper_action({"change_bus": [el_id for el_id in [2, 3, 5, 11, 12, 15]]}) + act += self.helper_action({"redispatch": [(el_id, amount) for el_id, amount in zip([0, 2], [-3., 28.9])]}) + act += self.helper_action({"curtail": [(el_id, amount) for el_id, amount in zip([3, 4], [0.5, 0.7])]}) + act += self.helper_action({"set_storage": [(el_id, amount) for el_id, amount in zip([0, 1], [-0.5, 0.7])]}) + act += self.helper_action({"raise_alarm": [0]}) + act += self.helper_action({"injection": {"prod_p": np.random.uniform(size=self.helper_action.n_gen), + "prod_v": np.random.normal(size=self.helper_action.n_gen), + "load_p": np.random.lognormal(size=self.helper_action.n_load), + "load_q": np.random.logistic(size=self.helper_action.n_load), + }}) + act += self.helper_action({"shunt": {"shunt_p": np.random.uniform(size=self.helper_action.n_shunt), + "shunt_q": np.random.normal(size=self.helper_action.n_shunt), + "shunt_bus": [(0, 1), (1, 2)], + } + }) + dict_ = act.as_serializable_dict() + act2 = self.helper_action(dict_) + assert act == act2 + dict_2 = act.as_serializable_dict() + assert dict_ == dict_2 + with tempfile.TemporaryFile(mode="w") as f: + json.dump(fp=f, obj=dict_) diff --git a/grid2op/tests/test_attached_envs.py b/grid2op/tests/test_attached_envs.py index 22608e0ef..0444c747a 100644 --- a/grid2op/tests/test_attached_envs.py +++ b/grid2op/tests/test_attached_envs.py @@ -15,7 +15,7 @@ from grid2op.Space import GridObjects from grid2op.Action.PowerlineSetAction import PowerlineSetAction from grid2op.Action.PlayableAction import PlayableAction -from grid2op.Observation.CompleteObservation import CompleteObservation +from grid2op.Observation.completeObservation import CompleteObservation from grid2op.Action.DontAct import DontAct from grid2op.Opponent import GeometricOpponent @@ -49,8 +49,8 @@ def test_action_space(self): def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) - assert self.env.observation_space.n == 1332 + 4 + 24 + 2, f"obs space size is {self.env.observation_space.n}," \ - f"should be {1332 + 4 + 24}" + assert self.env.observation_space.n == 1332 + 4 + 24 + 2 + 1, f"obs space size is {self.env.observation_space.n}," \ + f"should be {1332 + 4 + 24 + 2 + 1}" def test_random_action(self): """test i can perform some step (random)""" @@ -91,9 +91,9 @@ def test_action_space(self): def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) - assert self.env.observation_space.n == 1332 + 4 + 24 + 3 + 2, f"obs space size is " \ + assert self.env.observation_space.n == 1332 + 4 + 24 + 3 + 2 +1, f"obs space size is " \ f"{self.env.observation_space.n}," \ - f"should be {1363}" + f"should be {1366}" def test_random_action(self): """test i can perform some step (random)""" @@ -132,8 +132,8 @@ def test_action_space(self): def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) - assert self.env.observation_space.n == 4054 + 4 + 56 + 2, f"obs space size is {self.env.observation_space.n}," \ - f"should be {4054 + 4 + 56}" + assert self.env.observation_space.n == 4054 + 4 + 56 + 2 + 1, f"obs space size is {self.env.observation_space.n}," \ + f"should be {4117}" def test_random_action(self): """test i can perform some step (random)""" @@ -172,8 +172,8 @@ def test_action_space(self): def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) - assert self.env.observation_space.n == 438 + 4 + 4 + 2, f"obs space size is {self.env.observation_space.n}," \ - f"should be {438 + 4 + 4}" + assert self.env.observation_space.n == 438 + 4 + 4 + 2 + 1, f"obs space size is {self.env.observation_space.n}," \ + f"should be {438 + 4 + 4 + 2 + 1}" def test_random_action(self): """test i can perform some step (random)""" @@ -212,8 +212,8 @@ def test_action_space(self): def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) - assert self.env.observation_space.n == 438 + 4 + 4 + 2, f"obs space size is {self.env.observation_space.n}," \ - f"should be {438 + 4 + 4}" + assert self.env.observation_space.n == 438 + 4 + 4 + 2 + 1, f"obs space size is {self.env.observation_space.n}," \ + f"should be {438 + 4 + 4 + 3}" def test_random_action(self): """test i can perform some step (random)""" @@ -252,8 +252,8 @@ def test_action_space(self): def test_observation_space(self): assert issubclass(self.env.observation_space.subtype, CompleteObservation) - assert self.env.observation_space.n == 446 + 4 + 4 + 2, f"obs space size is {self.env.observation_space.n}," \ - f"should be {446 + 4 + 4}" + assert self.env.observation_space.n == 446 + 4 + 4 + 2 + 1, f"obs space size is {self.env.observation_space.n}," \ + f"should be {446 + 4 + 4 + 3}" def test_random_action(self): """test i can perform some step (random)""" diff --git a/grid2op/tests/test_attached_envs_compat.py b/grid2op/tests/test_attached_envs_compat.py index 3d0d1db74..dc263d348 100644 --- a/grid2op/tests/test_attached_envs_compat.py +++ b/grid2op/tests/test_attached_envs_compat.py @@ -15,7 +15,7 @@ from grid2op.Space import GridObjects from grid2op.Action.PowerlineSetAction import PowerlineSetAction from grid2op.Action.PlayableAction import PlayableAction -from grid2op.Observation.CompleteObservation import CompleteObservation +from grid2op.Observation.completeObservation import CompleteObservation from grid2op.Action.DontAct import DontAct import pdb diff --git a/grid2op/tests/test_back_to_orig.py b/grid2op/tests/test_back_to_orig.py new file mode 100644 index 000000000..d2e1f532e --- /dev/null +++ b/grid2op/tests/test_back_to_orig.py @@ -0,0 +1,277 @@ +# 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. + +# do some generic tests that can be implemented directly to test if a backend implementation can work out of the box +# with grid2op. +# see an example of test_Pandapower for how to use this suit. +import unittest +import numpy as np +import warnings + +import grid2op +from grid2op.Parameters import Parameters +from grid2op.Action import BaseAction +import pdb + +class Test_BackToOrig(unittest.TestCase): + def setUp(self) -> None: + self.env_name = "educ_case14_storage" + param = Parameters() + param.NO_OVERFLOW_DISCONNECTION = True + param.NB_TIMESTEP_COOLDOWN_LINE = 0 + param.NB_TIMESTEP_COOLDOWN_SUB = 0 + param.ACTIVATE_STORAGE_LOSS = False + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make(self.env_name, test=True, action_class=BaseAction, param=param) + + def tearDown(self) -> None: + self.env.close() + + def test_substation(self): + obs, reward, done, info = self.env.step(self.env.action_space({"set_bus": {"substations_id": [(2, (1, 2, 2, 1))]}})) + assert not done + obs, reward, done, info = self.env.step(self.env.action_space({"set_bus": {"substations_id": [(5, (1, 2, 2, 1, 2, 1, 1, 2))]}})) + assert not done + res = self.env.action_space.get_back_to_ref_state(obs) + assert len(res) == 1 + assert "substation" in res + assert len(res["substation"]) == 2 + for act in res["substation"]: + lines_impacted, subs_impacted = act.get_topological_impact() + assert subs_impacted[2] ^ subs_impacted[5] # xor + assert np.sum(lines_impacted) == 0 + + for act in res["substation"]: + obs, reward, done, info = self.env.step(act) + assert not done + assert len(self.env.action_space.get_back_to_ref_state(obs)) == 0# I am in the original topology + + def test_line(self): + obs = self.env.reset() + res = self.env.action_space.get_back_to_ref_state(obs) + assert len(res) == 0 + obs, reward, done, info = self.env.step(self.env.action_space({"set_line_status": [(12, -1)]})) + assert not done + obs, reward, done, info = self.env.step(self.env.action_space({"set_line_status": [(15, -1)]})) + assert not done + res = self.env.action_space.get_back_to_ref_state(obs) + assert len(res) == 1 + assert "powerline" in res + assert len(res["powerline"]) == 2 + for act in res["powerline"]: + lines_impacted, subs_impacted = act.get_topological_impact() + assert lines_impacted[12] ^ lines_impacted[15] # xor + assert np.sum(subs_impacted) == 0 + + for act in res["powerline"]: + obs, reward, done, info = self.env.step(act) + assert not done + assert len(self.env.action_space.get_back_to_ref_state(obs)) == 0 # I am in the original topology + + def test_redisp(self): + obs = self.env.reset() + res = self.env.action_space.get_back_to_ref_state(obs) + assert len(res) == 0 + obs, reward, done, info = self.env.step(self.env.action_space({"redispatch": [(0, self.env.gen_max_ramp_up[0]), (1, -self.env.gen_max_ramp_down[1])]})) + assert not done + res = self.env.action_space.get_back_to_ref_state(obs) + assert len(res) == 1 + assert "redispatching" in res + assert len(res["redispatching"]) == 1 # one action is enough + + obs, reward, done, info = self.env.step(self.env.action_space({"redispatch": [(0, self.env.gen_max_ramp_up[0]), (1, -self.env.gen_max_ramp_down[1])]})) + assert not done + res = self.env.action_space.get_back_to_ref_state(obs) + assert len(res) == 1 + assert "redispatching" in res + assert len(res["redispatching"]) == 2 # one action is NOT enough + + for act in res["redispatching"]: + obs, reward, done, info = self.env.step(act) + assert not done + assert np.max(np.abs(obs.target_dispatch)) <= 1e-6 # I am in the original topology + assert len(self.env.action_space.get_back_to_ref_state(obs)) == 0 + + # now try with "non integer" stuff + obs, reward, done, info = self.env.step(self.env.action_space({"redispatch": [(0, self.env.gen_max_ramp_up[0]), (1, -self.env.gen_max_ramp_down[1])]})) + assert not done + obs, reward, done, info = self.env.step(self.env.action_space({"redispatch": [(0, 0.5 * self.env.gen_max_ramp_up[0]), (1, -0.5 * self.env.gen_max_ramp_down[1])]})) + res = self.env.action_space.get_back_to_ref_state(obs) + assert len(res) == 1 + assert "redispatching" in res + assert len(res["redispatching"]) == 2 # one action is NOT enough + for act in res["redispatching"]: + obs, reward, done, info = self.env.step(act) + assert not done + assert np.max(np.abs(obs.target_dispatch)) <= 1e-6 # I am in the original topology + + # try with non integer, non symmetric stuff + obs, reward, done, info = self.env.step(self.env.action_space({"redispatch": [(0, self.env.gen_max_ramp_up[0]), (1, -self.env.gen_max_ramp_down[1])]})) + assert not done + obs, reward, done, info = self.env.step(self.env.action_space({"redispatch": [(0, 0.5 * self.env.gen_max_ramp_up[0])]})) + res = self.env.action_space.get_back_to_ref_state(obs) + assert len(res) == 1 + assert "redispatching" in res + assert len(res["redispatching"]) == 2 # one action is NOT enough + for act in res["redispatching"]: + obs, reward, done, info = self.env.step(act) + assert not done + assert np.max(np.abs(obs.target_dispatch)) <= 1e-6 # I am in the original topology + assert len(self.env.action_space.get_back_to_ref_state(obs)) == 0 + + def test_storage_no_loss(self): + obs = self.env.reset() + res = self.env.action_space.get_back_to_ref_state(obs) + assert len(res) == 0 + obs, reward, done, info = self.env.step(self.env.action_space({"set_storage": [(0, self.env.storage_max_p_absorb[0]), (1, -self.env.storage_max_p_prod[1])]})) + assert not done + res = self.env.action_space.get_back_to_ref_state(obs) + assert len(res) == 1 + assert "storage" in res + assert len(res["storage"]) == 1 # one action is enough (no losses) + + obs, reward, done, info = self.env.step(self.env.action_space({"set_storage": [(0, self.env.storage_max_p_absorb[0]), (1, -self.env.storage_max_p_prod[1])]})) + assert not done + res = self.env.action_space.get_back_to_ref_state(obs) + assert len(res) == 1 + assert "storage" in res + assert len(res["storage"]) == 2 # one action is NOT enough + + for act in res["storage"]: + obs, reward, done, info = self.env.step(act) + assert not done + assert len(self.env.action_space.get_back_to_ref_state(obs)) == 0 # I am in the original topology + + # now try with "non integer" stuff + obs, reward, done, info = self.env.step(self.env.action_space({"set_storage": [(0, self.env.storage_max_p_absorb[0]), (1, -self.env.storage_max_p_prod[1])]})) + assert not done + obs, reward, done, info = self.env.step(self.env.action_space({"set_storage": [(0, 0.5 * self.env.storage_max_p_absorb[0]), (1, -0.5 * self.env.storage_max_p_prod[1])]})) + res = self.env.action_space.get_back_to_ref_state(obs) + assert len(res) == 1 + assert "storage" in res + assert len(res["storage"]) == 2 # one action is NOT enough + for act in res["storage"]: + obs, reward, done, info = self.env.step(act) + assert not done + assert np.max(np.abs(obs.target_dispatch)) <= 1e-6 + assert len(self.env.action_space.get_back_to_ref_state(obs)) == 0 # I am in the original topology + + # try with non integer, non symmetric stuff + obs, reward, done, info = self.env.step(self.env.action_space({"set_storage": [(0, self.env.storage_max_p_absorb[0]), (1, -self.env.storage_max_p_prod[1])]})) + assert not done + obs, reward, done, info = self.env.step(self.env.action_space({"set_storage": [(0, 0.5 * self.env.storage_max_p_absorb[0])]})) + res = self.env.action_space.get_back_to_ref_state(obs) + assert len(res) == 1 + assert "storage" in res + assert len(res["storage"]) == 2 # one action is NOT enough + for act in res["storage"]: + obs, reward, done, info = self.env.step(act) + assert not done + assert len(self.env.action_space.get_back_to_ref_state(obs)) == 0 # I am in the original topology + + def test_storage_with_loss(self): + param = self.env.parameters + param.ACTIVATE_STORAGE_LOSS = True + self.env.change_parameters(param) + obs = self.env.reset() + res = self.env.action_space.get_back_to_ref_state(obs) + assert len(res) == 0 + + # check i get the right power if i do nothing + obs, reward, done, info = self.env.step(self.env.action_space()) + res = self.env.action_space.get_back_to_ref_state(obs) + assert len(res) == 1 + assert "storage" in res + assert len(res["storage"]) == 1 # one action is enough to compensate the losses + assert np.all(np.abs(res["storage"][0].storage_p - self.env.storage_loss) <= 1e-5) + + # now do some action + obs, reward, done, info = self.env.step(self.env.action_space({"set_storage": [(0, self.env.storage_max_p_absorb[0]), (1, -self.env.storage_max_p_prod[1])]})) + assert not done + res = self.env.action_space.get_back_to_ref_state(obs) + assert len(res) == 1 + assert "storage" in res + assert len(res["storage"]) == 2 # one action is NOT enough (no losses) + + obs, reward, done, info = self.env.step(self.env.action_space({"set_storage": [(0, self.env.storage_max_p_absorb[0]), (1, -self.env.storage_max_p_prod[1])]})) + assert not done + res = self.env.action_space.get_back_to_ref_state(obs) + assert len(res) == 1 + assert "storage" in res + assert len(res["storage"]) == 3 # two actions are NOT enough (losses) + + for act in res["storage"]: + obs, reward, done, info = self.env.step(act) + assert not done + dict_ = self.env.action_space.get_back_to_ref_state(obs) + assert len(dict_) == 1 + assert "storage" in dict_ + assert np.all(np.abs(dict_["storage"][0].storage_p - 3. * self.env.storage_loss) <= 1e-5) # I am in the original topology (up to the storage losses) + + # now try with "non integer" stuff + obs, reward, done, info = self.env.step(self.env.action_space({"set_storage": [(0, self.env.storage_max_p_absorb[0]), (1, -self.env.storage_max_p_prod[1])]})) + assert not done + obs, reward, done, info = self.env.step(self.env.action_space({"set_storage": [(0, 0.5 * self.env.storage_max_p_absorb[0]), (1, -0.5 * self.env.storage_max_p_prod[1])]})) + res = self.env.action_space.get_back_to_ref_state(obs) + assert len(res) == 1 + assert "storage" in res + assert len(res["storage"]) == 2 # one action is NOT enough + for act in res["storage"]: + obs, reward, done, info = self.env.step(act) + assert not done + dict_ = self.env.action_space.get_back_to_ref_state(obs) + assert len(dict_) == 1 + assert "storage" in dict_ + assert np.all(np.abs(dict_["storage"][0].storage_p - 2. * self.env.storage_loss) <= 1e-5) # I am in the original topology (up to the storage losses) + + # try with non integer, non symmetric stuff + obs, reward, done, info = self.env.step(self.env.action_space({"set_storage": [(0, self.env.storage_max_p_absorb[0]), (1, -self.env.storage_max_p_prod[1])]})) + assert not done + obs, reward, done, info = self.env.step(self.env.action_space({"set_storage": [(0, 0.5 * self.env.storage_max_p_absorb[0])]})) + res = self.env.action_space.get_back_to_ref_state(obs) + assert len(res) == 1 + assert "storage" in res + assert len(res["storage"]) == 2 # one action is NOT enough + for act in res["storage"]: + obs, reward, done, info = self.env.step(act) + assert not done + dict_ = self.env.action_space.get_back_to_ref_state(obs) + assert len(dict_) == 1 + assert "storage" in dict_ + assert np.all(np.abs(dict_["storage"][0].storage_p - 2. * self.env.storage_loss) <= 1e-5) # I am in the original topology (up to the storage losses) + + def test_curtailment(self): + obs, reward, done, info = self.env.step(self.env.action_space({"curtail": [(3, .05)]})) + assert not done + res = self.env.action_space.get_back_to_ref_state(obs) + assert len(res) == 1 + assert "curtailment" in res + assert len(res["curtailment"]) == 1 + for act in res["curtailment"]: + obs, reward, done, info = self.env.step(act) + assert not done + assert np.all(obs.curtailment_limit == 1.) + assert len(self.env.action_space.get_back_to_ref_state(obs)) == 0 # I am in the original topology + + obs, reward, done, info = self.env.step(self.env.action_space({"curtail": [(3, .05)]})) + obs, reward, done, info = self.env.step(self.env.action_space({"curtail": [(4, .5)]})) + assert not done + res = self.env.action_space.get_back_to_ref_state(obs) + assert len(res) == 1 + assert "curtailment" in res + assert len(res["curtailment"]) == 1 + for act in res["curtailment"]: + obs, reward, done, info = self.env.step(act) + assert not done + assert np.all(obs.curtailment_limit == 1.) + assert len(self.env.action_space.get_back_to_ref_state(obs)) == 0 # I am in the original topology + +# TODO test when not all action types are enable (typically the change / set part) +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/grid2op/tests/test_chronics_npy.py b/grid2op/tests/test_chronics_npy.py new file mode 100644 index 000000000..5f7b0aeb4 --- /dev/null +++ b/grid2op/tests/test_chronics_npy.py @@ -0,0 +1,408 @@ +# 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 grid2op +import unittest +import warnings +import copy +from grid2op.Parameters import Parameters +from grid2op.Chronics import FromNPY, GridStateFromFileWithForecastsWithMaintenance +from grid2op.Exceptions import Grid2OpException +from grid2op.Runner import Runner +import numpy as np +import pdb + +from grid2op.tests.helper_path_test import * + +class TestNPYChronics(unittest.TestCase): + """ + This class tests the possibility in grid2op to limit the number of call to "obs.simulate" + """ + def setUp(self): + self.env_name = "l2rpn_case14_sandbox" + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env_ref = grid2op.make(self.env_name, test=True) + + self.load_p = 1.0 * self.env_ref.chronics_handler.real_data.data.load_p + self.load_q = 1.0 * self.env_ref.chronics_handler.real_data.data.load_q + self.prod_p = 1.0 * self.env_ref.chronics_handler.real_data.data.prod_p + self.prod_v = 1.0 * self.env_ref.chronics_handler.real_data.data.prod_v + + def tearDown(self) -> None: + self.env_ref.close() + + def test_proper_start_end(self): + """test i can create an environment with the FromNPY class""" + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(self.env_name, + chronics_class=FromNPY, + test=True, + data_feeding_kwargs={"i_start": 0, + "i_end": 18, # excluded + "load_p": self.load_p, + "load_q": self.load_q, + "prod_p": self.prod_p, + "prod_v": self.prod_v} + ) + + for ts in range(10): + obs_ref, *_ = self.env_ref.step(env.action_space()) + assert np.all(obs_ref.gen_p[:-1] == self.prod_p[1 + ts, :-1]), f"error at iteration {ts}" + obs, *_ = env.step(env.action_space()) + assert np.all(obs_ref.gen_p == obs.gen_p), f"error at iteration {ts}" + + # test the "end" + for ts in range(7): + obs, *_ = env.step(env.action_space()) + obs, reward, done, info = env.step(env.action_space()) + assert done + assert obs.max_step == 18 + with self.assertRaises(Grid2OpException): + env.step(env.action_space()) # raises a Grid2OpException + env.close() + + def test_proper_start_end_2(self): + """test i can do as if the start was "later" """ + LAG = 5 + END = 18 + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(self.env_name, + chronics_class=FromNPY, + test=True, + data_feeding_kwargs={"i_start": LAG, + "i_end": END, + "load_p": self.load_p, + "load_q": self.load_q, + "prod_p": self.prod_p, + "prod_v": self.prod_v} + ) + + for ts in range(LAG): + obs_ref, *_ = self.env_ref.step(env.action_space()) + + for ts in range(END - LAG): + obs_ref, *_ = self.env_ref.step(env.action_space()) + assert np.all(obs_ref.gen_p[:-1] == self.prod_p[1 + ts + LAG, :-1]), f"error at iteration {ts}" + obs, *_ = env.step(env.action_space()) + assert np.all(obs_ref.gen_p == obs.gen_p), f"error at iteration {ts}" + assert obs.max_step == END + with self.assertRaises(Grid2OpException): + env.step(env.action_space()) # raises a Grid2OpException because the env is done + env.close() + + def test_iend_bigger_dim(self): + max_step = 5 + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(self.env_name, + chronics_class=FromNPY, + test=True, + data_feeding_kwargs={"i_start": 0, + "i_end": 10, # excluded + "load_p": self.load_p[:max_step,:], + "load_q": self.load_q[:max_step,:], + "prod_p": self.prod_p[:max_step,:], + "prod_v": self.prod_v[:max_step,:]} + ) + assert env.chronics_handler.real_data._load_p.shape[0] == max_step + for ts in range(max_step - 1): # -1 because one ts is "burnt" for the initialization + obs, reward, done, info = env.step(env.action_space()) + assert np.all(self.prod_p[1 + ts, :-1] == obs.gen_p[:-1]), f"error at iteration {ts}" + + obs, reward, done, info = env.step(env.action_space()) + assert done + assert obs.max_step == max_step + with self.assertRaises(Grid2OpException): + env.step(env.action_space()) # raises a Grid2OpException because the env is done + env.close() + + def test_change_iend(self): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(self.env_name, + chronics_class=FromNPY, + test=True, + data_feeding_kwargs={"load_p": self.load_p, + "load_q": self.load_q, + "prod_p": self.prod_p, + "prod_v": self.prod_v} + ) + assert env.chronics_handler.real_data._i_end == self.load_p.shape[0] + env.chronics_handler.real_data.change_i_end(15) + env.reset() + assert env.chronics_handler.real_data._i_end == 15 + env.reset() + assert env.chronics_handler.real_data._i_end == 15 + env.chronics_handler.real_data.change_i_end(25) + assert env.chronics_handler.real_data._i_end == 15 + env.reset() + assert env.chronics_handler.real_data._i_end == 25 + env.chronics_handler.real_data.change_i_end(None) # reset default value + env.reset() + assert env.chronics_handler.real_data._i_end == self.load_p.shape[0] + + # now make sure it recomputes the maximum even if i change the size of the input arrays + env.chronics_handler.real_data.change_chronics(self.load_p[:10], self.load_q[:10], self.prod_p[:10], self.prod_v[:10]) + env.reset() + assert env.chronics_handler.real_data._i_end == 10 + env.chronics_handler.real_data.change_chronics(self.load_p, self.load_q, self.prod_p, self.prod_v) + env.reset() + assert env.chronics_handler.real_data._i_end == self.load_p.shape[0] + env.close() + + def test_change_istart(self): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(self.env_name, + chronics_class=FromNPY, + test=True, + data_feeding_kwargs={"load_p": self.load_p, + "load_q": self.load_q, + "prod_p": self.prod_p, + "prod_v": self.prod_v} + ) + assert env.chronics_handler.real_data._i_start == 0 + env.chronics_handler.real_data.change_i_start(5) + env.reset() + assert env.chronics_handler.real_data._i_start == 5 + env.reset() + assert env.chronics_handler.real_data._i_start == 5 + env.chronics_handler.real_data.change_i_start(10) + assert env.chronics_handler.real_data._i_start == 5 + env.reset() + assert env.chronics_handler.real_data._i_start == 10 + env.chronics_handler.real_data.change_i_start(None) # reset default value + env.reset() + assert env.chronics_handler.real_data._i_start == 0 + env.close() + + def test_runner(self): + max_step = 10 + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(self.env_name, + chronics_class=FromNPY, + test=True, + data_feeding_kwargs={"i_start": 0, + "i_end": 10, # excluded + "load_p": self.load_p[:max_step,:], + "load_q": self.load_q[:max_step,:], + "prod_p": self.prod_p[:max_step,:], + "prod_v": self.prod_v[:max_step,:]} + ) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") # silence the UserWarning: Class FromNPY doesn't handle different input folder. "tell_id" method has no impact. + # warnings.warn("Class {} doesn't handle different input folder. \"tell_id\" method has no impact." + runner = Runner(**env.get_params_for_runner()) + res = runner.run(nb_episode=1) + assert res[0][3] == 10 # number of time step selected + env.close() + + def test_change_chronics(self): + """test i can change the chronics""" + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(self.env_name, + chronics_class=FromNPY, + test=True, + data_feeding_kwargs={"i_start": 0, + "i_end": 18, # excluded + "load_p": self.load_p, + "load_q": self.load_q, + "prod_p": self.prod_p, + "prod_v": self.prod_v} + ) + self.env_ref.reset() + + load_p = 1.0 * self.env_ref.chronics_handler.real_data.data.load_p + load_q = 1.0 * self.env_ref.chronics_handler.real_data.data.load_q + prod_p = 1.0 * self.env_ref.chronics_handler.real_data.data.prod_p + prod_v = 1.0 * self.env_ref.chronics_handler.real_data.data.prod_v + + env.chronics_handler.real_data.change_chronics(load_p, load_q, prod_p, prod_v) + for ts in range(10): + obs, *_ = env.step(env.action_space()) + assert np.all(self.prod_p[1 + ts, :-1] == obs.gen_p[:-1]), f"error at iteration {ts}" + env.reset() + for ts in range(10): + obs, *_ = env.step(env.action_space()) + assert np.all(prod_p[1 + ts, :-1] == obs.gen_p[:-1]), f"error at iteration {ts}" + env.close() + + def test_with_env_copy(self): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(self.env_name, + chronics_class=FromNPY, + test=True, + data_feeding_kwargs={"i_start": 0, + "i_end": 10, # excluded + "load_p": self.load_p, + "load_q": self.load_q, + "prod_p": self.prod_p, + "prod_v": self.prod_v} + ) + env_cpy = env.copy() + for ts in range(10): + obs, *_ = env.step(env.action_space()) + assert np.all(self.prod_p[1 + ts, :-1] == obs.gen_p[:-1]), f"error at iteration {ts}" + for ts in range(10): + obs_cpy, *_ = env_cpy.step(env.action_space()) + assert np.all(self.prod_p[1 + ts, :-1] == obs_cpy.gen_p[:-1]), f"error at iteration {ts}" + + self.env_ref.reset() + + load_p = 1.0 * self.env_ref.chronics_handler.real_data.data.load_p + load_q = 1.0 * self.env_ref.chronics_handler.real_data.data.load_q + prod_p = 1.0 * self.env_ref.chronics_handler.real_data.data.prod_p + prod_v = 1.0 * self.env_ref.chronics_handler.real_data.data.prod_v + env.chronics_handler.real_data.change_chronics(load_p, load_q, prod_p, prod_v) + env.reset() + env_cpy.reset() + for ts in range(10): + obs, *_ = env.step(env.action_space()) + assert np.all(prod_p[1 + ts, :-1] == obs.gen_p[:-1]), f"error at iteration {ts}" + for ts in range(10): + obs_cpy, *_ = env_cpy.step(env.action_space()) + assert np.all(self.prod_p[1 + ts, :-1] == obs_cpy.gen_p[:-1]), f"error at iteration {ts}" + env.close() + + def test_forecast(self): + load_p_f = 1.0 * self.env_ref.chronics_handler.real_data.data.load_p_forecast + load_q_f = 1.0 * self.env_ref.chronics_handler.real_data.data.load_q_forecast + prod_p_f = 1.0 * self.env_ref.chronics_handler.real_data.data.prod_p_forecast + prod_v_f = 1.0 * self.env_ref.chronics_handler.real_data.data.prod_v_forecast + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(self.env_name, + chronics_class=FromNPY, + test=True, + data_feeding_kwargs={"i_start": 0, + "i_end": 10, # excluded + "load_p": self.load_p, + "load_q": self.load_q, + "prod_p": self.prod_p, + "prod_v": self.prod_v, + "load_p_forecast": load_p_f, + "load_q_forecast": load_q_f, + "prod_p_forecast": prod_p_f, + "prod_v_forecast": prod_v_f, + } + ) + + for ts in range(10): + obs, *_ = env.step(env.action_space()) + assert np.all(self.prod_p[1 + ts, :-1] == obs.gen_p[:-1]), f"error at iteration {ts}" + sim_obs, *_ = obs.simulate(env.action_space()) + assert np.all(prod_p_f[1 + ts, :-1] == sim_obs.gen_p[:-1]), f"error at iteration {ts}" + assert sim_obs.minute_of_hour == (obs.minute_of_hour + 5) % 60, f"error at iteration {ts}" + env.close() + + def test_change_forecast(self): + load_p_f = 1.0 * self.env_ref.chronics_handler.real_data.data.load_p_forecast + load_q_f = 1.0 * self.env_ref.chronics_handler.real_data.data.load_q_forecast + prod_p_f = 1.0 * self.env_ref.chronics_handler.real_data.data.prod_p_forecast + prod_v_f = 1.0 * self.env_ref.chronics_handler.real_data.data.prod_v_forecast + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(self.env_name, + chronics_class=FromNPY, + test=True, + data_feeding_kwargs={"i_start": 0, + "i_end": 10, # excluded + "load_p": self.load_p, + "load_q": self.load_q, + "prod_p": self.prod_p, + "prod_v": self.prod_v, + "load_p_forecast": load_p_f, + "load_q_forecast": load_q_f, + "prod_p_forecast": prod_p_f, + "prod_v_forecast": prod_v_f, + } + ) + + env.chronics_handler.real_data.change_forecasts(self.load_p, self.load_q, self.prod_p, self.prod_v) # should not affect anything + for ts in range(10): + obs, *_ = env.step(env.action_space()) + assert np.all(self.prod_p[1 + ts, :-1] == obs.gen_p[:-1]), f"error at iteration {ts}" + sim_obs, *_ = obs.simulate(env.action_space()) + assert np.all(prod_p_f[1 + ts, :-1] == sim_obs.gen_p[:-1]), f"error at iteration {ts}" + assert sim_obs.minute_of_hour == (obs.minute_of_hour + 5) % 60, f"error at iteration {ts}" + + env.reset() # now forecast should be modified + for ts in range(10): + obs, *_ = env.step(env.action_space()) + assert np.all(self.prod_p[1 + ts, :-1] == obs.gen_p[:-1]), f"error at iteration {ts}" + sim_obs, *_ = obs.simulate(env.action_space()) + assert np.all(obs.gen_p == sim_obs.gen_p), f"error at iteration {ts}" + assert sim_obs.minute_of_hour == (obs.minute_of_hour + 5) % 60, f"error at iteration {ts}" + env.close() + + +class TestNPYChronicsWithHazards(unittest.TestCase): + """ + This class tests the possibility in grid2op to limit the number of call to "obs.simulate" + """ + def test_maintenance_ok(self): + param = Parameters() + param.NO_OVERFLOW_DISCONNECTION = True + env_path = os.path.join(PATH_DATA_TEST, "env_14_test_maintenance") + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env_ref = grid2op.make(env_path, + test=True, + param=param) + env_ref.chronics_handler.real_data.data.maintenance_starting_hour = 1 + env_ref.chronics_handler.real_data.data.maintenance_ending_hour = 2 + env_ref.seed(0) # 1 -> 108 + env_ref.reset() + load_p = 1.0 * env_ref.chronics_handler.real_data.data.load_p + load_q = 1.0 * env_ref.chronics_handler.real_data.data.load_q + prod_p = 1.0 * env_ref.chronics_handler.real_data.data.prod_p + prod_v = 1.0 * env_ref.chronics_handler.real_data.data.prod_v + load_p_f = 1.0 * env_ref.chronics_handler.real_data.data.load_p_forecast + load_q_f = 1.0 * env_ref.chronics_handler.real_data.data.load_q_forecast + prod_p_f = 1.0 * env_ref.chronics_handler.real_data.data.prod_p_forecast + prod_v_f = 1.0 * env_ref.chronics_handler.real_data.data.prod_v_forecast + maintenance = copy.deepcopy(env_ref.chronics_handler.real_data.data.maintenance) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env = grid2op.make(env_path, + chronics_class=FromNPY, + test=True, + data_feeding_kwargs={"i_start": 0, + "i_end": 10, # excluded + "load_p": load_p, + "load_q": load_q, + "prod_p": prod_p, + "prod_v": prod_v, + "load_p_forecast": load_p_f, + "load_q_forecast": load_q_f, + "prod_p_forecast": prod_p_f, + "prod_v_forecast": prod_v_f, + "maintenance": maintenance + }, + param=param + ) + obs = env.reset() + obs_ref = env_ref.reset() + for ts in range(8): + obs, *_ = env.step(env.action_space()) + obs_ref, *_ = env_ref.step(env.action_space()) + assert np.all(obs.time_before_cooldown_line == obs_ref.time_before_cooldown_line), f"error at step {ts}" + sim_obs, *_ = obs.simulate(env.action_space()) + sim_obs_ref, *_ = obs_ref.simulate(env.action_space()) + assert np.all(sim_obs.time_before_cooldown_line == sim_obs_ref.time_before_cooldown_line), f"error at step {ts}" + + # TODO test obs.max_step + # test hazards +if __name__ == "__main__": + unittest.main() diff --git a/grid2op/tests/test_gym_compat.py b/grid2op/tests/test_gym_compat.py index d77d15589..0dc82bb4e 100644 --- a/grid2op/tests/test_gym_compat.py +++ b/grid2op/tests/test_gym_compat.py @@ -54,11 +54,14 @@ def tearDown(self) -> None: self.env.close() def test_print_with_no_storage(self): - self.env = grid2op.make("l2rpn_icaps_2021", - test=True, - _add_to_name="TestGymCompatModule") + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make("l2rpn_icaps_2021", + test=True, + _add_to_name="TestGymCompatModule") env_gym = GymEnv(self.env) - res_ = env_gym.action_space.__str__() # this crashed + str_ = env_gym.action_space.__str__() # this crashed + str_ = env_gym.observation_space.__str__() def test_convert_togym(self): """test i can create the env""" @@ -67,7 +70,7 @@ def test_convert_togym(self): assert dim_act_space == 160 dim_obs_space = np.sum([np.sum(env_gym.observation_space[el].shape).astype(int) for el in env_gym.observation_space.spaces]) - size_th = 434 + 4 + 2 + size_th = 434 + 4 + 2 + 1 assert dim_obs_space == size_th, f"Size should be {size_th} but is {dim_obs_space}" # test that i can do basic stuff there diff --git a/grid2op/tests/test_issue_140.py b/grid2op/tests/test_issue_140.py index 07d189cd1..da8d5406e 100644 --- a/grid2op/tests/test_issue_140.py +++ b/grid2op/tests/test_issue_140.py @@ -46,7 +46,7 @@ def test_issue_140(self): }) with warnings.catch_warnings(): warnings.filterwarnings("ignore") - env = grid2op.make(env_name, param=param) + env = grid2op.make(env_name, param=param,) ts_per_chronics = 2016 seed = 725 diff --git a/grid2op/tests/test_issue_148.py b/grid2op/tests/test_issue_148.py index bdf274d49..b509f213c 100644 --- a/grid2op/tests/test_issue_148.py +++ b/grid2op/tests/test_issue_148.py @@ -30,7 +30,7 @@ def test_issue_148(self): param.NB_TIMESTEP_COOLDOWN_SUB = 3 with warnings.catch_warnings(): warnings.filterwarnings("ignore") - env = grid2op.make(os.path.join(PATH_CHRONICS, "env_14_test_maintenance"), + env = grid2op.make(os.path.join(PATH_CHRONICS, "env_14_test_maintenance"), test=True, param=param) ID_MAINT = 11 # in maintenance at the second time step diff --git a/grid2op/tests/test_issue_185.py b/grid2op/tests/test_issue_185.py index 075ec09ba..9438619f9 100644 --- a/grid2op/tests/test_issue_185.py +++ b/grid2op/tests/test_issue_185.py @@ -150,8 +150,6 @@ def test_issue_185_act_discrete_space(self): if obs not in gym_env.observation_space: for k in obs: if not obs[k] in gym_env.observation_space[k]: - import pdb - pdb.set_trace() raise RuntimeError(f"Error for key {k} for env {env_name}") diff --git a/grid2op/tests/test_issue_274.py b/grid2op/tests/test_issue_274.py new file mode 100644 index 000000000..02d011e26 --- /dev/null +++ b/grid2op/tests/test_issue_274.py @@ -0,0 +1,57 @@ +# 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 warnings +import grid2op +import unittest +import numpy as np + +class Issue245Tester(unittest.TestCase): + def setUp(self): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env = grid2op.make("l2rpn_icaps_2021", test=True) + + def test_same_opponent_state(self): + """test that the opponent state is correctly copied""" + self.env.seed(3) + self.env.reset() + init_attack_times = 1 * self.env._opponent._attack_times + env_cpy = self.env.copy() + after_attack_times = 1 * self.env._opponent._attack_times + copy_attack_times = 1 * env_cpy._opponent._attack_times + assert np.all(init_attack_times == after_attack_times) + assert np.all(init_attack_times == copy_attack_times) + + def test_same_opponent_space(self): + """test that the opponent space state (in particular the current attack) is properly copied""" + self.env.seed(3) + self.env.reset() + import pdb + init_attack_times = 1 * self.env._opponent._attack_times + assert np.all(init_attack_times == [5, 105, 180]) + for i in range(5): + obs, reward, done, info = self.env.step(self.env.action_space()) + assert info["opponent_attack_line"] is None + obs, reward, done, info = self.env.step(self.env.action_space()) + assert info["opponent_attack_line"] is not None + init_line_attacked = np.where(info["opponent_attack_line"])[0] + + for i in range(2): + env_cpy = self.env.copy() + *_, info = self.env.step(self.env.action_space()) + *_, info_cpy = env_cpy.step(self.env.action_space()) + assert info["opponent_attack_line"] is not None, f"no line attacked at iteration {i}" + assert info_cpy["opponent_attack_line"] is not None, f"no line attacked at iteration {i} for the copy env" + line_attacked = np.where(info["opponent_attack_line"])[0] + cpy_line_attacked = np.where(info_cpy["opponent_attack_line"])[0] + assert init_line_attacked == line_attacked, f"wrong line attack at iteration {i}" + assert init_line_attacked == cpy_line_attacked, f"wrong line attack at iteration {i} for the copy env" + + + diff --git a/grid2op/tests/test_nb_simulate_called.py b/grid2op/tests/test_nb_simulate_called.py new file mode 100644 index 000000000..d74feaa26 --- /dev/null +++ b/grid2op/tests/test_nb_simulate_called.py @@ -0,0 +1,187 @@ +# 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 grid2op +import unittest +import warnings +import copy +from grid2op.Parameters import Parameters +from grid2op.Exceptions import SimulateUsedTooMuchThisStep, SimulateUsedTooMuchThisEpisode + + +class TestSimulateCount(unittest.TestCase): + """ + This class tests the possibility in grid2op to limit the number of call to "obs.simulate" + """ + def _aux_make_env(self, param=None): + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + if param is not None: + env = grid2op.make("l2rpn_case14_sandbox", test=True, param=param) + else: + env = grid2op.make("l2rpn_case14_sandbox", test=True) + return env + + def test_simple_cases(self): + env = self._aux_make_env() + obs = env.reset() + # basic test + obs.simulate(env.action_space()) + assert env.observation_space.nb_simulate_called_this_step == 1 + + obs.simulate(env.action_space()) + obs.simulate(env.action_space()) + assert env.observation_space.nb_simulate_called_this_step == 3 + + obs = env.reset() + assert env.observation_space.nb_simulate_called_this_step == 0 + + def test_with_copies(self): + env = self._aux_make_env() + # test with copies + env_cpy = env.copy() + obs_cpy = env_cpy.reset() + assert env_cpy.observation_space.nb_simulate_called_this_step == 0 + + obs = env.reset() + obs.simulate(env.action_space()) + assert env.observation_space.nb_simulate_called_this_step == 1 + assert env_cpy.observation_space.nb_simulate_called_this_step == 0 + + obs_cpy.simulate(env.action_space()) + assert env.observation_space.nb_simulate_called_this_step == 1 + assert env_cpy.observation_space.nb_simulate_called_this_step == 1 + + obs_cpy.simulate(env.action_space()) + assert env.observation_space.nb_simulate_called_this_step == 1 + assert env_cpy.observation_space.nb_simulate_called_this_step == 2 + + obs_cpy = env_cpy.reset() + assert env.observation_space.nb_simulate_called_this_step == 1 + assert env_cpy.observation_space.nb_simulate_called_this_step == 0 + + def test_max_step(self): + MAX_SIMULATE_PER_STEP = 10 + param = Parameters() + param.MAX_SIMULATE_PER_STEP = MAX_SIMULATE_PER_STEP + env = self._aux_make_env(param) + obs = env.reset() + for i in range(MAX_SIMULATE_PER_STEP): + obs.simulate(env.action_space()) + with self.assertRaises(SimulateUsedTooMuchThisStep): + obs.simulate(env.action_space()) # raises a SimulateUsedTooMuchThisStep + + # should be OK now + obs, *_ = env.step(env.action_space()) + obs.simulate(env.action_space()) + + def test_max_episode(self): + MAX_SIMULATE_PER_EPISODE = 10 + param = Parameters() + param.MAX_SIMULATE_PER_EPISODE = MAX_SIMULATE_PER_EPISODE + env = self._aux_make_env(param) + obs = env.reset() + for i in range(MAX_SIMULATE_PER_EPISODE): + obs.simulate(env.action_space()) + obs, *_ = env.step(env.action_space()) + + with self.assertRaises(SimulateUsedTooMuchThisEpisode): + obs.simulate(env.action_space()) # raises a SimulateUsedTooMuchThisEpisode + + obs = env.reset() + for i in range(MAX_SIMULATE_PER_EPISODE): + obs.simulate(env.action_space()) # should work now (reset called) + obs, *_ = env.step(env.action_space()) + + with self.assertRaises(SimulateUsedTooMuchThisEpisode): + obs.simulate(env.action_space()) # raises a SimulateUsedTooMuchThisEpisode + + obs = env.reset() + obs.simulate(env.action_space()) + + def test_max_step_with_copy(self): + MAX_SIMULATE_PER_STEP = 10 + MAX_SIMULATE_PER_STEP_CPY = 5 + param = Parameters() + param.MAX_SIMULATE_PER_STEP = MAX_SIMULATE_PER_STEP + env = self._aux_make_env(param) + + param = copy.deepcopy(param) + param.MAX_SIMULATE_PER_STEP = MAX_SIMULATE_PER_STEP_CPY + env_cpy = env.copy() + env_cpy.change_parameters(param) + obs = env.reset() + obs_cpy = env_cpy.reset() + for i in range(MAX_SIMULATE_PER_STEP): + obs.simulate(env.action_space()) + with self.assertRaises(SimulateUsedTooMuchThisStep): + obs.simulate(env.action_space()) # raises a SimulateUsedTooMuchThisStep + + for i in range(MAX_SIMULATE_PER_STEP_CPY): + obs_cpy.simulate(env.action_space()) # should work + + with self.assertRaises(SimulateUsedTooMuchThisStep): + obs_cpy.simulate(env.action_space()) # raises a SimulateUsedTooMuchThisStep + + # should be OK now + obs, *_ = env.step(env.action_space()) + obs.simulate(env.action_space()) # I can simulate on the original env correctly + with self.assertRaises(SimulateUsedTooMuchThisStep): + obs_cpy.simulate(env.action_space()) # raises a SimulateUsedTooMuchThisStep + + def test_max_episode_with_copy(self): + MAX_SIMULATE_PER_EPISODE = 10 + MAX_SIMULATE_PER_EPISODE_CPY = 10 + param = Parameters() + param.MAX_SIMULATE_PER_EPISODE = MAX_SIMULATE_PER_EPISODE + env = self._aux_make_env(param) + param = copy.deepcopy(param) + param.MAX_SIMULATE_PER_EPISODE = MAX_SIMULATE_PER_EPISODE_CPY + env_cpy = env.copy() + env_cpy.change_parameters(param) + obs = env.reset() + obs_cpy = env_cpy.reset() + + for i in range(MAX_SIMULATE_PER_EPISODE): + obs.simulate(env.action_space()) + obs, *_ = env.step(env.action_space()) + with self.assertRaises(SimulateUsedTooMuchThisEpisode): + obs.simulate(env.action_space()) # raises a SimulateUsedTooMuchThisEpisode + + for i in range(MAX_SIMULATE_PER_EPISODE_CPY): + obs_cpy.simulate(env.action_space()) # should not raise + obs_cpy, *_ = env_cpy.step(env.action_space()) + with self.assertRaises(SimulateUsedTooMuchThisEpisode): + obs_cpy.simulate(env.action_space()) # raises a SimulateUsedTooMuchThisEpisode + + obs = env.reset() + for i in range(MAX_SIMULATE_PER_EPISODE): + obs.simulate(env.action_space()) # should work now (reset called) + with self.assertRaises(SimulateUsedTooMuchThisEpisode): + obs_cpy.simulate(env.action_space()) # raises a SimulateUsedTooMuchThisEpisode (copy not reset) + + def test_no_limit(self): + MAX_SIMULATE_PER_EPISODE = 7 + env = self._aux_make_env() + obs = env.reset() + for _ in range(MAX_SIMULATE_PER_EPISODE + 1): + obs.simulate(env.action_space()) + + # change parameters and see if the limit works + param = Parameters() + param.MAX_SIMULATE_PER_EPISODE = MAX_SIMULATE_PER_EPISODE + env.change_parameters(param) + obs = env.reset() + for _ in range(MAX_SIMULATE_PER_EPISODE): + obs.simulate(env.action_space()) + + with self.assertRaises(SimulateUsedTooMuchThisEpisode): + obs.simulate(env.action_space()) # raises a SimulateUsedTooMuchThisEpisode (copy not reset) + +if __name__ == "__main__": + unittest.main() diff --git a/grid2op/tests/test_pickling.py b/grid2op/tests/test_pickling.py index 751141a5c..35a8df72b 100644 --- a/grid2op/tests/test_pickling.py +++ b/grid2op/tests/test_pickling.py @@ -26,8 +26,13 @@ class TestMultiProc(unittest.TestCase): @staticmethod - def f(glop_stuff): - return glop_stuff.action_space.sample() + def f(env_gym): + return env_gym.action_space.sample() + + @staticmethod + def g(env_gym): + act = env_gym.action_space.sample() + return env_gym.step(act)[0] def test_basic(self): with warnings.catch_warnings(): @@ -71,6 +76,9 @@ def test_basic(self): env_gym2 = copy.deepcopy(env_gym) with ctx.Pool(2) as p: p.map(TestMultiProc.f, [env_gym1, env_gym2]) + + with ctx.Pool(2) as p: + p.map(TestMultiProc.g, [env_gym1, env_gym2]) if __name__ == '__main__': diff --git a/setup.py b/setup.py index 7edaa5d9e..1c7eac4c4 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,6 @@ def my_test_suite(): "pandas>=1.0.3", "pandapower>=2.2.2", "tqdm>=4.45.0", - "pathlib>=1.0.1", "networkx>=2.4", "requests>=2.23.0" ], @@ -45,22 +44,6 @@ def my_test_suite(): "psutil>=5.7.0", "gym>=0.17.2", ], - "challenge": [ - "numpy==1.18.5", - "scipy==1.4.1", - "pandas==1.1.0", - "pandapower==2.3.0", - "tqdm==4.48.2", - "pathlib==1.0.1", - "networkx==2.4", - "requests==2.24.0", - "tensorflow==2.3.0", - "Keras==2.4.3", - "torch==1.6.0", - "statsmodels==0.11.1", - "scikit-learn==0.23.2", - "gym==0.17.2", - ], "docs": [ "numpydoc>=0.9.2", "sphinx>=2.4.4", @@ -73,11 +56,11 @@ def my_test_suite(): "flask", "flask_wtf", "ujson" - ] + ], + "plot": ["imageio"] } } - setup(name='Grid2Op', version='1.6.4', description='An gym compatible environment to model sequential decision making for powersystems',