diff --git a/.readthedocs.yml b/.readthedocs.yml index 6f2d283a9..8dbbe353f 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,7 +1,14 @@ -version: 2 +version: "2" + +build: + os: "ubuntu-22.04" + tools: + python: "3.10" + +sphinx: + configuration: docs/conf.py python: - version: 3.8 install: - method: pip path: . diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f492ba4a1..1486819a0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -34,11 +34,20 @@ Change Log [1.9.8] - 20xx-yy-zz ---------------------- +- [FIXED] the `backend.check_kirchoff` function was not correct when some elements were disconnected + (the wrong columns of the p_bus and q_bus was set in case of disconnected elements) +- [FIXED] `PandapowerBackend`, when no slack was present +- [FIXED] the "BaseBackendTest" class did not correctly detect divergence in most cases (which lead + to weird bugs in failing tests) +- [FIXED] an issue with imageio having deprecated the `fps` kwargs (see https://github.com/rte-france/Grid2Op/issues/569) +- [ADDED] A type of environment that does not perform the "emulation of the protections" + for some part of the grid (`MaskedEnvironment`) see https://github.com/rte-france/Grid2Op/issues/571 - [IMPROVED] the CI speed: by not testing every possible numpy version but only most ancient and most recent - [IMPROVED] Runner now test grid2op version 1.9.6 and 1.9.7 - [IMPROVED] refacto `gridobj_cls._clear_class_attribute` and `gridobj_cls._clear_grid_dependant_class_attributes` - [IMPROVED] the bahviour of the generic class `MakeBackend` used for the test suite. - [IMPROVED] re introducing python 12 testing +- [IMPROVED] error messages in the automatic test suite (`AAATestBackendAPI`) [1.9.7] - 2023-12-01 ---------------------- diff --git a/docs/action.rst b/docs/action.rst index a370d4d8b..90abdaa57 100644 --- a/docs/action.rst +++ b/docs/action.rst @@ -85,7 +85,7 @@ you want to perform on the grid. For more information you can consult the help o To avoid extremely verbose things, as of grid2op 1.5.0, we introduced some convenience functions to allow easier action construction. You can now do `act.load_set_bus = ...` instead of the previously way -more verbose `act.update({"set_bus": {"loads_id": ...}}` +more verbose `act.update({"set_bus": {"loads_id": ...}})` .. _action-module-examples: diff --git a/docs/environment.rst b/docs/environment.rst index 11cac0a59..88213ffec 100644 --- a/docs/environment.rst +++ b/docs/environment.rst @@ -101,7 +101,7 @@ be equivalent to starting into the "middle" of a video game. If that is the case Finally, you might have noticed that each call to "env.reset" might take a while. This can dramatically increase the training time, especially at the beginning. This is due to the fact that each time `env.reset` is called, the whole chronics is read from the hard drive. If you want to lower this -impact then you might consult the `Optimize the data pipeline`_ section. +impact then you might consult the :ref:`environment-module-data-pipeline` page of the doc. .. _environment-module-chronics-info: diff --git a/examples/multi_agents/distributed_actions_centralized_observations.py b/examples/multi_agents/distributed_actions_centralized_observations.py index 928bfba2e..e32aee454 100644 --- a/examples/multi_agents/distributed_actions_centralized_observations.py +++ b/examples/multi_agents/distributed_actions_centralized_observations.py @@ -17,9 +17,9 @@ # agent_name : controlled substation id zones = {"agent_0": [0, 1, 2, 3, 4], - "agent_1": [5,6,7,8,9,10,11,12,13]} + "agent_1": [5, 6, 7, 8, 9, 10, 11, 12, 13]} env = MultiAgentEnv(cent_env, action_domains=zones) - + env.seed(0) dict_obs = env.reset() # dict with: key=agent_name, value=the SubGridObservation diff --git a/examples/multi_agents/ray_example.py b/examples/multi_agents/ray_example.py index b318c3b43..155e08f0c 100644 --- a/examples/multi_agents/ray_example.py +++ b/examples/multi_agents/ray_example.py @@ -9,6 +9,7 @@ """example with centralized observation and local actions""" import warnings import numpy as np +import copy from gym.spaces import Discrete, Box @@ -16,7 +17,7 @@ from ray.rllib.policy.policy import PolicySpec, Policy import grid2op -from grid2op.Action.PlayableAction import PlayableAction +from grid2op.Action import PlayableAction from grid2op.multi_agent.multiAgentEnv import MultiAgentEnv from grid2op.gym_compat import GymEnv, BoxGymObsSpace, DiscreteActSpace @@ -40,10 +41,18 @@ class MAEnvWrapper(MAEnv): def __init__(self, env_config=None): super().__init__() + if env_config is None: + env_config = {} + # you can customize stuff by using the "env config" if you want + backend = LightSimBackend() + if "backend_cls" in env_config: + backend = env_config["backend_cls"] + # you can do the same for other attribute to the environment + env = grid2op.make(ENV_NAME, action_class=PlayableAction, - backend=LightSimBackend()) + backend=backend) self.ma_env = MultiAgentEnv(env, ACTION_DOMAINS) @@ -55,41 +64,66 @@ def __init__(self, env_config=None): # with the grid2op / gym interface. self._gym_env = GymEnv(env) self._gym_env.observation_space.close() + + obs_attr_to_keep = ["gen_p", "rho"] + if "obs_attr_to_keep" in env_config: + obs_attr_to_keep = copy.deepcopy(env_config["obs_attr_to_keep"]) self._gym_env.observation_space = BoxGymObsSpace(env.observation_space, - attr_to_keep=["gen_p", - "rho"], + attr_to_keep=obs_attr_to_keep, replace_nan_by_0=True # replace Nan by 0. ) # we did not experiment yet with the "partially observable" setting # so for now we suppose all agents see the same observation # which is the full grid - self.observation_space = Box(shape=self._gym_env.observation_space.shape, - high=self._gym_env.observation_space.high, - low=self._gym_env.observation_space.low, - dtype=np.float32 - ) - - # we represent the action as discrete action for now. - # It should work to encode then differently using the - # gym_compat module for example - self._conv_action_space = { - agent_id : DiscreteActSpace(self.ma_env.action_spaces[agent_id]) + self._aux_observation_space = { + agent_id : BoxGymObsSpace(self.ma_env.observation_spaces[agent_id], + attr_to_keep=obs_attr_to_keep, + replace_nan_by_0=True # replace Nan by 0. + ) for agent_id in self.ma_env.agents } - # to avoid "weird" pickle issues - self.action_space = { - agent_id : Discrete(n=self.ma_env.action_spaces[agent_id].n) + self.observation_space = { + agent_id : Box(low=self._aux_observation_space[agent_id].low, + high=self._aux_observation_space[agent_id].high, + dtype=self._aux_observation_space[agent_id].dtype) for agent_id in self.ma_env.agents } - def reset(self): + # we represent the action as discrete action for now. + # It should work to encode then differently using the + # gym_compat module for example + act_type = "discrete" + if "act_type" in env_config: + act_type = env_config["act_type"] + + # for discrete actions + if act_type == "discrete": + self._conv_action_space = { + agent_id : DiscreteActSpace(self.ma_env.action_spaces[agent_id]) + for agent_id in self.ma_env.agents + } + + # to avoid "weird" pickle issues + self.action_space = { + agent_id : Discrete(n=self.ma_env.action_spaces[agent_id].n) + for agent_id in self.ma_env.agents + } + else: + raise NotImplementedError("Make the implementation in this case") + + def reset(self, *, seed=None, options=None): + if seed is not None: + self.seed(seed) + # reset the underlying multi agent environment obs = self.ma_env.reset() - return self._format_obs(obs) + return self._format_obs(obs), {} + def seed(self, seed): + return self.ma_env.seed(seed) def _format_obs(self, grid2op_obs): # NB we heavily use here that all agents see the same things @@ -132,7 +166,9 @@ def step(self, actions): # ignored for now info = {} - return gym_obs, r, done, info + truncateds = {k: False for k in self.ma_env.agents} + truncateds['__all__'] = truncateds[first_agent_id] + return gym_obs, r, done, truncateds, info def policy_mapping_fn(agent_id, episode, worker, **kwargs): @@ -141,7 +177,8 @@ def policy_mapping_fn(agent_id, episode, worker, **kwargs): if __name__ == "__main__": import ray - from ray.rllib.agents.ppo import ppo + # from ray.rllib.agents.ppo import ppo + from ray.rllib.algorithms.ppo import PPO, PPOConfig import json import os import shutil @@ -164,34 +201,55 @@ def policy_mapping_fn(agent_id, episode, worker, **kwargs): SELECT_ENV = MAEnvWrapper # Specifies the OpenAI Gym environment for Cart Pole N_ITER = 1000 # Number of training runs. - config = ppo.DEFAULT_CONFIG.copy() # PPO's default configuration. See the next code cell. - config["log_level"] = "WARN" # Suppress too many messages, but try "INFO" to see what can be printed. - - # Other settings we might adjust: - config["num_workers"] = 1 # Use > 1 for using more CPU cores, including over a cluster - config["num_sgd_iter"] = 10 # Number of SGD (stochastic gradient descent) iterations per training minibatch. - # I.e., for each minibatch of data, do this many passes over it to train. - config["sgd_minibatch_size"] = 64 # The amount of data records per minibatch - config["model"]["fcnet_hiddens"] = [100, 50] # - config["num_cpus_per_worker"] = 0 # This avoids running out of resources in the notebook environment when this cell is re-executed - config["vf_clip_param"] = 100 - - # multi agent specific config - config["multiagent"] = { - "policies" : { - "agent_0" : PolicySpec( - action_space=ray_ma_env.action_space["agent_0"] - ), - "agent_1" : PolicySpec( - action_space=ray_ma_env.action_space["agent_1"] - ) - }, - "policy_mapping_fn": policy_mapping_fn, - "policies_to_train": ["agent_0", "agent_1"], - } + # config = ppo.DEFAULT_CONFIG.copy() # PPO's default configuration. See the next code cell. + # config["log_level"] = "WARN" # Suppress too many messages, but try "INFO" to see what can be printed. + # # Other settings we might adjust: + # config["num_workers"] = 1 # Use > 1 for using more CPU cores, including over a cluster + # config["num_sgd_iter"] = 10 # Number of SGD (stochastic gradient descent) iterations per training minibatch. + # # I.e., for each minibatch of data, do this many passes over it to train. + # config["sgd_minibatch_size"] = 64 # The amount of data records per minibatch + # config["model"]["fcnet_hiddens"] = [100, 50] # + # config["num_cpus_per_worker"] = 0 # This avoids running out of resources in the notebook environment when this cell is re-executed + # config["vf_clip_param"] = 100 + + # # multi agent specific config + # config["multiagent"] = { + # "policies" : { + # "agent_0" : PolicySpec( + # action_space=ray_ma_env.action_space["agent_0"] + # ), + # "agent_1" : PolicySpec( + # action_space=ray_ma_env.action_space["agent_1"] + # ) + # }, + # "policy_mapping_fn": policy_mapping_fn, + # "policies_to_train": ["agent_0", "agent_1"], + # } + + # see ray doc for this... + # syntax changes every ray major version apparently... + config = PPOConfig() + config = config.training(gamma=0.9, lr=0.01, kl_coeff=0.3, train_batch_size=128) + config = config.resources(num_gpus=0) + config = config.rollouts(num_rollout_workers=1) + + # multi agent parts + config.multi_agent(policies={ + "agent_0" : PolicySpec( + action_space=ray_ma_env.action_space["agent_0"], + observation_space=ray_ma_env.observation_space["agent_0"] + ), + "agent_1" : PolicySpec( + action_space=ray_ma_env.action_space["agent_1"], + observation_space=ray_ma_env.observation_space["agent_1"], + ) + }, + policy_mapping_fn = policy_mapping_fn, + policies_to_train= ["agent_0", "agent_1"]) + #Trainer - agent = ppo.PPOTrainer(config, env=SELECT_ENV) + agent = PPO(config=config, env=SELECT_ENV) results = [] episode_data = [] diff --git a/examples/multi_agents/ray_example2.py b/examples/multi_agents/ray_example2.py index 0fbf4126d..2f7a1a608 100644 --- a/examples/multi_agents/ray_example2.py +++ b/examples/multi_agents/ray_example2.py @@ -10,6 +10,7 @@ import warnings import numpy as np +import copy from gym.spaces import Discrete, Box @@ -17,12 +18,12 @@ from ray.rllib.policy.policy import PolicySpec, Policy import grid2op -from grid2op.Action.PlayableAction import PlayableAction +from grid2op.Action import PlayableAction from grid2op.multi_agent.multiAgentEnv import MultiAgentEnv from grid2op.gym_compat import GymEnv, BoxGymObsSpace, DiscreteActSpace from lightsim2grid import LightSimBackend - +from grid2op.gym_compat.utils import ALL_ATTR_FOR_DISCRETE ENV_NAME = "l2rpn_case14_sandbox" DO_NOTHING_EPISODES = -1 # 200 @@ -43,15 +44,22 @@ class MAEnvWrapper(MAEnv): def __init__(self, env_config=None): super().__init__() + if env_config is None: + env_config = {} env = grid2op.make(ENV_NAME, action_class=PlayableAction, backend=LightSimBackend()) - + action_domains = copy.deepcopy(ACTION_DOMAINS) + if "action_domains" in env_config: + action_domains = env_config["action_domains"] + observation_domains = copy.deepcopy(OBSERVATION_DOMAINS) + if "observation_domains" in env_config: + observation_domains = env_config["observation_domains"] self.ma_env = MultiAgentEnv(env, - ACTION_DOMAINS, - OBSERVATION_DOMAINS) + action_domains, + observation_domains) self._agent_ids = set(self.ma_env.agents) self.ma_env.seed(0) @@ -61,9 +69,11 @@ def __init__(self, env_config=None): # with the grid2op / gym interface. self._gym_env = GymEnv(env) self._gym_env.observation_space.close() + obs_attr_to_keep = ["gen_p", "rho"] + if "obs_attr_to_keep" in env_config: + obs_attr_to_keep = copy.deepcopy(env_config["obs_attr_to_keep"]) self._gym_env.observation_space = BoxGymObsSpace(env.observation_space, - attr_to_keep=["gen_p", - "rho"], + attr_to_keep=obs_attr_to_keep, replace_nan_by_0=True # replace Nan by 0. ) @@ -72,7 +82,7 @@ def __init__(self, env_config=None): # which is the full grid self._aux_observation_space = { agent_id : BoxGymObsSpace(self.ma_env.observation_spaces[agent_id], - attr_to_keep=["gen_p", "rho"], + attr_to_keep=obs_attr_to_keep, replace_nan_by_0=True # replace Nan by 0. ) for agent_id in self.ma_env.agents @@ -88,8 +98,12 @@ def __init__(self, env_config=None): # we represent the action as discrete action for now. # It should work to encode then differently using the # gym_compat module for example + act_attr_to_keep = ALL_ATTR_FOR_DISCRETE + if "act_attr_to_keep" in env_config: + act_attr_to_keep = copy.deepcopy(env_config["act_attr_to_keep"]) + self._conv_action_space = { - agent_id : DiscreteActSpace(self.ma_env.action_spaces[agent_id]) + agent_id : DiscreteActSpace(self.ma_env.action_spaces[agent_id], attr_to_keep=act_attr_to_keep) for agent_id in self.ma_env.agents } @@ -99,11 +113,17 @@ def __init__(self, env_config=None): for agent_id in self.ma_env.agents } - def reset(self): + def reset(self, *, seed=None, options=None): + if seed is not None: + self.seed(seed) + # reset the underlying multi agent environment obs = self.ma_env.reset() - return self._format_obs(obs) + return self._format_obs(obs), {} + + def seed(self, seed): + return self.ma_env.seed(seed) def _format_obs(self, grid2op_obs): @@ -144,7 +164,9 @@ def step(self, actions): # ignored for now info = {} - return gym_obs, r, done, info + truncateds = {k: False for k in self.ma_env.agents} + truncateds['__all__'] = truncateds[first_agent_id] + return gym_obs, r, done, truncateds, info def policy_mapping_fn(agent_id, episode, worker, **kwargs): @@ -153,7 +175,8 @@ def policy_mapping_fn(agent_id, episode, worker, **kwargs): if __name__ == "__main__": import ray - from ray.rllib.agents.ppo import ppo + # from ray.rllib.agents.ppo import ppo + from ray.rllib.algorithms.ppo import PPO, PPOConfig import json import os import shutil @@ -172,40 +195,65 @@ def policy_mapping_fn(agent_id, episode, worker, **kwargs): info = ray.init(ignore_reinit_error=True) print("Dashboard URL: http://{}".format(info.address_info["webui_url"])) - #Configs (see ray's doc for more information) + # #Configs (see ray's doc for more information) SELECT_ENV = MAEnvWrapper # Specifies the OpenAI Gym environment for Cart Pole N_ITER = 1000 # Number of training runs. - config = ppo.DEFAULT_CONFIG.copy() # PPO's default configuration. See the next code cell. - config["log_level"] = "WARN" # Suppress too many messages, but try "INFO" to see what can be printed. - - # Other settings we might adjust: - config["num_workers"] = 1 # Use > 1 for using more CPU cores, including over a cluster - config["num_sgd_iter"] = 10 # Number of SGD (stochastic gradient descent) iterations per training minibatch. - # I.e., for each minibatch of data, do this many passes over it to train. - config["sgd_minibatch_size"] = 64 # The amount of data records per minibatch - config["model"]["fcnet_hiddens"] = [100, 50] # - config["num_cpus_per_worker"] = 0 # This avoids running out of resources in the notebook environment when this cell is re-executed - config["vf_clip_param"] = 100 - - # multi agent specific config - config["multiagent"] = { - "policies" : { - "agent_0" : PolicySpec( - action_space=ray_ma_env.action_space["agent_0"], - observation_space=ray_ma_env.observation_space["agent_0"], - ), - "agent_1" : PolicySpec( - action_space=ray_ma_env.action_space["agent_1"], - observation_space=ray_ma_env.observation_space["agent_1"], - ) - }, - "policy_mapping_fn": policy_mapping_fn, - "policies_to_train": ["agent_0", "agent_1"], - } + # config = ppo.DEFAULT_CONFIG.copy() # PPO's default configuration. See the next code cell. + # config["log_level"] = "WARN" # Suppress too many messages, but try "INFO" to see what can be printed. + + # # Other settings we might adjust: + # config["num_workers"] = 1 # Use > 1 for using more CPU cores, including over a cluster + # config["num_sgd_iter"] = 10 # Number of SGD (stochastic gradient descent) iterations per training minibatch. + # # I.e., for each minibatch of data, do this many passes over it to train. + # config["sgd_minibatch_size"] = 64 # The amount of data records per minibatch + # config["model"]["fcnet_hiddens"] = [100, 50] # + # config["num_cpus_per_worker"] = 0 # This avoids running out of resources in the notebook environment when this cell is re-executed + # config["vf_clip_param"] = 100 + + # # multi agent specific config + # config["multiagent"] = { + # "policies" : { + # "agent_0" : PolicySpec( + # action_space=ray_ma_env.action_space["agent_0"], + # observation_space=ray_ma_env.observation_space["agent_0"], + # ), + # "agent_1" : PolicySpec( + # action_space=ray_ma_env.action_space["agent_1"], + # observation_space=ray_ma_env.observation_space["agent_1"], + # ) + # }, + # "policy_mapping_fn": policy_mapping_fn, + # "policies_to_train": ["agent_0", "agent_1"], + # } + # #Trainer + # agent = ppo.PPOTrainer(config, env=SELECT_ENV) + + + # see ray doc for this... + # syntax changes every ray major version apparently... + config = PPOConfig() + config = config.training(gamma=0.9, lr=0.01, kl_coeff=0.3, train_batch_size=128) + config = config.resources(num_gpus=0) + config = config.rollouts(num_rollout_workers=1) + + # multi agent parts + config.multi_agent(policies={ + "agent_0" : PolicySpec( + action_space=ray_ma_env.action_space["agent_0"], + observation_space=ray_ma_env.observation_space["agent_0"] + ), + "agent_1" : PolicySpec( + action_space=ray_ma_env.action_space["agent_1"], + observation_space=ray_ma_env.observation_space["agent_1"], + ) + }, + policy_mapping_fn = policy_mapping_fn, + policies_to_train= ["agent_0", "agent_1"]) + #Trainer - agent = ppo.PPOTrainer(config, env=SELECT_ENV) + agent = PPO(config=config, env=SELECT_ENV) results = [] episode_data = [] diff --git a/grid2op/Backend/backend.py b/grid2op/Backend/backend.py index bf291aaf3..a06fc00b0 100644 --- a/grid2op/Backend/backend.py +++ b/grid2op/Backend/backend.py @@ -1023,10 +1023,12 @@ def next_grid_state(self, ] = True # disconnect the current power lines - if to_disc[lines_status].sum() == 0: - # no powerlines have been disconnected at this time step, i stop the computation there + if to_disc[lines_status].any() == 0: + # no powerlines have been disconnected at this time step, + # i stop the computation there break disconnected_during_cf[to_disc] = ts + # perform the disconnection action for i, el in enumerate(to_disc): if el: @@ -1124,18 +1126,19 @@ def check_kirchoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray p_ex, q_ex, v_ex, *_ = self.lines_ex_info() p_gen, q_gen, v_gen = self.generators_info() p_load, q_load, v_load = self.loads_info() - if self.n_storage > 0: + cls = type(self) + if cls.n_storage > 0: p_storage, q_storage, v_storage = self.storages_info() # fist check the "substation law" : nothing is created at any substation - p_subs = np.zeros(self.n_sub, dtype=dt_float) - q_subs = np.zeros(self.n_sub, dtype=dt_float) + p_subs = np.zeros(cls.n_sub, dtype=dt_float) + q_subs = np.zeros(cls.n_sub, dtype=dt_float) # check for each bus - p_bus = np.zeros((self.n_sub, 2), dtype=dt_float) - q_bus = np.zeros((self.n_sub, 2), dtype=dt_float) + p_bus = np.zeros((cls.n_sub, 2), dtype=dt_float) + q_bus = np.zeros((cls.n_sub, 2), dtype=dt_float) v_bus = ( - np.zeros((self.n_sub, 2, 2), dtype=dt_float) - 1.0 + np.zeros((cls.n_sub, 2, 2), dtype=dt_float) - 1.0 ) # sub, busbar, [min,max] topo_vect = self.get_topo_vect() @@ -1143,11 +1146,15 @@ def check_kirchoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray # for example, if two powerlines are such that line_or_to_subid is equal (eg both connected to substation 0) # then numpy do not guarantee that `p_subs[self.line_or_to_subid] += p_or` will add the two "corresponding p_or" # TODO this can be vectorized with matrix product, see example in obs.flow_bus_matrix (BaseObervation.py) - for i in range(self.n_line): - sub_or_id = self.line_or_to_subid[i] - sub_ex_id = self.line_ex_to_subid[i] - loc_bus_or = topo_vect[self.line_or_pos_topo_vect[i]] - 1 - loc_bus_ex = topo_vect[self.line_ex_pos_topo_vect[i]] - 1 + for i in range(cls.n_line): + sub_or_id = cls.line_or_to_subid[i] + sub_ex_id = cls.line_ex_to_subid[i] + if (topo_vect[cls.line_or_pos_topo_vect[i]] == -1 or + topo_vect[cls.line_ex_pos_topo_vect[i]] == -1): + # line is disconnected + continue + loc_bus_or = topo_vect[cls.line_or_pos_topo_vect[i]] - 1 + loc_bus_ex = topo_vect[cls.line_ex_pos_topo_vect[i]] - 1 # for substations p_subs[sub_or_id] += p_or[i] @@ -1184,92 +1191,104 @@ def check_kirchoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray v_bus[sub_ex_id,loc_bus_ex,][0] = min(v_bus[sub_ex_id,loc_bus_ex,][0],v_ex[i],) v_bus[sub_ex_id,loc_bus_ex,][1] = max(v_bus[sub_ex_id,loc_bus_ex,][1],v_ex[i],) - for i in range(self.n_gen): + for i in range(cls.n_gen): + if topo_vect[cls.gen_pos_topo_vect[i]] == -1: + # gen is disconnected + continue + # for substations - p_subs[self.gen_to_subid[i]] -= p_gen[i] - q_subs[self.gen_to_subid[i]] -= q_gen[i] + p_subs[cls.gen_to_subid[i]] -= p_gen[i] + q_subs[cls.gen_to_subid[i]] -= q_gen[i] # for bus p_bus[ - self.gen_to_subid[i], topo_vect[self.gen_pos_topo_vect[i]] - 1 + cls.gen_to_subid[i], topo_vect[cls.gen_pos_topo_vect[i]] - 1 ] -= p_gen[i] q_bus[ - self.gen_to_subid[i], topo_vect[self.gen_pos_topo_vect[i]] - 1 + cls.gen_to_subid[i], topo_vect[cls.gen_pos_topo_vect[i]] - 1 ] -= q_gen[i] # compute max and min values if v_gen[i]: # but only if gen is connected - v_bus[self.gen_to_subid[i], topo_vect[self.gen_pos_topo_vect[i]] - 1][ + v_bus[cls.gen_to_subid[i], topo_vect[cls.gen_pos_topo_vect[i]] - 1][ 0 ] = min( v_bus[ - self.gen_to_subid[i], topo_vect[self.gen_pos_topo_vect[i]] - 1 + cls.gen_to_subid[i], topo_vect[cls.gen_pos_topo_vect[i]] - 1 ][0], v_gen[i], ) - v_bus[self.gen_to_subid[i], topo_vect[self.gen_pos_topo_vect[i]] - 1][ + v_bus[cls.gen_to_subid[i], topo_vect[cls.gen_pos_topo_vect[i]] - 1][ 1 ] = max( v_bus[ - self.gen_to_subid[i], topo_vect[self.gen_pos_topo_vect[i]] - 1 + cls.gen_to_subid[i], topo_vect[cls.gen_pos_topo_vect[i]] - 1 ][1], v_gen[i], ) - for i in range(self.n_load): + for i in range(cls.n_load): + if topo_vect[cls.load_pos_topo_vect[i]] == -1: + # load is disconnected + continue + # for substations - p_subs[self.load_to_subid[i]] += p_load[i] - q_subs[self.load_to_subid[i]] += q_load[i] + p_subs[cls.load_to_subid[i]] += p_load[i] + q_subs[cls.load_to_subid[i]] += q_load[i] # for buses p_bus[ - self.load_to_subid[i], topo_vect[self.load_pos_topo_vect[i]] - 1 + cls.load_to_subid[i], topo_vect[cls.load_pos_topo_vect[i]] - 1 ] += p_load[i] q_bus[ - self.load_to_subid[i], topo_vect[self.load_pos_topo_vect[i]] - 1 + cls.load_to_subid[i], topo_vect[cls.load_pos_topo_vect[i]] - 1 ] += q_load[i] # compute max and min values if v_load[i]: # but only if load is connected - v_bus[self.load_to_subid[i], topo_vect[self.load_pos_topo_vect[i]] - 1][ + v_bus[cls.load_to_subid[i], topo_vect[cls.load_pos_topo_vect[i]] - 1][ 0 ] = min( v_bus[ - self.load_to_subid[i], topo_vect[self.load_pos_topo_vect[i]] - 1 + cls.load_to_subid[i], topo_vect[cls.load_pos_topo_vect[i]] - 1 ][0], v_load[i], ) - v_bus[self.load_to_subid[i], topo_vect[self.load_pos_topo_vect[i]] - 1][ + v_bus[cls.load_to_subid[i], topo_vect[cls.load_pos_topo_vect[i]] - 1][ 1 ] = max( v_bus[ - self.load_to_subid[i], topo_vect[self.load_pos_topo_vect[i]] - 1 + cls.load_to_subid[i], topo_vect[cls.load_pos_topo_vect[i]] - 1 ][1], v_load[i], ) - for i in range(self.n_storage): - p_subs[self.storage_to_subid[i]] += p_storage[i] - q_subs[self.storage_to_subid[i]] += q_storage[i] + for i in range(cls.n_storage): + if topo_vect[cls.storage_pos_topo_vect[i]] == -1: + # storage is disconnected + continue + + p_subs[cls.storage_to_subid[i]] += p_storage[i] + q_subs[cls.storage_to_subid[i]] += q_storage[i] p_bus[ - self.storage_to_subid[i], topo_vect[self.storage_pos_topo_vect[i]] - 1 + cls.storage_to_subid[i], topo_vect[cls.storage_pos_topo_vect[i]] - 1 ] += p_storage[i] q_bus[ - self.storage_to_subid[i], topo_vect[self.storage_pos_topo_vect[i]] - 1 + cls.storage_to_subid[i], topo_vect[cls.storage_pos_topo_vect[i]] - 1 ] += q_storage[i] # compute max and min values if v_storage[i] > 0: # the storage unit is connected v_bus[ - self.storage_to_subid[i], - topo_vect[self.storage_pos_topo_vect[i]] - 1, + cls.storage_to_subid[i], + topo_vect[cls.storage_pos_topo_vect[i]] - 1, ][0] = min( v_bus[ - self.storage_to_subid[i], - topo_vect[self.storage_pos_topo_vect[i]] - 1, + cls.storage_to_subid[i], + topo_vect[cls.storage_pos_topo_vect[i]] - 1, ][0], v_storage[i], ) @@ -1278,29 +1297,33 @@ def check_kirchoff(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray topo_vect[self.storage_pos_topo_vect[i]] - 1, ][1] = max( v_bus[ - self.storage_to_subid[i], - topo_vect[self.storage_pos_topo_vect[i]] - 1, + cls.storage_to_subid[i], + topo_vect[cls.storage_pos_topo_vect[i]] - 1, ][1], v_storage[i], ) - if type(self).shunts_data_available: + if cls.shunts_data_available: p_s, q_s, v_s, bus_s = self.shunt_info() - for i in range(self.n_shunt): + for i in range(cls.n_shunt): + if bus_s[i] == -1: + # shunt is disconnected + continue + # for substations - p_subs[self.shunt_to_subid[i]] += p_s[i] - q_subs[self.shunt_to_subid[i]] += q_s[i] + p_subs[cls.shunt_to_subid[i]] += p_s[i] + q_subs[cls.shunt_to_subid[i]] += q_s[i] # for buses - p_bus[self.shunt_to_subid[i], bus_s[i] - 1] += p_s[i] - q_bus[self.shunt_to_subid[i], bus_s[i] - 1] += q_s[i] + p_bus[cls.shunt_to_subid[i], bus_s[i] - 1] += p_s[i] + q_bus[cls.shunt_to_subid[i], bus_s[i] - 1] += q_s[i] # compute max and min values - v_bus[self.shunt_to_subid[i], bus_s[i] - 1][0] = min( - v_bus[self.shunt_to_subid[i], bus_s[i] - 1][0], v_s[i] + v_bus[cls.shunt_to_subid[i], bus_s[i] - 1][0] = min( + v_bus[cls.shunt_to_subid[i], bus_s[i] - 1][0], v_s[i] ) - v_bus[self.shunt_to_subid[i], bus_s[i] - 1][1] = max( - v_bus[self.shunt_to_subid[i], bus_s[i] - 1][1], v_s[i] + v_bus[cls.shunt_to_subid[i], bus_s[i] - 1][1] = max( + v_bus[cls.shunt_to_subid[i], bus_s[i] - 1][1], v_s[i] ) else: warnings.warn( diff --git a/grid2op/Environment/__init__.py b/grid2op/Environment/__init__.py index 1375aad0a..a9a4197b3 100644 --- a/grid2op/Environment/__init__.py +++ b/grid2op/Environment/__init__.py @@ -5,7 +5,8 @@ "SingleEnvMultiProcess", "MultiEnvMultiProcess", "MultiMixEnvironment", - "TimedOutEnvironment" + "TimedOutEnvironment", + "MaskedEnvironment" ] from grid2op.Environment.baseEnv import BaseEnv @@ -15,3 +16,4 @@ from grid2op.Environment.multiEnvMultiProcess import MultiEnvMultiProcess from grid2op.Environment.multiMixEnv import MultiMixEnvironment from grid2op.Environment.timedOutEnv import TimedOutEnvironment +from grid2op.Environment.maskedEnvironment import MaskedEnvironment diff --git a/grid2op/Environment/baseEnv.py b/grid2op/Environment/baseEnv.py index 40aaf5252..e0cbeea38 100644 --- a/grid2op/Environment/baseEnv.py +++ b/grid2op/Environment/baseEnv.py @@ -84,6 +84,65 @@ class BaseEnv(GridObjects, RandomObject, ABC): The documentation is showed here to document the common attributes of an "BaseEnvironment". + .. _danger-env-ownership: + + Notes + ------------------------ + + Note en environment data ownership + + .. danger:: + + + A non pythonic decision has been implemented in grid2op for various reasons: an environment + owns everything created from it. + + This means that if you (or the python interpreter) deletes the environment, you might not + use some data generate with this environment. + + More precisely, you cannot do something like: + + .. code-block:: python + + import grid2op + env = grid2op.make("l2rpn_case14_sandbox") + + saved_obs = [] + + obs = env.reset() + saved_obs.append(obs) + obs2, reward, done, info = env.step(env.action_space()) + saved_obs.append(obs2) + + saved_obs[0].simulate(env.action_space()) # works + del env + saved_obs[0].simulate(env.action_space()) # DOES NOT WORK + + It will raise an error like `Grid2OpException EnvError "This environment is closed. You cannot use it anymore."` + + This will also happen if you do things inside functions, for example like this: + + .. code-block:: python + + import grid2op + + def foo(manager): + env = grid2op.make("l2rpn_case14_sandbox") + obs = env.reset() + manager.append(obs) + obs2, reward, done, info = env.step(env.action_space()) + manager.append(obs2) + manager[0].simulate(env.action_space()) # works + return manager + + manager = [] + manager = foo(manager) + manager[0].simulate(env.action_space()) # DOES NOT WORK + + The same error is raised because the environment `env` is automatically deleted by python when the function `foo` ends + (well it might work on some cases, if the function is called before the variable `env` is actually deleted but you + should not rely on this behaviour.) + Attributes ---------- @@ -342,7 +401,7 @@ def __init__( ) self._timestep_overflow: np.ndarray = None self._nb_timestep_overflow_allowed: np.ndarray = None - self._hard_overflow_threshold: float = self._parameters.HARD_OVERFLOW_THRESHOLD + self._hard_overflow_threshold: np.ndarray = None # store actions "cooldown" self._times_before_line_status_actionable: np.ndarray = None @@ -626,7 +685,7 @@ def _custom_deepcopy_for_copy(self, new_obj, dict_=None): new_obj._nb_timestep_overflow_allowed = copy.deepcopy( self._nb_timestep_overflow_allowed ) - new_obj._hard_overflow_threshold = self._hard_overflow_threshold + new_obj._hard_overflow_threshold = copy.deepcopy(self._hard_overflow_threshold) # store actions "cooldown" new_obj._times_before_line_status_actionable = copy.deepcopy( @@ -1204,7 +1263,6 @@ def _has_been_initialized(self): self._gen_downtime = np.zeros(self.n_gen, dtype=dt_int) self._gen_activeprod_t = np.zeros(self.n_gen, dtype=dt_float) self._gen_activeprod_t_redisp = np.zeros(self.n_gen, dtype=dt_float) - self._nb_timestep_overflow_allowed = np.ones(shape=self.n_line, dtype=dt_int) self._max_timestep_line_status_deactivated = ( self._parameters.NB_TIMESTEP_COOLDOWN_LINE ) @@ -1220,6 +1278,11 @@ def _has_been_initialized(self): fill_value=self._parameters.NB_TIMESTEP_OVERFLOW_ALLOWED, dtype=dt_int, ) + self._hard_overflow_threshold = np.full( + shape=(self.n_line,), + fill_value=self._parameters.HARD_OVERFLOW_THRESHOLD, + dtype=dt_float, + ) self._timestep_overflow = np.zeros(shape=(self.n_line,), dtype=dt_int) # update the parameters @@ -1261,7 +1324,6 @@ def _update_parameters(self): # type of power flow to play # if True, then it will not disconnect lines above their thermal limits self._no_overflow_disconnection = self._parameters.NO_OVERFLOW_DISCONNECTION - self._hard_overflow_threshold = self._parameters.HARD_OVERFLOW_THRESHOLD # store actions "cooldown" self._max_timestep_line_status_deactivated = ( @@ -1275,7 +1337,7 @@ def _update_parameters(self): self._nb_timestep_overflow_allowed[ : ] = self._parameters.NB_TIMESTEP_OVERFLOW_ALLOWED - + self._hard_overflow_threshold[:] = self._parameters.HARD_OVERFLOW_THRESHOLD # hard overflow part self._env_dc = self._parameters.ENV_DC @@ -2957,16 +3019,19 @@ def _aux_register_env_converged(self, disc_lines, action, init_line_status, new_ # TODO is non zero and disconnected, this should be ok. self._time_extract_obs += time.perf_counter() - beg_res + def _backend_next_grid_state(self): + """overlaoded in MaskedEnv""" + return self.backend.next_grid_state(env=self, is_dc=self._env_dc) + def _aux_run_pf_after_state_properly_set( self, action, init_line_status, new_p, except_ ): has_error = True + detailed_info = None try: # compute the next _grid state beg_pf = time.perf_counter() - disc_lines, detailed_info, conv_ = self.backend.next_grid_state( - env=self, is_dc=self._env_dc - ) + disc_lines, detailed_info, conv_ = self._backend_next_grid_state() self._disc_lines[:] = disc_lines self._time_powerflow += time.perf_counter() - beg_pf if conv_ is None: @@ -3327,7 +3392,7 @@ def _reset_vectors_and_timings(self): ] = self._parameters.NB_TIMESTEP_OVERFLOW_ALLOWED self.nb_time_step = 0 # to have the first step at 0 - self._hard_overflow_threshold = self._parameters.HARD_OVERFLOW_THRESHOLD + self._hard_overflow_threshold[:] = self._parameters.HARD_OVERFLOW_THRESHOLD self._env_dc = self._parameters.ENV_DC self._times_before_line_status_actionable[:] = 0 diff --git a/grid2op/Environment/environment.py b/grid2op/Environment/environment.py index f7047204a..09df00f97 100644 --- a/grid2op/Environment/environment.py +++ b/grid2op/Environment/environment.py @@ -37,6 +37,14 @@ class Environment(BaseEnv): """ This class is the grid2op implementation of the "Environment" entity in the RL framework. + .. danger:: + + Long story short, once a environment is deleted, you cannot use anything it "holds" including, + but not limited to the capacity to perform `obs.simulate(...)` even if the `obs` is still + referenced. + + See :ref:`danger-env-ownership` (first danger block). + Attributes ---------- @@ -418,7 +426,7 @@ def _init_backend( raise Grid2OpException( "Impossible to initialize the powergrid, the powerflow diverge at iteration 0. " "Available information are: {}".format(info) - ) + ) from info["exception"][0] # test the backend returns object of the proper size if need_process_backend: diff --git a/grid2op/Environment/maskedEnvironment.py b/grid2op/Environment/maskedEnvironment.py new file mode 100644 index 000000000..b97bf986c --- /dev/null +++ b/grid2op/Environment/maskedEnvironment.py @@ -0,0 +1,159 @@ +# Copyright (c) 2023, 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 copy +import numpy as np +from typing import Tuple, Union, List +from grid2op.Environment.environment import Environment +from grid2op.Action import BaseAction +from grid2op.Observation import BaseObservation +from grid2op.Exceptions import EnvError +from grid2op.dtypes import dt_bool, dt_float, dt_int + + +class MaskedEnvironment(Environment): # TODO heritage ou alors on met un truc de base + """This class is the grid2op implementation of a "maked" environment: lines not in the + `lines_of_interest` mask will NOT be deactivated by the environment is the flow is too high + (or moderately high for too long.) + + .. warning:: + This class might not behave normally if used with TimeOutEnvironment, MultiEnv, MultiMixEnv etc. + + .. warning:: + At time of writing, the behaviour of "obs.simulate" is not modified + """ + # some kind of infinity value + # NB we multiply np.finfo(dt_float).max by a small number (1e-7) to avoid overflow + # indeed, _hard_overflow_threshold is multiply by the flow on the lines + INF_VAL_THM_LIM = 1e-7 * np.finfo(dt_float).max + + # some kind of infinity value + INF_VAL_TS_OVERFLOW_ALLOW = np.iinfo(dt_int).max - 1 + + def __init__(self, + grid2op_env: Union[Environment, dict], + lines_of_interest): + + self._lines_of_interest = self._make_lines_of_interest(lines_of_interest) + if isinstance(grid2op_env, Environment): + super().__init__(**grid2op_env.get_kwargs()) + elif isinstance(grid2op_env, dict): + super().__init__(**grid2op_env) + else: + raise EnvError(f"For MaskedEnvironment you need to provide " + f"either an Environment or a dict " + f"for grid2op_env. You provided: {type(grid2op_env)}") + + def _make_lines_of_interest(self, lines_of_interest): + # NB is called BEFORE the env has been created... + if isinstance(lines_of_interest, np.ndarray): + # if lines_of_interest.size() != type(self).n_line: + # raise EnvError("Impossible to init A masked environment when the number of lines " + # "of the mask do not match the number of lines on the grid.") + res = lines_of_interest.astype(dt_bool) + if res.sum() == 0: + raise EnvError("You cannot use MaskedEnvironment and masking all " + "the grid. If you don't want to simulate powerline " + "disconnection when they are game over, please " + "set params.NO_OVERFLOW_DISCONNECT=True (see doc)") + else: + raise EnvError("Format of lines_of_interest is not understood. " + "Please provide a vector of the size of the " + "number of lines on the grid.") + return res + + def _reset_vectors_and_timings(self): + super()._reset_vectors_and_timings() + self._hard_overflow_threshold[~self._lines_of_interest] = type(self).INF_VAL_THM_LIM + self._nb_timestep_overflow_allowed[~self._lines_of_interest] = type(self).INF_VAL_TS_OVERFLOW_ALLOW + + def get_kwargs(self, with_backend=True, with_chronics_handler=True): + res = {} + res["lines_of_interest"] = copy.deepcopy(self._lines_of_interest) + res["grid2op_env"] = super().get_kwargs(with_backend, with_chronics_handler) + return res + + def get_params_for_runner(self): + res = super().get_params_for_runner() + res["envClass"] = MaskedEnvironment + res["other_env_kwargs"] = {"lines_of_interest": copy.deepcopy(self._lines_of_interest)} + return res + + def _custom_deepcopy_for_copy(self, new_obj): + super()._custom_deepcopy_for_copy(new_obj) + new_obj._lines_of_interest = copy.deepcopy(self._lines_of_interest) + + @classmethod + def init_obj_from_kwargs(cls, + other_env_kwargs, + init_env_path, + init_grid_path, + chronics_handler, + backend, + parameters, + name, + names_chronics_to_backend, + actionClass, + observationClass, + rewardClass, + legalActClass, + voltagecontrolerClass, + other_rewards, + opponent_space_type, + opponent_action_class, + opponent_class, + opponent_init_budget, + opponent_budget_per_ts, + opponent_budget_class, + opponent_attack_duration, + opponent_attack_cooldown, + kwargs_opponent, + with_forecast, + attention_budget_cls, + kwargs_attention_budget, + has_attention_budget, + logger, + kwargs_observation, + observation_bk_class, + observation_bk_kwargs, + _raw_backend_class, + _read_from_local_dir): + res = MaskedEnvironment(grid2op_env={"init_env_path": init_env_path, + "init_grid_path": init_grid_path, + "chronics_handler": chronics_handler, + "backend": backend, + "parameters": parameters, + "name": name, + "names_chronics_to_backend": names_chronics_to_backend, + "actionClass": actionClass, + "observationClass": observationClass, + "rewardClass": rewardClass, + "legalActClass": legalActClass, + "voltagecontrolerClass": voltagecontrolerClass, + "other_rewards": other_rewards, + "opponent_space_type": opponent_space_type, + "opponent_action_class": opponent_action_class, + "opponent_class": opponent_class, + "opponent_init_budget": opponent_init_budget, + "opponent_budget_per_ts": opponent_budget_per_ts, + "opponent_budget_class": opponent_budget_class, + "opponent_attack_duration": opponent_attack_duration, + "opponent_attack_cooldown": opponent_attack_cooldown, + "kwargs_opponent": kwargs_opponent, + "with_forecast": with_forecast, + "attention_budget_cls": attention_budget_cls, + "kwargs_attention_budget": kwargs_attention_budget, + "has_attention_budget": has_attention_budget, + "logger": logger, + "kwargs_observation": kwargs_observation, + "observation_bk_class": observation_bk_class, + "observation_bk_kwargs": observation_bk_kwargs, + "_raw_backend_class": _raw_backend_class, + "_read_from_local_dir": _read_from_local_dir}, + **other_env_kwargs) + return res diff --git a/grid2op/Environment/timedOutEnv.py b/grid2op/Environment/timedOutEnv.py index fcccd7641..af5558ebe 100644 --- a/grid2op/Environment/timedOutEnv.py +++ b/grid2op/Environment/timedOutEnv.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2020, RTE (https://www.rte-france.com) +# Copyright (c) 2023, 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, @@ -23,7 +23,10 @@ class TimedOutEnvironment(Environment): # TODO heritage ou alors on met un truc of the `step` function. For more information, see the documentation of - :func:`TimedOutEnvironment.step` for + :func:`TimedOutEnvironment.step` + + .. warning:: + This class might not behave normally if used with MaskedEnvironment, MultiEnv, MultiMixEnv etc. Attributes ---------- diff --git a/grid2op/Episode/EpisodeReplay.py b/grid2op/Episode/EpisodeReplay.py index 6213bf450..b21f21fc7 100644 --- a/grid2op/Episode/EpisodeReplay.py +++ b/grid2op/Episode/EpisodeReplay.py @@ -102,15 +102,15 @@ def replay_episode( load_info: ``str`` Defaults to "p". What kind of values to show on loads. - Can be oneof `["p", "v", None]` + Can be one of `["p", "v", None]` gen_info: ``str`` Defaults to "p". What kind of values to show on generators. - Can be oneof `["p", "v", None]` + Can be one of `["p", "v", None]` line_info: ``str`` Defaults to "rho". What kind of values to show on lines. - Can be oneof `["rho", "a", "p", "v", None]` + Can be one of `["rho", "a", "p", "v", None]` resolution: ``tuple`` Defaults to (1280, 720). The resolution to use for the gif. @@ -187,7 +187,12 @@ def replay_episode( # Export all frames as gif if enabled if gif_name is not None and len(frames) > 0: try: - imageio.mimwrite(gif_path, frames, fps=fps) + try: + # with imageio > 2.5 you need to compute the duration + imageio.mimwrite(gif_path, frames, duration=1000./fps) + except TypeError: + # imageio <= 2.5 can be given fps directly + imageio.mimwrite(gif_path, frames, fps=fps) # Try to compress try: from pygifsicle import optimize diff --git a/grid2op/Observation/baseObservation.py b/grid2op/Observation/baseObservation.py index 7ec8c3a7d..7495e72b3 100644 --- a/grid2op/Observation/baseObservation.py +++ b/grid2op/Observation/baseObservation.py @@ -4214,7 +4214,18 @@ def get_forecast_env(self) -> "grid2op.Environment.Environment": f_obs_3, *_ = forecast_env.step(act_3) sim_obs_3, *_ = sim_obs_2.simulate(act_3) # f_obs_3 should be sim_obs_3 - + + .. danger:: + + Long story short, once a environment (and a forecast_env is one) + is deleted, you cannot use anything it "holds" including, + but not limited to the capacity to perform `obs.simulate(...)` even if the `obs` is still + referenced. + + See :ref:`danger-env-ownership` (first danger block). + + This caused issue https://github.com/rte-france/Grid2Op/issues/568 for example. + Returns ------- grid2op.Environment.Environment @@ -4346,8 +4357,26 @@ def get_env_from_external_forecasts(self, you have 100 rows then you have 100 steps. .. warning:: - We remind that, if you provide some forecasts, it is expected that + We remind that, if you provide some forecasts, it is expected that they allow some powerflow to converge. + The balance between total generation on one side and total demand and losses on the other should also + make "as close as possible" to reduce some modeling artifact (by the backend, grid2op does not check + anything here). + + Finally, make sure that your input data meet the constraints on the generators (pmin, pmax and ramps) + otherwise you might end up with incorrect behaviour. Grid2op supposes that data fed to it + is consistent with its model. If not it's "undefined behaviour". + + .. danger:: + + Long story short, once a environment (and a forecast_env is one) + is deleted, you cannot use anything it "holds" including, + but not limited to the capacity to perform `obs.simulate(...)` even if the `obs` is still + referenced. + + See :ref:`danger-env-ownership` (first danger block). + This caused issue https://github.com/rte-france/Grid2Op/issues/568 for example. + Examples -------- A typical use might look like diff --git a/grid2op/Runner/runner.py b/grid2op/Runner/runner.py index c790b0883..59747a116 100644 --- a/grid2op/Runner/runner.py +++ b/grid2op/Runner/runner.py @@ -1137,6 +1137,7 @@ def run( returned list are not necessarily sorted by this value) - "cum_reward" the cumulative reward obtained by the :attr:`Runner.Agent` on this episode i - "nb_time_step": the number of time steps played in this episode. + - "total_step": the total number of time steps possible in this episode. - "episode_data" : [Optional] The :class:`EpisodeData` corresponding to this episode run only if `add_detailed_output=True` - "add_nb_highres_sim": [Optional] The estimated number of calls to high resolution simulator made diff --git a/grid2op/multi_agent/subgridAction.py b/grid2op/multi_agent/subgridAction.py index 55a54b399..afd6c5d78 100644 --- a/grid2op/multi_agent/subgridAction.py +++ b/grid2op/multi_agent/subgridAction.py @@ -381,16 +381,29 @@ def to_global(self, global_action_space: ActionSpace): "the target action type does not suppor it") if self._modif_interco_set_status: - raise NotImplementedError("What to do if I modified an interco status (set) ?") + # TODO not tested + if global_action_space.supports_type("set_line_status"): + global_action._modif_set_status = True + global_action._set_line_status[my_cls.interco_to_lineid] = self._set_interco_status + else: + warnings.warn("The set_line_status part of this local action has been removed because " + "the target action type does not suppor it") + if self._modif_interco_change_status: - raise NotImplementedError("What to do if I modified an interco status (change) ?") + # TODO not tested + if global_action_space.supports_type("change_line_status"): + global_action._modif_change_status = True + global_action._set_line_status[my_cls.interco_to_lineid] = self._set_interco_status + else: + warnings.warn("The change_line_status part of this local action has been removed because " + "the target action type does not suppor it") + if self._modif_inj: raise NotImplementedError("What to do if I modified an injection ?") if self._modif_alarm: raise NotImplementedError("What to do if I modified an alarm ?") return global_action - def impact_on_objects(self) -> dict: # TODO not tested res = super().impact_on_objects() diff --git a/grid2op/tests/BaseBackendTest.py b/grid2op/tests/BaseBackendTest.py index ad24c2ca6..b8f99b617 100644 --- a/grid2op/tests/BaseBackendTest.py +++ b/grid2op/tests/BaseBackendTest.py @@ -177,8 +177,8 @@ def test_load_file(self): assert np.all(backend.get_topo_vect() == np.ones(np.sum(backend.sub_info))) - conv = backend.runpf() - assert conv, "powerflow diverge it is not supposed to!" + conv, *_ = backend.runpf() + assert conv, f"powerflow diverge it is not supposed to! Error {_}" with warnings.catch_warnings(): warnings.filterwarnings("ignore") @@ -199,8 +199,8 @@ def test_assert_grid_correct(self): backend.load_grid(path_matpower, case_file) type(backend).set_env_name("TestLoadingCase_env2_test_assert_grid_correct") backend.assert_grid_correct() - conv = backend.runpf() - assert conv, "powerflow diverge it is not supposed to!" + conv, *_ = backend.runpf() + assert conv, f"powerflow diverge it is not supposed to! Error {_}" backend.assert_grid_correct_after_powerflow() @@ -262,8 +262,8 @@ def test_theta_ok(self): def test_runpf_dc(self): self.skip_if_needed() - conv = self.backend.runpf(is_dc=True) - assert conv + conv, *_ = self.backend.runpf(is_dc=True) + assert conv, f"powerflow diverge with error {_}" true_values_dc = np.array( [ 147.83859556, @@ -317,7 +317,8 @@ def test_runpf(self): 2.80741759e01, ] ) - conv = self.backend.runpf(is_dc=False) + conv, *_ = self.backend.runpf(is_dc=False) + assert conv, f"powerflow diverge with error {_}" assert conv p_or, *_ = self.backend.lines_or_info() assert self.compare_vect(p_or, true_values_ac) @@ -325,8 +326,8 @@ def test_runpf(self): def test_voltage_convert_powerlines(self): self.skip_if_needed() # i have the correct voltages in powerlines if the formula to link mw, mvar, kv and amps is correct - conv = self.backend.runpf(is_dc=False) - assert conv, "powerflow diverge at loading" + conv, *_ = self.backend.runpf(is_dc=False) + assert conv, f"powerflow diverge at loading with error {_}" p_or, q_or, v_or, a_or = self.backend.lines_or_info() a_th = np.sqrt(p_or**2 + q_or**2) * 1e3 / (np.sqrt(3) * v_or) @@ -341,8 +342,8 @@ def test_voltages_correct_load_gen(self): # i have the right voltages to generators and load, if it's the same as the voltage (correct from the above test) # of the powerline connected to it. - conv = self.backend.runpf(is_dc=False) - assert conv, "powerflow diverge at loading" + conv, *_ = self.backend.runpf(is_dc=False) + assert conv, f"powerflow diverge at loading with error {_}" load_p, load_q, load_v = self.backend.loads_info() gen_p, gen__q, gen_v = self.backend.generators_info() p_or, q_or, v_or, a_or = self.backend.lines_or_info() @@ -384,33 +385,37 @@ def test_voltages_correct_load_gen(self): continue assert False, "generator {} has not been checked".format(g_id) - def test_copy(self): + def test_copy_ac(self, is_dc=False): self.skip_if_needed() - conv = self.backend.runpf(is_dc=False) - assert conv, "powerflow diverge at loading" + conv, *_ = self.backend.runpf(is_dc=is_dc) + assert conv, f"powerflow diverge at loading with error {_}" l_id = 3 p_or_orig, *_ = self.backend.lines_or_info() - adn_backend_cpy = self.backend.copy() + backend_cpy = self.backend.copy() self.backend._disconnect_line(l_id) - conv = self.backend.runpf(is_dc=False) - assert conv - conv2 = adn_backend_cpy.runpf(is_dc=False) - assert conv2 + conv, *_ = self.backend.runpf(is_dc=is_dc) + assert conv, f"original backend diverged with error {_}" + conv2 = backend_cpy.runpf(is_dc=is_dc) + assert conv2, f"copied backend diverged with error {_}" p_or_ref, *_ = self.backend.lines_or_info() - p_or, *_ = adn_backend_cpy.lines_or_info() + p_or, *_ = backend_cpy.lines_or_info() assert self.compare_vect( p_or_orig, p_or ), "the copied object affects its original 'parent'" assert ( np.abs(p_or_ref[l_id]) <= self.tol_one - ), "powerline {} has not been disconnected".format(l_id) + ), "powerline {} has not been disconnected in orig backend".format(l_id) + + def test_copy_dc(self): + self.skip_if_needed() + self.test_copy_ac(True) def test_copy2(self): self.skip_if_needed() self.backend._disconnect_line(8) - conv = self.backend.runpf(is_dc=False) + conv, *_ = self.backend.runpf(is_dc=False) p_or_orig, *_ = self.backend.lines_or_info() adn_backend_cpy = self.backend.copy() @@ -520,12 +525,12 @@ def test_pf_ac_dc(self): 5.77869057, ] ) - conv = self.backend.runpf(is_dc=True) - assert conv + conv, *_ = self.backend.runpf(is_dc=True) + assert conv, f"error {_}" p_or_orig, q_or_orig, *_ = self.backend.lines_or_info() assert np.all(q_or_orig == 0.0), "in dc mode all q must be zero" - conv = self.backend.runpf(is_dc=False) - assert conv + conv, *_ = self.backend.runpf(is_dc=False) + assert conv, f"error {_}" p_or_orig, q_or_orig, *_ = self.backend.lines_or_info() assert self.compare_vect(q_or_orig, true_values_ac) @@ -567,11 +572,11 @@ def test_disconnect_line(self): continue backend_cpy = self.backend.copy() backend_cpy._disconnect_line(i) - conv = backend_cpy.runpf() + conv, *_ = backend_cpy.runpf() assert ( conv - ), "Power flow computation does not converge if line {} is removed".format( - i + ), "Power flow computation does not converge if line {} is removed with error ".format( + i, _ ) flows = backend_cpy.get_line_status() assert not flows[i] @@ -579,7 +584,8 @@ def test_disconnect_line(self): def test_donothing_action(self): self.skip_if_needed() - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() + assert conv, f"error {_}" init_flow = self.backend.get_line_flow() init_lp, *_ = self.backend.loads_info() init_gp, *_ = self.backend.generators_info() @@ -596,8 +602,8 @@ def test_donothing_action(self): # assert self.compare_vect(init_gp, after_gp) # check i didn't modify the generators # TODO here !!! problem with steady state P=C+L assert np.all(init_ls == after_ls) # check i didn't disconnect any powerlines - conv = self.backend.runpf() - assert conv, "Cannot perform a powerflow after doing nothing" + conv, *_ = self.backend.runpf() + assert conv, f"Cannot perform a powerflow after doing nothing with error {_}" after_flow = self.backend.get_line_flow() assert self.compare_vect(init_flow, after_flow) @@ -608,8 +614,8 @@ def test_apply_action_active_value(self): # also multiply by 2 # i set up the stuff to have exactly 0 losses - conv = self.backend.runpf(is_dc=True) - assert conv, "powergrid diverge after loading (even in DC)" + conv, *_ = self.backend.runpf(is_dc=True) + assert conv, f"powergrid diverge after loading (even in DC) with error {_}" init_flow, *_ = self.backend.lines_or_info() init_lp, init_l_q, *_ = self.backend.loads_info() init_gp, *_ = self.backend.generators_info() @@ -623,7 +629,8 @@ def test_apply_action_active_value(self): bk_action = self.bkact_class() bk_action += action self.backend.apply_action(bk_action) - conv = self.backend.runpf(is_dc=True) + conv, *_ = self.backend.runpf(is_dc=True) + assert conv, f"powergrid diverge with error {_}" # now the system has exactly 0 losses (ie sum load = sum gen) # i check that if i divide by 2, then everything is divided by 2 @@ -641,8 +648,8 @@ def test_apply_action_active_value(self): bk_action = self.bkact_class() bk_action += action self.backend.apply_action(bk_action) - conv = self.backend.runpf(is_dc=True) - assert conv, "Cannot perform a powerflow after doing nothing" + conv, *_ = self.backend.runpf(is_dc=True) + assert conv, "Cannot perform a powerflow after doing nothing (dc)" after_lp, after_lq, *_ = self.backend.loads_info() after_gp, *_ = self.backend.generators_info() @@ -656,10 +663,10 @@ def test_apply_action_active_value(self): # i'm in DC mode, i can't check for reactive values... assert ( np.max(np.abs(p_subs)) <= self.tolvect - ), "problem with active values, at substation" + ), "problem with active values, at substation (kirchoff for DC)" assert ( np.max(np.abs(p_bus.flatten())) <= self.tolvect - ), "problem with active values, at a bus" + ), "problem with active values, at a bus (kirchoff for DC)" assert self.compare_vect( new_pp, after_gp @@ -673,8 +680,8 @@ def test_apply_action_active_value(self): def test_apply_action_prod_v(self): self.skip_if_needed() - conv = self.backend.runpf(is_dc=False) - assert conv, "powergrid diverge after loading" + conv, *_ = self.backend.runpf(is_dc=False) + assert conv, f"powergrid diverge after loading with error {_}" prod_p_init, prod_q_init, prod_v_init = self.backend.generators_info() ratio = 1.05 action = self.action_env( @@ -683,8 +690,8 @@ def test_apply_action_prod_v(self): bk_action = self.bkact_class() bk_action += action self.backend.apply_action(bk_action) - conv = self.backend.runpf(is_dc=False) - assert conv, "Cannot perform a powerflow after modifying the powergrid" + conv, *_ = self.backend.runpf(is_dc=False) + assert conv, f"Cannot perform a powerflow after modifying the powergrid with error {_}" prod_p_after, prod_q_after, prod_v_after = self.backend.generators_info() assert self.compare_vect( @@ -694,7 +701,8 @@ def test_apply_action_prod_v(self): def test_apply_action_maintenance(self): self.skip_if_needed() # retrieve some initial data to be sure only a subpart of the _grid is modified - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() + assert conv, f"powerflow diverge with , error: {_}" init_lp, *_ = self.backend.loads_info() init_gp, *_ = self.backend.generators_info() @@ -709,8 +717,8 @@ def test_apply_action_maintenance(self): self.backend.apply_action(bk_action) # compute a load flow an performs more tests - conv = self.backend.runpf() - assert conv, "Power does not converge if line {} is removed".format(19) + conv, *_ = self.backend.runpf() + assert conv, "Power does not converge if line {} is removed with error {}".format(19, _) # performs basic check after_lp, *_ = self.backend.loads_info() @@ -728,8 +736,8 @@ def test_apply_action_maintenance(self): def test_apply_action_hazard(self): self.skip_if_needed() - conv = self.backend.runpf() - assert conv, "powerflow did not converge at iteration 0" + conv, *_ = self.backend.runpf() + assert conv, f"powerflow did not converge at iteration 0, with error {_}" init_lp, *_ = self.backend.loads_info() init_gp, *_ = self.backend.generators_info() @@ -743,8 +751,8 @@ def test_apply_action_hazard(self): self.backend.apply_action(bk_action) # compute a load flow an performs more tests - conv = self.backend.runpf() - assert conv, "Power does not converge if line {} is removed".format(19) + conv, *_ = self.backend.runpf() + assert conv, "Power does not converge if line {} is removed with error {}".format(19, _) # performs basic check after_lp, *_ = self.backend.loads_info() @@ -759,7 +767,8 @@ def test_apply_action_hazard(self): def test_apply_action_disconnection(self): self.skip_if_needed() # retrieve some initial data to be sure only a subpart of the _grid is modified - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() + assert conv, f"powerflow diverge with , error: {_}" init_lp, *_ = self.backend.loads_info() init_gp, *_ = self.backend.generators_info() @@ -779,10 +788,10 @@ def test_apply_action_disconnection(self): self.backend.apply_action(bk_action) # compute a load flow an performs more tests - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() assert ( conv - ), "Powerflow does not converge if lines {} and {} are removed".format(17, 19) + ), "Powerflow does not converge if lines {} and {} are removed with error {}".format(17, 19, _) # performs basic check after_lp, *_ = self.backend.loads_info() @@ -858,7 +867,8 @@ def _check_kirchoff(self): def test_get_topo_vect_speed(self): # retrieve some initial data to be sure only a subpart of the _grid is modified self.skip_if_needed() - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() + assert conv, f"powerflow diverge with , error: {_}" init_amps_flow = self.backend.get_line_flow() # check that maintenance vector is properly taken into account @@ -869,8 +879,8 @@ def test_get_topo_vect_speed(self): bk_action += action # apply the action here self.backend.apply_action(bk_action) - conv = self.backend.runpf() - assert conv + conv, *_ = self.backend.runpf() + assert conv, f"powerflow diverge with , error: {_}" after_amps_flow = self.backend.get_line_flow() topo_vect = self.backend.get_topo_vect() @@ -940,7 +950,8 @@ def test_get_topo_vect_speed(self): def test_topo_set1sub(self): # retrieve some initial data to be sure only a subpart of the _grid is modified self.skip_if_needed() - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() + assert conv, f"powerflow diverge with , error: {_}" init_amps_flow = self.backend.get_line_flow() # check that maintenance vector is properly taken into account @@ -952,8 +963,8 @@ def test_topo_set1sub(self): # apply the action here self.backend.apply_action(bk_action) - conv = self.backend.runpf() - assert conv + conv, *_ = self.backend.runpf() + assert conv, f"powerflow diverge with , error: {_}" after_amps_flow = self.backend.get_line_flow() topo_vect = self.backend.get_topo_vect() @@ -1037,7 +1048,8 @@ def test_topo_set1sub(self): def test_topo_change1sub(self): # check that switching the bus of 3 object is equivalent to set them to bus 2 (as above) self.skip_if_needed() - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() + assert conv, f"powerflow diverge with , error: {_}" init_amps_flow = self.backend.get_line_flow() # check that maintenance vector is properly taken into account @@ -1050,8 +1062,8 @@ def test_topo_change1sub(self): self.backend.apply_action(bk_action) # run the powerflow - conv = self.backend.runpf() - assert conv + conv, *_ = self.backend.runpf() + assert conv, f"powerflow diverge with , error: {_}" after_amps_flow = self.backend.get_line_flow() topo_vect = self.backend.get_topo_vect() @@ -1111,7 +1123,8 @@ def test_topo_change_1sub_twice(self): # check that switching the bus of 3 object is equivalent to set them to bus 2 (as above) # and that setting it again is equivalent to doing nothing self.skip_if_needed() - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() + assert conv, f"powerflow diverge with , error: {_}" init_amps_flow = copy.deepcopy(self.backend.get_line_flow()) # check that maintenance vector is properly taken into account @@ -1123,9 +1136,9 @@ def test_topo_change_1sub_twice(self): # apply the action here self.backend.apply_action(bk_action) - conv = self.backend.runpf() + conv, *_ = self.backend.runpf() bk_action.reset() - assert conv + assert conv, f"powerflow diverge with , error: {_}" after_amps_flow = self.backend.get_line_flow() topo_vect = self.backend.get_topo_vect() @@ -1186,8 +1199,8 @@ def test_topo_change_1sub_twice(self): # apply the action here self.backend.apply_action(bk_action) - conv = self.backend.runpf() - assert conv + conv, *_ = self.backend.runpf() + assert conv, f"powerflow diverge with error: {_}" after_amps_flow = self.backend.get_line_flow() assert self.compare_vect(after_amps_flow, init_amps_flow) @@ -1214,8 +1227,8 @@ def test_topo_change_2sub(self): # apply the action here self.backend.apply_action(bk_action) - conv = self.backend.runpf() - assert conv, "powerflow diverge it should not" + conv, *_ = self.backend.runpf() + assert conv, f"powerflow diverge it should not, error: {_}" # check the _grid is correct topo_vect = self.backend.get_topo_vect() @@ -1684,8 +1697,8 @@ def test_next_grid_state_1overflow_envNoCF(self): self.backend.load_grid(self.path_matpower, case_file) type(self.backend).set_no_storage() self.backend.assert_grid_correct() - conv = self.backend.runpf() - assert conv, "powerflow should converge at loading" + conv, *_ = self.backend.runpf() + assert conv, f"powerflow should converge at loading, error: {_}" lines_flows_init = self.backend.get_line_flow() thermal_limit = 10 * lines_flows_init thermal_limit[self.id_first_line_disco] = ( @@ -1728,8 +1741,8 @@ def test_nb_timestep_overflow_disc0(self): self.backend.load_grid(self.path_matpower, case_file) type(self.backend).set_no_storage() self.backend.assert_grid_correct() - conv = self.backend.runpf() - assert conv, "powerflow should converge at loading" + conv, *_ = self.backend.runpf() + assert conv, f"powerflow should converge at loading, error: {_}" lines_flows_init = self.backend.get_line_flow() thermal_limit = 10 * lines_flows_init @@ -2728,7 +2741,7 @@ def test_issue_134(self): } ) obs, reward, done, info = env.step(action) - assert not done + assert not done, f"Episode should not have ended here, error : {info['exception']}" assert obs.line_status[LINE_ID] == False assert obs.topo_vect[obs.line_or_pos_topo_vect[LINE_ID]] == -1 assert obs.topo_vect[obs.line_ex_pos_topo_vect[LINE_ID]] == -1 diff --git a/grid2op/tests/aaa_test_backend_interface.py b/grid2op/tests/aaa_test_backend_interface.py index e45361b04..9abf19761 100644 --- a/grid2op/tests/aaa_test_backend_interface.py +++ b/grid2op/tests/aaa_test_backend_interface.py @@ -405,7 +405,7 @@ def test_11_modify_load_pf_getter(self): backend.apply_action(bk_act) # modification of load_p, load_q and gen_p res2 = backend.runpf(is_dc=False) - assert res2[0], "backend should not have diverge after such a little perturbation" + assert res2[0], f"backend should not have diverged after such a little perturbation. It diverges with error {res2[1]}" tmp2 = backend.loads_info() assert len(tmp) == 3, "loads_info() should return 3 elements: load_p, load_q, load_v (see doc)" load_p_after, load_q_after, load_v_after = tmp2 @@ -428,7 +428,8 @@ def test_11_modify_load_pf_getter(self): bk_act += action backend.apply_action(bk_act) # modification of load_p, load_q and gen_p res_tmp = backend.runpf(is_dc=False) - assert res_tmp[0], "backend should not have diverge after such a little perturbation" + assert res_tmp[0], (f"backend should not have diverged after such a little perturbation. " + f"It diverges with error {res_tmp[1]} for load {load_id}") tmp = backend.loads_info() assert np.abs(tmp[0][load_id] - load_p_init[load_id]) >= delta_mw / 2., f"error when trying to modify load {load_id}: check the consistency between backend.loads_info() and backend.apply_action for load_p" assert np.abs(tmp[1][load_id] - load_q_init[load_id]) >= delta_mvar / 2., f"error when trying to modify load {load_id}: check the consistency between backend.loads_info() and backend.apply_action for load_q" @@ -463,12 +464,16 @@ def test_12_modify_gen_pf_getter(self): backend.apply_action(bk_act) # modification of load_p, load_q and gen_p res2 = backend.runpf(is_dc=False) - assert res2[0], "backend should not have diverge after such a little perturbation" + assert res2[0], f"backend should not have diverged after such a little perturbation. It diverges with error {res2[1]}" tmp2 = backend.generators_info() assert len(tmp) == 3, "generators_info() should return 3 elements: gen_p, gen_q, gen_v (see doc)" gen_p_after, gen_q_after, gen_v_after = tmp2 - assert not np.allclose(gen_p_after, gen_p_init), f"gen_p does not seemed to be modified by apply_action when generators are impacted (active value): check `apply_action` for gen_p / prod_p" - assert not np.allclose(gen_v_after, gen_v_init), f"gen_v does not seemed to be modified by apply_action when generators are impacted (voltage setpoint value): check `apply_action` for gen_v / prod_v" + assert not np.allclose(gen_p_after, gen_p_init), (f"gen_p does not seemed to be modified by apply_action when " + "generators are impacted (active value): check `apply_action` " + "for gen_p / prod_p") + assert not np.allclose(gen_v_after, gen_v_init), (f"gen_v does not seemed to be modified by apply_action when " + "generators are impacted (voltage setpoint value): check `apply_action` " + "for gen_v / prod_v") # now a basic check for "one gen at a time" # NB this test cannot be done like this for "prod_v" / gen_v because two generators might be connected to the same @@ -486,7 +491,8 @@ def test_12_modify_gen_pf_getter(self): bk_act += action backend.apply_action(bk_act) res_tmp = backend.runpf(is_dc=False) - assert res_tmp[0], "backend should not have diverge after such a little perturbation" + assert res_tmp[0], (f"backend should not have diverged after such a little " + f"perturbation. It diverges with error {res_tmp[1]} for gen {gen_id}") tmp = backend.generators_info() if np.abs(tmp[0][gen_id] - gen_p_init[gen_id]) <= delta_mw / 2.: # in case of non distributed slack, backend cannot control the generator acting as the slack. @@ -541,7 +547,8 @@ def test_13_disco_reco_lines_pf_getter(self): bk_act += action1 backend.apply_action(bk_act) # disconnection of line 0 only res_disco = backend.runpf(is_dc=False) - assert res_disco[0], f"your backend diverge after disconnection of line {line_id}, which should not be the case" + # backend._grid.tell_solver_need_reset() + assert res_disco[0], f"your backend diverges after disconnection of line {line_id}, which should not be the case" tmp_or_disco = backend.lines_or_info() tmp_ex_disco = backend.lines_ex_info() assert not np.allclose(tmp_or_disco[0], p_or), f"p_or does not seemed to be modified by apply_action when a powerline is disconnected (active value): check `apply_action` for line connection disconnection" @@ -565,7 +572,7 @@ def test_13_disco_reco_lines_pf_getter(self): bk_act += action2 backend.apply_action(bk_act) # disconnection of line 0 only res_disco = backend.runpf(is_dc=False) - assert res_disco[0], f"your backend diverge after disconnection of line {line_id}, which should not be the case" + assert res_disco[0], f"your backend diverges after disconnection of line {line_id}, which should not be the case" tmp_or_reco = backend.lines_or_info() tmp_ex_reco = backend.lines_ex_info() assert not np.allclose(tmp_or_disco[0], tmp_or_reco[0]), f"p_or does not seemed to be modified by apply_action when a powerline is reconnected (active value): check `apply_action` for line connection reconnection" @@ -648,7 +655,8 @@ def test_14change_topology(self): bk_act += action1 backend.apply_action(bk_act) # everything on busbar 2 at sub 0 res = backend.runpf(is_dc=False) - assert res[0], "Your powerflow has diverged after the loading of the file, which should not happen" + assert res[0], (f"Your powerflow has diverged after a topological change at substation {sub_id} with error {res[1]}." + f"\nCheck `apply_action` for topology.") if not cls.shunts_data_available: warnings.warn(f"{type(self).__name__} test_14change_topology: This test is not performed in depth as your backend does not support shunts") @@ -1080,7 +1088,7 @@ def test_22_islanded_grid_stops_computation(self): bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 res = backend.runpf(is_dc=False) - assert not res[0], "It is expected that your backend return `False` in case of non connected grid in AC." + assert not res[0], f"It is expected that your backend return `(False, _)` in case of non connected grid in AC." error = res[1] assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" if not isinstance(error, BackendError): @@ -1096,7 +1104,7 @@ def test_22_islanded_grid_stops_computation(self): bk_act += action backend.apply_action(bk_act) # mix of bus 1 and 2 on substation 1 res = backend.runpf(is_dc=True) - assert not res[0], "It is expected that your backend throws an exception inheriting from BackendError in case of non connected grid in DC." + assert not res[0], f"It is expected that your backend return `(False, _)` in case of non connected grid in DC." error = res[1] assert isinstance(error, Grid2OpException), f"When your backend return `False`, we expect it throws an exception inheriting from Grid2OpException (second return value), backend returned {type(error)}" if not isinstance(error, BackendError): @@ -1125,6 +1133,7 @@ def test_23_disco_line_v_null(self): backend.apply_action(bk_act) res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after a line disconnection, error was {res[1]}" p_or, q_or, v_or, a_or = backend.lines_or_info() p_ex, q_ex, v_ex, a_ex = backend.lines_ex_info() assert np.allclose(v_or[line_id], 0.), f"v_or should be 0. for disconnected line, but is currently {v_or[line_id]} (AC)" @@ -1141,6 +1150,7 @@ def test_23_disco_line_v_null(self): backend.apply_action(bk_act) res = backend.runpf(is_dc=True) + assert res[0], f"Your backend diverged in DC after a line disconnection, error was {res[1]}" p_or, q_or, v_or, a_or = backend.lines_or_info() p_ex, q_ex, v_ex, a_ex = backend.lines_ex_info() assert np.allclose(v_or[line_id], 0.), f"v_or should be 0. for disconnected line, but is currently {v_or[line_id]} (DC)" @@ -1177,6 +1187,7 @@ def test_24_disco_shunt_v_null(self): bk_act += action backend.apply_action(bk_act) res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after a shunt disconnection, error was {res[1]}" p_, q_, v_, bus_ = backend.shunt_info() assert np.allclose(v_[shunt_id], 0.), f"v should be 0. for disconnected shunt, but is currently {v_[shunt_id]} (AC)" assert bus_[shunt_id] == -1, f"bus_ should be -1 for disconnected shunt, but is currently {bus_[shunt_id]} (AC)" @@ -1189,6 +1200,7 @@ def test_24_disco_shunt_v_null(self): bk_act += action backend.apply_action(bk_act) res = backend.runpf(is_dc=True) + assert res[0], f"Your backend diverged in DC after a shunt disconnection, error was {res[1]}" p_, q_, v_, bus_ = backend.shunt_info() assert np.allclose(v_[shunt_id], 0.), f"v should be 0. for disconnected shunt, but is currently {v_[shunt_id]} (DC)" assert bus_[shunt_id] == -1, f"bus_ should be -1 for disconnected shunt, but is currently {bus_[shunt_id]} (DC)" @@ -1221,6 +1233,7 @@ def test_25_disco_storage_v_null(self): backend.apply_action(bk_act) res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after a storage disconnection, error was {res[1]}" p_, q_, v_ = backend.storages_info() assert np.allclose(v_[storage_id], 0.), f"v should be 0. for disconnected storage, but is currently {v_[storage_id]} (AC)" @@ -1232,6 +1245,7 @@ def test_25_disco_storage_v_null(self): bk_act += action backend.apply_action(bk_act) res = backend.runpf(is_dc=True) + assert res[0], f"Your backend diverged in DC after a storage disconnection, error was {res[1]}" p_, q_, v_ = backend.storages_info() assert np.allclose(v_[storage_id], 0.), f"v should be 0. for disconnected storage, but is currently {v_[storage_id]} (AC)" @@ -1261,7 +1275,8 @@ def test_26_copy(self): # backend can be copied backend_cpy = backend.copy() assert isinstance(backend_cpy, type(backend)), f"backend.copy() is supposed to return an object of the same type as your backend. Check backend.copy()" - backend.runpf(is_dc=False) + res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after a copy, error was {res[1]}" # now modify original one init_gen_p, *_ = backend.generators_info() init_load_p, *_ = backend.loads_info() @@ -1274,6 +1289,7 @@ def test_26_copy(self): backend.apply_action(bk_act) res = backend.runpf(is_dc=True) res_cpy = backend_cpy.runpf(is_dc=True) + assert res_cpy[0], f"Your backend diverged in DC after a copy, error was {res_cpy[1]}" p_or, *_ = backend.lines_or_info() p_or_cpy, *_ = backend_cpy.lines_or_info() @@ -1302,6 +1318,7 @@ def test_27_topo_vect_disconnect(self): cls = type(backend) res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after loading, error was {res[1]}" topo_vect_orig = self._aux_check_topo_vect(backend) # disconnect line @@ -1313,6 +1330,7 @@ def test_27_topo_vect_disconnect(self): bk_act += action backend.apply_action(bk_act) res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after a line disconnection, error was {res[1]}" topo_vect = self._aux_check_topo_vect(backend) error_msg = (f"Line {line_id} has been disconnected, yet according to 'topo_vect' " f"is still connected (origin side) to busbar {topo_vect[cls.line_or_pos_topo_vect[line_id]]}") @@ -1331,6 +1349,7 @@ def test_27_topo_vect_disconnect(self): bk_act += action backend.apply_action(bk_act) res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after a storage disconnection, error was {res[1]}" topo_vect = self._aux_check_topo_vect(backend) error_msg = (f"Storage {sto_id} has been disconnected, yet according to 'topo_vect' " f"is still connected (origin side) to busbar {topo_vect[cls.storage_pos_topo_vect[line_id]]}") @@ -1353,6 +1372,7 @@ def test_27_topo_vect_disconnect(self): bk_act += action backend.apply_action(bk_act) res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after a shunt disconnection, error was {res[1]}" topo_vect = self._aux_check_topo_vect(backend) error_msg = (f"Disconnecting a shunt should have no impact on the topo_vect vector " f"as shunt are not taken into account in this") @@ -1439,6 +1459,7 @@ def _aux_check_el_generic(self, backend, busbar_id, bk_act += action backend.apply_action(bk_act) # apply the action res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after setting a {el_nm} on busbar {busbar_id}, error was {res[1]}" # now check the topology vector topo_vect = self._aux_check_topo_vect(backend) error_msg = (f"{el_nm} {el_id} has been moved to busbar {busbar_id}, yet according to 'topo_vect' " @@ -1464,6 +1485,7 @@ def test_28_topo_vect_set(self): cls = type(backend) res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after loading the grid state, error was {res[1]}" topo_vect_orig = self._aux_check_topo_vect(backend) # line or @@ -1476,6 +1498,7 @@ def test_28_topo_vect_set(self): bk_act += action backend.apply_action(bk_act) res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after setting a line (or side) on busbar 2, error was {res[1]}" topo_vect = self._aux_check_topo_vect(backend) error_msg = (f"Line {line_id} (or. side) has been moved to busbar {busbar_id}, yet according to 'topo_vect' " f"is still connected (origin side) to busbar {topo_vect[cls.line_or_pos_topo_vect[line_id]]}") @@ -1491,6 +1514,7 @@ def test_28_topo_vect_set(self): bk_act += action backend.apply_action(bk_act) res = backend.runpf(is_dc=False) + assert res[0], f"Your backend diverged in AC after setting a line (ex side) on busbar 2, error was {res[1]}" topo_vect = self._aux_check_topo_vect(backend) error_msg = (f"Line {line_id} (ex. side) has been moved to busbar {busbar_id}, yet according to 'topo_vect' " f"is still connected (ext side) to busbar {topo_vect[cls.line_ex_pos_topo_vect[line_id]]}") diff --git a/grid2op/tests/test_Environment.py b/grid2op/tests/test_Environment.py index ac1e96df5..055f3e865 100644 --- a/grid2op/tests/test_Environment.py +++ b/grid2op/tests/test_Environment.py @@ -845,7 +845,7 @@ def _check_env_param(self, env, param): # type of power flow to play # if True, then it will not disconnect lines above their thermal limits assert env._no_overflow_disconnection == param.NO_OVERFLOW_DISCONNECTION - assert env._hard_overflow_threshold == param.HARD_OVERFLOW_THRESHOLD + assert (env._hard_overflow_threshold == param.HARD_OVERFLOW_THRESHOLD).all() # store actions "cooldown" assert ( diff --git a/grid2op/tests/test_MaskedEnvironment.py b/grid2op/tests/test_MaskedEnvironment.py new file mode 100644 index 000000000..41ed76110 --- /dev/null +++ b/grid2op/tests/test_MaskedEnvironment.py @@ -0,0 +1,230 @@ +# Copyright (c) 2019-2023, 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 unittest +import numpy as np + +import grid2op +from grid2op.Environment import MaskedEnvironment +from grid2op.Runner import Runner +from grid2op.gym_compat import (GymEnv, + BoxGymActSpace, + BoxGymObsSpace, + DiscreteActSpace, + MultiDiscreteActSpace) + + +class TestMaskedEnvironment(unittest.TestCase): + @staticmethod + def get_mask(): + mask = np.full(20, fill_value=False, dtype=bool) + mask[[0, 1, 4, 2, 3, 6, 5]] = True # THT part + return mask + + def setUp(self) -> None: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + self.env_in = MaskedEnvironment(grid2op.make("l2rpn_case14_sandbox", test=True, _add_to_name=type(self).__name__), + lines_of_interest=TestMaskedEnvironment.get_mask()) + self.env_out = MaskedEnvironment(grid2op.make("l2rpn_case14_sandbox", test=True, _add_to_name=type(self).__name__), + lines_of_interest=~TestMaskedEnvironment.get_mask()) + self.line_id = 3 + th_lim = self.env_in.get_thermal_limit() * 2. # avoid all problem in general + th_lim[self.line_id] /= 10. # make sure to get trouble in line 3 + # env_in: line is int the area + self.env_in.set_thermal_limit(th_lim) + # env_out: line is out of the area + self.env_out.set_thermal_limit(th_lim) + + TestMaskedEnvironment._init_env(self.env_in) + TestMaskedEnvironment._init_env(self.env_out) + + @staticmethod + def _init_env(env): + env.set_id(0) + env.seed(0) + env.reset() + + def tearDown(self) -> None: + self.env_in.close() + self.env_out.close() + return super().tearDown() + + def test_right_type(self): + assert isinstance(self.env_in, MaskedEnvironment) + assert isinstance(self.env_out, MaskedEnvironment) + assert hasattr(self.env_in, "_lines_of_interest") + assert hasattr(self.env_out, "_lines_of_interest") + assert self.env_in._lines_of_interest[self.line_id], "line_id should be in env_in" + assert not self.env_out._lines_of_interest[self.line_id], "line_id should not be in env_out" + + def test_ok(self): + act = self.env_in.action_space() + for i in range(10): + obs_in, reward, done, info = self.env_in.step(act) + obs_out, reward, done, info = self.env_out.step(act) + if i < 2: # 2 : 2 full steps already + assert obs_in.timestep_overflow[self.line_id] == i + 1, f"error for step {i}: {obs_in.timestep_overflow[self.line_id]}" + assert obs_out.timestep_overflow[self.line_id] == i + 1, f"error for step {i}: {obs_out.timestep_overflow[self.line_id]}" + else: + # cooldown applied for line 3: + # - it disconnect stuff in `self.env_in` + # - it does not affect anything in `self.env_out` + assert not obs_in.line_status[self.line_id] + assert obs_out.timestep_overflow[self.line_id] == i + 1, f"error for step {i}: {obs_out.timestep_overflow[self.line_id]}" + + def test_reset(self): + # timestep_overflow should be 0 initially even if the flow is too high + obs = self.env_in.reset() + assert obs.timestep_overflow[self.line_id] == 0 + assert obs.rho[self.line_id] > 1. + + +class TestMaskedEnvironmentCpy(TestMaskedEnvironment): + def setUp(self) -> None: + super().setUp() + init_int = self.env_in + init_out = self.env_out + self.env_in = self.env_in.copy() + self.env_out = self.env_out.copy() + init_int.close() + init_out.close() + + +class TestMaskedEnvironmentRunner(unittest.TestCase): + def setUp(self) -> None: + TestMaskedEnvironment.setUp(self) + self.max_iter = 10 + + def tearDown(self) -> None: + self.env_in.close() + self.env_out.close() + return super().tearDown() + + def test_runner_can_make(self): + runner = Runner(**self.env_in.get_params_for_runner()) + env2 = runner.init_env() + assert isinstance(env2, MaskedEnvironment) + assert (env2._lines_of_interest == self.env_in._lines_of_interest).all() + + def test_runner(self): + # create the runner + runner_in = Runner(**self.env_in.get_params_for_runner()) + runner_out = Runner(**self.env_out.get_params_for_runner()) + res_in, *_ = runner_in.run(nb_episode=1, max_iter=self.max_iter, env_seeds=[0], episode_id=[0], add_detailed_output=True) + res_out, *_ = runner_out.run(nb_episode=1, max_iter=self.max_iter, env_seeds=[0], episode_id=[0], add_detailed_output=True) + res_in2, *_ = runner_in.run(nb_episode=1, max_iter=self.max_iter, env_seeds=[0], episode_id=[0]) + # check correct results are obtained when agregated + assert res_in[3] == 10 + assert res_in2[3] == 10 + assert res_out[3] == 10 + assert np.allclose(res_in[2], 645.4992065) + assert np.allclose(res_in2[2], 645.4992065) + assert np.allclose(res_out[2], 645.7020874) + + # check detailed results + ep_data_in = res_in[-1] + ep_data_out = res_out[-1] + for i in range(self.max_iter + 1): + obs_in = ep_data_in.observations[i] + obs_out = ep_data_out.observations[i] + if i < 3: + assert obs_in.timestep_overflow[self.line_id] == i, f"error for step {i}: {obs_in.timestep_overflow[self.line_id]}" + assert obs_out.timestep_overflow[self.line_id] == i, f"error for step {i}: {obs_out.timestep_overflow[self.line_id]}" + else: + # cooldown applied for line 3: + # - it disconnect stuff in `self.env_in` + # - it does not affect anything in `self.env_out` + assert not obs_in.line_status[self.line_id], f"error for step {i}: line is not disconnected" + assert obs_out.timestep_overflow[self.line_id] == i, f"error for step {i}: {obs_out.timestep_overflow[self.line_id]}" + + + +class TestMaskedEnvironmentGym(unittest.TestCase): + def setUp(self) -> None: + TestMaskedEnvironment.setUp(self) + + def tearDown(self) -> None: + self.env_in.close() + self.env_out.close() + return super().tearDown() + + def _aux_run_envs(self, act, env_gym_in, env_gym_out): + for i in range(10): + obs_in, reward, done, truncated, info = env_gym_in.step(act) + obs_out, reward, done, truncated, info = env_gym_out.step(act) + if i < 2: # 2 : 2 full steps already + assert obs_in["timestep_overflow"][self.line_id] == i + 1, f"error for step {i}: {obs_in['timestep_overflow'][self.line_id]}" + assert obs_out['timestep_overflow'][self.line_id] == i + 1, f"error for step {i}: {obs_out['timestep_overflow'][self.line_id]}" + else: + # cooldown applied for line 3: + # - it disconnect stuff in `self.env_in` + # - it does not affect anything in `self.env_out` + assert not obs_in["line_status"][self.line_id] + assert obs_out["timestep_overflow"][self.line_id] == i + 1, f"error for step {i}: {obs_out['timestep_overflow'][self.line_id]}" + + def test_gym_with_step(self): + """test the step function also disconnects (or not) the lines""" + env_gym_in = GymEnv(self.env_in) + env_gym_out = GymEnv(self.env_out) + act = {} + self._aux_run_envs(act, env_gym_in, env_gym_out) + env_gym_in.reset() + env_gym_out.reset() + self._aux_run_envs(act, env_gym_in, env_gym_out) + + def test_gym_normal(self): + """test I can create the gym env""" + env_gym = GymEnv(self.env_in) + env_gym.reset() + + def test_gym_box(self): + """test I can create the gym env with box ob space and act space""" + env_gym_in = GymEnv(self.env_in) + env_gym_out = GymEnv(self.env_out) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env_gym_in.action_space = BoxGymActSpace(self.env_in.action_space) + env_gym_in.observation_space = BoxGymObsSpace(self.env_in.observation_space) + env_gym_out.action_space = BoxGymActSpace(self.env_out.action_space) + env_gym_out.observation_space = BoxGymObsSpace(self.env_out.observation_space) + env_gym_in.reset() + env_gym_out.reset() + + def test_gym_discrete(self): + """test I can create the gym env with discrete act space""" + env_gym_in = GymEnv(self.env_in) + env_gym_out = GymEnv(self.env_out) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env_gym_in.action_space = DiscreteActSpace(self.env_in.action_space) + env_gym_out.action_space = DiscreteActSpace(self.env_out.action_space) + env_gym_in.reset() + env_gym_out.reset() + act = 0 + self._aux_run_envs(act, env_gym_in, env_gym_out) + + + def test_gym_multidiscrete(self): + """test I can create the gym env with multi discrete act space""" + env_gym_in = GymEnv(self.env_in) + env_gym_out = GymEnv(self.env_out) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore") + env_gym_in.action_space = MultiDiscreteActSpace(self.env_in.action_space) + env_gym_out.action_space = MultiDiscreteActSpace(self.env_out.action_space) + env_gym_in.reset() + env_gym_out.reset() + act = env_gym_in.action_space.sample() + act[:] = 0 + self._aux_run_envs(act, env_gym_in, env_gym_out) + + +if __name__ == "__main__": + unittest.main()