diff --git a/setup.py b/setup.py index 6cb499cb..7b3618eb 100644 --- a/setup.py +++ b/setup.py @@ -16,6 +16,7 @@ EXTRAS["genset_mpc"] = ["Mosek", "cvxopt"] EXTRAS["dev"] = [ "pytest", + "pytest-subtests", "flake8", "sphinx", "pydata_sphinx_theme", diff --git a/src/pymgrid/convert/to_nonmodular_ops.py b/src/pymgrid/convert/to_nonmodular_ops.py index 0d171d64..9cb8603f 100644 --- a/src/pymgrid/convert/to_nonmodular_ops.py +++ b/src/pymgrid/convert/to_nonmodular_ops.py @@ -91,7 +91,7 @@ def add_pv_params(pv_module, params_dict): _add_to_architecture(params_dict, 'PV') _add_to_parameters(params_dict, PV_rated_power=pv_module.max_act) _add_to_df_actions(params_dict, 'pv_consummed','pv_curtailed','pv') - _add_to_df_status(params_dict, pv=[pv_module.current_renewable.item()]) + _add_to_df_status(params_dict, pv=[pv_module.current_renewable]) _add_to_df_actual_generation(params_dict, 'pv_consummed','pv_curtailed') _add_to_control_dict(params_dict, 'pv_consummed', 'pv_curtailed', 'pv') diff --git a/src/pymgrid/envs/discrete/discrete.py b/src/pymgrid/envs/discrete/discrete.py index 948decd8..c7056529 100644 --- a/src/pymgrid/envs/discrete/discrete.py +++ b/src/pymgrid/envs/discrete/discrete.py @@ -43,7 +43,6 @@ def __init__(self, overgeneration_cost=overgeneration_cost) self.action_space, self.actions_list = self._get_action_space() - self.log_dict = ModularLogger() def _get_action_space(self): """ @@ -55,19 +54,19 @@ def _get_action_space(self): :return: """ # n_actions = 2**(self.modules.fixed.) - fixed_sources = [(module.name, module.action_space['unnormalized'].shape[0], n_actions) - for module in self.modules.fixed.sources.iterlist() - for n_actions in range(module.action_space['unnormalized'].shape[0])] + controllable_sources = [(module.name, module.action_space.shape[0], n_actions) + for module in self.modules.controllable.sources.iterlist() + for n_actions in range(module.action_space.shape[0])] - fixed_sources.extend([(module.name, module.action_space['unnormalized'].shape[0], n_actions) - for module in self.modules.fixed.source_and_sinks.iterlist() - for n_actions in range(module.action_space['unnormalized'].shape[0])]) + controllable_sources.extend([(module.name, module.action_space.shape[0], n_actions) + for module in self.modules.controllable.source_and_sinks.iterlist() + for n_actions in range(module.action_space.shape[0])]) - priority_lists = list(permutations(fixed_sources)) + priority_lists = list(permutations(controllable_sources)) n_actions = len(priority_lists) if n_actions > 1000: - warn(f'Microgrid with {len(fixed_sources)} fixed source modules defines large action space with ' + warn(f'Microgrid with {len(controllable_sources)} fixed source modules defines large action space with ' f'{n_actions} elements.') space = Discrete(n_actions) @@ -80,10 +79,6 @@ def _get_action(self, action_num): action = self.get_empty_action() loads, total_load = self._get_load() - for load_module, load in loads.items(): - module_name, module_num = load_module - action[module_name][module_num] = -1.0 * load - renewable = self._get_renewable() assert total_load >= 0 and renewable >= 0 @@ -147,10 +142,14 @@ def _get_action(self, action_num): # If we have, e.g. a genset (with two actions) action[module_name][element_number] = np.array(action[module_name][element_number]) + bad_keys = [k for k, v in action.items() if v is None] + if len(bad_keys): + raise RuntimeError(f'None values found in action, corresponding to keys\n\t{bad_keys}') + return action def step(self, action): - self.log_dict.log(action=action) + self._microgrid_logger.log(action=action) microgrid_action = self._get_action(action) return super().step(microgrid_action, normalized=False) @@ -158,7 +157,7 @@ def _get_load(self): loads = dict() total_load = 0.0 for fixed_sink in self.fixed.sinks.iterlist(): - loads[fixed_sink.name] = fixed_sink.max_consumption.item() + loads[fixed_sink.name] = fixed_sink.max_consumption total_load += fixed_sink.max_consumption return loads, total_load diff --git a/src/pymgrid/microgrid/microgrid.py b/src/pymgrid/microgrid/microgrid.py index 162dc694..22209f01 100644 --- a/src/pymgrid/microgrid/microgrid.py +++ b/src/pymgrid/microgrid/microgrid.py @@ -93,6 +93,7 @@ def __init__(self, overgeneration_cost) self._balance_logger = ModularLogger() + self._microgrid_logger = ModularLogger() # log additional information. def _get_unbalanced_energy_module(self, loss_load_cost, @@ -150,7 +151,8 @@ def reset(self): """ return { **{name: [module.reset() for module in module_list] for name, module_list in self.modules.iterdict()}, - **{"balance": self._balance_logger.flush()} + **{"balance": self._balance_logger.flush(), + "other": self._microgrid_logger.flush()} } def run(self, control, normalized=True): @@ -181,11 +183,14 @@ def run(self, control, normalized=True): control_copy = control.copy() microgrid_step = MicrogridStep() - for name, modules in self.static.iterdict(): + for name, modules in self.fixed.iterdict(): for module in modules: microgrid_step.append(name, *module.step(0.0, normalized=False)) - for name, modules in self.fixed.iterdict(): + fixed_provided, fixed_consumed, _ = microgrid_step.balance() + log_dict = self._get_log_dict(fixed_provided, fixed_consumed, prefix='fixed') + + for name, modules in self.controllable.iterdict(): try: module_controls = control_copy.pop(name) except KeyError: @@ -203,7 +208,8 @@ def run(self, control, normalized=True): provided, consumed, _ = microgrid_step.balance() difference = provided - consumed # if difference > 0, have an excess. Try to use flex sinks to dissapate # otherwise, insufficient. Use flex sources to make up - log_dict = self._get_log_dict(provided, consumed, prefix='fixed') + + log_dict = self._get_log_dict(provided-fixed_provided, consumed-fixed_consumed, log_dict=log_dict, prefix='controllable') if len(control_copy) > 0: warn(f'\nIgnoring the following keys in passed control:\n {list(control_copy.keys())}') @@ -250,7 +256,7 @@ def run(self, control, normalized=True): return microgrid_step.output() def _get_log_dict(self, provided_energy, absorbed_energy, log_dict=None, prefix=None): - _log_dict = dict(provided_to_microgrid=provided_energy, absorbed_by_microgrid=absorbed_energy) + _log_dict = dict(provided_to_microgrid=provided_energy, absorbed_from_microgrid=absorbed_energy) _log_dict = {(prefix + '_' + k if prefix is not None else k): v for k, v in _log_dict.items()} if log_dict: _log_dict.update(log_dict) @@ -278,7 +284,7 @@ def sample_action(self, strict_bound=False, sample_flex_modules=False): """ - module_iterator = self._modules.module_dict() if sample_flex_modules else self._modules.fixed.module_dict() + module_iterator = self._modules.module_dict() if sample_flex_modules else self._modules.controllable.module_dict() return {module_name: [module.sample_action(strict_bound=strict_bound) for module in module_list] for module_name, module_list in module_iterator.items() if module_list[0].action_space.shape[0]} @@ -302,7 +308,7 @@ def get_empty_action(self, sample_flex_modules=False): Empty action. """ - module_iterator = self._modules.module_dict() if sample_flex_modules else self._modules.fixed.module_dict() + module_iterator = self._modules.module_dict() if sample_flex_modules else self._modules.controllable.module_dict() return {module_name: [None]*len(module_list) for module_name, module_list in module_iterator.items() if module_list[0].action_space.shape[0]} @@ -380,25 +386,21 @@ def get_log(self, as_frame=True, drop_singleton_key=False): for key, value in self._balance_logger.to_dict().items(): _log_dict[('balance', 0, key)] = value - if hasattr(self, 'log_dict'): - for key, value in self.log_dict.items(): - _log_dict[(key, '', '')] = value + for key, value in self._microgrid_logger.items(): + _log_dict[(key, 0, '')] = value col_names = ['module_name', 'module_number', 'field'] + df = pd.DataFrame(_log_dict) + df.columns = pd.MultiIndex.from_tuples(df.columns.to_list(), names=col_names) + if drop_singleton_key: - keys_arr = np.array(list(_log_dict.keys())) - module_counters = keys_arr[:, 1].astype(np.int64) - if module_counters.min() == module_counters.max(): - _log_dict = {(key[0], key[2]): value for key, value in _log_dict.items()} - col_names.pop(1) + df.columns = df.columns.remove_unused_levels() if as_frame: - df = pd.DataFrame(_log_dict) - df.columns.set_names(col_names, inplace=True) return df - return _log_dict + return df.to_dict() def get_forecast_horizon(self): """ @@ -484,6 +486,17 @@ def flex(self): """ return self._modules.flex + @property + def controllable(self): + """ + List of all controllable modules in the microgrid. + + Returns + ------- + list of modules + """ + return self._modules.controllable + @property def module_list(self): """ diff --git a/src/pymgrid/modules/base/base_module.py b/src/pymgrid/modules/base/base_module.py index 8a210ab2..6bc90a4a 100644 --- a/src/pymgrid/modules/base/base_module.py +++ b/src/pymgrid/modules/base/base_module.py @@ -241,6 +241,9 @@ def as_source(self, energy_demand): f'module is not a source and ' \ f'can only be called with negative energy.' + if self.module_type[-1] == 'fixed': + return self.update(None, as_source=True) + if energy_demand > self.max_production: if self.raise_errors: self._raise_error(energy_demand, self.max_production, as_source=True) @@ -290,6 +293,9 @@ def as_sink(self, energy_excess): assert energy_excess >= 0 + if self.module_type[-1] == 'fixed': + return self.update(None, as_sink=True) + if energy_excess > self.max_consumption: if self.raise_errors: self._raise_error(energy_excess, self.max_consumption, as_sink=True) @@ -324,7 +330,7 @@ def update(self, external_energy_change, as_source=False, as_sink=False): Parameters ---------- - external_energy_change : float + external_energy_change : float or None Amount of energy to provide or absorb. as_source : bool Whether the module is acting as a source. diff --git a/src/pymgrid/modules/battery_module.py b/src/pymgrid/modules/battery_module.py index bc81999c..c4b68445 100644 --- a/src/pymgrid/modules/battery_module.py +++ b/src/pymgrid/modules/battery_module.py @@ -58,7 +58,7 @@ class BatteryModule(BaseMicrogridModule): If False, actions are clipped to the limit possible. """ - module_type = ('battery', 'fixed') + module_type = ('battery', 'controllable') yaml_tag = f"!BatteryModule" yaml_dumper = yaml.SafeDumper yaml_loader = yaml.SafeLoader diff --git a/src/pymgrid/modules/genset_module.py b/src/pymgrid/modules/genset_module.py index 0bf04911..7bc0a6ac 100644 --- a/src/pymgrid/modules/genset_module.py +++ b/src/pymgrid/modules/genset_module.py @@ -51,7 +51,7 @@ class GensetModule(BaseMicrogridModule): Name of the energy provided by this module, to be used in logging. """ - module_type = 'genset', 'fixed' + module_type = 'genset', 'controllable' yaml_tag = f"!Genset" yaml_dumper = yaml.SafeDumper yaml_loader = yaml.SafeLoader diff --git a/src/pymgrid/modules/grid_module.py b/src/pymgrid/modules/grid_module.py index dcb013c3..8b2a1f27 100644 --- a/src/pymgrid/modules/grid_module.py +++ b/src/pymgrid/modules/grid_module.py @@ -62,7 +62,7 @@ class GridModule(BaseTimeSeriesMicrogridModule): """ - module_type = ('grid', 'fixed') + module_type = ('grid', 'controllable') yaml_tag = u"!GridModule" yaml_loader = yaml.SafeLoader diff --git a/src/pymgrid/modules/load_module.py b/src/pymgrid/modules/load_module.py index bc82dc55..4eab0bb7 100644 --- a/src/pymgrid/modules/load_module.py +++ b/src/pymgrid/modules/load_module.py @@ -48,7 +48,7 @@ class LoadModule(BaseTimeSeriesMicrogridModule): If False, actions are clipped to the limit possible. """ - module_type = ('load', 'static') + module_type = ('load', 'fixed') yaml_tag = u"!LoadModule" yaml_dumper = yaml.SafeDumper yaml_loader = yaml.SafeLoader diff --git a/src/pymgrid/modules/module_container.py b/src/pymgrid/modules/module_container.py index e6b5aeed..782036fb 100644 --- a/src/pymgrid/modules/module_container.py +++ b/src/pymgrid/modules/module_container.py @@ -174,23 +174,23 @@ def get_subcontainers(modules): """ :return: List[Tuple] - 3-element tuples of (fixed_flex_static, source_or_sink, container) + 3-element tuples of (fixed_flex_controllable, source_or_sink, container) """ - source_sink_keys = ('sources' , 'sinks', 'source_and_sinks') + source_sink_keys = ('sources', 'sinks', 'source_and_sinks') fixed = {k: dict() for k in source_sink_keys} flex = {k: dict() for k in source_sink_keys} - static = {k: dict() for k in source_sink_keys} + controllable = {k: dict() for k in source_sink_keys} module_names = dict() for module in modules: try: # module is a tuple module_name, module = module - fixed_flex_static = module.__class__.module_type[1] + fixed_flex_controllable = module.__class__.module_type[1] except TypeError: # module is a module try: - module_name, fixed_flex_static = module.__class__.module_type + module_name, fixed_flex_controllable = module.__class__.module_type except TypeError: raise NotImplementedError( f'Must define the class attribute module_type for class {module.__class__.__name__}') @@ -201,19 +201,19 @@ def get_subcontainers(modules): source_sink_both = 'source_and_sinks' if module.is_sink and module.is_source else \ 'sources' if module.is_source else 'sinks' - if fixed_flex_static == 'fixed': + if fixed_flex_controllable == 'fixed': d = fixed - elif fixed_flex_static == 'flex': + elif fixed_flex_controllable == 'flex': d = flex - elif fixed_flex_static == 'static': - d = static + elif fixed_flex_controllable == 'controllable': + d = controllable else: - raise TypeError(f'Cannot parse fixed_flex_static from module type {module.__class__.module_type}') + raise TypeError(f'Cannot parse fixed_flex_controllable from module type {module.__class__.module_type}') try: - module_names[module_name] = (fixed_flex_static, source_sink_both) + module_names[module_name] = (fixed_flex_controllable, source_sink_both) except KeyError: - raise NameError(f'Attempted to add module {module_name} of type {(fixed_flex_static, source_sink_both)}, ' + raise NameError(f'Attempted to add module {module_name} of type {(fixed_flex_controllable, source_sink_both)}, ' f'but there is an identically named module of type {module_names[module_name]}.') try: @@ -224,10 +224,10 @@ def get_subcontainers(modules): modules_dict = dict(fixed=fixed, flex=flex, - static=static) + controllable=controllable) containers = {(ffs, source_sink_both): _ModuleSubContainer(modules_dict[ffs][source_sink_both]) - for ffs in ('fixed', 'flex', 'static') + for ffs in ('fixed', 'flex', 'controllable') for source_sink_both in source_sink_keys} return containers diff --git a/src/pymgrid/modules/renewable_module.py b/src/pymgrid/modules/renewable_module.py index 6125ba75..c75fafff 100644 --- a/src/pymgrid/modules/renewable_module.py +++ b/src/pymgrid/modules/renewable_module.py @@ -76,7 +76,8 @@ def update(self, external_energy_change, as_source=False, as_sink=False): assert as_source, f'Class {self.__class__.__name__} can only be used as a source.' assert external_energy_change <= self.current_renewable, f'Cannot provide more than {self.current_renewable}' - info = {'provided_energy': external_energy_change, 'curtailment': self.current_renewable.item()-external_energy_change} + info = {'provided_energy': external_energy_change, + 'curtailment': self.current_renewable-external_energy_change} return 0.0, self._done(), info @@ -99,7 +100,7 @@ def current_renewable(self): Renewable production. """ - return self._time_series[self._current_step] + return self._time_series[self._current_step].item() @property def is_source(self): diff --git a/tests/helpers/modular_microgrid.py b/tests/helpers/modular_microgrid.py index 96b527f1..5c2fae87 100644 --- a/tests/helpers/modular_microgrid.py +++ b/tests/helpers/modular_microgrid.py @@ -11,7 +11,7 @@ ) -def get_modular_microgrid(remove_modules=(), additional_modules=None, add_unbalanced_module=True): +def get_modular_microgrid(remove_modules=(), retain_only=None, additional_modules=None, add_unbalanced_module=True): modules = dict( genset=GensetModule(running_min_production=10, running_max_production=50, genset_cost=0.5), @@ -30,11 +30,16 @@ def get_modular_microgrid(remove_modules=(), additional_modules=None, add_unbala grid=GridModule(max_import=100, max_export=0, time_series=np.ones((100, 3)), raise_errors=True) ) - for module in remove_modules: - try: - modules.pop(module) - except KeyError: - raise NameError(f"Module {module} not one of default modules {list(modules.keys())}.") + if retain_only is not None: + modules = {k: v for k, v in modules.items() if k in retain_only} + if remove_modules: + raise RuntimeError('Can pass either remove_modules or retain_only, but not both.') + else: + for module in remove_modules: + try: + modules.pop(module) + except KeyError: + raise NameError(f"Module {module} not one of default modules {list(modules.keys())}.") modules = list(modules.values()) modules.extend(additional_modules if additional_modules else []) diff --git a/tests/helpers/test_case.py b/tests/helpers/test_case.py index cc860c9f..03c19b48 100644 --- a/tests/helpers/test_case.py +++ b/tests/helpers/test_case.py @@ -7,9 +7,11 @@ class TestCase(unittest.TestCase): def assertEqual(self, first, second, msg=None) -> None: try: super().assertEqual(first, second, msg=msg) - except ValueError: + except (ValueError, AssertionError): try: np.testing.assert_equal(first, second, err_msg=msg if msg else '') except AssertionError as e: - np.testing.assert_allclose(first, second, err_msg=msg if msg else '') - warn(f"Arrays are close but not equal: {e.args[0]}") + try: + np.testing.assert_allclose(first, second, rtol=1e-7, atol=1e-10, err_msg=msg if msg else '') + except TypeError: + raise e diff --git a/tests/microgrid/test_microgrid.py b/tests/microgrid/test_microgrid.py index 20c0c274..c358a078 100644 --- a/tests/microgrid/test_microgrid.py +++ b/tests/microgrid/test_microgrid.py @@ -1,6 +1,9 @@ import numpy as np +import pandas as pd + from pymgrid import Microgrid +from pymgrid.modules import LoadModule, RenewableModule from tests.helpers.modular_microgrid import get_modular_microgrid from tests.helpers.test_case import TestCase @@ -65,3 +68,181 @@ def test_sample_action_all_modules_populated(self): has_corresponding_action = False self.assertTrue(empty_action_space != has_corresponding_action) # XOR + + +class TestMicrogridLoadPV(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 + + def set_ts(self): + ts = 10 * np.random.rand(100) + return ts, ts + + 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 + + def test_populated_correctly(self): + self.assertTrue(hasattr(self.microgrid.modules, 'load')) + self.assertTrue(hasattr(self.microgrid.modules, 'renewable')) + self.assertEqual(len(self.microgrid.modules), self.n_modules) # load, pv, unbalanced + + def test_current_load_correct(self): + try: + current_load = self.microgrid.modules.load.item().current_load + except ValueError: + # More than one load module + current_load = sum(load.current_load for load in self.microgrid.modules.load) + self.assertEqual(current_load, self.load_ts[0]) + + def test_current_pv_correct(self): + try: + current_renewable = self.microgrid.modules.renewable.item().current_renewable + except ValueError: + # More than one load module + current_renewable = sum(renewable.current_renewable for renewable in self.microgrid.modules.renewable) + self.assertEqual(current_renewable, self.pv_ts[0]) + + def test_sample_action(self): + sampled_action = self.microgrid.sample_action() + self.assertEqual(len(sampled_action), 0) + + def test_sample_action_with_flex(self): + sampled_action = self.microgrid.sample_action(sample_flex_modules=True) + self.assertEqual(len(sampled_action), 2) + self.assertIn('renewable', sampled_action) + self.assertIn('balancing', sampled_action) + self.assertEqual(len(sampled_action['renewable']), self.n_pvs) + + def test_state_dict(self): + sd = self.microgrid.state_dict + self.assertIn('load', sd) + self.assertIn('renewable', sd) + self.assertIn('balancing', sd) + self.assertEqual(len(sd['load']), self.n_loads) + self.assertEqual(len(sd['balancing']), 1) + + def test_state_series(self): + ss = self.microgrid.state_series + self.assertEqual({'load', 'renewable'}, set(ss.index.get_level_values(0))) + self.assertEqual(ss['load'].index.get_level_values(0).nunique(), self.n_loads) + self.assertEqual(ss['renewable'].index.get_level_values(0).nunique(), self.n_pvs) + self.assertEqual(ss['load'].index.get_level_values(0).nunique(), self.n_loads) + + def test_to_nonmodular(self): + if self.n_pvs > 1 or self.n_loads > 1: + with self.assertRaises(ValueError) as e: + self.microgrid.to_nonmodular() + self.assertIn("Cannot convert modular microgrid with multiple modules of same type", e) + + else: + nonmodular = self.microgrid.to_nonmodular() + self.assertTrue(nonmodular.architecture['PV']) + self.assertFalse(nonmodular.architecture['battery']) + self.assertFalse(nonmodular.architecture['grid']) + self.assertFalse(nonmodular.architecture['genset']) + + def check_step(self, microgrid, step_number=0): + + control = microgrid.get_empty_action() + self.assertEqual(len(control), 0) + + obs, reward, done, info = microgrid.run(control) + loss_load = self.load_ts[step_number]-self.pv_ts[step_number] + loss_load_cost = self.microgrid.modules.balancing[0].loss_load_cost * max(loss_load, 0) + + 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())) + + load_met = min(self.load_ts[step_number], self.pv_ts[step_number]) + loss_load = max(self.load_ts[step_number] - load_met, 0) + pv_curtailment = max(self.pv_ts[step_number]-load_met, 0) + + # Checking the log populated correctly. + + log_row = microgrid.log.iloc[step_number] + log_entry = lambda module, entry: log_row.loc[pd.IndexSlice[module, :, entry]].sum() + + # Check that there are log entries for all modules of each name + self.assertEqual(log_row['load'].index.get_level_values(0).nunique(), self.n_loads) + + self.assertEqual(log_entry('load', 'load_current'), -1 * self.load_ts[step_number]) + self.assertEqual(log_entry('load', 'load_met'), self.load_ts[step_number]) + + if loss_load == 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', 'curtailment'), pv_curtailment) + + 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', 'fixed_provided_to_microgrid'), 0.0) + 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) + self.assertEqual(log_entry('balance', 'controllable_provided_to_microgrid'), 0.0) + + return microgrid + + def test_run_one_step(self): + microgrid = self.microgrid + self.check_step(microgrid=microgrid, step_number=0) + + def test_run_n_steps(self): + microgrid = self.microgrid + for step in range(len(self.load_ts)): + with self.subTest(step=step): + microgrid = self.check_step(microgrid=microgrid, step_number=step) + + +class TestMicrogridLoadExcessPV(TestMicrogridLoadPV): + # Same as above but pv is greater than load. + 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 TestMicrogridPVExcessLoad(TestMicrogridLoadPV): + # Load greater than PV. + 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 TestMicrogridTwoLoads(TestMicrogridLoadPV): + 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 + + assert all(load_1_ts > 0) + assert all(load_2_ts > 0) + + 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 + + +class TestMicrogridTwoPV(TestMicrogridLoadPV): + 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 + + assert all(pv_1_ts > 0) + assert all(pv_2_ts > 0) + + 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