Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/pymgrid/convert/to_nonmodular_ops.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)))

Expand Down Expand Up @@ -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}.')

Expand Down Expand Up @@ -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})

Expand Down
36 changes: 31 additions & 5 deletions src/pymgrid/microgrid/microgrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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``.
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -213,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()}
}
Expand Down
1 change: 1 addition & 0 deletions src/pymgrid/modules/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
189 changes: 189 additions & 0 deletions src/pymgrid/modules/curtailment_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import numpy as np

import yaml

from pymgrid.modules.base import BaseMicrogridModule, BaseTimeSeriesMicrogridModule
from pymgrid.modules.module_container import ModuleContainer


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
self._next_max_consumption = None

def reset(self, module_container=None):
if module_container:
self.setup(module_container)

return super().reset()

def setup(self, module_container):
"""

Parameters
----------
module_container : :class:`.ModuleContainer`

Returns
-------

"""
if self._modules_to_curtail is None:
curtailment_modules = module_container.fixed.sources.to_list()

else:
curtailment_modules = []

for module_ref in self._modules_to_curtail:
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
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!')

curtailment = min(external_energy_change, self.max_consumption)
info = {'absorbed_energy': curtailment, 'net_renewable_usage': self.max_consumption-curtailment}
reward = -1.0 * self.get_cost(curtailment)

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()

@property
def state(self):
return np.array([])

@property
def min_obs(self):
return np.array([])

@property
def max_obs(self):
return np.array([])

@property
def min_act(self):
# TODO (ahalev) find a better bound
return -np.inf

@property
def max_act(self):
return 0.0

@property
def max_consumption(self):
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):
return True

@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()})'
2 changes: 1 addition & 1 deletion src/pymgrid/modules/unbalanced_energy_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions tests/control/test_mpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
)

Expand Down Expand Up @@ -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])
5 changes: 4 additions & 1 deletion tests/helpers/modular_microgrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)):
Expand Down Expand Up @@ -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)
1 change: 1 addition & 0 deletions tests/helpers/test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from unittest.util import safe_repr


class TestCase(unittest.TestCase):
def assertEqual(self, first, second, msg=None) -> None:
try:
Expand Down
Loading