diff --git a/docs/source/conf.py b/docs/source/conf.py
index 065ff137..f7734d00 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -7,6 +7,9 @@
import os
import sys
+from copy import deepcopy
+from builtins import object
+
import pymgrid
@@ -29,6 +32,7 @@
'sphinx.ext.autosummary',
'sphinx.ext.doctest',
'sphinx.ext.linkcode',
+ 'sphinx.ext.intersphinx',
'nbsphinx',
'nbsphinx_link',
'IPython.sphinxext.ipython_console_highlighting'
@@ -50,8 +54,13 @@
html_static_path = ['_static']
-
-skip_members = ['yaml_flow_style']
+# These are attributes that don't have a __doc__ attribute to read ':meta private:' from.
+skip_members = ['yaml_flow_style',
+ 'metadata',
+ 'render_mode',
+ 'reward_range',
+ 'spec'
+ ]
def autodoc_skip_member(app, what, name, obj, skip, options):
@@ -68,6 +77,17 @@ def autodoc_skip_member(app, what, name, obj, skip, options):
return None
+def autodoc_process_signature(app, what, name, obj, options, signature, return_annotation):
+ """
+ If a class signature is being read from cls.__new__, we want to replace it with the signature from cls.__init__.
+ """
+ if what == 'class' and signature[1:] in str(inspect.signature(obj.__new__)):
+ obj_copy = deepcopy(obj)
+ obj_copy.__new__ = object.__new__
+ signature = str(inspect.signature(obj_copy))
+ return signature, return_annotation
+
+
def linkcode_resolve(domain, info):
"""
Determine the URL corresponding to Python object
@@ -116,8 +136,14 @@ def linkcode_resolve(domain, info):
fn = os.path.relpath(fn, start=os.path.dirname(pymgrid.__file__))
- return f"https://github.com/Total-RD/pymgrid/tree/v{pymgrid.__version__}/src/pymgrid/{fn}{linespec}"
+ return f'https://github.com/Total-RD/pymgrid/tree/v{pymgrid.__version__}/src/pymgrid/{fn}{linespec}'
+
+
+intersphinx_mapping = {
+ 'gym': ('https://www.gymlibrary.dev/', None)
+}
def setup(app):
app.connect('autodoc-skip-member', autodoc_skip_member)
+ app.connect('autodoc-process-signature', autodoc_process_signature)
diff --git a/docs/source/reference/envs/index.rst b/docs/source/reference/envs/index.rst
new file mode 100644
index 00000000..47b3fec7
--- /dev/null
+++ b/docs/source/reference/envs/index.rst
@@ -0,0 +1,30 @@
+.. _api.envs:
+
+Reinforcement Learning
+======================
+
+.. currentmodule:: pymgrid.envs
+
+Environment classes using the `OpenAI Gym API `_ for reinforcement learning.
+
+Discrete Environment
+--------------------
+
+Environment with a discrete action space.
+
+
+.. autosummary::
+ :toctree: ../api/envs/
+
+ DiscreteMicrogridEnv
+
+Continuous Environment
+----------------------
+
+Environment with a discrete action space.
+
+
+.. autosummary::
+ :toctree: ../api/envs/
+
+ ContinuousMicrogridEnv
diff --git a/docs/source/reference/index.rst b/docs/source/reference/index.rst
index a69f9b4a..e258a6f6 100644
--- a/docs/source/reference/index.rst
+++ b/docs/source/reference/index.rst
@@ -8,3 +8,4 @@ This page contains an overview of all public *pymgrid* objects and functions.
microgrid
modules/index
+ envs/index
diff --git a/local/pymgrid reports.docx b/local/pymgrid reports.docx
new file mode 100644
index 00000000..7146e8c9
Binary files /dev/null and b/local/pymgrid reports.docx differ
diff --git a/src/pymgrid/envs/base/base.py b/src/pymgrid/envs/base/base.py
index f64e8cd7..7582636f 100644
--- a/src/pymgrid/envs/base/base.py
+++ b/src/pymgrid/envs/base/base.py
@@ -7,19 +7,63 @@
class BaseMicrogridEnv(Microgrid, Env):
+ """
+ Base class for all microgrid environments.
+
+ Implements the `OpenAI Gym API `_ for a microgrid;
+ inherits from both :class:`.Microgrid` and :class:`gym.Env`.
+
+ Parameters
+ ----------
+ modules : list, Microgrid, NonModularMicrogrid, or int.
+ The constructor can be called in three ways:
+
+ 1. Passing a list of microgrid modules. This is identical to the :class:`.Microgrid` constructor.
+
+ 2. Passing a :class:`.Microgrid` or :class:`.NonModularMicrogrid` instance.
+ This will effectively wrap the microgrid instance with the Gym API.
+
+ 3. Passing an integer in [0, 25).
+ This will be result in loading the corresponding `pymgrid25` benchmark microgrids.
+
+ add_unbalanced_module : bool, default True.
+ Whether to add an unbalanced energy module to your microgrid. Such a module computes and attributes
+ costs to any excess supply or demand.
+ Set to True unless ``modules`` contains an :class:`.UnbalancedEnergyModule`.
+
+ loss_load_cost : float, default 10.0
+ Cost per unit of unmet demand. Ignored if ``add_unbalanced_module=False``.
+
+ overgeneration_cost : float, default 2.0
+ Cost per unit of excess generation. Ignored if ``add_unbalanced_module=False``.
+
+ flat_spaces : bool, default True
+ Whether the environment's spaces should be flat.
+
+ If True, all continuous spaces are :class:`gym:gym.spaces.Box`.
+
+ Otherwise, they are nested :class:`gym:gym.spaces.Dict` of :class:`gym:gym.spaces.Tuple`
+ of :class:`gym:gym.spaces.Box`, corresponding to the structure of the ``control`` arg of :meth:`.Microgrid.run`.
+
+ """
+
+ action_space = None
+ 'Space object corresponding to valid actions.'
+
+ observation_space = None
+ 'Space object corresponding to valid observations.'
+
def __new__(cls, modules, *args, **kwargs):
if isinstance(modules, (NonModularMicrogrid, Microgrid)):
instance = cls.from_microgrid(modules)
- cls.__init__ = skip_init(cls, cls.__init__)
- return instance
- elif "scenario" in kwargs or "microgrid_number" in kwargs:
- scenario = kwargs.get("scenario", "pymgrid25")
- microgrid_number = kwargs.get("microgrid_number", 0)
- instance = cls.from_scenario(microgrid_number=microgrid_number)
- cls.__init__ = skip_init(cls, cls.__init__)
- return instance
- return super().__new__(cls)
+ elif isinstance(modules, int):
+ instance = cls.from_scenario(modules)
+ else:
+ return super().__new__(cls)
+
+ cls.__init__ = skip_init(cls, cls.__init__)
+ return instance
def __init__(self,
modules,
@@ -59,8 +103,39 @@ def reset(self):
obs = super().reset()
return flatten(self._nested_observation_space, obs) if self._flat_spaces else obs
+ @property
+ def flat_spaces(self):
+ """
+ Whether the environment's spaces are flat.
+
+ If True, all continuous spaces are :class:`gym:gym.spaces.Box`.
+
+ Otherwise, they are nested :class:`gym:gym.spaces.Dict` of :class:`gym:gym.spaces.Tuple`
+ of :class:`gym:gym.spaces.Box`, corresponding to the structure of the ``control`` arg of :meth:`Microgrid.run`.
+
+ Returns
+ -------
+ flat_spaces : bool
+ Whether the environment's spaces are flat.
+
+ """
+ return self._flat_spaces
+
@classmethod
def from_microgrid(cls, microgrid):
+ """
+ Construct a microgrid from
+
+ Parameters
+ ----------
+ microgrid_number : int, default 0
+ Number of the microgrid to return. ``0<=microgrid_number<25``.
+
+ Returns
+ -------
+ scenario : pymgrid.Microgrid
+ The loaded microgrid.
+ """
try:
return cls(microgrid.module_tuples(), add_unbalanced_module=False)
except AttributeError:
diff --git a/src/pymgrid/envs/base/skip_init.py b/src/pymgrid/envs/base/skip_init.py
index 38ce456f..fc2d901f 100644
--- a/src/pymgrid/envs/base/skip_init.py
+++ b/src/pymgrid/envs/base/skip_init.py
@@ -1,9 +1,19 @@
def skip_init(cls, init):
"""
Skip init once on cls, and then revert to original init.
- :param cls: Class to skip init on.
- :param init: original init.
- :return: callable that skips init once.
+
+ Parameters
+ ----------
+ cls : Type
+ Class to skip init on.
+ init : callable
+ Original init.
+
+ Returns
+ -------
+ skip_init : callable
+ Callable that skips init once.
+
"""
def reset_init(*args, **kwargs):
cls.__init__ = init
diff --git a/src/pymgrid/envs/continuous/continuous.py b/src/pymgrid/envs/continuous/continuous.py
index e45805b1..a3715785 100644
--- a/src/pymgrid/envs/continuous/continuous.py
+++ b/src/pymgrid/envs/continuous/continuous.py
@@ -5,26 +5,53 @@
class ContinuousMicrogridEnv(BaseMicrogridEnv):
- def __init__(self,
- modules,
- add_unbalanced_module=True,
- loss_load_cost=10,
- overgeneration_cost=2,
- flat_spaces=True
- ):
+ """
+ Microgrid environment with a continuous action space.
- self._nested_action_space = self._get_nested_action_space()
- super().__init__(modules,
- add_unbalanced_module=add_unbalanced_module,
- loss_load_cost=loss_load_cost,
- overgeneration_cost=overgeneration_cost,
- flat_spaces=flat_spaces)
+ Implements the `OpenAI Gym API `_ for a microgrid;
+ inherits from both :class:`.Microgrid` and :class:`gym.Env`.
+
+ Parameters
+ ----------
+ modules : list, Microgrid, NonModularMicrogrid, or int.
+ The constructor can be called in three ways:
+
+ 1. Passing a list of microgrid modules. This is identical to the :class:`.Microgrid` constructor.
+
+ 2. Passing a :class:`.Microgrid` or :class:`.NonModularMicrogrid` instance.
+ This will effectively wrap the microgrid instance with the Gym API.
+
+ 3. Passing an integer in [0, 25).
+ This will be result in loading the corresponding `pymgrid25` benchmark microgrids.
+
+ add_unbalanced_module : bool, default True.
+ Whether to add an unbalanced energy module to your microgrid. Such a module computes and attributes
+ costs to any excess supply or demand.
+ Set to True unless ``modules`` contains an :class:`.UnbalancedEnergyModule`.
+
+ loss_load_cost : float, default 10.0
+ Cost per unit of unmet demand. Ignored if ``add_unbalanced_module=False``.
+
+ overgeneration_cost : float, default 2.0
+ Cost per unit of excess generation. Ignored if ``add_unbalanced_module=False``.
+
+ flat_spaces : bool, default True
+ Whether the environment's spaces should be flat.
+
+ If True, all continuous spaces are :class:`gym:gym.spaces.Box`.
+
+ Otherwise, they are nested :class:`gym:gym.spaces.Dict` of :class:`gym:gym.spaces.Tuple`
+ of :class:`gym:gym.spaces.Box`, corresponding to the structure of the ``control`` arg of :meth:`.Microgrid.run`.
+
+ """
+ _nested_action_space = None
def _get_nested_action_space(self):
return Dict({name: Tuple([module.action_spaces['normalized'] for module in modules_list])
for name, modules_list in self.fixed.iterdict() if modules_list[0].is_source})
def _get_action_space(self):
+ self._nested_action_space = self._get_nested_action_space()
return flatten_space(self._nested_action_space) if self._flat_spaces else self._nested_action_space
def _get_action(self, action):
diff --git a/src/pymgrid/envs/discrete/discrete.py b/src/pymgrid/envs/discrete/discrete.py
index e4f09972..acd258a7 100644
--- a/src/pymgrid/envs/discrete/discrete.py
+++ b/src/pymgrid/envs/discrete/discrete.py
@@ -1,8 +1,10 @@
-from itertools import permutations
-from warnings import warn
import numpy as np
+import yaml
+
+from itertools import permutations
from gym.spaces import Discrete
from math import isclose
+from warnings import warn
from pymgrid.envs.base import BaseMicrogridEnv
from pymgrid.utils.logger import ModularLogger
@@ -24,6 +26,11 @@ class DiscreteMicrogridEnv(BaseMicrogridEnv):
Each tuple contains three elements (module_name, total_actions_for_{module_name}, action_num).
For example: (('genset', 0), 2, 1) is a tuple defining the first element (of two) for ('genset', 0).
"""
+
+ yaml_tag = u"!DiscreteMicrogridEnv"
+ yaml_loader = yaml.SafeLoader
+ yaml_dumper = yaml.SafeDumper
+
def __init__(self,
modules,
add_unbalanced_module=True,
diff --git a/src/pymgrid/microgrid/microgrid.py b/src/pymgrid/microgrid/microgrid.py
index c1cef1b7..960ffe21 100644
--- a/src/pymgrid/microgrid/microgrid.py
+++ b/src/pymgrid/microgrid/microgrid.py
@@ -523,7 +523,7 @@ def dump(self, stream=None):
on the value of ``stream``. If ``stream is None``, array-like objects are serialized inline. If ``stream`` is
a stream to a file-like object, however, array-like objects will be serialized as `.csv.gz` files in a
directory relative to ``stream``, and the relative locations stored inline in the YAML file. For an example of
- this behavior, see `data/scenario/pymgrid25/microgrid_0`.
+ this behavior, see `data/scenario/pymgrid25/microgrid_0`.
"""
return yaml.safe_dump(self, stream=stream)
@@ -630,7 +630,7 @@ def from_scenario(cls, microgrid_number=0):
Parameters
----------
microgrid_number : int, default 0
- Number of the microgrid to return. 0<=microgrid_number<25.
+ Number of the microgrid to return. ``0<=microgrid_number<25``.
Returns
-------
@@ -639,6 +639,10 @@ def from_scenario(cls, microgrid_number=0):
"""
from pymgrid import PROJECT_PATH
n = microgrid_number
+
+ if n not in np.arange(25):
+ raise TypeError(f'Invalid microgrid_number {n}, must be an integer in the range [0, 25).')
+
with open(PROJECT_PATH / f"data/scenario/pymgrid25/microgrid_{n}/microgrid_{n}.yaml", "r") as f:
return cls.load(f)
diff --git a/src/pymgrid/modules/base/base_module.py b/src/pymgrid/modules/base/base_module.py
index b1ffb885..03e08794 100644
--- a/src/pymgrid/modules/base/base_module.py
+++ b/src/pymgrid/modules/base/base_module.py
@@ -410,7 +410,7 @@ def to_normalized(self, value, act=False, obs=False):
def from_normalized(self, value, act=False, obs=False):
"""
- Un-normalized an action or observation.
+ Un-normalize an action or observation.
Parameters
----------
diff --git a/src/pymgrid/modules/genset_module.py b/src/pymgrid/modules/genset_module.py
index d05f4ce0..e3f19ef7 100644
--- a/src/pymgrid/modules/genset_module.py
+++ b/src/pymgrid/modules/genset_module.py
@@ -48,7 +48,7 @@ class GensetModule(BaseMicrogridModule):
If False, actions are clipped to the limit possible.
provided_energy_name : str, default "genset_production"
- Name of the energy provided by this microgrid used in logging.
+ Name of the energy provided by this module, to be used in logging.
"""
module_type = 'genset', 'fixed'
diff --git a/src/pymgrid/modules/load_module.py b/src/pymgrid/modules/load_module.py
index 438e069d..f1e2ad2a 100644
--- a/src/pymgrid/modules/load_module.py
+++ b/src/pymgrid/modules/load_module.py
@@ -6,6 +6,51 @@
class LoadModule(BaseTimeSeriesMicrogridModule):
+ """
+ A renewable energy module.
+
+ The classic examples of renewables are photovoltaics (PV) and wind turbines.
+
+ Parameters
+ ----------
+ time_series : array-like, shape (n_steps, )
+ Time series of load demand.
+
+ loss_load_cost : float, default 10.0
+ Cost per unit of loss load (unmet load).
+
+ forecaster : callable, float, "oracle", or None, default None.
+ Function that gives a forecast n-steps ahead.
+
+ * If ``callable``, must take as arguments ``(val_c: float, val_{c+n}: float, n: int)``, where
+
+ * ``val_c`` is the current value in the time series: ``self.time_series[self.current_step]``
+
+ * ``val_{c+n}`` is the value in the time series n steps in the future
+
+ * n is the number of steps in the future at which we are forecasting.
+
+ The output ``forecast = forecaster(val_c, val_{c+n}, n)`` must have the same sign
+ as the inputs ``val_c`` and ``val_{c+n}``.
+
+ * If ``float``, serves as a standard deviation for a mean-zero gaussian noise function
+ that is added to the true value.
+
+ * If ``"oracle"``, gives a perfect forecast.
+
+ * If ``None``, no forecast.
+
+ forecast_horizon : int.
+ Number of steps in the future to forecast. If forecaster is None, ignored and 0 is returned.
+
+ forecaster_increase_uncertainty : bool, default False
+ Whether to increase uncertainty for farther-out dates if using a GaussianNoiseForecaster. Ignored otherwise..
+
+ raise_errors : bool, default False
+ Whether to raise errors if bounds are exceeded in an action.
+ If False, actions are clipped to the limit possible.
+
+ """
module_type = ('load', 'fixed')
yaml_tag = u"!LoadModule"
yaml_dumper = yaml.SafeDumper
@@ -13,7 +58,7 @@ class LoadModule(BaseTimeSeriesMicrogridModule):
def __init__(self,
time_series,
- loss_load_cost,
+ loss_load_cost=10.0,
forecaster=None,
forecast_horizon=DEFAULT_HORIZON,
forecaster_increase_uncertainty=False,
@@ -53,6 +98,15 @@ def max_consumption(self):
@property
def current_load(self):
+ """
+ Current load.
+
+ Returns
+ -------
+ load : float
+ Current load demand.
+
+ """
return self._time_series[self._current_step]
@property
diff --git a/src/pymgrid/modules/renewable_module.py b/src/pymgrid/modules/renewable_module.py
index fd5ea6d7..6125ba75 100644
--- a/src/pymgrid/modules/renewable_module.py
+++ b/src/pymgrid/modules/renewable_module.py
@@ -6,6 +6,51 @@
class RenewableModule(BaseTimeSeriesMicrogridModule):
+ """
+ A renewable energy module.
+
+ The classic examples of renewables are photovoltaics (PV) and wind turbines.
+
+ Parameters
+ ----------
+ time_series : array-like, shape (n_steps, )
+ Time series of renewable production.
+
+ forecaster : callable, float, "oracle", or None, default None.
+ Function that gives a forecast n-steps ahead.
+
+ * If ``callable``, must take as arguments ``(val_c: float, val_{c+n}: float, n: int)``, where
+
+ * ``val_c`` is the current value in the time series: ``self.time_series[self.current_step]``
+
+ * ``val_{c+n}`` is the value in the time series n steps in the future
+
+ * n is the number of steps in the future at which we are forecasting.
+
+ The output ``forecast = forecaster(val_c, val_{c+n}, n)`` must have the same sign
+ as the inputs ``val_c`` and ``val_{c+n}``.
+
+ * If ``float``, serves as a standard deviation for a mean-zero gaussian noise function
+ that is added to the true value.
+
+ * If ``"oracle"``, gives a perfect forecast.
+
+ * If ``None``, no forecast.
+
+ forecast_horizon : int.
+ Number of steps in the future to forecast. If forecaster is None, ignored and 0 is returned.
+
+ forecaster_increase_uncertainty : bool, default False
+ Whether to increase uncertainty for farther-out dates if using a GaussianNoiseForecaster. Ignored otherwise.
+
+ provided_energy_name: str, default "renewable_used"
+ Name of the energy provided by this module, to be used in logging.
+
+ raise_errors : bool, default False
+ Whether to raise errors if bounds are exceeded in an action.
+ If False, actions are clipped to the limit possible.
+
+ """
module_type = ('renewable', 'flex')
yaml_tag = u"!RenewableModule"
yaml_loader = yaml.SafeLoader
@@ -45,6 +90,15 @@ def max_production(self):
@property
def current_renewable(self):
+ """
+ Current renewable production.
+
+ Returns
+ -------
+ renewable : float
+ Renewable production.
+
+ """
return self._time_series[self._current_step]
@property
diff --git a/src/pymgrid/modules/unbalanced_energy_module.py b/src/pymgrid/modules/unbalanced_energy_module.py
index 7060f665..0f0c9479 100644
--- a/src/pymgrid/modules/unbalanced_energy_module.py
+++ b/src/pymgrid/modules/unbalanced_energy_module.py
@@ -33,12 +33,38 @@ def update(self, external_energy_change, as_source=False, as_sink=False):
return reward, False, info
def get_cost(self, energy_amount, as_source, as_sink):
- if as_source: # loss-load
+ """
+ Get the cost of unmet load or excess production.
+
+ Parameters
+ ----------
+ energy_amount : float>=0
+ Amount of unmet load or excess production.
+
+ as_source : bool
+ Whether the energy is unmet load.
+
+ as_sink : bool
+ Whether the energy is excess production.
+
+ Returns
+ -------
+ cost : float
+
+ Raises
+ ------
+ TypeError
+ If both as_source and as_sink are True or neither are.
+
+ """
+ if as_source and as_sink:
+ raise TypeError("as_source and as_sink cannot both be True.")
+ if as_source: # loss load
return self.loss_load_cost*energy_amount
elif as_sink:
return self.overgeneration_cost*energy_amount
else:
- raise RuntimeError
+ raise TypeError("One of as_source or as_sink must be True.")
@property
def state_dict(self):
@@ -50,7 +76,6 @@ def state(self):
@property
def min_obs(self):
- # Min charge amount, min soc
return np.array([])
@property
@@ -80,5 +105,3 @@ def is_source(self):
@property
def is_sink(self):
return True
-
-