From f5a68eca9849e2de9e281a25f8f007eb1b892888 Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 11:48:27 -0700 Subject: [PATCH 01/28] add renewable cuttailment module --- .../modules/renewable_curtailment_module.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/pymgrid/modules/renewable_curtailment_module.py diff --git a/src/pymgrid/modules/renewable_curtailment_module.py b/src/pymgrid/modules/renewable_curtailment_module.py new file mode 100644 index 00000000..627e0d0c --- /dev/null +++ b/src/pymgrid/modules/renewable_curtailment_module.py @@ -0,0 +1,28 @@ +import numpy as np +import yaml + +from pymgrid.modules.base import BaseMicrogridModule + + +class RenewableCurtailmentModule(BaseMicrogridModule): + def update(self, external_energy_change, as_source=False, as_sink=False): + pass + + def _state_dict(self): + pass + + @property + def min_obs(self): + pass + + @property + def max_obs(self): + pass + + @property + def min_act(self): + pass + + @property + def max_act(self): + pass \ No newline at end of file From d909641226ef2e3834a345550c7c9acaf9104f73 Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 11:48:48 -0700 Subject: [PATCH 02/28] whitespace --- src/pymgrid/modules/unbalanced_energy_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymgrid/modules/unbalanced_energy_module.py b/src/pymgrid/modules/unbalanced_energy_module.py index e69877ec..bdfdd554 100644 --- a/src/pymgrid/modules/unbalanced_energy_module.py +++ b/src/pymgrid/modules/unbalanced_energy_module.py @@ -15,7 +15,7 @@ def __init__(self, initial_step=0, loss_load_cost=10, overgeneration_cost=2.0, - normalized_action_bounds = (0, 1) + normalized_action_bounds=(0, 1) ): super().__init__(raise_errors, From d1619ef90bee6159c2680ae0733c35c0c05ab843 Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 13:41:33 -0700 Subject: [PATCH 03/28] whitespace --- src/pymgrid/modules/renewable_curtailment_module.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pymgrid/modules/renewable_curtailment_module.py b/src/pymgrid/modules/renewable_curtailment_module.py index 627e0d0c..1afc42c0 100644 --- a/src/pymgrid/modules/renewable_curtailment_module.py +++ b/src/pymgrid/modules/renewable_curtailment_module.py @@ -1,4 +1,5 @@ import numpy as np + import yaml from pymgrid.modules.base import BaseMicrogridModule From ecb650ae17d1beac947e4c341577475b9504046d Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 13:41:50 -0700 Subject: [PATCH 04/28] update new module --- .../modules/renewable_curtailment_module.py | 140 +++++++++++++++++- 1 file changed, 133 insertions(+), 7 deletions(-) diff --git a/src/pymgrid/modules/renewable_curtailment_module.py b/src/pymgrid/modules/renewable_curtailment_module.py index 1afc42c0..8c74c00c 100644 --- a/src/pymgrid/modules/renewable_curtailment_module.py +++ b/src/pymgrid/modules/renewable_curtailment_module.py @@ -2,28 +2,154 @@ import yaml -from pymgrid.modules.base import BaseMicrogridModule +from pymgrid.modules.base import BaseMicrogridModule, BaseTimeSeriesMicrogridModule +from pymgrid.modules.module_container import ModuleContainer -class RenewableCurtailmentModule(BaseMicrogridModule): +class CurtailmentModule(BaseMicrogridModule): + """ + A curtailment module for renewables. + + The classic examples of renewables are photovoltaics (PV) and wind turbines. This module allows for their + production to be curtailed without incurring costs (or incurring costs different from other overgeneration). + + Parameters + ---------- + modules_to_curtail : list-like or None, default None + List of modules to curtail or None, in which case all curtailment will apply to all fixed source modules, + of which renewables are by default. Can contain any of the following: + + * :class:`.BaseMicrogridModule` : + Specific modules to curtail: subclasses of the base module. + + * tuple, length two : + tuple of the form ``(module_name, module_number)`` pointing to a specific module, e.g. ``('renewable', 0)``. + + * str : + Name of a particular module type, e.g. ``'renewable'``. All modules in :class:`.Microgrid.modules```['renewable'] + will be included. + + * None : + Use all fixed source modules, of which renewables are. + + curtailment_cost : float, default 0.0 + Unit cost of curtailment. + + 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 = ('curtailment', 'flex') + yaml_tag = u"!CurtailmentModule" + yaml_dumper = yaml.SafeDumper + yaml_loader = yaml.SafeLoader + + def __init__(self, + modules_to_curtail=None, + initial_step=0, + curtailment_cost=0.0, + normalized_action_bounds=(0, 1), + raise_errors=False): + + super().__init__(raise_errors, + initial_step=initial_step, + normalized_action_bounds=normalized_action_bounds, + provided_energy_name=None, + absorbed_energy_name='curtailment') + + self._modules_to_curtail = modules_to_curtail + self.curtailment_cost = curtailment_cost + + self._curtailment_modules = None + + def reset(self): + pass + + def setup(self, microgrid): + """ + + Parameters + ---------- + microgrid : :class:`pymgrid.Microgrid` + + Returns + ------- + + """ + if self._modules_to_curtail is None: + curtailment_modules = microgrid.modules.fixed.source.to_dict() + + else: + curtailment_modules = [] + + for module_ref in self._modules_to_curtail: + curtailment_modules.extend(self._get_modules_from_ref(microgrid.module, module_ref)) + + self._curtailment_modules = ModuleContainer(curtailment_modules, set_names=False) + + def _get_modules_from_ref(self, modules, ref): + if isinstance(ref, BaseMicrogridModule): # Module + referenced_modules = [ref] + + elif isinstance(ref, tuple): # Name of a module, e.g. ('renewable', 0) + if ref == self.name: + raise NameError('Cannot reference itself.') + + try: + referenced_modules = [modules[ref[0]][ref[1]]] + except (KeyError, IndexError): + raise NameError(f'Module {ref} not found.') + + elif isinstance(ref, str): # Name of a module type, e.g. 'renewable' + try: + referenced_modules = modules[ref] + except KeyError: + raise NameError(f'Module {ref} not found.') + else: + raise TypeError(f"Unrecognized module reference '{ref}'.") + + return referenced_modules + def update(self, external_energy_change, as_source=False, as_sink=False): + assert as_sink + + if not self._curtailment_modules: + raise RuntimeError('Must call RenewableCurtailmentModule.setup before usage!') + pass def _state_dict(self): - pass + return dict() + + @property + def state(self): + return np.array([]) @property def min_obs(self): - pass + return np.array([]) @property def max_obs(self): - pass + return np.array([]) @property def min_act(self): - pass + return -np.inf @property def max_act(self): - pass \ No newline at end of file + return np.inf + + @property + def max_consumption(self): + return self._curtailment_modules + + @property + def is_sink(self): + return True + + @property + def absorption_marginal_cost(self): + return self.curtailment_cost From 45b768eca2a9879f971e4f078a172da5c79be149 Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 14:03:30 -0700 Subject: [PATCH 05/28] write update meth --- .../modules/renewable_curtailment_module.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/pymgrid/modules/renewable_curtailment_module.py b/src/pymgrid/modules/renewable_curtailment_module.py index 8c74c00c..45f984ec 100644 --- a/src/pymgrid/modules/renewable_curtailment_module.py +++ b/src/pymgrid/modules/renewable_curtailment_module.py @@ -117,7 +117,14 @@ def update(self, external_energy_change, as_source=False, as_sink=False): if not self._curtailment_modules: raise RuntimeError('Must call RenewableCurtailmentModule.setup before usage!') - pass + curtailment = min(external_energy_change, self.max_consumption) + info = {'absorbed_energy': curtailment} + reward = -1.0 * self.get_cost(curtailment) + + return reward, False, info + + def get_cost(self, curtailment): + return self.curtailment_cost * curtailment def _state_dict(self): return dict() @@ -136,15 +143,16 @@ def max_obs(self): @property def min_act(self): + # TODO (ahalev) find a better bound return -np.inf @property def max_act(self): - return np.inf + return 0.0 @property def max_consumption(self): - return self._curtailment_modules + return self._curtailment_modules.get_attrs('production').sum().item() @property def is_sink(self): From 1e99eeb482a41fc557db04ce863873be0aeea32c Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 14:04:15 -0700 Subject: [PATCH 06/28] rename dir --- .../{renewable_curtailment_module.py => curtailment_module.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/pymgrid/modules/{renewable_curtailment_module.py => curtailment_module.py} (100%) diff --git a/src/pymgrid/modules/renewable_curtailment_module.py b/src/pymgrid/modules/curtailment_module.py similarity index 100% rename from src/pymgrid/modules/renewable_curtailment_module.py rename to src/pymgrid/modules/curtailment_module.py From 2a38eec527475e28b36e017db04bda506aee58f4 Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 14:04:26 -0700 Subject: [PATCH 07/28] import into modules init --- src/pymgrid/modules/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pymgrid/modules/__init__.py b/src/pymgrid/modules/__init__.py index b262b248..8e284458 100644 --- a/src/pymgrid/modules/__init__.py +++ b/src/pymgrid/modules/__init__.py @@ -1,4 +1,5 @@ from .battery.battery_module import BatteryModule +from .curtailment_module import CurtailmentModule from .genset_module import GensetModule from .grid_module import GridModule from .load_module import LoadModule From e138a81c98ac33a972cb3d430866ab9424171f28 Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 14:13:06 -0700 Subject: [PATCH 08/28] pass container not microgrid --- src/pymgrid/modules/curtailment_module.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pymgrid/modules/curtailment_module.py b/src/pymgrid/modules/curtailment_module.py index 45f984ec..600fa576 100644 --- a/src/pymgrid/modules/curtailment_module.py +++ b/src/pymgrid/modules/curtailment_module.py @@ -66,7 +66,7 @@ def __init__(self, def reset(self): pass - def setup(self, microgrid): + def setup(self, module_container): """ Parameters @@ -78,13 +78,13 @@ def setup(self, microgrid): """ if self._modules_to_curtail is None: - curtailment_modules = microgrid.modules.fixed.source.to_dict() + curtailment_modules = module_container.fixed.source.to_dict() else: curtailment_modules = [] for module_ref in self._modules_to_curtail: - curtailment_modules.extend(self._get_modules_from_ref(microgrid.module, module_ref)) + curtailment_modules.extend(self._get_modules_from_ref(module_container, module_ref)) self._curtailment_modules = ModuleContainer(curtailment_modules, set_names=False) From 3888f52e8014dc2eff7cf4c97bf1143d2abb0ca3 Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 14:13:30 -0700 Subject: [PATCH 09/28] pull container into microgrid --- src/pymgrid/microgrid/microgrid.py | 31 ++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/pymgrid/microgrid/microgrid.py b/src/pymgrid/microgrid/microgrid.py index 4bd30c0f..12542daa 100644 --- a/src/pymgrid/microgrid/microgrid.py +++ b/src/pymgrid/microgrid/microgrid.py @@ -6,7 +6,7 @@ from warnings import warn from pymgrid.microgrid import DEFAULT_HORIZON -from pymgrid.modules import ModuleContainer, UnbalancedEnergyModule +from pymgrid.modules import ModuleContainer, UnbalancedEnergyModule, CurtailmentModule from pymgrid.microgrid.utils.step import MicrogridStep from pymgrid.utils.eq import verbose_eq from pymgrid.utils.logger import ModularLogger @@ -33,7 +33,9 @@ class Microgrid(yaml.YAMLObject): .. note:: The constructor copies modules passed to it. - add_unbalanced_module : bool, default True. + add_curtailment_module : bool, default False + + 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 ``UnbalancedEnergyModule``. @@ -101,14 +103,18 @@ class Microgrid(yaml.YAMLObject): def __init__(self, modules, + add_curtailment_module=False, add_unbalanced_module=True, + curtailment_cost=0.0, loss_load_cost=10., overgeneration_cost=2., reward_shaping_func=None, trajectory_func=None): self._modules = self._get_module_container(modules, + add_curtailment_module, add_unbalanced_module, + curtailment_cost, loss_load_cost, overgeneration_cost) @@ -137,7 +143,13 @@ def _get_unbalanced_energy_module(self, overgeneration_cost=overgeneration_cost ) - def _get_module_container(self, modules, add_unbalanced_module, loss_load_cost, overgeneration_cost): + def _get_module_container(self, + modules, + add_curtailment_module, + add_unbalanced_module, + curtailment_cost, + loss_load_cost, + overgeneration_cost): """ Types of _modules: Fixed source: provides energy to the microgrid. @@ -168,10 +180,21 @@ def _get_module_container(self, modules, add_unbalanced_module, loss_load_cost, if not pd.api.types.is_list_like(modules): raise TypeError("modules must be list-like of modules.") + if add_curtailment_module: + curtailment_module = CurtailmentModule(curtailment_cost=curtailment_cost) + modules.append(curtailment_module) + else: + curtailment_module = None + if add_unbalanced_module: modules.append(self._get_unbalanced_energy_module(loss_load_cost, overgeneration_cost)) - return ModuleContainer(modules) + container = ModuleContainer(modules) + + if curtailment_module: + curtailment_module.setup(container) + + return container def _check_trajectory_func(self, trajectory_func): if trajectory_func is None: From 99c5f48182b2343fd2bca4b34a5215a9514a3dea Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 15:38:55 -0700 Subject: [PATCH 10/28] add curtailment params check to curtailment module --- src/pymgrid/convert/to_nonmodular_ops.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/pymgrid/convert/to_nonmodular_ops.py b/src/pymgrid/convert/to_nonmodular_ops.py index 9cb8603f..f64173e4 100644 --- a/src/pymgrid/convert/to_nonmodular_ops.py +++ b/src/pymgrid/convert/to_nonmodular_ops.py @@ -1,4 +1,6 @@ -from pymgrid.modules import LoadModule, RenewableModule, BatteryModule, GridModule, GensetModule, UnbalancedEnergyModule +from pymgrid.modules import ( + LoadModule, RenewableModule, BatteryModule, GridModule, GensetModule, UnbalancedEnergyModule, CurtailmentModule +) from copy import deepcopy import pandas as pd import numpy as np @@ -26,7 +28,7 @@ def get_empty_params(): def check_viability(modular): - classes = LoadModule, RenewableModule, BatteryModule, GridModule, GensetModule, UnbalancedEnergyModule + classes = LoadModule, RenewableModule, BatteryModule, GridModule, GensetModule, UnbalancedEnergyModule, CurtailmentModule classes_str = '\n'.join([str(x) for x in classes]) n_modules_by_cls = dict(zip(classes, [0]*len(classes))) @@ -72,6 +74,8 @@ def add_params_from_module(module, params_dict): add_genset_params(module, params_dict) elif isinstance(module, UnbalancedEnergyModule): add_unbalanced_energy_params(module, params_dict) + elif isinstance(module, CurtailmentModule): + check_curtailment_params(module) else: raise ValueError(f'Cannot parse module {module}.') @@ -178,6 +182,11 @@ def add_unbalanced_energy_params(unbalanced_energy_module, params_dict): _add_to_df_cost(params_dict, 'overgeneration') +def check_curtailment_params(curtailment_module): + if curtailment_module.curtailment_cost != 0: + warn(f'Curtailment cost {curtailment_module.curtailment_cost} will be ignored in conversion to nonmodular.') + + def _add_empty(params_dict, subdict_name, *keys): params_dict[subdict_name].update({k: [] for k in keys}) From bea62b77a1c2e788614ddf6f382ea21ffe7f940e Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 15:39:17 -0700 Subject: [PATCH 11/28] fix consumption computation --- src/pymgrid/modules/curtailment_module.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/pymgrid/modules/curtailment_module.py b/src/pymgrid/modules/curtailment_module.py index 600fa576..7c29360a 100644 --- a/src/pymgrid/modules/curtailment_module.py +++ b/src/pymgrid/modules/curtailment_module.py @@ -62,6 +62,7 @@ def __init__(self, self.curtailment_cost = curtailment_cost self._curtailment_modules = None + self._next_max_consumption = None def reset(self): pass @@ -87,6 +88,7 @@ def setup(self, module_container): curtailment_modules.extend(self._get_modules_from_ref(module_container, module_ref)) self._curtailment_modules = ModuleContainer(curtailment_modules, set_names=False) + self._update_max_consumption() def _get_modules_from_ref(self, modules, ref): if isinstance(ref, BaseMicrogridModule): # Module @@ -121,11 +123,22 @@ def update(self, external_energy_change, as_source=False, as_sink=False): info = {'absorbed_energy': curtailment} reward = -1.0 * self.get_cost(curtailment) - return reward, False, info + done = self._update_max_consumption() + + return reward, done, info def get_cost(self, curtailment): return self.curtailment_cost * curtailment + def _update_max_consumption(self): + try: + self._next_max_consumption = self._curtailment_modules.get_attrs('max_production').sum().item() + return False + except IndexError: + assert self._current_step == self._curtailment_modules.get_attrs('final_step', unique=True).item() - 1 + self._next_max_consumption = 0.0 + return True + def _state_dict(self): return dict() @@ -152,7 +165,13 @@ def max_act(self): @property def max_consumption(self): - return self._curtailment_modules.get_attrs('production').sum().item() + module_current_step = self._curtailment_modules.get_attrs('current_step', unique=True).item() + if not self._current_step == module_current_step - 1: + raise RuntimeError(f'self.current_step={self._current_step} is not one less than curtailment module current' + f'step ({module_current_step}). This module should only be called after curtailment' + f'modules.') + + return self._next_max_consumption @property def is_sink(self): From eaa178f642d99b629c03e31a7215b899420b1bfb Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 15:39:28 -0700 Subject: [PATCH 12/28] bug fix in setup --- src/pymgrid/modules/curtailment_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymgrid/modules/curtailment_module.py b/src/pymgrid/modules/curtailment_module.py index 7c29360a..09b21b9a 100644 --- a/src/pymgrid/modules/curtailment_module.py +++ b/src/pymgrid/modules/curtailment_module.py @@ -79,7 +79,7 @@ def setup(self, module_container): """ if self._modules_to_curtail is None: - curtailment_modules = module_container.fixed.source.to_dict() + curtailment_modules = module_container.fixed.sources.to_list() else: curtailment_modules = [] From f7381543c1766ab1fd33778b5ad02d0ba6a0e786 Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 15:39:35 -0700 Subject: [PATCH 13/28] add repr --- src/pymgrid/modules/curtailment_module.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pymgrid/modules/curtailment_module.py b/src/pymgrid/modules/curtailment_module.py index 09b21b9a..b007dbbd 100644 --- a/src/pymgrid/modules/curtailment_module.py +++ b/src/pymgrid/modules/curtailment_module.py @@ -180,3 +180,7 @@ def is_sink(self): @property def absorption_marginal_cost(self): return self.curtailment_cost + + def __repr__(self): + return f'CurtailmentModule(' \ + f'modules={self._curtailment_modules.get_attrs("name").squeeze(axis=0).values.tolist()})' From bc49a6721554070e054a6faa73f84d8b59ead177 Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 15:40:52 -0700 Subject: [PATCH 14/28] fix excess pv tests --- tests/microgrid/test_microgrid.py | 45 +++++++++++++++---------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/tests/microgrid/test_microgrid.py b/tests/microgrid/test_microgrid.py index 3b168e4c..2e07fcd4 100644 --- a/tests/microgrid/test_microgrid.py +++ b/tests/microgrid/test_microgrid.py @@ -310,11 +310,11 @@ def test_set_initial_step(self): self.assertEqual(initial_step, 1) -class TestMicrogridLoadPV(TestCase): +class TestMicrogridLoadPVWithCurtailment(TestCase): def setUp(self): self.load_ts, self.pv_ts = self.set_ts() self.microgrid, self.n_loads, self.n_pvs = self.set_microgrid() - self.n_modules = 1 + self.n_loads + self.n_pvs + self.n_modules = 2 + self.n_loads + self.n_pvs def set_ts(self): ts = 10 * np.random.rand(100) @@ -323,7 +323,7 @@ def set_ts(self): def set_microgrid(self): load = LoadModule(time_series=self.load_ts, raise_errors=True) pv = RenewableModule(time_series=self.pv_ts, raise_errors=True) - return Microgrid([load, pv]), 1, 1 + return Microgrid([load, pv], add_curtailment_module=True), 1, 1 def test_populated_correctly(self): self.assertTrue(hasattr(self.microgrid.modules, 'load')) @@ -352,7 +352,7 @@ def test_sample_action(self): def test_sample_action_with_flex(self): sampled_action = self.microgrid.sample_action(sample_flex_modules=True) - self.assertEqual(len(sampled_action), 1) + self.assertEqual(len(sampled_action), 2) self.assertIn('balancing', sampled_action) def test_state_dict(self): @@ -390,12 +390,10 @@ def check_step(self, microgrid, step_number=0): obs, reward, done, info = microgrid.run(control) loss_load = self.load_ts[step_number]-self.pv_ts[step_number] - overgeneration = -1 * loss_load loss_load_cost = self.microgrid.modules.balancing[0].loss_load_cost * max(loss_load, 0) - overgeneration_cost = self.microgrid.modules.balancing[0].overgeneration_cost * max(overgeneration, 0) - self.assertEqual(loss_load_cost + overgeneration_cost, -1*reward) + self.assertEqual(loss_load_cost, -1*reward) self.assertEqual(len(microgrid.log), step_number + 1) self.assertTrue(all(module in microgrid.log for module in microgrid.modules.names())) @@ -418,13 +416,13 @@ def check_step(self, microgrid, step_number=0): self.assertEqual(log_entry('load', 'load_met'), load_met) self.assertEqual(log_entry('renewable', 'renewable_current'), self.pv_ts[step_number]) - self.assertEqual(log_entry('renewable', 'renewable_used'), load_met) + self.assertEqual(log_entry('renewable', 'renewable_used')-log_entry('curtailment', 'curtailment'), load_met) self.assertEqual(log_entry('balancing', 'loss_load'), loss_load) self.assertEqual(log_entry('balance', 'reward'), -1 * loss_load_cost) - self.assertEqual(log_entry('balance', 'overall_provided_to_microgrid'), self.load_ts[step_number]) - self.assertEqual(log_entry('balance', 'overall_absorbed_from_microgrid'), self.load_ts[step_number]) + self.assertEqual(log_entry('balance', 'overall_provided_to_microgrid'), self.pv_ts[step_number]) + self.assertEqual(log_entry('balance', 'overall_absorbed_from_microgrid'), self.pv_ts[step_number]) self.assertEqual(log_entry('balance', 'fixed_provided_to_microgrid'), self.pv_ts[step_number]) self.assertEqual(log_entry('balance', 'fixed_absorbed_from_microgrid'), self.load_ts[step_number]) self.assertEqual(log_entry('balance', 'controllable_absorbed_from_microgrid'), 0.0) @@ -443,7 +441,7 @@ def test_run_n_steps(self): microgrid = self.check_step(microgrid=microgrid, step_number=step) -class TestMicrogridLoadExcessPV(TestMicrogridLoadPV): +class TestMicrogridLoadExcessPVWithCurtailment(TestMicrogridLoadPVWithCurtailment): # Same as above but pv is greater than load. def set_ts(self): load_ts = 10*np.random.rand(100) @@ -451,7 +449,7 @@ def set_ts(self): return load_ts, pv_ts -class TestMicrogridPVExcessLoad(TestMicrogridLoadPV): +class TestMicrogridPVExcessLoadWithCurtailment(TestMicrogridLoadPVWithCurtailment): # Load greater than PV. def set_ts(self): pv_ts = 10 * np.random.rand(100) @@ -459,7 +457,7 @@ def set_ts(self): return load_ts, pv_ts -class TestMicrogridTwoLoads(TestMicrogridLoadPV): +class TestMicrogridTwoLoadsWithCurtailment(TestMicrogridLoadPVWithCurtailment): def set_microgrid(self): load_1_ts = self.load_ts*(1-np.random.rand(*self.load_ts.shape)) load_2_ts = self.load_ts - load_1_ts @@ -470,10 +468,10 @@ def set_microgrid(self): load_1 = LoadModule(time_series=load_1_ts, raise_errors=True) load_2 = LoadModule(time_series=load_2_ts, raise_errors=True) pv = RenewableModule(time_series=self.pv_ts, raise_errors=True) - return Microgrid([load_1, load_2, pv]), 2, 1 + return Microgrid([load_1, load_2, pv], add_curtailment_module=True), 2, 1 -class TestMicrogridTwoPV(TestMicrogridLoadPV): +class TestMicrogridTwoPVWithCurtailment(TestMicrogridLoadPVWithCurtailment): def set_microgrid(self): pv_1_ts = self.pv_ts*(1-np.random.rand(*self.pv_ts.shape)) pv_2_ts = self.pv_ts - pv_1_ts @@ -484,10 +482,10 @@ def set_microgrid(self): load = LoadModule(time_series=self.load_ts, raise_errors=True) pv_1 = RenewableModule(time_series=pv_1_ts, raise_errors=True) pv_2 = RenewableModule(time_series=pv_2_ts) - return Microgrid([load, pv_1, pv_2]), 1, 2 + return Microgrid([load, pv_1, pv_2], add_curtailment_module=True), 1, 2 -class TestMicrogridTwoEach(TestMicrogridLoadPV): +class TestMicrogridTwoEachWithCurtailment(TestMicrogridLoadPVWithCurtailment): def set_microgrid(self): load_1_ts = self.load_ts*(1-np.random.rand(*self.load_ts.shape)) load_2_ts = self.load_ts - load_1_ts @@ -505,10 +503,10 @@ def set_microgrid(self): pv_1 = RenewableModule(time_series=pv_1_ts, raise_errors=True) pv_2 = RenewableModule(time_series=pv_2_ts) - return Microgrid([load_1, load_2, pv_1, pv_2]), 2, 2 + return Microgrid([load_1, load_2, pv_1, pv_2], add_curtailment_module=True), 2, 2 -class TestMicrogridManyEach(TestMicrogridLoadPV): +class TestMicrogridManyEachWithCurtailment(TestMicrogridLoadPVWithCurtailment): def set_microgrid(self): n_loads = np.random.randint(3, 10) n_pvs = np.random.randint(3, 10) @@ -534,27 +532,28 @@ def set_microgrid(self): load_modules = [LoadModule(time_series=ts) for ts in load_ts] pv_modules = [RenewableModule(time_series=ts) for ts in pv_ts] - return Microgrid([*load_modules, *pv_modules]), n_loads, n_pvs + return Microgrid([*load_modules, *pv_modules], add_curtailment_module=True), n_loads, n_pvs -class TestMicrogridManyEachExcessPV(TestMicrogridManyEach): +class TestMicrogridManyEachExcessPV(TestMicrogridManyEachWithCurtailment): def set_ts(self): load_ts = 10*np.random.rand(100) pv_ts = load_ts + 5*np.random.rand(100) return load_ts, pv_ts -class TestMicrogridManyEachExcessLoad(TestMicrogridManyEach): +class TestMicrogridManyEachExcessLoad(TestMicrogridManyEachWithCurtailment): def set_ts(self): pv_ts = 10*np.random.rand(100) load_ts = pv_ts + 5*np.random.rand(100) return load_ts, pv_ts -class TestMicrogridRewardShaping(TestMicrogridLoadPV): +class TestMicrogridRewardShapingWithCurtailment(TestMicrogridLoadPVWithCurtailment): def set_microgrid(self): original_microgrid, n_loads, n_pvs = super().set_microgrid() new_microgrid = Microgrid(original_microgrid.modules.to_tuples(), + add_curtailment_module=True, add_unbalanced_module=False, reward_shaping_func=self.reward_shaping_func) From 082b40cf08d5f81816a7574196409a153f57d81e Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 16:36:06 -0700 Subject: [PATCH 15/28] fix tests for excess load --- tests/microgrid/test_microgrid.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/microgrid/test_microgrid.py b/tests/microgrid/test_microgrid.py index 2e07fcd4..21bedfe9 100644 --- a/tests/microgrid/test_microgrid.py +++ b/tests/microgrid/test_microgrid.py @@ -421,8 +421,11 @@ def check_step(self, microgrid, step_number=0): self.assertEqual(log_entry('balancing', 'loss_load'), loss_load) self.assertEqual(log_entry('balance', 'reward'), -1 * loss_load_cost) - self.assertEqual(log_entry('balance', 'overall_provided_to_microgrid'), self.pv_ts[step_number]) - self.assertEqual(log_entry('balance', 'overall_absorbed_from_microgrid'), self.pv_ts[step_number]) + + overall_provided_absorbed = max(self.pv_ts[step_number], self.load_ts[step_number]) + self.assertEqual(log_entry('balance', 'overall_provided_to_microgrid'), overall_provided_absorbed) + self.assertEqual(log_entry('balance', 'overall_absorbed_from_microgrid'), overall_provided_absorbed) + self.assertEqual(log_entry('balance', 'fixed_provided_to_microgrid'), self.pv_ts[step_number]) self.assertEqual(log_entry('balance', 'fixed_absorbed_from_microgrid'), self.load_ts[step_number]) self.assertEqual(log_entry('balance', 'controllable_absorbed_from_microgrid'), 0.0) From cff34a8cc6940e549845ea9fabedc0f63a52c8f1 Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 16:37:12 -0700 Subject: [PATCH 16/28] formatting --- tests/microgrid/test_microgrid.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/microgrid/test_microgrid.py b/tests/microgrid/test_microgrid.py index 21bedfe9..d2c1a314 100644 --- a/tests/microgrid/test_microgrid.py +++ b/tests/microgrid/test_microgrid.py @@ -420,12 +420,11 @@ def check_step(self, microgrid, step_number=0): self.assertEqual(log_entry('balancing', 'loss_load'), loss_load) - self.assertEqual(log_entry('balance', 'reward'), -1 * loss_load_cost) - overall_provided_absorbed = max(self.pv_ts[step_number], self.load_ts[step_number]) self.assertEqual(log_entry('balance', 'overall_provided_to_microgrid'), overall_provided_absorbed) self.assertEqual(log_entry('balance', 'overall_absorbed_from_microgrid'), overall_provided_absorbed) + self.assertEqual(log_entry('balance', 'reward'), -1 * loss_load_cost) self.assertEqual(log_entry('balance', 'fixed_provided_to_microgrid'), self.pv_ts[step_number]) self.assertEqual(log_entry('balance', 'fixed_absorbed_from_microgrid'), self.load_ts[step_number]) self.assertEqual(log_entry('balance', 'controllable_absorbed_from_microgrid'), 0.0) From 199c32e5e06461cb72ec84153f1ed4f49e8ce96b Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 16:47:48 -0700 Subject: [PATCH 17/28] whitespace --- tests/helpers/test_case.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/helpers/test_case.py b/tests/helpers/test_case.py index b7520e30..cf9f1ab3 100644 --- a/tests/helpers/test_case.py +++ b/tests/helpers/test_case.py @@ -4,6 +4,7 @@ from unittest.util import safe_repr + class TestCase(unittest.TestCase): def assertEqual(self, first, second, msg=None) -> None: try: From 9e29bffc7a2a930832928430736c5604ab203fae Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 16:48:02 -0700 Subject: [PATCH 18/28] check curtailment in check_step --- tests/microgrid/test_microgrid.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/microgrid/test_microgrid.py b/tests/microgrid/test_microgrid.py index d2c1a314..4b4080b9 100644 --- a/tests/microgrid/test_microgrid.py +++ b/tests/microgrid/test_microgrid.py @@ -399,8 +399,8 @@ def check_step(self, microgrid, step_number=0): self.assertTrue(all(module in microgrid.log for module in microgrid.modules.names())) load_met = min(self.load_ts[step_number], self.pv_ts[step_number]) - loss_load = max(self.load_ts[step_number] - load_met, 0) - + loss_load = max(self.load_ts[step_number] - self.pv_ts[step_number], 0) + curtailment = max(self.pv_ts[step_number] - self.load_ts[step_number], 0) # Checking the log populated correctly. log_row = microgrid.log.iloc[step_number] @@ -418,6 +418,7 @@ def check_step(self, microgrid, step_number=0): self.assertEqual(log_entry('renewable', 'renewable_current'), self.pv_ts[step_number]) self.assertEqual(log_entry('renewable', 'renewable_used')-log_entry('curtailment', 'curtailment'), load_met) + self.assertEqual(log_entry('curtailment', 'curtailment'), curtailment) self.assertEqual(log_entry('balancing', 'loss_load'), loss_load) overall_provided_absorbed = max(self.pv_ts[step_number], self.load_ts[step_number]) From c3ea460595a689d5f983a97047a1278eef4c440d Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 16:48:29 -0700 Subject: [PATCH 19/28] add check for curtailment module in populated_correctly --- tests/microgrid/test_microgrid.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/microgrid/test_microgrid.py b/tests/microgrid/test_microgrid.py index 4b4080b9..1ec48557 100644 --- a/tests/microgrid/test_microgrid.py +++ b/tests/microgrid/test_microgrid.py @@ -328,6 +328,9 @@ def set_microgrid(self): def test_populated_correctly(self): self.assertTrue(hasattr(self.microgrid.modules, 'load')) self.assertTrue(hasattr(self.microgrid.modules, 'renewable')) + self.assertTrue(hasattr(self.microgrid.modules, 'curtailment')) + self.assertEqual(len(self.microgrid.modules.curtailment), 1) + self.assertEqual(len(self.microgrid.modules), self.n_modules) # load, pv, unbalanced def test_current_load_correct(self): From cbf64628a8a34ee39e036b8fe1514cd7e3bb6b9b Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 17:40:23 -0700 Subject: [PATCH 20/28] allow module_container passthrough to reset --- src/pymgrid/modules/curtailment_module.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pymgrid/modules/curtailment_module.py b/src/pymgrid/modules/curtailment_module.py index b007dbbd..0803a2ac 100644 --- a/src/pymgrid/modules/curtailment_module.py +++ b/src/pymgrid/modules/curtailment_module.py @@ -64,8 +64,11 @@ def __init__(self, self._curtailment_modules = None self._next_max_consumption = None - def reset(self): - pass + def reset(self, module_container=None): + if module_container: + self.setup(module_container) + + return super().reset() def setup(self, module_container): """ From ee62ecb0b8496ec307a25705ff10fb141f1eff78 Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 17:40:40 -0700 Subject: [PATCH 21/28] pass containers through to curtailment module --- src/pymgrid/microgrid/microgrid.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pymgrid/microgrid/microgrid.py b/src/pymgrid/microgrid/microgrid.py index 12542daa..0c210b46 100644 --- a/src/pymgrid/microgrid/microgrid.py +++ b/src/pymgrid/microgrid/microgrid.py @@ -236,8 +236,11 @@ def reset(self): Observations from resetting the modules as well as the flushed balance log. """ self._set_trajectory() + def reset_args(module): return (self.modules,) if isinstance(module, CurtailmentModule) else () + return { - **{name: [module.reset() for module in module_list] for name, module_list in self.modules.iterdict()}, + **{name: [module.reset(*reset_args(module)) for module in module_list] + for name, module_list in self.modules.iterdict()}, **{"balance": self._balance_logger.flush(), "other": self._microgrid_logger.flush()} } From 76a5865ed5e3f6024646f3a8385378bfd08157af Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 17:40:50 -0700 Subject: [PATCH 22/28] update docstring --- src/pymgrid/modules/curtailment_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymgrid/modules/curtailment_module.py b/src/pymgrid/modules/curtailment_module.py index 0803a2ac..37541a7f 100644 --- a/src/pymgrid/modules/curtailment_module.py +++ b/src/pymgrid/modules/curtailment_module.py @@ -75,7 +75,7 @@ def setup(self, module_container): Parameters ---------- - microgrid : :class:`pymgrid.Microgrid` + module_container : :class:`.ModuleContainer` Returns ------- From 51355e4f3070b2ed34f28be03a6984f80ef3d97d Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 17:40:57 -0700 Subject: [PATCH 23/28] update info --- src/pymgrid/modules/curtailment_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymgrid/modules/curtailment_module.py b/src/pymgrid/modules/curtailment_module.py index 37541a7f..d77a19b6 100644 --- a/src/pymgrid/modules/curtailment_module.py +++ b/src/pymgrid/modules/curtailment_module.py @@ -123,7 +123,7 @@ def update(self, external_energy_change, as_source=False, as_sink=False): raise RuntimeError('Must call RenewableCurtailmentModule.setup before usage!') curtailment = min(external_energy_change, self.max_consumption) - info = {'absorbed_energy': curtailment} + info = {'absorbed_energy': curtailment, 'net_renewable_usage': self.max_consumption-curtailment} reward = -1.0 * self.get_cost(curtailment) done = self._update_max_consumption() From 9d36a1993d6abfd1a7dbbabe04164840fdd1f892 Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 17:41:13 -0700 Subject: [PATCH 24/28] fix mpc tests --- tests/control/test_mpc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/control/test_mpc.py b/tests/control/test_mpc.py index d92d1adc..142ca83a 100644 --- a/tests/control/test_mpc.py +++ b/tests/control/test_mpc.py @@ -29,7 +29,7 @@ def test_run_with_load_pv_battery_grid(self): self.assertEqual(mpc_output.shape[0], max_steps) self.assertEqual(mpc_output[("grid", 0, "grid_import")].values + mpc_output[("battery", 0, "discharge_amount")].values + - mpc_output[("renewable", 0, "renewable_used")].values, + mpc_output[("curtailment", 0, "net_renewable_usage")].values, [load_const] * mpc_output.shape[0] ) @@ -128,7 +128,7 @@ def test_run_with_load_pv_battery_grid_different_names(self): self.assertEqual(mpc_output[("load_with_name", 0, "load_met")].values, [load_const]*mpc_output.shape[0]) self.assertEqual(mpc_output[("grid", 0, "grid_import")].values + mpc_output[("battery", 0, "discharge_amount")].values + - mpc_output[("pv_with_name", 0, "renewable_used")].values, + mpc_output[("curtailment", 0, "net_renewable_usage")].values, [load_const] * mpc_output.shape[0] ) self.assertEqual(mpc_output[("load_with_name", 0, "load_met")].values, [load_const]*mpc_output.shape[0]) From 8a52cf03f2c4109c5e8826ac9499b4c4b4f222c0 Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 17:41:53 -0700 Subject: [PATCH 25/28] add curtailment module to microgrid in tests --- tests/helpers/modular_microgrid.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/helpers/modular_microgrid.py b/tests/helpers/modular_microgrid.py index 542d9e61..1f1b22d7 100644 --- a/tests/helpers/modular_microgrid.py +++ b/tests/helpers/modular_microgrid.py @@ -15,6 +15,7 @@ def get_modular_microgrid(remove_modules=(), retain_only=None, additional_modules=None, add_unbalanced_module=True, + add_curtailment_module=True, timeseries_length=100, modules_only=False, normalized_action_bounds=(0, 1)): @@ -63,4 +64,6 @@ def get_modular_microgrid(remove_modules=(), if modules_only: return modules - return Microgrid(modules, add_unbalanced_module=add_unbalanced_module) + return Microgrid(modules, + add_unbalanced_module=add_unbalanced_module, + add_curtailment_module=add_curtailment_module) From 850f6c3f359fc8bf8aa5d411e3243cdd4267a8ff Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 17:42:12 -0700 Subject: [PATCH 26/28] fix renewable tests --- .../module_tests/test_renewable_module.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/tests/microgrid/modules/module_tests/test_renewable_module.py b/tests/microgrid/modules/module_tests/test_renewable_module.py index 0bff0e10..631e59d9 100644 --- a/tests/microgrid/modules/module_tests/test_renewable_module.py +++ b/tests/microgrid/modules/module_tests/test_renewable_module.py @@ -1,3 +1,4 @@ +import numpy as np from pymgrid.modules import RenewableModule @@ -11,7 +12,7 @@ class TestRenewableModuleNoForecasting(TestTimeseriesModuleNoForecasting): __test__ = True - action_space_dim = 1 + action_space_dim = 0 def get_module(self): return RenewableModule(self.module_time_series) @@ -24,20 +25,16 @@ def test_step(self): renewable_module = self.get_module() self.assertEqual(renewable_module.current_renewable, self.time_series[0]) - unnormalized_action = 1 - action = renewable_module.to_normalized(unnormalized_action, act=True) - obs, reward, done, info = renewable_module.step(action) + obs, reward, done, info = renewable_module.step(np.array([])) obs = renewable_module.from_normalized(obs, obs=True) self.assertEqual(obs, self.time_series[1]) self.assertEqual(reward, 0) self.assertFalse(done) - self.assertEqual(info["provided_energy"], unnormalized_action) - self.assertEqual(info["curtailment"], 0) - + self.assertEqual(info["provided_energy"], self.time_series[0]) class TestRenewableModuleForecasting(TestTimeseriesModuleForecasting): __test__ = True - action_space_dim = 1 + action_space_dim = 0 def get_module(self): return RenewableModule(self.module_time_series, forecaster="oracle", forecast_horizon=self.forecast_horizon) @@ -46,15 +43,13 @@ def test_step(self): renewable_module = self.get_module() self.assertEqual(renewable_module.current_renewable, self.time_series[0]) - unnormalized_action = 1 - action = renewable_module.to_normalized(unnormalized_action, act=True) + action = renewable_module.to_normalized(np.array([]), act=True) obs, reward, done, info = renewable_module.step(action) obs = renewable_module.from_normalized(obs, obs=True) self.assertEqual(obs, self.time_series[1:self.forecast_horizon+2]) self.assertEqual(reward, 0) self.assertFalse(done) - self.assertEqual(info["provided_energy"], unnormalized_action) - self.assertEqual(info["curtailment"], 0) + self.assertEqual(info["provided_energy"], self.time_series[0]) class TestRenewableModuleForecastingNegativeVals(TestTimeSeriesModuleForecastingNegativeVals, From fe40a7a9cb5bbbcb84ffb4a87b5732bb6375200b Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 17:42:23 -0700 Subject: [PATCH 27/28] fix microgrid test --- tests/microgrid/test_microgrid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/microgrid/test_microgrid.py b/tests/microgrid/test_microgrid.py index 1ec48557..e16fa824 100644 --- a/tests/microgrid/test_microgrid.py +++ b/tests/microgrid/test_microgrid.py @@ -559,7 +559,7 @@ class TestMicrogridRewardShapingWithCurtailment(TestMicrogridLoadPVWithCurtailme def set_microgrid(self): original_microgrid, n_loads, n_pvs = super().set_microgrid() new_microgrid = Microgrid(original_microgrid.modules.to_tuples(), - add_curtailment_module=True, + add_curtailment_module=False, add_unbalanced_module=False, reward_shaping_func=self.reward_shaping_func) From fae2195ce3a8d6ca2facabac0c3772175a4b401d Mon Sep 17 00:00:00 2001 From: ahalev Date: Thu, 18 May 2023 17:43:58 -0700 Subject: [PATCH 28/28] fix renewable serialization test --- tests/microgrid/serialize/test_microgrid_serialization.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/microgrid/serialize/test_microgrid_serialization.py b/tests/microgrid/serialize/test_microgrid_serialization.py index 4d6a961a..52636ef9 100644 --- a/tests/microgrid/serialize/test_microgrid_serialization.py +++ b/tests/microgrid/serialize/test_microgrid_serialization.py @@ -16,7 +16,8 @@ def test_serialize_no_modules(self): def test_serialize_with_renewable(self): microgrid = get_modular_microgrid(remove_modules=["genset", "battery", "load", "grid"], - add_unbalanced_module=False) + add_unbalanced_module=False, + add_curtailment_module=False) self.assertEqual(len(microgrid.modules), 1) self.assertEqual(microgrid, Microgrid.load(microgrid.dump()))