Source code for grid2op.Chronics.time_series_from_handlers

# 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.

from datetime import datetime, timedelta
import os
import numpy as np
import copy
from typing import Optional, Union, Dict, Literal

import grid2op
from grid2op.Exceptions import (
    ChronicsNotFoundError, HandlerError
)

from grid2op.Chronics.gridValue import GridValue
from grid2op.Chronics.handlers import BaseHandler

from grid2op.Exceptions.grid2OpException import Grid2OpException
from grid2op.dtypes import dt_int, dt_float


[docs]class FromHandlers(GridValue): """This class allows to use the :class:`grid2op.Chronics.handlers.BaseHandler` (and all the derived class, see :ref:`tshandler-module`) to generate the "input time series" of the environment. This class does nothing in particular beside making sure the "formalism" of the Handlers can be adapted to generate compliant grid2op data. .. seealso:: :ref:`tshandler-module` for more information In order to use the handlers you need to: - tell grid2op that you are going to generate time series from "handlers" by using `FromHandlers` class - for each type of data ("gen_p", "gen_v", "load_p", "load_q", "maintenance", "gen_p_forecasted", "load_p_forecasted", "load_q_forecasted" and "load_v_forecasted") you need to provide a way to "handle" this type of data: you need a specific handler. You need at least to provide handlers for the environment data types ("gen_p", "gen_v", "load_p", "load_q"). If you do not provide handlers for some data (*e.g* for "maintenance", "gen_p_forecasted", "load_p_forecasted", "load_q_forecasted" and "load_v_forecasted") then it will be treated like "change nothing": - there will be no maintenance if you do not provide a handler for maintenance - for forecast it's a bit different... You will benefit from forecast if at least one handler generates some (**though we do not recommend to do it**). And in that case, the "missing handlers" will be treated as "no data available, keep as it was last time" .. warning:: You cannot mix up all types of handler with each other. We wrote in the description of each Handlers some conditions for them to work well. Examples --------- You can use the handers this way: .. code-block:: python import grid2op from grid2op.Chronics import FromHandlers from grid2op.Chronics.handlers import CSVHandler, DoNothingHandler, PerfectForecastHandler env_name = "l2rpn_case14_sandbox" env = grid2op.make(env_name, data_feeding_kwargs={"gridvalueClass": FromHandlers, "gen_p_handler": CSVHandler("prod_p"), "load_p_handler": CSVHandler("load_p"), "gen_v_handler": DoNothingHandler("prod_v"), "load_q_handler": CSVHandler("load_q"), "gen_p_for_handler": PerfectForecastHandler("prod_p_forecasted"), "load_p_for_handler": PerfectForecastHandler("load_p_forecasted"), "load_q_for_handler": PerfectForecastHandler("load_q_forecasted"), } ) obs = env.reset() # and now you can use "env" as any grid2op environment. More examples are given in the :ref:`tshandler-module` . Notes ------ For the environment, data, the handler are called in the order: "load_p", "load_q", "gen_p" and finally "gen_v". They are called once per step (per handler) at most. Then the maintenance (and hazards) data are generated with the appropriate handler. Finally, the forecast data are called after the environment data (and the maintenance data) once per step and per horizon. Horizon are called "in the order" (all data "for 5 minutes", all data "for 10 minutes", all data for "15 minutes" etc.). And for a given horizon, like the environment it is called in the order: "load_p", "load_q", "gen_p" and "gen_v". About the seeding, the handlers are seeded in the order: - load_p - load_q - gen_p - gen_v - maintenance - hazards - load_p_for - load_q_for - gen_p_for - gen_v_for Each individual handler will have its own pseudo random generator and the same seed will be used regardless of the presence / absence of other handlers. For example, regardless of the fact that you have a `maintenance_handler`, if you type `env.seed(0)` the `load_p_for_handler` will behave exactly the same (it will generate the same numbers whether or not you have maintenance or not.) """ MULTI_CHRONICS = False def __init__( self, path, # can be None ! load_p_handler, load_q_handler, gen_p_handler, gen_v_handler, maintenance_handler=None, hazards_handler=None, load_p_for_handler=None, load_q_for_handler=None, gen_p_for_handler=None, gen_v_for_handler=None, init_state_handler=None, time_interval=timedelta(minutes=5), sep=";", # here for compatibility with grid2op, but not used max_iter=-1, start_datetime=datetime(year=2019, month=1, day=1), chunk_size=None, h_forecast=(5,), ): GridValue.__init__( self, time_interval=time_interval, max_iter=max_iter, start_datetime=start_datetime, chunk_size=chunk_size, ) self.path = path if self.path is not None: self._init_date_time() # all my "handlers" (I need to perform a deepcopy otherwise data are kept between episode...) self.gen_p_handler : BaseHandler = copy.deepcopy(gen_p_handler) self.gen_v_handler : BaseHandler = copy.deepcopy(gen_v_handler) self.load_p_handler : BaseHandler = copy.deepcopy(load_p_handler) self.load_q_handler : BaseHandler = copy.deepcopy(load_q_handler) self.maintenance_handler : Optional[BaseHandler] = copy.deepcopy(maintenance_handler) self.hazards_handler : Optional[BaseHandler] = copy.deepcopy(hazards_handler) self.gen_p_for_handler : Optional[BaseHandler] = copy.deepcopy(gen_p_for_handler) self.gen_v_for_handler : Optional[BaseHandler] = copy.deepcopy(gen_v_for_handler) self.load_p_for_handler : Optional[BaseHandler] = copy.deepcopy(load_p_for_handler) self.load_q_for_handler : Optional[BaseHandler] = copy.deepcopy(load_q_for_handler) self.init_state_handler : Optional[BaseHandler] = copy.deepcopy(init_state_handler) # when there are no maintenance / hazards, build this only once self._no_mh_time = None self._no_mh_duration = None # define the active handlers self._active_handlers = [self.gen_p_handler, self.gen_v_handler, self.load_p_handler, self.load_q_handler] self._forcast_handlers = [] if self.maintenance_handler is not None: self._active_handlers.append(self.maintenance_handler) if self.hazards_handler is not None: self._active_handlers.append(self.hazards_handler) if self.gen_p_for_handler is not None: self._active_handlers.append(self.gen_p_for_handler) self._forcast_handlers.append(self.gen_p_for_handler) if self.gen_v_for_handler is not None: self._active_handlers.append(self.gen_v_for_handler) self._forcast_handlers.append(self.gen_v_for_handler) if self.load_p_for_handler is not None: self._active_handlers.append(self.load_p_for_handler) self._forcast_handlers.append(self.load_p_for_handler) if self.load_q_for_handler is not None: self._active_handlers.append(self.load_q_for_handler) self._forcast_handlers.append(self.load_q_for_handler) if self.init_state_handler is not None: self._active_handlers.append(self.init_state_handler) self._check_types() # now synch all handlers for handl in self._forcast_handlers: handl.set_h_forecast(h_forecast) # set the current path of the time series self._set_path(self.path) if chunk_size is not None: self.set_chunk_size(chunk_size) if max_iter != -1: self._set_max_iter(max_iter) self.init_datetime() self.current_inj = None def _check_types(self): for handl in self._active_handlers: if not isinstance(handl, BaseHandler): raise HandlerError("One of the \"handler\" used in your time series does not " "inherit from `BaseHandler`. This is not supported.")
[docs] def initialize( self, order_backend_loads, order_backend_prods, order_backend_lines, order_backend_subs, names_chronics_to_backend=None, ): # set the current path of the time series self._set_path(self.path) # give the right date and times to the "handlers" self.init_datetime() self.n_gen = len(order_backend_prods) self.n_load = len(order_backend_loads) self.n_line = len(order_backend_lines) self.curr_iter = 0 self.current_inj = None self.gen_p_handler.initialize(order_backend_prods, names_chronics_to_backend) self.gen_v_handler.initialize(order_backend_prods, names_chronics_to_backend) self.load_p_handler.initialize(order_backend_loads, names_chronics_to_backend) self.load_q_handler.initialize(order_backend_loads, names_chronics_to_backend) self._update_max_iter() # might be used in the forecast if self.gen_p_for_handler is not None: self.gen_p_for_handler.initialize(order_backend_prods, names_chronics_to_backend) if self.gen_v_for_handler is not None: self.gen_v_for_handler.initialize(order_backend_prods, names_chronics_to_backend) if self.load_p_for_handler is not None: self.load_p_for_handler.initialize(order_backend_loads, names_chronics_to_backend) if self.load_q_for_handler is not None: self.load_q_for_handler.initialize(order_backend_loads, names_chronics_to_backend) self._update_max_iter() # might be used in the maintenance if self.maintenance_handler is not None: self.maintenance_handler.initialize(order_backend_lines, names_chronics_to_backend) if self.hazards_handler is not None: self.hazards_handler.initialize(order_backend_lines, names_chronics_to_backend) # when there are no maintenance / hazards, build this only once self._no_mh_time = np.full(self.n_line, fill_value=-1, dtype=dt_int) self._no_mh_duration = np.full(self.n_line, fill_value=0, dtype=dt_int) self._update_max_iter()
[docs] def load_next(self): self.current_datetime += self.time_interval self.curr_iter += 1 res = {} # load the injection dict_inj, prod_v = self._load_injection() res["injection"] = dict_inj # load maintenance if self.maintenance_handler is not None: tmp_ = self.maintenance_handler.load_next(res) if tmp_ is not None: res["maintenance"] = tmp_ maintenance_time, maintenance_duration = self.maintenance_handler.load_next_maintenance() else: maintenance_time = self._no_mh_time maintenance_duration = self._no_mh_duration # load hazards if self.hazard_duration is not None: res["hazards"] = self.hazards_handler.load_next(res) hazard_duration = self.hazards_handler.load_next_hazard() else: hazard_duration = self._no_mh_duration self.current_inj = res return ( self.current_datetime, res, maintenance_time, maintenance_duration, hazard_duration, prod_v, )
[docs] def max_timestep(self): return self.max_iter
[docs] def next_chronics(self): self.current_datetime = self.start_datetime self.curr_iter = 0 for el in self._active_handlers: el.next_chronics() self._update_max_iter()
[docs] def done(self): # I am done if the part I control is "over" if self._max_iter > 0 and self.curr_iter > self._max_iter: return True # or if any of the handler is "done" for handl in self._active_handlers: if handl.done(): return True return False
[docs] def check_validity(self, backend): for el in self._active_handlers: el.check_validity(backend) # TODO other things here maybe ??? return True
def _aux_forecasts(self, h_id, dict_, key, for_handler, base_handler, handlers): if for_handler is not None: tmp_ = for_handler.forecast(h_id, self.current_inj, dict_, base_handler, handlers) if tmp_ is not None: dict_[key] = dt_float(1.0) * tmp_
[docs] def forecasts(self): res = [] if not self._forcast_handlers: # nothing to handle forecast in this class return res handlers = (self.load_p_handler, self.load_q_handler, self.gen_p_handler, self.gen_v_handler) for h_id, h in enumerate(self._forcast_handlers[0].get_available_horizons()): dict_ = {} self._aux_forecasts(h_id, dict_, "load_p", self.load_p_for_handler, self.load_p_handler, handlers) self._aux_forecasts(h_id, dict_, "load_q", self.load_q_for_handler, self.load_q_handler, handlers) self._aux_forecasts(h_id, dict_, "prod_p", self.gen_p_for_handler, self.gen_p_handler, handlers) self._aux_forecasts(h_id, dict_, "prod_v", self.gen_v_for_handler, self.gen_v_handler, handlers) res_d = {} if dict_: res_d["injection"] = dict_ forecast_datetime = self.current_datetime + timedelta(minutes=h) res.append((forecast_datetime, res_d)) return res
[docs] def get_kwargs(self, dict_): dict_["gen_p_handler"] = copy.deepcopy(self.gen_p_handler)._clear() if self.gen_p_handler is not None else None dict_["gen_v_handler"] = copy.deepcopy(self.gen_v_handler)._clear() if self.gen_v_handler is not None else None dict_["load_p_handler"] = copy.deepcopy(self.load_p_handler)._clear() if self.load_p_handler is not None else None dict_["load_q_handler"] = copy.deepcopy(self.load_q_handler)._clear() if self.load_q_handler is not None else None dict_["maintenance_handler"] = copy.deepcopy(self.maintenance_handler)._clear() if self.maintenance_handler is not None else None dict_["hazards_handler"] = copy.deepcopy(self.hazards_handler)._clear() if self.hazards_handler is not None else None dict_["gen_p_for_handler"] = copy.deepcopy(self.gen_p_for_handler)._clear() if self.gen_p_for_handler is not None else None dict_["gen_v_for_handler"] = copy.deepcopy(self.gen_v_for_handler)._clear() if self.gen_v_for_handler is not None else None dict_["load_p_for_handler"] = copy.deepcopy(self.load_p_for_handler)._clear() if self.load_p_for_handler is not None else None dict_["load_q_for_handler"] = copy.deepcopy(self.load_q_for_handler)._clear() if self.load_q_for_handler is not None else None return dict_
[docs] def get_id(self) -> str: if self.path is not None: return self.path else: # TODO raise NotImplementedError()
[docs] def shuffle(self, shuffler=None): # TODO pass
[docs] def sample_next_chronics(self, probabilities=None): # TODO pass
[docs] def set_chunk_size(self, new_chunk_size): # TODO for el in self._active_handlers: el.set_chunk_size(new_chunk_size)
def _set_max_iter(self, max_iter): self.max_iter = int(max_iter) for el in self._active_handlers: el._set_max_iter(max_iter) def init_datetime(self): for handl in self._active_handlers: handl.set_times(self.start_datetime, self.time_interval)
[docs] def seed(self, seed): super().seed(seed) max_seed = np.iinfo(dt_int).max seeds = self.space_prng.randint(max_seed, size=11) # this way of doing ensure the same seed given by the environment is # used even if some "handlers" are missing # (if env.seed(0) is called, then regardless of maintenance_handler or not, # gen_p_for_handler will always be seeded with the same number) lp_seed = self.load_p_handler.seed(seeds[0]) lq_seed = self.load_q_handler.seed(seeds[1]) gp_seed = self.gen_p_handler.seed(seeds[2]) gv_seed = self.gen_v_handler.seed(seeds[3]) maint_seed = None if self.maintenance_handler is not None: maint_seed = self.maintenance_handler.seed(seeds[4]) haz_seed = None if self.hazards_handler is not None: haz_seed = self.hazards_handler.seed(seeds[5]) lpf_seed = None if self.load_p_for_handler is not None: lpf_seed = self.load_p_for_handler.seed(seeds[6]) lqf_seed = None if self.load_q_for_handler is not None: lqf_seed = self.load_q_for_handler.seed(seeds[7]) gpf_seed = None if self.gen_p_for_handler is not None: gpf_seed = self.gen_p_for_handler.seed(seeds[8]) gvf_seed = None if self.gen_v_for_handler is not None: gvf_seed = self.gen_v_for_handler.seed(seeds[9]) init_state_seed = None if self.init_state_handler is not None: init_state_seed = self.init_state_handler.seed(seeds[10]) return (seed, gp_seed, gv_seed, lp_seed, lq_seed, maint_seed, haz_seed, gpf_seed, gvf_seed, lpf_seed, lqf_seed, init_state_seed)
def _set_path(self, path): """tell the handler where this chronics is located""" if path is None: return for el in self._active_handlers: el.set_path(path) def set_max_episode_duration(self, max_ep_dur): for handl in self._active_handlers: handl.set_max_episode_duration(max_ep_dur) def _update_max_iter(self): # get the max iter from the handlers max_iters = [el.get_max_iter() for el in self._active_handlers] max_iters = [el for el in max_iters if el != -1] # get the max iter from myself if self._max_iter != -1: max_iters.append(self.max_iter) # prevent empty list if not max_iters: max_iters.append(self.max_iter) # take the minimum self.max_iter = np.min(max_iters) # update everyone with the "new" max iter max_ep_dur = [el.max_episode_duration for el in self._active_handlers] max_ep_dur = [el for el in max_ep_dur if el is not None] if max_ep_dur: if self.max_iter == -1: self.max_iter = np.min(max_ep_dur) else: self.max_iter = min(self.max_iter, np.min(max_ep_dur)) if self.max_iter != -1: self.set_max_episode_duration(self.max_iter) def _load_injection(self): dict_ = {} prod_v = None if self.load_p_handler is not None: tmp_ = self.load_p_handler.load_next(dict_) if tmp_ is not None: dict_["load_p"] = dt_float(1.0) * tmp_ if self.load_q_handler is not None: tmp_ = self.load_q_handler.load_next(dict_) if tmp_ is not None: dict_["load_q"] = dt_float(1.0) * tmp_ if self.gen_p_handler is not None: tmp_ = self.gen_p_handler.load_next(dict_) if tmp_ is not None: dict_["prod_p"] = dt_float(1.0) * tmp_ if self.gen_v_handler is not None: tmp_ = self.gen_v_handler.load_next(dict_) if tmp_ is not None: prod_v = dt_float(1.0) * tmp_ return dict_, prod_v def _init_date_time(self): # in csv handler if os.path.exists(os.path.join(self.path, "start_datetime.info")): with open(os.path.join(self.path, "start_datetime.info"), "r") as f: a = f.read().rstrip().lstrip() try: tmp = datetime.strptime(a, "%Y-%m-%d %H:%M") except ValueError: tmp = datetime.strptime(a, "%Y-%m-%d") except Exception: raise ChronicsNotFoundError( 'Impossible to understand the content of "start_datetime.info". Make sure ' 'it\'s composed of only one line with a datetime in the "%Y-%m-%d %H:%M"' "format." ) self.start_datetime = tmp self.current_datetime = tmp if os.path.exists(os.path.join(self.path, "time_interval.info")): with open(os.path.join(self.path, "time_interval.info"), "r") as f: a = f.read().rstrip().lstrip() try: tmp = datetime.strptime(a, "%H:%M") except ValueError: tmp = datetime.strptime(a, "%M") except Exception: raise ChronicsNotFoundError( 'Impossible to understand the content of "time_interval.info". Make sure ' 'it\'s composed of only one line with a datetime in the "%H:%M"' "format." ) self.time_interval = timedelta(hours=tmp.hour, minutes=tmp.minute)
[docs] def fast_forward(self, nb_timestep): for _ in range(nb_timestep): self.load_next() # for this class I suppose the real data AND the forecast are read each step self.forecasts()
[docs] def get_init_action(self, names_chronics_to_backend: Optional[Dict[Literal["loads", "prods", "lines"], Dict[str, str]]]=None) -> Union["grid2op.Action.playableAction.PlayableAction", None]: from grid2op.Action import BaseAction if self.init_state_handler is None: return None act_as_dict = self.init_state_handler.get_init_dict_action() if act_as_dict is None: return None if self.action_space is None: raise Grid2OpException(f"We detected an action to set the intial state of the grid " f"but we cannot build it because the 'action_space' of the time" f"serie is not set.") try: act : BaseAction = self.action_space(act_as_dict, check_legal=False, _names_chronics_to_backend=names_chronics_to_backend) except Grid2OpException as exc_: raise Grid2OpException(f"Impossible to build the action to set the grid. Please fix the " f"file located at {self.init_state_handler.path}.") from exc_ # TODO check change bus, redispatching, change status etc. # TODO basically anything that would be suspicious here error, reason = act.is_ambiguous() if error: raise Grid2OpException(f"The action to set the grid to its original configuration " f"is ambiguous. Please check {self.init_state_handler.path}") from reason return act
[docs] def regenerate_with_new_seed(self): for handl in self._active_handlers: handl.regenerate_with_new_seed()