diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d18c1a3e1..624ae6f25 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,7 +2,10 @@ name: "CodeQL" on: push: - branches: [ "dev", "master" ] + branches: + - dev + - master + - 'release/**' pull_request: branches: [ "dev" ] schedule: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8a4a7fff9..3e5f73159 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,6 +5,7 @@ on: branches: - master - dev + - 'release/**' pull_request: jobs: diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml index 874a5bfb7..a09540cfe 100644 --- a/.github/workflows/packaging.yml +++ b/.github/workflows/packaging.yml @@ -3,7 +3,10 @@ name: packaging on: # Make sure packaging process is not broken push: - branches: [master, dev] + branches: + - master + - dev + - 'release/**' pull_request: # Make a package for release release: @@ -31,7 +34,7 @@ jobs: run: python -m build . - name: Run twine check run: twine check dist/* - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: tox-gh-actions-dist path: dist diff --git a/.github/workflows/tox_checks.yml b/.github/workflows/tox_checks.yml index f575ed022..e5cf4fa76 100644 --- a/.github/workflows/tox_checks.yml +++ b/.github/workflows/tox_checks.yml @@ -6,6 +6,7 @@ on: branches: - master - dev + - 'release/**' pull_request: workflow_dispatch: @@ -38,7 +39,7 @@ jobs: python-version: "${{ env.default_python || '3.9' }}" - name: Pip cache - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ matrix.toxenv }}-${{ hashFiles('tox.ini', 'setup.py') }} diff --git a/.github/workflows/tox_pytests.yml b/.github/workflows/tox_pytests.yml index 44c8d09c9..a28cb482d 100644 --- a/.github/workflows/tox_pytests.yml +++ b/.github/workflows/tox_pytests.yml @@ -5,6 +5,7 @@ on: branches: - master - dev + - 'release/**' pull_request: workflow_dispatch: diff --git a/README.rst b/README.rst index 21501fdf2..73819ba26 100644 --- a/README.rst +++ b/README.rst @@ -165,7 +165,8 @@ There are several solvers that can work with oemof, both open source and commerc Two open source solvers are widely used (CBC and GLPK), but oemof suggests CBC (Coin-or branch and cut). It may be useful to compare results of different solvers to see which performs best. Other commercial solvers, like Gurobi or Cplex, are also options. -Have a look at the `pyomo docs `_ to learn about which solvers are supported. +Have a look at the `pyomo docs `_ +to learn about which solvers are supported. Check the solver installation by executing the test_installation example below (see section Installation Test). @@ -188,7 +189,7 @@ Check the solver installation by executing the test_installation example (see th Please follow the installation instructions on the respective homepages for details. -CBC-solver: https://projects.coin-or.org/Cbc +CBC-solver: https://github.com/coin-or/Cbc GLPK-solver: http://arnab-deka.com/posts/2010/02/installing-glpk-on-a-mac/ diff --git a/docs/changelog.rst b/docs/changelog.rst index 05eaa230c..bb1a07f3f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,8 @@ These are new features and improvements of note in each release .. include:: whatsnew/v0-6-0.rst +.. include:: whatsnew/v0-5-6.rst +.. 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/conf.py b/docs/conf.py index d81c66685..e6b25a295 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -73,11 +73,8 @@ def setup(app): r"https://requires.io/.*", r"https://matrix.to/*", r"https://forum.openmod-initiative.org/*", -] + ( - [ - r"https://github.com/oemof/oemof-solph/issues/*", - r"https://github.com/oemof/oemof-solph/pull/*", - ] - if "TRAVIS" not in os.environ - else [] -) + r"https://github.com/oemof/oemof-solph/issues/*", + r"https://github.com/oemof/oemof-solph/pull/*", + # Due to traffic limitation, the folowwing creates a 403 in CI pipeline: + "https://www.sciencedirect.com/science/article/abs/pii/S036054421500331X", +] diff --git a/docs/usage.rst b/docs/usage.rst index 9dbbbec2c..1a9c265bf 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -24,7 +24,9 @@ a detailed and complete description of all oemof-solph modules. How can I use solph? -------------------- -To use solph you have to install oemof.solph and at least one solver (see :ref:`installation_label`), which can be used together with pyomo (e.g. CBC, GLPK, Gurobi, Cplex). See the `pyomo installation guide `_ for all supported solvers. +To use solph you have to install oemof.solph and at least one solver (see :ref:`installation_label`), +which can be used together with `pyomo `_ +(e.g. CBC, GLPK, Gurobi, Cplex). You can test it by executing one of the existing examples (see :ref:`examples_label`). Be aware that the examples require the CBC solver but you can change the solver name in the example files to your solver. diff --git a/docs/whatsnew/v0-3-2.rst b/docs/whatsnew/v0-3-2.rst index e9765f7a4..d9ca30af7 100644 --- a/docs/whatsnew/v0-3-2.rst +++ b/docs/whatsnew/v0-3-2.rst @@ -19,8 +19,7 @@ New components Documentation #################### -* Revision of the `outputlib documentation - `_. +* Revision of the outputlib documentation. Other changes #################### diff --git a/docs/whatsnew/v0-5-5.rst b/docs/whatsnew/v0-5-5.rst new file mode 100644 index 000000000..944d2e1b8 --- /dev/null +++ b/docs/whatsnew/v0-5-5.rst @@ -0,0 +1,12 @@ +v0.5.5 (August 29th, 2024) +-------------------------- + +Bug fixes +######### + +* Fix iterating over _FakeSequence objects + +Contributors +############ + +* Patrik Schönfeldt diff --git a/docs/whatsnew/v0-5-6.rst b/docs/whatsnew/v0-5-6.rst new file mode 100644 index 000000000..a6c92aebf --- /dev/null +++ b/docs/whatsnew/v0-5-6.rst @@ -0,0 +1,17 @@ +v0.5.6 +------ + +Bug fixes +######### + +* Update required Pyomo version to allow working with numpy >= 2.0.0. + +Known issues +############ + +* Indexing of Storage with capacity investment is off by one. + +Contributors +############ + +* Patrik Schönfeldt diff --git a/docs/whatsnew/v0-6-0.rst b/docs/whatsnew/v0-6-0.rst index 6808ba412..f5db57b34 100644 --- a/docs/whatsnew/v0-6-0.rst +++ b/docs/whatsnew/v0-6-0.rst @@ -31,8 +31,6 @@ Other changes Known issues ############ -* Incompatible to numpy >= 2.0.0. This is because of Pyomo, but we have to - enforce a lower version in our package. Contributors ############ diff --git a/pyproject.toml b/pyproject.toml index 60f422d70..d50b4de2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,9 +57,9 @@ requires-python = ">=3.8" dependencies = [ "blinker", "dill", - "numpy < 2.0.0", - "pandas >= 2.0.0", - "pyomo >= 6.6.0, < 7.0", + "numpy >= 2.0.0", + "pandas >= 2.2.0", + "pyomo >= 6.8.0", "networkx", "oemof.tools >= 0.4.3", "oemof.network >= 0.5.0", diff --git a/src/oemof/solph/__init__.py b/src/oemof/solph/__init__.py index ab45cf3e6..e9c067e1c 100644 --- a/src/oemof/solph/__init__.py +++ b/src/oemof/solph/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.6.0a2" +__version__ = "0.6.0a3" from . import buses from . import components diff --git a/src/oemof/solph/_plumbing.py b/src/oemof/solph/_plumbing.py index bb6ed5b80..07e6a4517 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 ---------- @@ -30,15 +30,15 @@ def sequence(iterable_or_scalar): -------- >>> y = sequence([1,2,3,4,5,6,7,8,9,10,11]) >>> y[0] - 1 + np.int64(1) >>> y[10] - 11 + np.int64(11) >>> import pandas as pd >>> s1 = sequence(pd.Series([1,5,9])) >>> s1[2] - 9 + np.int64(9) >>> x = sequence(10) >>> x[0] @@ -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 e6ed731db..027b1e070 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 ca8d323d9..cbbd25a17 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): @@ -53,15 +54,15 @@ class GenericStorage(Node): :class:`oemof.solph.options.Investment` object Absolute nominal capacity of the storage, fixed value or object describing parameter of investment optimisations. - invest_relation_input_capacity : numeric or None, :math:`r_{cap,in}` + invest_relation_input_capacity : numeric (iterable or scalar) or None, :math:`r_{cap,in}` Ratio between the investment variable of the input Flow and the investment variable of the storage: :math:`\dot{E}_{in,invest} = E_{invest} \cdot r_{cap,in}` - invest_relation_output_capacity : numeric or None, :math:`r_{cap,out}` + invest_relation_output_capacity : numeric (iterable or scalar) or None, :math:`r_{cap,out}` Ratio between the investment variable of the output Flow and the investment variable of the storage: :math:`\dot{E}_{out,invest} = E_{invest} \cdot r_{cap,out}` - invest_relation_input_output : numeric or None, :math:`r_{in,out}` + invest_relation_input_output : numeric (iterable or scalar) or None, :math:`r_{in,out}` Ratio between the investment variable of the output Flow and the investment variable of the input flow. This ratio used to fix the flow investments to each other. @@ -240,22 +241,24 @@ 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.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 = sequence( + invest_relation_input_output ) - self.outflow_conversion_factor = solph_sequence( - outflow_conversion_factor + self.invest_relation_input_capacity = sequence( + invest_relation_input_capacity + ) + self.invest_relation_output_capacity = sequence( + invest_relation_output_capacity ) - 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.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 self.lifetime_inflow = lifetime_inflow self.lifetime_outflow = lifetime_outflow @@ -272,24 +275,22 @@ def _set_flows(self): coupled with storage capacity via invest relations """ for flow in self.inputs.values(): - if ( - self.invest_relation_input_capacity is not None - and not isinstance(flow.investment, Investment) - ): + if self.invest_relation_input_capacity[ + 0 + ] is not None and not isinstance(flow.investment, Investment): flow.investment = Investment(lifetime=self.lifetime_inflow) for flow in self.outputs.values(): - if ( - self.invest_relation_output_capacity is not None - and not isinstance(flow.investment, Investment) - ): + if self.invest_relation_output_capacity[ + 0 + ] is not None and not isinstance(flow.investment, Investment): flow.investment = Investment(lifetime=self.lifetime_outflow) def _check_invest_attributes(self): """Raise errors for infeasible investment attribute combinations""" if ( - self.invest_relation_input_output is not None - and self.invest_relation_output_capacity is not None - and self.invest_relation_input_capacity is not None + self.invest_relation_input_output[0] is not None + and self.invest_relation_output_capacity[0] is not None + and self.invest_relation_input_capacity[0] is not None ): e2 = ( "Overdetermined. Three investment object will be coupled" @@ -495,7 +496,9 @@ def _create(self, group=None): self.STORAGES_WITH_INVEST_FLOW_REL = Set( initialize=[ - n for n in group if n.invest_relation_input_output is not None + n + for n in group + if n.invest_relation_input_output[0] is not None ] ) @@ -588,7 +591,7 @@ def _power_coupled(_): for p in m.PERIODS: expr = ( m.InvestmentFlowBlock.total[n, o[n], p] - ) * n.invest_relation_input_output == ( + ) * n.invest_relation_input_output[p] == ( m.InvestmentFlowBlock.total[i[n], n, p] ) self.power_coupled.add((n, p), expr) @@ -616,7 +619,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] @@ -628,7 +631,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)): # We actually want to iterate over all TIMEPOINTS except the # 0th. As integers are used for the index, this is equicalent # to iterating over the TIMESTEPS with one offset. @@ -1179,7 +1182,7 @@ def _create(self, group): initialize=[ n for n in group - if n.invest_relation_input_capacity is not None + if n.invest_relation_input_capacity[0] is not None ] ) @@ -1187,13 +1190,15 @@ def _create(self, group): initialize=[ n for n in group - if n.invest_relation_output_capacity is not None + if n.invest_relation_output_capacity[0] is not None ] ) self.INVEST_REL_IN_OUT = Set( initialize=[ - n for n in group if n.invest_relation_input_output is not None + n + for n in group + if n.invest_relation_input_output[0] is not None ] ) @@ -1566,7 +1571,7 @@ def _power_coupled(block): for p in m.PERIODS: expr = ( m.InvestmentFlowBlock.total[n, o[n], p] - ) * n.invest_relation_input_output == ( + ) * n.invest_relation_input_output[p] == ( m.InvestmentFlowBlock.total[i[n], n, p] ) self.power_coupled.add((n, p), expr) @@ -1587,7 +1592,8 @@ def _storage_capacity_inflow_invest_rule(block): for p in m.PERIODS: expr = ( m.InvestmentFlowBlock.total[i[n], n, p] - == self.total[n, p] * n.invest_relation_input_capacity + == self.total[n, p] + * n.invest_relation_input_capacity[p] ) self.storage_capacity_inflow.add((n, p), expr) @@ -1609,7 +1615,8 @@ def _storage_capacity_outflow_invest_rule(block): for p in m.PERIODS: expr = ( m.InvestmentFlowBlock.total[n, o[n], p] - == self.total[n, p] * n.invest_relation_output_capacity + == self.total[n, p] + * n.invest_relation_output_capacity[p] ) self.storage_capacity_outflow.add((n, p), expr) @@ -1880,7 +1887,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( @@ -1898,7 +1905,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/_flow.py b/src/oemof/solph/flows/_flow.py index 3ebb9051e..1452bcfde 100644 --- a/src/oemof/solph/flows/_flow.py +++ b/src/oemof/solph/flows/_flow.py @@ -111,7 +111,7 @@ class Flow(Edge): >>> f.variable_costs[2] 5 >>> f.fix[2] - 4 + np.int64(4) Creating a flow object with time-depended lower and upper bounds: diff --git a/src/oemof/solph/flows/_investment_flow_block.py b/src/oemof/solph/flows/_investment_flow_block.py index bad8c6b8d..1947e8f42 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 3d2c1d9f2..59bd83f0a 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""" @@ -326,7 +328,9 @@ def _startup_costs(self): m = self.parent_block() 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] @@ -349,7 +353,10 @@ def _shutdown_costs(self): m = self.parent_block() 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] @@ -372,7 +379,10 @@ def _activity_costs(self): m = self.parent_block() 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] @@ -395,7 +405,10 @@ def _inactivity_costs(self): m = self.parent_block() 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 ad3b1caa1..8d2996876 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_capacity ) - 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_capacity @@ -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 a3dd0e4cd..764b873ae 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_processing.py b/tests/test_processing.py index 15b09c096..a7b3ed2e2 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -164,8 +164,6 @@ def test_nodes_with_none_exclusion(self): { "balanced": True, "initial_storage_level": 0, - "invest_relation_input_capacity": 1 / 6, - "invest_relation_output_capacity": 1 / 6, "investment_age": 0, "investment_existing": 0, "investment_interest_rate": 0, @@ -180,6 +178,8 @@ def test_nodes_with_none_exclusion(self): "fixed_losses_absolute": 0, "fixed_losses_relative": 0, "inflow_conversion_factor": 1, + "invest_relation_input_capacity": 1 / 6, + "invest_relation_output_capacity": 1 / 6, "loss_rate": 0, "max_storage_level": 1, "min_storage_level": 0, @@ -204,8 +204,6 @@ def test_nodes_with_none_exclusion_old_name(self): { "balanced": True, "initial_storage_level": 0, - "invest_relation_input_capacity": 1 / 6, - "invest_relation_output_capacity": 1 / 6, "investment_age": 0, "investment_existing": 0, "investment_interest_rate": 0, @@ -220,6 +218,8 @@ def test_nodes_with_none_exclusion_old_name(self): "fixed_losses_absolute": 0, "fixed_losses_relative": 0, "inflow_conversion_factor": 1, + "invest_relation_input_capacity": 1 / 6, + "invest_relation_output_capacity": 1 / 6, "loss_rate": 0, "max_storage_level": 1, "min_storage_level": 0, 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 e7b557428..24f485400 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")