diff --git a/docs/changelog.rst b/docs/changelog.rst index 63c0c0356..7f0fe3d06 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,6 +9,7 @@ These are new features and improvements of note in each release :backlinks: top +.. include:: whatsnew/v0-5-5.rst .. include:: whatsnew/v0-5-4.rst .. include:: whatsnew/v0-5-3.rst .. include:: whatsnew/v0-5-2.rst diff --git a/docs/whatsnew/v0-5-5.rst b/docs/whatsnew/v0-5-5.rst new file mode 100644 index 000000000..c8afd9a9c --- /dev/null +++ b/docs/whatsnew/v0-5-5.rst @@ -0,0 +1,12 @@ +v0.5.5 () +-------------------------- + +Bug fixes +######### + +* Fix iterating over _FakeSequence objects + +Contributors +############ + +* Patrik Schönfeldt diff --git a/src/oemof/solph/_plumbing.py b/src/oemof/solph/_plumbing.py index bb6ed5b80..77c8ffb09 100644 --- a/src/oemof/solph/_plumbing.py +++ b/src/oemof/solph/_plumbing.py @@ -10,7 +10,7 @@ SPDX-License-Identifier: MIT """ - +import warnings from collections import abc from itertools import repeat @@ -19,8 +19,8 @@ def sequence(iterable_or_scalar): """Checks if an object is iterable (except string) or scalar and returns - the original sequence if object is an iterable and an 'emulated' - sequence object of class _Sequence if object is a scalar or string. + the an numpy array of the sequence if object is an iterable or an + 'emulated' sequence object of class _FakeSequence if object is a scalar. Parameters ---------- @@ -56,6 +56,43 @@ def sequence(iterable_or_scalar): return _FakeSequence(value=iterable_or_scalar) +def valid_sequence(sequence, length: int) -> bool: + """Checks if an object is a numpy array of at least the given length + or an 'emulated' sequence object of class _FakeSequence. + If unset, the latter is set to the required lenght. + + """ + if sequence[0] is None: + return False + + if isinstance(sequence, _FakeSequence): + if sequence.size is None: + sequence.size = length + + if sequence.size == length: + return True + else: + return False + + if isinstance(sequence, np.ndarray): + if sequence.size == length: + return True + # --- BEGIN: To be removed for versions >= v0.6 --- + elif sequence.size > length: + warnings.warn( + "Sequence longer than needed" + f" ({sequence.size} items instead of {length})." + " This will be trated as an error in the future.", + FutureWarning, + ) + return True + # --- END --- + else: + raise ValueError(f"Lentgh of {sequence} should be {length}.") + + return False + + class _FakeSequence: """Emulates a list whose length is not known in advance. diff --git a/src/oemof/solph/components/_extraction_turbine_chp.py b/src/oemof/solph/components/_extraction_turbine_chp.py index cef1a6152..dc8fb393b 100644 --- a/src/oemof/solph/components/_extraction_turbine_chp.py +++ b/src/oemof/solph/components/_extraction_turbine_chp.py @@ -23,8 +23,8 @@ from pyomo.environ import BuildAction from pyomo.environ import Constraint -from oemof.solph._plumbing import sequence as solph_sequence -from oemof.solph.components._converter import Converter +from oemof.solph._plumbing import sequence +from oemof.solph.components import Converter class ExtractionTurbineCHP(Converter): @@ -87,7 +87,7 @@ def __init__( custom_attributes=custom_attributes, ) self.conversion_factor_full_condensation = { - k: solph_sequence(v) + k: sequence(v) for k, v in conversion_factor_full_condensation.items() } diff --git a/src/oemof/solph/components/_generic_storage.py b/src/oemof/solph/components/_generic_storage.py index a9c5e1c8a..4a7b23c75 100644 --- a/src/oemof/solph/components/_generic_storage.py +++ b/src/oemof/solph/components/_generic_storage.py @@ -38,7 +38,8 @@ from oemof.solph._helpers import check_node_object_for_missing_attribute from oemof.solph._options import Investment -from oemof.solph._plumbing import sequence as solph_sequence +from oemof.solph._plumbing import sequence +from oemof.solph._plumbing import valid_sequence class GenericStorage(Node): @@ -224,19 +225,15 @@ def __init__( self.initial_storage_level = initial_storage_level self.balanced = balanced - self.loss_rate = solph_sequence(loss_rate) - self.fixed_losses_relative = solph_sequence(fixed_losses_relative) - self.fixed_losses_absolute = solph_sequence(fixed_losses_absolute) - self.inflow_conversion_factor = solph_sequence( - inflow_conversion_factor - ) - self.outflow_conversion_factor = solph_sequence( - outflow_conversion_factor - ) - self.max_storage_level = solph_sequence(max_storage_level) - self.min_storage_level = solph_sequence(min_storage_level) - self.fixed_costs = solph_sequence(fixed_costs) - self.storage_costs = solph_sequence(storage_costs) + self.loss_rate = sequence(loss_rate) + self.fixed_losses_relative = sequence(fixed_losses_relative) + self.fixed_losses_absolute = sequence(fixed_losses_absolute) + self.inflow_conversion_factor = sequence(inflow_conversion_factor) + self.outflow_conversion_factor = sequence(outflow_conversion_factor) + self.max_storage_level = sequence(max_storage_level) + self.min_storage_level = sequence(min_storage_level) + self.fixed_costs = sequence(fixed_costs) + self.storage_costs = sequence(storage_costs) self.invest_relation_input_output = invest_relation_input_output self.invest_relation_input_capacity = invest_relation_input_capacity self.invest_relation_output_capacity = invest_relation_output_capacity @@ -603,7 +600,7 @@ def _objective_expression(self): if m.es.periods is not None: for n in self.STORAGES: - if n.fixed_costs[0] is not None: + if valid_sequence(n.fixed_costs, len(m.PERIODS)): fixed_costs += sum( n.nominal_storage_capacity * n.fixed_costs[pp] @@ -615,7 +612,7 @@ def _objective_expression(self): storage_costs = 0 for n in self.STORAGES: - if n.storage_costs[0] is not None: + if valid_sequence(n.storage_costs, len(m.TIMESTEPS)): storage_costs += ( self.storage_content[n, 0] * n.storage_costs[0] ) @@ -1861,7 +1858,7 @@ def _objective_expression(self): period_investment_costs[p] += investment_costs_increment for n in self.INVESTSTORAGES: - if n.investment.fixed_costs[0] is not None: + if valid_sequence(n.investment.fixed_costs, len(m.PERIODS)): lifetime = n.investment.lifetime for p in m.PERIODS: range_limit = min( @@ -1879,7 +1876,7 @@ def _objective_expression(self): ) for n in self.EXISTING_INVESTSTORAGES: - if n.investment.fixed_costs[0] is not None: + if valid_sequence(n.investment.fixed_costs, len(m.PERIODS)): lifetime = n.investment.lifetime age = n.investment.age range_limit = min( diff --git a/src/oemof/solph/components/experimental/_sink_dsm.py b/src/oemof/solph/components/experimental/_sink_dsm.py index 1d910f54e..4645a5899 100644 --- a/src/oemof/solph/components/experimental/_sink_dsm.py +++ b/src/oemof/solph/components/experimental/_sink_dsm.py @@ -38,6 +38,7 @@ from oemof.solph._options import Investment from oemof.solph._plumbing import sequence +from oemof.solph._plumbing import valid_sequence from oemof.solph.components._sink import Sink @@ -703,7 +704,7 @@ def _objective_expression(self): * (1 + m.discount_rate) ** (-m.es.periods_years[p]) ) - if g.fixed_costs[0] is not None: + if valid_sequence(g.fixed_costs, len(m.PERIODS)): fixed_costs += sum( max(g.max_capacity_up, g.max_capacity_down) * g.fixed_costs[pp] @@ -1434,7 +1435,7 @@ def _objective_expression(self): * (1 + m.discount_rate) ** (-m.es.periods_years[p]) ) - if g.investment.fixed_costs[0] is not None: + if valid_sequence(g.investment.fixed_costs, len(m.PERIODS)): lifetime = g.investment.lifetime for p in m.PERIODS: range_limit = min( @@ -1452,7 +1453,7 @@ def _objective_expression(self): ) for g in self.EXISTING_INVESTDSM: - if g.investment.fixed_costs[0] is not None: + if valid_sequence(g.investment.fixed_costs, len(m.PERIODS)): lifetime = g.investment.lifetime age = g.investment.age range_limit = min( @@ -2198,7 +2199,7 @@ def _objective_expression(self): * (1 + m.discount_rate) ** (-m.es.periods_years[p]) ) - if g.fixed_costs[0] is not None: + if valid_sequence(g.fixed_costs, len(m.PERIODS)): fixed_costs += sum( max(g.max_capacity_up, g.max_capacity_down) * g.fixed_costs[pp] @@ -3290,7 +3291,7 @@ def _objective_expression(self): * (1 + m.discount_rate) ** (-m.es.periods_years[p]) ) - if g.investment.fixed_costs[0] is not None: + if valid_sequence(g.investment.fixed_costs, len(m.PERIODS)): lifetime = g.investment.lifetime for p in m.PERIODS: range_limit = min( @@ -3308,7 +3309,7 @@ def _objective_expression(self): ) for g in self.EXISTING_INVESTDSM: - if g.investment.fixed_costs[0] is not None: + if valid_sequence(g.investment.fixed_costs, len(m.PERIODS)): lifetime = g.investment.lifetime age = g.investment.age range_limit = min( @@ -4391,7 +4392,7 @@ def _objective_expression(self): * (1 + m.discount_rate) ** (-m.es.periods_years[p]) ) - if g.fixed_costs[0] is not None: + if valid_sequence(g.fixed_costs, len(m.PERIODS)): fixed_costs += sum( max(g.max_capacity_up, g.max_capacity_down) * g.fixed_costs[pp] @@ -5791,7 +5792,7 @@ def _objective_expression(self): * (1 + m.discount_rate) ** (-m.es.periods_years[p]) ) - if g.investment.fixed_costs[0] is not None: + if valid_sequence(g.investment.fixed_costs, len(m.PERIODS)): lifetime = g.investment.lifetime for p in m.PERIODS: range_limit = min( @@ -5809,7 +5810,7 @@ def _objective_expression(self): ) for g in self.EXISTING_INVESTDSM: - if g.investment.fixed_costs[0] is not None: + if valid_sequence(g.investment.fixed_costs, len(m.PERIODS)): lifetime = g.investment.lifetime age = g.investment.age range_limit = min( diff --git a/src/oemof/solph/flows/_investment_flow_block.py b/src/oemof/solph/flows/_investment_flow_block.py index 99efec0dc..bb0ee5d2c 100644 --- a/src/oemof/solph/flows/_investment_flow_block.py +++ b/src/oemof/solph/flows/_investment_flow_block.py @@ -29,6 +29,8 @@ from pyomo.core import Var from pyomo.core.base.block import ScalarBlock +from oemof.solph._plumbing import valid_sequence + class InvestmentFlowBlock(ScalarBlock): r"""Block for all flows with :attr:`Investment` being not None. @@ -1007,7 +1009,9 @@ def _objective_expression(self): period_investment_costs[p] += investment_costs_increment for i, o in self.INVESTFLOWS: - if m.flows[i, o].investment.fixed_costs[0] is not None: + if valid_sequence( + m.flows[i, o].investment.fixed_costs, len(m.PERIODS) + ): lifetime = m.flows[i, o].investment.lifetime for p in m.PERIODS: range_limit = min( @@ -1022,7 +1026,9 @@ def _objective_expression(self): ) for i, o in self.EXISTING_INVESTFLOWS: - if m.flows[i, o].investment.fixed_costs[0] is not None: + if valid_sequence( + m.flows[i, o].investment.fixed_costs, len(m.PERIODS) + ): lifetime = m.flows[i, o].investment.lifetime age = m.flows[i, o].investment.age range_limit = min( diff --git a/src/oemof/solph/flows/_non_convex_flow_block.py b/src/oemof/solph/flows/_non_convex_flow_block.py index aa1cf457c..31b870ca0 100644 --- a/src/oemof/solph/flows/_non_convex_flow_block.py +++ b/src/oemof/solph/flows/_non_convex_flow_block.py @@ -25,6 +25,8 @@ from pyomo.core import Var from pyomo.core.base.block import ScalarBlock +from oemof.solph._plumbing import valid_sequence + class NonConvexFlowBlock(ScalarBlock): r""" @@ -327,7 +329,9 @@ def _startup_costs(self): if m.es.periods is None: for i, o in self.STARTUPFLOWS: - if m.flows[i, o].nonconvex.startup_costs[0] is not None: + if valid_sequence( + m.flows[i, o].nonconvex.startup_costs, len(m.TIMESTEPS) + ): startup_costs += sum( self.startup[i, o, t] * m.flows[i, o].nonconvex.startup_costs[t] @@ -335,7 +339,9 @@ def _startup_costs(self): ) else: for i, o in self.STARTUPFLOWS: - if m.flows[i, o].nonconvex.startup_costs[0] is not None: + if valid_sequence( + m.flows[i, o].nonconvex.startup_costs, len(m.TIMESTEPS) + ): startup_costs += sum( self.startup[i, o, t] * m.flows[i, o].nonconvex.startup_costs[t] @@ -361,7 +367,10 @@ def _shutdown_costs(self): if m.es.periods is None: for i, o in self.SHUTDOWNFLOWS: - if m.flows[i, o].nonconvex.shutdown_costs[0] is not None: + if valid_sequence( + m.flows[i, o].nonconvex.shutdown_costs, + len(m.TIMESTEPS), + ): shutdown_costs += sum( self.shutdown[i, o, t] * m.flows[i, o].nonconvex.shutdown_costs[t] @@ -369,7 +378,10 @@ def _shutdown_costs(self): ) else: for i, o in self.SHUTDOWNFLOWS: - if m.flows[i, o].nonconvex.shutdown_costs[0] is not None: + if valid_sequence( + m.flows[i, o].nonconvex.shutdown_costs, + len(m.TIMESTEPS), + ): shutdown_costs += sum( self.shutdown[i, o, t] * m.flows[i, o].nonconvex.shutdown_costs[t] @@ -395,7 +407,10 @@ def _activity_costs(self): if m.es.periods is None: for i, o in self.ACTIVITYCOSTFLOWS: - if m.flows[i, o].nonconvex.activity_costs[0] is not None: + if valid_sequence( + m.flows[i, o].nonconvex.activity_costs, + len(m.TIMESTEPS), + ): activity_costs += sum( self.status[i, o, t] * m.flows[i, o].nonconvex.activity_costs[t] @@ -403,7 +418,10 @@ def _activity_costs(self): ) else: for i, o in self.ACTIVITYCOSTFLOWS: - if m.flows[i, o].nonconvex.activity_costs[0] is not None: + if valid_sequence( + m.flows[i, o].nonconvex.activity_costs, + len(m.TIMESTEPS), + ): activity_costs += sum( self.status[i, o, t] * m.flows[i, o].nonconvex.activity_costs[t] @@ -429,7 +447,10 @@ def _inactivity_costs(self): if m.es.periods is None: for i, o in self.INACTIVITYCOSTFLOWS: - if m.flows[i, o].nonconvex.inactivity_costs[0] is not None: + if valid_sequence( + m.flows[i, o].nonconvex.inactivity_costs, + len(m.TIMESTEPS), + ): inactivity_costs += sum( (1 - self.status[i, o, t]) * m.flows[i, o].nonconvex.inactivity_costs[t] @@ -437,7 +458,10 @@ def _inactivity_costs(self): ) else: for i, o in self.INACTIVITYCOSTFLOWS: - if m.flows[i, o].nonconvex.inactivity_costs[0] is not None: + if valid_sequence( + m.flows[i, o].nonconvex.inactivity_costs, + len(m.TIMESTEPS), + ): inactivity_costs += sum( (1 - self.status[i, o, t]) * m.flows[i, o].nonconvex.inactivity_costs[t] diff --git a/src/oemof/solph/flows/_simple_flow_block.py b/src/oemof/solph/flows/_simple_flow_block.py index 7cf21bced..e7d8d6841 100644 --- a/src/oemof/solph/flows/_simple_flow_block.py +++ b/src/oemof/solph/flows/_simple_flow_block.py @@ -26,6 +26,8 @@ from pyomo.core import Var from pyomo.core.base.block import ScalarBlock +from oemof.solph._plumbing import valid_sequence + class SimpleFlowBlock(ScalarBlock): r"""Flow block with definitions for standard flows. @@ -172,12 +174,16 @@ def _create_variables(self, group): ) # set upper bound of gradient variable for i, o, f in group: - if m.flows[i, o].positive_gradient_limit[0] is not None: + if valid_sequence( + m.flows[i, o].positive_gradient_limit, len(m.TIMESTEPS) + ): for t in m.TIMESTEPS: self.positive_gradient[i, o, t].setub( f.positive_gradient_limit[t] * f.nominal_value ) - if m.flows[i, o].negative_gradient_limit[0] is not None: + if valid_sequence( + m.flows[i, o].negative_gradient_limit, len(m.TIMESTEPS) + ): for t in m.TIMESTEPS: self.negative_gradient[i, o, t].setub( f.negative_gradient_limit[t] * f.nominal_value @@ -429,7 +435,9 @@ def _objective_expression(self): if m.es.periods is None: for i, o in m.FLOWS: - if m.flows[i, o].variable_costs[0] is not None: + if valid_sequence( + m.flows[i, o].variable_costs, len(m.TIMESTEPS) + ): for t in m.TIMESTEPS: variable_costs += ( m.flow[i, o, t] @@ -439,7 +447,9 @@ def _objective_expression(self): else: for i, o in m.FLOWS: - if m.flows[i, o].variable_costs[0] is not None: + if valid_sequence( + m.flows[i, o].variable_costs, len(m.TIMESTEPS) + ): for p, t in m.TIMEINDEX: variable_costs += ( m.flow[i, o, t] @@ -464,7 +474,7 @@ def _objective_expression(self): # Fixed costs for units with limited lifetime for i, o in self.LIFETIME_FLOWS: - if m.flows[i, o].fixed_costs[0] is not None: + if valid_sequence(m.flows[i, o].fixed_costs, len(m.TIMESTEPS)): range_limit = min( m.es.end_year_of_optimization, m.flows[i, o].lifetime, @@ -477,7 +487,7 @@ def _objective_expression(self): ) for i, o in self.LIFETIME_AGE_FLOWS: - if m.flows[i, o].fixed_costs[0] is not None: + if valid_sequence(m.flows[i, o].fixed_costs, len(m.TIMESTEPS)): range_limit = min( m.es.end_year_of_optimization, m.flows[i, o].lifetime - m.flows[i, o].age, diff --git a/src/oemof/solph/flows/experimental/_electrical_line.py b/src/oemof/solph/flows/experimental/_electrical_line.py index 2b5e2eb85..ad08f99ff 100644 --- a/src/oemof/solph/flows/experimental/_electrical_line.py +++ b/src/oemof/solph/flows/experimental/_electrical_line.py @@ -25,7 +25,7 @@ from pyomo.environ import Set from pyomo.environ import Var -from oemof.solph._plumbing import sequence as solph_sequence +from oemof.solph._plumbing import sequence from oemof.solph.buses.experimental._electrical_bus import ElectricalBus from oemof.solph.flows._flow import Flow @@ -75,7 +75,7 @@ def __init__(self, **kwargs): nonconvex=kwargs.get("nonconvex"), custom_attributes=kwargs.get("costom_attributes"), ) - self.reactance = solph_sequence(kwargs.get("reactance", 0.00001)) + self.reactance = sequence(kwargs.get("reactance", 0.00001)) self.input = kwargs.get("input") self.output = kwargs.get("output") diff --git a/tests/test_plumbing.py b/tests/test_plumbing.py index b51187be8..f97ff96c1 100644 --- a/tests/test_plumbing.py +++ b/tests/test_plumbing.py @@ -11,6 +11,7 @@ from oemof.solph._plumbing import _FakeSequence from oemof.solph._plumbing import sequence +from oemof.solph._plumbing import valid_sequence def test_fake_sequence(): @@ -63,3 +64,31 @@ def test_sequence(): seq_ab = sequence("ab") assert isinstance(seq_ab, str) assert seq_ab == "ab" + + +def test_valid_sequence(): + np_array = np.array([0, 1, 2, 3, 4]) + assert valid_sequence(np_array, 5) + + with pytest.warns(FutureWarning, match="Sequence longer than needed"): + valid_sequence(np_array, 4) + + # it's not that long + with pytest.raises(ValueError): + valid_sequence(np_array, 1337) + + fake_sequence = _FakeSequence(42) + assert valid_sequence(fake_sequence, 5) + assert len(fake_sequence) == 5 + + # wil not automatically overwrite size + assert not valid_sequence(fake_sequence, 1337) + assert len(fake_sequence) == 5 + + # manually overwriting length is still possible + fake_sequence.size = 1337 + assert valid_sequence(fake_sequence, 1337) + assert len(fake_sequence) == 1337 + + # strings are no valid sequences + assert not valid_sequence("abc", 3) diff --git a/tests/test_scripts/test_solph/test_generic_caes/generic_caes.csv b/tests/test_scripts/test_solph/test_generic_caes/generic_caes.csv index 0c00956b4..1658a9f4c 100644 --- a/tests/test_scripts/test_solph/test_generic_caes/generic_caes.csv +++ b/tests/test_scripts/test_solph/test_generic_caes/generic_caes.csv @@ -238,4 +238,3 @@ timestep,price_el_sink,price_el_source 237,-33.57,33.57 238,-30.51,30.51 239,-30.57,30.57 -240,-29.28,29.28 diff --git a/tests/test_scripts/test_solph/test_generic_caes/test_generic_caes.py b/tests/test_scripts/test_solph/test_generic_caes/test_generic_caes.py index dc40286cb..8c9e041e4 100644 --- a/tests/test_scripts/test_solph/test_generic_caes/test_generic_caes.py +++ b/tests/test_scripts/test_solph/test_generic_caes/test_generic_caes.py @@ -34,7 +34,7 @@ def test_gen_caes(): data = pd.read_csv(full_filename) # select periods - periods = len(data) - 1 + periods = len(data) # create an energy system idx = pd.date_range("1/1/2017", periods=periods, freq="h") diff --git a/tests/test_scripts/test_solph/test_generic_chp/ccet.csv b/tests/test_scripts/test_solph/test_generic_chp/ccet.csv index ec231419f..185a1f1a2 100644 --- a/tests/test_scripts/test_solph/test_generic_chp/ccet.csv +++ b/tests/test_scripts/test_solph/test_generic_chp/ccet.csv @@ -198,4 +198,3 @@ timestep,demand_th,price_el,Eta_el_max_woDH,P_max_woDH,Eta_el_min_woDH,P_min_woD 197,0.97,-50,0.53,199.73,0.43,80.65,0.19,31.71,0.19 198,0.98,-50,0.53,199.73,0.43,80.65,0.19,31.71,0.19 199,0.99,-50,0.53,199.73,0.43,80.65,0.19,31.71,0.19 -200,1,-50,0.53,199.73,0.43,80.65,0.19,31.71,0.19 diff --git a/tests/test_scripts/test_solph/test_generic_chp/test_generic_chp.py b/tests/test_scripts/test_solph/test_generic_chp/test_generic_chp.py index 6c9f8c423..c5770d279 100644 --- a/tests/test_scripts/test_solph/test_generic_chp/test_generic_chp.py +++ b/tests/test_scripts/test_solph/test_generic_chp/test_generic_chp.py @@ -28,7 +28,7 @@ def test_gen_chp(): data = pd.read_csv(full_filename) # select periods - periods = len(data) - 1 + periods = len(data) # create an energy system idx = pd.date_range("1/1/2017", periods=periods, freq="h")