From fe3618525309939e06e457c44ad6d1230281d370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Thu, 22 Aug 2024 09:27:03 +0200 Subject: [PATCH 01/45] Add planned change to changelog --- docs/whatsnew/v0-6-0.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/whatsnew/v0-6-0.rst b/docs/whatsnew/v0-6-0.rst index 339fae4d9..122bbe7a7 100644 --- a/docs/whatsnew/v0-6-0.rst +++ b/docs/whatsnew/v0-6-0.rst @@ -10,6 +10,8 @@ API changes effectively the same) had double weight before. Also, if the initial storage level is defined, the costs just offset the objective value without changing anything else. +* The parameters `GenericStorage.nominal_storage_capacity` and + `Flow.nominal_value` are now both called `nominal_calacity`. New features ############ From 5194cad3ef2cd588b613688b1bbe001347bcdc07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Thu, 22 Aug 2024 09:47:20 +0200 Subject: [PATCH 02/45] Remove tansitional "Transformer" wrapper It was agreed to provide them for solph v0.5 but remove them with v0.6. --- docs/whatsnew/v0-6-0.rst | 3 ++ src/oemof/solph/components/__init__.py | 4 --- src/oemof/solph/components/_converter.py | 30 ------------------- .../solph/components/_offset_converter.py | 28 ----------------- .../test_components/test_offset_converter.py | 2 +- tests/test_solph_network_classes.py | 15 ---------- 6 files changed, 4 insertions(+), 78 deletions(-) diff --git a/docs/whatsnew/v0-6-0.rst b/docs/whatsnew/v0-6-0.rst index 339fae4d9..006a5f31d 100644 --- a/docs/whatsnew/v0-6-0.rst +++ b/docs/whatsnew/v0-6-0.rst @@ -10,6 +10,9 @@ API changes effectively the same) had double weight before. Also, if the initial storage level is defined, the costs just offset the objective value without changing anything else. +* Tansitional wrappers that still allowed to use "Transformer" and + "OffsetTransformer" have been removed. Use of the new names + ("Converter" and "OffsetConverter") is now obligatory. New features ############ diff --git a/src/oemof/solph/components/__init__.py b/src/oemof/solph/components/__init__.py index e53a0659e..53021185e 100644 --- a/src/oemof/solph/components/__init__.py +++ b/src/oemof/solph/components/__init__.py @@ -11,13 +11,11 @@ from . import experimental from ._converter import Converter -from ._converter import Transformer from ._extraction_turbine_chp import ExtractionTurbineCHP from ._generic_chp import GenericCHP from ._generic_storage import GenericStorage from ._link import Link from ._offset_converter import OffsetConverter -from ._offset_converter import OffsetTransformer from ._offset_converter import slope_offset_from_nonconvex_input from ._offset_converter import slope_offset_from_nonconvex_output from ._sink import Sink @@ -31,9 +29,7 @@ "GenericStorage", "OffsetConverter", "Link", - "OffsetTransformer", "Sink", - "Transformer", "Source", "slope_offset_from_nonconvex_input", "slope_offset_from_nonconvex_output", diff --git a/src/oemof/solph/components/_converter.py b/src/oemof/solph/components/_converter.py index ab660ea02..255fd0111 100644 --- a/src/oemof/solph/components/_converter.py +++ b/src/oemof/solph/components/_converter.py @@ -21,8 +21,6 @@ """ -from warnings import warn - from oemof.network import Node from pyomo.core import BuildAction from pyomo.core import Constraint @@ -138,34 +136,6 @@ def constraint_group(self): return ConverterBlock -# --- BEGIN: To be removed for versions >= v0.6 --- -class Transformer(Converter): - def __init__( - self, - label=None, - inputs=None, - outputs=None, - conversion_factors=None, - custom_attributes=None, - ): - super().__init__( - label=label, - inputs=inputs, - outputs=outputs, - conversion_factors=conversion_factors, - custom_attributes=custom_attributes, - ) - warn( - "solph.components.Transformer has been renamed to" - " solph.components.Converter. The transitional wrapper" - " will be deleted in the future.", - FutureWarning, - ) - - -# --- END --- - - class ConverterBlock(ScalarBlock): r"""Block for the linear relation of nodes with type :class:`~oemof.solph.components._converter.ConverterBlock` diff --git a/src/oemof/solph/components/_offset_converter.py b/src/oemof/solph/components/_offset_converter.py index b078c9ced..403bae9df 100644 --- a/src/oemof/solph/components/_offset_converter.py +++ b/src/oemof/solph/components/_offset_converter.py @@ -378,34 +378,6 @@ def plot_partload(self, bus, tstep): return fig, ax -# --- BEGIN: To be removed for versions >= v0.6 --- -class OffsetTransformer(OffsetConverter): - def __init__( - self, - inputs, - outputs, - label=None, - coefficients=None, - custom_attributes=None, - ): - super().__init__( - label=label, - inputs=inputs, - outputs=outputs, - coefficients=coefficients, - custom_attributes=custom_attributes, - ) - warn( - "solph.components.OffsetTransformer has been renamed to" - " solph.components.OffsetConverter. The transitional wrapper" - " will be deleted in the future.", - FutureWarning, - ) - - -# --- END --- - - class OffsetConverterBlock(ScalarBlock): r"""Block for the relation of nodes with type :class:`~oemof.solph.components._offset_converter.OffsetConverter` diff --git a/tests/test_components/test_offset_converter.py b/tests/test_components/test_offset_converter.py index 997a1140c..f3d7d2e09 100644 --- a/tests/test_components/test_offset_converter.py +++ b/tests/test_components/test_offset_converter.py @@ -223,7 +223,7 @@ def test_wrong_number_of_coefficients(): bus1 = solph.Bus() bus2 = solph.Bus() with pytest.raises(ValueError, match="Two coefficients"): - solph.components.OffsetTransformer( + solph.components.OffsetConverter( inputs={ bus1: solph.Flow(nominal_value=2, nonconvex=solph.NonConvex()) }, diff --git a/tests/test_solph_network_classes.py b/tests/test_solph_network_classes.py index 71726a286..7feae7ea3 100644 --- a/tests/test_solph_network_classes.py +++ b/tests/test_solph_network_classes.py @@ -72,21 +72,6 @@ def test_converter_missing_input_create_empty_dict(self): assert converter.inputs == {} -def test_transformer_wrapper(): - # two warnings: Wrapper and no inputs/outputs - with pytest.warns(FutureWarning): - with pytest.warns(SuspiciousUsageWarning): - solph.components.Transformer() - - -def test_offset_transformer_wrapper(): - with pytest.warns(FutureWarning): - solph.components.OffsetTransformer( - inputs={solph.Bus("bus"): solph.Flow(nonconvex=solph.NonConvex())}, - outputs={}, - ) - - def test_wrong_combination_invest_and_nominal_value(): msg = "For backward compatibility, the option investment overwrites" with pytest.raises(AttributeError, match=msg): From cd0b8b5303795c4722e05598a06e70d2c25f1d21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Thu, 22 Aug 2024 09:58:02 +0200 Subject: [PATCH 03/45] Remove transitional wrappers for Flow agruments It was agreed to provide them for solph v0.5 but remove them with v0.6. --- docs/whatsnew/v0-6-0.rst | 4 ++++ src/oemof/solph/flows/_flow.py | 31 ----------------------------- tests/test_flows/test_flow_class.py | 20 ------------------- tests/test_solph_network_classes.py | 12 ----------- 4 files changed, 4 insertions(+), 63 deletions(-) diff --git a/docs/whatsnew/v0-6-0.rst b/docs/whatsnew/v0-6-0.rst index 006a5f31d..2a96e2b1c 100644 --- a/docs/whatsnew/v0-6-0.rst +++ b/docs/whatsnew/v0-6-0.rst @@ -13,6 +13,10 @@ API changes * Tansitional wrappers that still allowed to use "Transformer" and "OffsetTransformer" have been removed. Use of the new names ("Converter" and "OffsetConverter") is now obligatory. +* Tansitional wrappers that still allowed to use "investment", + "summed_min", and "summed_max" as arguments to initialise a Flow + have been removed. Use of the new names ("nominal_value", + "full_load_time_min", and "full_load_time_max") is now obligatory. New features ############ diff --git a/src/oemof/solph/flows/_flow.py b/src/oemof/solph/flows/_flow.py index 6ed8e97ab..d642f8d32 100644 --- a/src/oemof/solph/flows/_flow.py +++ b/src/oemof/solph/flows/_flow.py @@ -137,11 +137,6 @@ def __init__( lifetime=None, age=None, fixed_costs=None, - # --- BEGIN: To be removed for versions >= v0.6 --- - investment=None, - summed_max=None, - summed_min=None, - # --- END --- custom_attributes=None, ): # TODO: Check if we can inherit from pyomo.core.base.var _VarData @@ -150,32 +145,6 @@ def __init__( # is created. E.g. create the variable in the energy system and # populate with information afterwards when creating objects. - # --- BEGIN: The following code can be removed for versions >= v0.6 --- - if investment is not None: - msg = ( - "For backward compatibility," - " the option investment overwrites the option nominal_value." - + " Both options cannot be set at the same time." - ) - if nominal_value is not None: - raise AttributeError(msg) - else: - warn(msg, FutureWarning) - nominal_value = investment - - msg = ( - "\nThe parameter '{0}' is deprecated and will be removed " - + "in version v0.6.\nUse the parameter '{1}', " - + "to avoid this warning and future problems. " - ) - if summed_max is not None: - warn(msg.format("summed_max", "full_load_time_max"), FutureWarning) - full_load_time_max = summed_max - if summed_min is not None: - warn(msg.format("summed_min", "full_load_time_min"), FutureWarning) - full_load_time_min = summed_min - # --- END --- - super().__init__() if custom_attributes is not None: diff --git a/tests/test_flows/test_flow_class.py b/tests/test_flows/test_flow_class.py index f59c907b6..42859f6f8 100644 --- a/tests/test_flows/test_flow_class.py +++ b/tests/test_flows/test_flow_class.py @@ -9,32 +9,12 @@ SPDX-License-Identifier: MIT """ -import warnings - import pytest from oemof.solph import NonConvex from oemof.solph.flows import Flow -def test_summed_max_future_warning(): - """Can be removed with v0.6.""" - msg = "The parameter 'summed_max' is deprecated and will be removed" - with warnings.catch_warnings(record=True) as w: - Flow(nominal_value=1, summed_max=2) - assert len(w) == 1 - assert msg in str(w[-1].message) - - -def test_summed_min_future_warning(): - """Can be removed with v0.6.""" - msg = "The parameter 'summed_min' is deprecated and will be removed" - with warnings.catch_warnings(record=True) as w: - Flow(nominal_value=1, summed_min=2) - assert len(w) == 1 - assert msg in str(w[-1].message) - - def test_source_with_full_load_time_max(): Flow(nominal_value=1, full_load_time_max=2) diff --git a/tests/test_solph_network_classes.py b/tests/test_solph_network_classes.py index 7feae7ea3..e3febfd2d 100644 --- a/tests/test_solph_network_classes.py +++ b/tests/test_solph_network_classes.py @@ -72,18 +72,6 @@ def test_converter_missing_input_create_empty_dict(self): assert converter.inputs == {} -def test_wrong_combination_invest_and_nominal_value(): - msg = "For backward compatibility, the option investment overwrites" - with pytest.raises(AttributeError, match=msg): - solph.flows.Flow(investment=solph.Investment(), nominal_value=4) - - -def test_invest_attribute_warning(): - msg = "For backward compatibility, the option investment overwrites" - with pytest.warns(FutureWarning, match=msg): - solph.flows.Flow(investment=solph.Investment()) - - def test_fixed_costs_warning(): msg = ( "Be aware that the fixed costs attribute is only\n" From 0463b4dc226ac40662d030227a6251be0aa94b40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Thu, 22 Aug 2024 11:30:03 +0200 Subject: [PATCH 04/45] Add removal hint for OffsetConverter coefficients --- .../solph/components/_offset_converter.py | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/oemof/solph/components/_offset_converter.py b/src/oemof/solph/components/_offset_converter.py index 403bae9df..84570b866 100644 --- a/src/oemof/solph/components/_offset_converter.py +++ b/src/oemof/solph/components/_offset_converter.py @@ -151,30 +151,26 @@ def __init__( custom_properties=custom_attributes, ) + # --- BEGIN: To be removed for versions >= v0.7 --- # this part is used for the transition phase from the old # OffsetConverter API to the new one. It calcualtes the # conversion_factors and normed_offsets from the coefficients and the # outputs information on min and max. - if ( - coefficients is not None - and conversion_factors is None - and normed_offsets is None - ): + if coefficients is not None: + if conversion_factors is not None or normed_offsets is not None: + msg = ( + "The deprecated argument `coefficients` cannot be used " + "in combination with its replacements " + "(`conversion_factors` and `normed_offsets`)." + ) + raise TypeError(msg) + normed_offsets, conversion_factors = ( self.normed_offset_and_conversion_factors_from_coefficients( coefficients ) ) - - elif coefficients is not None and ( - conversion_factors is not None or normed_offsets is not None - ): - msg = ( - "The deprecated argument `coefficients` cannot be used in " - "combination with its replacements (`conversion_factors` and " - "`normed_offsets`)." - ) - raise TypeError(msg) + # --- END --- _reference_flow = [v for v in self.inputs.values() if v.nonconvex] _reference_flow += [v for v in self.outputs.values() if v.nonconvex] @@ -252,6 +248,7 @@ def __init__( def constraint_group(self): return OffsetConverterBlock + # --- BEGIN: To be removed for versions >= v0.7 --- def normed_offset_and_conversion_factors_from_coefficients( self, coefficients ): @@ -318,6 +315,8 @@ def normed_offset_and_conversion_factors_from_coefficients( return normed_offsets, conversion_factors + # --- END --- + def plot_partload(self, bus, tstep): """Create a matplotlib figure of the flow to nonconvex flow relation. From 343cbc905f43a61bbb873ac2de1679032be00a73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Thu, 22 Aug 2024 11:52:52 +0200 Subject: [PATCH 05/45] Package requiring pyomo >= 6.8.0 This allows to work with numpy >= 2.0.0. --- docs/changelog.rst | 1 + docs/whatsnew/v0-5-5.rst | 12 ++++++++++++ pyproject.toml | 4 ++-- src/oemof/solph/__init__.py | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 docs/whatsnew/v0-5-5.rst 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..2bef736a8 --- /dev/null +++ b/docs/whatsnew/v0-5-5.rst @@ -0,0 +1,12 @@ +v0.5.5 () +-------------------------- + +Bug fixes +######### + +* Update required Pyomo version to allow working with numpy >= 2.0.0. + +Contributors +############ + +* Patrik Schönfeldt diff --git a/pyproject.toml b/pyproject.toml index 60f422d70..7626d8049 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,9 +57,9 @@ requires-python = ">=3.8" dependencies = [ "blinker", "dill", - "numpy < 2.0.0", + "numpy", "pandas >= 2.0.0", - "pyomo >= 6.6.0, < 7.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 ebdb22c2c..679f9bd05 100644 --- a/src/oemof/solph/__init__.py +++ b/src/oemof/solph/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.5.4" +__version__ = "0.5.5b1" from . import buses from . import components From 39717e5ffdb71da019c103ad469b753f2332072f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Thu, 22 Aug 2024 12:16:36 +0200 Subject: [PATCH 06/45] Require up-to-date Pandas It's almost impossible to have doctests that work with both, numpy 1.x and numpy 2.x. Introducin a lower bound to Pandas as well allows to remove Pandas <2.2 compatibility code. --- pyproject.toml | 4 ++-- src/oemof/solph/_energy_system.py | 10 +--------- src/oemof/solph/_plumbing.py | 6 +++--- src/oemof/solph/flows/_flow.py | 2 +- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7626d8049..d50b4de2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,8 +57,8 @@ requires-python = ">=3.8" dependencies = [ "blinker", "dill", - "numpy", - "pandas >= 2.0.0", + "numpy >= 2.0.0", + "pandas >= 2.2.0", "pyomo >= 6.8.0", "networkx", "oemof.tools >= 0.4.3", diff --git a/src/oemof/solph/_energy_system.py b/src/oemof/solph/_energy_system.py index 79681ad04..51f02dc35 100644 --- a/src/oemof/solph/_energy_system.py +++ b/src/oemof/solph/_energy_system.py @@ -302,13 +302,5 @@ def create_time_index( number = round(hoy / interval) if start is None: start = f"1/1/{year}" - try: - time_index = pd.date_range( - start, periods=number + 1, freq=f"{interval}h" - ) - except ValueError: - # Pandas <2.2 compatibility - time_index = pd.date_range( - start, periods=number + 1, freq=f"{interval}H" - ) + time_index = pd.date_range(start, periods=number + 1, freq=f"{interval}h") return time_index diff --git a/src/oemof/solph/_plumbing.py b/src/oemof/solph/_plumbing.py index bb6ed5b80..50b7e28cb 100644 --- a/src/oemof/solph/_plumbing.py +++ b/src/oemof/solph/_plumbing.py @@ -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] diff --git a/src/oemof/solph/flows/_flow.py b/src/oemof/solph/flows/_flow.py index 6ed8e97ab..2189512f2 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: From 6ed34e4b39e25e4c401e66d2496a15a66bea7995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Thu, 22 Aug 2024 13:08:47 +0200 Subject: [PATCH 07/45] Rename Flow argument from nominal_value to nominal_capacity I still have to keep explaining people what the nominal_value is meant for and they just understand if I name it nominal_capacity, instead. Reflecting a bit, the capacity of a Flow (e.g. a power line) is easier to interpret as the "nominal value". Thus the refactoring. --- src/oemof/solph/flows/_flow.py | 50 +++++++++++++++++++++-------- tests/test_flows/test_flow_class.py | 4 +-- tests/test_solph_network_classes.py | 6 ++-- 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/oemof/solph/flows/_flow.py b/src/oemof/solph/flows/_flow.py index 6ed8e97ab..ded156642 100644 --- a/src/oemof/solph/flows/_flow.py +++ b/src/oemof/solph/flows/_flow.py @@ -41,10 +41,10 @@ class Flow(Edge): Parameters ---------- - nominal_value : numeric, :math:`P_{nom}` or + nominal_capacity : numeric, :math:`P_{nom}` or :class:`Investment ` - The nominal value of the flow, either fixed or as an investement - optimisation. If this value is set the corresponding optimization + The nominal calacity of the flow, either fixed or as an investement + optimisation. If this value is set, the corresponding optimization variable of the flow object will be bounded by this value multiplied by min(lower bound)/max(upper bound). variable_costs : numeric (iterable or scalar), default: 0, :math:`c` @@ -68,11 +68,11 @@ class Flow(Edge): full_load_time_max : numeric, :math:`t_{full\_load,max}` Maximum energy transported by the flow expressed as the time (in hours) that the flow would have to run at nominal capacity - (`nominal_value`). + (`nominal_capacity`). full_load_time_min : numeric, :math:`t_{full\_load,min}` Minimum energy transported by the flow expressed as the time (in hours) that the flow would have to run at nominal capacity - (`nominal_value`). + (`nominal_capacity`). integer : boolean Set True to bound the flow values to integers. nonconvex : :class:`NonConvex ` @@ -107,7 +107,7 @@ class Flow(Edge): -------- Creating a fixed flow object: - >>> f = Flow(nominal_value=2, fix=[10, 4, 4], variable_costs=5) + >>> f = Flow(nominal_capacity=2, fix=[10, 4, 4], variable_costs=5) >>> f.variable_costs[2] 5 >>> f.fix[2] @@ -115,14 +115,17 @@ class Flow(Edge): Creating a flow object with time-depended lower and upper bounds: - >>> f1 = Flow(min=[0.2, 0.3], max=0.99, nominal_value=100) + >>> f1 = Flow(min=[0.2, 0.3], max=0.99, nominal_capacity=100) >>> f1.max[1] 0.99 """ # noqa: E501 def __init__( self, + nominal_capacity=None, + # --- BEGIN: To be removed for versions >= v0.7 --- nominal_value=None, + # --- END --- variable_costs=0, min=None, max=None, @@ -176,6 +179,27 @@ def __init__( full_load_time_min = summed_min # --- END --- + # --- BEGIN: The following code can be removed for versions >= v0.7 --- + if nominal_value is not None: + msg = ( + "For backward compatibility," + " the option nominal_value overwrites the option" + " nominal_capacity." + + " Both options cannot be set at the same time." + ) + if nominal_capacity is not None: + raise AttributeError(msg) + else: + warn(msg, FutureWarning) + nominal_capacity = nominal_value + + msg = ( + "\nThe parameter '{0}' is deprecated and will be removed " + + "in version v0.6.\nUse the parameter '{1}', " + + "to avoid this warning and future problems. " + ) + # --- END --- + super().__init__() if custom_attributes is not None: @@ -189,12 +213,12 @@ def __init__( "{} must be a finite value. Passing an infinite " "value is not allowed." ) - if isinstance(nominal_value, numbers.Real): - if not math.isfinite(nominal_value): - raise ValueError(infinite_error_msg.format("nominal_value")) - self.nominal_value = nominal_value - elif isinstance(nominal_value, Investment): - self.investment = nominal_value + if isinstance(nominal_capacity, numbers.Real): + if not math.isfinite(nominal_capacity): + raise ValueError(infinite_error_msg.format("nominal_capacity")) + self.nominal_value = nominal_capacity + elif isinstance(nominal_capacity, Investment): + self.investment = nominal_capacity if fixed_costs is not None: msg = ( diff --git a/tests/test_flows/test_flow_class.py b/tests/test_flows/test_flow_class.py index f59c907b6..4ddcda1fb 100644 --- a/tests/test_flows/test_flow_class.py +++ b/tests/test_flows/test_flow_class.py @@ -21,7 +21,7 @@ def test_summed_max_future_warning(): """Can be removed with v0.6.""" msg = "The parameter 'summed_max' is deprecated and will be removed" with warnings.catch_warnings(record=True) as w: - Flow(nominal_value=1, summed_max=2) + Flow(nominal_capacity=1, summed_max=2) assert len(w) == 1 assert msg in str(w[-1].message) @@ -30,7 +30,7 @@ def test_summed_min_future_warning(): """Can be removed with v0.6.""" msg = "The parameter 'summed_min' is deprecated and will be removed" with warnings.catch_warnings(record=True) as w: - Flow(nominal_value=1, summed_min=2) + Flow(nominal_capacity=1, summed_min=2) assert len(w) == 1 assert msg in str(w[-1].message) diff --git a/tests/test_solph_network_classes.py b/tests/test_solph_network_classes.py index 71726a286..5edad18c9 100644 --- a/tests/test_solph_network_classes.py +++ b/tests/test_solph_network_classes.py @@ -128,12 +128,12 @@ def test_flow_with_fix_and_min_max(): def test_infinite_values(): - msg1 = "nominal_value must be a finite value" + msg1 = "nominal_capacity must be a finite value" msg2 = "max must be a finite value" with pytest.raises(ValueError, match=msg1): - solph.flows.Flow(nominal_value=float("+inf")) + solph.flows.Flow(nominal_capacity=float("+inf")) with pytest.raises(ValueError, match=msg2): - solph.flows.Flow(nominal_value=1, max=float("+inf")) + solph.flows.Flow(nominal_capacity=1, max=float("+inf")) def test_attributes_needing_nominal_value_get_it(): From e91c7fc97e46a66858b8c3116d26d60d5171b0fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Thu, 22 Aug 2024 13:21:27 +0200 Subject: [PATCH 08/45] Test Flow(nominal_value) wrapper --- tests/test_flows/test_simple_flow.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_flows/test_simple_flow.py b/tests/test_flows/test_simple_flow.py index 3c351e0ce..8dca29043 100644 --- a/tests/test_flows/test_simple_flow.py +++ b/tests/test_flows/test_simple_flow.py @@ -55,3 +55,17 @@ def test_full_load_time_min(): flow_result = list(_run_flow_model(flow)["flow"][:-1]) assert flow_result == pytest.approx(4 * [2] + [1] + 5 * [0]) + + +# --- BEGIN: The following code can be removed for versions >= v0.7 --- +def test_nominal_value_warning(): + with pytest.warns(FutureWarning, match="nominal_value"): + _ = solph.flows.Flow(nominal_value=2) + + +def test_nominal_value_error(): + with pytest.raises(AttributeError, match="nominal_value"): + _ = solph.flows.Flow(nominal_value=2, nominal_capacity=1) + + +# --- END --- From 375cf7c7d8180373a1da21ca4aee868741aa8d35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Thu, 22 Aug 2024 13:57:32 +0200 Subject: [PATCH 09/45] Rename Flow argument nominal_value to nominal_capacity --- docs/usage.rst | 70 +++++++++---------- docs/whatsnew/v0-2-2.rst | 2 +- docs/whatsnew/v0-5-1.rst | 2 +- docs/whatsnew/v0-5-3.rst | 2 +- docs/whatsnew/v0-6-0.rst | 2 +- examples/activity_costs/activity_costs.py | 6 +- examples/basic_example/basic_example.py | 18 ++--- .../dual_variable_example.py | 12 ++-- examples/electrical/transshipment.py | 14 ++-- .../emission_constraint.py | 8 ++- examples/excel_reader/dispatch.py | 12 ++-- .../flexible_modelling/add_constraints.py | 6 +- .../flexible_modelling/saturating_storage.py | 6 +- examples/flow_count_limit/flow_count_limit.py | 8 +-- .../example_generic_invest.py | 8 +-- examples/gradient_example/gradient_example.py | 4 +- .../diesel_genset_nonconvex_investment.py | 12 ++-- .../house_with_nonconvex_investment.py | 8 +-- .../house_without_nonconvex_investment.py | 10 +-- .../minimal_invest.py | 4 +- examples/min_max_runtimes/min_max_runtimes.py | 6 +- ...fset_diesel_genset_nonconvex_investment.py | 12 ++-- examples/simple_dispatch/simple_dispatch.py | 23 +++--- .../startup_shutdown.py | 6 +- .../storage_balanced_unbalanced/storage.py | 6 +- examples/storage_costs/storage_costs.py | 8 +-- .../v1_invest_optimize_all_technologies.py | 9 +-- ...v2_invest_optimize_only_gas_and_storage.py | 8 +-- ...optimize_only_storage_with_fossil_share.py | 10 +-- ...mize_all_technologies_with_fossil_share.py | 11 +-- .../storage_level_constraint.py | 10 +-- .../non_equidistant_time_step_example.py | 6 +- .../simple_time_step_example.py | 6 +- examples/tuple_as_labels/tuple_as_label.py | 20 ++++-- examples/variable_chp/variable_chp.py | 16 +++-- src/oemof/solph/_console_scripts.py | 4 +- .../components/_extraction_turbine_chp.py | 2 +- .../solph/components/_generic_storage.py | 4 +- src/oemof/solph/components/_link.py | 4 +- .../solph/components/_offset_converter.py | 2 +- .../_piecewise_linear_converter.py | 2 +- .../solph/constraints/equate_variables.py | 4 +- src/oemof/solph/constraints/integral_limit.py | 4 +- .../solph/constraints/investment_limit.py | 6 +- .../flows/experimental/_electrical_line.py | 2 +- tests/test_components.py | 10 +-- .../test_components/test_offset_converter.py | 24 ++++--- tests/test_components/test_sink.py | 2 +- tests/test_components/test_source.py | 2 +- tests/test_components/test_storage.py | 32 +++++---- .../test_constraints_module.py | 8 +-- tests/test_constraints/test_equate_flows.py | 8 +-- .../test_constraints/test_flow_count_limit.py | 8 +-- tests/test_constraints/test_storage_level.py | 18 +++-- tests/test_flows/test_flow_class.py | 4 +- tests/test_flows/test_non_convex_flow.py | 18 ++--- tests/test_flows/test_simple_flow.py | 6 +- tests/test_grouping.py | 6 +- tests/test_models.py | 12 ++-- tests/test_non_equidistant_time_index.py | 4 +- tests/test_options.py | 2 +- tests/test_outputlib/__init__.py | 22 +++--- tests/test_processing.py | 4 +- .../test_connect_invest.py | 8 +-- .../test_add_constraints.py | 6 +- .../test_generic_chp/test_generic_chp.py | 4 +- .../test_simple_invest_fixed.py | 6 +- .../test_solph/test_lopf/test_lopf.py | 12 ++-- .../test_multi_period_dispatch_model.py | 42 ++++++----- .../test_multi_period_investment_model.py | 38 +++++----- .../test_piecewiselineartransformer.py | 6 +- .../test_simple_model/test_simple_dispatch.py | 23 +++--- .../test_simple_dispatch_one.py | 12 ++-- ...t_simple_dispatch_one_explicit_timemode.py | 12 ++-- .../test_simple_model/test_simple_invest.py | 22 +++--- .../test_invest_storage_regression.py | 8 +-- .../test_storage_investment.py | 15 ++-- .../test_storage_with_tuple_label.py | 17 +++-- .../test_variable_chp/test_variable_chp.py | 16 +++-- tests/test_solph_network_classes.py | 2 +- tests/test_warnings.py | 6 +- 81 files changed, 470 insertions(+), 380 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index 58247d9d9..a51fb2d66 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -266,13 +266,13 @@ Sink (basic) A sink is normally used to define the demand within an energy model but it can also be used to detect excesses. The example shows the electricity demand of the electricity_bus defined above. -The *'my_demand_series'* should be sequence of normalised valueswhile the *'nominal_value'* is the maximum demand the normalised sequence is multiplied with. +The *'my_demand_series'* should be sequence of normalised valueswhile the *'nominal_capacity'* is the maximum demand the normalised sequence is multiplied with. Giving *'my_demand_series'* as parameter *'fix'* means that the demand cannot be changed by the solver. .. code-block:: python solph.components.Sink(label='electricity_demand', inputs={electricity_bus: solph.flows.Flow( - fix=my_demand_series, nominal_value=nominal_demand)}) + fix=my_demand_series, nominal_capacity=nominal_demand)}) In contrast to the demand sink the excess sink has normally less restrictions but is open to take the whole excess. @@ -290,21 +290,21 @@ Source (basic) A source can represent a pv-system, a wind power plant, an import of natural gas or a slack variable to avoid creating an in-feasible model. -While a wind power plant will have as feed-in depending on the weather conditions the natural_gas import might be restricted by maximum value (*nominal_value*) and an annual limit (*full_load_time_max*). +While a wind power plant will have as feed-in depending on the weather conditions the natural_gas import might be restricted by maximum value (*nominal_capacity*) and an annual limit (*full_load_time_max*). As we do have to pay for imported gas we should set variable costs. Comparable to the demand series an *fix* is used to define a fixed the normalised output of a wind power plant. Alternatively, you might use *max* to allow for easy curtailment. -The *nominal_value* sets the installed capacity. +The *nominal_capacity* sets the installed capacity. .. code-block:: python solph.components.Source( label='import_natural_gas', outputs={my_energysystem.groups['natural_gas']: solph.flows.Flow( - nominal_value=1000, full_load_time_max=1000000, variable_costs=50)}) + nominal_capacity=1000, full_load_time_max=1000000, variable_costs=50)}) solph.components.Source(label='wind', outputs={electricity_bus: solph.flows.Flow( - fix=wind_power_feedin_series, nominal_value=1000000)}) + fix=wind_power_feedin_series, nominal_capacity=1000000)}) .. note:: The Source class is only a plug and provides no additional constraints or variables. @@ -327,7 +327,7 @@ A condensing power plant can be defined by a converter with one input (fuel) and solph.components.Converter( label="pp_gas", inputs={bgas: solph.flows.Flow()}, - outputs={b_el: solph.flows.Flow(nominal_value=10e10)}, + outputs={b_el: solph.flows.Flow(nominal_capacity=10e10)}, conversion_factors={electricity_bus: 0.58}) A CHP power plant would be defined in the same manner but with two outputs: @@ -341,8 +341,8 @@ A CHP power plant would be defined in the same manner but with two outputs: solph.components.Converter( label='pp_chp', inputs={b_gas: Flow()}, - outputs={b_el: Flow(nominal_value=30), - b_th: Flow(nominal_value=40)}, + outputs={b_el: Flow(nominal_capacity=30), + b_th: Flow(nominal_capacity=40)}, conversion_factors={b_el: 0.3, b_th: 0.4}) A CHP power plant with 70% coal and 30% natural gas can be defined with two inputs and two outputs: @@ -357,8 +357,8 @@ A CHP power plant with 70% coal and 30% natural gas can be defined with two inpu solph.components.Converter( label='pp_chp', inputs={b_gas: Flow(), b_coal: Flow()}, - outputs={b_el: Flow(nominal_value=30), - b_th: Flow(nominal_value=40)}, + outputs={b_el: Flow(nominal_capacity=30), + b_th: Flow(nominal_capacity=40)}, conversion_factors={b_el: 0.3, b_th: 0.4, b_coal: 0.7, b_gas: 0.3}) @@ -428,7 +428,7 @@ applies when the second flow is zero (*`conversion_factor_full_condensation`*). solph.components._extractionTurbineCHP( label='variable_chp_gas', - inputs={b_gas: solph.flows.Flow(nominal_value=10e10)}, + inputs={b_gas: solph.flows.Flow(nominal_capacity=10e10)}, outputs={b_el: solph.flows.Flow(), b_th: solph.flows.Flow()}, conversion_factors={b_el: 0.3, b_th: 0.5}, conversion_factor_full_condensation={b_el: 0.5}) @@ -559,15 +559,15 @@ GenericStorage (component) A component to model a storage with its basic characteristics. The GenericStorage is designed for one input and one output. The ``nominal_storage_capacity`` of the storage signifies the storage capacity. You can either set it to the net capacity or to the gross capacity and limit it using the min/max attribute. -To limit the input and output flows, you can define the ``nominal_value`` in the Flow objects. +To limit the input and output flows, you can define the ``nominal_capacity`` in the Flow objects. Furthermore, an efficiency for loading, unloading and a loss rate can be defined. .. code-block:: python solph.components.GenericStorage( label='storage', - inputs={b_el: solph.flows.Flow(nominal_value=9, variable_costs=10)}, - outputs={b_el: solph.flows.Flow(nominal_value=25, variable_costs=10)}, + inputs={b_el: solph.flows.Flow(nominal_capacity=9, variable_costs=10)}, + outputs={b_el: solph.flows.Flow(nominal_capacity=25, variable_costs=10)}, loss_rate=0.001, nominal_storage_capacity=50, inflow_conversion_factor=0.98, outflow_conversion_factor=0.8) @@ -588,8 +588,8 @@ The following code block shows an example of the storage parametrization for the solph.components.GenericStorage( label='storage', - inputs={b_el: solph.flows.Flow(nominal_value=9, variable_costs=10)}, - outputs={b_el: solph.flows.Flow(nominal_value=25, variable_costs=10)}, + inputs={b_el: solph.flows.Flow(nominal_capacity=9, variable_costs=10)}, + outputs={b_el: solph.flows.Flow(nominal_capacity=25, variable_costs=10)}, loss_rate=0.001, nominal_storage_capacity=50, initial_storage_level=0.5, balanced=True, inflow_conversion_factor=0.98, outflow_conversion_factor=0.8) @@ -625,7 +625,7 @@ Based on the `GenericStorage` object the `GenericInvestmentStorageBlock` adds tw * Invest into the flow parameters e.g. a turbine or a pump * Invest into capacity of the storage e.g. a basin or a battery cell -Investment in this context refers to the value of the variable for the 'nominal_value' (installed capacity) in the investment mode. +Investment in this context refers to the value of the variable for the 'nominal_capacity' (installed capacity) in the investment mode. As an addition to other flow-investments, the storage class implements the possibility to couple or decouple the flows with the capacity of the storage. @@ -643,8 +643,8 @@ The following example pictures a Pumped Hydroelectric Energy Storage (PHES). Bot solph.components.GenericStorage( label='PHES', - inputs={b_el: solph.flows.Flow(nominal_value=solph.Investment(ep_costs=500))}, - outputs={b_el: solph.flows.Flow(nominal_value=solph.Investment(ep_costs=500)}, + inputs={b_el: solph.flows.Flow(nominal_capacity=solph.Investment(ep_costs=500))}, + outputs={b_el: solph.flows.Flow(nominal_capacity=solph.Investment(ep_costs=500)}, loss_rate=0.001, inflow_conversion_factor=0.98, outflow_conversion_factor=0.8), investment = solph.Investment(ep_costs=40)) @@ -755,7 +755,7 @@ Then we can create our component with the buses attached to it. ... label='boiler', ... inputs={ ... bfuel: solph.flows.Flow( - ... nominal_value=P_out_max, + ... nominal_capacity=P_out_max, ... max=l_max, ... min=l_min, ... nonconvex=solph.NonConvex() @@ -775,7 +775,7 @@ Then we can create our component with the buses attached to it. will serve as the reference for the `conversion_factors` and the `normed_offsets`. The `NonConvex` flow also holds - - the `nominal_value` (or `Investment` in case of investment optimization), + - the `nominal_capacity` (can be `Investment` in case of investment optimization), - the `min` and - the `max` attributes. @@ -897,7 +897,7 @@ This small example of PV, grid and SinkDSM shows how to use the component grid = solph.components.Source(label='Grid', outputs={ b_elec: solph.flows.Flow( - nominal_value=10000, + nominal_capacity=10000, variable_costs=50)} ) es.add(grid) @@ -907,7 +907,7 @@ This small example of PV, grid and SinkDSM shows how to use the component outputs={ b_elec: solph.flows.Flow( fix=data['pv'], - nominal_value=3.5)} + nominal_capacity=3.5)} ) es.add(s_wind) @@ -956,7 +956,7 @@ The annual savings by building up new capacity must therefore compensate the ann See the API of the :py:class:`~oemof.solph.options.Investment` class to see all possible parameters. Basically, an instance of the Investment class can be added to a Flow, a -Storage or a DSM Sink. All parameters that usually refer to the *nominal_value/capacity* will +Storage or a DSM Sink. All parameters that usually refer to the *nominal_capacity* will now refer to the investment variables and existing capacity. It is also possible to set a maximum limit for the capacity that can be build. If existing capacity is considered for a component with investment mode enabled, @@ -981,7 +981,7 @@ turbines. solph.components.Source(label='new_wind_pp', outputs={electricity: solph.flows.Flow( fix=wind_power_time_series, - nominal_value=solph.Investment(ep_costs=epc, maximum=50000))}) + nominal_capacity=solph.Investment(ep_costs=epc, maximum=50000))}) Let's slightly alter the case and consider for already existing wind power capacity of 20,000 kW. We're still expecting the total wind power capacity, thus we @@ -991,7 +991,7 @@ allow for 30,000 kW of new installations and formulate as follows. solph.components.Source(label='new_wind_pp', outputs={electricity: solph.flows.Flow( fix=wind_power_time_series, - nominal_value=solph.Investment(ep_costs=epc, + nominal_capacity=solph.Investment(ep_costs=epc, maximum=30000, existing=20000))}) @@ -1026,7 +1026,7 @@ example of a converter: label='converter_nonconvex', inputs={bus_0: solph.flows.Flow()}, outputs={bus_1: solph.flows.Flow( - nominal_value=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=4, maximum=100, minimum=20, @@ -1183,7 +1183,7 @@ Then you add all the *components* and *buses* to your energy system, just as you label="electricity_demand", inputs={ electricity_bus: solph.flows.Flow( - nominal_value=1000, fix=[0.8] * len(my_index) + nominal_capacity=1000, fix=[0.8] * len(my_index) ) }, ) @@ -1212,7 +1212,7 @@ Here is an example inputs={hydrogen_bus: solph.flows.Flow()}, outputs={ electricity_bus: solph.flows.Flow( - nominal_value=solph.Investment( + nominal_capacity=solph.Investment( maximum=1000, ep_costs=1e6, lifetime=30, @@ -1246,7 +1246,7 @@ This would mean that for investments in the particular period, these values woul inputs={hydrogen_bus: solph.flows.Flow()}, outputs={ electricity_bus: solph.flows.Flow( - nominal_value=solph.Investment( + nominal_capacity=solph.Investment( maximum=1000, ep_costs=[1e6, 1.1e6], lifetime=30, @@ -1273,7 +1273,7 @@ For components that is not invested into, you also can specify some additional a inputs={coal_bus: solph.flows.Flow()}, outputs={ electricity_bus: solph.flows.Flow( - nominal_value=600, + nominal_capacity=600, max=1, min=0.4, lifetime=50, @@ -1377,9 +1377,9 @@ class, and only the optimal dispatch strategy of an existing asset with a given inputs={b_gas: solph.flows.Flow()}, outputs={b_el: solph.flows.Flow( nonconvex=solph.NonConvex(), - nominal_value=30, + nominal_capacity=30, min=0.5), - b_th: solph.flows.Flow(nominal_value=40)}, + b_th: solph.flows.Flow(nominal_capacity=40)}, conversion_factors={b_el: 0.3, b_th: 0.4}) The class :py:class:`~oemof.solph.options.NonConvex` for the electrical output of the created Converter (i.e., CHP) @@ -1421,7 +1421,7 @@ This nonlinearity is linearised in the min=0.2, max=1, nonconvex=solph.NonConvex(), - nominal_value=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=90, maximum=150, # required for the linearization ), diff --git a/docs/whatsnew/v0-2-2.rst b/docs/whatsnew/v0-2-2.rst index 2af434373..d2c191e6d 100644 --- a/docs/whatsnew/v0-2-2.rst +++ b/docs/whatsnew/v0-2-2.rst @@ -29,7 +29,7 @@ New features `invest_relation_output_capacity` replace the existing attributes `nominal_input_capacity_ratio` and `nominal_input_capacity_ratio` for the investment mode. In case of the dispatch mode one should use the - `nominal_value` of the Flow classes. The attributes + `nominal_capacity` of the Flow classes. The attributes `nominal_input_capacity_ratio` and `nominal_input_capacity_ratio` will be removed in v0.3.0. Please adapt your application to avoid problems in the future (`Issue #480 `_). diff --git a/docs/whatsnew/v0-5-1.rst b/docs/whatsnew/v0-5-1.rst index 06c327774..9a2b30ad9 100644 --- a/docs/whatsnew/v0-5-1.rst +++ b/docs/whatsnew/v0-5-1.rst @@ -13,7 +13,7 @@ API changes (Note that we always had the argument "conversion_factor".) * Unify API for constant sized objects and sizing of investment. For both, `Flow` and `GenericStorage`, the argument `investment` is now deprecated. Instead, - `nominal_value` and `nominal_storage_capacity` accept an `Investment` object. + `nominal_capacity` and `nominal_storage_capacity` accept an `Investment` object. * Change investment for experimental :class:`oemof.solph.components.experimental._sink_dsm.SinkDSM`: Remove obsolete parameters `flex_share_down` and `flex_share_up`. * Mainline link component :class:`oemof.solph.components._link.Link` from experimental. diff --git a/docs/whatsnew/v0-5-3.rst b/docs/whatsnew/v0-5-3.rst index c4dcb591f..6aa70ff6a 100644 --- a/docs/whatsnew/v0-5-3.rst +++ b/docs/whatsnew/v0-5-3.rst @@ -9,7 +9,7 @@ API changes to the `NonConvex` flow, and `normed_offsets` with the normed offsets relative to the `NonConvex` flow. * The `NonConvex` attribute must be specified for one of `Flow` at the inlets - or outlets of the `OffsetConverter`. `min`, `max` and `nominal_value` have to + or outlets of the `OffsetConverter`. `min`, `max` and `nominal_capacity` have to be specified for the same `Flow`. New features diff --git a/docs/whatsnew/v0-6-0.rst b/docs/whatsnew/v0-6-0.rst index 122bbe7a7..5b95cd94b 100644 --- a/docs/whatsnew/v0-6-0.rst +++ b/docs/whatsnew/v0-6-0.rst @@ -11,7 +11,7 @@ API changes initial storage level is defined, the costs just offset the objective value without changing anything else. * The parameters `GenericStorage.nominal_storage_capacity` and - `Flow.nominal_value` are now both called `nominal_calacity`. + `Flow.nominal_capacity` are now both called `nominal_calacity`. New features ############ diff --git a/examples/activity_costs/activity_costs.py b/examples/activity_costs/activity_costs.py index 9f129c015..2ecb4b2e4 100644 --- a/examples/activity_costs/activity_costs.py +++ b/examples/activity_costs/activity_costs.py @@ -69,14 +69,14 @@ def main(): sink_heat = solph.components.Sink( label="demand", - inputs={b_heat: solph.Flow(fix=demand_heat, nominal_value=1)}, + inputs={b_heat: solph.Flow(fix=demand_heat, nominal_capacity=1)}, ) fireplace = solph.components.Source( label="fireplace", outputs={ b_heat: solph.Flow( - nominal_value=3, + nominal_capacity=3, variable_costs=0, nonconvex=solph.NonConvex(activity_costs=activity_costs), ) @@ -85,7 +85,7 @@ def main(): boiler = solph.components.Source( label="boiler", - outputs={b_heat: solph.Flow(nominal_value=10, variable_costs=1)}, + outputs={b_heat: solph.Flow(nominal_capacity=10, variable_costs=1)}, ) es.add(sink_heat, fireplace, boiler) diff --git a/examples/basic_example/basic_example.py b/examples/basic_example/basic_example.py index b188b4ca8..cfd8e32f5 100644 --- a/examples/basic_example/basic_example.py +++ b/examples/basic_example/basic_example.py @@ -173,7 +173,7 @@ def main(dump_and_restore=False): label="wind", outputs={ bus_electricity: flows.Flow( - fix=data["wind"], nominal_value=1000000 + fix=data["wind"], nominal_capacity=1000000 ) }, ) @@ -185,20 +185,20 @@ def main(dump_and_restore=False): label="pv", outputs={ bus_electricity: flows.Flow( - fix=data["pv"], nominal_value=582000 + fix=data["pv"], nominal_capacity=582000 ) }, ) ) # create simple sink object representing the electrical demand - # nominal_value is set to 1 because demand_el is not a normalised series + # nominal_capacity is set to 1 because demand_el is not a normalised series energysystem.add( components.Sink( label="demand", inputs={ bus_electricity: flows.Flow( - fix=data["demand_el"], nominal_value=1 + fix=data["demand_el"], nominal_capacity=1 ) }, ) @@ -211,7 +211,7 @@ def main(dump_and_restore=False): inputs={bus_gas: flows.Flow()}, outputs={ bus_electricity: flows.Flow( - nominal_value=10e10, variable_costs=50 + nominal_capacity=10e10, variable_costs=50 ) }, conversion_factors={bus_electricity: 0.58}, @@ -220,15 +220,17 @@ def main(dump_and_restore=False): # create storage object representing a battery nominal_capacity = 10077997 - nominal_value = nominal_capacity / 6 + nominal_capacity = nominal_capacity / 6 battery_storage = components.GenericStorage( nominal_storage_capacity=nominal_capacity, label=STORAGE_LABEL, - inputs={bus_electricity: flows.Flow(nominal_value=nominal_value)}, + inputs={ + bus_electricity: flows.Flow(nominal_capacity=nominal_capacity) + }, outputs={ bus_electricity: flows.Flow( - nominal_value=nominal_value, variable_costs=0.001 + nominal_capacity=nominal_capacity, variable_costs=0.001 ) }, loss_rate=0.00, diff --git a/examples/dual_variable_example/dual_variable_example.py b/examples/dual_variable_example/dual_variable_example.py index 4ccc1105a..0e26c2472 100644 --- a/examples/dual_variable_example/dual_variable_example.py +++ b/examples/dual_variable_example/dual_variable_example.py @@ -198,7 +198,7 @@ def main(): energysystem.add( cmp.Source( label="pv", - outputs={bus_elec: flows.Flow(fix=pv, nominal_value=700)}, + outputs={bus_elec: flows.Flow(fix=pv, nominal_capacity=700)}, ) ) @@ -206,7 +206,7 @@ def main(): energysystem.add( cmp.Sink( label="demand", - inputs={bus_elec: flows.Flow(fix=demand, nominal_value=1)}, + inputs={bus_elec: flows.Flow(fix=demand, nominal_capacity=1)}, ) ) @@ -215,7 +215,7 @@ def main(): cmp.Converter( label="pp_gas", inputs={bus_gas: flows.Flow()}, - outputs={bus_elec: flows.Flow(nominal_value=400)}, + outputs={bus_elec: flows.Flow(nominal_capacity=400)}, conversion_factors={bus_elec: 0.5}, ) ) @@ -225,9 +225,11 @@ def main(): storage = cmp.GenericStorage( nominal_storage_capacity=cap, label="storage", - inputs={bus_elec: flows.Flow(nominal_value=cap / 6)}, + inputs={bus_elec: flows.Flow(nominal_capacity=cap / 6)}, outputs={ - bus_elec: flows.Flow(nominal_value=cap / 6, variable_costs=0.001) + bus_elec: flows.Flow( + nominal_capacity=cap / 6, variable_costs=0.001 + ) }, loss_rate=0.00, initial_storage_level=0, diff --git a/examples/electrical/transshipment.py b/examples/electrical/transshipment.py index d5e0e53ef..3a1e181e4 100644 --- a/examples/electrical/transshipment.py +++ b/examples/electrical/transshipment.py @@ -144,8 +144,8 @@ def main(): label="line_0", inputs={b_0: Flow(), b_1: Flow()}, outputs={ - b_1: Flow(nominal_value=Investment()), - b_0: Flow(nominal_value=Investment()), + b_1: Flow(nominal_capacity=Investment()), + b_0: Flow(nominal_capacity=Investment()), }, conversion_factors={(b_0, b_1): 0.95, (b_1, b_0): 0.9}, ) @@ -154,26 +154,28 @@ def main(): es.add( cmp.Source( label="gen_0", - outputs={b_0: Flow(nominal_value=100, variable_costs=50)}, + outputs={b_0: Flow(nominal_capacity=100, variable_costs=50)}, ) ) es.add( cmp.Source( label="gen_1", - outputs={b_1: Flow(nominal_value=100, variable_costs=50)}, + outputs={b_1: Flow(nominal_capacity=100, variable_costs=50)}, ) ) es.add( cmp.Sink( - label="load_0", inputs={b_0: Flow(nominal_value=150, fix=[0, 1])} + label="load_0", + inputs={b_0: Flow(nominal_capacity=150, fix=[0, 1])}, ) ) es.add( cmp.Sink( - label="load_1", inputs={b_1: Flow(nominal_value=150, fix=[1, 0])} + label="load_1", + inputs={b_1: Flow(nominal_capacity=150, fix=[1, 0])}, ) ) diff --git a/examples/emission_constraint/emission_constraint.py b/examples/emission_constraint/emission_constraint.py index 73537ead4..bced70d38 100644 --- a/examples/emission_constraint/emission_constraint.py +++ b/examples/emission_constraint/emission_constraint.py @@ -57,7 +57,7 @@ def main(): label="biomass", outputs={ bel: solph.Flow( - nominal_value=100, + nominal_capacity=100, variable_costs=10, fix=[0.1, 0.2, 0.3], custom_attributes={"emission_factor": 0.01}, @@ -84,7 +84,9 @@ def main(): label="demand", inputs={ bel: solph.Flow( - nominal_value=200, variable_costs=10, fix=[0.1, 0.2, 0.3] + nominal_capacity=200, + variable_costs=10, + fix=[0.1, 0.2, 0.3], ) }, ) @@ -95,7 +97,7 @@ def main(): solph.components.Converter( label="pp_gas", inputs={bgas: solph.Flow()}, - outputs={bel: solph.Flow(nominal_value=200)}, + outputs={bel: solph.Flow(nominal_capacity=200)}, conversion_factors={bel: 0.58}, ) ) diff --git a/examples/excel_reader/dispatch.py b/examples/excel_reader/dispatch.py index 97baad936..efb534f24 100644 --- a/examples/excel_reader/dispatch.py +++ b/examples/excel_reader/dispatch.py @@ -178,7 +178,7 @@ def create_nodes(nd=None): for i, re in nd["renewables"].iterrows(): if re["active"]: # set static outflow values - outflow_args = {"nominal_value": re["capacity"]} + outflow_args = {"nominal_capacity": re["capacity"]} # get time series for node and parameter for col in nd["timeseries"].columns.values: if col.split(".")[0] == re["label"]: @@ -196,7 +196,7 @@ def create_nodes(nd=None): for i, de in nd["demand"].iterrows(): if de["active"]: # set static inflow values - inflow_args = {"nominal_value": de["nominal value"]} + inflow_args = {"nominal_capacity": de["nominal value"]} # get time series for node and parameter for col in nd["timeseries"].columns.values: if col.split(".")[0] == de["label"]: @@ -225,7 +225,9 @@ def create_nodes(nd=None): label=t["label"], inputs={busd[t["from"]]: solph.Flow(**inflow_args)}, outputs={ - busd[t["to"]]: solph.Flow(nominal_value=t["capacity"]) + busd[t["to"]]: solph.Flow( + nominal_capacity=t["capacity"] + ) }, conversion_factors={busd[t["to"]]: t["efficiency"]}, ) @@ -238,13 +240,13 @@ def create_nodes(nd=None): label=s["label"], inputs={ busd[s["bus"]]: solph.Flow( - nominal_value=s["capacity inflow"], + nominal_capacity=s["capacity inflow"], variable_costs=s["variable input costs"], ) }, outputs={ busd[s["bus"]]: solph.Flow( - nominal_value=s["capacity outflow"], + nominal_capacity=s["capacity outflow"], variable_costs=s["variable output costs"], ) }, diff --git a/examples/flexible_modelling/add_constraints.py b/examples/flexible_modelling/add_constraints.py index 1f1efceab..375b49735 100644 --- a/examples/flexible_modelling/add_constraints.py +++ b/examples/flexible_modelling/add_constraints.py @@ -69,18 +69,18 @@ def run_add_constraints_example(solver="cbc", nologg=False): sink = cmp.Sink( label="Sink", - inputs={b_el: Flow(nominal_value=40, fix=[0.5, 0.4, 0.3, 1])}, + inputs={b_el: Flow(nominal_capacity=40, fix=[0.5, 0.4, 0.3, 1])}, ) pp_oil = cmp.Converter( label="pp_oil", inputs={boil: Flow()}, - outputs={b_el: Flow(nominal_value=50, variable_costs=25)}, + outputs={b_el: Flow(nominal_capacity=50, variable_costs=25)}, conversion_factors={b_el: 0.39}, ) pp_lig = cmp.Converter( label="pp_lig", inputs={blig: Flow()}, - outputs={b_el: Flow(nominal_value=50, variable_costs=10)}, + outputs={b_el: Flow(nominal_capacity=50, variable_costs=10)}, conversion_factors={b_el: 0.41}, ) diff --git a/examples/flexible_modelling/saturating_storage.py b/examples/flexible_modelling/saturating_storage.py index 46f9d7730..757e685bb 100644 --- a/examples/flexible_modelling/saturating_storage.py +++ b/examples/flexible_modelling/saturating_storage.py @@ -50,7 +50,7 @@ def saturating_storage_example(): es.add( solph.components.Source( label="source_el", - outputs={bel: solph.Flow(nominal_value=1, fix=1)}, + outputs={bel: solph.Flow(nominal_capacity=1, fix=1)}, ) ) @@ -59,7 +59,7 @@ def saturating_storage_example(): label="sink_el", inputs={ bel: solph.Flow( - nominal_value=1, + nominal_capacity=1, variable_costs=1, ) }, @@ -74,7 +74,7 @@ def saturating_storage_example(): battery = solph.components.GenericStorage( label="battery", nominal_storage_capacity=storage_capacity, - inputs={bel: solph.Flow(nominal_value=inflow_capacity)}, + inputs={bel: solph.Flow(nominal_capacity=inflow_capacity)}, outputs={bel: solph.Flow(variable_costs=2)}, initial_storage_level=0, balanced=False, diff --git a/examples/flow_count_limit/flow_count_limit.py b/examples/flow_count_limit/flow_count_limit.py index 210d85052..b8a25fd47 100644 --- a/examples/flow_count_limit/flow_count_limit.py +++ b/examples/flow_count_limit/flow_count_limit.py @@ -71,7 +71,7 @@ def main(): outputs={ bel: solph.Flow( nonconvex=solph.NonConvex(), - nominal_value=210, + nominal_capacity=210, variable_costs=[-1, -5, -1, -1], max=[1, 1, 1, 0], custom_attributes={"my_keyword": True}, @@ -88,7 +88,7 @@ def main(): bel: solph.Flow( nonconvex=solph.NonConvex(), variable_costs=[-2, -1, -2, -2], - nominal_value=250, + nominal_capacity=250, max=[1, 1, 1, 0], custom_attributes={"my_keyword": False}, ) @@ -105,7 +105,7 @@ def main(): variable_costs=1, nonconvex=solph.NonConvex(), max=[1, 1, 1, 0], - nominal_value=145, + nominal_capacity=145, ) }, ) @@ -119,7 +119,7 @@ def main(): bel: solph.Flow( custom_attributes={"my_keyword": True}, fix=[0, 1, 1, 0], - nominal_value=130, + nominal_capacity=130, ) }, ) diff --git a/examples/generic_invest_limit/example_generic_invest.py b/examples/generic_invest_limit/example_generic_invest.py index a428f9c8f..a7b3a397e 100644 --- a/examples/generic_invest_limit/example_generic_invest.py +++ b/examples/generic_invest_limit/example_generic_invest.py @@ -105,7 +105,7 @@ def main(): es.add( solph.components.Sink( label="demand_a", - inputs={bus_a_1: solph.Flow(fix=data, nominal_value=1)}, + inputs={bus_a_1: solph.Flow(fix=data, nominal_capacity=1)}, ) ) @@ -130,7 +130,7 @@ def main(): es.add( solph.components.Sink( label="demand_b", - inputs={bus_b_1: solph.Flow(fix=data, nominal_value=1)}, + inputs={bus_b_1: solph.Flow(fix=data, nominal_capacity=1)}, ) ) @@ -141,7 +141,7 @@ def main(): inputs={bus_a_0: solph.Flow()}, outputs={ bus_a_1: solph.Flow( - nominal_value=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=epc_invest, custom_attributes={"space": 2}, ), @@ -158,7 +158,7 @@ def main(): inputs={bus_b_0: solph.Flow()}, outputs={ bus_b_1: solph.Flow( - nominal_value=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=epc_invest, custom_attributes={"space": 1}, ), diff --git a/examples/gradient_example/gradient_example.py b/examples/gradient_example/gradient_example.py index c1f7f58cb..b50269a1a 100644 --- a/examples/gradient_example/gradient_example.py +++ b/examples/gradient_example/gradient_example.py @@ -131,7 +131,7 @@ def main(): energysystem.add( cmp.Sink( label="demand", - inputs={bel: flows.Flow(fix=demand, nominal_value=1)}, + inputs={bel: flows.Flow(fix=demand, nominal_capacity=1)}, ) ) @@ -142,7 +142,7 @@ def main(): inputs={bgas: flows.Flow()}, outputs={ bel: flows.Flow( - nominal_value=10e5, + nominal_capacity=10e5, negative_gradient_limit=gradient, positive_gradient_limit=gradient, ) diff --git a/examples/invest_nonconvex_flow_examples/diesel_genset_nonconvex_investment.py b/examples/invest_nonconvex_flow_examples/diesel_genset_nonconvex_investment.py index 9ea6dfa30..60f12b7e9 100644 --- a/examples/invest_nonconvex_flow_examples/diesel_genset_nonconvex_investment.py +++ b/examples/invest_nonconvex_flow_examples/diesel_genset_nonconvex_investment.py @@ -132,7 +132,7 @@ def main(): outputs={ b_el_dc: solph.flows.Flow( fix=solar_potential / peak_solar_potential, - nominal_value=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=epc_pv * n_days / n_days_in_year ), variable_costs=0, @@ -159,7 +159,7 @@ def main(): variable_costs=variable_cost_diesel_genset, min=min_load, max=max_load, - nominal_value=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=epc_diesel_genset * n_days / n_days_in_year, maximum=2 * peak_demand, ), @@ -175,7 +175,7 @@ def main(): label="rectifier", inputs={ b_el_ac: solph.flows.Flow( - nominal_value=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=epc_rectifier * n_days / n_days_in_year ), variable_costs=0, @@ -193,7 +193,7 @@ def main(): label="inverter", inputs={ b_el_dc: solph.flows.Flow( - nominal_value=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=epc_inverter * n_days / n_days_in_year ), variable_costs=0, @@ -215,7 +215,7 @@ def main(): inputs={b_el_dc: solph.flows.Flow(variable_costs=0)}, outputs={ b_el_dc: solph.flows.Flow( - nominal_value=solph.Investment(ep_costs=0) + nominal_capacity=solph.Investment(ep_costs=0) ) }, initial_storage_level=0.0, @@ -234,7 +234,7 @@ def main(): inputs={ b_el_ac: solph.flows.Flow( fix=hourly_demand / peak_demand, - nominal_value=peak_demand, + nominal_capacity=peak_demand, ) }, ) diff --git a/examples/invest_nonconvex_flow_examples/house_with_nonconvex_investment.py b/examples/invest_nonconvex_flow_examples/house_with_nonconvex_investment.py index ba50494ab..2d9f345ef 100644 --- a/examples/invest_nonconvex_flow_examples/house_with_nonconvex_investment.py +++ b/examples/invest_nonconvex_flow_examples/house_with_nonconvex_investment.py @@ -71,7 +71,7 @@ def heat_demand(d): inputs={ b_heat: solph.flows.Flow( fix=[heat_demand(day) for day in range(0, periods)], - nominal_value=10, + nominal_capacity=10, ) }, ) @@ -91,7 +91,7 @@ def heat_demand(d): minimum_uptime=5, initial_status=1, ), - nominal_value=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=epc, minimum=1.0, maximum=10.0, @@ -104,14 +104,14 @@ def heat_demand(d): label="boiler", outputs={ b_heat: solph.flows.Flow( - nominal_value=10, min=0.3, variable_costs=0.2 + nominal_capacity=10, min=0.3, variable_costs=0.2 ) }, ) excess_heat = solph.components.Sink( label="excess_heat", - inputs={b_heat: solph.flows.Flow(nominal_value=10)}, + inputs={b_heat: solph.flows.Flow(nominal_capacity=10)}, ) es.add(demand_heat, fireplace, boiler, excess_heat) diff --git a/examples/invest_nonconvex_flow_examples/house_without_nonconvex_investment.py b/examples/invest_nonconvex_flow_examples/house_without_nonconvex_investment.py index 15baeacf3..3c00e88f6 100644 --- a/examples/invest_nonconvex_flow_examples/house_without_nonconvex_investment.py +++ b/examples/invest_nonconvex_flow_examples/house_without_nonconvex_investment.py @@ -80,7 +80,7 @@ def solar_thermal(d): inputs={ b_heat: solph.flows.Flow( fix=[heat_demand(day) for day in range(0, periods)], - nominal_value=10, + nominal_capacity=10, ) }, ) @@ -89,7 +89,7 @@ def solar_thermal(d): label="fireplace", outputs={ b_heat: solph.flows.Flow( - nominal_value=10, + nominal_capacity=10, min=0.4, max=1.0, variable_costs=0.1, @@ -104,7 +104,7 @@ def solar_thermal(d): boiler = solph.components.Source( label="boiler", outputs={ - b_heat: solph.flows.Flow(nominal_value=10, variable_costs=0.2) + b_heat: solph.flows.Flow(nominal_capacity=10, variable_costs=0.2) }, ) @@ -117,7 +117,7 @@ def solar_thermal(d): outputs={ b_heat: solph.flows.Flow( fix=[solar_thermal(day) for day in range(0, periods)], - nominal_value=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=epc, minimum=1.0, maximum=5.0 ), ) @@ -126,7 +126,7 @@ def solar_thermal(d): excess_heat = solph.components.Sink( label="excess_heat", - inputs={b_heat: solph.flows.Flow(nominal_value=10)}, + inputs={b_heat: solph.flows.Flow(nominal_capacity=10)}, ) es.add(demand_heat, fireplace, boiler, thermal_collector, excess_heat) diff --git a/examples/investment_with_minimal_invest/minimal_invest.py b/examples/investment_with_minimal_invest/minimal_invest.py index 6302fa816..e48d0416f 100644 --- a/examples/investment_with_minimal_invest/minimal_invest.py +++ b/examples/investment_with_minimal_invest/minimal_invest.py @@ -66,7 +66,7 @@ def main(): es.add( solph.components.Sink( label="demand", - inputs={bus_1: solph.Flow(fix=data, nominal_value=1)}, + inputs={bus_1: solph.Flow(fix=data, nominal_capacity=1)}, ) ) @@ -86,7 +86,7 @@ def main(): inputs={bus_0: solph.Flow()}, outputs={ bus_1: solph.Flow( - nominal_value=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=c_var, maximum=p_install_max, minimum=p_install_min, diff --git a/examples/min_max_runtimes/min_max_runtimes.py b/examples/min_max_runtimes/min_max_runtimes.py index 46a56deed..285ad5eed 100644 --- a/examples/min_max_runtimes/min_max_runtimes.py +++ b/examples/min_max_runtimes/min_max_runtimes.py @@ -50,7 +50,7 @@ def main(): demand_el = solph.components.Sink( label="demand_el", - inputs={bel: solph.Flow(fix=demand_el, nominal_value=10)}, + inputs={bel: solph.Flow(fix=demand_el, nominal_capacity=10)}, ) dummy_el = solph.components.Sink( @@ -61,7 +61,7 @@ def main(): label="plant_min_down_constraints", outputs={ bel: solph.Flow( - nominal_value=10, + nominal_capacity=10, min=0.5, max=1.0, variable_costs=10, @@ -76,7 +76,7 @@ def main(): label="plant_min_up_constraints", outputs={ bel: solph.Flow( - nominal_value=10, + nominal_capacity=10, min=0.5, max=1.0, variable_costs=10, diff --git a/examples/offset_converter_example/offset_diesel_genset_nonconvex_investment.py b/examples/offset_converter_example/offset_diesel_genset_nonconvex_investment.py index 94ebe0825..3bea2bcae 100644 --- a/examples/offset_converter_example/offset_diesel_genset_nonconvex_investment.py +++ b/examples/offset_converter_example/offset_diesel_genset_nonconvex_investment.py @@ -130,7 +130,7 @@ def offset_converter_example(): outputs={ b_el_dc: solph.flows.Flow( fix=solar_potential / peak_solar_potential, - nominal_value=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=epc_pv * n_days / n_days_in_year ), variable_costs=0, @@ -173,7 +173,7 @@ def offset_converter_example(): variable_costs=variable_cost_diesel_genset, min=min_load, max=max_load, - nominal_value=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=epc_diesel_genset * n_days / n_days_in_year, maximum=2 * peak_demand, ), @@ -190,7 +190,7 @@ def offset_converter_example(): label="rectifier", inputs={ b_el_ac: solph.flows.Flow( - nominal_value=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=epc_rectifier * n_days / n_days_in_year ), variable_costs=0, @@ -208,7 +208,7 @@ def offset_converter_example(): label="inverter", inputs={ b_el_dc: solph.flows.Flow( - nominal_value=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=epc_inverter * n_days / n_days_in_year ), variable_costs=0, @@ -230,7 +230,7 @@ def offset_converter_example(): inputs={b_el_dc: solph.flows.Flow(variable_costs=0)}, outputs={ b_el_dc: solph.flows.Flow( - nominal_value=solph.Investment(ep_costs=0) + nominal_capacity=solph.Investment(ep_costs=0) ) }, initial_storage_level=0.0, @@ -249,7 +249,7 @@ def offset_converter_example(): inputs={ b_el_ac: solph.flows.Flow( fix=hourly_demand / peak_demand, - nominal_value=peak_demand, + nominal_capacity=peak_demand, ) }, ) diff --git a/examples/simple_dispatch/simple_dispatch.py b/examples/simple_dispatch/simple_dispatch.py index ea9e9ab66..9ade61c4a 100644 --- a/examples/simple_dispatch/simple_dispatch.py +++ b/examples/simple_dispatch/simple_dispatch.py @@ -108,13 +108,14 @@ def main(): energysystem.add( Source( label="wind", - outputs={bel: Flow(fix=data["wind"], nominal_value=66.3)}, + outputs={bel: Flow(fix=data["wind"], nominal_capacity=66.3)}, ) ) energysystem.add( Source( - label="pv", outputs={bel: Flow(fix=data["pv"], nominal_value=65.3)} + label="pv", + outputs={bel: Flow(fix=data["pv"], nominal_capacity=65.3)}, ) ) @@ -122,14 +123,14 @@ def main(): energysystem.add( Sink( label="demand_el", - inputs={bel: Flow(nominal_value=85, fix=data["demand_el"])}, + inputs={bel: Flow(nominal_capacity=85, fix=data["demand_el"])}, ) ) energysystem.add( Sink( label="demand_th", - inputs={bth: Flow(nominal_value=40, fix=data["demand_th"])}, + inputs={bth: Flow(nominal_capacity=40, fix=data["demand_th"])}, ) ) @@ -138,7 +139,7 @@ def main(): Converter( label="pp_coal", inputs={bcoal: Flow()}, - outputs={bel: Flow(nominal_value=20.2, variable_costs=25)}, + outputs={bel: Flow(nominal_capacity=20.2, variable_costs=25)}, conversion_factors={bel: 0.39}, ) ) @@ -147,7 +148,7 @@ def main(): Converter( label="pp_lig", inputs={blig: Flow()}, - outputs={bel: Flow(nominal_value=11.8, variable_costs=19)}, + outputs={bel: Flow(nominal_capacity=11.8, variable_costs=19)}, conversion_factors={bel: 0.41}, ) ) @@ -156,7 +157,7 @@ def main(): Converter( label="pp_gas", inputs={bgas: Flow()}, - outputs={bel: Flow(nominal_value=41, variable_costs=40)}, + outputs={bel: Flow(nominal_capacity=41, variable_costs=40)}, conversion_factors={bel: 0.50}, ) ) @@ -165,7 +166,7 @@ def main(): Converter( label="pp_oil", inputs={boil: Flow()}, - outputs={bel: Flow(nominal_value=5, variable_costs=50)}, + outputs={bel: Flow(nominal_capacity=5, variable_costs=50)}, conversion_factors={bel: 0.28}, ) ) @@ -176,8 +177,8 @@ def main(): label="pp_chp", inputs={bgas: Flow()}, outputs={ - bel: Flow(nominal_value=30, variable_costs=42), - bth: Flow(nominal_value=40), + bel: Flow(nominal_capacity=30, variable_costs=42), + bth: Flow(nominal_capacity=40), }, conversion_factors={bel: 0.3, bth: 0.4}, ) @@ -196,7 +197,7 @@ def main(): Converter( label="heat_pump", inputs={bel: Flow(), b_heat_source: Flow()}, - outputs={bth: Flow(nominal_value=10)}, + outputs={bth: Flow(nominal_capacity=10)}, conversion_factors={bel: 1 / 3, b_heat_source: (cop - 1) / cop}, ) ) diff --git a/examples/start_and_shutdown_costs/startup_shutdown.py b/examples/start_and_shutdown_costs/startup_shutdown.py index 47b9349b6..648a02f49 100644 --- a/examples/start_and_shutdown_costs/startup_shutdown.py +++ b/examples/start_and_shutdown_costs/startup_shutdown.py @@ -71,7 +71,7 @@ def main(): demand_el = solph.components.Sink( label="demand_el", - inputs={bel: solph.Flow(fix=demand_el, nominal_value=10)}, + inputs={bel: solph.Flow(fix=demand_el, nominal_capacity=10)}, ) # pp1 and pp2 are competing to serve overall 12 units load at lowest cost @@ -81,7 +81,7 @@ def main(): # the start and shutdown costs of pp2 change its marginal costs pp1 = solph.components.Source( label="power_plant1", - outputs={bel: solph.Flow(nominal_value=10, variable_costs=10.25)}, + outputs={bel: solph.Flow(nominal_capacity=10, variable_costs=10.25)}, ) # shutdown costs only work in combination with a minimum load @@ -92,7 +92,7 @@ def main(): label="power_plant2", outputs={ bel: solph.Flow( - nominal_value=10, + nominal_capacity=10, min=0.5, max=1.0, variable_costs=10, diff --git a/examples/storage_balanced_unbalanced/storage.py b/examples/storage_balanced_unbalanced/storage.py index 591274630..d26e8419e 100644 --- a/examples/storage_balanced_unbalanced/storage.py +++ b/examples/storage_balanced_unbalanced/storage.py @@ -89,7 +89,9 @@ def storage_example(): solph.components.Source( label="pv_el_{0}".format(name), outputs={ - bel: solph.Flow(fix=timeseries["pv_el"], nominal_value=1) + bel: solph.Flow( + fix=timeseries["pv_el"], nominal_capacity=1 + ) }, ) ) @@ -99,7 +101,7 @@ def storage_example(): label="demand_el_{0}".format(name), inputs={ bel: solph.Flow( - fix=timeseries["demand_el"], nominal_value=1 + fix=timeseries["demand_el"], nominal_capacity=1 ) }, ) diff --git a/examples/storage_costs/storage_costs.py b/examples/storage_costs/storage_costs.py index 312c5ab49..39bfae556 100644 --- a/examples/storage_costs/storage_costs.py +++ b/examples/storage_costs/storage_costs.py @@ -89,13 +89,13 @@ def storage_costs_example(): nominal_storage_capacity=10, inputs={ bel: solph.Flow( - nominal_value=1, + nominal_capacity=1, variable_costs=electricity_price, ) }, outputs={ bel: solph.Flow( - nominal_value=1, + nominal_capacity=1, variable_costs=-electricity_price, ) }, @@ -111,13 +111,13 @@ def storage_costs_example(): nominal_storage_capacity=10, inputs={ bel: solph.Flow( - nominal_value=1, + nominal_capacity=1, variable_costs=electricity_price, ) }, outputs={ bel: solph.Flow( - nominal_value=1, + nominal_capacity=1, variable_costs=-electricity_price, ) }, diff --git a/examples/storage_investment/v1_invest_optimize_all_technologies.py b/examples/storage_investment/v1_invest_optimize_all_technologies.py index 8f3749fd5..45c5a1d25 100644 --- a/examples/storage_investment/v1_invest_optimize_all_technologies.py +++ b/examples/storage_investment/v1_invest_optimize_all_technologies.py @@ -151,7 +151,7 @@ def main(): outputs={ bel: solph.Flow( fix=data["wind"], - nominal_value=solph.Investment(ep_costs=epc_wind), + nominal_capacity=solph.Investment(ep_costs=epc_wind), ) }, ) @@ -161,7 +161,8 @@ def main(): label="pv", outputs={ bel: solph.Flow( - fix=data["pv"], nominal_value=solph.Investment(ep_costs=epc_pv) + fix=data["pv"], + nominal_capacity=solph.Investment(ep_costs=epc_pv), ) }, ) @@ -169,14 +170,14 @@ def main(): # create simple sink object representing the electrical demand demand = solph.components.Sink( label="demand", - inputs={bel: solph.Flow(fix=data["demand_el"], nominal_value=1)}, + inputs={bel: solph.Flow(fix=data["demand_el"], nominal_capacity=1)}, ) # create simple Converter object representing a gas power plant pp_gas = solph.components.Converter( label="pp_gas", inputs={bgas: solph.Flow()}, - outputs={bel: solph.Flow(nominal_value=10e10, variable_costs=0)}, + outputs={bel: solph.Flow(nominal_capacity=10e10, variable_costs=0)}, conversion_factors={bel: 0.58}, ) diff --git a/examples/storage_investment/v2_invest_optimize_only_gas_and_storage.py b/examples/storage_investment/v2_invest_optimize_only_gas_and_storage.py index 00c251044..528059e03 100644 --- a/examples/storage_investment/v2_invest_optimize_only_gas_and_storage.py +++ b/examples/storage_investment/v2_invest_optimize_only_gas_and_storage.py @@ -150,26 +150,26 @@ def main(): # create fixed source object representing wind power plants wind = solph.components.Source( label="wind", - outputs={bel: solph.Flow(fix=data["wind"], nominal_value=1000000)}, + outputs={bel: solph.Flow(fix=data["wind"], nominal_capacity=1000000)}, ) # create fixed source object representing pv power plants pv = solph.components.Source( label="pv", - outputs={bel: solph.Flow(fix=data["pv"], nominal_value=600000)}, + outputs={bel: solph.Flow(fix=data["pv"], nominal_capacity=600000)}, ) # create simple sink object representing the electrical demand demand = solph.components.Sink( label="demand", - inputs={bel: solph.Flow(fix=data["demand_el"], nominal_value=1)}, + inputs={bel: solph.Flow(fix=data["demand_el"], nominal_capacity=1)}, ) # create simple Converter object representing a gas power plant pp_gas = solph.components.Converter( label="pp_gas", inputs={bgas: solph.Flow()}, - outputs={bel: solph.Flow(nominal_value=10e10, variable_costs=0)}, + outputs={bel: solph.Flow(nominal_capacity=10e10, variable_costs=0)}, conversion_factors={bel: 0.58}, ) diff --git a/examples/storage_investment/v3_invest_optimize_only_storage_with_fossil_share.py b/examples/storage_investment/v3_invest_optimize_only_storage_with_fossil_share.py index f682620cf..4c254e2be 100644 --- a/examples/storage_investment/v3_invest_optimize_only_storage_with_fossil_share.py +++ b/examples/storage_investment/v3_invest_optimize_only_storage_with_fossil_share.py @@ -147,7 +147,7 @@ def main(): label="rgas", outputs={ bgas: solph.Flow( - nominal_value=fossil_share + nominal_capacity=fossil_share * consumption_total / 0.58 * number_timesteps @@ -160,26 +160,26 @@ def main(): # create fixed source object representing wind power plants wind = solph.components.Source( label="wind", - outputs={bel: solph.Flow(fix=data["wind"], nominal_value=1000000)}, + outputs={bel: solph.Flow(fix=data["wind"], nominal_capacity=1000000)}, ) # create fixed source object representing pv power plants pv = solph.components.Source( label="pv", - outputs={bel: solph.Flow(fix=data["pv"], nominal_value=600000)}, + outputs={bel: solph.Flow(fix=data["pv"], nominal_capacity=600000)}, ) # create simple sink object representing the electrical demand demand = solph.components.Sink( label="demand", - inputs={bel: solph.Flow(fix=data["demand_el"], nominal_value=1)}, + inputs={bel: solph.Flow(fix=data["demand_el"], nominal_capacity=1)}, ) # create simple Converter object representing a gas power plant pp_gas = solph.components.Converter( label="pp_gas", inputs={bgas: solph.Flow()}, - outputs={bel: solph.Flow(nominal_value=10e10, variable_costs=0)}, + outputs={bel: solph.Flow(nominal_capacity=10e10, variable_costs=0)}, conversion_factors={bel: 0.58}, ) diff --git a/examples/storage_investment/v4_invest_optimize_all_technologies_with_fossil_share.py b/examples/storage_investment/v4_invest_optimize_all_technologies_with_fossil_share.py index a9c3511c9..2e9264ff1 100644 --- a/examples/storage_investment/v4_invest_optimize_all_technologies_with_fossil_share.py +++ b/examples/storage_investment/v4_invest_optimize_all_technologies_with_fossil_share.py @@ -146,7 +146,7 @@ def main(): label="rgas", outputs={ bgas: solph.Flow( - nominal_value=fossil_share + nominal_capacity=fossil_share * consumption_total / 0.58 * number_timesteps @@ -162,7 +162,7 @@ def main(): outputs={ bel: solph.Flow( fix=data["wind"], - nominal_value=solph.Investment(ep_costs=epc_wind), + nominal_capacity=solph.Investment(ep_costs=epc_wind), ) }, ) @@ -172,7 +172,8 @@ def main(): label="pv", outputs={ bel: solph.Flow( - fix=data["pv"], nominal_value=solph.Investment(ep_costs=epc_pv) + fix=data["pv"], + nominal_capacity=solph.Investment(ep_costs=epc_pv), ) }, ) @@ -180,14 +181,14 @@ def main(): # create simple sink object representing the electrical demand demand = solph.components.Sink( label="demand", - inputs={bel: solph.Flow(fix=data["demand_el"], nominal_value=1)}, + inputs={bel: solph.Flow(fix=data["demand_el"], nominal_capacity=1)}, ) # create simple Converter object representing a gas power plant pp_gas = solph.components.Converter( label="pp_gas", inputs={bgas: solph.Flow()}, - outputs={bel: solph.Flow(nominal_value=10e10, variable_costs=0)}, + outputs={bel: solph.Flow(nominal_capacity=10e10, variable_costs=0)}, conversion_factors={bel: 0.58}, ) diff --git a/examples/storage_level_constraint/storage_level_constraint.py b/examples/storage_level_constraint/storage_level_constraint.py index 9fd51a607..cbc237445 100644 --- a/examples/storage_level_constraint/storage_level_constraint.py +++ b/examples/storage_level_constraint/storage_level_constraint.py @@ -73,22 +73,24 @@ def storage_level_constraint_example(): in_0 = Source( label="in_0", - outputs={multiplexer: Flow(nominal_value=0.5, variable_costs=0.15)}, + outputs={multiplexer: Flow(nominal_capacity=0.5, variable_costs=0.15)}, ) es.add(in_0) - in_1 = Source(label="in_1", outputs={multiplexer: Flow(nominal_value=0.1)}) + in_1 = Source( + label="in_1", outputs={multiplexer: Flow(nominal_capacity=0.1)} + ) es.add(in_1) out_0 = Sink( label="out_0", - inputs={multiplexer: Flow(nominal_value=0.25, variable_costs=-0.1)}, + inputs={multiplexer: Flow(nominal_capacity=0.25, variable_costs=-0.1)}, ) es.add(out_0) out_1 = Sink( label="out_1", - inputs={multiplexer: Flow(nominal_value=0.15, variable_costs=-0.1)}, + inputs={multiplexer: Flow(nominal_capacity=0.15, variable_costs=-0.1)}, ) es.add(out_1) diff --git a/examples/time_index_example/non_equidistant_time_step_example.py b/examples/time_index_example/non_equidistant_time_step_example.py index 8e8d47081..02c91af34 100644 --- a/examples/time_index_example/non_equidistant_time_step_example.py +++ b/examples/time_index_example/non_equidistant_time_step_example.py @@ -75,7 +75,7 @@ def main(): label="source", outputs={ bus: solph.flows.Flow( - nominal_value=16, + nominal_capacity=16, variable_costs=0.2, max=[0, 0, 0, 0, 0, 0, 0, 1, 1], ) @@ -99,7 +99,7 @@ def main(): inputs={bus: solph.flows.Flow()}, outputs={ bus: solph.flows.Flow( - nominal_value=4, max=[0, 0, 0, 0, 0, 0, 0, 1, 1] + nominal_capacity=4, max=[0, 0, 0, 0, 0, 0, 0, 1, 1] ) }, nominal_storage_capacity=8, @@ -110,7 +110,7 @@ def main(): label="sink", inputs={ bus: solph.flows.Flow( - nominal_value=8, + nominal_capacity=8, variable_costs=0.1, fix=[0.75, 0.5, 0, 0, 1, 0, 0, 0, 0], ) diff --git a/examples/time_index_example/simple_time_step_example.py b/examples/time_index_example/simple_time_step_example.py index 7a0509872..9d6cb5b07 100644 --- a/examples/time_index_example/simple_time_step_example.py +++ b/examples/time_index_example/simple_time_step_example.py @@ -14,7 +14,7 @@ If the storage is balanced, this is the same storage level as in the last time step. * The nominal_value in Flows has to be interpreted in means of power: We have - nominal_value=0.5, but the maximum change of the storage content of an ideal + nominal_capacity=0.5, but the maximum change of the storage content of an ideal storage is 0.125. Code @@ -60,7 +60,7 @@ def main(): label="source", outputs={ bus: solph.flows.Flow( - nominal_value=2, + nominal_capacity=2, variable_costs=0.2, max=[0, 0, 0, 0, 1, 0.25, 0.75, 1], ) @@ -77,7 +77,7 @@ def main(): label="sink", inputs={ bus: solph.flows.Flow( - nominal_value=2, + nominal_capacity=2, variable_costs=0.1, fix=[1, 1, 0.5, 0.5, 0, 0, 0, 0], ) diff --git a/examples/tuple_as_labels/tuple_as_label.py b/examples/tuple_as_labels/tuple_as_label.py index b31fe559f..50c904851 100644 --- a/examples/tuple_as_labels/tuple_as_label.py +++ b/examples/tuple_as_labels/tuple_as_label.py @@ -198,7 +198,7 @@ def main(): energysystem.add( comp.Source( label=Label("ee_source", "electricity", "wind"), - outputs={bel: flows.Flow(fix=data["wind"], nominal_value=2000)}, + outputs={bel: flows.Flow(fix=data["wind"], nominal_capacity=2000)}, ) ) @@ -206,7 +206,7 @@ def main(): energysystem.add( comp.Source( label=Label("ee_source", "electricity", "pv"), - outputs={bel: flows.Flow(fix=data["pv"], nominal_value=3000)}, + outputs={bel: flows.Flow(fix=data["pv"], nominal_capacity=3000)}, ) ) @@ -215,7 +215,9 @@ def main(): comp.Sink( label=Label("sink", "electricity", "demand"), inputs={ - bel: flows.Flow(fix=data["demand_el"] / 1000, nominal_value=1) + bel: flows.Flow( + fix=data["demand_el"] / 1000, nominal_capacity=1 + ) }, ) ) @@ -225,7 +227,9 @@ def main(): comp.Converter( label=Label("power plant", "electricity", "gas"), inputs={bgas: flows.Flow()}, - outputs={bel: flows.Flow(nominal_value=10000, variable_costs=50)}, + outputs={ + bel: flows.Flow(nominal_capacity=10000, variable_costs=50) + }, conversion_factors={bel: 0.58}, ) ) @@ -235,8 +239,12 @@ def main(): storage = comp.GenericStorage( nominal_storage_capacity=nominal_storage_capacity, label=Label("storage", "electricity", "battery"), - inputs={bel: flows.Flow(nominal_value=nominal_storage_capacity / 6)}, - outputs={bel: flows.Flow(nominal_value=nominal_storage_capacity / 6)}, + inputs={ + bel: flows.Flow(nominal_capacity=nominal_storage_capacity / 6) + }, + outputs={ + bel: flows.Flow(nominal_capacity=nominal_storage_capacity / 6) + }, loss_rate=0.00, initial_storage_level=None, inflow_conversion_factor=1, diff --git a/examples/variable_chp/variable_chp.py b/examples/variable_chp/variable_chp.py index 8561c3c32..03b2d905c 100644 --- a/examples/variable_chp/variable_chp.py +++ b/examples/variable_chp/variable_chp.py @@ -176,13 +176,15 @@ def main(): noded["demand_elec"] = solph.components.Sink( label="demand_elec", inputs={ - noded["bel"]: solph.Flow(fix=data["demand_el"], nominal_value=1) + noded["bel"]: solph.Flow(fix=data["demand_el"], nominal_capacity=1) }, ) noded["demand_el_2"] = solph.components.Sink( label="demand_el_2", inputs={ - noded["bel2"]: solph.Flow(fix=data["demand_el"], nominal_value=1) + noded["bel2"]: solph.Flow( + fix=data["demand_el"], nominal_capacity=1 + ) }, ) @@ -191,7 +193,7 @@ def main(): label="demand_therm", inputs={ noded["bth"]: solph.Flow( - fix=data["demand_th"], nominal_value=741000 + fix=data["demand_th"], nominal_capacity=741000 ) }, ) @@ -199,7 +201,7 @@ def main(): label="demand_th_2", inputs={ noded["bth2"]: solph.Flow( - fix=data["demand_th"], nominal_value=741000 + fix=data["demand_th"], nominal_capacity=741000 ) }, ) @@ -207,7 +209,7 @@ def main(): # This is just a dummy Converter with a nominal input of zero noded["fixed_chp_gas"] = solph.components.Converter( label="fixed_chp_gas", - inputs={noded["bgas"]: solph.Flow(nominal_value=0)}, + inputs={noded["bgas"]: solph.Flow(nominal_capacity=0)}, outputs={noded["bel"]: solph.Flow(), noded["bth"]: solph.Flow()}, conversion_factors={noded["bel"]: 0.3, noded["bth"]: 0.5}, ) @@ -215,7 +217,7 @@ def main(): # create a fixed Converter to distribute to the heat_2 and elec_2 buses noded["fixed_chp_gas_2"] = solph.components.Converter( label="fixed_chp_gas_2", - inputs={noded["bgas"]: solph.Flow(nominal_value=10e10)}, + inputs={noded["bgas"]: solph.Flow(nominal_capacity=10e10)}, outputs={noded["bel2"]: solph.Flow(), noded["bth2"]: solph.Flow()}, conversion_factors={noded["bel2"]: 0.3, noded["bth2"]: 0.5}, ) @@ -223,7 +225,7 @@ def main(): # create a fixed Converter to distribute to the heat and elec buses noded["variable_chp_gas"] = solph.components.ExtractionTurbineCHP( label="variable_chp_gas", - inputs={noded["bgas"]: solph.Flow(nominal_value=10e10)}, + inputs={noded["bgas"]: solph.Flow(nominal_capacity=10e10)}, outputs={noded["bel"]: solph.Flow(), noded["bth"]: solph.Flow()}, conversion_factors={noded["bel"]: 0.3, noded["bth"]: 0.5}, conversion_factor_full_condensation={noded["bel"]: 0.5}, diff --git a/src/oemof/solph/_console_scripts.py b/src/oemof/solph/_console_scripts.py index ecc15c77b..0eccd6840 100644 --- a/src/oemof/solph/_console_scripts.py +++ b/src/oemof/solph/_console_scripts.py @@ -34,14 +34,14 @@ def _check_oemof_installation(solvers): solph.components.Sink( label="demand", inputs={ - bel: solph.flows.Flow(fix=[10, 20, 30, 40, 50], nominal_value=1) + bel: solph.flows.Flow(fix=[10, 20, 30, 40, 50], nominal_capacity=1) }, ) solph.components.Converter( label="pp_gas", inputs={bgas: solph.flows.Flow()}, outputs={ - bel: solph.flows.Flow(nominal_value=10e10, variable_costs=50) + bel: solph.flows.Flow(nominal_capacity=10e10, variable_costs=50) }, conversion_factors={bel: 0.58}, ) diff --git a/src/oemof/solph/components/_extraction_turbine_chp.py b/src/oemof/solph/components/_extraction_turbine_chp.py index cef1a6152..e6ed731db 100644 --- a/src/oemof/solph/components/_extraction_turbine_chp.py +++ b/src/oemof/solph/components/_extraction_turbine_chp.py @@ -64,7 +64,7 @@ class ExtractionTurbineCHP(Converter): >>> bgas = solph.buses.Bus(label='commodityBus') >>> et_chp = solph.components.ExtractionTurbineCHP( ... label='variable_chp_gas', - ... inputs={bgas: solph.flows.Flow(nominal_value=10e10)}, + ... inputs={bgas: solph.flows.Flow(nominal_capacity=10e10)}, ... outputs={bel: solph.flows.Flow(), bth: solph.flows.Flow()}, ... conversion_factors={bel: 0.3, bth: 0.5}, ... conversion_factor_full_condensation={bel: 0.5}) diff --git a/src/oemof/solph/components/_generic_storage.py b/src/oemof/solph/components/_generic_storage.py index 62a04dc87..f8fd76df7 100644 --- a/src/oemof/solph/components/_generic_storage.py +++ b/src/oemof/solph/components/_generic_storage.py @@ -141,8 +141,8 @@ class GenericStorage(Node): >>> my_storage = solph.components.GenericStorage( ... label='storage', ... nominal_storage_capacity=1000, - ... inputs={my_bus: solph.flows.Flow(nominal_value=200, variable_costs=10)}, - ... outputs={my_bus: solph.flows.Flow(nominal_value=200)}, + ... inputs={my_bus: solph.flows.Flow(nominal_capacity=200, variable_costs=10)}, + ... outputs={my_bus: solph.flows.Flow(nominal_capacity=200)}, ... loss_rate=0.01, ... initial_storage_level=0, ... max_storage_level = 0.9, diff --git a/src/oemof/solph/components/_link.py b/src/oemof/solph/components/_link.py index d5ca435c4..b6042c91a 100644 --- a/src/oemof/solph/components/_link.py +++ b/src/oemof/solph/components/_link.py @@ -60,8 +60,8 @@ class Link(Node): >>> link = solph.components.Link( ... label="transshipment_link", - ... inputs={bel0: solph.flows.Flow(nominal_value=4), - ... bel1: solph.flows.Flow(nominal_value=2)}, + ... inputs={bel0: solph.flows.Flow(nominal_capacity=4), + ... bel1: solph.flows.Flow(nominal_capacity=2)}, ... outputs={bel0: solph.flows.Flow(), ... bel1: solph.flows.Flow()}, ... conversion_factors={(bel0, bel1): 0.8, (bel1, bel0): 0.9}) diff --git a/src/oemof/solph/components/_offset_converter.py b/src/oemof/solph/components/_offset_converter.py index b078c9ced..b33442f6c 100644 --- a/src/oemof/solph/components/_offset_converter.py +++ b/src/oemof/solph/components/_offset_converter.py @@ -108,7 +108,7 @@ class OffsetConverter(Node): ... label='ostf', ... inputs={bel: solph.flows.Flow()}, ... outputs={bth: solph.flows.Flow( - ... nominal_value=l_nominal, min=l_min, max=l_max, + ... nominal_capacity=l_nominal, min=l_min, max=l_max, ... nonconvex=solph.NonConvex())}, ... conversion_factors={bel: slope}, ... normed_offsets={bel: offset}, diff --git a/src/oemof/solph/components/experimental/_piecewise_linear_converter.py b/src/oemof/solph/components/experimental/_piecewise_linear_converter.py index 666dfd701..ef2e5192b 100644 --- a/src/oemof/solph/components/experimental/_piecewise_linear_converter.py +++ b/src/oemof/solph/components/experimental/_piecewise_linear_converter.py @@ -54,7 +54,7 @@ class PiecewiseLinearConverter(Node): >>> pwltf = solph.components.experimental.PiecewiseLinearConverter( ... label='pwltf', ... inputs={b_gas: solph.flows.Flow( - ... nominal_value=100, + ... nominal_capacity=100, ... variable_costs=1)}, ... outputs={b_el: solph.flows.Flow()}, ... in_breakpoints=[0,25,50,75,100], diff --git a/src/oemof/solph/constraints/equate_variables.py b/src/oemof/solph/constraints/equate_variables.py index 52237830d..0238a2896 100644 --- a/src/oemof/solph/constraints/equate_variables.py +++ b/src/oemof/solph/constraints/equate_variables.py @@ -74,12 +74,12 @@ def equate_variables(model, var1, var2, factor1=1, name=None): ... label='powerline_1_2', ... inputs={bel1: solph.flows.Flow()}, ... outputs={bel2: solph.flows.Flow( - ... nominal_value=solph.Investment(ep_costs=20))})) + ... nominal_capacity=solph.Investment(ep_costs=20))})) >>> energysystem.add(solph.components.Converter( ... label='powerline_2_1', ... inputs={bel2: solph.flows.Flow()}, ... outputs={bel1: solph.flows.Flow( - ... nominal_value=solph.Investment(ep_costs=20))})) + ... nominal_capacity=solph.Investment(ep_costs=20))})) >>> om = solph.Model(energysystem) >>> line12 = energysystem.groups['powerline_1_2'] >>> line21 = energysystem.groups['powerline_2_1'] diff --git a/src/oemof/solph/constraints/integral_limit.py b/src/oemof/solph/constraints/integral_limit.py index 0544226db..38d09ec58 100644 --- a/src/oemof/solph/constraints/integral_limit.py +++ b/src/oemof/solph/constraints/integral_limit.py @@ -127,10 +127,10 @@ def generic_integral_limit( ... ) >>> bel = solph.buses.Bus(label='electricityBus') >>> flow1 = solph.flows.Flow( - ... nominal_value=100, + ... nominal_capacity=100, ... custom_attributes={"my_factor": 0.8}, ... ) - >>> flow2 = solph.flows.Flow(nominal_value=50) + >>> flow2 = solph.flows.Flow(nominal_capacity=50) >>> src1 = solph.components.Source(label='source1', outputs={bel: flow1}) >>> src2 = solph.components.Source(label='source2', outputs={bel: flow2}) >>> energysystem.add(bel, src1, src2) diff --git a/src/oemof/solph/constraints/investment_limit.py b/src/oemof/solph/constraints/investment_limit.py index 1d3b62454..abb6ec9fe 100644 --- a/src/oemof/solph/constraints/investment_limit.py +++ b/src/oemof/solph/constraints/investment_limit.py @@ -178,16 +178,16 @@ def additional_investment_flow_limit(model, keyword, limit=None): ... ) >>> bus = solph.buses.Bus(label='bus_1') >>> sink = solph.components.Sink(label="sink", inputs={bus: - ... solph.flows.Flow(nominal_value=10, fix=[10, 20, 30, 40, 50])}) + ... solph.flows.Flow(nominal_capacity=10, fix=[10, 20, 30, 40, 50])}) >>> src1 = solph.components.Source( ... label='source_0', outputs={bus: solph.flows.Flow( - ... nominal_value=solph.Investment( + ... nominal_capacity=solph.Investment( ... ep_costs=50, custom_attributes={"space": 4}, ... )) ... }) >>> src2 = solph.components.Source( ... label='source_1', outputs={bus: solph.flows.Flow( - ... nominal_value=solph.Investment( + ... nominal_capacity=solph.Investment( ... ep_costs=100, custom_attributes={"space": 1}, ... )) ... }) diff --git a/src/oemof/solph/flows/experimental/_electrical_line.py b/src/oemof/solph/flows/experimental/_electrical_line.py index 2b5e2eb85..8edd417f0 100644 --- a/src/oemof/solph/flows/experimental/_electrical_line.py +++ b/src/oemof/solph/flows/experimental/_electrical_line.py @@ -60,7 +60,7 @@ class ElectricalLine(Flow): def __init__(self, **kwargs): super().__init__( - nominal_value=kwargs.get("nominal_value"), + nominal_capacity=kwargs.get("nominal_value"), variable_costs=kwargs.get("variable_costs", 0), min=kwargs.get("min"), max=kwargs.get("max"), diff --git a/tests/test_components.py b/tests/test_components.py index f66230bfc..02a7b1d1e 100644 --- a/tests/test_components.py +++ b/tests/test_components.py @@ -70,8 +70,8 @@ def test_generic_storage_3(): components.GenericStorage( label="storage4", nominal_storage_capacity=45, - inputs={bel: Flow(nominal_value=23, variable_costs=10e10)}, - outputs={bel: Flow(nominal_value=7.5, variable_costs=10e10)}, + inputs={bel: Flow(nominal_capacity=23, variable_costs=10e10)}, + outputs={bel: Flow(nominal_capacity=7.5, variable_costs=10e10)}, loss_rate=0.00, initial_storage_level=0, inflow_conversion_factor=1, @@ -112,7 +112,9 @@ def test_generic_storage_with_non_convex_investment(): outputs={bel: Flow()}, invest_relation_input_capacity=1 / 6, invest_relation_output_capacity=1 / 6, - nominal_value=Investment(nonconvex=True, existing=5, maximum=25), + nominal_capacity=Investment( + nonconvex=True, existing=5, maximum=25 + ), ) @@ -261,7 +263,7 @@ def test_offsetconverter_investment_not_on_nonconvex(): b_diesel = Bus(label="bus_diesel") b_heat = Bus(label="bus_heat") components.OffsetConverter( - inputs={b_diesel: Flow(nominal_value=Investment(maximum=1))}, + inputs={b_diesel: Flow(nominal_capacity=Investment(maximum=1))}, outputs={b_heat: Flow(nonconvex=NonConvex())}, ) diff --git a/tests/test_components/test_offset_converter.py b/tests/test_components/test_offset_converter.py index 997a1140c..c094cb5d0 100644 --- a/tests/test_components/test_offset_converter.py +++ b/tests/test_components/test_offset_converter.py @@ -172,7 +172,7 @@ def test_custom_properties(): bus2 = solph.Bus() oc = solph.components.OffsetConverter( inputs={ - bus1: solph.Flow(nominal_value=2, nonconvex=solph.NonConvex()) + bus1: solph.Flow(nominal_capacity=2, nonconvex=solph.NonConvex()) }, outputs={bus2: solph.Flow()}, conversion_factors={bus2: 2}, @@ -189,7 +189,9 @@ def test_invalid_conversion_factor(): with pytest.raises(ValueError, match="Conversion factors cannot be "): solph.components.OffsetConverter( inputs={ - bus1: solph.Flow(nominal_value=2, nonconvex=solph.NonConvex()) + bus1: solph.Flow( + nominal_capacity=2, nonconvex=solph.NonConvex() + ) }, outputs={bus2: solph.Flow()}, conversion_factors={ @@ -206,7 +208,9 @@ def test_invalid_normed_offset(): with pytest.raises(ValueError, match="Normed offsets cannot be "): solph.components.OffsetConverter( inputs={ - bus1: solph.Flow(nominal_value=2, nonconvex=solph.NonConvex()) + bus1: solph.Flow( + nominal_capacity=2, nonconvex=solph.NonConvex() + ) }, outputs={bus2: solph.Flow()}, conversion_factors={ @@ -225,7 +229,9 @@ def test_wrong_number_of_coefficients(): with pytest.raises(ValueError, match="Two coefficients"): solph.components.OffsetTransformer( inputs={ - bus1: solph.Flow(nominal_value=2, nonconvex=solph.NonConvex()) + bus1: solph.Flow( + nominal_capacity=2, nonconvex=solph.NonConvex() + ) }, outputs={bus2: solph.Flow()}, coefficients=(1, 2, 3), @@ -474,7 +480,7 @@ def test_two_OffsetConverters_with_and_without_investment(): outputs={ es.groups["bus output 0"]: solph.Flow( nonconvex=solph.NonConvex(), - nominal_value=solph.Investment( + nominal_capacity=solph.Investment( maximum=nominal_value, ep_costs=10 ), ) @@ -528,7 +534,7 @@ def test_OffsetConverter_05x_compatibility(): outputs={ es.groups["bus output 0"]: solph.Flow( nonconvex=solph.NonConvex(), - nominal_value=nominal_value, + nominal_capacity=nominal_value, min=minimal_value / nominal_value, ) }, @@ -575,7 +581,7 @@ def test_error_handling(): outputs={ output_bus: solph.Flow( nonconvex=solph.NonConvex(), - nominal_value=10, + nominal_capacity=10, min=0.3, ) }, @@ -595,7 +601,7 @@ def test_error_handling(): outputs={ output_bus: solph.Flow( nonconvex=solph.NonConvex(), - nominal_value=10, + nominal_capacity=10, min=0.3, ) }, @@ -612,7 +618,7 @@ def test_error_handling(): outputs={ output_bus: solph.Flow( nonconvex=solph.NonConvex(), - nominal_value=10, + nominal_capacity=10, min=0.3, ) }, diff --git a/tests/test_components/test_sink.py b/tests/test_components/test_sink.py index fa0559650..a4bce2a26 100644 --- a/tests/test_components/test_sink.py +++ b/tests/test_components/test_sink.py @@ -32,7 +32,7 @@ def test_multi_input_sink(): solph.components.Sink( inputs={ es.node[f"bus input {i}"]: solph.Flow( - nominal_value=1, + nominal_capacity=1, variable_costs=costs, ) for i in range(num_in) diff --git a/tests/test_components/test_source.py b/tests/test_components/test_source.py index 7219639b0..cce218fc6 100644 --- a/tests/test_components/test_source.py +++ b/tests/test_components/test_source.py @@ -31,7 +31,7 @@ def test_multi_output_source(): solph.components.Source( outputs={ es.node[f"bus input {i}"]: solph.Flow( - nominal_value=1, + nominal_capacity=1, variable_costs=costs, ) for i in range(num_out) diff --git a/tests/test_components/test_storage.py b/tests/test_components/test_storage.py index 763883637..16faf3fae 100644 --- a/tests/test_components/test_storage.py +++ b/tests/test_components/test_storage.py @@ -66,12 +66,14 @@ def test_invest_power_uncoupled(): "storage", inputs={ bus: solph.Flow( - variable_costs=-1, nominal_value=solph.Investment(ep_costs=0.1) + variable_costs=-1, + nominal_capacity=solph.Investment(ep_costs=0.1), ) }, outputs={ bus: solph.Flow( - variable_costs=1, nominal_value=solph.Investment(ep_costs=0.1) + variable_costs=1, + nominal_capacity=solph.Investment(ep_costs=0.1), ) }, nominal_storage_capacity=10, @@ -110,12 +112,14 @@ def test_invest_power_coupled(): "storage", inputs={ bus: solph.Flow( - variable_costs=-1, nominal_value=solph.Investment(ep_costs=0.1) + variable_costs=-1, + nominal_capacity=solph.Investment(ep_costs=0.1), ) }, outputs={ bus: solph.Flow( - variable_costs=1, nominal_value=solph.Investment(ep_costs=0.1) + variable_costs=1, + nominal_capacity=solph.Investment(ep_costs=0.1), ) }, nominal_storage_capacity=10, @@ -153,8 +157,8 @@ def test_storage_charging(): storage = solph.components.GenericStorage( "storage", - inputs={bus: solph.Flow(nominal_value=2, variable_costs=-2)}, - outputs={bus: solph.Flow(nominal_value=0.1)}, + inputs={bus: solph.Flow(nominal_capacity=2, variable_costs=-2)}, + outputs={bus: solph.Flow(nominal_capacity=0.1)}, nominal_storage_capacity=19, initial_storage_level=0, balanced=False, @@ -188,8 +192,8 @@ def test_invest_content_uncoupled(): storage = solph.components.GenericStorage( "storage", - inputs={bus: solph.Flow(nominal_value=2, variable_costs=-2)}, - outputs={bus: solph.Flow(nominal_value=0.1)}, + inputs={bus: solph.Flow(nominal_capacity=2, variable_costs=-2)}, + outputs={bus: solph.Flow(nominal_capacity=0.1)}, nominal_storage_capacity=solph.Investment( ep_costs=0.1, ), @@ -228,8 +232,8 @@ def test_invest_content_minimum(): storage = solph.components.GenericStorage( "storage", - inputs={bus: solph.Flow(nominal_value=2, variable_costs=-2)}, - outputs={bus: solph.Flow(nominal_value=0.1, variable_costs=0.1)}, + inputs={bus: solph.Flow(nominal_capacity=2, variable_costs=-2)}, + outputs={bus: solph.Flow(nominal_capacity=0.1, variable_costs=0.1)}, nominal_storage_capacity=solph.Investment( ep_costs=0.1, minimum=32, @@ -269,8 +273,8 @@ def test_invest_content_minimum_nonconvex(): storage = solph.components.GenericStorage( "storage", - inputs={bus: solph.Flow(nominal_value=2, variable_costs=0.1)}, - outputs={bus: solph.Flow(nominal_value=0.1, variable_costs=0.1)}, + inputs={bus: solph.Flow(nominal_capacity=2, variable_costs=0.1)}, + outputs={bus: solph.Flow(nominal_capacity=0.1, variable_costs=0.1)}, nominal_storage_capacity=solph.Investment( ep_costs=0.1, minimum=32, @@ -312,11 +316,11 @@ def test_invest_content_maximum(): "storage", inputs={ bus: solph.Flow( - nominal_value=2, + nominal_capacity=2, variable_costs=[-2 + i * 0.01 for i in range(0, 11)], ) }, - outputs={bus: solph.Flow(nominal_value=0.1, variable_costs=0.1)}, + outputs={bus: solph.Flow(nominal_capacity=0.1, variable_costs=0.1)}, nominal_storage_capacity=solph.Investment( ep_costs=0.1, maximum=10, diff --git a/tests/test_constraints/test_constraints_module.py b/tests/test_constraints/test_constraints_module.py index 6a36b7049..30283a526 100644 --- a/tests/test_constraints/test_constraints_module.py +++ b/tests/test_constraints/test_constraints_module.py @@ -26,7 +26,7 @@ def test_integral_limit(): ) bel = solph.buses.Bus(label="electricityBus", balanced=False) flow1 = solph.flows.Flow( - nominal_value=100, + nominal_capacity=100, custom_attributes={ "my_factor": integral_weight1, "emission_factor": emission_factor_low, @@ -34,11 +34,11 @@ def test_integral_limit(): variable_costs=-1, ) flow2 = solph.flows.Flow( - nominal_value=50, + nominal_capacity=50, variable_costs=-0.5, ) flow3 = solph.flows.Flow( - nominal_value=100, + nominal_capacity=100, custom_attributes={ "my_factor": integral_weight3, "emission_factor": emission_factor_low, @@ -46,7 +46,7 @@ def test_integral_limit(): variable_costs=-0.5, ) flow4 = solph.flows.Flow( - nominal_value=500, + nominal_capacity=500, custom_attributes={ "emission_factor": emission_factor_high, }, diff --git a/tests/test_constraints/test_equate_flows.py b/tests/test_constraints/test_equate_flows.py index 33d76e2a0..7dc5c3e7b 100644 --- a/tests/test_constraints/test_equate_flows.py +++ b/tests/test_constraints/test_equate_flows.py @@ -28,7 +28,7 @@ def test_equate_flows(): b1: solph.Flow( variable_costs=-0.5, max=[0.5, 1], - nominal_value=4, + nominal_capacity=4, custom_attributes={"keyword1": "group 1"}, ) }, @@ -38,7 +38,7 @@ def test_equate_flows(): inputs={ b1: solph.Flow( variable_costs=0.1, - nominal_value=2, + nominal_capacity=2, custom_attributes={"keyword2": "group 2"}, ) }, @@ -48,7 +48,7 @@ def test_equate_flows(): inputs={ b1: solph.Flow( variable_costs=0.2, - nominal_value=3, + nominal_capacity=3, custom_attributes={"keyword2": "group 2"}, ) }, @@ -58,7 +58,7 @@ def test_equate_flows(): inputs={ b1: solph.Flow( variable_costs=0.2, - nominal_value=3, + nominal_capacity=3, custom_attributes={"keyword3": "no group"}, ) }, diff --git a/tests/test_constraints/test_flow_count_limit.py b/tests/test_constraints/test_flow_count_limit.py index 9b53300c6..89e6678e2 100644 --- a/tests/test_constraints/test_flow_count_limit.py +++ b/tests/test_constraints/test_flow_count_limit.py @@ -27,7 +27,7 @@ def test_flow_count_limit(): inputs={ b1: solph.Flow( variable_costs=[-0.5, 0.5], - nominal_value=4, + nominal_capacity=4, min=0.5, nonconvex=solph.NonConvex(), custom_attributes={"keyword1": "group 1"}, @@ -39,7 +39,7 @@ def test_flow_count_limit(): inputs={ b1: solph.Flow( variable_costs=[-0.2, 0.2], - nominal_value=2, + nominal_capacity=2, min=0.5, nonconvex=solph.NonConvex(), custom_attributes={"keyword1": "also group 1"}, @@ -51,7 +51,7 @@ def test_flow_count_limit(): inputs={ b1: solph.Flow( variable_costs=[-0.1, 0.1], - nominal_value=3, + nominal_capacity=3, min=0.5, nonconvex=solph.NonConvex(), custom_attributes={"keyword1": "still group 1"}, @@ -63,7 +63,7 @@ def test_flow_count_limit(): inputs={ b1: solph.Flow( variable_costs=[-0.1, 0.2], - nominal_value=3, + nominal_capacity=3, min=0.5, nonconvex=solph.NonConvex(), custom_attributes={"keyword2": "not in group 1"}, diff --git a/tests/test_constraints/test_storage_level.py b/tests/test_constraints/test_storage_level.py index 325b2e8ff..e55f7a7ed 100644 --- a/tests/test_constraints/test_storage_level.py +++ b/tests/test_constraints/test_storage_level.py @@ -32,28 +32,34 @@ def test_storage_level_constraint(): in_100 = solph.components.Source( label="in_100", - outputs={multiplexer: solph.Flow(nominal_value=5, variable_costs=0.1)}, + outputs={ + multiplexer: solph.Flow(nominal_capacity=5, variable_costs=0.1) + }, ) in_050 = solph.components.Source( label="in_050", - outputs={multiplexer: solph.Flow(nominal_value=5, variable_costs=0.1)}, + outputs={ + multiplexer: solph.Flow(nominal_capacity=5, variable_costs=0.1) + }, ) in_000 = solph.components.Source( label="in_000", - outputs={multiplexer: solph.Flow(nominal_value=5, variable_costs=0.1)}, + outputs={ + multiplexer: solph.Flow(nominal_capacity=5, variable_costs=0.1) + }, ) out_000 = solph.components.Sink( label="out_000", - inputs={multiplexer: solph.Flow(nominal_value=5)}, + inputs={multiplexer: solph.Flow(nominal_capacity=5)}, ) out_050 = solph.components.Sink( label="out_050", - inputs={multiplexer: solph.Flow(nominal_value=5)}, + inputs={multiplexer: solph.Flow(nominal_capacity=5)}, ) out_100 = solph.components.Sink( label="out_100", - inputs={multiplexer: solph.Flow(nominal_value=5)}, + inputs={multiplexer: solph.Flow(nominal_capacity=5)}, ) es.add(in_000, in_050, in_100, out_000, out_050, out_100) diff --git a/tests/test_flows/test_flow_class.py b/tests/test_flows/test_flow_class.py index 4ddcda1fb..cf5805d23 100644 --- a/tests/test_flows/test_flow_class.py +++ b/tests/test_flows/test_flow_class.py @@ -36,7 +36,7 @@ def test_summed_min_future_warning(): def test_source_with_full_load_time_max(): - Flow(nominal_value=1, full_load_time_max=2) + Flow(nominal_capacity=1, full_load_time_max=2) def test_nonconvex_positive_gradient_error(): @@ -74,7 +74,7 @@ def test_non_convex_negative_gradient_error(): def test_fix_sequence(): - flow = Flow(nominal_value=4, fix=[0.3, 0.2, 0.7]) + flow = Flow(nominal_capacity=4, fix=[0.3, 0.2, 0.7]) assert flow.fix[0] == 0.3 assert flow.fix[1] == 0.2 diff --git a/tests/test_flows/test_non_convex_flow.py b/tests/test_flows/test_non_convex_flow.py index d5960a898..1f8198e9d 100644 --- a/tests/test_flows/test_non_convex_flow.py +++ b/tests/test_flows/test_non_convex_flow.py @@ -16,7 +16,7 @@ def test_initial_status_off(): # negative costs but turned off initially flow = solph.flows.Flow( - nominal_value=10, + nominal_capacity=10, nonconvex=solph.NonConvex(initial_status=0, minimum_downtime=5), variable_costs=-1, ) @@ -27,7 +27,7 @@ def test_initial_status_off(): def test_maximum_shutdowns(): flow = solph.flows.Flow( - nominal_value=10, + nominal_capacity=10, min=0.5, nonconvex=solph.NonConvex(maximum_shutdowns=1), variable_costs=[1, -2, 1, 1, 1, -5, 1, 1, 1, -2], @@ -39,7 +39,7 @@ def test_maximum_shutdowns(): def test_maximum_startups(): flow = solph.flows.Flow( - nominal_value=10, + nominal_capacity=10, min=0.5, nonconvex=solph.NonConvex(maximum_startups=1), variable_costs=[1, -4, 1, 1, 1, -5, 1, 1, 5, -3], @@ -52,7 +52,7 @@ def test_maximum_startups(): def test_initial_status_on(): # positive costs but turned on initially flow = solph.flows.Flow( - nominal_value=10, + nominal_capacity=10, min=0.5, nonconvex=solph.NonConvex(initial_status=1, minimum_uptime=3), variable_costs=1, @@ -65,7 +65,7 @@ def test_initial_status_on(): def test_activity_costs(): # activity costs higher then revenue for first time steps flow = solph.flows.Flow( - nominal_value=10, + nominal_capacity=10, min=0.1, max=[0.1] + [i * 0.1 for i in range(1, 10)], nonconvex=solph.NonConvex(activity_costs=9 * [1] + [10]), @@ -79,7 +79,7 @@ def test_activity_costs(): def test_inactivity_costs(): # inactivity costs lower then running costs for middle time steps flow = solph.flows.Flow( - nominal_value=10, + nominal_capacity=10, min=[i * 0.1 for i in range(10)], nonconvex=solph.NonConvex(inactivity_costs=9 * [1] + [10]), variable_costs=0.45, @@ -94,7 +94,7 @@ def test_startup_costs_start_off(): # startup costs higher then effect of shutting down flow = solph.flows.Flow( - nominal_value=10, + nominal_capacity=10, min=0.1, nonconvex=solph.NonConvex(startup_costs=5, initial_status=0), variable_costs=price_pattern, @@ -109,7 +109,7 @@ def test_startup_costs_start_on(): # startup costs higher then effect of shutting down flow = solph.flows.Flow( - nominal_value=10, + nominal_capacity=10, min=0.1, nonconvex=solph.NonConvex(startup_costs=5, initial_status=1), variable_costs=price_pattern, @@ -124,7 +124,7 @@ def test_shutdown_costs_start_on(): # shutdown costs higher then effect of shutting down flow = solph.flows.Flow( - nominal_value=10, + nominal_capacity=10, min=0.1, nonconvex=solph.NonConvex(shutdown_costs=5, initial_status=1), variable_costs=price_pattern, diff --git a/tests/test_flows/test_simple_flow.py b/tests/test_flows/test_simple_flow.py index 8dca29043..4bc0c4b36 100644 --- a/tests/test_flows/test_simple_flow.py +++ b/tests/test_flows/test_simple_flow.py @@ -19,7 +19,7 @@ def test_gradient_limit(): price_pattern = [8] + 8 * [-1] + [8] flow = solph.flows.Flow( - nominal_value=2, + nominal_capacity=2, variable_costs=price_pattern, positive_gradient_limit=0.4, negative_gradient_limit=0.25, @@ -35,7 +35,7 @@ def test_full_load_time_max(): price_pattern = [-i for i in range(11)] flow = solph.flows.Flow( - nominal_value=2, + nominal_capacity=2, variable_costs=price_pattern, full_load_time_max=4.5, ) @@ -48,7 +48,7 @@ def test_full_load_time_min(): price_pattern = [i for i in range(11)] flow = solph.flows.Flow( - nominal_value=2, + nominal_capacity=2, variable_costs=price_pattern, full_load_time_min=4.5, ) diff --git a/tests/test_grouping.py b/tests/test_grouping.py index c9f0c450d..fb074aebc 100644 --- a/tests/test_grouping.py +++ b/tests/test_grouping.py @@ -41,7 +41,7 @@ def test_investment_flow_grouping(self): label="Source", outputs={ b: solph.flows.Flow( - fix=[12, 16, 14], nominal_value=1000000 + fix=[12, 16, 14], nominal_capacity=1000000 ) }, ) @@ -55,7 +55,9 @@ def test_investment_flow_grouping(self): full_load_time_max=2.3, variable_costs=25, max=0.8, - nominal_value=Investment(ep_costs=500, maximum=10e5), + nominal_capacity=Investment( + ep_costs=500, maximum=10e5 + ), ) }, ) diff --git a/tests/test_models.py b/tests/test_models.py index e5c51f9bd..feb982132 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -23,12 +23,14 @@ def test_infeasible_model(): es.add(bel) es.add( solph.components.Sink( - inputs={bel: solph.flows.Flow(nominal_value=5, fix=[1])} + inputs={bel: solph.flows.Flow(nominal_capacity=5, fix=[1])} ) ) es.add( solph.components.Source( - outputs={bel: solph.flows.Flow(nominal_value=4, variable_costs=5)} + outputs={ + bel: solph.flows.Flow(nominal_capacity=4, variable_costs=5) + } ) ) m = solph.Model(es) @@ -60,7 +62,7 @@ def test_multi_period_default_discount_rate(): label="sink", inputs={ bel: solph.flows.Flow( - nominal_value=5, fix=[1] * len(timeindex) + nominal_capacity=5, fix=[1] * len(timeindex) ) }, ) @@ -68,7 +70,9 @@ def test_multi_period_default_discount_rate(): es.add( solph.components.Source( label="source", - outputs={bel: solph.flows.Flow(nominal_value=4, variable_costs=5)}, + outputs={ + bel: solph.flows.Flow(nominal_capacity=4, variable_costs=5) + }, ) ) msg = ( diff --git a/tests/test_non_equidistant_time_index.py b/tests/test_non_equidistant_time_index.py index 1b68df738..24aed8d14 100644 --- a/tests/test_non_equidistant_time_index.py +++ b/tests/test_non_equidistant_time_index.py @@ -40,7 +40,7 @@ def setup_class(cls): inputs={b_diesel: flows.Flow(variable_costs=2)}, outputs={ b_el1: flows.Flow( - variable_costs=1, nominal_value=Investment(ep_costs=500) + variable_costs=1, nominal_capacity=Investment(ep_costs=500) ) }, conversion_factors={b_el1: 0.5}, @@ -64,7 +64,7 @@ def setup_class(cls): label="demand_el", inputs={ b_el1: flows.Flow( - nominal_value=1, + nominal_capacity=1, fix=demand_values, ) }, diff --git a/tests/test_options.py b/tests/test_options.py index 2b26b9d2f..b33baed83 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -16,4 +16,4 @@ def test_check_age_and_lifetime(): """Check error being thrown if age > lifetime""" msg = "A unit's age must be smaller than its expected lifetime." with pytest.raises(AttributeError, match=msg): - solph.Flow(nominal_value=solph.Investment(age=41, lifetime=40)) + solph.Flow(nominal_capacity=solph.Investment(age=41, lifetime=40)) diff --git a/tests/test_outputlib/__init__.py b/tests/test_outputlib/__init__.py index 5e86927e3..b717b21e2 100644 --- a/tests/test_outputlib/__init__.py +++ b/tests/test_outputlib/__init__.py @@ -33,7 +33,7 @@ outputs={ bel: Flow( fix=data["wind"], - nominal_value=66.3, + nominal_capacity=66.3, ) }, ) @@ -43,7 +43,7 @@ outputs={ bel: Flow( fix=data["pv"], - nominal_value=65.3, + nominal_capacity=65.3, ) }, ) @@ -53,7 +53,7 @@ label="demand_elec", inputs={ bel: Flow( - nominal_value=85, + nominal_capacity=85, fix=data["demand_el"], ) }, @@ -63,7 +63,7 @@ label="demand_therm", inputs={ bth: Flow( - nominal_value=40, + nominal_capacity=40, fix=data["demand_th"], ) }, @@ -73,28 +73,28 @@ pp_coal = Converter( label="pp_coal", inputs={bcoal: Flow()}, - outputs={bel: Flow(nominal_value=20.2, variable_costs=25)}, + outputs={bel: Flow(nominal_capacity=20.2, variable_costs=25)}, conversion_factors={bel: 0.39}, ) pp_lig = Converter( label="pp_lig", inputs={blig: Flow()}, - outputs={bel: Flow(nominal_value=11.8, variable_costs=19)}, + outputs={bel: Flow(nominal_capacity=11.8, variable_costs=19)}, conversion_factors={bel: 0.41}, ) pp_gas = Converter( label="pp_gas", inputs={bgas: Flow()}, - outputs={bel: Flow(nominal_value=41, variable_costs=40)}, + outputs={bel: Flow(nominal_capacity=41, variable_costs=40)}, conversion_factors={bel: 0.50}, ) pp_oil = Converter( label="pp_oil", inputs={boil: Flow()}, - outputs={bel: Flow(nominal_value=5, variable_costs=50)}, + outputs={bel: Flow(nominal_capacity=5, variable_costs=50)}, conversion_factors={bel: 0.28}, ) @@ -103,8 +103,8 @@ label="pp_chp", inputs={bgas: Flow()}, outputs={ - bel: Flow(nominal_value=30, variable_costs=42), - bth: Flow(nominal_value=40), + bel: Flow(nominal_capacity=30, variable_costs=42), + bth: Flow(nominal_capacity=40), }, conversion_factors={bel: 0.3, bth: 0.4}, ) @@ -118,7 +118,7 @@ heat_pump = Converter( label="heat_pump", inputs={bel: Flow(), b_heat_source: Flow()}, - outputs={bth: Flow(nominal_value=10)}, + outputs={bth: Flow(nominal_capacity=10)}, conversion_factors={bel: 1 / 3, b_heat_source: (cop - 1) / cop}, ) diff --git a/tests/test_processing.py b/tests/test_processing.py index c7b93c900..e563fece7 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -51,7 +51,7 @@ def setup_class(cls): inputs={b_diesel: Flow(variable_costs=2)}, outputs={ b_el1: Flow( - variable_costs=1, nominal_value=Investment(ep_costs=0.5) + variable_costs=1, nominal_capacity=Investment(ep_costs=0.5) ) }, conversion_factors={b_el1: 2}, @@ -75,7 +75,7 @@ def setup_class(cls): label="demand_el", inputs={ b_el2: Flow( - nominal_value=1, + nominal_capacity=1, fix=cls.demand_values, ) }, diff --git a/tests/test_scripts/test_solph/test_connect_invest/test_connect_invest.py b/tests/test_scripts/test_solph/test_connect_invest/test_connect_invest.py index a409177fb..7af2cab04 100644 --- a/tests/test_scripts/test_solph/test_connect_invest/test_connect_invest.py +++ b/tests/test_scripts/test_solph/test_connect_invest/test_connect_invest.py @@ -57,7 +57,7 @@ def test_connect_invest(): es.add( components.Source( label="wind", - outputs={bel1: Flow(fix=data["wind"], nominal_value=1000000)}, + outputs={bel1: Flow(fix=data["wind"], nominal_capacity=1000000)}, ) ) @@ -65,7 +65,7 @@ def test_connect_invest(): es.add( components.Sink( label="demand", - inputs={bel1: Flow(fix=data["demand_el"], nominal_value=1)}, + inputs={bel1: Flow(fix=data["demand_el"], nominal_capacity=1)}, ) ) @@ -86,14 +86,14 @@ def test_connect_invest(): line12 = components.Converter( label="line12", inputs={bel1: Flow()}, - outputs={bel2: Flow(nominal_value=Investment(ep_costs=20))}, + outputs={bel2: Flow(nominal_capacity=Investment(ep_costs=20))}, ) es.add(line12) line21 = components.Converter( label="line21", inputs={bel2: Flow()}, - outputs={bel1: Flow(nominal_value=Investment(ep_costs=20))}, + outputs={bel1: Flow(nominal_capacity=Investment(ep_costs=20))}, ) es.add(line21) diff --git a/tests/test_scripts/test_solph/test_flexible_modelling/test_add_constraints.py b/tests/test_scripts/test_solph/test_flexible_modelling/test_add_constraints.py index 01a63e426..00868963d 100644 --- a/tests/test_scripts/test_solph/test_flexible_modelling/test_add_constraints.py +++ b/tests/test_scripts/test_solph/test_flexible_modelling/test_add_constraints.py @@ -45,13 +45,13 @@ def test_add_constraints_example(solver="cbc", nologg=False): es.add( components.Sink( label="Sink", - inputs={b_el: Flow(nominal_value=40, fix=[0.5, 0.4, 0.3, 1])}, + inputs={b_el: Flow(nominal_capacity=40, fix=[0.5, 0.4, 0.3, 1])}, ) ) pp_oil = components.Converter( label="pp_oil", inputs={boil: Flow()}, - outputs={b_el: Flow(nominal_value=50, variable_costs=25)}, + outputs={b_el: Flow(nominal_capacity=50, variable_costs=25)}, conversion_factors={b_el: 0.39}, ) @@ -60,7 +60,7 @@ def test_add_constraints_example(solver="cbc", nologg=False): components.Converter( label="pp_lig", inputs={blig: Flow()}, - outputs={b_el: Flow(nominal_value=50, variable_costs=10)}, + outputs={b_el: Flow(nominal_capacity=50, variable_costs=10)}, conversion_factors={b_el: 0.41}, ) ) 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..e7b557428 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 @@ -59,7 +59,9 @@ def test_gen_chp(): solph.components.Sink( label="demand_th", inputs={ - bth: solph.flows.Flow(fix=data["demand_th"], nominal_value=200) + bth: solph.flows.Flow( + fix=data["demand_th"], nominal_capacity=200 + ) }, ) ) diff --git a/tests/test_scripts/test_solph/test_invest_fix_flow/test_simple_invest_fixed.py b/tests/test_scripts/test_solph/test_invest_fix_flow/test_simple_invest_fixed.py index 1a025b9a6..2a90f4e1d 100644 --- a/tests/test_scripts/test_solph/test_invest_fix_flow/test_simple_invest_fixed.py +++ b/tests/test_scripts/test_solph/test_invest_fix_flow/test_simple_invest_fixed.py @@ -50,14 +50,16 @@ def test_dispatch_fix_example(solver="cbc", periods=10): pv = Source( label="pv", outputs={ - bel: Flow(fix=data["pv"], nominal_value=Investment(ep_costs=ep_pv)) + bel: Flow( + fix=data["pv"], nominal_capacity=Investment(ep_costs=ep_pv) + ) }, ) # demands (electricity/heat) demand_el = Sink( label="demand_elec", - inputs={bel: Flow(nominal_value=85, fix=data["demand_el"])}, + inputs={bel: Flow(nominal_capacity=85, fix=data["demand_el"])}, ) datetimeindex = pd.date_range("1/1/2012", periods=periods, freq="h") diff --git a/tests/test_scripts/test_solph/test_lopf/test_lopf.py b/tests/test_scripts/test_solph/test_lopf/test_lopf.py index e831734a4..1a6246697 100644 --- a/tests/test_scripts/test_solph/test_lopf/test_lopf.py +++ b/tests/test_scripts/test_solph/test_lopf/test_lopf.py @@ -55,7 +55,7 @@ def test_lopf(solver="cbc"): input=b_el0, output=b_el1, reactance=0.0001, - nominal_value=Investment(ep_costs=10), + nominal_capacity=Investment(ep_costs=10), min=-1, max=1, ) @@ -64,7 +64,7 @@ def test_lopf(solver="cbc"): input=b_el1, output=b_el2, reactance=0.0001, - nominal_value=60, + nominal_capacity=60, min=-1, max=1, ) @@ -73,7 +73,7 @@ def test_lopf(solver="cbc"): input=b_el2, output=b_el0, reactance=0.0001, - nominal_value=60, + nominal_capacity=60, min=-1, max=1, ) @@ -81,21 +81,21 @@ def test_lopf(solver="cbc"): es.add( Source( label="gen_0", - outputs={b_el0: Flow(nominal_value=100, variable_costs=50)}, + outputs={b_el0: Flow(nominal_capacity=100, variable_costs=50)}, ) ) es.add( Source( label="gen_1", - outputs={b_el1: Flow(nominal_value=100, variable_costs=25)}, + outputs={b_el1: Flow(nominal_capacity=100, variable_costs=25)}, ) ) es.add( Sink( label="load", - inputs={b_el2: Flow(nominal_value=100, fix=1)}, + inputs={b_el2: Flow(nominal_capacity=100, fix=1)}, ) ) diff --git a/tests/test_scripts/test_solph/test_multi_period_model/test_multi_period_dispatch_model.py b/tests/test_scripts/test_solph/test_multi_period_model/test_multi_period_dispatch_model.py index c61952720..3e391938a 100644 --- a/tests/test_scripts/test_solph/test_multi_period_model/test_multi_period_dispatch_model.py +++ b/tests/test_scripts/test_solph/test_multi_period_model/test_multi_period_dispatch_model.py @@ -83,13 +83,15 @@ def test_multi_period_dispatch_model(solver="cbc"): bus_el: flows.Flow( variable_costs=0, fix=[110] + [90] * (len(timeindex) - 1), - nominal_value=1, + nominal_capacity=1, ) }, ) source_shortage = components.Source( label="DE_source_shortage", - outputs={bus_el: flows.Flow(variable_costs=1e10, nominal_value=1e10)}, + outputs={ + bus_el: flows.Flow(variable_costs=1e10, nominal_capacity=1e10) + }, ) source_wind_fr = components.Source( label="FR_source_wind", @@ -97,14 +99,14 @@ def test_multi_period_dispatch_model(solver="cbc"): bus_el_fr: flows.Flow( variable_costs=0, fix=[45] * len(timeindex), - nominal_value=1, + nominal_capacity=1, ) }, ) source_shortage_fr = components.Source( label="FR_source_shortage", outputs={ - bus_el_fr: flows.Flow(variable_costs=1e10, nominal_value=1e10) + bus_el_fr: flows.Flow(variable_costs=1e10, nominal_capacity=1e10) }, ) @@ -112,36 +114,42 @@ def test_multi_period_dispatch_model(solver="cbc"): sink_el = components.Sink( label="DE_sink_el", inputs={ - bus_el: flows.Flow(fix=[80] * len(timeindex), nominal_value=1) + bus_el: flows.Flow(fix=[80] * len(timeindex), nominal_capacity=1) }, ) sink_excess = components.Sink( label="DE_sink_excess", - inputs={bus_el: flows.Flow(variable_costs=1e10, nominal_value=1e10)}, + inputs={ + bus_el: flows.Flow(variable_costs=1e10, nominal_capacity=1e10) + }, ) sink_el_fr = components.Sink( label="FR_sink_el", inputs={ - bus_el_fr: flows.Flow(fix=[50] * len(timeindex), nominal_value=1) + bus_el_fr: flows.Flow( + fix=[50] * len(timeindex), nominal_capacity=1 + ) }, ) sink_excess_fr = components.Sink( label="FR_sink_excess", - inputs={bus_el_fr: flows.Flow(variable_costs=1e3, nominal_value=1e10)}, + inputs={ + bus_el_fr: flows.Flow(variable_costs=1e3, nominal_capacity=1e10) + }, ) # Create converters pp_lignite = components.Converter( label="DE_pp_lignite", inputs={bus_lignite: flows.Flow()}, - outputs={bus_el: flows.Flow(nominal_value=100, variable_costs=1)}, + outputs={bus_el: flows.Flow(nominal_capacity=100, variable_costs=1)}, conversion_factors={bus_el: 0.38}, ) pp_hardcoal = components.Converter( label="DE_pp_hardcoal", inputs={bus_hardcoal: flows.Flow()}, - outputs={bus_el: flows.Flow(nominal_value=100, variable_costs=2)}, + outputs={bus_el: flows.Flow(nominal_capacity=100, variable_costs=2)}, conversion_factors={bus_el: 0.45}, ) @@ -150,7 +158,7 @@ def test_multi_period_dispatch_model(solver="cbc"): inputs={bus_natgas: flows.Flow()}, outputs={ bus_el: flows.Flow( - nominal_value=100, + nominal_capacity=100, variable_costs=3, ) }, @@ -162,7 +170,7 @@ def test_multi_period_dispatch_model(solver="cbc"): inputs={bus_natgas: flows.Flow()}, outputs={ bus_el: flows.Flow( - nominal_value=100, + nominal_capacity=100, variable_costs=4, ) }, @@ -171,9 +179,11 @@ def test_multi_period_dispatch_model(solver="cbc"): storage_el = components.GenericStorage( label="DE_storage_el", - inputs={bus_el: flows.Flow(nominal_value=20, variable_costs=0, max=1)}, + inputs={ + bus_el: flows.Flow(nominal_capacity=20, variable_costs=0, max=1) + }, outputs={ - bus_el: flows.Flow(nominal_value=20, variable_costs=0, max=1) + bus_el: flows.Flow(nominal_capacity=20, variable_costs=0, max=1) }, nominal_storage_capacity=20, loss_rate=0, @@ -189,8 +199,8 @@ def test_multi_period_dispatch_model(solver="cbc"): link_de_fr = components.Link( label="link_DE_FR", inputs={ - bus_el: flows.Flow(nominal_value=10), - bus_el_fr: flows.Flow(nominal_value=10), + bus_el: flows.Flow(nominal_capacity=10), + bus_el_fr: flows.Flow(nominal_capacity=10), }, outputs={ bus_el_fr: flows.Flow(), diff --git a/tests/test_scripts/test_solph/test_multi_period_model/test_multi_period_investment_model.py b/tests/test_scripts/test_solph/test_multi_period_model/test_multi_period_investment_model.py index 43c45834b..fcaf31c93 100644 --- a/tests/test_scripts/test_solph/test_multi_period_model/test_multi_period_investment_model.py +++ b/tests/test_scripts/test_solph/test_multi_period_model/test_multi_period_investment_model.py @@ -86,14 +86,14 @@ def test_multi_period_investment_model(solver="cbc"): bus_el: flows.Flow( variable_costs=0, fix=[110] + [90] * (len(timeindex) - 1), - nominal_value=1, + nominal_capacity=1, ) }, ) source_shortage = components.Source( label="DE_source_shortage", outputs={ - bus_el: flows.Flow(variable_costs=1e10, nominal_value=1e10) + bus_el: flows.Flow(variable_costs=1e10, nominal_capacity=1e10) }, ) source_wind_FR = components.Source( @@ -102,14 +102,16 @@ def test_multi_period_investment_model(solver="cbc"): bus_el_FR: flows.Flow( variable_costs=0, fix=[45] * len(timeindex), - nominal_value=1, + nominal_capacity=1, ) }, ) source_shortage_FR = components.Source( label="FR_source_shortage", outputs={ - bus_el_FR: flows.Flow(variable_costs=1e10, nominal_value=1e10) + bus_el_FR: flows.Flow( + variable_costs=1e10, nominal_capacity=1e10 + ) }, ) @@ -117,27 +119,31 @@ def test_multi_period_investment_model(solver="cbc"): sink_el = components.Sink( label="DE_sink_el", inputs={ - bus_el: flows.Flow(fix=[80] * len(timeindex), nominal_value=1) + bus_el: flows.Flow( + fix=[80] * len(timeindex), nominal_capacity=1 + ) }, ) sink_excess = components.Sink( label="DE_sink_excess", inputs={ - bus_el: flows.Flow(variable_costs=1e10, nominal_value=1e10) + bus_el: flows.Flow(variable_costs=1e10, nominal_capacity=1e10) }, ) sink_el_FR = components.Sink( label="FR_sink_el", inputs={ bus_el_FR: flows.Flow( - fix=[50] * len(timeindex), nominal_value=1 + fix=[50] * len(timeindex), nominal_capacity=1 ) }, ) sink_excess_FR = components.Sink( label="FR_sink_excess", inputs={ - bus_el_FR: flows.Flow(variable_costs=1e3, nominal_value=1e10) + bus_el_FR: flows.Flow( + variable_costs=1e3, nominal_capacity=1e10 + ) }, ) @@ -147,7 +153,7 @@ def test_multi_period_investment_model(solver="cbc"): inputs={bus_lignite: flows.Flow()}, outputs={ bus_el: flows.Flow( - nominal_value=_options.Investment( + nominal_capacity=_options.Investment( maximum=1000, ep_costs=2e6, existing=0, @@ -166,7 +172,7 @@ def test_multi_period_investment_model(solver="cbc"): inputs={bus_hardcoal: flows.Flow()}, # )}, outputs={ bus_el: flows.Flow( - nominal_value=_options.Investment( + nominal_capacity=_options.Investment( maximum=1000, ep_costs=1.6e6, existing=0, @@ -185,7 +191,7 @@ def test_multi_period_investment_model(solver="cbc"): inputs={bus_natgas: flows.Flow()}, # )}, outputs={ bus_el: flows.Flow( - nominal_value=_options.Investment( + nominal_capacity=_options.Investment( maximum=1000, ep_costs=1e6, existing=0, @@ -204,7 +210,7 @@ def test_multi_period_investment_model(solver="cbc"): inputs={bus_natgas: flows.Flow()}, # )}, outputs={ bus_el: flows.Flow( - nominal_value=_options.Investment( + nominal_capacity=_options.Investment( maximum=1000, ep_costs=[0.6e6, 0.5e6, 0.8e6, 0.4e6], existing=0, @@ -225,7 +231,7 @@ def test_multi_period_investment_model(solver="cbc"): bus_el: flows.Flow( variable_costs=0, max=1, - nominal_value=_options.Investment( + nominal_capacity=_options.Investment( maximum=20, ep_costs=1000, existing=10, @@ -239,7 +245,7 @@ def test_multi_period_investment_model(solver="cbc"): bus_el: flows.Flow( variable_costs=0, max=1, - nominal_value=_options.Investment( + nominal_capacity=_options.Investment( maximum=20, ep_costs=1000, existing=10, @@ -274,10 +280,10 @@ def test_multi_period_investment_model(solver="cbc"): label="link_DE_FR", inputs={ bus_el: flows.Flow( - nominal_value=10, + nominal_capacity=10, ), bus_el_FR: flows.Flow( - nominal_value=10, + nominal_capacity=10, ), }, outputs={bus_el_FR: flows.Flow(), bus_el: flows.Flow()}, diff --git a/tests/test_scripts/test_solph/test_piecewiselineartransformer/test_piecewiselineartransformer.py b/tests/test_scripts/test_solph/test_piecewiselineartransformer/test_piecewiselineartransformer.py index 64823a6b9..b3bf89a77 100644 --- a/tests/test_scripts/test_solph/test_piecewiselineartransformer/test_piecewiselineartransformer.py +++ b/tests/test_scripts/test_solph/test_piecewiselineartransformer/test_piecewiselineartransformer.py @@ -38,7 +38,7 @@ def test_pwltf(): b_el = Bus(label="electricity") demand_el = Sink( label="demand", - inputs={b_el: Flow(nominal_value=1, fix=demand)}, + inputs={b_el: Flow(nominal_capacity=1, fix=demand)}, ) energysystem.add(b_gas, b_el, demand_el) @@ -52,7 +52,9 @@ def conv_func(x): # Create and add PiecewiseLinearConverter pwltf = solph.components.experimental.PiecewiseLinearConverter( label="pwltf", - inputs={b_gas: solph.flows.Flow(nominal_value=100, variable_costs=1)}, + inputs={ + b_gas: solph.flows.Flow(nominal_capacity=100, variable_costs=1) + }, outputs={b_el: solph.flows.Flow()}, in_breakpoints=in_breakpoints, conversion_function=conv_func, diff --git a/tests/test_scripts/test_solph/test_simple_model/test_simple_dispatch.py b/tests/test_scripts/test_solph/test_simple_model/test_simple_dispatch.py index e8d12b264..7705fc4c3 100644 --- a/tests/test_scripts/test_solph/test_simple_model/test_simple_dispatch.py +++ b/tests/test_scripts/test_solph/test_simple_model/test_simple_dispatch.py @@ -54,50 +54,51 @@ def test_dispatch_example(solver="cbc", periods=24 * 5): # sources wind = Source( - label="wind", outputs={bel: Flow(fix=data["wind"], nominal_value=66.3)} + label="wind", + outputs={bel: Flow(fix=data["wind"], nominal_capacity=66.3)}, ) pv = Source( - label="pv", outputs={bel: Flow(fix=data["pv"], nominal_value=65.3)} + label="pv", outputs={bel: Flow(fix=data["pv"], nominal_capacity=65.3)} ) # demands (electricity/heat) demand_el = Sink( label="demand_elec", - inputs={bel: Flow(nominal_value=85, fix=data["demand_el"])}, + inputs={bel: Flow(nominal_capacity=85, fix=data["demand_el"])}, ) demand_th = Sink( label="demand_therm", - inputs={bth: Flow(nominal_value=40, fix=data["demand_th"])}, + inputs={bth: Flow(nominal_capacity=40, fix=data["demand_th"])}, ) # power plants pp_coal = Converter( label="pp_coal", inputs={bcoal: Flow()}, - outputs={bel: Flow(nominal_value=20.2, variable_costs=25)}, + outputs={bel: Flow(nominal_capacity=20.2, variable_costs=25)}, conversion_factors={bel: 0.39}, ) pp_lig = Converter( label="pp_lig", inputs={blig: Flow()}, - outputs={bel: Flow(nominal_value=11.8, variable_costs=19)}, + outputs={bel: Flow(nominal_capacity=11.8, variable_costs=19)}, conversion_factors={bel: 0.41}, ) pp_gas = Converter( label="pp_gas", inputs={bgas: Flow()}, - outputs={bel: Flow(nominal_value=41, variable_costs=40)}, + outputs={bel: Flow(nominal_capacity=41, variable_costs=40)}, conversion_factors={bel: 0.50}, ) pp_oil = Converter( label="pp_oil", inputs={boil: Flow()}, - outputs={bel: Flow(nominal_value=5, variable_costs=50)}, + outputs={bel: Flow(nominal_capacity=5, variable_costs=50)}, conversion_factors={bel: 0.28}, ) @@ -106,8 +107,8 @@ def test_dispatch_example(solver="cbc", periods=24 * 5): label="pp_chp", inputs={bgas: Flow()}, outputs={ - bel: Flow(nominal_value=30, variable_costs=42), - bth: Flow(nominal_value=40), + bel: Flow(nominal_capacity=30, variable_costs=42), + bth: Flow(nominal_capacity=40), }, conversion_factors={bel: 0.3, bth: 0.4}, ) @@ -121,7 +122,7 @@ def test_dispatch_example(solver="cbc", periods=24 * 5): heat_pump = Converter( label="heat_pump", inputs={bel: Flow(), b_heat_source: Flow()}, - outputs={bth: Flow(nominal_value=10)}, + outputs={bth: Flow(nominal_capacity=10)}, conversion_factors={bel: 1 / 3, b_heat_source: (cop - 1) / cop}, ) diff --git a/tests/test_scripts/test_solph/test_simple_model/test_simple_dispatch_one.py b/tests/test_scripts/test_solph/test_simple_model/test_simple_dispatch_one.py index 99a2f7478..2ba6aebbe 100644 --- a/tests/test_scripts/test_solph/test_simple_model/test_simple_dispatch_one.py +++ b/tests/test_scripts/test_solph/test_simple_model/test_simple_dispatch_one.py @@ -40,16 +40,16 @@ def test_dispatch_one_time_step(solver="cbc"): # sources wind = Source( - label="wind", outputs={bel: Flow(fix=0.5, nominal_value=66.3)} + label="wind", outputs={bel: Flow(fix=0.5, nominal_capacity=66.3)} ) # demands (electricity/heat) demand_el = Sink( - label="demand_elec", inputs={bel: Flow(nominal_value=85, fix=0.3)} + label="demand_elec", inputs={bel: Flow(nominal_capacity=85, fix=0.3)} ) demand_th = Sink( - label="demand_therm", inputs={bth: Flow(nominal_value=40, fix=0.2)} + label="demand_therm", inputs={bth: Flow(nominal_capacity=40, fix=0.2)} ) # combined heat and power plant (chp) @@ -57,8 +57,8 @@ def test_dispatch_one_time_step(solver="cbc"): label="pp_chp", inputs={bgas: Flow()}, outputs={ - bel: Flow(nominal_value=30, variable_costs=42), - bth: Flow(nominal_value=40), + bel: Flow(nominal_capacity=30, variable_costs=42), + bth: Flow(nominal_capacity=40), }, conversion_factors={bel: 0.3, bth: 0.4}, ) @@ -72,7 +72,7 @@ def test_dispatch_one_time_step(solver="cbc"): heat_pump = Converter( label="heat_pump", inputs={bel: Flow(), b_heat_source: Flow()}, - outputs={bth: Flow(nominal_value=10)}, + outputs={bth: Flow(nominal_capacity=10)}, conversion_factors={bel: 1 / 3, b_heat_source: (cop - 1) / cop}, ) diff --git a/tests/test_scripts/test_solph/test_simple_model/test_simple_dispatch_one_explicit_timemode.py b/tests/test_scripts/test_solph/test_simple_model/test_simple_dispatch_one_explicit_timemode.py index 99a2f7478..2ba6aebbe 100644 --- a/tests/test_scripts/test_solph/test_simple_model/test_simple_dispatch_one_explicit_timemode.py +++ b/tests/test_scripts/test_solph/test_simple_model/test_simple_dispatch_one_explicit_timemode.py @@ -40,16 +40,16 @@ def test_dispatch_one_time_step(solver="cbc"): # sources wind = Source( - label="wind", outputs={bel: Flow(fix=0.5, nominal_value=66.3)} + label="wind", outputs={bel: Flow(fix=0.5, nominal_capacity=66.3)} ) # demands (electricity/heat) demand_el = Sink( - label="demand_elec", inputs={bel: Flow(nominal_value=85, fix=0.3)} + label="demand_elec", inputs={bel: Flow(nominal_capacity=85, fix=0.3)} ) demand_th = Sink( - label="demand_therm", inputs={bth: Flow(nominal_value=40, fix=0.2)} + label="demand_therm", inputs={bth: Flow(nominal_capacity=40, fix=0.2)} ) # combined heat and power plant (chp) @@ -57,8 +57,8 @@ def test_dispatch_one_time_step(solver="cbc"): label="pp_chp", inputs={bgas: Flow()}, outputs={ - bel: Flow(nominal_value=30, variable_costs=42), - bth: Flow(nominal_value=40), + bel: Flow(nominal_capacity=30, variable_costs=42), + bth: Flow(nominal_capacity=40), }, conversion_factors={bel: 0.3, bth: 0.4}, ) @@ -72,7 +72,7 @@ def test_dispatch_one_time_step(solver="cbc"): heat_pump = Converter( label="heat_pump", inputs={bel: Flow(), b_heat_source: Flow()}, - outputs={bth: Flow(nominal_value=10)}, + outputs={bth: Flow(nominal_capacity=10)}, conversion_factors={bel: 1 / 3, b_heat_source: (cop - 1) / cop}, ) diff --git a/tests/test_scripts/test_solph/test_simple_model/test_simple_invest.py b/tests/test_scripts/test_solph/test_simple_model/test_simple_invest.py index 3634406f3..0d6e5d3b9 100644 --- a/tests/test_scripts/test_solph/test_simple_model/test_simple_invest.py +++ b/tests/test_scripts/test_solph/test_simple_model/test_simple_invest.py @@ -61,7 +61,7 @@ def test_dispatch_example(solver="cbc", periods=24 * 5): outputs={ bel: Flow( fix=data["wind"], - nominal_value=Investment(ep_costs=ep_wind, existing=100), + nominal_capacity=Investment(ep_costs=ep_wind, existing=100), ) }, ) @@ -72,7 +72,7 @@ def test_dispatch_example(solver="cbc", periods=24 * 5): outputs={ bel: Flow( fix=data["pv"], - nominal_value=Investment(ep_costs=ep_pv, existing=80), + nominal_capacity=Investment(ep_costs=ep_pv, existing=80), ) }, ) @@ -80,40 +80,40 @@ def test_dispatch_example(solver="cbc", periods=24 * 5): # demands (electricity/heat) demand_el = Sink( label="demand_elec", - inputs={bel: Flow(nominal_value=85, fix=data["demand_el"])}, + inputs={bel: Flow(nominal_capacity=85, fix=data["demand_el"])}, ) demand_th = Sink( label="demand_therm", - inputs={bth: Flow(nominal_value=40, fix=data["demand_th"])}, + inputs={bth: Flow(nominal_capacity=40, fix=data["demand_th"])}, ) # power plants pp_coal = Converter( label="pp_coal", inputs={bcoal: Flow()}, - outputs={bel: Flow(nominal_value=20.2, variable_costs=25)}, + outputs={bel: Flow(nominal_capacity=20.2, variable_costs=25)}, conversion_factors={bel: 0.39}, ) pp_lig = Converter( label="pp_lig", inputs={blig: Flow()}, - outputs={bel: Flow(nominal_value=11.8, variable_costs=19)}, + outputs={bel: Flow(nominal_capacity=11.8, variable_costs=19)}, conversion_factors={bel: 0.41}, ) pp_gas = Converter( label="pp_gas", inputs={bgas: Flow()}, - outputs={bel: Flow(nominal_value=41, variable_costs=40)}, + outputs={bel: Flow(nominal_capacity=41, variable_costs=40)}, conversion_factors={bel: 0.50}, ) pp_oil = Converter( label="pp_oil", inputs={boil: Flow()}, - outputs={bel: Flow(nominal_value=5, variable_costs=50)}, + outputs={bel: Flow(nominal_capacity=5, variable_costs=50)}, conversion_factors={bel: 0.28}, ) @@ -122,8 +122,8 @@ def test_dispatch_example(solver="cbc", periods=24 * 5): label="pp_chp", inputs={bgas: Flow()}, outputs={ - bel: Flow(nominal_value=30, variable_costs=42), - bth: Flow(nominal_value=40), + bel: Flow(nominal_capacity=30, variable_costs=42), + bth: Flow(nominal_capacity=40), }, conversion_factors={bel: 0.3, bth: 0.4}, ) @@ -137,7 +137,7 @@ def test_dispatch_example(solver="cbc", periods=24 * 5): heat_pump = Converter( label="el_heat_pump", inputs={bel: Flow(), b_heat_source: Flow()}, - outputs={bth: Flow(nominal_value=10)}, + outputs={bth: Flow(nominal_capacity=10)}, conversion_factors={bel: 1 / 3, b_heat_source: (cop - 1) / cop}, ) diff --git a/tests/test_scripts/test_solph/test_storage_investment/test_invest_storage_regression.py b/tests/test_scripts/test_solph/test_storage_investment/test_invest_storage_regression.py index b33f915e5..244556e1f 100644 --- a/tests/test_scripts/test_solph/test_storage_investment/test_invest_storage_regression.py +++ b/tests/test_scripts/test_solph/test_storage_investment/test_invest_storage_regression.py @@ -39,7 +39,7 @@ def test_regression_investment_storage(solver="cbc"): label="demand", inputs={ bel: solph.flows.Flow( - fix=[209643, 207497, 200108, 191892], nominal_value=1 + fix=[209643, 207497, 200108, 191892], nominal_capacity=1 ) }, ) @@ -57,7 +57,7 @@ def test_regression_investment_storage(solver="cbc"): solph.components.Converter( label="pp_gas", inputs={bgas: solph.flows.Flow()}, - outputs={bel: solph.flows.Flow(nominal_value=300000)}, + outputs={bel: solph.flows.Flow(nominal_capacity=300000)}, conversion_factors={bel: 0.58}, ) ) @@ -68,14 +68,14 @@ def test_regression_investment_storage(solver="cbc"): label="storage", inputs={ bel: solph.flows.Flow( - nominal_value=solph.Investment( + nominal_capacity=solph.Investment( existing=625046 / 6, maximum=0 ) ) }, outputs={ bel: solph.flows.Flow( - nominal_value=solph.Investment( + nominal_capacity=solph.Investment( existing=104174.33, maximum=1 ) ) diff --git a/tests/test_scripts/test_solph/test_storage_investment/test_storage_investment.py b/tests/test_scripts/test_solph/test_storage_investment/test_storage_investment.py index 92232979b..478d7273d 100644 --- a/tests/test_scripts/test_solph/test_storage_investment/test_storage_investment.py +++ b/tests/test_scripts/test_solph/test_storage_investment/test_storage_investment.py @@ -80,7 +80,9 @@ def test_optimise_storage_size( solph.components.Sink( label="demand", inputs={ - bel: solph.flows.Flow(fix=data["demand_el"], nominal_value=1) + bel: solph.flows.Flow( + fix=data["demand_el"], nominal_capacity=1 + ) }, ) ) @@ -91,7 +93,8 @@ def test_optimise_storage_size( label="rgas", outputs={ bgas: solph.flows.Flow( - nominal_value=194397000 * 400 / 8760, full_load_time_max=1 + nominal_capacity=194397000 * 400 / 8760, + full_load_time_max=1, ) }, ) @@ -101,7 +104,9 @@ def test_optimise_storage_size( solph.components.Source( label="wind", outputs={ - bel: solph.flows.Flow(fix=data["wind"], nominal_value=1000000) + bel: solph.flows.Flow( + fix=data["wind"], nominal_capacity=1000000 + ) }, ) ) @@ -110,7 +115,7 @@ def test_optimise_storage_size( solph.components.Source( label="pv", outputs={ - bel: solph.flows.Flow(fix=data["pv"], nominal_value=582000) + bel: solph.flows.Flow(fix=data["pv"], nominal_capacity=582000) }, ) ) @@ -120,7 +125,7 @@ def test_optimise_storage_size( label="pp_gas", inputs={bgas: solph.flows.Flow()}, outputs={ - bel: solph.flows.Flow(nominal_value=10e10, variable_costs=50) + bel: solph.flows.Flow(nominal_capacity=10e10, variable_costs=50) }, conversion_factors={bel: 0.58}, ) diff --git a/tests/test_scripts/test_solph/test_storage_investment/test_storage_with_tuple_label.py b/tests/test_scripts/test_solph/test_storage_investment/test_storage_with_tuple_label.py index 7dea33903..3027f02fe 100644 --- a/tests/test_scripts/test_solph/test_storage_investment/test_storage_with_tuple_label.py +++ b/tests/test_scripts/test_solph/test_storage_investment/test_storage_with_tuple_label.py @@ -90,7 +90,9 @@ def test_tuples_as_labels_example( solph.components.Sink( label=Label("sink", "electricity", "demand"), inputs={ - bel: solph.flows.Flow(fix=data["demand_el"], nominal_value=1) + bel: solph.flows.Flow( + fix=data["demand_el"], nominal_capacity=1 + ) }, ) ) @@ -101,7 +103,8 @@ def test_tuples_as_labels_example( label=Label("source", "natural_gas", "commodity"), outputs={ bgas: solph.flows.Flow( - nominal_value=194397000 * 400 / 8760, full_load_time_max=1 + nominal_capacity=194397000 * 400 / 8760, + full_load_time_max=1, ) }, ) @@ -111,7 +114,9 @@ def test_tuples_as_labels_example( solph.components.Source( label=Label("renewable", "electricity", "wind"), outputs={ - bel: solph.flows.Flow(fix=data["wind"], nominal_value=1000000) + bel: solph.flows.Flow( + fix=data["wind"], nominal_capacity=1000000 + ) }, ) ) @@ -122,7 +127,7 @@ def test_tuples_as_labels_example( outputs={ bel: solph.flows.Flow( fix=data["pv"], - nominal_value=582000, + nominal_capacity=582000, ) }, ) @@ -134,7 +139,9 @@ def test_tuples_as_labels_example( label=Label("pp", "electricity", "natural_gas"), inputs={bgas: solph.flows.Flow()}, outputs={ - bel: solph.flows.Flow(nominal_value=10e10, variable_costs=50) + bel: solph.flows.Flow( + nominal_capacity=10e10, variable_costs=50 + ) }, conversion_factors={bel: 0.58}, ) diff --git a/tests/test_scripts/test_solph/test_variable_chp/test_variable_chp.py b/tests/test_scripts/test_solph/test_variable_chp/test_variable_chp.py index 4b790bd22..0f9f4c514 100644 --- a/tests/test_scripts/test_solph/test_variable_chp/test_variable_chp.py +++ b/tests/test_scripts/test_solph/test_variable_chp/test_variable_chp.py @@ -87,7 +87,9 @@ def test_variable_chp(filename="variable_chp.csv", solver="cbc"): solph.components.Sink( label=("demand", "elec1"), inputs={ - bel: solph.flows.Flow(fix=data["demand_el"], nominal_value=1) + bel: solph.flows.Flow( + fix=data["demand_el"], nominal_capacity=1 + ) }, ) ) @@ -95,7 +97,9 @@ def test_variable_chp(filename="variable_chp.csv", solver="cbc"): solph.components.Sink( label=("demand", "elec2"), inputs={ - bel2: solph.flows.Flow(fix=data["demand_el"], nominal_value=1) + bel2: solph.flows.Flow( + fix=data["demand_el"], nominal_capacity=1 + ) }, ) ) @@ -106,7 +110,7 @@ def test_variable_chp(filename="variable_chp.csv", solver="cbc"): label=("demand", "therm1"), inputs={ bth: solph.flows.Flow( - fix=data["demand_th"], nominal_value=741000 + fix=data["demand_th"], nominal_capacity=741000 ) }, ) @@ -116,7 +120,7 @@ def test_variable_chp(filename="variable_chp.csv", solver="cbc"): label=("demand", "therm2"), inputs={ bth2: solph.flows.Flow( - fix=data["demand_th"], nominal_value=741000 + fix=data["demand_th"], nominal_capacity=741000 ) }, ) @@ -126,7 +130,7 @@ def test_variable_chp(filename="variable_chp.csv", solver="cbc"): energysystem.add( solph.components.Converter( label=("fixed_chp", "gas"), - inputs={bgas: solph.flows.Flow(nominal_value=10e10)}, + inputs={bgas: solph.flows.Flow(nominal_capacity=10e10)}, outputs={bel2: solph.flows.Flow(), bth2: solph.flows.Flow()}, conversion_factors={bel2: 0.3, bth2: 0.5}, ) @@ -136,7 +140,7 @@ def test_variable_chp(filename="variable_chp.csv", solver="cbc"): energysystem.add( solph.components.ExtractionTurbineCHP( label=("variable_chp", "gas"), - inputs={bgas: solph.flows.Flow(nominal_value=10e10)}, + inputs={bgas: solph.flows.Flow(nominal_capacity=10e10)}, outputs={bel: solph.flows.Flow(), bth: solph.flows.Flow()}, conversion_factors={bel: 0.3, bth: 0.5}, conversion_factor_full_condensation={bel: 0.5}, diff --git a/tests/test_solph_network_classes.py b/tests/test_solph_network_classes.py index 5edad18c9..5d3a11a50 100644 --- a/tests/test_solph_network_classes.py +++ b/tests/test_solph_network_classes.py @@ -160,7 +160,7 @@ def test_attributes_needing_nominal_value_get_it(): def test_min_max_values_for_bidirectional_flow(): a = solph.flows.Flow(bidirectional=True) # use default values b = solph.flows.Flow( - bidirectional=True, nominal_value=1, min=-0.8, max=0.9 + bidirectional=True, nominal_capacity=1, min=-0.8, max=0.9 ) assert a.bidirectional assert a.max[0] == 1 diff --git a/tests/test_warnings.py b/tests/test_warnings.py index 6b66f961d..ef81c732e 100644 --- a/tests/test_warnings.py +++ b/tests/test_warnings.py @@ -94,7 +94,7 @@ def test_nonconvex_investment_without_maximum_raises_warning(warning_fixture): variable_costs=25, min=0.2, max=0.8, - nominal_value=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=500, # no maximum is provided here ), nonconvex=solph.NonConvex(), @@ -140,8 +140,8 @@ def test_link_raise_key_error_in_Linkblock(warning_fixture): link = solph.components.Link( label="transshipment_link", inputs={ - bel0: solph.flows.Flow(nominal_value=4), - bel1: solph.flows.Flow(nominal_value=2), + bel0: solph.flows.Flow(nominal_capacity=4), + bel1: solph.flows.Flow(nominal_capacity=2), }, outputs={bel0: solph.flows.Flow(), look_out: solph.flows.Flow()}, conversion_factors={(bel0, bel1): 0.8, (bel1, bel0): 0.7}, From df65fe40da4e175f8ef05103cd1993946a66b4c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Thu, 22 Aug 2024 14:17:05 +0200 Subject: [PATCH 10/45] Rename attribute Flow.nominal_value to nominal_capacity --- .../simple_time_step_example.py | 2 +- src/oemof/solph/_models.py | 8 +- .../solph/components/_generic_storage.py | 2 +- .../solph/components/_offset_converter.py | 2 +- .../_piecewise_linear_converter.py | 6 +- src/oemof/solph/constraints/storage_level.py | 6 +- src/oemof/solph/flows/_flow.py | 14 +-- .../flows/_invest_non_convex_flow_block.py | 2 +- .../solph/flows/_non_convex_flow_block.py | 4 +- src/oemof/solph/flows/_simple_flow_block.py | 20 ++--- .../flows/experimental/_electrical_line.py | 2 +- .../test_components/test_offset_converter.py | 86 ++++++++++--------- tests/test_flows/test_flow_class.py | 2 +- tests/test_flows/test_simple_flow.py | 8 +- tests/test_processing.py | 4 +- 15 files changed, 87 insertions(+), 81 deletions(-) diff --git a/examples/time_index_example/simple_time_step_example.py b/examples/time_index_example/simple_time_step_example.py index 9d6cb5b07..868f8f93d 100644 --- a/examples/time_index_example/simple_time_step_example.py +++ b/examples/time_index_example/simple_time_step_example.py @@ -13,7 +13,7 @@ * The initial_storage_level of a GenericStorage is given at the first time step. If the storage is balanced, this is the same storage level as in the last time step. -* The nominal_value in Flows has to be interpreted in means of power: We have +* The nominal_capacity in Flows has to be interpreted in means of power: We have nominal_capacity=0.5, but the maximum change of the storage content of an ideal storage is 0.125. diff --git a/src/oemof/solph/_models.py b/src/oemof/solph/_models.py index 5ecbbd474..74e49b69d 100644 --- a/src/oemof/solph/_models.py +++ b/src/oemof/solph/_models.py @@ -314,25 +314,25 @@ def _add_parent_block_variables(self): self.flow = po.Var(self.FLOWS, self.TIMESTEPS, within=po.Reals) for o, i in self.FLOWS: - if self.flows[o, i].nominal_value is not None: + if self.flows[o, i].nominal_capacity is not None: if self.flows[o, i].fix[self.TIMESTEPS.at(1)] is not None: for t in self.TIMESTEPS: self.flow[o, i, t].value = ( self.flows[o, i].fix[t] - * self.flows[o, i].nominal_value + * self.flows[o, i].nominal_capacity ) self.flow[o, i, t].fix() else: for t in self.TIMESTEPS: self.flow[o, i, t].setub( self.flows[o, i].max[t] - * self.flows[o, i].nominal_value + * self.flows[o, i].nominal_capacity ) if not self.flows[o, i].nonconvex: for t in self.TIMESTEPS: self.flow[o, i, t].setlb( self.flows[o, i].min[t] - * self.flows[o, i].nominal_value + * self.flows[o, i].nominal_capacity ) elif (o, i) in self.UNIDIRECTIONAL_FLOWS: for t in self.TIMESTEPS: diff --git a/src/oemof/solph/components/_generic_storage.py b/src/oemof/solph/components/_generic_storage.py index f8fd76df7..eff79ae11 100644 --- a/src/oemof/solph/components/_generic_storage.py +++ b/src/oemof/solph/components/_generic_storage.py @@ -104,7 +104,7 @@ class GenericStorage(Node): max_storage_level : numeric (iterable or scalar), :math:`c_{max}(t)` see: min_storage_level investment : :class:`oemof.solph.options.Investment` object - Object indicating if a nominal_value of the flow is determined by + Object indicating if a nominal_capacity of the flow is determined by the optimization problem. Note: This will refer all attributes to an investment variable instead of to the nominal_storage_capacity. The nominal_storage_capacity should not be set (or set to None) if an diff --git a/src/oemof/solph/components/_offset_converter.py b/src/oemof/solph/components/_offset_converter.py index b33442f6c..25c51f95a 100644 --- a/src/oemof/solph/components/_offset_converter.py +++ b/src/oemof/solph/components/_offset_converter.py @@ -46,7 +46,7 @@ class OffsetConverter(Node): normed_offsets : dict, (:math:`y_\text{0,normed}(t)`) Dict containing the respective bus as key and as value the parameter :math:`y_\text{0,normed}(t)`. It represents the y-intercept with respect - to the `NonConvex` flow divided by the `nominal_value` of the + to the `NonConvex` flow divided by the `nominal_capacity` of the `NonConvex` flow (this is for internal purposes). The value can either be a scalar or a sequence with length of time horizon for simulation. Notes diff --git a/src/oemof/solph/components/experimental/_piecewise_linear_converter.py b/src/oemof/solph/components/experimental/_piecewise_linear_converter.py index ef2e5192b..2cd155d42 100644 --- a/src/oemof/solph/components/experimental/_piecewise_linear_converter.py +++ b/src/oemof/solph/components/experimental/_piecewise_linear_converter.py @@ -94,8 +94,10 @@ def __init__( + "more than 1 input and 1 output!" ) - nominal_value = [a.nominal_value for a in self.inputs.values()][0] - if max(self.in_breakpoints) < nominal_value: + nominal_capacity = [a.nominal_capacity for a in self.inputs.values()][ + 0 + ] + if max(self.in_breakpoints) < nominal_capacity: raise ValueError( "Largest in_breakpoint must be larger or equal " + "nominal value" diff --git a/src/oemof/solph/constraints/storage_level.py b/src/oemof/solph/constraints/storage_level.py index 87c892bdf..60d538c10 100644 --- a/src/oemof/solph/constraints/storage_level.py +++ b/src/oemof/solph/constraints/storage_level.py @@ -24,7 +24,7 @@ def storage_level_constraint( As a GenericStorage just allows exactly one input and one output, an additional bus, the multiplexer_bus, is used for the connections. Note that all Flow objects connected to the multiplexer_bus have to have - a nominal_value. + a nominal_capacity. Parameters ---------- @@ -91,7 +91,7 @@ def _output_active_rule(m): def _constraint_output_rule(m, o, t): return ( m.flow[multiplexer_bus, o, t] - / m.flows[multiplexer_bus, o].nominal_value + / m.flows[multiplexer_bus, o].nominal_capacity <= active_output[o, t] ) @@ -156,7 +156,7 @@ def _input_active_rule(m): def _constraint_input_rule(m, i, t): return ( m.flow[i, multiplexer_bus, t] - / m.flows[i, multiplexer_bus].nominal_value + / m.flows[i, multiplexer_bus].nominal_capacity <= 1 - inactive_input[i, t] ) diff --git a/src/oemof/solph/flows/_flow.py b/src/oemof/solph/flows/_flow.py index ded156642..6e2d859d8 100644 --- a/src/oemof/solph/flows/_flow.py +++ b/src/oemof/solph/flows/_flow.py @@ -53,12 +53,12 @@ class Flow(Edge): will be added to the objective expression of the optimization problem. max : numeric (iterable or scalar), :math:`f_{max}` Normed maximum value of the flow. The flow absolute maximum will be - calculated by multiplying :attr:`nominal_value` with :attr:`max` + calculated by multiplying :attr:`nominal_capacity` with :attr:`max` min : numeric (iterable or scalar), :math:`f_{min}` Normed minimum value of the flow (see :attr:`max`). fix : numeric (iterable or scalar), :math:`f_{fix}` Normed fixed value for the flow variable. Will be multiplied with the - :attr:`nominal_value` to get the absolute value. + :attr:`nominal_capacity` to get the absolute value. positive_gradient_limit : numeric (iterable, scalar or None) the normed *upper bound* on the positive difference (`flow[t-1] < flow[t]`) of two consecutive flow values. @@ -206,7 +206,7 @@ def __init__( for attribute, value in custom_attributes.items(): setattr(self, attribute, value) - self.nominal_value = None + self.nominal_capacity = None self.investment = None infinite_error_msg = ( @@ -216,7 +216,7 @@ def __init__( if isinstance(nominal_capacity, numbers.Real): if not math.isfinite(nominal_capacity): raise ValueError(infinite_error_msg.format("nominal_capacity")) - self.nominal_value = nominal_capacity + self.nominal_capacity = nominal_capacity elif isinstance(nominal_capacity, Investment): self.investment = nominal_capacity @@ -260,7 +260,7 @@ def __init__( "max", ] sequences = ["fix", "variable_costs", "min", "max"] - if self.investment is None and self.nominal_value is None: + if self.investment is None and self.nominal_capacity is None: for attr in need_nominal_value: if isinstance(eval(attr), Iterable): the_attr = eval(attr)[0] @@ -287,7 +287,9 @@ def __init__( for attr in sequences: setattr(self, attr, sequence(eval(attr))) - if self.nominal_value is not None and not math.isfinite(self.max[0]): + if self.nominal_capacity is not None and not math.isfinite( + self.max[0] + ): raise ValueError(infinite_error_msg.format("max")) # Checking for impossible gradient combinations diff --git a/src/oemof/solph/flows/_invest_non_convex_flow_block.py b/src/oemof/solph/flows/_invest_non_convex_flow_block.py index 747c17f79..84be801c8 100644 --- a/src/oemof/solph/flows/_invest_non_convex_flow_block.py +++ b/src/oemof/solph/flows/_invest_non_convex_flow_block.py @@ -121,7 +121,7 @@ def _investvar_bound_rule(block, i, o, p): # `status_nominal` is a parameter which represents the # multiplication of a binary variable (`status`) - # and a continuous variable (`invest` or `nominal_value`) + # and a continuous variable (`invest` or `nominal_capacity`) self.status_nominal = Var( self.INVEST_NON_CONVEX_FLOWS, m.TIMESTEPS, within=NonNegativeReals ) diff --git a/src/oemof/solph/flows/_non_convex_flow_block.py b/src/oemof/solph/flows/_non_convex_flow_block.py index 88eb76850..3d2c1d9f2 100644 --- a/src/oemof/solph/flows/_non_convex_flow_block.py +++ b/src/oemof/solph/flows/_non_convex_flow_block.py @@ -97,7 +97,7 @@ def _create_variables(self): # `status_nominal` is a parameter which represents the # multiplication of a binary variable (`status`) - # and a continuous variable (`invest` or `nominal_value`) + # and a continuous variable (`invest` or `nominal_capacity`) self.status_nominal = Var( self.NONCONVEX_FLOWS, m.TIMESTEPS, within=NonNegativeReals ) @@ -648,7 +648,7 @@ def _status_nominal_rule(_, i, o, t): """Rule definition for status_nominal""" expr = ( self.status_nominal[i, o, t] - == self.status[i, o, t] * m.flows[i, o].nominal_value + == self.status[i, o, t] * m.flows[i, o].nominal_capacity ) return expr diff --git a/src/oemof/solph/flows/_simple_flow_block.py b/src/oemof/solph/flows/_simple_flow_block.py index dc893592b..ad3b1caa1 100644 --- a/src/oemof/solph/flows/_simple_flow_block.py +++ b/src/oemof/solph/flows/_simple_flow_block.py @@ -75,7 +75,7 @@ def _create_sets(self, group): (g[0], g[1]) for g in group if g[2].full_load_time_max is not None - and g[2].nominal_value is not None + and g[2].nominal_capacity is not None ] ) @@ -84,7 +84,7 @@ def _create_sets(self, group): (g[0], g[1]) for g in group if g[2].full_load_time_min is not None - and g[2].nominal_value is not None + and g[2].nominal_capacity is not None ] ) @@ -175,12 +175,12 @@ def _create_variables(self, group): if m.flows[i, o].positive_gradient_limit[0] is not None: for t in m.TIMESTEPS: self.positive_gradient[i, o, t].setub( - f.positive_gradient_limit[t] * f.nominal_value + f.positive_gradient_limit[t] * f.nominal_capacity ) if m.flows[i, o].negative_gradient_limit[0] is not None: for t in m.TIMESTEPS: self.negative_gradient[i, o, t].setub( - f.negative_gradient_limit[t] * f.nominal_value + f.negative_gradient_limit[t] * f.nominal_capacity ) def _create_constraints(self): @@ -220,7 +220,7 @@ def _flow_full_load_time_max_rule(model): ) rhs = ( m.flows[inp, out].full_load_time_max - * m.flows[inp, out].nominal_value + * m.flows[inp, out].nominal_capacity ) self.full_load_time_max_constr.add((inp, out), lhs <= rhs) @@ -240,7 +240,7 @@ def _flow_full_load_time_min_rule(_): ) rhs = ( m.flows[inp, out].full_load_time_min - * m.flows[inp, out].nominal_value + * m.flows[inp, out].nominal_capacity ) self.full_load_time_min_constr.add((inp, out), lhs >= rhs) @@ -451,12 +451,12 @@ def _objective_expression(self): # Fixed costs for units with no lifetime limit if ( m.flows[i, o].fixed_costs[0] is not None - and m.flows[i, o].nominal_value is not None + and m.flows[i, o].nominal_capacity is not None and (i, o) not in self.LIFETIME_FLOWS and (i, o) not in self.LIFETIME_AGE_FLOWS ): fixed_costs += sum( - m.flows[i, o].nominal_value + m.flows[i, o].nominal_capacity * m.flows[i, o].fixed_costs[pp] * ((1 + m.discount_rate) ** (-pp)) for pp in range(m.es.end_year_of_optimization) @@ -470,7 +470,7 @@ def _objective_expression(self): m.flows[i, o].lifetime, ) fixed_costs += sum( - m.flows[i, o].nominal_value + m.flows[i, o].nominal_capacity * m.flows[i, o].fixed_costs[pp] * ((1 + m.discount_rate) ** (-pp)) for pp in range(range_limit) @@ -483,7 +483,7 @@ def _objective_expression(self): m.flows[i, o].lifetime - m.flows[i, o].age, ) fixed_costs += sum( - m.flows[i, o].nominal_value + m.flows[i, o].nominal_capacity * m.flows[i, o].fixed_costs[pp] * ((1 + m.discount_rate) ** (-pp)) for pp in range(range_limit) diff --git a/src/oemof/solph/flows/experimental/_electrical_line.py b/src/oemof/solph/flows/experimental/_electrical_line.py index 8edd417f0..a3dd0e4cd 100644 --- a/src/oemof/solph/flows/experimental/_electrical_line.py +++ b/src/oemof/solph/flows/experimental/_electrical_line.py @@ -60,7 +60,7 @@ class ElectricalLine(Flow): def __init__(self, **kwargs): super().__init__( - nominal_capacity=kwargs.get("nominal_value"), + nominal_capacity=kwargs.get("nominal_capacity"), variable_costs=kwargs.get("variable_costs", 0), min=kwargs.get("min"), max=kwargs.get("max"), diff --git a/tests/test_components/test_offset_converter.py b/tests/test_components/test_offset_converter.py index c094cb5d0..82bdd9b59 100644 --- a/tests/test_components/test_offset_converter.py +++ b/tests/test_components/test_offset_converter.py @@ -53,7 +53,7 @@ def solve_and_extract_results(es): def check_results( results, reference_bus, - nominal_value, + nominal_capacity, minimal_value, eta_at_nom, eta_at_min, @@ -62,7 +62,7 @@ def check_results( if "input" in reference_bus.label: slope, offset = slope_offset_from_nonconvex_input( 1, - minimal_value / nominal_value, + minimal_value / nominal_capacity, eta_at_nom[bus], eta_at_min[bus], ) @@ -75,7 +75,7 @@ def check_results( else: slope, offset = slope_offset_from_nonconvex_output( 1, - minimal_value / nominal_value, + minimal_value / nominal_capacity, eta_at_nom[bus], eta_at_min[bus], ) @@ -87,7 +87,7 @@ def check_results( ]["sequences"]["status"] flow_expected = ( - offset * nominal_value * reference_flow_status + offset * nominal_capacity * reference_flow_status + slope * reference_flow ) if "input" in bus.label: @@ -103,7 +103,7 @@ def check_results( def add_OffsetConverter( - es, reference_bus, nominal_value, minimal_value, eta_at_nom, eta_at_min + es, reference_bus, nominal_capacity, minimal_value, eta_at_nom, eta_at_min ): # Use of experimental API to access nodes by label. # Can be removed with future release of network. @@ -123,19 +123,21 @@ def add_OffsetConverter( if reference_bus in oc_outputs: f = oc_outputs[reference_bus] get_slope_and_offset = slope_offset_from_nonconvex_output - fix = [0] + np.linspace(minimal_value, nominal_value, 9).tolist() + fix = [0] + np.linspace( + minimal_value, nominal_capacity, 9 + ).tolist() else: f = oc_inputs[reference_bus] get_slope_and_offset = slope_offset_from_nonconvex_input fix = [0] + np.linspace( minimal_value * eta_at_min[es.node["bus output 0"]], - nominal_value * eta_at_nom[es.node["bus output 0"]], + nominal_capacity * eta_at_nom[es.node["bus output 0"]], 9, ).tolist() fix_flow = es.flows()[es.node["bus output 0"], es.node["sink 0"]] fix_flow.fix = fix - fix_flow.nominal_value = 1 + fix_flow.nominal_capacity = 1 slopes = {} offsets = {} @@ -145,7 +147,7 @@ def add_OffsetConverter( continue slope, offset = get_slope_and_offset( 1, - minimal_value / nominal_value, + minimal_value / nominal_capacity, eta_at_nom[bus], eta_at_min[bus], ) @@ -153,8 +155,8 @@ def add_OffsetConverter( offsets[bus] = offset f.nonconvex = solph.NonConvex() - f.nominal_value = nominal_value - f.min = sequence(minimal_value / nominal_value) + f.nominal_capacity = nominal_capacity + f.min = sequence(minimal_value / nominal_capacity) oc = solph.components.OffsetConverter( label="offset converter", @@ -243,7 +245,7 @@ def test_OffsetConverter_single_input_output_ref_output(): num_out = 1 es = create_energysystem_stub(num_in, num_out) - nominal_value = 10 + nominal_capacity = 10 minimal_value = 3 eta_at_nom = {es.groups["bus input 0"]: 0.7} @@ -252,7 +254,7 @@ def test_OffsetConverter_single_input_output_ref_output(): add_OffsetConverter( es, es.groups["bus output 0"], - nominal_value, + nominal_capacity, minimal_value, eta_at_nom, eta_at_min, @@ -263,7 +265,7 @@ def test_OffsetConverter_single_input_output_ref_output(): check_results( results, es.groups["bus output 0"], - nominal_value, + nominal_capacity, minimal_value, eta_at_nom, eta_at_min, @@ -275,7 +277,7 @@ def test_OffsetConverter_single_input_output_ref_output_eta_decreasing(): num_out = 1 es = create_energysystem_stub(num_in, num_out) - nominal_value = 10 + nominal_capacity = 10 minimal_value = 3 eta_at_nom = {es.groups["bus input 0"]: 0.5} @@ -284,7 +286,7 @@ def test_OffsetConverter_single_input_output_ref_output_eta_decreasing(): add_OffsetConverter( es, es.groups["bus output 0"], - nominal_value, + nominal_capacity, minimal_value, eta_at_nom, eta_at_min, @@ -295,7 +297,7 @@ def test_OffsetConverter_single_input_output_ref_output_eta_decreasing(): check_results( results, es.groups["bus output 0"], - nominal_value, + nominal_capacity, minimal_value, eta_at_nom, eta_at_min, @@ -307,7 +309,7 @@ def test_OffsetConverter_single_input_output_ref_input(): num_out = 1 es = create_energysystem_stub(num_in, num_out) - nominal_value = 10 + nominal_capacity = 10 minimal_value = 3 eta_at_nom = {es.groups["bus output 0"]: 0.7} @@ -316,7 +318,7 @@ def test_OffsetConverter_single_input_output_ref_input(): add_OffsetConverter( es, es.groups["bus input 0"], - nominal_value, + nominal_capacity, minimal_value, eta_at_nom, eta_at_min, @@ -327,7 +329,7 @@ def test_OffsetConverter_single_input_output_ref_input(): check_results( results, es.groups["bus input 0"], - nominal_value, + nominal_capacity, minimal_value, eta_at_nom, eta_at_min, @@ -339,7 +341,7 @@ def test_OffsetConverter_single_input_output_ref_input_eta_decreasing(): num_out = 1 es = create_energysystem_stub(num_in, num_out) - nominal_value = 10 + nominal_capacity = 10 minimal_value = 3 eta_at_nom = {es.groups["bus output 0"]: 0.5} @@ -348,7 +350,7 @@ def test_OffsetConverter_single_input_output_ref_input_eta_decreasing(): add_OffsetConverter( es, es.groups["bus input 0"], - nominal_value, + nominal_capacity, minimal_value, eta_at_nom, eta_at_min, @@ -359,7 +361,7 @@ def test_OffsetConverter_single_input_output_ref_input_eta_decreasing(): check_results( results, es.groups["bus input 0"], - nominal_value, + nominal_capacity, minimal_value, eta_at_nom, eta_at_min, @@ -371,7 +373,7 @@ def test_OffsetConverter_double_input_output_ref_input(): num_out = 2 es = create_energysystem_stub(num_in, num_out) - nominal_value = 10 + nominal_capacity = 10 minimal_value = 3 eta_at_nom = { @@ -388,7 +390,7 @@ def test_OffsetConverter_double_input_output_ref_input(): add_OffsetConverter( es, es.groups["bus input 0"], - nominal_value, + nominal_capacity, minimal_value, eta_at_nom, eta_at_min, @@ -399,7 +401,7 @@ def test_OffsetConverter_double_input_output_ref_input(): check_results( results, es.groups["bus input 0"], - nominal_value, + nominal_capacity, minimal_value, eta_at_nom, eta_at_min, @@ -412,7 +414,7 @@ def test_OffsetConverter_double_input_output_ref_output(): es = create_energysystem_stub(num_in, num_out) - nominal_value = 10 + nominal_capacity = 10 minimal_value = 3 eta_at_nom = { @@ -429,7 +431,7 @@ def test_OffsetConverter_double_input_output_ref_output(): add_OffsetConverter( es, es.groups["bus output 0"], - nominal_value, + nominal_capacity, minimal_value, eta_at_nom, eta_at_min, @@ -440,7 +442,7 @@ def test_OffsetConverter_double_input_output_ref_output(): check_results( results, es.groups["bus output 0"], - nominal_value, + nominal_capacity, minimal_value, eta_at_nom, eta_at_min, @@ -453,7 +455,7 @@ def test_two_OffsetConverters_with_and_without_investment(): es = create_energysystem_stub(num_in, num_out) - nominal_value = 10 + nominal_capacity = 10 minimal_value = 3 eta_at_nom = { @@ -466,7 +468,7 @@ def test_two_OffsetConverters_with_and_without_investment(): add_OffsetConverter( es, es.groups["bus output 0"], - nominal_value, + nominal_capacity, minimal_value, eta_at_nom, eta_at_min, @@ -481,7 +483,7 @@ def test_two_OffsetConverters_with_and_without_investment(): es.groups["bus output 0"]: solph.Flow( nonconvex=solph.NonConvex(), nominal_capacity=solph.Investment( - maximum=nominal_value, ep_costs=10 + maximum=nominal_capacity, ep_costs=10 ), ) }, @@ -507,25 +509,25 @@ def test_OffsetConverter_05x_compatibility(): num_out = 1 es = create_energysystem_stub(num_in, num_out) - nominal_value = 10 + nominal_capacity = 10 minimal_value = 3 # Use of experimental API to access nodes by label. # Can be removed with future release of network. with warnings.catch_warnings(): warnings.simplefilter("ignore", category=ExperimentalFeatureWarning) - fix = [0] + np.linspace(minimal_value, nominal_value, 9).tolist() + fix = [0] + np.linspace(minimal_value, nominal_capacity, 9).tolist() fix_flow = es.flows()[es.node["bus output 0"], es.node["sink 0"]] fix_flow.fix = fix - fix_flow.nominal_value = 1 + fix_flow.nominal_capacity = 1 eta_at_nom = 0.7 eta_at_min = 0.5 - slope = (nominal_value - minimal_value) / ( - nominal_value / eta_at_nom - minimal_value / eta_at_min + slope = (nominal_capacity - minimal_value) / ( + nominal_capacity / eta_at_nom - minimal_value / eta_at_min ) - offset = minimal_value / nominal_value * (1 - slope / eta_at_min) + offset = minimal_value / nominal_capacity * (1 - slope / eta_at_min) warnings.filterwarnings("ignore", "", DeprecationWarning) oc = solph.components.OffsetConverter( @@ -534,8 +536,8 @@ def test_OffsetConverter_05x_compatibility(): outputs={ es.groups["bus output 0"]: solph.Flow( nonconvex=solph.NonConvex(), - nominal_capacity=nominal_value, - min=minimal_value / nominal_value, + nominal_capacity=nominal_capacity, + min=minimal_value / nominal_capacity, ) }, coefficients=(offset, slope), @@ -547,7 +549,7 @@ def test_OffsetConverter_05x_compatibility(): results = solve_and_extract_results(es) slope, offset = slope_offset_from_nonconvex_output( - 1, minimal_value / nominal_value, 0.7, 0.5 + 1, minimal_value / nominal_capacity, 0.7, 0.5 ) output_flow = results["offset converter", "bus output 0"]["sequences"][ "flow" @@ -557,7 +559,7 @@ def test_OffsetConverter_05x_compatibility(): ]["status"] input_flow_expected = ( - offset * nominal_value * output_flow_status + slope * output_flow + offset * nominal_capacity * output_flow_status + slope * output_flow ) input_flow_actual = results["bus input 0", "offset converter"][ "sequences" diff --git a/tests/test_flows/test_flow_class.py b/tests/test_flows/test_flow_class.py index cf5805d23..5c3955c83 100644 --- a/tests/test_flows/test_flow_class.py +++ b/tests/test_flows/test_flow_class.py @@ -82,6 +82,6 @@ def test_fix_sequence(): def test_fix_sequence_non_nominal(): - """Attribute fix needs nominal_value""" + """Attribute fix needs nominal_capacity""" with pytest.raises(AttributeError): Flow(fix=[0.3, 0.2, 0.7]) diff --git a/tests/test_flows/test_simple_flow.py b/tests/test_flows/test_simple_flow.py index 4bc0c4b36..e5c74ed91 100644 --- a/tests/test_flows/test_simple_flow.py +++ b/tests/test_flows/test_simple_flow.py @@ -58,13 +58,13 @@ def test_full_load_time_min(): # --- BEGIN: The following code can be removed for versions >= v0.7 --- -def test_nominal_value_warning(): - with pytest.warns(FutureWarning, match="nominal_value"): +def test_nominal_capacity_warning(): + with pytest.warns(FutureWarning, match="nominal_capacity"): _ = solph.flows.Flow(nominal_value=2) -def test_nominal_value_error(): - with pytest.raises(AttributeError, match="nominal_value"): +def test_nominal_capacity_error(): + with pytest.raises(AttributeError, match="nominal_capacity"): _ = solph.flows.Flow(nominal_value=2, nominal_capacity=1) diff --git a/tests/test_processing.py b/tests/test_processing.py index e563fece7..0299ff627 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -99,7 +99,7 @@ def test_flows_with_none_exclusion(self): { "bidirectional": False, "integer": False, - "nominal_value": 1, + "nominal_capacity": 1, "max": 1, "min": 0, "variable_costs": 0, @@ -124,7 +124,7 @@ def test_flows_without_none_exclusion(self): "lifetime": None, "integer": False, "investment": None, - "nominal_value": 1, + "nominal_capacity": 1, "nonconvex": None, "bidirectional": False, "full_load_time_max": None, From a2bca51479e7557e90f21d800aed26ad4ac1de80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Thu, 22 Aug 2024 14:33:03 +0200 Subject: [PATCH 11/45] Rename "value" to "capacity" in docstrings --- examples/electrical/transshipment.py | 2 +- examples/excel_reader/dispatch.py | 2 +- src/oemof/solph/components/_offset_converter.py | 4 ++-- src/oemof/solph/flows/_investment_flow_block.py | 4 ++-- tests/test_components.py | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/electrical/transshipment.py b/examples/electrical/transshipment.py index 3a1e181e4..513aabfed 100644 --- a/examples/electrical/transshipment.py +++ b/examples/electrical/transshipment.py @@ -77,7 +77,7 @@ def draw_graph( grph : networkxGraph A graph to draw. edge_labels : boolean - Use nominal values of flow as edge label + Use nominal capacities of flow as edge label node_color : dict or string Hex color code oder matplotlib color for each node. If string, all colors are the same. diff --git a/examples/excel_reader/dispatch.py b/examples/excel_reader/dispatch.py index efb534f24..8927d6e9b 100644 --- a/examples/excel_reader/dispatch.py +++ b/examples/excel_reader/dispatch.py @@ -301,7 +301,7 @@ def draw_graph( grph : networkxGraph A graph to draw. edge_labels : boolean - Use nominal values of flow as edge label + Use nominal capacities of flow as edge label node_color : dict or string Hex color code oder matplotlib color for each node. If string, all colors are the same. diff --git a/src/oemof/solph/components/_offset_converter.py b/src/oemof/solph/components/_offset_converter.py index 25c51f95a..74778673d 100644 --- a/src/oemof/solph/components/_offset_converter.py +++ b/src/oemof/solph/components/_offset_converter.py @@ -579,7 +579,7 @@ def slope_offset_from_nonconvex_input( With the input load being at 100 %, in this example, the efficiency should be 30 %. With the input load being at 50 %, it should be 40 %. We can - calcualte slope and the offset which is normed to the nominal value of + calcualte slope and the offset which is normed to the nominal capacity of the referenced flow (in this case the input flow) always. >>> slope, offset = solph.components.slope_offset_from_nonconvex_input( @@ -656,7 +656,7 @@ def slope_offset_from_nonconvex_output( With the output load being at 100 %, in this example, the efficiency should be 80 %. With the input load being at 50 %, it should be 70 %. We can - calcualte slope and the offset, which is normed to the nominal value of + calcualte slope and the offset, which is normed to the nominal capacity of the referenced flow (in this case the output flow) always. >>> slope, offset = solph.components.slope_offset_from_nonconvex_output( diff --git a/src/oemof/solph/flows/_investment_flow_block.py b/src/oemof/solph/flows/_investment_flow_block.py index e77ec40b5..bad8c6b8d 100644 --- a/src/oemof/solph/flows/_investment_flow_block.py +++ b/src/oemof/solph/flows/_investment_flow_block.py @@ -185,12 +185,12 @@ def _create_variables(self, _): Value of the investment variable in period p, equal to what is being invested and equivalent resp. similar to - the nominal value of the flows after optimization. + the nominal capacity of the flows after optimization. * :math:`P_{total}(p)` Total installed capacity / energy in period p, - equivalent to the nominal value of the flows after optimization. + equivalent to the nominal capacity of the flows after optimization. * :math:`P_{old}(p)` diff --git a/tests/test_components.py b/tests/test_components.py index 02a7b1d1e..8618af937 100644 --- a/tests/test_components.py +++ b/tests/test_components.py @@ -43,7 +43,7 @@ def test_generic_storage_1(): def test_generic_storage_2(): - """Nominal value defined with investment model.""" + """Nominal capacity defined with investment model.""" bel = Bus() with pytest.raises( AttributeError, @@ -65,7 +65,7 @@ def test_generic_storage_2(): def test_generic_storage_3(): - """Nominal value defined with investment model.""" + """Nominal capacity defined with investment model.""" bel = Bus() components.GenericStorage( label="storage4", From 6973a31e3c0990197e212cecf94af74a83de988b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Thu, 22 Aug 2024 14:43:29 +0200 Subject: [PATCH 12/45] Rename nominal_storage_capacity to nominal_capacity --- .../solph/components/_generic_storage.py | 27 +++++++++--- tests/test_components/test_storage.py | 41 +++++++++++++++---- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/src/oemof/solph/components/_generic_storage.py b/src/oemof/solph/components/_generic_storage.py index eff79ae11..e0e178403 100644 --- a/src/oemof/solph/components/_generic_storage.py +++ b/src/oemof/solph/components/_generic_storage.py @@ -167,7 +167,8 @@ def __init__( label=None, inputs=None, outputs=None, - nominal_storage_capacity=None, + nominal_capacity=None, + nominal_storage_capacity=None, # Can be removed for versions >= v0.7 initial_storage_level=None, investment=None, invest_relation_input_output=None, @@ -207,20 +208,34 @@ def __init__( + " nominal_storage_capacity." + " Both options cannot be set at the same time." ) - if nominal_storage_capacity is not None: + if nominal_capacity is not None: raise AttributeError(msg) else: warn(msg, FutureWarning) nominal_storage_capacity = investment # --- END --- + # --- BEGIN: The following code can be removed for versions >= v0.6 --- + if nominal_storage_capacity is not None: + msg = ( + "For backward compatibility," + " the option nominal_storage_capacity overwrites the option" + + " nominal_capacity." + + " Both options cannot be set at the same time." + ) + if nominal_capacity is not None: + raise AttributeError(msg) + else: + warn(msg, FutureWarning) + nominal_capacity = nominal_storage_capacity + # --- END --- self.nominal_storage_capacity = None self.investment = None self._invest_group = False - if isinstance(nominal_storage_capacity, numbers.Real): - self.nominal_storage_capacity = nominal_storage_capacity - elif isinstance(nominal_storage_capacity, Investment): - self.investment = nominal_storage_capacity + if isinstance(nominal_capacity, numbers.Real): + self.nominal_storage_capacity = nominal_capacity + elif isinstance(nominal_capacity, Investment): + self.investment = nominal_capacity self._invest_group = True self.initial_storage_level = initial_storage_level diff --git a/tests/test_components/test_storage.py b/tests/test_components/test_storage.py index 16faf3fae..ec6494456 100644 --- a/tests/test_components/test_storage.py +++ b/tests/test_components/test_storage.py @@ -28,7 +28,7 @@ def test_relative_losses(): "storage", inputs={bus: solph.Flow(variable_costs=1)}, outputs={bus: solph.Flow(variable_costs=1)}, - nominal_storage_capacity=10, + nominal_capacity=10, initial_storage_level=1, loss_rate=0.004125876075, # half life of one week ) @@ -76,7 +76,7 @@ def test_invest_power_uncoupled(): nominal_capacity=solph.Investment(ep_costs=0.1), ) }, - nominal_storage_capacity=10, + nominal_capacity=10, initial_storage_level=0, balanced=False, ) @@ -122,7 +122,7 @@ def test_invest_power_coupled(): nominal_capacity=solph.Investment(ep_costs=0.1), ) }, - nominal_storage_capacity=10, + nominal_capacity=10, invest_relation_input_output=0.5, initial_storage_level=0, balanced=False, @@ -159,7 +159,7 @@ def test_storage_charging(): "storage", inputs={bus: solph.Flow(nominal_capacity=2, variable_costs=-2)}, outputs={bus: solph.Flow(nominal_capacity=0.1)}, - nominal_storage_capacity=19, + nominal_capacity=19, initial_storage_level=0, balanced=False, ) @@ -194,7 +194,7 @@ def test_invest_content_uncoupled(): "storage", inputs={bus: solph.Flow(nominal_capacity=2, variable_costs=-2)}, outputs={bus: solph.Flow(nominal_capacity=0.1)}, - nominal_storage_capacity=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=0.1, ), initial_storage_level=0, @@ -234,7 +234,7 @@ def test_invest_content_minimum(): "storage", inputs={bus: solph.Flow(nominal_capacity=2, variable_costs=-2)}, outputs={bus: solph.Flow(nominal_capacity=0.1, variable_costs=0.1)}, - nominal_storage_capacity=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=0.1, minimum=32, ), @@ -275,7 +275,7 @@ def test_invest_content_minimum_nonconvex(): "storage", inputs={bus: solph.Flow(nominal_capacity=2, variable_costs=0.1)}, outputs={bus: solph.Flow(nominal_capacity=0.1, variable_costs=0.1)}, - nominal_storage_capacity=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=0.1, minimum=32, nonconvex=solph.NonConvex(), @@ -321,7 +321,7 @@ def test_invest_content_maximum(): ) }, outputs={bus: solph.Flow(nominal_capacity=0.1, variable_costs=0.1)}, - nominal_storage_capacity=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=0.1, maximum=10, ), @@ -344,3 +344,28 @@ def test_invest_content_maximum(): assert storage_content == pytest.approx( [min(i * 1.9, 10) for i in range(0, 11)] ) + + +# --- BEGIN: The following code can be removed for versions >= v0.6 --- +def test_capacity_keyword_wrapper_warning(): + with pytest.warns(FutureWarning, match="nominal_storage_capacity"): + bus = solph.Bus() + _ = solph.components.GenericStorage( + nominal_storage_capacity=5, + inputs={bus: solph.Flow()}, + outputs={bus: solph.Flow()}, + ) + + +def test_capacity_keyword_wrapper_error(): + with pytest.raises(AttributeError, match="nominal_storage_capacity"): + bus = solph.Bus() + _ = solph.components.GenericStorage( + nominal_storage_capacity=5, + nominal_capacity=5, + inputs={bus: solph.Flow()}, + outputs={bus: solph.Flow()}, + ) + + +# --- END --- From 96ab6f847f12068560fa22b6ca9a8448e8176d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Thu, 22 Aug 2024 14:49:19 +0200 Subject: [PATCH 13/45] Rename GenericStorage keyword to nominal_capacity This way, it is analogue to teh keyword of the Flow. --- docs/usage.rst | 4 ++-- examples/basic_example/basic_example.py | 2 +- .../dual_variable_example.py | 2 +- examples/excel_reader/dispatch.py | 2 +- .../flexible_modelling/saturating_storage.py | 2 +- examples/gradient_example/gradient_example.py | 2 +- .../diesel_genset_nonconvex_investment.py | 2 +- .../offset_diesel_genset_nonconvex_investment.py | 2 +- examples/storage_balanced_unbalanced/storage.py | 2 +- examples/storage_costs/storage_costs.py | 4 ++-- .../v1_invest_optimize_all_technologies.py | 2 +- .../v2_invest_optimize_only_gas_and_storage.py | 2 +- ...st_optimize_only_storage_with_fossil_share.py | 2 +- ...ptimize_all_technologies_with_fossil_share.py | 2 +- .../storage_level_constraint.py | 2 +- .../non_equidistant_time_step_example.py | 4 ++-- .../simple_time_step_example.py | 2 +- examples/tuple_as_labels/tuple_as_label.py | 2 +- src/oemof/solph/components/_generic_storage.py | 4 ++-- src/oemof/solph/constraints/shared_limit.py | 4 ++-- tests/test_components.py | 16 +++++++--------- tests/test_constraints/test_storage_level.py | 2 +- tests/test_non_equidistant_time_index.py | 2 +- tests/test_processing.py | 2 +- .../test_connect_invest/test_connect_invest.py | 2 +- .../test_multi_period_dispatch_model.py | 2 +- .../test_multi_period_investment_model.py | 2 +- .../test_invest_storage_regression.py | 2 +- .../test_storage_investment.py | 2 +- .../test_storage_with_tuple_label.py | 2 +- 30 files changed, 41 insertions(+), 43 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index a51fb2d66..9dbbbec2c 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -568,7 +568,7 @@ Furthermore, an efficiency for loading, unloading and a loss rate can be defined label='storage', inputs={b_el: solph.flows.Flow(nominal_capacity=9, variable_costs=10)}, outputs={b_el: solph.flows.Flow(nominal_capacity=25, variable_costs=10)}, - loss_rate=0.001, nominal_storage_capacity=50, + loss_rate=0.001, nominal_capacity=50, inflow_conversion_factor=0.98, outflow_conversion_factor=0.8) For initialising the state of charge before the first time step (time step zero) the parameter ``initial_storage_level`` (default value: ``None``) can be set by a numeric value as fraction of the storage capacity. @@ -590,7 +590,7 @@ The following code block shows an example of the storage parametrization for the label='storage', inputs={b_el: solph.flows.Flow(nominal_capacity=9, variable_costs=10)}, outputs={b_el: solph.flows.Flow(nominal_capacity=25, variable_costs=10)}, - loss_rate=0.001, nominal_storage_capacity=50, + loss_rate=0.001, nominal_capacity=50, initial_storage_level=0.5, balanced=True, inflow_conversion_factor=0.98, outflow_conversion_factor=0.8) diff --git a/examples/basic_example/basic_example.py b/examples/basic_example/basic_example.py index cfd8e32f5..2afd5a4e2 100644 --- a/examples/basic_example/basic_example.py +++ b/examples/basic_example/basic_example.py @@ -223,7 +223,7 @@ def main(dump_and_restore=False): nominal_capacity = nominal_capacity / 6 battery_storage = components.GenericStorage( - nominal_storage_capacity=nominal_capacity, + nominal_capacity=nominal_capacity, label=STORAGE_LABEL, inputs={ bus_electricity: flows.Flow(nominal_capacity=nominal_capacity) diff --git a/examples/dual_variable_example/dual_variable_example.py b/examples/dual_variable_example/dual_variable_example.py index 0e26c2472..a06f792d6 100644 --- a/examples/dual_variable_example/dual_variable_example.py +++ b/examples/dual_variable_example/dual_variable_example.py @@ -223,7 +223,7 @@ def main(): # create storage object representing a battery cap = 400 storage = cmp.GenericStorage( - nominal_storage_capacity=cap, + nominal_capacity=cap, label="storage", inputs={bus_elec: flows.Flow(nominal_capacity=cap / 6)}, outputs={ diff --git a/examples/excel_reader/dispatch.py b/examples/excel_reader/dispatch.py index 8927d6e9b..baa099a99 100644 --- a/examples/excel_reader/dispatch.py +++ b/examples/excel_reader/dispatch.py @@ -250,7 +250,7 @@ def create_nodes(nd=None): variable_costs=s["variable output costs"], ) }, - nominal_storage_capacity=s["nominal capacity"], + nominal_capacity=s["nominal capacity"], loss_rate=s["capacity loss"], initial_storage_level=s["initial capacity"], max_storage_level=s["capacity max"], diff --git a/examples/flexible_modelling/saturating_storage.py b/examples/flexible_modelling/saturating_storage.py index 757e685bb..342a6733f 100644 --- a/examples/flexible_modelling/saturating_storage.py +++ b/examples/flexible_modelling/saturating_storage.py @@ -73,7 +73,7 @@ def saturating_storage_example(): storage_capacity = 10 battery = solph.components.GenericStorage( label="battery", - nominal_storage_capacity=storage_capacity, + nominal_capacity=storage_capacity, inputs={bel: solph.Flow(nominal_capacity=inflow_capacity)}, outputs={bel: solph.Flow(variable_costs=2)}, initial_storage_level=0, diff --git a/examples/gradient_example/gradient_example.py b/examples/gradient_example/gradient_example.py index b50269a1a..166b85e62 100644 --- a/examples/gradient_example/gradient_example.py +++ b/examples/gradient_example/gradient_example.py @@ -153,7 +153,7 @@ def main(): # create storage object representing a battery storage = cmp.GenericStorage( - nominal_storage_capacity=999999999, + nominal_capacity=999999999, label="storage", inputs={bel: flows.Flow()}, outputs={bel: flows.Flow()}, diff --git a/examples/invest_nonconvex_flow_examples/diesel_genset_nonconvex_investment.py b/examples/invest_nonconvex_flow_examples/diesel_genset_nonconvex_investment.py index 60f12b7e9..f400c3619 100644 --- a/examples/invest_nonconvex_flow_examples/diesel_genset_nonconvex_investment.py +++ b/examples/invest_nonconvex_flow_examples/diesel_genset_nonconvex_investment.py @@ -209,7 +209,7 @@ def main(): epc_battery = 101.00 # currency/kWh/year battery = solph.components.GenericStorage( label="battery", - nominal_storage_capacity=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=epc_battery * n_days / n_days_in_year ), inputs={b_el_dc: solph.flows.Flow(variable_costs=0)}, diff --git a/examples/offset_converter_example/offset_diesel_genset_nonconvex_investment.py b/examples/offset_converter_example/offset_diesel_genset_nonconvex_investment.py index 3bea2bcae..841a6b8a1 100644 --- a/examples/offset_converter_example/offset_diesel_genset_nonconvex_investment.py +++ b/examples/offset_converter_example/offset_diesel_genset_nonconvex_investment.py @@ -224,7 +224,7 @@ def offset_converter_example(): epc_battery = 101.00 # currency/kWh/year battery = solph.components.GenericStorage( label="battery", - nominal_storage_capacity=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=epc_battery * n_days / n_days_in_year ), inputs={b_el_dc: solph.flows.Flow(variable_costs=0)}, diff --git a/examples/storage_balanced_unbalanced/storage.py b/examples/storage_balanced_unbalanced/storage.py index d26e8419e..23aa73a5d 100644 --- a/examples/storage_balanced_unbalanced/storage.py +++ b/examples/storage_balanced_unbalanced/storage.py @@ -118,7 +118,7 @@ def storage_example(): es.add( solph.components.GenericStorage( label="storage_elec_{0}".format(name), - nominal_storage_capacity=PARAMETER["nominal_storage_capacity"], + nominal_capacity=PARAMETER["nominal_storage_capacity"], inputs={bel: solph.Flow()}, outputs={bel: solph.Flow()}, initial_storage_level=data_set["initial_storage_level"], diff --git a/examples/storage_costs/storage_costs.py b/examples/storage_costs/storage_costs.py index 39bfae556..90ce6a03a 100644 --- a/examples/storage_costs/storage_costs.py +++ b/examples/storage_costs/storage_costs.py @@ -86,7 +86,7 @@ def storage_costs_example(): # last four time steps but emptying it is not a good option. battery1 = solph.components.GenericStorage( label="battery 1", - nominal_storage_capacity=10, + nominal_capacity=10, inputs={ bel: solph.Flow( nominal_capacity=1, @@ -108,7 +108,7 @@ def storage_costs_example(): # Electric Storage 2 battery2 = solph.components.GenericStorage( label="battery 2", - nominal_storage_capacity=10, + nominal_capacity=10, inputs={ bel: solph.Flow( nominal_capacity=1, diff --git a/examples/storage_investment/v1_invest_optimize_all_technologies.py b/examples/storage_investment/v1_invest_optimize_all_technologies.py index 45c5a1d25..6a3d74fa9 100644 --- a/examples/storage_investment/v1_invest_optimize_all_technologies.py +++ b/examples/storage_investment/v1_invest_optimize_all_technologies.py @@ -192,7 +192,7 @@ def main(): invest_relation_output_capacity=1 / 6, inflow_conversion_factor=1, outflow_conversion_factor=0.8, - nominal_storage_capacity=solph.Investment(ep_costs=epc_storage), + nominal_capacity=solph.Investment(ep_costs=epc_storage), ) energysystem.add(excess, gas_resource, wind, pv, demand, pp_gas, storage) diff --git a/examples/storage_investment/v2_invest_optimize_only_gas_and_storage.py b/examples/storage_investment/v2_invest_optimize_only_gas_and_storage.py index 528059e03..863410bb3 100644 --- a/examples/storage_investment/v2_invest_optimize_only_gas_and_storage.py +++ b/examples/storage_investment/v2_invest_optimize_only_gas_and_storage.py @@ -184,7 +184,7 @@ def main(): invest_relation_output_capacity=1 / 6, inflow_conversion_factor=1, outflow_conversion_factor=0.8, - nominal_storage_capacity=solph.Investment(ep_costs=epc_storage), + nominal_capacity=solph.Investment(ep_costs=epc_storage), ) energysystem.add(excess, gas_resource, wind, pv, demand, pp_gas, storage) diff --git a/examples/storage_investment/v3_invest_optimize_only_storage_with_fossil_share.py b/examples/storage_investment/v3_invest_optimize_only_storage_with_fossil_share.py index 4c254e2be..15a420c26 100644 --- a/examples/storage_investment/v3_invest_optimize_only_storage_with_fossil_share.py +++ b/examples/storage_investment/v3_invest_optimize_only_storage_with_fossil_share.py @@ -194,7 +194,7 @@ def main(): invest_relation_output_capacity=1 / 6, inflow_conversion_factor=1, outflow_conversion_factor=0.8, - nominal_storage_capacity=solph.Investment(ep_costs=epc_storage), + nominal_capacity=solph.Investment(ep_costs=epc_storage), ) energysystem.add(excess, gas_resource, wind, pv, demand, pp_gas, storage) diff --git a/examples/storage_investment/v4_invest_optimize_all_technologies_with_fossil_share.py b/examples/storage_investment/v4_invest_optimize_all_technologies_with_fossil_share.py index 2e9264ff1..d8cfc49c4 100644 --- a/examples/storage_investment/v4_invest_optimize_all_technologies_with_fossil_share.py +++ b/examples/storage_investment/v4_invest_optimize_all_technologies_with_fossil_share.py @@ -203,7 +203,7 @@ def main(): invest_relation_output_capacity=1 / 6, inflow_conversion_factor=1, outflow_conversion_factor=0.8, - nominal_storage_capacity=solph.Investment(ep_costs=epc_storage), + nominal_capacity=solph.Investment(ep_costs=epc_storage), ) energysystem.add(excess, gas_resource, wind, pv, demand, pp_gas, storage) diff --git a/examples/storage_level_constraint/storage_level_constraint.py b/examples/storage_level_constraint/storage_level_constraint.py index cbc237445..247ef9dc0 100644 --- a/examples/storage_level_constraint/storage_level_constraint.py +++ b/examples/storage_level_constraint/storage_level_constraint.py @@ -61,7 +61,7 @@ def storage_level_constraint_example(): storage = GenericStorage( label="storage", - nominal_storage_capacity=3, + nominal_capacity=3, initial_storage_level=1, balanced=True, loss_rate=0.05, diff --git a/examples/time_index_example/non_equidistant_time_step_example.py b/examples/time_index_example/non_equidistant_time_step_example.py index 02c91af34..33f40fb3d 100644 --- a/examples/time_index_example/non_equidistant_time_step_example.py +++ b/examples/time_index_example/non_equidistant_time_step_example.py @@ -87,7 +87,7 @@ def main(): label="storage_fixed", inputs={bus: solph.flows.Flow()}, outputs={bus: solph.flows.Flow()}, - nominal_storage_capacity=8, + nominal_capacity=8, initial_storage_level=1, fixed_losses_absolute=1, # 1 energy unit loss per hour ) @@ -102,7 +102,7 @@ def main(): nominal_capacity=4, max=[0, 0, 0, 0, 0, 0, 0, 1, 1] ) }, - nominal_storage_capacity=8, + nominal_capacity=8, initial_storage_level=1, loss_rate=0.5, # 50 % losses per hour ) diff --git a/examples/time_index_example/simple_time_step_example.py b/examples/time_index_example/simple_time_step_example.py index 868f8f93d..4ff1957eb 100644 --- a/examples/time_index_example/simple_time_step_example.py +++ b/examples/time_index_example/simple_time_step_example.py @@ -70,7 +70,7 @@ def main(): label="storage", inputs={bus: solph.flows.Flow()}, outputs={bus: solph.flows.Flow()}, - nominal_storage_capacity=4, + nominal_capacity=4, initial_storage_level=0.5, ) sink = solph.components.Sink( diff --git a/examples/tuple_as_labels/tuple_as_label.py b/examples/tuple_as_labels/tuple_as_label.py index 50c904851..f8fea8b4e 100644 --- a/examples/tuple_as_labels/tuple_as_label.py +++ b/examples/tuple_as_labels/tuple_as_label.py @@ -237,7 +237,7 @@ def main(): # create storage object representing a battery nominal_storage_capacity = 5000 storage = comp.GenericStorage( - nominal_storage_capacity=nominal_storage_capacity, + nominal_capacity=nominal_storage_capacity, label=Label("storage", "electricity", "battery"), inputs={ bel: flows.Flow(nominal_capacity=nominal_storage_capacity / 6) diff --git a/src/oemof/solph/components/_generic_storage.py b/src/oemof/solph/components/_generic_storage.py index e0e178403..a976114b0 100644 --- a/src/oemof/solph/components/_generic_storage.py +++ b/src/oemof/solph/components/_generic_storage.py @@ -140,7 +140,7 @@ class GenericStorage(Node): >>> my_storage = solph.components.GenericStorage( ... label='storage', - ... nominal_storage_capacity=1000, + ... nominal_capacity=1000, ... inputs={my_bus: solph.flows.Flow(nominal_capacity=200, variable_costs=10)}, ... outputs={my_bus: solph.flows.Flow(nominal_capacity=200)}, ... loss_rate=0.01, @@ -151,7 +151,7 @@ class GenericStorage(Node): >>> my_investment_storage = solph.components.GenericStorage( ... label='storage', - ... nominal_storage_capacity=solph.Investment(ep_costs=50), + ... nominal_capacity=solph.Investment(ep_costs=50), ... inputs={my_bus: solph.flows.Flow()}, ... outputs={my_bus: solph.flows.Flow()}, ... loss_rate=0.02, diff --git a/src/oemof/solph/constraints/shared_limit.py b/src/oemof/solph/constraints/shared_limit.py index 3ab92b965..70a51bbbb 100644 --- a/src/oemof/solph/constraints/shared_limit.py +++ b/src/oemof/solph/constraints/shared_limit.py @@ -69,13 +69,13 @@ def shared_limit( >>> b2 = solph.buses.Bus(label="Party2Bus") >>> storage1 = solph.components.GenericStorage( ... label="Party1Storage", - ... nominal_storage_capacity=5, + ... nominal_capacity=5, ... inputs={b1: solph.flows.Flow()}, ... outputs={b1: solph.flows.Flow()} ... ) >>> storage2 = solph.components.GenericStorage( ... label="Party2Storage", - ... nominal_storage_capacity=5, + ... nominal_capacity=5, ... inputs={b1: solph.flows.Flow()}, ... outputs={b1: solph.flows.Flow()} ... ) diff --git a/tests/test_components.py b/tests/test_components.py index 8618af937..0dbc2d993 100644 --- a/tests/test_components.py +++ b/tests/test_components.py @@ -36,7 +36,7 @@ def test_generic_storage_1(): invest_relation_input_output=1, invest_relation_output_capacity=1, invest_relation_input_capacity=1, - nominal_storage_capacity=Investment(), + nominal_capacity=Investment(), inflow_conversion_factor=1, outflow_conversion_factor=0.8, ) @@ -51,7 +51,7 @@ def test_generic_storage_2(): ): components.GenericStorage( label="storage3", - nominal_storage_capacity=45, + nominal_capacity=45, inputs={bel: Flow(variable_costs=10e10)}, outputs={bel: Flow(variable_costs=10e10)}, loss_rate=0.00, @@ -69,7 +69,7 @@ def test_generic_storage_3(): bel = Bus() components.GenericStorage( label="storage4", - nominal_storage_capacity=45, + nominal_capacity=45, inputs={bel: Flow(nominal_capacity=23, variable_costs=10e10)}, outputs={bel: Flow(nominal_capacity=7.5, variable_costs=10e10)}, loss_rate=0.00, @@ -87,7 +87,7 @@ def test_generic_storage_4(): ): components.GenericStorage( label="storage4", - nominal_storage_capacity=10, + nominal_capacity=10, inputs={bel: Flow(variable_costs=10e10)}, outputs={bel: Flow(variable_costs=10e10)}, loss_rate=0.00, @@ -130,7 +130,7 @@ def test_generic_storage_with_non_convex_invest_maximum(): outputs={bel: Flow()}, invest_relation_input_capacity=1 / 6, invest_relation_output_capacity=1 / 6, - nominal_storage_capacity=Investment(nonconvex=True), + nominal_capacity=Investment(nonconvex=True), ) @@ -146,7 +146,7 @@ def test_generic_storage_with_convex_invest_offset(): outputs={bel: Flow()}, invest_relation_input_capacity=1 / 6, invest_relation_output_capacity=1 / 6, - nominal_storage_capacity=Investment(offset=10), + nominal_capacity=Investment(offset=10), ) @@ -177,9 +177,7 @@ def test_generic_storage_with_invest_and_fixed_losses_absolute(): label="storage4", inputs={bel: Flow()}, outputs={bel: Flow()}, - nominal_storage_capacity=Investment( - ep_costs=23, minimum=0, existing=0 - ), + nominal_capacity=Investment(ep_costs=23, minimum=0, existing=0), fixed_losses_absolute=[0, 0, 4], ) diff --git a/tests/test_constraints/test_storage_level.py b/tests/test_constraints/test_storage_level.py index e55f7a7ed..6c066e162 100644 --- a/tests/test_constraints/test_storage_level.py +++ b/tests/test_constraints/test_storage_level.py @@ -19,7 +19,7 @@ def test_storage_level_constraint(): storage = solph.components.GenericStorage( label="storage", - nominal_storage_capacity=3, + nominal_capacity=3, initial_storage_level=1, balanced=True, storage_costs=0.1, diff --git a/tests/test_non_equidistant_time_index.py b/tests/test_non_equidistant_time_index.py index 24aed8d14..ee6534ed0 100644 --- a/tests/test_non_equidistant_time_index.py +++ b/tests/test_non_equidistant_time_index.py @@ -48,7 +48,7 @@ def setup_class(cls): batt = cmp.GenericStorage( label="storage", - nominal_storage_capacity=1000, + nominal_capacity=1000, inputs={b_el1: flows.Flow(variable_costs=3)}, outputs={b_el1: flows.Flow(variable_costs=2.5)}, loss_rate=0.00, diff --git a/tests/test_processing.py b/tests/test_processing.py index 0299ff627..15b09c096 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -67,7 +67,7 @@ def setup_class(cls): invest_relation_output_capacity=1 / 6, inflow_conversion_factor=1, outflow_conversion_factor=0.8, - nominal_storage_capacity=Investment(ep_costs=0.4), + nominal_capacity=Investment(ep_costs=0.4), ) cls.demand_values = [0.0] + [100] * 23 diff --git a/tests/test_scripts/test_solph/test_connect_invest/test_connect_invest.py b/tests/test_scripts/test_solph/test_connect_invest/test_connect_invest.py index 7af2cab04..c518cf402 100644 --- a/tests/test_scripts/test_solph/test_connect_invest/test_connect_invest.py +++ b/tests/test_scripts/test_solph/test_connect_invest/test_connect_invest.py @@ -79,7 +79,7 @@ def test_connect_invest(): invest_relation_output_capacity=1 / 6, inflow_conversion_factor=1, outflow_conversion_factor=0.8, - nominal_storage_capacity=Investment(ep_costs=0.2), + nominal_capacity=Investment(ep_costs=0.2), ) es.add(storage) diff --git a/tests/test_scripts/test_solph/test_multi_period_model/test_multi_period_dispatch_model.py b/tests/test_scripts/test_solph/test_multi_period_model/test_multi_period_dispatch_model.py index 3e391938a..83f7a5834 100644 --- a/tests/test_scripts/test_solph/test_multi_period_model/test_multi_period_dispatch_model.py +++ b/tests/test_scripts/test_solph/test_multi_period_model/test_multi_period_dispatch_model.py @@ -185,7 +185,7 @@ def test_multi_period_dispatch_model(solver="cbc"): outputs={ bus_el: flows.Flow(nominal_capacity=20, variable_costs=0, max=1) }, - nominal_storage_capacity=20, + nominal_capacity=20, loss_rate=0, initial_storage_level=0, max_storage_level=1, diff --git a/tests/test_scripts/test_solph/test_multi_period_model/test_multi_period_investment_model.py b/tests/test_scripts/test_solph/test_multi_period_model/test_multi_period_investment_model.py index fcaf31c93..047afd9e9 100644 --- a/tests/test_scripts/test_solph/test_multi_period_model/test_multi_period_investment_model.py +++ b/tests/test_scripts/test_solph/test_multi_period_model/test_multi_period_investment_model.py @@ -265,7 +265,7 @@ def test_multi_period_investment_model(solver="cbc"): invest_relation_input_capacity=None, invest_relation_output_capacity=None, fixed_costs=10, - nominal_storage_capacity=_options.Investment( + nominal_capacity=_options.Investment( maximum=20, ep_costs=1000, existing=10, diff --git a/tests/test_scripts/test_solph/test_storage_investment/test_invest_storage_regression.py b/tests/test_scripts/test_solph/test_storage_investment/test_invest_storage_regression.py index 244556e1f..79d388791 100644 --- a/tests/test_scripts/test_solph/test_storage_investment/test_invest_storage_regression.py +++ b/tests/test_scripts/test_solph/test_storage_investment/test_invest_storage_regression.py @@ -86,7 +86,7 @@ def test_regression_investment_storage(solver="cbc"): invest_relation_output_capacity=1 / 6, inflow_conversion_factor=1, outflow_conversion_factor=0.8, - nominal_storage_capacity=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=50, existing=625046, ), diff --git a/tests/test_scripts/test_solph/test_storage_investment/test_storage_investment.py b/tests/test_scripts/test_solph/test_storage_investment/test_storage_investment.py index 478d7273d..e64ee1fe4 100644 --- a/tests/test_scripts/test_solph/test_storage_investment/test_storage_investment.py +++ b/tests/test_scripts/test_solph/test_storage_investment/test_storage_investment.py @@ -144,7 +144,7 @@ def test_optimise_storage_size( invest_relation_output_capacity=1 / 6, inflow_conversion_factor=1, outflow_conversion_factor=0.8, - nominal_storage_capacity=solph.Investment( + nominal_capacity=solph.Investment( ep_costs=epc, existing=6851, ), diff --git a/tests/test_scripts/test_solph/test_storage_investment/test_storage_with_tuple_label.py b/tests/test_scripts/test_solph/test_storage_investment/test_storage_with_tuple_label.py index 3027f02fe..e907b9177 100644 --- a/tests/test_scripts/test_solph/test_storage_investment/test_storage_with_tuple_label.py +++ b/tests/test_scripts/test_solph/test_storage_investment/test_storage_with_tuple_label.py @@ -151,7 +151,7 @@ def test_tuples_as_labels_example( energysystem.add( solph.components.GenericStorage( label=Label("storage", "electricity", "battery"), - nominal_storage_capacity=204685, + nominal_capacity=204685, inputs={bel: solph.flows.Flow(variable_costs=10e10)}, outputs={bel: solph.flows.Flow(variable_costs=10e10)}, loss_rate=0.00, From 822d8a783f21ca81d826ff1275723e49f301e7c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Thu, 22 Aug 2024 15:35:13 +0200 Subject: [PATCH 14/45] Remove unneccesary code --- src/oemof/solph/flows/_flow.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/oemof/solph/flows/_flow.py b/src/oemof/solph/flows/_flow.py index 6e2d859d8..30f0dd61e 100644 --- a/src/oemof/solph/flows/_flow.py +++ b/src/oemof/solph/flows/_flow.py @@ -192,12 +192,6 @@ def __init__( else: warn(msg, FutureWarning) nominal_capacity = nominal_value - - msg = ( - "\nThe parameter '{0}' is deprecated and will be removed " - + "in version v0.6.\nUse the parameter '{1}', " - + "to avoid this warning and future problems. " - ) # --- END --- super().__init__() From 2ffc0becda88b702d0c5d72b5b2045ba054552a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Thu, 22 Aug 2024 15:41:21 +0200 Subject: [PATCH 15/45] Remove argument "invest" from GenericStorage --- .../solph/components/_generic_storage.py | 15 --------- tests/test_components.py | 33 ------------------- 2 files changed, 48 deletions(-) diff --git a/src/oemof/solph/components/_generic_storage.py b/src/oemof/solph/components/_generic_storage.py index 62a04dc87..9a1dfd6c2 100644 --- a/src/oemof/solph/components/_generic_storage.py +++ b/src/oemof/solph/components/_generic_storage.py @@ -169,7 +169,6 @@ def __init__( outputs=None, nominal_storage_capacity=None, initial_storage_level=None, - investment=None, invest_relation_input_output=None, invest_relation_input_capacity=None, invest_relation_output_capacity=None, @@ -199,20 +198,6 @@ def __init__( outputs=outputs, custom_properties=custom_attributes, ) - # --- BEGIN: The following code can be removed for versions >= v0.6 --- - if investment is not None: - msg = ( - "For backward compatibility," - " the option investment overwrites the option" - + " nominal_storage_capacity." - + " Both options cannot be set at the same time." - ) - if nominal_storage_capacity is not None: - raise AttributeError(msg) - else: - warn(msg, FutureWarning) - nominal_storage_capacity = investment - # --- END --- self.nominal_storage_capacity = None self.investment = None diff --git a/tests/test_components.py b/tests/test_components.py index f66230bfc..b1e7e0cac 100644 --- a/tests/test_components.py +++ b/tests/test_components.py @@ -42,28 +42,6 @@ def test_generic_storage_1(): ) -def test_generic_storage_2(): - """Nominal value defined with investment model.""" - bel = Bus() - with pytest.raises( - AttributeError, - match="For backward compatibility, the option investment overwrites", - ): - components.GenericStorage( - label="storage3", - nominal_storage_capacity=45, - inputs={bel: Flow(variable_costs=10e10)}, - outputs={bel: Flow(variable_costs=10e10)}, - loss_rate=0.00, - initial_storage_level=0, - invest_relation_input_capacity=1 / 6, - invest_relation_output_capacity=1 / 6, - inflow_conversion_factor=1, - outflow_conversion_factor=0.8, - investment=Investment(ep_costs=23), - ) - - def test_generic_storage_3(): """Nominal value defined with investment model.""" bel = Bus() @@ -148,17 +126,6 @@ def test_generic_storage_with_convex_invest_offset(): ) -def test_generic_storage_invest_warning(): - with pytest.warns(FutureWarning): - bel = Bus() - components.GenericStorage( - label="storage7", - inputs={bel: Flow()}, - outputs={bel: Flow()}, - investment=Investment(), - ) - - def test_generic_storage_with_invest_and_fixed_losses_absolute(): """ Storage with fixed losses in the investment mode but no minimum or existing From d9c972ee9aca4baf9993abd578499fdb5785dbf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Thu, 22 Aug 2024 15:43:34 +0200 Subject: [PATCH 16/45] Remove docstring for argument "invest" of GenericStorage --- src/oemof/solph/components/_generic_storage.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/oemof/solph/components/_generic_storage.py b/src/oemof/solph/components/_generic_storage.py index 9a1dfd6c2..0366c7a69 100644 --- a/src/oemof/solph/components/_generic_storage.py +++ b/src/oemof/solph/components/_generic_storage.py @@ -103,12 +103,6 @@ class GenericStorage(Node): To set different values in every time step use a sequence. max_storage_level : numeric (iterable or scalar), :math:`c_{max}(t)` see: min_storage_level - investment : :class:`oemof.solph.options.Investment` object - Object indicating if a nominal_value of the flow is determined by - the optimization problem. Note: This will refer all attributes to an - investment variable instead of to the nominal_storage_capacity. The - nominal_storage_capacity should not be set (or set to None) if an - investment object is used. storage_costs : numeric (iterable or scalar), :math:`c_{storage}(t)` Cost (per energy) for having energy in the storage, starting from time point :math:`t_{1}`. From 6909370fe9be4f992b784f1ff54d03bdf846fa38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Thu, 22 Aug 2024 16:06:40 +0200 Subject: [PATCH 17/45] Properly name nominal_capacity in docstring --- src/oemof/solph/components/_generic_storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/oemof/solph/components/_generic_storage.py b/src/oemof/solph/components/_generic_storage.py index a976114b0..43bd06946 100644 --- a/src/oemof/solph/components/_generic_storage.py +++ b/src/oemof/solph/components/_generic_storage.py @@ -49,7 +49,7 @@ class GenericStorage(Node): Parameters ---------- - nominal_storage_capacity : numeric, :math:`E_{nom}` or + nominal_capacity : numeric, :math:`E_{nom}` or :class:`oemof.solph.options.Investment` object Absolute nominal capacity of the storage, fixed value or object describing parameter of investment optimisations. From 7339c6365d6e2b9dbb6e190547458748ee554f93 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Sun, 6 Oct 2024 08:16:53 +0200 Subject: [PATCH 18/45] Change version from v2 to v4 for artifact upload --- .github/workflows/packaging.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml index d53aa82b8..a09540cfe 100644 --- a/.github/workflows/packaging.yml +++ b/.github/workflows/packaging.yml @@ -34,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 From 551fa97bc5ebf618d6686207f01ad9c3cf5ed46c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 15 Oct 2024 16:36:39 +0200 Subject: [PATCH 19/45] Release v0.6.0a2 --- src/oemof/solph/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 599946d94861898e922881c26c576596e007153e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 15 Oct 2024 16:58:51 +0200 Subject: [PATCH 20/45] Change to upload-artifact@v4 (GH CI) --- .github/workflows/packaging.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml index d53aa82b8..a09540cfe 100644 --- a/.github/workflows/packaging.yml +++ b/.github/workflows/packaging.yml @@ -34,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 From 78246512c1275fe682382e9bb086026374281e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 15 Oct 2024 17:11:31 +0200 Subject: [PATCH 21/45] Prepare to have a v0.5.6 --- docs/changelog.rst | 1 + docs/whatsnew/v0-5-6.rst | 12 ++++++++++++ src/oemof/solph/__init__.py | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 docs/whatsnew/v0-5-6.rst diff --git a/docs/changelog.rst b/docs/changelog.rst index 7f0fe3d06..2ac9650d1 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-6.rst .. include:: whatsnew/v0-5-5.rst .. include:: whatsnew/v0-5-4.rst .. include:: whatsnew/v0-5-3.rst diff --git a/docs/whatsnew/v0-5-6.rst b/docs/whatsnew/v0-5-6.rst new file mode 100644 index 000000000..954bb5329 --- /dev/null +++ b/docs/whatsnew/v0-5-6.rst @@ -0,0 +1,12 @@ +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. diff --git a/src/oemof/solph/__init__.py b/src/oemof/solph/__init__.py index 7f58c9578..b275f6168 100644 --- a/src/oemof/solph/__init__.py +++ b/src/oemof/solph/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.5.5" +__version__ = "0.5.6a1" from . import buses from . import components From d3410d748b991ab5f0299f6df1ada5a49b9c7378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 15 Oct 2024 17:14:29 +0200 Subject: [PATCH 22/45] Add Contributors to v0.6.6 whatsnew --- docs/whatsnew/v0-5-6.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/whatsnew/v0-5-6.rst b/docs/whatsnew/v0-5-6.rst index 954bb5329..a6c92aebf 100644 --- a/docs/whatsnew/v0-5-6.rst +++ b/docs/whatsnew/v0-5-6.rst @@ -10,3 +10,8 @@ Known issues ############ * Indexing of Storage with capacity investment is off by one. + +Contributors +############ + +* Patrik Schönfeldt From cacc09d0d7848ad2e2165e505fe4542e1ba3ff4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Mon, 25 Nov 2024 17:13:28 +0100 Subject: [PATCH 23/45] Update examples/tuple_as_labels/tuple_as_label.py Co-authored-by: Johannes Kochems <40718083+jokochems@users.noreply.github.com> --- examples/tuple_as_labels/tuple_as_label.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tuple_as_labels/tuple_as_label.py b/examples/tuple_as_labels/tuple_as_label.py index f8fea8b4e..d8c8d3b7e 100644 --- a/examples/tuple_as_labels/tuple_as_label.py +++ b/examples/tuple_as_labels/tuple_as_label.py @@ -237,7 +237,7 @@ def main(): # create storage object representing a battery nominal_storage_capacity = 5000 storage = comp.GenericStorage( - nominal_capacity=nominal_storage_capacity, + nominal_capacity=nominal_capacity, label=Label("storage", "electricity", "battery"), inputs={ bel: flows.Flow(nominal_capacity=nominal_storage_capacity / 6) From 9b137cd87ad647711fb11da33db8df3cb1930b50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Mon, 25 Nov 2024 17:13:47 +0100 Subject: [PATCH 24/45] Update examples/tuple_as_labels/tuple_as_label.py Co-authored-by: Johannes Kochems <40718083+jokochems@users.noreply.github.com> --- examples/tuple_as_labels/tuple_as_label.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tuple_as_labels/tuple_as_label.py b/examples/tuple_as_labels/tuple_as_label.py index d8c8d3b7e..3a6a49b25 100644 --- a/examples/tuple_as_labels/tuple_as_label.py +++ b/examples/tuple_as_labels/tuple_as_label.py @@ -235,7 +235,7 @@ def main(): ) # create storage object representing a battery - nominal_storage_capacity = 5000 + nominal_capacity = 5000 storage = comp.GenericStorage( nominal_capacity=nominal_capacity, label=Label("storage", "electricity", "battery"), From b98016796ff88f71b9b6ec61f7c1b9e216200e2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Mon, 25 Nov 2024 17:23:51 +0100 Subject: [PATCH 25/45] Apply suggestions from code review Co-authored-by: Johannes Kochems <40718083+jokochems@users.noreply.github.com> --- docs/whatsnew/v0-6-0.rst | 2 +- examples/tuple_as_labels/tuple_as_label.py | 4 ++-- src/oemof/solph/components/_generic_storage.py | 2 +- src/oemof/solph/flows/_flow.py | 4 ++-- tests/test_flows/test_simple_flow.py | 2 +- .../test_storage_investment/test_storage_investment.py | 2 +- .../test_solph/test_variable_chp/test_variable_chp.py | 4 ++-- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/whatsnew/v0-6-0.rst b/docs/whatsnew/v0-6-0.rst index 5b95cd94b..6808ba412 100644 --- a/docs/whatsnew/v0-6-0.rst +++ b/docs/whatsnew/v0-6-0.rst @@ -11,7 +11,7 @@ API changes initial storage level is defined, the costs just offset the objective value without changing anything else. * The parameters `GenericStorage.nominal_storage_capacity` and - `Flow.nominal_capacity` are now both called `nominal_calacity`. + `Flow.nominal_capacity` are now both called `nominal_capacity`. New features ############ diff --git a/examples/tuple_as_labels/tuple_as_label.py b/examples/tuple_as_labels/tuple_as_label.py index 3a6a49b25..d72bd8d24 100644 --- a/examples/tuple_as_labels/tuple_as_label.py +++ b/examples/tuple_as_labels/tuple_as_label.py @@ -240,10 +240,10 @@ def main(): nominal_capacity=nominal_capacity, label=Label("storage", "electricity", "battery"), inputs={ - bel: flows.Flow(nominal_capacity=nominal_storage_capacity / 6) + bel: flows.Flow(nominal_capacity=nominal_capacity / 6) }, outputs={ - bel: flows.Flow(nominal_capacity=nominal_storage_capacity / 6) + bel: flows.Flow(nominal_capacity=nominal_capacity / 6) }, loss_rate=0.00, initial_storage_level=None, diff --git a/src/oemof/solph/components/_generic_storage.py b/src/oemof/solph/components/_generic_storage.py index 43bd06946..ca8d323d9 100644 --- a/src/oemof/solph/components/_generic_storage.py +++ b/src/oemof/solph/components/_generic_storage.py @@ -218,7 +218,7 @@ def __init__( if nominal_storage_capacity is not None: msg = ( "For backward compatibility," - " the option nominal_storage_capacity overwrites the option" + + " the option nominal_storage_capacity overwrites the option" + " nominal_capacity." + " Both options cannot be set at the same time." ) diff --git a/src/oemof/solph/flows/_flow.py b/src/oemof/solph/flows/_flow.py index 30f0dd61e..3ebb9051e 100644 --- a/src/oemof/solph/flows/_flow.py +++ b/src/oemof/solph/flows/_flow.py @@ -183,8 +183,8 @@ def __init__( if nominal_value is not None: msg = ( "For backward compatibility," - " the option nominal_value overwrites the option" - " nominal_capacity." + + " the option nominal_value overwrites the option" + + " nominal_capacity." + " Both options cannot be set at the same time." ) if nominal_capacity is not None: diff --git a/tests/test_flows/test_simple_flow.py b/tests/test_flows/test_simple_flow.py index e5c74ed91..6612b77dc 100644 --- a/tests/test_flows/test_simple_flow.py +++ b/tests/test_flows/test_simple_flow.py @@ -59,7 +59,7 @@ def test_full_load_time_min(): # --- BEGIN: The following code can be removed for versions >= v0.7 --- def test_nominal_capacity_warning(): - with pytest.warns(FutureWarning, match="nominal_capacity"): + with pytest.warns(FutureWarning, match="nominal_value"): _ = solph.flows.Flow(nominal_value=2) diff --git a/tests/test_scripts/test_solph/test_storage_investment/test_storage_investment.py b/tests/test_scripts/test_solph/test_storage_investment/test_storage_investment.py index e64ee1fe4..89cbf2b61 100644 --- a/tests/test_scripts/test_solph/test_storage_investment/test_storage_investment.py +++ b/tests/test_scripts/test_solph/test_storage_investment/test_storage_investment.py @@ -125,7 +125,7 @@ def test_optimise_storage_size( label="pp_gas", inputs={bgas: solph.flows.Flow()}, outputs={ - bel: solph.flows.Flow(nominal_capacity=10e10, variable_costs=50) + bel: solph.flows.Flow(nominal_capacity=1e11, variable_costs=50) }, conversion_factors={bel: 0.58}, ) diff --git a/tests/test_scripts/test_solph/test_variable_chp/test_variable_chp.py b/tests/test_scripts/test_solph/test_variable_chp/test_variable_chp.py index 0f9f4c514..49a4a5dd4 100644 --- a/tests/test_scripts/test_solph/test_variable_chp/test_variable_chp.py +++ b/tests/test_scripts/test_solph/test_variable_chp/test_variable_chp.py @@ -130,7 +130,7 @@ def test_variable_chp(filename="variable_chp.csv", solver="cbc"): energysystem.add( solph.components.Converter( label=("fixed_chp", "gas"), - inputs={bgas: solph.flows.Flow(nominal_capacity=10e10)}, + inputs={bgas: solph.flows.Flow(nominal_capacity=1e11)}, outputs={bel2: solph.flows.Flow(), bth2: solph.flows.Flow()}, conversion_factors={bel2: 0.3, bth2: 0.5}, ) @@ -140,7 +140,7 @@ def test_variable_chp(filename="variable_chp.csv", solver="cbc"): energysystem.add( solph.components.ExtractionTurbineCHP( label=("variable_chp", "gas"), - inputs={bgas: solph.flows.Flow(nominal_capacity=10e10)}, + inputs={bgas: solph.flows.Flow(nominal_capacity=1e11)}, outputs={bel: solph.flows.Flow(), bth: solph.flows.Flow()}, conversion_factors={bel: 0.3, bth: 0.5}, conversion_factor_full_condensation={bel: 0.5}, From 988da2b0eed2e67478e44672157a96e7adf8f659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Mon, 25 Nov 2024 17:26:21 +0100 Subject: [PATCH 26/45] Update tests/test_components/test_storage.py --- tests/test_components/test_storage.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_components/test_storage.py b/tests/test_components/test_storage.py index ec6494456..f14e2f9af 100644 --- a/tests/test_components/test_storage.py +++ b/tests/test_components/test_storage.py @@ -366,6 +366,4 @@ def test_capacity_keyword_wrapper_error(): inputs={bus: solph.Flow()}, outputs={bus: solph.Flow()}, ) - - # --- END --- From f3e4d7ced98becc5c2a9c2ab031245c6df990534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Mon, 25 Nov 2024 17:27:57 +0100 Subject: [PATCH 27/45] Adhere to Black --- examples/tuple_as_labels/tuple_as_label.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/tuple_as_labels/tuple_as_label.py b/examples/tuple_as_labels/tuple_as_label.py index d72bd8d24..3e0290ce5 100644 --- a/examples/tuple_as_labels/tuple_as_label.py +++ b/examples/tuple_as_labels/tuple_as_label.py @@ -239,12 +239,8 @@ def main(): storage = comp.GenericStorage( nominal_capacity=nominal_capacity, label=Label("storage", "electricity", "battery"), - inputs={ - bel: flows.Flow(nominal_capacity=nominal_capacity / 6) - }, - outputs={ - bel: flows.Flow(nominal_capacity=nominal_capacity / 6) - }, + inputs={bel: flows.Flow(nominal_capacity=nominal_capacity / 6)}, + outputs={bel: flows.Flow(nominal_capacity=nominal_capacity / 6)}, loss_rate=0.00, initial_storage_level=None, inflow_conversion_factor=1, From 444b685089df9418b1b3e7300943fde596086416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Mon, 25 Nov 2024 17:41:23 +0100 Subject: [PATCH 28/45] Adhere to Black --- tests/test_components/test_storage.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_components/test_storage.py b/tests/test_components/test_storage.py index f14e2f9af..ec6494456 100644 --- a/tests/test_components/test_storage.py +++ b/tests/test_components/test_storage.py @@ -366,4 +366,6 @@ def test_capacity_keyword_wrapper_error(): inputs={bus: solph.Flow()}, outputs={bus: solph.Flow()}, ) + + # --- END --- From dfc3c1266b646807fbe342e6f0eb6b09060864ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 26 Nov 2024 09:01:22 +0100 Subject: [PATCH 29/45] Fix links to pyomo doc Unfortunately, pyomo seems to have dropped the list of solvers we pointed to. Also, there is hardly any advice on how to install solvers left. --- README.rst | 577 ++++---- docs/usage.rst | 3412 ++++++++++++++++++++++++------------------------ 2 files changed, 1996 insertions(+), 1993 deletions(-) diff --git a/README.rst b/README.rst index 21501fdf2..6d69c9571 100644 --- a/README.rst +++ b/README.rst @@ -1,288 +1,289 @@ - -|tox-pytest| |tox-checks| |appveyor| |coveralls| |codecov| - -|scrutinizer| |codacy| |codeclimate| - -|wheel| |packaging| |supported-versions| - -|docs| |zenodo| - -|version| |commits-since| |chat| - - ------------------------------- - -.. |tox-pytest| image:: https://github.com/oemof/oemof-solph/workflows/tox%20pytests/badge.svg?branch=dev - :target: https://github.com/oemof/oemof-solph/actions?query=workflow%3A%22tox+checks%22 - -.. |tox-checks| image:: https://github.com/oemof/oemof-solph/workflows/tox%20checks/badge.svg?branch=dev - :target: https://github.com/oemof/oemof-solph/actions?query=workflow%3A%22tox+checks%22 - -.. |packaging| image:: https://github.com/oemof/oemof-solph/workflows/packaging/badge.svg?branch=dev - :target: https://github.com/oemof/oemof-solph/actions?query=workflow%3Apackaging - -.. |docs| image:: https://readthedocs.org/projects/oemof-solph/badge/?style=flat - :target: https://readthedocs.org/projects/oemof-solph - :alt: Documentation Status - -.. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/oemof/oemof-solph?branch=dev&svg=true - :alt: AppVeyor Build Status - :target: https://ci.appveyor.com/project/oemof-developer/oemof-solph - -.. |coveralls| image:: https://coveralls.io/repos/oemof/oemof-solph/badge.svg?branch=dev&service=github - :alt: Coverage Status - :target: https://coveralls.io/github/oemof/oemof-solph - -.. |codecov| image:: https://codecov.io/gh/oemof/oemof-solph/branch/dev/graphs/badge.svg?branch=dev - :alt: Coverage Status - :target: https://codecov.io/gh/oemof/oemof-solph - -.. |codacy| image:: https://api.codacy.com/project/badge/Grade/a6e5cb2dd2694c73895e142e4cf680d5 - :target: https://app.codacy.com/gh/oemof/oemof-solph/dashboard - :alt: Codacy Code Quality Status - -.. |codeclimate| image:: https://codeclimate.com/github/oemof/oemof-solph/badges/gpa.svg - :target: https://codeclimate.com/github/oemof/oemof-solph - :alt: CodeClimate Quality Status - -.. |version| image:: https://img.shields.io/pypi/v/oemof.solph.svg - :alt: PyPI Package latest release - :target: https://pypi.org/project/oemof.solph - -.. |wheel| image:: https://img.shields.io/pypi/wheel/oemof.solph.svg - :alt: PyPI Wheel - :target: https://pypi.org/project/oemof.solph - -.. |supported-versions| image:: https://img.shields.io/pypi/pyversions/oemof.solph.svg - :alt: Supported versions - :target: https://pypi.org/project/oemof.solph - -.. |supported-implementations| image:: https://img.shields.io/pypi/implementation/oemof.solph.svg - :alt: Supported implementations - :target: https://pypi.org/project/oemof.solph - -.. |commits-since| image:: https://img.shields.io/github/commits-since/oemof/oemof-solph/latest/dev - :alt: Commits since latest release - :target: https://github.com/oemof/oemof-solph/compare/master...dev - -.. |zenodo| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.596235.svg - :alt: Zenodo DOI - :target: https://doi.org/10.5281/zenodo.596235 - -.. |scrutinizer| image:: https://img.shields.io/scrutinizer/quality/g/oemof/oemof-solph/dev.svg - :alt: Scrutinizer Status - :target: https://scrutinizer-ci.com/g/oemof/oemof-solph/ - -.. |chat| image:: https://img.shields.io/badge/chat-oemof:matrix.org-%238ADCF7 - :alt: matrix-chat - :target: https://matrix.to/#/#oemof:matrix.org - - -.. figure:: https://raw.githubusercontent.com/oemof/oemof-solph/492e3f5a0dda7065be30d33a37b0625027847518/docs/_logo/logo_oemof_solph_FULL.svg - :align: center - ------------------------------- - -=========== -oemof.solph -=========== - -**A model generator for energy system modelling and optimisation (LP/MILP)** - -.. contents:: - :depth: 2 - :local: - :backlinks: top - - -Introduction -============ - -The oemof.solph package is part of the -`Open energy modelling framework (oemof) `_. -This is an organisational framework to bundle tools for energy (system) modelling. -oemof-solph is a model generator for energy system modelling and optimisation. - -The package ``oemof.solph`` is very often called just ``oemof``. -This is because installing the ``oemof`` meta package was once the best way to get ``oemof.solph``. -Notice that you should prefeably install ``oemof.solph`` instead of ``oemof`` -if you want to use ``solph``. - - -Everybody is welcome to use and/or develop oemof.solph. -Read our `contribution `_ section. - -Contribution is already possible on a low level by simply fixing typos in -oemof's documentation or rephrasing sections which are unclear. -If you want to support us that way please fork the oemof-solph repository to your own -GitHub account and make changes as described in the `github guidelines `_ - -If you have questions regarding the use of oemof including oemof.solph you can visit the openmod forum (`tag oemof `_ or `tag oemof-solph `_) and open a new thread if your questions hasn't been already answered. - -Keep in touch! - You can become a watcher at our `github site `_, -but this will bring you quite a few mails and might be more interesting for developers. -If you just want to get the latest news, like when is the next oemof meeting, -you can follow our news-blog at `oemof.org `_. - -Documentation -============= -The `oemof.solph documentation `_ is powered by readthedocs. Use the `project site `_ of oemof.solph to choose the version of the documentation. Go to the `download page `_ to download different versions and formats (pdf, html, epub) of the documentation. - - -.. _installation_label: - -Installation -============ - - -If you have a working Python installation, use pypi to install the latest version of oemof.solph. -Python >= 3.8 is recommended. Lower versions may work but are not tested. - -We highly recommend to use virtual environments. -Please refer to the documentation of your Python distribution (e.g. Anaconda, -Micromamba, or the version of Python that came with your Linux installation) -to learn how to set up and use virtual environments. - -:: - - (venv) pip install oemof.solph - -If you want to use the latest features, you might want to install the **developer version**. The developer version is not recommended for productive use:: - - (venv) pip install https://github.com/oemof/oemof-solph/archive/dev.zip - - -For running an oemof-solph optimisation model, you need to install a solver. -Following you will find guidelines for the installation process for different operating systems. - -.. _windows_solver_label: -.. _linux_solver_label: - -Installing a solver -------------------- - -There are several solvers that can work with oemof, both open source and commercial. -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. - -Check the solver installation by executing the test_installation example below (see section Installation Test). - -**Linux** - -To install the solvers have a look at the package repository of your Linux distribution or search for precompiled packages. GLPK and CBC ares available at Debian, Feodora, Ubuntu and others. - -**Windows** - - 1. Download `CBC `_ - 2. Download `GLPK (64/32 bit) `_ - 3. Unpack CBC/GLPK to any folder (e.g. C:/Users/Somebody/my_programs) - 4. Add the path of the executable files of both solvers to the PATH variable using `this tutorial `_ - 5. Restart Windows - -Check the solver installation by executing the test_installation example (see the `Installation test` section). - - -**Mac OSX** - -Please follow the installation instructions on the respective homepages for details. - -CBC-solver: https://projects.coin-or.org/Cbc - -GLPK-solver: http://arnab-deka.com/posts/2010/02/installing-glpk-on-a-mac/ - -If you install the CBC solver via brew (highly recommended), it should work without additional configuration. - - -**conda** - -Provided you are using a Linux or MacOS, the CBC-solver can also be installed in a `conda` environment. Please note, that it is highly recommended to `use pip after conda `_, so: - -.. code:: console - - (venv) conda install -c conda-forge coincbc - (venv) pip install oemof.solph - - -.. _check_installation_label: - -Installation test ------------------ - -Test the installation and the installed solver by running the installation test -in your virtual environment: - -.. code:: console - - (venv) oemof_installation_test - -If the installation was successful, you will receive something like this: - -.. code:: console - - ********* - Solver installed with oemof: - glpk: working - cplex: not working - cbc: working - gurobi: not working - ********* - oemof.solph successfully installed. - -as an output. - -Contributing -============ - -A warm welcome to all who want to join the developers and contribute to -oemof.solph. - -Information on the details and how to approach us can be found -`in the oemof documentation `_ . - -Citing -====== - -For explicitly citing solph, you might want to refer to -`DOI:10.1016/j.simpa.2020.100028 `_, -which gives an overview over the capabilities of solph. -The core ideas of oemof as a whole are described in -`DOI:10.1016/j.esr.2018.07.001 `_ -(preprint at `arXiv:1808.0807 `_). -To allow citing specific versions, we use the zenodo project to get a DOI for each version. - -Example Applications -==================== - -The combination of specific modules (often including other packages) is called an -application (app). For example, it can depict a concrete energy system model. -You can find a large variety of helpful examples in the documentation. -The examples show the optimisation of different energy systems and are supposed -to help new users to understand the framework's structure. - -You are welcome to contribute your own examples via a `pull request `_ -or by e-mailing us (see `here `_ for contact information). - -License -======= - -Copyright (c) oemof developer group - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + +|tox-pytest| |tox-checks| |appveyor| |coveralls| |codecov| + +|scrutinizer| |codacy| |codeclimate| + +|wheel| |packaging| |supported-versions| + +|docs| |zenodo| + +|version| |commits-since| |chat| + + +------------------------------ + +.. |tox-pytest| image:: https://github.com/oemof/oemof-solph/workflows/tox%20pytests/badge.svg?branch=dev + :target: https://github.com/oemof/oemof-solph/actions?query=workflow%3A%22tox+checks%22 + +.. |tox-checks| image:: https://github.com/oemof/oemof-solph/workflows/tox%20checks/badge.svg?branch=dev + :target: https://github.com/oemof/oemof-solph/actions?query=workflow%3A%22tox+checks%22 + +.. |packaging| image:: https://github.com/oemof/oemof-solph/workflows/packaging/badge.svg?branch=dev + :target: https://github.com/oemof/oemof-solph/actions?query=workflow%3Apackaging + +.. |docs| image:: https://readthedocs.org/projects/oemof-solph/badge/?style=flat + :target: https://readthedocs.org/projects/oemof-solph + :alt: Documentation Status + +.. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/oemof/oemof-solph?branch=dev&svg=true + :alt: AppVeyor Build Status + :target: https://ci.appveyor.com/project/oemof-developer/oemof-solph + +.. |coveralls| image:: https://coveralls.io/repos/oemof/oemof-solph/badge.svg?branch=dev&service=github + :alt: Coverage Status + :target: https://coveralls.io/github/oemof/oemof-solph + +.. |codecov| image:: https://codecov.io/gh/oemof/oemof-solph/branch/dev/graphs/badge.svg?branch=dev + :alt: Coverage Status + :target: https://codecov.io/gh/oemof/oemof-solph + +.. |codacy| image:: https://api.codacy.com/project/badge/Grade/a6e5cb2dd2694c73895e142e4cf680d5 + :target: https://app.codacy.com/gh/oemof/oemof-solph/dashboard + :alt: Codacy Code Quality Status + +.. |codeclimate| image:: https://codeclimate.com/github/oemof/oemof-solph/badges/gpa.svg + :target: https://codeclimate.com/github/oemof/oemof-solph + :alt: CodeClimate Quality Status + +.. |version| image:: https://img.shields.io/pypi/v/oemof.solph.svg + :alt: PyPI Package latest release + :target: https://pypi.org/project/oemof.solph + +.. |wheel| image:: https://img.shields.io/pypi/wheel/oemof.solph.svg + :alt: PyPI Wheel + :target: https://pypi.org/project/oemof.solph + +.. |supported-versions| image:: https://img.shields.io/pypi/pyversions/oemof.solph.svg + :alt: Supported versions + :target: https://pypi.org/project/oemof.solph + +.. |supported-implementations| image:: https://img.shields.io/pypi/implementation/oemof.solph.svg + :alt: Supported implementations + :target: https://pypi.org/project/oemof.solph + +.. |commits-since| image:: https://img.shields.io/github/commits-since/oemof/oemof-solph/latest/dev + :alt: Commits since latest release + :target: https://github.com/oemof/oemof-solph/compare/master...dev + +.. |zenodo| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.596235.svg + :alt: Zenodo DOI + :target: https://doi.org/10.5281/zenodo.596235 + +.. |scrutinizer| image:: https://img.shields.io/scrutinizer/quality/g/oemof/oemof-solph/dev.svg + :alt: Scrutinizer Status + :target: https://scrutinizer-ci.com/g/oemof/oemof-solph/ + +.. |chat| image:: https://img.shields.io/badge/chat-oemof:matrix.org-%238ADCF7 + :alt: matrix-chat + :target: https://matrix.to/#/#oemof:matrix.org + + +.. figure:: https://raw.githubusercontent.com/oemof/oemof-solph/492e3f5a0dda7065be30d33a37b0625027847518/docs/_logo/logo_oemof_solph_FULL.svg + :align: center + +------------------------------ + +=========== +oemof.solph +=========== + +**A model generator for energy system modelling and optimisation (LP/MILP)** + +.. contents:: + :depth: 2 + :local: + :backlinks: top + + +Introduction +============ + +The oemof.solph package is part of the +`Open energy modelling framework (oemof) `_. +This is an organisational framework to bundle tools for energy (system) modelling. +oemof-solph is a model generator for energy system modelling and optimisation. + +The package ``oemof.solph`` is very often called just ``oemof``. +This is because installing the ``oemof`` meta package was once the best way to get ``oemof.solph``. +Notice that you should prefeably install ``oemof.solph`` instead of ``oemof`` +if you want to use ``solph``. + + +Everybody is welcome to use and/or develop oemof.solph. +Read our `contribution `_ section. + +Contribution is already possible on a low level by simply fixing typos in +oemof's documentation or rephrasing sections which are unclear. +If you want to support us that way please fork the oemof-solph repository to your own +GitHub account and make changes as described in the `github guidelines `_ + +If you have questions regarding the use of oemof including oemof.solph you can visit the openmod forum (`tag oemof `_ or `tag oemof-solph `_) and open a new thread if your questions hasn't been already answered. + +Keep in touch! - You can become a watcher at our `github site `_, +but this will bring you quite a few mails and might be more interesting for developers. +If you just want to get the latest news, like when is the next oemof meeting, +you can follow our news-blog at `oemof.org `_. + +Documentation +============= +The `oemof.solph documentation `_ is powered by readthedocs. Use the `project site `_ of oemof.solph to choose the version of the documentation. Go to the `download page `_ to download different versions and formats (pdf, html, epub) of the documentation. + + +.. _installation_label: + +Installation +============ + + +If you have a working Python installation, use pypi to install the latest version of oemof.solph. +Python >= 3.8 is recommended. Lower versions may work but are not tested. + +We highly recommend to use virtual environments. +Please refer to the documentation of your Python distribution (e.g. Anaconda, +Micromamba, or the version of Python that came with your Linux installation) +to learn how to set up and use virtual environments. + +:: + + (venv) pip install oemof.solph + +If you want to use the latest features, you might want to install the **developer version**. The developer version is not recommended for productive use:: + + (venv) pip install https://github.com/oemof/oemof-solph/archive/dev.zip + + +For running an oemof-solph optimisation model, you need to install a solver. +Following you will find guidelines for the installation process for different operating systems. + +.. _windows_solver_label: +.. _linux_solver_label: + +Installing a solver +------------------- + +There are several solvers that can work with oemof, both open source and commercial. +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. + +Check the solver installation by executing the test_installation example below (see section Installation Test). + +**Linux** + +To install the solvers have a look at the package repository of your Linux distribution or search for precompiled packages. GLPK and CBC ares available at Debian, Feodora, Ubuntu and others. + +**Windows** + + 1. Download `CBC `_ + 2. Download `GLPK (64/32 bit) `_ + 3. Unpack CBC/GLPK to any folder (e.g. C:/Users/Somebody/my_programs) + 4. Add the path of the executable files of both solvers to the PATH variable using `this tutorial `_ + 5. Restart Windows + +Check the solver installation by executing the test_installation example (see the `Installation test` section). + + +**Mac OSX** + +Please follow the installation instructions on the respective homepages for details. + +CBC-solver: https://projects.coin-or.org/Cbc + +GLPK-solver: http://arnab-deka.com/posts/2010/02/installing-glpk-on-a-mac/ + +If you install the CBC solver via brew (highly recommended), it should work without additional configuration. + + +**conda** + +Provided you are using a Linux or MacOS, the CBC-solver can also be installed in a `conda` environment. Please note, that it is highly recommended to `use pip after conda `_, so: + +.. code:: console + + (venv) conda install -c conda-forge coincbc + (venv) pip install oemof.solph + + +.. _check_installation_label: + +Installation test +----------------- + +Test the installation and the installed solver by running the installation test +in your virtual environment: + +.. code:: console + + (venv) oemof_installation_test + +If the installation was successful, you will receive something like this: + +.. code:: console + + ********* + Solver installed with oemof: + glpk: working + cplex: not working + cbc: working + gurobi: not working + ********* + oemof.solph successfully installed. + +as an output. + +Contributing +============ + +A warm welcome to all who want to join the developers and contribute to +oemof.solph. + +Information on the details and how to approach us can be found +`in the oemof documentation `_ . + +Citing +====== + +For explicitly citing solph, you might want to refer to +`DOI:10.1016/j.simpa.2020.100028 `_, +which gives an overview over the capabilities of solph. +The core ideas of oemof as a whole are described in +`DOI:10.1016/j.esr.2018.07.001 `_ +(preprint at `arXiv:1808.0807 `_). +To allow citing specific versions, we use the zenodo project to get a DOI for each version. + +Example Applications +==================== + +The combination of specific modules (often including other packages) is called an +application (app). For example, it can depict a concrete energy system model. +You can find a large variety of helpful examples in the documentation. +The examples show the optimisation of different energy systems and are supposed +to help new users to understand the framework's structure. + +You are welcome to contribute your own examples via a `pull request `_ +or by e-mailing us (see `here `_ for contact information). + +License +======= + +Copyright (c) oemof developer group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/usage.rst b/docs/usage.rst index 58247d9d9..ff174706c 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -1,1705 +1,1707 @@ -.. _oemof_solph_label: - -.. _using_oemof_label: - -~~~~~~~~~~~~ -User's guide -~~~~~~~~~~~~ - -Solph is an oemof-package, designed to create and solve linear or mixed-integer linear optimization problems. The package is based on pyomo. To create an energy system model generic and specific components are available. To get started with solph, checkout the examples in the :ref:`examples_label` section. - -This User's guide provides a user-friendly introduction into oemof-solph, -which includes small examples and nice illustrations. -However, the functionalities of oemof-solph go beyond the content of this User's guide section. -So, if you want to know all details of a certain component or a function, -please go the :ref:`api_reference_label`. There, you will find -a detailed and complete description of all oemof-solph modules. - -.. contents:: - :depth: 2 - :local: - :backlinks: top - - -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. -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. - -Once the examples work you are close to your first energy model. - - -Handling of Warnings -^^^^^^^^^^^^^^^^^^^^ - -The solph library is designed to be as generic as possible to make it possible -to use it in different use cases. This concept makes it difficult to raise -Errors or Warnings because sometimes untypical combinations of parameters are -allowed even though they might be wrong in over 99% of the use cases. - -Therefore, a SuspiciousUsageWarning was introduced. This warning will warn you -if you do something untypical. If you are sure that you know what you are doing -you can switch the warning off. - -See `the debugging module of oemof-tools `_ for more -information. - - -Set up an energy system -^^^^^^^^^^^^^^^^^^^^^^^ - -In most cases an EnergySystem object is defined when we start to build up an energy system model. The EnergySystem object will be the main container for the model's elements. - -The model time is defined by the number of intervals and the length of intervals. The length of each interval does not have to be the same. This can be defined in two ways: - -1. Define the length of each interval in an array/Series where the number of the elements is the number of intervals. -2. Define a `pandas.DatetimeIndex` with all time steps that encloses an interval. Be aware that you have to define n+1 time points to get n intervals. For non-leap year with hourly values that means 8761 time points to get 8760 interval e.g. 2018-01-01 00:00 to 2019-01-01 00:00. - -The index will also be used for the results. For a numeric index the resulting time series will indexed with a numeric index starting with 0. - -One can use the function -:py:func:`create_time_index` to create an equidistant datetime index. By default the function creates an hourly index for one year, so online the year has to be passed to the function. But it is also possible to change the length of the interval to quarter hours etc. The default number of intervals is the number needed to cover the given year but the value can be overwritten by the user. - -It is also possible to define the datetime index using pandas. See `pandas date_range guide `_ for more information. - -Both code blocks will create an hourly datetime index for 2011: - -.. code-block:: python - - from oemof.solph import create_time_index - my_index = create_time_index(2011) - -.. code-block:: python - - import pandas as pd - my_index = pd.date_range('1/1/2011', periods=8761, freq='h') - -This index can be used to define the EnergySystem: - -.. code-block:: python - - import oemof.solph as solph - my_energysystem = solph.EnergySystem(timeindex=my_index) - -Now you can start to add the components of the network. - - -Add components to the energy system -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -After defining an instance of the EnergySystem class, in the following you have to add all nodes you define to your EnergySystem. - -Basically, there are two types of *nodes* - *components* and *buses*. Every Component has to be connected with one or more *buses*. The connection between a *component* and a *bus* is the *flow*. - -All solph *components* can be used to set up an energy system model but you should read the documentation of each *component* to learn about usage and restrictions. For example it is not possible to combine every *component* with every *flow*. Furthermore, you can add your own *components* in your application (see below) but we would be pleased to integrate them into solph if they are of general interest (see :ref:`feature_requests_and_feedback`). - -An example of a simple energy system shows the usage of the nodes for -real world representations: - -.. image:: _files/oemof_solph_example.svg - :scale: 70 % - :alt: alternate text - :align: center - -The figure shows a simple energy system using the four basic network classes and the Bus class. -If you remove the transmission line (transport 1 and transport 2) you get two systems but they are still one energy system in terms of solph and will be optimised at once. - -There are different ways to add components to an *energy system*. The following line adds a *bus* object to the *energy system* defined above. - -.. code-block:: python - - my_energysystem.add(solph.buses.Bus()) - -It is also possible to assign the bus to a variable and add it afterwards. In that case it is easy to add as many objects as you like. - -.. code-block:: python - - my_bus1 = solph.buses.Bus() - my_bus2 = solph.buses.Bus() - my_energysystem.add(my_bus1, my_bus2) - -Therefore it is also possible to add lists or dictionaries with components but you have to dissolve them. - -.. code-block:: python - - # add a list - my_energysystem.add(*my_list) - - # add a dictionary - my_energysystem.add(*my_dictionary.values()) - - -Bus -+++ - -All flows into and out of a *bus* are balanced (by default). Therefore an instance of the Bus class represents a grid or network without losses. To define an instance of a Bus only a unique label is necessary. If you do not set a label a random label is used but this makes it difficult to get the results later on. - -To make it easier to connect the bus to a component you can optionally assign a variable for later use. - -.. code-block:: python - - solph.buses.Bus(label='natural_gas') - electricity_bus = solph.buses.Bus(label='electricity') - -.. note:: See the :py:class:`~oemof.solph.buses._bus.Bus` class for all parameters and the mathematical background. - - -Flow -++++ - -The flow class has to be used to connect nodes and buses. An instance of the Flow class is normally used in combination with the definition of a component. -A Flow can be limited by upper and lower bounds (constant or time-dependent) or by summarised limits. -For all parameters see the API documentation of the :py:class:`~oemof.solph.flows._flow.Flow` class or the examples of the nodes below. A basic flow can be defined without any parameter. - -.. code-block:: python - - solph.flows.Flow() - -oemof.solph has different types of *flows* but you should be aware that you cannot connect every *flow* type with every *component*. - -.. note:: See the :py:class:`~oemof.solph.flows._flow.Flow` class for all parameters and the mathematical background. - -Components -++++++++++ - -Components are divided in two categories. Well-tested components (solph.components) and experimental components (solph.components.experimental). The experimental section was created to lower the entry barrier for new components. Be aware that these components might not be properly documented or even sometimes do not even work as intended. Let us know if you have successfully used and tested these components. This is the first step to move them to the regular components section. - -See :ref:`oemof_solph_components_label` for a list of all components. - - -.. _oemof_solph_optimise_es_label: - -Optimise your energy system -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The typical optimisation of an energy system in solph is the dispatch optimisation, which means that the use of the sources is optimised to satisfy the demand at least costs. -Therefore, variable cost can be defined for all components. The cost for gas should be defined in the gas source while the variable costs of the gas power plant are caused by operating material. -The actual fuel cost in turn is calculated in the framework itself considering the efficiency of the power plant. -You can deviate from this scheme but you should keep it consistent to make it understandable for others. - -Costs do not have to be monetary costs but could be emissions or other variable units. - -Furthermore, it is possible to optimise the capacity of different components using the investment mode (see :ref:`investment_mode_label`). - -Since v0.5.1, there also is the possibility to have multi-period (i.e. dynamic) investments over longer-time horizon which is in experimental state (see :ref:`multi_period_mode_label`). - -.. code-block:: python - - # set up a simple least cost optimisation - om = solph.Model(my_energysystem) - - # solve the energy model using the CBC solver - om.solve(solver='cbc', solve_kwargs={'tee': True}) - -If you want to analyse the lp-file to see all equations and bounds you can write the file to you disc. In that case you should reduce the timesteps to 3. This will increase the readability of the file. - -.. code-block:: python - - # set up a simple least cost optimisation - om = solph.Model(my_energysystem) - - # write the lp file for debugging or other reasons - om.write('path/my_model.lp', io_options={'symbolic_solver_labels': True}) - -Analysing your results -^^^^^^^^^^^^^^^^^^^^^^ - -If you want to analyse your results, you should first dump your EnergySystem instance to permanently store results. Otherwise you would have to run the simulation again. - -.. code-block:: python - - my_energysystem.results = processing.results(om) - my_energysystem.dump('my_path', 'my_dump.oemof') - -If you need the meta results of the solver you can do the following: - -.. code-block:: python - - my_energysystem.results['main'] = processing.results(om) - my_energysystem.results['meta'] = processing.meta_results(om) - my_energysystem.dump('my_path', 'my_dump.oemof') - -To restore the dump you can simply create an EnergySystem instance and restore your dump into it. - -.. code-block:: python - - import oemof.solph as solph - my_energysystem = solph.EnergySystem() - my_energysystem.restore('my_path', 'my_dump.oemof') - results = my_energysystem.results - - # If you use meta results do the following instead of the previous line. - results = my_energysystem.results['main'] - meta = my_energysystem.results['meta'] - - -If you call dump/restore without any parameters, the dump will be stored as *'es_dump.oemof'* into the *'.oemof/dumps/'* folder created in your HOME directory. - -See :ref:`oemof_outputlib_label` to learn how to process, plot and analyse the results. - - -.. _oemof_solph_components_label: - -Solph components ----------------- - - * :ref:`oemof_solph_components_sink_label` - * :ref:`oemof_solph_components_source_label` - * :ref:`oemof_solph_components_converter_label` - * :ref:`oemof_solph_components_extraction_turbine_chp_label` - * :ref:`oemof_solph_components_generic_caes_label` - * :ref:`oemof_solph_components_generic_chp_label` - * :ref:`oemof_solph_components_generic_storage_label` - * :ref:`oemof_solph_custom_electrical_line_label` - * :ref:`oemof_solph_custom_link_label` - * :ref:`oemof_solph_custom_sinkdsm_label` - - -.. _oemof_solph_components_sink_label: - -Sink (basic) -^^^^^^^^^^^^ - -A sink is normally used to define the demand within an energy model but it can also be used to detect excesses. - -The example shows the electricity demand of the electricity_bus defined above. -The *'my_demand_series'* should be sequence of normalised valueswhile the *'nominal_value'* is the maximum demand the normalised sequence is multiplied with. -Giving *'my_demand_series'* as parameter *'fix'* means that the demand cannot be changed by the solver. - -.. code-block:: python - - solph.components.Sink(label='electricity_demand', inputs={electricity_bus: solph.flows.Flow( - fix=my_demand_series, nominal_value=nominal_demand)}) - -In contrast to the demand sink the excess sink has normally less restrictions but is open to take the whole excess. - -.. code-block:: python - - solph.components.Sink(label='electricity_excess', inputs={electricity_bus: solph.flows.Flow()}) - -.. note:: The Sink class is only a plug and provides no additional constraints or variables. - - -.. _oemof_solph_components_source_label: - -Source (basic) -^^^^^^^^^^^^^^ - -A source can represent a pv-system, a wind power plant, an import of natural gas or a slack variable to avoid creating an in-feasible model. - -While a wind power plant will have as feed-in depending on the weather conditions the natural_gas import might be restricted by maximum value (*nominal_value*) and an annual limit (*full_load_time_max*). -As we do have to pay for imported gas we should set variable costs. -Comparable to the demand series an *fix* is used to define a fixed the normalised output of a wind power plant. -Alternatively, you might use *max* to allow for easy curtailment. -The *nominal_value* sets the installed capacity. - -.. code-block:: python - - solph.components.Source( - label='import_natural_gas', - outputs={my_energysystem.groups['natural_gas']: solph.flows.Flow( - nominal_value=1000, full_load_time_max=1000000, variable_costs=50)}) - - solph.components.Source(label='wind', outputs={electricity_bus: solph.flows.Flow( - fix=wind_power_feedin_series, nominal_value=1000000)}) - -.. note:: The Source class is only a plug and provides no additional constraints or variables. - -.. _oemof_solph_components_converter_label: - -Converter (basic) -^^^^^^^^^^^^^^^^^ - -An instance of the Converter class can represent a node with multiple input and output flows such as a power plant, a transport line or any kind of a transforming process as electrolysis, a cooling device or a heat pump. -The efficiency has to be constant within one time step to get a linear transformation. -You can define a different efficiency for every time step (e.g. the thermal powerplant efficiency according to the ambient temperature) but this series has to be predefined and cannot be changed within the optimisation. - -A condensing power plant can be defined by a converter with one input (fuel) and one output (electricity). - -.. code-block:: python - - b_gas = solph.buses.Bus(label='natural_gas') - b_el = solph.buses.Bus(label='electricity') - - solph.components.Converter( - label="pp_gas", - inputs={bgas: solph.flows.Flow()}, - outputs={b_el: solph.flows.Flow(nominal_value=10e10)}, - conversion_factors={electricity_bus: 0.58}) - -A CHP power plant would be defined in the same manner but with two outputs: - -.. code-block:: python - - b_gas = solph.buses.Bus(label='natural_gas') - b_el = solph.buses.Bus(label='electricity') - b_th = solph.buses.Bus(label='heat') - - solph.components.Converter( - label='pp_chp', - inputs={b_gas: Flow()}, - outputs={b_el: Flow(nominal_value=30), - b_th: Flow(nominal_value=40)}, - conversion_factors={b_el: 0.3, b_th: 0.4}) - -A CHP power plant with 70% coal and 30% natural gas can be defined with two inputs and two outputs: - -.. code-block:: python - - b_gas = solph.buses.Bus(label='natural_gas') - b_coal = solph.buses.Bus(label='hard_coal') - b_el = solph.buses.Bus(label='electricity') - b_th = solph.buses.Bus(label='heat') - - solph.components.Converter( - label='pp_chp', - inputs={b_gas: Flow(), b_coal: Flow()}, - outputs={b_el: Flow(nominal_value=30), - b_th: Flow(nominal_value=40)}, - conversion_factors={b_el: 0.3, b_th: 0.4, - b_coal: 0.7, b_gas: 0.3}) - -A heat pump would be defined in the same manner. New buses are defined to make the code cleaner: - -.. code-block:: python - - b_el = solph.buses.Bus(label='electricity') - b_th_low = solph.buses.Bus(label='low_temp_heat') - b_th_high = solph.buses.Bus(label='high_temp_heat') - - # The cop (coefficient of performance) of the heat pump can be defined as - # a scalar or a sequence. - cop = 3 - - solph.components.Converter( - label='heat_pump', - inputs={b_el: Flow(), b_th_low: Flow()}, - outputs={b_th_high: Flow()}, - conversion_factors={b_el: 1/cop, - b_th_low: (cop-1)/cop}) - -If the low-temperature reservoir is nearly infinite (ambient air heat pump) the -low temperature bus is not needed and, therefore, a Converter with one input -is sufficient. - -.. note:: See the :py:class:`~oemof.solph.components.converter.Converter` class for all parameters and the mathematical background. - -.. _oemof_solph_components_extraction_turbine_chp_label: - -ExtractionTurbineCHP (component) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The :py:class:`~oemof.solph.components._extraction_turbine_chp.ExtractionTurbineCHP` -inherits from the :ref:`oemof_solph_components_converter_label` class. Like the name indicates, -the application example for the component is a flexible combined heat and power -(chp) plant. Of course, an instance of this class can represent also another -component with one input and two output flows and a flexible ratio between -these flows, with the following constraints: - -.. include:: ../src/oemof/solph/components/_extraction_turbine_chp.py - :start-after: _ETCHP-equations: - :end-before: """ - -These constraints are applied in addition to those of a standard -:class:`~oemof.solph.components.Converter`. The constraints limit the range of -the possible operation points, like the following picture shows. For a certain -flow of fuel, there is a line of operation points, whose slope is defined by -the power loss factor :math:`\beta` (in some contexts also referred to as -:math:`C_v`). The second constraint limits the decrease of electrical power and -incorporates the backpressure coefficient :math:`C_b`. - -.. image:: _files/ExtractionTurbine_range_of_operation.svg - :width: 70 % - :alt: variable_chp_plot.svg - :align: center - -For now, :py:class:`~oemof.solph.components._extraction_turbine_chp.ExtractionTurbineCHP` instances must -have one input and two output flows. The class allows the definition -of a different efficiency for every time step that can be passed as a series -of parameters that are fixed before the optimisation. In contrast to the -:py:class:`~oemof.solph.components.Converter`, a main flow and a tapped flow is -defined. For the main flow you can define a separate conversion factor that -applies when the second flow is zero (*`conversion_factor_full_condensation`*). - -.. code-block:: python - - solph.components._extractionTurbineCHP( - label='variable_chp_gas', - inputs={b_gas: solph.flows.Flow(nominal_value=10e10)}, - outputs={b_el: solph.flows.Flow(), b_th: solph.flows.Flow()}, - conversion_factors={b_el: 0.3, b_th: 0.5}, - conversion_factor_full_condensation={b_el: 0.5}) - -The key of the parameter *'conversion_factor_full_condensation'* defines which -of the two flows is the main flow. In the example above, the flow to the Bus -*'b_el'* is the main flow and the flow to the Bus *'b_th'* is the tapped flow. -The following plot shows how the variable chp (right) schedules it's electrical -and thermal power production in contrast to a fixed chp (left). The plot is the -output of an example in the `example directory -`_. - -.. image:: _files/variable_chp_plot.svg - :scale: 10 % - :alt: variable_chp_plot.svg - :align: center - -.. note:: See the :py:class:`~oemof.solph.components._extraction_turbine_chp.ExtractionTurbineCHP` class for all parameters and the mathematical background. - - -.. _oemof_solph_components_generic_chp_label: - -GenericCHP (component) -^^^^^^^^^^^^^^^^^^^^^^ - -With the GenericCHP class it is possible to model different types of CHP plants (combined cycle extraction turbines, -back pressure turbines and motoric CHP), which use different ranges of operation, as shown in the figure below. - -.. image:: _files/GenericCHP.svg - :scale: 70 % - :alt: scheme of GenericCHP operation range - :align: center - -Combined cycle extraction turbines: The minimal and maximal electric power without district heating -(red dots in the figure) define maximum load and minimum load of the plant. Beta defines electrical power loss through -heat extraction. The minimal thermal condenser load to cooling water and the share of flue gas losses -at maximal heat extraction determine the right boundary of the operation range. - -.. code-block:: python - - solph.components.GenericCHP( - label='combined_cycle_extraction_turbine', - fuel_input={bgas: solph.flows.Flow( - H_L_FG_share_max=[0.19 for p in range(0, periods)])}, - electrical_output={bel: solph.flows.Flow( - P_max_woDH=[200 for p in range(0, periods)], - P_min_woDH=[80 for p in range(0, periods)], - Eta_el_max_woDH=[0.53 for p in range(0, periods)], - Eta_el_min_woDH=[0.43 for p in range(0, periods)])}, - heat_output={bth: solph.flows.Flow( - Q_CW_min=[30 for p in range(0, periods)])}, - Beta=[0.19 for p in range(0, periods)], - back_pressure=False) - -For modeling a back pressure CHP, the attribute `back_pressure` has to be set to True. -The ratio of power and heat production in a back pressure plant is fixed, therefore the operation range -is just a line (see figure). Again, the `P_min_woDH` and `P_max_woDH`, the efficiencies at these points and the share of flue -gas losses at maximal heat extraction have to be specified. In this case “without district heating” is not to be taken -literally since an operation without heat production is not possible. It is advised to set `Beta` to zero, so the minimal and -maximal electric power without district heating are the same as in the operation point (see figure). The minimal -thermal condenser load to cooling water has to be zero, because there is no condenser besides the district heating unit. - - -.. code-block:: python - - solph.components.GenericCHP( - label='back_pressure_turbine', - fuel_input={bgas: solph.flows.Flow( - H_L_FG_share_max=[0.19 for p in range(0, periods)])}, - electrical_output={bel: solph.flows.Flow( - P_max_woDH=[200 for p in range(0, periods)], - P_min_woDH=[80 for p in range(0, periods)], - Eta_el_max_woDH=[0.53 for p in range(0, periods)], - Eta_el_min_woDH=[0.43 for p in range(0, periods)])}, - heat_output={bth: solph.flows.Flow( - Q_CW_min=[0 for p in range(0, periods)])}, - Beta=[0 for p in range(0, periods)], - back_pressure=True) - -A motoric chp has no condenser, so `Q_CW_min` is zero. Electrical power does not depend on the amount of heat used -so `Beta` is zero. The minimal and maximal electric power (without district heating) and the efficiencies at these -points are needed, whereas the use of electrical power without using thermal energy is not possible. -With `Beta=0` there is no difference between these points and the electrical output in the operation range. -As a consequence of the functionality of a motoric CHP, share of flue gas losses at maximal heat extraction but also -at minimal heat extraction have to be specified. - - -.. code-block:: python - - solph.components.GenericCHP( - label='motoric_chp', - fuel_input={bgas: solph.flows.Flow( - H_L_FG_share_max=[0.18 for p in range(0, periods)], - H_L_FG_share_min=[0.41 for p in range(0, periods)])}, - electrical_output={bel: solph.flows.Flow( - P_max_woDH=[200 for p in range(0, periods)], - P_min_woDH=[100 for p in range(0, periods)], - Eta_el_max_woDH=[0.44 for p in range(0, periods)], - Eta_el_min_woDH=[0.40 for p in range(0, periods)])}, - heat_output={bth: solph.flows.Flow( - Q_CW_min=[0 for p in range(0, periods)])}, - Beta=[0 for p in range(0, periods)], - back_pressure=False) - -Modeling different types of plants means telling the component to use different constraints. Constraint 1 to 9 -are active in all three cases. Constraint 10 depends on the attribute back_pressure. If true, the constraint is -an equality, if not it is a less or equal. Constraint 11 is only needed for modeling motoric CHP which is done by -setting the attribute `H_L_FG_share_min`. - -.. include:: ../src/oemof/solph/components/_generic_chp.py - :start-after: _GenericCHP-equations1-10: - :end-before: **For the attribute** - -If :math:`\dot{H}_{L,FG,min}` is given, e.g. for a motoric CHP: - -.. include:: ../src/oemof/solph/components/_generic_chp.py - :start-after: _GenericCHP-equations11: - :end-before: """ - -.. note:: See the :py:class:`~oemof.solph.components._generic_chp.GenericCHP` class for all parameters and the mathematical background. - - -.. _oemof_solph_components_generic_storage_label: - -GenericStorage (component) -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -A component to model a storage with its basic characteristics. The -GenericStorage is designed for one input and one output. -The ``nominal_storage_capacity`` of the storage signifies the storage capacity. You can either set it to the net capacity or to the gross capacity and limit it using the min/max attribute. -To limit the input and output flows, you can define the ``nominal_value`` in the Flow objects. -Furthermore, an efficiency for loading, unloading and a loss rate can be defined. - -.. code-block:: python - - solph.components.GenericStorage( - label='storage', - inputs={b_el: solph.flows.Flow(nominal_value=9, variable_costs=10)}, - outputs={b_el: solph.flows.Flow(nominal_value=25, variable_costs=10)}, - loss_rate=0.001, nominal_storage_capacity=50, - inflow_conversion_factor=0.98, outflow_conversion_factor=0.8) - -For initialising the state of charge before the first time step (time step zero) the parameter ``initial_storage_level`` (default value: ``None``) can be set by a numeric value as fraction of the storage capacity. -Additionally the parameter ``balanced`` (default value: ``True``) sets the relation of the state of charge of time step zero and the last time step. -If ``balanced=True``, the state of charge in the last time step is equal to initial value in time step zero. -Use ``balanced=False`` with caution as energy might be added to or taken from the energy system due to different states of charge in time step zero and the last time step. -Generally, with these two parameters four configurations are possible, which might result in different solutions of the same optimization model: - - * ``initial_storage_level=None``, ``balanced=True`` (default setting): The state of charge in time step zero is a result of the optimization. The state of charge of the last time step is equal to time step zero. Thus, the storage is not violating the energy conservation by adding or taking energy from the system due to different states of charge at the beginning and at the end of the optimization period. - * ``initial_storage_level=0.5``, ``balanced=True``: The state of charge in time step zero is fixed to 0.5 (50 % charged). The state of charge in the last time step is also constrained by 0.5 due to the coupling parameter ``balanced`` set to ``True``. - * ``initial_storage_level=None``, ``balanced=False``: Both, the state of charge in time step zero and the last time step are a result of the optimization and not coupled. - * ``initial_storage_level=0.5``, ``balanced=False``: The state of charge in time step zero is constrained by a given value. The state of charge of the last time step is a result of the optimization. - -The following code block shows an example of the storage parametrization for the second configuration: - -.. code-block:: python - - solph.components.GenericStorage( - label='storage', - inputs={b_el: solph.flows.Flow(nominal_value=9, variable_costs=10)}, - outputs={b_el: solph.flows.Flow(nominal_value=25, variable_costs=10)}, - loss_rate=0.001, nominal_storage_capacity=50, - initial_storage_level=0.5, balanced=True, - inflow_conversion_factor=0.98, outflow_conversion_factor=0.8) - -If you want to view the temporal course of the state of charge of your storage -after the optimisation, you need to check the ``storage_content`` in the results: - -.. code-block:: python - - from oemof.solph import processing, views - results = processing.results(om) - column_name = (('your_storage_label', 'None'), 'storage_content') - SC = views.node(results, 'your_storage_label')['sequences'][column_name] - -The ``storage_content`` is the absolute value of the current stored energy. -By calling: - -.. code-block:: python - - views.node(results, 'your_storage_label')['scalars'] - -you get the results of the scalar values of your storage, e.g. the initial -storage content before time step zero (``init_content``). - -For more information see the definition of the :py:class:`~oemof.solph.components._generic_storage.GenericStorage` class or check the :ref:`examples_label`. - - -Using an investment object with the GenericStorage component -+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -Based on the `GenericStorage` object the `GenericInvestmentStorageBlock` adds two main investment possibilities. - - * Invest into the flow parameters e.g. a turbine or a pump - * Invest into capacity of the storage e.g. a basin or a battery cell - -Investment in this context refers to the value of the variable for the 'nominal_value' (installed capacity) in the investment mode. - -As an addition to other flow-investments, the storage class implements the possibility to couple or decouple the flows -with the capacity of the storage. -Three parameters are responsible for connecting the flows and the capacity of the storage: - - * ``invest_relation_input_capacity`` fixes the input flow investment to the capacity investment. A ratio of 1 means that the storage can be filled within one time-period. - * ``invest_relation_output_capacity`` fixes the output flow investment to the capacity investment. A ratio of 1 means that the storage can be emptied within one period. - * ``invest_relation_input_output`` fixes the input flow investment to the output flow investment. For values <1, the input will be smaller and for values >1 the input flow will be larger. - -You should not set all 3 parameters at the same time, since it will lead to overdetermination. - -The following example pictures a Pumped Hydroelectric Energy Storage (PHES). Both flows and the storage itself (representing: pump, turbine, basin) are free in their investment. You can set the parameters to `None` or delete them as `None` is the default value. - -.. code-block:: python - - solph.components.GenericStorage( - label='PHES', - inputs={b_el: solph.flows.Flow(nominal_value=solph.Investment(ep_costs=500))}, - outputs={b_el: solph.flows.Flow(nominal_value=solph.Investment(ep_costs=500)}, - loss_rate=0.001, - inflow_conversion_factor=0.98, outflow_conversion_factor=0.8), - investment = solph.Investment(ep_costs=40)) - -The following example describes a battery with flows coupled to the capacity of the storage. - -.. code-block:: python - - solph.components.GenericStorage( - label='battery', - inputs={b_el: solph.flows.Flow()}, - outputs={b_el: solph.flows.Flow()}, - loss_rate=0.001, - inflow_conversion_factor=0.98, - outflow_conversion_factor=0.8, - invest_relation_input_capacity = 1/6, - invest_relation_output_capacity = 1/6, - investment = solph.Investment(ep_costs=400)) - - -.. note:: See the :py:class:`~oemof.solph.components._generic_storage.GenericStorage` class for all parameters and the mathematical background. - - -.. _oemof_solph_custom_link_label: - -Link -^^^^ - -The `Link` allows to model connections between two busses, e.g. modeling the transshipment of electric energy between two regions. - -.. note:: See the :py:class:`~oemof.solph.components.experimental._link.Link` class for all parameters and the mathematical background. - - - -OffsetConverter (component) -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The `OffsetConverter` object makes it possible to create a Converter with efficiencies depending on the part load condition. -For this it is necessary to define one flow as a nonconvex flow and to set a minimum load. -The following example illustrates how to define an OffsetConverter for given -information for an output, i.e. a combined heat and power plant. The plant -generates up to 100 kW electric energy at an efficiency of 40 %. In minimal -load the electric efficiency is at 30 %, and the minimum possible load is 50 % -of the nominal load. At the same time, heat is produced with a constant -efficiency. By using the `OffsetConverter` a linear relation of in- and output -power with a power dependent efficiency is generated. - -.. code-block:: python - - >>> from oemof import solph - - >>> eta_el_min = 0.3 # electrical efficiency at minimal operation point - >>> eta_el_max = 0.4 # electrical efficiency at nominal operation point - >>> eta_th_min = 0.5 # thermal efficiency at minimal operation point - >>> eta_th_max = 0.5 # thermal efficiency at nominal operation point - >>> P_out_min = 20 # absolute minimal output power - >>> P_out_max = 100 # absolute nominal output power - -As reference for our system we use the input and will mark that flow as -nonconvex respectively. The efficiencies for electricity and heat output have -therefore to be defined with respect to the input flow. The same is true for -the minimal and maximal load. Therefore, we first calculate the minimum and -maximum input of fuel and then derive the slope and offset for both outputs. - -.. code-block:: python - - >>> P_in_max = P_out_max / eta_el_max - >>> P_in_min = P_out_min / eta_el_min - >>> P_in_max - 250.0 - >>> round(P_in_min, 2) - 66.67 - -With that information, we can derive the normed minimal and maximal load of the -nonconvex flow, and calculate the slope and the offset for both outputs. Note, -that the offset for the heat output is 0, because the thermal heat output -efficiency is constant. - -.. code-block:: python - - >>> l_max = 1 - >>> l_min = P_in_min / P_in_max - >>> slope_el, offset_el = solph.components.slope_offset_from_nonconvex_input( - ... l_max, l_min, eta_el_max, eta_el_min - ... ) - >>> slope_th, offset_th = solph.components.slope_offset_from_nonconvex_input( - ... l_max, l_min, eta_th_max, eta_th_min - ... ) - >>> round(slope_el, 3) - 0.436 - >>> round(offset_el, 3) - -0.036 - >>> round(slope_th, 3) - 0.5 - >>> round(offset_th, 3) - 0.0 - -Then we can create our component with the buses attached to it. - -.. code-block:: python - - >>> bfuel = solph.Bus("fuel") - >>> bel = solph.Bus("electricity") - >>> bth = solph.Bus("heat") - - # define OffsetConverter - >>> diesel_genset = solph.components.OffsetConverter( - ... label='boiler', - ... inputs={ - ... bfuel: solph.flows.Flow( - ... nominal_value=P_out_max, - ... max=l_max, - ... min=l_min, - ... nonconvex=solph.NonConvex() - ... ) - ... }, - ... outputs={ - ... bel: solph.flows.Flow(), - ... bth: solph.flows.Flow(), - ... }, - ... conversion_factors={bel: slope_el, bth: slope_th}, - ... normed_offsets={bel: offset_el, bth: offset_th}, - ... ) - -.. note:: - - One of the inputs and outputs has to be a `NonConvex` flow and this flow - will serve as the reference for the `conversion_factors` and the - `normed_offsets`. The `NonConvex` flow also holds - - - the `nominal_value` (or `Investment` in case of investment optimization), - - the `min` and - - the `max` attributes. - - The `conversion_factors` and `normed_offsets` are specified similar to the - `Converter` API with dictionaries referencing the respective input and - output buses. Note, that you cannot have the `conversion_factors` or - `normed_offsets` point to the `NonConvex` flow. - -The following figures show the power at the electrical and the thermal output -and the resepctive ratios to the nonconvex flow (normalized). The efficiency -becomes non-linear. - -.. image:: _files/OffsetConverter_relations_1.svg - :width: 70 % - :alt: OffsetConverter_relations_1.svg - :align: center - - -.. image:: _files/OffsetConverter_relations_2.svg - :width: 70 % - :alt: OffsetConverter_relations_2.svg - :align: center - -.. math:: - - \eta = P(t) / P_\text{ref}(t) - -It also becomes clear, why the component has been named `OffsetConverter`. The -linear equation of inflow to electrical outflow does not hit the origin, but is -offset. By multiplying the offset :math:`y_\text{0,normed}` with the binary -status variable of the `NonConvex` flow, the origin (0, 0) becomes part of the -solution space and the boiler is allowed to switch off. - -.. include:: ../src/oemof/solph/components/_offset_converter.py - :start-after: _OffsetConverter-equations: - :end-before: """ - -The parameters :math:`y_\text{0,normed}` and :math:`m` can be given by scalars or by series in order to define a different efficiency equation for every timestep. -It is also possible to define multiple outputs. - -.. note:: See the :py:class:`~oemof.solph.components._offset_converter.OffsetConverter` class for all parameters and the mathematical background. - - -.. _oemof_solph_custom_electrical_line_label: - -ElectricalLine (experimental) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Electrical line. - -.. note:: See the :py:class:`~oemof.solph.flows.experimental._electrical_line.ElectricalLine` class for all parameters and the mathematical background. - - -.. _oemof_solph_components_generic_caes_label: - -GenericCAES (experimental) -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Compressed Air Energy Storage (CAES). -The following constraints describe the CAES: - -.. include:: ../src/oemof/solph/components/experimental/_generic_caes.py - :start-after: _GenericCAES-equations: - :end-before: """ - -.. note:: See the :py:class:`~oemof.solph.components.experimental._generic_caes.GenericCAES` class for all parameters and the mathematical background. - - -.. _oemof_solph_custom_sinkdsm_label: - -SinkDSM (experimental) -^^^^^^^^^^^^^^^^^^^^^^ - -:class:`~oemof.solph.custom.sink_dsm.SinkDSM` can used to represent flexibility in a demand time series. -It can represent both, load shifting or load shedding. -For load shifting, elasticity of the demand is described by upper (`~oemof.solph.custom.sink_dsm.SinkDSM.capacity_up`) and lower (`~oemof.solph.custom.SinkDSM.capacity_down`) bounds where within the demand is allowed to vary. -Upwards shifted demand is then balanced with downwards shifted demand. -For load shedding, shedding capability is described by `~oemof.solph.custom.SinkDSM.capacity_down`. -It both, load shifting and load shedding are allowed, `~oemof.solph.custom.SinkDSM.capacity_down` limits the sum of both downshift categories. - -:class:`~oemof.solph.custom.sink_dsm.SinkDSM` provides three approaches how the Demand-Side Management (DSM) flexibility is represented in constraints -It can be used for both, dispatch and investments modeling. - -* "DLR": Implementation of the DSM modeling approach from by Gils (2015): `Balancing of Intermittent Renewable Power Generation by Demand Response and Thermal Energy Storage, Stuttgart, `_, - Details: :class:`~oemof.solph.custom.sink_dsm.SinkDSMDLRBlock` and :class:`~oemof.solph.custom.sink_dsm.SinkDSMDLRInvestmentBlock` -* "DIW": Implementation of the DSM modeling approach by Zerrahn & Schill (2015): `On the representation of demand-side management in power system models `_, - in: Energy (84), pp. 840-845, 10.1016/j.energy.2015.03.037. Details: :class:`~oemof.solph.custom.sink_dsm.SinkDSMDIWBlock` and :class:`~oemof.solph.custom.sink_dsm.SinkDSMDIWInvestmentBlock` -* "oemof": Is a fairly simple approach. Within a defined windows of time steps, demand can be shifted within the defined bounds of elasticity. - The window sequentially moves forwards. Details: :class:`~oemof.solph.custom.sink_dsm.SinkDSMOemofBlock` and :class:`~oemof.solph.custom.sink_dsm.SinkDSMOemofInvestmentBlock` - -Cost can be associated to either demand up shifts or demand down shifts or both. - -This small example of PV, grid and SinkDSM shows how to use the component - -.. code-block:: python - - # Create some data - pv_day = [(-(1 / 6 * x ** 2) + 6) / 6 for x in range(-6, 7)] - pv_ts = [0] * 6 + pv_day + [0] * 6 - data_dict = {"demand_el": [3] * len(pv_ts), - "pv": pv_ts, - "Cap_up": [0.5] * len(pv_ts), - "Cap_do": [0.5] * len(pv_ts)} - data = pd.DataFrame.from_dict(data_dict) - - # Do timestamp stuff - datetimeindex = pd.date_range(start='1/1/2013', periods=len(data.index), freq='h') - data['timestamp'] = datetimeindex - data.set_index('timestamp', inplace=True) - - # Create Energy System - es = solph.EnergySystem(timeindex=datetimeindex) - - # Create bus representing electricity grid - b_elec = solph.buses.Bus(label='Electricity bus') - es.add(b_elec) - - # Create a back supply - grid = solph.components.Source(label='Grid', - outputs={ - b_elec: solph.flows.Flow( - nominal_value=10000, - variable_costs=50)} - ) - es.add(grid) - - # PV supply from time series - s_wind = solph.components.Source(label='wind', - outputs={ - b_elec: solph.flows.Flow( - fix=data['pv'], - nominal_value=3.5)} - ) - es.add(s_wind) - - # Create DSM Sink - demand_dsm = solph.custom.SinkDSM(label="DSM", - inputs={b_elec: solph.flows.Flow()}, - demand=data['demand_el'], - capacity_up=data["Cap_up"], - capacity_down=data["Cap_do"], - delay_time=6, - max_demand=1, - max_capacity_up=1, - max_capacity_down=1, - approach="DIW", - cost_dsm_down=5) - es.add(demand_dsm) - -Yielding the following results - -.. image:: _files/Plot_delay_2013-01-01.svg - :width: 85 % - :alt: Plot_delay_2013-01-01.svg - :align: center - - -.. note:: - * Keyword argument `method` from v0.4.1 has been renamed to `approach` in v0.4.2 and methods have been renamed. - * The parameters `demand`, `capacity_up` and `capacity_down` have been normalized to allow investments modeling. To retreive the original dispatch behaviour from v0.4.1, set `max_demand=1`, `max_capacity_up=1`, `max_capacity_down=1`. - * This component is a candidate component. It's implemented as a custom component for users that like to use and test the component at early stage. Please report issues to improve the component. - * See the :py:class:`~oemof.solph.custom.sink_dsm.SinkDSM` class for all parameters and the mathematical background. - - -.. _investment_mode_label: - -Investment optimisation -------------------------- - -As described in :ref:`oemof_solph_optimise_es_label` the typical way to optimise an energy system is the dispatch optimisation based on marginal costs. Solph also provides a combined dispatch and investment optimisation. -This standard investment mode is limited to one period where all investments happen at the start of the optimization time frame. If you want to optimize longer-term horizons and allow investments at the beginning -of each of multiple periods, also taking into account units lifetimes, you can try the :ref:`multi_period_mode_label`. Please be aware that the multi-period feature is experimental. If you experience any bugs or unexpected -behaviour, please report them. - -In the standard investment mode, based on investment costs you can compare the usage of existing components against building up new capacity. -The annual savings by building up new capacity must therefore compensate the annuity of the investment costs (the time period does not have to be one year, but depends on your Datetime index). - -See the API of the :py:class:`~oemof.solph.options.Investment` class to see all possible parameters. - -Basically, an instance of the Investment class can be added to a Flow, a -Storage or a DSM Sink. All parameters that usually refer to the *nominal_value/capacity* will -now refer to the investment variables and existing capacity. It is also -possible to set a maximum limit for the capacity that can be build. -If existing capacity is considered for a component with investment mode enabled, -the *ep_costs* still apply only to the newly built capacity, i.e. the existing capacity -comes at no costs. - -The investment object can be used in Flows and some components. See the -:ref:`oemof_solph_components_label` section for detailed information of each -component. Besides the flows, it can be invested into - -* :ref:`oemof_solph_components_generic_storage_label` and -* :ref:`oemof_solph_custom_sinkdsm_label` - -For example if you want to find out what would be the optimal capacity of a wind -power plant to decrease the costs of an existing energy system, you can define -this model and add an investment source. -The *wind_power_time_series* has to be a normalised feed-in time series of you -wind power plant. The maximum value might be caused by limited space for wind -turbines. - -.. code-block:: python - - solph.components.Source(label='new_wind_pp', outputs={electricity: solph.flows.Flow( - fix=wind_power_time_series, - nominal_value=solph.Investment(ep_costs=epc, maximum=50000))}) - -Let's slightly alter the case and consider for already existing wind power -capacity of 20,000 kW. We're still expecting the total wind power capacity, thus we -allow for 30,000 kW of new installations and formulate as follows. - -.. code-block:: python - - solph.components.Source(label='new_wind_pp', outputs={electricity: solph.flows.Flow( - fix=wind_power_time_series, - nominal_value=solph.Investment(ep_costs=epc, - maximum=30000, - existing=20000))}) - -The periodical costs (*ep_costs*) are typically calculated as annuities, i.e. as follows: - -.. code-block:: python - - capex = 1000 # investment cost - lifetime = 20 # life expectancy - wacc = 0.05 # weighted average of capital cost - epc = capex * (wacc * (1 + wacc) ** lifetime) / ((1 + wacc) ** lifetime - 1) - -This also implemented in the annuity function of the economics module in the oemof.tools package. The code above would look like this: - -.. code-block:: python - - from oemof.tools import economics - epc = economics.annuity(1000, 20, 0.05) - -So far, the investment costs and the installed capacity are mathematically a -line through origin. But what if there is a minimum threshold for doing an -investment, e.g. you cannot buy gas turbines lower than a certain -nominal power, or, the marginal costs of bigger plants -decrease. -Therefore, you can use the parameter *nonconvex* and *offset* of the -investment class. Both, work with investment in flows and storages. Here is an -example of a converter: - -.. code-block:: python - - trafo = solph.components.Converter( - label='converter_nonconvex', - inputs={bus_0: solph.flows.Flow()}, - outputs={bus_1: solph.flows.Flow( - nominal_value=solph.Investment( - ep_costs=4, - maximum=100, - minimum=20, - nonconvex=True, - offset=400))}, - conversion_factors={bus_1: 0.9}) - -In this examples, it is assumed, that independent of the size of the -converter, there are always fix investment costs of 400 (€). -The minimum investment size is 20 (kW) -and the costs per installed unit are 4 (€/kW). With this -option, you could theoretically approximate every cost function you want. But -be aware that for every nonconvex investment flow or storage you are using, -an additional binary variable is created. This might boost your computing time -into the limitless. - -The following figures illustrates the use of the nonconvex investment flow. -Here, :math:`c_{invest,fix}` is the *offset* value and :math:`c_{invest,var}` is -the *ep_costs* value: - -.. image:: _files/nonconvex_invest_investcosts_power.svg - :width: 70 % - :alt: nonconvex_invest_investcosts_power.svg - :align: center - -In case of a convex investment (which is the default setting -`nonconvex=False`), the *minimum* attribute leads to a forced investment, -whereas in the nonconvex case, the investment can become zero as well. - -The calculation of the specific costs per kilowatt installed capacity results -in the following relation for convex and nonconvex investments: - -.. image:: _files/nonconvex_invest_specific_costs.svg - :width: 70 % - :alt: nonconvex_invest_specific_costs.svg - :align: center - -See :py:class:`~oemof.solph.blocks.investment_flow.InvestmentFlow` and -:py:class:`~oemof.solph.components._generic_storage.GenericInvestmentStorageBlock` for all the -mathematical background, like variables and constraints, which are used. - -.. note:: At the moment the investment class is not compatible with the MIP classes :py:class:`~oemof.solph.options.NonConvex`. - - -.. _multi_period_mode_label: - -Multi-period (investment) mode (experimental) ---------------------------------------------- -Sometimes you might be interested in how energy systems could evolve in the longer-term, e.g. until 2045 or 2050 to meet some -carbon neutrality and climate protection or RES and energy efficiency targets. - -While in principle, you could try to model this in oemof.solph using the standard investment mode described above (see :ref:`investment_mode_label`), -you would make the implicit assumption that your entire system is built at the start of your optimization and doesn't change over time. -To address this shortcoming, the multi-period (investment) feature has been introduced. Be aware that it is still experimental. -So feel free to report any bugs or unexpected behaviour if you come across them. - -While in principle, you can define a dispatch-only multi-period system, this doesn't make much sense. The power of the multi-period feature -only unfolds if you look at long-term investments. Let's see how. - -First, you start by defining your energy system as you might have done before, but you - -* choose a longer-term time horizon (spanning multiple years, i.e. multiple periods) and -* explicitly define the `periods` attribute of your energy system which lists the time steps for each period. - -.. code-block:: python - - import pandas as pd - import oemof.solph as solph - - my_index = pd.date_range('1/1/2013', periods=17520, freq='h') - periods = [ - pd.date_range('1/1/2013', periods=8760, freq='h'), - pd.date_range('1/1/2014', periods=8760, freq='h'), - ] - my_energysystem = solph.EnergySystem(timeindex=my_index, periods=periods) - -If you want to use a multi-period model you have define periods of your energy system explicitly. This way, -you are forced to critically think, e.g. about handling leap years, and take some design decisions. It is possible to -define periods with different lengths, but remember that decommissioning of components is possible only at the -beginning of each period. This means that if the life of a component is just a little longer, it will remain for the -entire next period. This can have a particularly large impact the longer your periods are. - -To assist you, here is a plain python snippet that includes leap years which you can just copy -and adjust to your needs: - -.. code-block:: python - - def determine_periods(datetimeindex): - """Explicitly define and return periods of the energy system - - Leap years have 8784 hourly time steps, regular years 8760. - - Parameters - ---------- - datetimeindex : pd.date_range - DatetimeIndex of the model comprising all time steps - - Returns - ------- - periods : list - periods for the optimization run - """ - years = sorted(list(set(getattr(datetimeindex, "year")))) - periods = [] - filter_series = datetimeindex.to_series() - for number, year in enumerate(years): - start = filter_series.loc[filter_series.index.year == year].min() - end = filter_series.loc[filter_series.index.year == year].max() - periods.append(pd.date_range(start, end, freq=datetimeindex.freq)) - - return periods - -So if you want to use this, the above would simplify to: - -.. code-block:: python - - import pandas as pd - import oemof.solph as solph - - # Define your method (or import it from somewhere else) - def determine_periods(datetimeindex): - ... - - my_index = pd.date_range('1/1/2013', periods=17520, freq='h') - periods = determine_periods(my_index) # Make use of method - my_energysystem = solph.EnergySystem(timeindex=my_index, periods=periods) - - -Then you add all the *components* and *buses* to your energy system, just as you are used to with, but with few additions. - -.. code-block:: python - - hydrogen_bus = solph.buses.Bus(label="hydrogen") - coal_bus = solph.buses.Bus(label="coal") - electricity_bus = solph.buses.Bus(label="electricity") - - hydrogen_source = solph.components.Source( - label="green_hydrogen", - outputs={ - hydrogen_bus: solph.flows.Flow( - variable_costs=[25] * 8760 + [30] * 8760 - ) - }, - ) - - coal_source = solph.components.Source( - label="hardcoal", - outputs={ - coal_bus: solph.flows.Flow(variable_costs=[20] * 8760 + [24] * 8760) - }, - ) - - electrical_sink = solph.components.Sink( - label="electricity_demand", - inputs={ - electricity_bus: solph.flows.Flow( - nominal_value=1000, fix=[0.8] * len(my_index) - ) - }, - ) - -So defining buses is the same as for standard models. Also defining components that do not have any investments associated with -them or any lifetime limitations is the same. - -Now if you want to have components that can be invested into, you use the investment option, just as in :ref:`investment_mode_label`, -but with a few minor additions and modifications in the investment object itself which you specify by additional attributes: - -* You have to specify a `lifetime` attribute. This is the components assumed technical lifetime in years. If it is 20 years, - the model invests into it and your simulation has a 30 years horizon, the plant will be decommissioned. Now the model is - free to reinvest or choose another option to fill up the missing capacity. -* You can define an initial `age` if you have `existing` capacity. If you do not specify anything, the default value 0 will be used, - meaning your `existing` capacity has just been newly invested. -* You can define an `interest_rate` that the investor you model has, i.e. the return he desires expressed as the weighted - average osts of capital (wacc) and used for calculating annuities in the model itself. -* You also can define `fixed_costs`, i.e. costs that occur every period independent of the plants usage. - -Here is an example - -.. code-block:: python - - hydrogen_power_plant = solph.components.Converter( - label="hydrogen_pp", - inputs={hydrogen_bus: solph.flows.Flow()}, - outputs={ - electricity_bus: solph.flows.Flow( - nominal_value=solph.Investment( - maximum=1000, - ep_costs=1e6, - lifetime=30, - interest_rate=0.06, - fixed_costs=100, - ), - variable_costs=3, - ) - }, - conversion_factors={electricity_bus: 0.6}, - ) - -.. warning:: - - The `ep_costs` attribute for investments is used in a different way in a multi-period model. Instead - of periodical costs, it depicts (nominal or real) investment expenses, so actual Euros you have to pay per kW or MW - (or whatever power or energy unit) installed. Also, you can depict a change in investment expenses over time, - so instead of providing a scalar value, you could define a list with investment expenses with one value for each period modelled. - - Annuities are calculated within the model. You do not have to do that. - Also the model takes care of discounting future expenses / cashflows. - -Below is what it would look like if you altered `ep_costs` and `fixed_costs` per period. This can be done by simply -providing a list. Note that the length of the list must equal the number of periods of your model. -This would mean that for investments in the particular period, these values would be the one that are applied over their lifetime. - -.. code-block:: python - - hydrogen_power_plant = solph.components.Converter( - label="hydrogen_pp", - inputs={hydrogen_bus: solph.flows.Flow()}, - outputs={ - electricity_bus: solph.flows.Flow( - nominal_value=solph.Investment( - maximum=1000, - ep_costs=[1e6, 1.1e6], - lifetime=30, - interest_rate=0.06, - fixed_costs=[100, 110], - ), - variable_costs=3, - ) - }, - conversion_factors={electricity_bus: 0.6}, - ) - -For components that is not invested into, you also can specify some additional attributes for their inflows and outflows: - -* You can specify a `lifetime` attribute. This can be used to depict existing plants going offline when reaching their lifetime. -* You can define an initial `age`. Also, this can be used for existing plants. -* You also can define `fixed_costs`, i.e. costs that occur every period independent of the plants usage. How they are handled - depends on whether the flow has a limited or an unlimited lifetime. - -.. code-block:: python - - coal_power_plant = solph.components.Converter( - label="existing_coal_pp", - inputs={coal_bus: solph.flows.Flow()}, - outputs={ - electricity_bus: solph.flows.Flow( - nominal_value=600, - max=1, - min=0.4, - lifetime=50, - age=46, - fixed_costs=100, - variable_costs=3, - ) - }, - conversion_factors={electricity_bus: 0.36}, - ) - -To solve our model and retrieve results, you basically perform the same operations as for standard models. -So it works like this: - -.. code-block:: python - - my_energysystem.add( - hydrogen_bus, - coal_bus, - electricity_bus, - hydrogen_source, - coal_source, - electrical_sink, - hydrogen_power_plant, - coal_power_plant, - ) - - om = solph.Model(my_energysystem) - om.solve(solver="cbc", solve_kwargs={"tee": True}) - - # Obtain results - results = solph.processing.results(om) - hydrogen_results = solph.views.node(results, "hydrogen_pp") - - # Show investment plan for hydrogen power plants - print(hydrogen_results["period_scalars"]) - -The keys in the results dict in a multi-period model are "sequences" and "period_scalars". -So for sequences, it is all the same, while for scalar values, we now have values for each period. - -Besides the `invest` variable, new variables are introduced as well. These are: - -* `total`: The total capacity installed, i.e. how much is actually there in a given period. -* `old`: (Overall) capacity to be decommissioned in a given period. -* `old_end`: Endogenous capacity to be decommissioned in a given period. This is capacity that has been invested into - in the model itself. -* `old_exo`: Exogenous capacity to be decommissioned in a given period. This is capacity that was already existing and - given by the `existing` attribute. - -.. note:: - - * You can specify a `discount_rate` for the model. If you do not do so, 0.02 will be used as a default, corresponding - to sort of a social discount rate. If you work with costs in real terms, discounting is obsolete, so define - `discount_rate = 0` in that case. - * You can specify an `interest_rate` for every investment object. If you do not do so, it will be chosen the same - as the model's `discount_rate`. You could use this default to model a perfect competition administered by some sort of - social planner, but even in a social planner setting, you might want to deviate from the `discount_rate` - value and/or discriminate among technologies with different risk profiles and hence different interest requirements. - * For storage units, the `initial_content` is not allowed combined with multi-period investments. - The storage inflow and outflow are forced to zero until the storage unit is invested into. - * You can specify periods of different lengths, but the frequency of your timeindex needs to be consistent. Also, - you could use the `timeincrement` attribute of the energy system to model different weightings. Be aware that this - has not yet been tested. - * For now, both, the `timeindex` as well as the `timeincrement` of an energy system have to be defined since they - have to be of the same length for a multi-period model. - * You can choose whether to re-evaluate assets at the end of the optimization horizon. If you set attribute - `use_remaining_value` of the energy system to True (defaults to False), this leads to the model evaluating the - difference in the asset value at the end of the optimization horizon vs. at the time the investment was made. - The difference in value is added to or subtracted from the respective investment costs increment, - assuming assets are to be liquidated / re-evaluated at the end of the optimization horizon. - * Also please be aware, that periods correspond to years by default. You could also choose - monthly periods, but you would need to be very careful in parameterizing your energy system and your model and also, - this would mean monthly discounting (if applicable) as well as specifying your plants lifetimes in months. - - -Mixed Integer (Linear) Problems -------------------------------- - -Solph also allows you to model components with respect to more technical details, -such as minimum power production. This can be done in both possible combinations, -as dispatch optimization with fixed capacities or combined dispatch and investment optimization. - -Dispatch Optimization -^^^^^^^^^^^^^^^^^^^^^ -In dispatch optimization, it is assumed that the capacities of the assets are already known, -but the optimal dispatch strategy must be obtained. -For this purpose, the class :py:class:`~oemof.solph._options.NonConvex` should be used, as seen in the following example. - -Note that this flow class's usage is incompatible with the :py:mod:`~oemof.solph.options.Investment` option. This means that, -as stated before, the optimal capacity of the converter cannot be obtained using the :py:class:`~oemof.solph.flows.NonConvexFlow` -class, and only the optimal dispatch strategy of an existing asset with a given capacity can be optimized here. - -.. code-block:: python - - b_gas = solph.buses.Bus(label='natural_gas') - b_el = solph.buses.Bus(label='electricity') - b_th = solph.buses.Bus(label='heat') - - solph.components.Converter( - label='pp_chp', - inputs={b_gas: solph.flows.Flow()}, - outputs={b_el: solph.flows.Flow( - nonconvex=solph.NonConvex(), - nominal_value=30, - min=0.5), - b_th: solph.flows.Flow(nominal_value=40)}, - conversion_factors={b_el: 0.3, b_th: 0.4}) - -The class :py:class:`~oemof.solph.options.NonConvex` for the electrical output of the created Converter (i.e., CHP) -will create a 'status' variable for the flow. -This will be used to model, for example, minimal/maximal power production constraints if the -attributes `min`/`max` of the flow are set. It will also be used to include start-up constraints and costs -if corresponding attributes of the class are provided. For more information, see the API of the -:py:class:`~oemof.solph.flows.NonConvexFlow` class. - -.. note:: The usage of this class can sometimes be tricky as there are many interdenpendencies. So - check out the examples and do not hesitate to ask the developers if your model does - not work as expected. - -Combination of Dispatch and Investment Optimisation -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Since version 'v0.5', it is also possilbe to combine the investment and nonconvex option. -Therefore, a new constraint block for flows, called :py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` has been developed, -which combines both :py:class:`~oemof.solph._options.Investment` and :py:class:`~oemof.solph._options.NonConvex` classes. -The new class offers the possibility to perform the investment optimization of an asset considering `min`/`max` values of the flow -as fractions of the optimal capacity. Moreover, it obtains the optimal 'status' of the flow during the simulation period. - -It must be noted that in a straighforward implementation, a binary variable -representing the 'status' of the flow at each time is multiplied by the 'invest' parameter, -which is a continuous variable representing the capacity of the asset being optimized (i.e., :math:`status \times invest`). -This nonlinearity is linearised in the -:py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` - -.. code-block:: python - - b_diesel = solph.buses.Bus(label='diesel') - b_el = solph.buses.Bus(label='electricity') - - solph.components.Converter( - label='diesel_genset', - inputs={b_diesel: solph.flows.Flow()}, - outputs={ - b_el: solph.flows.Flow( - variable_costs=0.04, - min=0.2, - max=1, - nonconvex=solph.NonConvex(), - nominal_value=solph.Investment( - ep_costs=90, - maximum=150, # required for the linearization - ), - ) - }, - conversion_factors={b_el: 0.3}) - -The following diagram shows the duration curve of a typical diesel genset in a hybrid mini-grid system consisting of a diesel genset, -PV cells, battery, inverter, and rectifier. By using the :py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` class, -it is possible to obtain the optimal capacity of this component and simultaneously limit its operation between `min` and `max` loads. - -.. image:: _files/diesel_genset_nonconvex_invest_flow.svg - :width: 100 % - :alt: diesel_genset_nonconvex_invest_flow.svg - :align: center - -Without using the new :py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` class, if the same system is optimized again, but this -time using the :py:class:`~oemof.solph.flows._investment_flow_block.InvestmentFlowBlock`, the corresponding duration curve would be similar to the following -figure. However, assuming that the diesel genset has a minimum operation load of 20% (as seen in the figure), the -:py:class:`~oemof.solph.flows._investment_flow_block.InvestmentFlowBlock` cannot prevent operations at lower loads than 20%, and it would result in -an infeasible operation of this device for around 50% of its annual operation. - -Moreover, using the :py:class:`~oemof.solph.flows._investment_flow_block.InvestmentFlowBlock` class in the given case study would result in a significantly -oversized diesel genset, which has a 30% larger capacity compared with the optimal capacity obtained from the -:py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` class. - -.. image:: _files/diesel_genset_investment_flow.svg - :width: 100 % - :alt: diesel_genset_investment_flow.svg - :align: center - -Solving such an optimisation problem considering `min`/`max` loads without the :py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` class, the only possibility is first to obtain the optimal capacity using the -:py:class:`~oemof.solph.flows._investment_flow_block.InvestmentFlowBlock` and then implement the `min`/`max` loads using the -:py:class:`~oemof.solph.flows._non_convex_flow_block.NonConvexFlowBlock` class. The following duration curve would be obtained by applying -this method to the same diesel genset. - -.. image:: _files/diesel_genset_nonconvex_flow.svg - :width: 100 % - :alt: diesel_genset_nonconvex_flow.svg - :align: center - -Because of the oversized diesel genset obtained from this approach, the capacity of the PV and battery in the given case study -would be 13% and 43% smaller than the capacities obtained using the :py:class:`~oemof.solph.flows.NonConvexInvestmentFlow` class. -This results in a 15% reduction in the share of renewable energy sources to cover the given demand and a higher levelized -cost of electricity. Last but not least, apart from the nonreliable results, using :py:class:`~oemof.solph._options.Investment` -and :py:class:`~oemof.solph._options.NonConvex` classes for the dispatch and investment optimization of the given case study -increases the computation time by more than 9 times compared to the -:py:class:`~oemof.solph.flows.NonConvexInvestmentFlow` class. - - -Adding additional constraints ------------------------------ - -You can add additional constraints to your :py:class:`~oemof.solph.models.Model`. -See :ref:`custom_constraints_label` to learn how to do it. - -Some predefined additional constraints can be found in the -:py:mod:`~oemof.solph.constraints` module. - - * Emission limit for the model -> :func:`~.oemof.solph.constraints.emission_limit` - * Generic integral limit (general form of emission limit) -> :func:`~.oemof.solph.constraints.generic_integral_limit` - * Coupling of two variables e.g. investment variables) with a factor -> :func:`~.oemof.solph.constraints.equate_variables` - * Overall investment limit -> :func:`~.oemof.solph.constraints.investment_limit` - * Generic investment limit -> :func:`~.oemof.solph.constraints.additional_investment_flow_limit` - * Limit active flow count -> :func:`~.oemof.solph.constraints.limit_active_flow_count` - * Limit active flow count by keyword -> :func:`~.oemof.solph.constraints.limit_active_flow_count_by_keyword` - - -The Grouping module (Sets) --------------------------- -To construct constraints, -variables and objective expressions inside all Block classes -and the :py:mod:`~oemof.solph.models` modules, so called groups are used. Consequently, -certain constraints are created for all elements of a specific group. Thus, -mathematically the groups depict sets of elements inside the model. - -The grouping is handled by the solph grouping module :py:mod:`~oemof.solph.groupings` -which is based on the groupings module functionality of oemof network. You -do not need to understand how the underlying functionality works. Instead, checkout -how the solph grouping module is used to create groups. - -The simplest form is a function that looks at every node of the energy system and -returns a key for the group depending e.g. on node attributes: - -.. code-block:: python - - def constraint_grouping(node): - if isinstance(node, Bus) and node.balanced: - return blocks.Bus - if isinstance(node, Converter): - return blocks.Converter - GROUPINGS = [constraint_grouping] - -This function can be passed in a list to `groupings` of -:class:`oemof.solph.network.energy_system.EnergySystem`. So that we end up with two groups, -one with all Converters and one with all Buses that are balanced. These -groups are simply stored in a dictionary. There are some advanced functionalities -to group two connected nodes with their connecting flow and others -(see for example: FlowsWithNodes class in the oemof.network package). - - -Using the Excel (csv) reader ----------------------------- - -Alternatively to a manual creation of energy system component objects as describe above, can also be created from a excel sheet (libreoffice, gnumeric...). - -The idea is to create different sheets within one spreadsheet file for different components. Afterwards you can loop over the rows with the attributes in the columns. The name of the columns may differ from the name of the attribute. You may even create two sheets for the GenericStorage class with attributes such as C-rate for batteries or capacity of turbine for a PHES. - -Once you have create your specific excel reader you can lower the entry barrier for other users. It is some sort of a GUI in form of platform independent spreadsheet software and to make data and models exchangeable in one archive. - -See :ref:`excel_reader_example_label` for an excel reader example. - - -.. _oemof_outputlib_label: - -Handling Results --------------------- - -The main purpose of the processing module is to collect and organise results. -The views module will provide some typical representations of the results. -Plots are not part of solph, because plots are highly individual. However, the -provided pandas.DataFrames are a good start for plots. Some basic functions -for plotting of optimisation results can be found in the separate repository -`oemof_visio `_. - -The ``processing.results`` function gives back the results as a python -dictionary holding pandas Series for scalar values and pandas DataFrames for -all nodes and flows between them. This way we can make use of the full power -of the pandas package available to process the results. - -See the `pandas documentation `_ -to learn how to `visualise -`_, -`read or write -`_ or how to -`access parts of the DataFrame -`_ to -process them. - -The results chapter consists of three parts: - -.. contents:: - :depth: 1 - :local: - :backlinks: top - -The first step is the processing of the results (:ref:`results_collect_results_label`) -This is followed by basic examples of the general analysis of the results -(:ref:`res_general_approach_label`) and finally the use of functionality already included in solph -for providing a quick access to your results (:ref:`results_easy_access_label`). -Especially for larger energy systems the general approach will help you to -write your own results processing functions. - -.. _results_collect_results_label: - -Collecting results -^^^^^^^^^^^^^^^^^^ - -Collecting results can be done with the help of the processing module. A solved -model is needed: - -.. code-block:: python - - [...] - model.solve(solver=solver) - results = solph.processing.results(model) - -The scalars and sequences describe nodes (with keys like (node, None)) and -flows between nodes (with keys like (node_1, node_2)). You can directly extract -the data in the dictionary by using these keys, where "node" is the name of -the object you want to address. -Processing the results is the prerequisite for the examples in the following -sections. - -.. _res_general_approach_label: - -General approach -^^^^^^^^^^^^^^^^ - -As stated above, after processing you will get a dictionary with all result -data. -If you want to access your results directly via labels, you -can continue with :ref:`results_easy_access_label`. For a systematic analysis list comprehensions -are the easiest way of filtering and analysing your results. - -The keys of the results dictionary are tuples containing two nodes. Since flows -have a starting node and an ending node, you get a list of all flows by -filtering the results using the following expression: - -.. code-block:: python - - flows = [x for x in results.keys() if x[1] is not None] - -On the same way you can get a list of all nodes by applying: - -.. code-block:: python - - nodes = [x for x in results.keys() if x[1] is None] - -Probably you will just get storages as nodes, if you have some in your energy -system. Note, that just nodes containing decision variables are listed, e.g. a -Source or a Converter object does not have decision variables. These are in -the flows from or to the nodes. - -All items within the results dictionary are dictionaries and have two items -with 'scalars' and 'sequences' as keys: - -.. code-block:: python - - for flow in flows: - print(flow) - print(results[flow]['scalars']) - print(results[flow]['sequences']) - -There many options of filtering the flows and nodes as you prefer. -The following will give you all flows which are outputs of converter: - -.. code-block:: python - - flows_from_converter = [x for x in flows if isinstance( - x[0], solph.components.Converter)] - -You can filter your flows, if the label of in- or output contains a given -string, e.g.: - -.. code-block:: python - - flows_to_elec = [x for x in results.keys() if 'elec' in x[1].label] - -Getting all labels of the starting node of your investment flows: - -.. code-block:: python - - flows_invest = [x[0].label for x in flows if hasattr( - results[x]['scalars'], 'invest')] - - -.. _results_easy_access_label: - -Easy access -^^^^^^^^^^^ - -The solph package provides some functions which will help you to access your -results directly via labels, which is helpful especially for small energy -systems. -So, if you want to address objects by their label, you can convert the results -dictionary such that the keys are changed to strings given by the labels: - -.. code-block:: python - - views.convert_keys_to_strings(results) - print(results[('wind', 'bus_electricity')]['sequences'] - - -Another option is to access data belonging to a grouping by the name of the grouping -(`note also this section on groupings `_. -Given the label of an object, e.g. 'wind' you can access the grouping by its label -and use this to extract data from the results dictionary. - -.. code-block:: python - - node_wind = energysystem.groups['wind'] - print(results[(node_wind, bus_electricity)]) - - -However, in many situations it might be convenient to use the views module to -collect information on a specific node. You can request all data related to a -specific node by using either the node's variable name or its label: - -.. code-block:: python - - data_wind = solph.views.node(results, 'wind') - - -A function for collecting and printing meta results, i.e. information on the objective function, -the problem and the solver, is provided as well: - -.. code-block:: python - - meta_results = solph.processing.meta_results(om) - pp.pprint(meta_results) +.. _oemof_solph_label: + +.. _using_oemof_label: + +~~~~~~~~~~~~ +User's guide +~~~~~~~~~~~~ + +Solph is an oemof-package, designed to create and solve linear or mixed-integer linear optimization problems. The package is based on pyomo. To create an energy system model generic and specific components are available. To get started with solph, checkout the examples in the :ref:`examples_label` section. + +This User's guide provides a user-friendly introduction into oemof-solph, +which includes small examples and nice illustrations. +However, the functionalities of oemof-solph go beyond the content of this User's guide section. +So, if you want to know all details of a certain component or a function, +please go the :ref:`api_reference_label`. There, you will find +a detailed and complete description of all oemof-solph modules. + +.. contents:: + :depth: 2 + :local: + :backlinks: top + + +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). +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. + +Once the examples work you are close to your first energy model. + + +Handling of Warnings +^^^^^^^^^^^^^^^^^^^^ + +The solph library is designed to be as generic as possible to make it possible +to use it in different use cases. This concept makes it difficult to raise +Errors or Warnings because sometimes untypical combinations of parameters are +allowed even though they might be wrong in over 99% of the use cases. + +Therefore, a SuspiciousUsageWarning was introduced. This warning will warn you +if you do something untypical. If you are sure that you know what you are doing +you can switch the warning off. + +See `the debugging module of oemof-tools `_ for more +information. + + +Set up an energy system +^^^^^^^^^^^^^^^^^^^^^^^ + +In most cases an EnergySystem object is defined when we start to build up an energy system model. The EnergySystem object will be the main container for the model's elements. + +The model time is defined by the number of intervals and the length of intervals. The length of each interval does not have to be the same. This can be defined in two ways: + +1. Define the length of each interval in an array/Series where the number of the elements is the number of intervals. +2. Define a `pandas.DatetimeIndex` with all time steps that encloses an interval. Be aware that you have to define n+1 time points to get n intervals. For non-leap year with hourly values that means 8761 time points to get 8760 interval e.g. 2018-01-01 00:00 to 2019-01-01 00:00. + +The index will also be used for the results. For a numeric index the resulting time series will indexed with a numeric index starting with 0. + +One can use the function +:py:func:`create_time_index` to create an equidistant datetime index. By default the function creates an hourly index for one year, so online the year has to be passed to the function. But it is also possible to change the length of the interval to quarter hours etc. The default number of intervals is the number needed to cover the given year but the value can be overwritten by the user. + +It is also possible to define the datetime index using pandas. See `pandas date_range guide `_ for more information. + +Both code blocks will create an hourly datetime index for 2011: + +.. code-block:: python + + from oemof.solph import create_time_index + my_index = create_time_index(2011) + +.. code-block:: python + + import pandas as pd + my_index = pd.date_range('1/1/2011', periods=8761, freq='h') + +This index can be used to define the EnergySystem: + +.. code-block:: python + + import oemof.solph as solph + my_energysystem = solph.EnergySystem(timeindex=my_index) + +Now you can start to add the components of the network. + + +Add components to the energy system +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +After defining an instance of the EnergySystem class, in the following you have to add all nodes you define to your EnergySystem. + +Basically, there are two types of *nodes* - *components* and *buses*. Every Component has to be connected with one or more *buses*. The connection between a *component* and a *bus* is the *flow*. + +All solph *components* can be used to set up an energy system model but you should read the documentation of each *component* to learn about usage and restrictions. For example it is not possible to combine every *component* with every *flow*. Furthermore, you can add your own *components* in your application (see below) but we would be pleased to integrate them into solph if they are of general interest (see :ref:`feature_requests_and_feedback`). + +An example of a simple energy system shows the usage of the nodes for +real world representations: + +.. image:: _files/oemof_solph_example.svg + :scale: 70 % + :alt: alternate text + :align: center + +The figure shows a simple energy system using the four basic network classes and the Bus class. +If you remove the transmission line (transport 1 and transport 2) you get two systems but they are still one energy system in terms of solph and will be optimised at once. + +There are different ways to add components to an *energy system*. The following line adds a *bus* object to the *energy system* defined above. + +.. code-block:: python + + my_energysystem.add(solph.buses.Bus()) + +It is also possible to assign the bus to a variable and add it afterwards. In that case it is easy to add as many objects as you like. + +.. code-block:: python + + my_bus1 = solph.buses.Bus() + my_bus2 = solph.buses.Bus() + my_energysystem.add(my_bus1, my_bus2) + +Therefore it is also possible to add lists or dictionaries with components but you have to dissolve them. + +.. code-block:: python + + # add a list + my_energysystem.add(*my_list) + + # add a dictionary + my_energysystem.add(*my_dictionary.values()) + + +Bus ++++ + +All flows into and out of a *bus* are balanced (by default). Therefore an instance of the Bus class represents a grid or network without losses. To define an instance of a Bus only a unique label is necessary. If you do not set a label a random label is used but this makes it difficult to get the results later on. + +To make it easier to connect the bus to a component you can optionally assign a variable for later use. + +.. code-block:: python + + solph.buses.Bus(label='natural_gas') + electricity_bus = solph.buses.Bus(label='electricity') + +.. note:: See the :py:class:`~oemof.solph.buses._bus.Bus` class for all parameters and the mathematical background. + + +Flow +++++ + +The flow class has to be used to connect nodes and buses. An instance of the Flow class is normally used in combination with the definition of a component. +A Flow can be limited by upper and lower bounds (constant or time-dependent) or by summarised limits. +For all parameters see the API documentation of the :py:class:`~oemof.solph.flows._flow.Flow` class or the examples of the nodes below. A basic flow can be defined without any parameter. + +.. code-block:: python + + solph.flows.Flow() + +oemof.solph has different types of *flows* but you should be aware that you cannot connect every *flow* type with every *component*. + +.. note:: See the :py:class:`~oemof.solph.flows._flow.Flow` class for all parameters and the mathematical background. + +Components +++++++++++ + +Components are divided in two categories. Well-tested components (solph.components) and experimental components (solph.components.experimental). The experimental section was created to lower the entry barrier for new components. Be aware that these components might not be properly documented or even sometimes do not even work as intended. Let us know if you have successfully used and tested these components. This is the first step to move them to the regular components section. + +See :ref:`oemof_solph_components_label` for a list of all components. + + +.. _oemof_solph_optimise_es_label: + +Optimise your energy system +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The typical optimisation of an energy system in solph is the dispatch optimisation, which means that the use of the sources is optimised to satisfy the demand at least costs. +Therefore, variable cost can be defined for all components. The cost for gas should be defined in the gas source while the variable costs of the gas power plant are caused by operating material. +The actual fuel cost in turn is calculated in the framework itself considering the efficiency of the power plant. +You can deviate from this scheme but you should keep it consistent to make it understandable for others. + +Costs do not have to be monetary costs but could be emissions or other variable units. + +Furthermore, it is possible to optimise the capacity of different components using the investment mode (see :ref:`investment_mode_label`). + +Since v0.5.1, there also is the possibility to have multi-period (i.e. dynamic) investments over longer-time horizon which is in experimental state (see :ref:`multi_period_mode_label`). + +.. code-block:: python + + # set up a simple least cost optimisation + om = solph.Model(my_energysystem) + + # solve the energy model using the CBC solver + om.solve(solver='cbc', solve_kwargs={'tee': True}) + +If you want to analyse the lp-file to see all equations and bounds you can write the file to you disc. In that case you should reduce the timesteps to 3. This will increase the readability of the file. + +.. code-block:: python + + # set up a simple least cost optimisation + om = solph.Model(my_energysystem) + + # write the lp file for debugging or other reasons + om.write('path/my_model.lp', io_options={'symbolic_solver_labels': True}) + +Analysing your results +^^^^^^^^^^^^^^^^^^^^^^ + +If you want to analyse your results, you should first dump your EnergySystem instance to permanently store results. Otherwise you would have to run the simulation again. + +.. code-block:: python + + my_energysystem.results = processing.results(om) + my_energysystem.dump('my_path', 'my_dump.oemof') + +If you need the meta results of the solver you can do the following: + +.. code-block:: python + + my_energysystem.results['main'] = processing.results(om) + my_energysystem.results['meta'] = processing.meta_results(om) + my_energysystem.dump('my_path', 'my_dump.oemof') + +To restore the dump you can simply create an EnergySystem instance and restore your dump into it. + +.. code-block:: python + + import oemof.solph as solph + my_energysystem = solph.EnergySystem() + my_energysystem.restore('my_path', 'my_dump.oemof') + results = my_energysystem.results + + # If you use meta results do the following instead of the previous line. + results = my_energysystem.results['main'] + meta = my_energysystem.results['meta'] + + +If you call dump/restore without any parameters, the dump will be stored as *'es_dump.oemof'* into the *'.oemof/dumps/'* folder created in your HOME directory. + +See :ref:`oemof_outputlib_label` to learn how to process, plot and analyse the results. + + +.. _oemof_solph_components_label: + +Solph components +---------------- + + * :ref:`oemof_solph_components_sink_label` + * :ref:`oemof_solph_components_source_label` + * :ref:`oemof_solph_components_converter_label` + * :ref:`oemof_solph_components_extraction_turbine_chp_label` + * :ref:`oemof_solph_components_generic_caes_label` + * :ref:`oemof_solph_components_generic_chp_label` + * :ref:`oemof_solph_components_generic_storage_label` + * :ref:`oemof_solph_custom_electrical_line_label` + * :ref:`oemof_solph_custom_link_label` + * :ref:`oemof_solph_custom_sinkdsm_label` + + +.. _oemof_solph_components_sink_label: + +Sink (basic) +^^^^^^^^^^^^ + +A sink is normally used to define the demand within an energy model but it can also be used to detect excesses. + +The example shows the electricity demand of the electricity_bus defined above. +The *'my_demand_series'* should be sequence of normalised valueswhile the *'nominal_value'* is the maximum demand the normalised sequence is multiplied with. +Giving *'my_demand_series'* as parameter *'fix'* means that the demand cannot be changed by the solver. + +.. code-block:: python + + solph.components.Sink(label='electricity_demand', inputs={electricity_bus: solph.flows.Flow( + fix=my_demand_series, nominal_value=nominal_demand)}) + +In contrast to the demand sink the excess sink has normally less restrictions but is open to take the whole excess. + +.. code-block:: python + + solph.components.Sink(label='electricity_excess', inputs={electricity_bus: solph.flows.Flow()}) + +.. note:: The Sink class is only a plug and provides no additional constraints or variables. + + +.. _oemof_solph_components_source_label: + +Source (basic) +^^^^^^^^^^^^^^ + +A source can represent a pv-system, a wind power plant, an import of natural gas or a slack variable to avoid creating an in-feasible model. + +While a wind power plant will have as feed-in depending on the weather conditions the natural_gas import might be restricted by maximum value (*nominal_value*) and an annual limit (*full_load_time_max*). +As we do have to pay for imported gas we should set variable costs. +Comparable to the demand series an *fix* is used to define a fixed the normalised output of a wind power plant. +Alternatively, you might use *max* to allow for easy curtailment. +The *nominal_value* sets the installed capacity. + +.. code-block:: python + + solph.components.Source( + label='import_natural_gas', + outputs={my_energysystem.groups['natural_gas']: solph.flows.Flow( + nominal_value=1000, full_load_time_max=1000000, variable_costs=50)}) + + solph.components.Source(label='wind', outputs={electricity_bus: solph.flows.Flow( + fix=wind_power_feedin_series, nominal_value=1000000)}) + +.. note:: The Source class is only a plug and provides no additional constraints or variables. + +.. _oemof_solph_components_converter_label: + +Converter (basic) +^^^^^^^^^^^^^^^^^ + +An instance of the Converter class can represent a node with multiple input and output flows such as a power plant, a transport line or any kind of a transforming process as electrolysis, a cooling device or a heat pump. +The efficiency has to be constant within one time step to get a linear transformation. +You can define a different efficiency for every time step (e.g. the thermal powerplant efficiency according to the ambient temperature) but this series has to be predefined and cannot be changed within the optimisation. + +A condensing power plant can be defined by a converter with one input (fuel) and one output (electricity). + +.. code-block:: python + + b_gas = solph.buses.Bus(label='natural_gas') + b_el = solph.buses.Bus(label='electricity') + + solph.components.Converter( + label="pp_gas", + inputs={bgas: solph.flows.Flow()}, + outputs={b_el: solph.flows.Flow(nominal_value=10e10)}, + conversion_factors={electricity_bus: 0.58}) + +A CHP power plant would be defined in the same manner but with two outputs: + +.. code-block:: python + + b_gas = solph.buses.Bus(label='natural_gas') + b_el = solph.buses.Bus(label='electricity') + b_th = solph.buses.Bus(label='heat') + + solph.components.Converter( + label='pp_chp', + inputs={b_gas: Flow()}, + outputs={b_el: Flow(nominal_value=30), + b_th: Flow(nominal_value=40)}, + conversion_factors={b_el: 0.3, b_th: 0.4}) + +A CHP power plant with 70% coal and 30% natural gas can be defined with two inputs and two outputs: + +.. code-block:: python + + b_gas = solph.buses.Bus(label='natural_gas') + b_coal = solph.buses.Bus(label='hard_coal') + b_el = solph.buses.Bus(label='electricity') + b_th = solph.buses.Bus(label='heat') + + solph.components.Converter( + label='pp_chp', + inputs={b_gas: Flow(), b_coal: Flow()}, + outputs={b_el: Flow(nominal_value=30), + b_th: Flow(nominal_value=40)}, + conversion_factors={b_el: 0.3, b_th: 0.4, + b_coal: 0.7, b_gas: 0.3}) + +A heat pump would be defined in the same manner. New buses are defined to make the code cleaner: + +.. code-block:: python + + b_el = solph.buses.Bus(label='electricity') + b_th_low = solph.buses.Bus(label='low_temp_heat') + b_th_high = solph.buses.Bus(label='high_temp_heat') + + # The cop (coefficient of performance) of the heat pump can be defined as + # a scalar or a sequence. + cop = 3 + + solph.components.Converter( + label='heat_pump', + inputs={b_el: Flow(), b_th_low: Flow()}, + outputs={b_th_high: Flow()}, + conversion_factors={b_el: 1/cop, + b_th_low: (cop-1)/cop}) + +If the low-temperature reservoir is nearly infinite (ambient air heat pump) the +low temperature bus is not needed and, therefore, a Converter with one input +is sufficient. + +.. note:: See the :py:class:`~oemof.solph.components.converter.Converter` class for all parameters and the mathematical background. + +.. _oemof_solph_components_extraction_turbine_chp_label: + +ExtractionTurbineCHP (component) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :py:class:`~oemof.solph.components._extraction_turbine_chp.ExtractionTurbineCHP` +inherits from the :ref:`oemof_solph_components_converter_label` class. Like the name indicates, +the application example for the component is a flexible combined heat and power +(chp) plant. Of course, an instance of this class can represent also another +component with one input and two output flows and a flexible ratio between +these flows, with the following constraints: + +.. include:: ../src/oemof/solph/components/_extraction_turbine_chp.py + :start-after: _ETCHP-equations: + :end-before: """ + +These constraints are applied in addition to those of a standard +:class:`~oemof.solph.components.Converter`. The constraints limit the range of +the possible operation points, like the following picture shows. For a certain +flow of fuel, there is a line of operation points, whose slope is defined by +the power loss factor :math:`\beta` (in some contexts also referred to as +:math:`C_v`). The second constraint limits the decrease of electrical power and +incorporates the backpressure coefficient :math:`C_b`. + +.. image:: _files/ExtractionTurbine_range_of_operation.svg + :width: 70 % + :alt: variable_chp_plot.svg + :align: center + +For now, :py:class:`~oemof.solph.components._extraction_turbine_chp.ExtractionTurbineCHP` instances must +have one input and two output flows. The class allows the definition +of a different efficiency for every time step that can be passed as a series +of parameters that are fixed before the optimisation. In contrast to the +:py:class:`~oemof.solph.components.Converter`, a main flow and a tapped flow is +defined. For the main flow you can define a separate conversion factor that +applies when the second flow is zero (*`conversion_factor_full_condensation`*). + +.. code-block:: python + + solph.components._extractionTurbineCHP( + label='variable_chp_gas', + inputs={b_gas: solph.flows.Flow(nominal_value=10e10)}, + outputs={b_el: solph.flows.Flow(), b_th: solph.flows.Flow()}, + conversion_factors={b_el: 0.3, b_th: 0.5}, + conversion_factor_full_condensation={b_el: 0.5}) + +The key of the parameter *'conversion_factor_full_condensation'* defines which +of the two flows is the main flow. In the example above, the flow to the Bus +*'b_el'* is the main flow and the flow to the Bus *'b_th'* is the tapped flow. +The following plot shows how the variable chp (right) schedules it's electrical +and thermal power production in contrast to a fixed chp (left). The plot is the +output of an example in the `example directory +`_. + +.. image:: _files/variable_chp_plot.svg + :scale: 10 % + :alt: variable_chp_plot.svg + :align: center + +.. note:: See the :py:class:`~oemof.solph.components._extraction_turbine_chp.ExtractionTurbineCHP` class for all parameters and the mathematical background. + + +.. _oemof_solph_components_generic_chp_label: + +GenericCHP (component) +^^^^^^^^^^^^^^^^^^^^^^ + +With the GenericCHP class it is possible to model different types of CHP plants (combined cycle extraction turbines, +back pressure turbines and motoric CHP), which use different ranges of operation, as shown in the figure below. + +.. image:: _files/GenericCHP.svg + :scale: 70 % + :alt: scheme of GenericCHP operation range + :align: center + +Combined cycle extraction turbines: The minimal and maximal electric power without district heating +(red dots in the figure) define maximum load and minimum load of the plant. Beta defines electrical power loss through +heat extraction. The minimal thermal condenser load to cooling water and the share of flue gas losses +at maximal heat extraction determine the right boundary of the operation range. + +.. code-block:: python + + solph.components.GenericCHP( + label='combined_cycle_extraction_turbine', + fuel_input={bgas: solph.flows.Flow( + H_L_FG_share_max=[0.19 for p in range(0, periods)])}, + electrical_output={bel: solph.flows.Flow( + P_max_woDH=[200 for p in range(0, periods)], + P_min_woDH=[80 for p in range(0, periods)], + Eta_el_max_woDH=[0.53 for p in range(0, periods)], + Eta_el_min_woDH=[0.43 for p in range(0, periods)])}, + heat_output={bth: solph.flows.Flow( + Q_CW_min=[30 for p in range(0, periods)])}, + Beta=[0.19 for p in range(0, periods)], + back_pressure=False) + +For modeling a back pressure CHP, the attribute `back_pressure` has to be set to True. +The ratio of power and heat production in a back pressure plant is fixed, therefore the operation range +is just a line (see figure). Again, the `P_min_woDH` and `P_max_woDH`, the efficiencies at these points and the share of flue +gas losses at maximal heat extraction have to be specified. In this case “without district heating” is not to be taken +literally since an operation without heat production is not possible. It is advised to set `Beta` to zero, so the minimal and +maximal electric power without district heating are the same as in the operation point (see figure). The minimal +thermal condenser load to cooling water has to be zero, because there is no condenser besides the district heating unit. + + +.. code-block:: python + + solph.components.GenericCHP( + label='back_pressure_turbine', + fuel_input={bgas: solph.flows.Flow( + H_L_FG_share_max=[0.19 for p in range(0, periods)])}, + electrical_output={bel: solph.flows.Flow( + P_max_woDH=[200 for p in range(0, periods)], + P_min_woDH=[80 for p in range(0, periods)], + Eta_el_max_woDH=[0.53 for p in range(0, periods)], + Eta_el_min_woDH=[0.43 for p in range(0, periods)])}, + heat_output={bth: solph.flows.Flow( + Q_CW_min=[0 for p in range(0, periods)])}, + Beta=[0 for p in range(0, periods)], + back_pressure=True) + +A motoric chp has no condenser, so `Q_CW_min` is zero. Electrical power does not depend on the amount of heat used +so `Beta` is zero. The minimal and maximal electric power (without district heating) and the efficiencies at these +points are needed, whereas the use of electrical power without using thermal energy is not possible. +With `Beta=0` there is no difference between these points and the electrical output in the operation range. +As a consequence of the functionality of a motoric CHP, share of flue gas losses at maximal heat extraction but also +at minimal heat extraction have to be specified. + + +.. code-block:: python + + solph.components.GenericCHP( + label='motoric_chp', + fuel_input={bgas: solph.flows.Flow( + H_L_FG_share_max=[0.18 for p in range(0, periods)], + H_L_FG_share_min=[0.41 for p in range(0, periods)])}, + electrical_output={bel: solph.flows.Flow( + P_max_woDH=[200 for p in range(0, periods)], + P_min_woDH=[100 for p in range(0, periods)], + Eta_el_max_woDH=[0.44 for p in range(0, periods)], + Eta_el_min_woDH=[0.40 for p in range(0, periods)])}, + heat_output={bth: solph.flows.Flow( + Q_CW_min=[0 for p in range(0, periods)])}, + Beta=[0 for p in range(0, periods)], + back_pressure=False) + +Modeling different types of plants means telling the component to use different constraints. Constraint 1 to 9 +are active in all three cases. Constraint 10 depends on the attribute back_pressure. If true, the constraint is +an equality, if not it is a less or equal. Constraint 11 is only needed for modeling motoric CHP which is done by +setting the attribute `H_L_FG_share_min`. + +.. include:: ../src/oemof/solph/components/_generic_chp.py + :start-after: _GenericCHP-equations1-10: + :end-before: **For the attribute** + +If :math:`\dot{H}_{L,FG,min}` is given, e.g. for a motoric CHP: + +.. include:: ../src/oemof/solph/components/_generic_chp.py + :start-after: _GenericCHP-equations11: + :end-before: """ + +.. note:: See the :py:class:`~oemof.solph.components._generic_chp.GenericCHP` class for all parameters and the mathematical background. + + +.. _oemof_solph_components_generic_storage_label: + +GenericStorage (component) +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A component to model a storage with its basic characteristics. The +GenericStorage is designed for one input and one output. +The ``nominal_storage_capacity`` of the storage signifies the storage capacity. You can either set it to the net capacity or to the gross capacity and limit it using the min/max attribute. +To limit the input and output flows, you can define the ``nominal_value`` in the Flow objects. +Furthermore, an efficiency for loading, unloading and a loss rate can be defined. + +.. code-block:: python + + solph.components.GenericStorage( + label='storage', + inputs={b_el: solph.flows.Flow(nominal_value=9, variable_costs=10)}, + outputs={b_el: solph.flows.Flow(nominal_value=25, variable_costs=10)}, + loss_rate=0.001, nominal_storage_capacity=50, + inflow_conversion_factor=0.98, outflow_conversion_factor=0.8) + +For initialising the state of charge before the first time step (time step zero) the parameter ``initial_storage_level`` (default value: ``None``) can be set by a numeric value as fraction of the storage capacity. +Additionally the parameter ``balanced`` (default value: ``True``) sets the relation of the state of charge of time step zero and the last time step. +If ``balanced=True``, the state of charge in the last time step is equal to initial value in time step zero. +Use ``balanced=False`` with caution as energy might be added to or taken from the energy system due to different states of charge in time step zero and the last time step. +Generally, with these two parameters four configurations are possible, which might result in different solutions of the same optimization model: + + * ``initial_storage_level=None``, ``balanced=True`` (default setting): The state of charge in time step zero is a result of the optimization. The state of charge of the last time step is equal to time step zero. Thus, the storage is not violating the energy conservation by adding or taking energy from the system due to different states of charge at the beginning and at the end of the optimization period. + * ``initial_storage_level=0.5``, ``balanced=True``: The state of charge in time step zero is fixed to 0.5 (50 % charged). The state of charge in the last time step is also constrained by 0.5 due to the coupling parameter ``balanced`` set to ``True``. + * ``initial_storage_level=None``, ``balanced=False``: Both, the state of charge in time step zero and the last time step are a result of the optimization and not coupled. + * ``initial_storage_level=0.5``, ``balanced=False``: The state of charge in time step zero is constrained by a given value. The state of charge of the last time step is a result of the optimization. + +The following code block shows an example of the storage parametrization for the second configuration: + +.. code-block:: python + + solph.components.GenericStorage( + label='storage', + inputs={b_el: solph.flows.Flow(nominal_value=9, variable_costs=10)}, + outputs={b_el: solph.flows.Flow(nominal_value=25, variable_costs=10)}, + loss_rate=0.001, nominal_storage_capacity=50, + initial_storage_level=0.5, balanced=True, + inflow_conversion_factor=0.98, outflow_conversion_factor=0.8) + +If you want to view the temporal course of the state of charge of your storage +after the optimisation, you need to check the ``storage_content`` in the results: + +.. code-block:: python + + from oemof.solph import processing, views + results = processing.results(om) + column_name = (('your_storage_label', 'None'), 'storage_content') + SC = views.node(results, 'your_storage_label')['sequences'][column_name] + +The ``storage_content`` is the absolute value of the current stored energy. +By calling: + +.. code-block:: python + + views.node(results, 'your_storage_label')['scalars'] + +you get the results of the scalar values of your storage, e.g. the initial +storage content before time step zero (``init_content``). + +For more information see the definition of the :py:class:`~oemof.solph.components._generic_storage.GenericStorage` class or check the :ref:`examples_label`. + + +Using an investment object with the GenericStorage component ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +Based on the `GenericStorage` object the `GenericInvestmentStorageBlock` adds two main investment possibilities. + + * Invest into the flow parameters e.g. a turbine or a pump + * Invest into capacity of the storage e.g. a basin or a battery cell + +Investment in this context refers to the value of the variable for the 'nominal_value' (installed capacity) in the investment mode. + +As an addition to other flow-investments, the storage class implements the possibility to couple or decouple the flows +with the capacity of the storage. +Three parameters are responsible for connecting the flows and the capacity of the storage: + + * ``invest_relation_input_capacity`` fixes the input flow investment to the capacity investment. A ratio of 1 means that the storage can be filled within one time-period. + * ``invest_relation_output_capacity`` fixes the output flow investment to the capacity investment. A ratio of 1 means that the storage can be emptied within one period. + * ``invest_relation_input_output`` fixes the input flow investment to the output flow investment. For values <1, the input will be smaller and for values >1 the input flow will be larger. + +You should not set all 3 parameters at the same time, since it will lead to overdetermination. + +The following example pictures a Pumped Hydroelectric Energy Storage (PHES). Both flows and the storage itself (representing: pump, turbine, basin) are free in their investment. You can set the parameters to `None` or delete them as `None` is the default value. + +.. code-block:: python + + solph.components.GenericStorage( + label='PHES', + inputs={b_el: solph.flows.Flow(nominal_value=solph.Investment(ep_costs=500))}, + outputs={b_el: solph.flows.Flow(nominal_value=solph.Investment(ep_costs=500)}, + loss_rate=0.001, + inflow_conversion_factor=0.98, outflow_conversion_factor=0.8), + investment = solph.Investment(ep_costs=40)) + +The following example describes a battery with flows coupled to the capacity of the storage. + +.. code-block:: python + + solph.components.GenericStorage( + label='battery', + inputs={b_el: solph.flows.Flow()}, + outputs={b_el: solph.flows.Flow()}, + loss_rate=0.001, + inflow_conversion_factor=0.98, + outflow_conversion_factor=0.8, + invest_relation_input_capacity = 1/6, + invest_relation_output_capacity = 1/6, + investment = solph.Investment(ep_costs=400)) + + +.. note:: See the :py:class:`~oemof.solph.components._generic_storage.GenericStorage` class for all parameters and the mathematical background. + + +.. _oemof_solph_custom_link_label: + +Link +^^^^ + +The `Link` allows to model connections between two busses, e.g. modeling the transshipment of electric energy between two regions. + +.. note:: See the :py:class:`~oemof.solph.components.experimental._link.Link` class for all parameters and the mathematical background. + + + +OffsetConverter (component) +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The `OffsetConverter` object makes it possible to create a Converter with efficiencies depending on the part load condition. +For this it is necessary to define one flow as a nonconvex flow and to set a minimum load. +The following example illustrates how to define an OffsetConverter for given +information for an output, i.e. a combined heat and power plant. The plant +generates up to 100 kW electric energy at an efficiency of 40 %. In minimal +load the electric efficiency is at 30 %, and the minimum possible load is 50 % +of the nominal load. At the same time, heat is produced with a constant +efficiency. By using the `OffsetConverter` a linear relation of in- and output +power with a power dependent efficiency is generated. + +.. code-block:: python + + >>> from oemof import solph + + >>> eta_el_min = 0.3 # electrical efficiency at minimal operation point + >>> eta_el_max = 0.4 # electrical efficiency at nominal operation point + >>> eta_th_min = 0.5 # thermal efficiency at minimal operation point + >>> eta_th_max = 0.5 # thermal efficiency at nominal operation point + >>> P_out_min = 20 # absolute minimal output power + >>> P_out_max = 100 # absolute nominal output power + +As reference for our system we use the input and will mark that flow as +nonconvex respectively. The efficiencies for electricity and heat output have +therefore to be defined with respect to the input flow. The same is true for +the minimal and maximal load. Therefore, we first calculate the minimum and +maximum input of fuel and then derive the slope and offset for both outputs. + +.. code-block:: python + + >>> P_in_max = P_out_max / eta_el_max + >>> P_in_min = P_out_min / eta_el_min + >>> P_in_max + 250.0 + >>> round(P_in_min, 2) + 66.67 + +With that information, we can derive the normed minimal and maximal load of the +nonconvex flow, and calculate the slope and the offset for both outputs. Note, +that the offset for the heat output is 0, because the thermal heat output +efficiency is constant. + +.. code-block:: python + + >>> l_max = 1 + >>> l_min = P_in_min / P_in_max + >>> slope_el, offset_el = solph.components.slope_offset_from_nonconvex_input( + ... l_max, l_min, eta_el_max, eta_el_min + ... ) + >>> slope_th, offset_th = solph.components.slope_offset_from_nonconvex_input( + ... l_max, l_min, eta_th_max, eta_th_min + ... ) + >>> round(slope_el, 3) + 0.436 + >>> round(offset_el, 3) + -0.036 + >>> round(slope_th, 3) + 0.5 + >>> round(offset_th, 3) + 0.0 + +Then we can create our component with the buses attached to it. + +.. code-block:: python + + >>> bfuel = solph.Bus("fuel") + >>> bel = solph.Bus("electricity") + >>> bth = solph.Bus("heat") + + # define OffsetConverter + >>> diesel_genset = solph.components.OffsetConverter( + ... label='boiler', + ... inputs={ + ... bfuel: solph.flows.Flow( + ... nominal_value=P_out_max, + ... max=l_max, + ... min=l_min, + ... nonconvex=solph.NonConvex() + ... ) + ... }, + ... outputs={ + ... bel: solph.flows.Flow(), + ... bth: solph.flows.Flow(), + ... }, + ... conversion_factors={bel: slope_el, bth: slope_th}, + ... normed_offsets={bel: offset_el, bth: offset_th}, + ... ) + +.. note:: + + One of the inputs and outputs has to be a `NonConvex` flow and this flow + will serve as the reference for the `conversion_factors` and the + `normed_offsets`. The `NonConvex` flow also holds + + - the `nominal_value` (or `Investment` in case of investment optimization), + - the `min` and + - the `max` attributes. + + The `conversion_factors` and `normed_offsets` are specified similar to the + `Converter` API with dictionaries referencing the respective input and + output buses. Note, that you cannot have the `conversion_factors` or + `normed_offsets` point to the `NonConvex` flow. + +The following figures show the power at the electrical and the thermal output +and the resepctive ratios to the nonconvex flow (normalized). The efficiency +becomes non-linear. + +.. image:: _files/OffsetConverter_relations_1.svg + :width: 70 % + :alt: OffsetConverter_relations_1.svg + :align: center + + +.. image:: _files/OffsetConverter_relations_2.svg + :width: 70 % + :alt: OffsetConverter_relations_2.svg + :align: center + +.. math:: + + \eta = P(t) / P_\text{ref}(t) + +It also becomes clear, why the component has been named `OffsetConverter`. The +linear equation of inflow to electrical outflow does not hit the origin, but is +offset. By multiplying the offset :math:`y_\text{0,normed}` with the binary +status variable of the `NonConvex` flow, the origin (0, 0) becomes part of the +solution space and the boiler is allowed to switch off. + +.. include:: ../src/oemof/solph/components/_offset_converter.py + :start-after: _OffsetConverter-equations: + :end-before: """ + +The parameters :math:`y_\text{0,normed}` and :math:`m` can be given by scalars or by series in order to define a different efficiency equation for every timestep. +It is also possible to define multiple outputs. + +.. note:: See the :py:class:`~oemof.solph.components._offset_converter.OffsetConverter` class for all parameters and the mathematical background. + + +.. _oemof_solph_custom_electrical_line_label: + +ElectricalLine (experimental) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Electrical line. + +.. note:: See the :py:class:`~oemof.solph.flows.experimental._electrical_line.ElectricalLine` class for all parameters and the mathematical background. + + +.. _oemof_solph_components_generic_caes_label: + +GenericCAES (experimental) +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Compressed Air Energy Storage (CAES). +The following constraints describe the CAES: + +.. include:: ../src/oemof/solph/components/experimental/_generic_caes.py + :start-after: _GenericCAES-equations: + :end-before: """ + +.. note:: See the :py:class:`~oemof.solph.components.experimental._generic_caes.GenericCAES` class for all parameters and the mathematical background. + + +.. _oemof_solph_custom_sinkdsm_label: + +SinkDSM (experimental) +^^^^^^^^^^^^^^^^^^^^^^ + +:class:`~oemof.solph.custom.sink_dsm.SinkDSM` can used to represent flexibility in a demand time series. +It can represent both, load shifting or load shedding. +For load shifting, elasticity of the demand is described by upper (`~oemof.solph.custom.sink_dsm.SinkDSM.capacity_up`) and lower (`~oemof.solph.custom.SinkDSM.capacity_down`) bounds where within the demand is allowed to vary. +Upwards shifted demand is then balanced with downwards shifted demand. +For load shedding, shedding capability is described by `~oemof.solph.custom.SinkDSM.capacity_down`. +It both, load shifting and load shedding are allowed, `~oemof.solph.custom.SinkDSM.capacity_down` limits the sum of both downshift categories. + +:class:`~oemof.solph.custom.sink_dsm.SinkDSM` provides three approaches how the Demand-Side Management (DSM) flexibility is represented in constraints +It can be used for both, dispatch and investments modeling. + +* "DLR": Implementation of the DSM modeling approach from by Gils (2015): `Balancing of Intermittent Renewable Power Generation by Demand Response and Thermal Energy Storage, Stuttgart, `_, + Details: :class:`~oemof.solph.custom.sink_dsm.SinkDSMDLRBlock` and :class:`~oemof.solph.custom.sink_dsm.SinkDSMDLRInvestmentBlock` +* "DIW": Implementation of the DSM modeling approach by Zerrahn & Schill (2015): `On the representation of demand-side management in power system models `_, + in: Energy (84), pp. 840-845, 10.1016/j.energy.2015.03.037. Details: :class:`~oemof.solph.custom.sink_dsm.SinkDSMDIWBlock` and :class:`~oemof.solph.custom.sink_dsm.SinkDSMDIWInvestmentBlock` +* "oemof": Is a fairly simple approach. Within a defined windows of time steps, demand can be shifted within the defined bounds of elasticity. + The window sequentially moves forwards. Details: :class:`~oemof.solph.custom.sink_dsm.SinkDSMOemofBlock` and :class:`~oemof.solph.custom.sink_dsm.SinkDSMOemofInvestmentBlock` + +Cost can be associated to either demand up shifts or demand down shifts or both. + +This small example of PV, grid and SinkDSM shows how to use the component + +.. code-block:: python + + # Create some data + pv_day = [(-(1 / 6 * x ** 2) + 6) / 6 for x in range(-6, 7)] + pv_ts = [0] * 6 + pv_day + [0] * 6 + data_dict = {"demand_el": [3] * len(pv_ts), + "pv": pv_ts, + "Cap_up": [0.5] * len(pv_ts), + "Cap_do": [0.5] * len(pv_ts)} + data = pd.DataFrame.from_dict(data_dict) + + # Do timestamp stuff + datetimeindex = pd.date_range(start='1/1/2013', periods=len(data.index), freq='h') + data['timestamp'] = datetimeindex + data.set_index('timestamp', inplace=True) + + # Create Energy System + es = solph.EnergySystem(timeindex=datetimeindex) + + # Create bus representing electricity grid + b_elec = solph.buses.Bus(label='Electricity bus') + es.add(b_elec) + + # Create a back supply + grid = solph.components.Source(label='Grid', + outputs={ + b_elec: solph.flows.Flow( + nominal_value=10000, + variable_costs=50)} + ) + es.add(grid) + + # PV supply from time series + s_wind = solph.components.Source(label='wind', + outputs={ + b_elec: solph.flows.Flow( + fix=data['pv'], + nominal_value=3.5)} + ) + es.add(s_wind) + + # Create DSM Sink + demand_dsm = solph.custom.SinkDSM(label="DSM", + inputs={b_elec: solph.flows.Flow()}, + demand=data['demand_el'], + capacity_up=data["Cap_up"], + capacity_down=data["Cap_do"], + delay_time=6, + max_demand=1, + max_capacity_up=1, + max_capacity_down=1, + approach="DIW", + cost_dsm_down=5) + es.add(demand_dsm) + +Yielding the following results + +.. image:: _files/Plot_delay_2013-01-01.svg + :width: 85 % + :alt: Plot_delay_2013-01-01.svg + :align: center + + +.. note:: + * Keyword argument `method` from v0.4.1 has been renamed to `approach` in v0.4.2 and methods have been renamed. + * The parameters `demand`, `capacity_up` and `capacity_down` have been normalized to allow investments modeling. To retreive the original dispatch behaviour from v0.4.1, set `max_demand=1`, `max_capacity_up=1`, `max_capacity_down=1`. + * This component is a candidate component. It's implemented as a custom component for users that like to use and test the component at early stage. Please report issues to improve the component. + * See the :py:class:`~oemof.solph.custom.sink_dsm.SinkDSM` class for all parameters and the mathematical background. + + +.. _investment_mode_label: + +Investment optimisation +------------------------- + +As described in :ref:`oemof_solph_optimise_es_label` the typical way to optimise an energy system is the dispatch optimisation based on marginal costs. Solph also provides a combined dispatch and investment optimisation. +This standard investment mode is limited to one period where all investments happen at the start of the optimization time frame. If you want to optimize longer-term horizons and allow investments at the beginning +of each of multiple periods, also taking into account units lifetimes, you can try the :ref:`multi_period_mode_label`. Please be aware that the multi-period feature is experimental. If you experience any bugs or unexpected +behaviour, please report them. + +In the standard investment mode, based on investment costs you can compare the usage of existing components against building up new capacity. +The annual savings by building up new capacity must therefore compensate the annuity of the investment costs (the time period does not have to be one year, but depends on your Datetime index). + +See the API of the :py:class:`~oemof.solph.options.Investment` class to see all possible parameters. + +Basically, an instance of the Investment class can be added to a Flow, a +Storage or a DSM Sink. All parameters that usually refer to the *nominal_value/capacity* will +now refer to the investment variables and existing capacity. It is also +possible to set a maximum limit for the capacity that can be build. +If existing capacity is considered for a component with investment mode enabled, +the *ep_costs* still apply only to the newly built capacity, i.e. the existing capacity +comes at no costs. + +The investment object can be used in Flows and some components. See the +:ref:`oemof_solph_components_label` section for detailed information of each +component. Besides the flows, it can be invested into + +* :ref:`oemof_solph_components_generic_storage_label` and +* :ref:`oemof_solph_custom_sinkdsm_label` + +For example if you want to find out what would be the optimal capacity of a wind +power plant to decrease the costs of an existing energy system, you can define +this model and add an investment source. +The *wind_power_time_series* has to be a normalised feed-in time series of you +wind power plant. The maximum value might be caused by limited space for wind +turbines. + +.. code-block:: python + + solph.components.Source(label='new_wind_pp', outputs={electricity: solph.flows.Flow( + fix=wind_power_time_series, + nominal_value=solph.Investment(ep_costs=epc, maximum=50000))}) + +Let's slightly alter the case and consider for already existing wind power +capacity of 20,000 kW. We're still expecting the total wind power capacity, thus we +allow for 30,000 kW of new installations and formulate as follows. + +.. code-block:: python + + solph.components.Source(label='new_wind_pp', outputs={electricity: solph.flows.Flow( + fix=wind_power_time_series, + nominal_value=solph.Investment(ep_costs=epc, + maximum=30000, + existing=20000))}) + +The periodical costs (*ep_costs*) are typically calculated as annuities, i.e. as follows: + +.. code-block:: python + + capex = 1000 # investment cost + lifetime = 20 # life expectancy + wacc = 0.05 # weighted average of capital cost + epc = capex * (wacc * (1 + wacc) ** lifetime) / ((1 + wacc) ** lifetime - 1) + +This also implemented in the annuity function of the economics module in the oemof.tools package. The code above would look like this: + +.. code-block:: python + + from oemof.tools import economics + epc = economics.annuity(1000, 20, 0.05) + +So far, the investment costs and the installed capacity are mathematically a +line through origin. But what if there is a minimum threshold for doing an +investment, e.g. you cannot buy gas turbines lower than a certain +nominal power, or, the marginal costs of bigger plants +decrease. +Therefore, you can use the parameter *nonconvex* and *offset* of the +investment class. Both, work with investment in flows and storages. Here is an +example of a converter: + +.. code-block:: python + + trafo = solph.components.Converter( + label='converter_nonconvex', + inputs={bus_0: solph.flows.Flow()}, + outputs={bus_1: solph.flows.Flow( + nominal_value=solph.Investment( + ep_costs=4, + maximum=100, + minimum=20, + nonconvex=True, + offset=400))}, + conversion_factors={bus_1: 0.9}) + +In this examples, it is assumed, that independent of the size of the +converter, there are always fix investment costs of 400 (€). +The minimum investment size is 20 (kW) +and the costs per installed unit are 4 (€/kW). With this +option, you could theoretically approximate every cost function you want. But +be aware that for every nonconvex investment flow or storage you are using, +an additional binary variable is created. This might boost your computing time +into the limitless. + +The following figures illustrates the use of the nonconvex investment flow. +Here, :math:`c_{invest,fix}` is the *offset* value and :math:`c_{invest,var}` is +the *ep_costs* value: + +.. image:: _files/nonconvex_invest_investcosts_power.svg + :width: 70 % + :alt: nonconvex_invest_investcosts_power.svg + :align: center + +In case of a convex investment (which is the default setting +`nonconvex=False`), the *minimum* attribute leads to a forced investment, +whereas in the nonconvex case, the investment can become zero as well. + +The calculation of the specific costs per kilowatt installed capacity results +in the following relation for convex and nonconvex investments: + +.. image:: _files/nonconvex_invest_specific_costs.svg + :width: 70 % + :alt: nonconvex_invest_specific_costs.svg + :align: center + +See :py:class:`~oemof.solph.blocks.investment_flow.InvestmentFlow` and +:py:class:`~oemof.solph.components._generic_storage.GenericInvestmentStorageBlock` for all the +mathematical background, like variables and constraints, which are used. + +.. note:: At the moment the investment class is not compatible with the MIP classes :py:class:`~oemof.solph.options.NonConvex`. + + +.. _multi_period_mode_label: + +Multi-period (investment) mode (experimental) +--------------------------------------------- +Sometimes you might be interested in how energy systems could evolve in the longer-term, e.g. until 2045 or 2050 to meet some +carbon neutrality and climate protection or RES and energy efficiency targets. + +While in principle, you could try to model this in oemof.solph using the standard investment mode described above (see :ref:`investment_mode_label`), +you would make the implicit assumption that your entire system is built at the start of your optimization and doesn't change over time. +To address this shortcoming, the multi-period (investment) feature has been introduced. Be aware that it is still experimental. +So feel free to report any bugs or unexpected behaviour if you come across them. + +While in principle, you can define a dispatch-only multi-period system, this doesn't make much sense. The power of the multi-period feature +only unfolds if you look at long-term investments. Let's see how. + +First, you start by defining your energy system as you might have done before, but you + +* choose a longer-term time horizon (spanning multiple years, i.e. multiple periods) and +* explicitly define the `periods` attribute of your energy system which lists the time steps for each period. + +.. code-block:: python + + import pandas as pd + import oemof.solph as solph + + my_index = pd.date_range('1/1/2013', periods=17520, freq='h') + periods = [ + pd.date_range('1/1/2013', periods=8760, freq='h'), + pd.date_range('1/1/2014', periods=8760, freq='h'), + ] + my_energysystem = solph.EnergySystem(timeindex=my_index, periods=periods) + +If you want to use a multi-period model you have define periods of your energy system explicitly. This way, +you are forced to critically think, e.g. about handling leap years, and take some design decisions. It is possible to +define periods with different lengths, but remember that decommissioning of components is possible only at the +beginning of each period. This means that if the life of a component is just a little longer, it will remain for the +entire next period. This can have a particularly large impact the longer your periods are. + +To assist you, here is a plain python snippet that includes leap years which you can just copy +and adjust to your needs: + +.. code-block:: python + + def determine_periods(datetimeindex): + """Explicitly define and return periods of the energy system + + Leap years have 8784 hourly time steps, regular years 8760. + + Parameters + ---------- + datetimeindex : pd.date_range + DatetimeIndex of the model comprising all time steps + + Returns + ------- + periods : list + periods for the optimization run + """ + years = sorted(list(set(getattr(datetimeindex, "year")))) + periods = [] + filter_series = datetimeindex.to_series() + for number, year in enumerate(years): + start = filter_series.loc[filter_series.index.year == year].min() + end = filter_series.loc[filter_series.index.year == year].max() + periods.append(pd.date_range(start, end, freq=datetimeindex.freq)) + + return periods + +So if you want to use this, the above would simplify to: + +.. code-block:: python + + import pandas as pd + import oemof.solph as solph + + # Define your method (or import it from somewhere else) + def determine_periods(datetimeindex): + ... + + my_index = pd.date_range('1/1/2013', periods=17520, freq='h') + periods = determine_periods(my_index) # Make use of method + my_energysystem = solph.EnergySystem(timeindex=my_index, periods=periods) + + +Then you add all the *components* and *buses* to your energy system, just as you are used to with, but with few additions. + +.. code-block:: python + + hydrogen_bus = solph.buses.Bus(label="hydrogen") + coal_bus = solph.buses.Bus(label="coal") + electricity_bus = solph.buses.Bus(label="electricity") + + hydrogen_source = solph.components.Source( + label="green_hydrogen", + outputs={ + hydrogen_bus: solph.flows.Flow( + variable_costs=[25] * 8760 + [30] * 8760 + ) + }, + ) + + coal_source = solph.components.Source( + label="hardcoal", + outputs={ + coal_bus: solph.flows.Flow(variable_costs=[20] * 8760 + [24] * 8760) + }, + ) + + electrical_sink = solph.components.Sink( + label="electricity_demand", + inputs={ + electricity_bus: solph.flows.Flow( + nominal_value=1000, fix=[0.8] * len(my_index) + ) + }, + ) + +So defining buses is the same as for standard models. Also defining components that do not have any investments associated with +them or any lifetime limitations is the same. + +Now if you want to have components that can be invested into, you use the investment option, just as in :ref:`investment_mode_label`, +but with a few minor additions and modifications in the investment object itself which you specify by additional attributes: + +* You have to specify a `lifetime` attribute. This is the components assumed technical lifetime in years. If it is 20 years, + the model invests into it and your simulation has a 30 years horizon, the plant will be decommissioned. Now the model is + free to reinvest or choose another option to fill up the missing capacity. +* You can define an initial `age` if you have `existing` capacity. If you do not specify anything, the default value 0 will be used, + meaning your `existing` capacity has just been newly invested. +* You can define an `interest_rate` that the investor you model has, i.e. the return he desires expressed as the weighted + average osts of capital (wacc) and used for calculating annuities in the model itself. +* You also can define `fixed_costs`, i.e. costs that occur every period independent of the plants usage. + +Here is an example + +.. code-block:: python + + hydrogen_power_plant = solph.components.Converter( + label="hydrogen_pp", + inputs={hydrogen_bus: solph.flows.Flow()}, + outputs={ + electricity_bus: solph.flows.Flow( + nominal_value=solph.Investment( + maximum=1000, + ep_costs=1e6, + lifetime=30, + interest_rate=0.06, + fixed_costs=100, + ), + variable_costs=3, + ) + }, + conversion_factors={electricity_bus: 0.6}, + ) + +.. warning:: + + The `ep_costs` attribute for investments is used in a different way in a multi-period model. Instead + of periodical costs, it depicts (nominal or real) investment expenses, so actual Euros you have to pay per kW or MW + (or whatever power or energy unit) installed. Also, you can depict a change in investment expenses over time, + so instead of providing a scalar value, you could define a list with investment expenses with one value for each period modelled. + + Annuities are calculated within the model. You do not have to do that. + Also the model takes care of discounting future expenses / cashflows. + +Below is what it would look like if you altered `ep_costs` and `fixed_costs` per period. This can be done by simply +providing a list. Note that the length of the list must equal the number of periods of your model. +This would mean that for investments in the particular period, these values would be the one that are applied over their lifetime. + +.. code-block:: python + + hydrogen_power_plant = solph.components.Converter( + label="hydrogen_pp", + inputs={hydrogen_bus: solph.flows.Flow()}, + outputs={ + electricity_bus: solph.flows.Flow( + nominal_value=solph.Investment( + maximum=1000, + ep_costs=[1e6, 1.1e6], + lifetime=30, + interest_rate=0.06, + fixed_costs=[100, 110], + ), + variable_costs=3, + ) + }, + conversion_factors={electricity_bus: 0.6}, + ) + +For components that is not invested into, you also can specify some additional attributes for their inflows and outflows: + +* You can specify a `lifetime` attribute. This can be used to depict existing plants going offline when reaching their lifetime. +* You can define an initial `age`. Also, this can be used for existing plants. +* You also can define `fixed_costs`, i.e. costs that occur every period independent of the plants usage. How they are handled + depends on whether the flow has a limited or an unlimited lifetime. + +.. code-block:: python + + coal_power_plant = solph.components.Converter( + label="existing_coal_pp", + inputs={coal_bus: solph.flows.Flow()}, + outputs={ + electricity_bus: solph.flows.Flow( + nominal_value=600, + max=1, + min=0.4, + lifetime=50, + age=46, + fixed_costs=100, + variable_costs=3, + ) + }, + conversion_factors={electricity_bus: 0.36}, + ) + +To solve our model and retrieve results, you basically perform the same operations as for standard models. +So it works like this: + +.. code-block:: python + + my_energysystem.add( + hydrogen_bus, + coal_bus, + electricity_bus, + hydrogen_source, + coal_source, + electrical_sink, + hydrogen_power_plant, + coal_power_plant, + ) + + om = solph.Model(my_energysystem) + om.solve(solver="cbc", solve_kwargs={"tee": True}) + + # Obtain results + results = solph.processing.results(om) + hydrogen_results = solph.views.node(results, "hydrogen_pp") + + # Show investment plan for hydrogen power plants + print(hydrogen_results["period_scalars"]) + +The keys in the results dict in a multi-period model are "sequences" and "period_scalars". +So for sequences, it is all the same, while for scalar values, we now have values for each period. + +Besides the `invest` variable, new variables are introduced as well. These are: + +* `total`: The total capacity installed, i.e. how much is actually there in a given period. +* `old`: (Overall) capacity to be decommissioned in a given period. +* `old_end`: Endogenous capacity to be decommissioned in a given period. This is capacity that has been invested into + in the model itself. +* `old_exo`: Exogenous capacity to be decommissioned in a given period. This is capacity that was already existing and + given by the `existing` attribute. + +.. note:: + + * You can specify a `discount_rate` for the model. If you do not do so, 0.02 will be used as a default, corresponding + to sort of a social discount rate. If you work with costs in real terms, discounting is obsolete, so define + `discount_rate = 0` in that case. + * You can specify an `interest_rate` for every investment object. If you do not do so, it will be chosen the same + as the model's `discount_rate`. You could use this default to model a perfect competition administered by some sort of + social planner, but even in a social planner setting, you might want to deviate from the `discount_rate` + value and/or discriminate among technologies with different risk profiles and hence different interest requirements. + * For storage units, the `initial_content` is not allowed combined with multi-period investments. + The storage inflow and outflow are forced to zero until the storage unit is invested into. + * You can specify periods of different lengths, but the frequency of your timeindex needs to be consistent. Also, + you could use the `timeincrement` attribute of the energy system to model different weightings. Be aware that this + has not yet been tested. + * For now, both, the `timeindex` as well as the `timeincrement` of an energy system have to be defined since they + have to be of the same length for a multi-period model. + * You can choose whether to re-evaluate assets at the end of the optimization horizon. If you set attribute + `use_remaining_value` of the energy system to True (defaults to False), this leads to the model evaluating the + difference in the asset value at the end of the optimization horizon vs. at the time the investment was made. + The difference in value is added to or subtracted from the respective investment costs increment, + assuming assets are to be liquidated / re-evaluated at the end of the optimization horizon. + * Also please be aware, that periods correspond to years by default. You could also choose + monthly periods, but you would need to be very careful in parameterizing your energy system and your model and also, + this would mean monthly discounting (if applicable) as well as specifying your plants lifetimes in months. + + +Mixed Integer (Linear) Problems +------------------------------- + +Solph also allows you to model components with respect to more technical details, +such as minimum power production. This can be done in both possible combinations, +as dispatch optimization with fixed capacities or combined dispatch and investment optimization. + +Dispatch Optimization +^^^^^^^^^^^^^^^^^^^^^ +In dispatch optimization, it is assumed that the capacities of the assets are already known, +but the optimal dispatch strategy must be obtained. +For this purpose, the class :py:class:`~oemof.solph._options.NonConvex` should be used, as seen in the following example. + +Note that this flow class's usage is incompatible with the :py:mod:`~oemof.solph.options.Investment` option. This means that, +as stated before, the optimal capacity of the converter cannot be obtained using the :py:class:`~oemof.solph.flows.NonConvexFlow` +class, and only the optimal dispatch strategy of an existing asset with a given capacity can be optimized here. + +.. code-block:: python + + b_gas = solph.buses.Bus(label='natural_gas') + b_el = solph.buses.Bus(label='electricity') + b_th = solph.buses.Bus(label='heat') + + solph.components.Converter( + label='pp_chp', + inputs={b_gas: solph.flows.Flow()}, + outputs={b_el: solph.flows.Flow( + nonconvex=solph.NonConvex(), + nominal_value=30, + min=0.5), + b_th: solph.flows.Flow(nominal_value=40)}, + conversion_factors={b_el: 0.3, b_th: 0.4}) + +The class :py:class:`~oemof.solph.options.NonConvex` for the electrical output of the created Converter (i.e., CHP) +will create a 'status' variable for the flow. +This will be used to model, for example, minimal/maximal power production constraints if the +attributes `min`/`max` of the flow are set. It will also be used to include start-up constraints and costs +if corresponding attributes of the class are provided. For more information, see the API of the +:py:class:`~oemof.solph.flows.NonConvexFlow` class. + +.. note:: The usage of this class can sometimes be tricky as there are many interdenpendencies. So + check out the examples and do not hesitate to ask the developers if your model does + not work as expected. + +Combination of Dispatch and Investment Optimisation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Since version 'v0.5', it is also possilbe to combine the investment and nonconvex option. +Therefore, a new constraint block for flows, called :py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` has been developed, +which combines both :py:class:`~oemof.solph._options.Investment` and :py:class:`~oemof.solph._options.NonConvex` classes. +The new class offers the possibility to perform the investment optimization of an asset considering `min`/`max` values of the flow +as fractions of the optimal capacity. Moreover, it obtains the optimal 'status' of the flow during the simulation period. + +It must be noted that in a straighforward implementation, a binary variable +representing the 'status' of the flow at each time is multiplied by the 'invest' parameter, +which is a continuous variable representing the capacity of the asset being optimized (i.e., :math:`status \times invest`). +This nonlinearity is linearised in the +:py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` + +.. code-block:: python + + b_diesel = solph.buses.Bus(label='diesel') + b_el = solph.buses.Bus(label='electricity') + + solph.components.Converter( + label='diesel_genset', + inputs={b_diesel: solph.flows.Flow()}, + outputs={ + b_el: solph.flows.Flow( + variable_costs=0.04, + min=0.2, + max=1, + nonconvex=solph.NonConvex(), + nominal_value=solph.Investment( + ep_costs=90, + maximum=150, # required for the linearization + ), + ) + }, + conversion_factors={b_el: 0.3}) + +The following diagram shows the duration curve of a typical diesel genset in a hybrid mini-grid system consisting of a diesel genset, +PV cells, battery, inverter, and rectifier. By using the :py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` class, +it is possible to obtain the optimal capacity of this component and simultaneously limit its operation between `min` and `max` loads. + +.. image:: _files/diesel_genset_nonconvex_invest_flow.svg + :width: 100 % + :alt: diesel_genset_nonconvex_invest_flow.svg + :align: center + +Without using the new :py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` class, if the same system is optimized again, but this +time using the :py:class:`~oemof.solph.flows._investment_flow_block.InvestmentFlowBlock`, the corresponding duration curve would be similar to the following +figure. However, assuming that the diesel genset has a minimum operation load of 20% (as seen in the figure), the +:py:class:`~oemof.solph.flows._investment_flow_block.InvestmentFlowBlock` cannot prevent operations at lower loads than 20%, and it would result in +an infeasible operation of this device for around 50% of its annual operation. + +Moreover, using the :py:class:`~oemof.solph.flows._investment_flow_block.InvestmentFlowBlock` class in the given case study would result in a significantly +oversized diesel genset, which has a 30% larger capacity compared with the optimal capacity obtained from the +:py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` class. + +.. image:: _files/diesel_genset_investment_flow.svg + :width: 100 % + :alt: diesel_genset_investment_flow.svg + :align: center + +Solving such an optimisation problem considering `min`/`max` loads without the :py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` class, the only possibility is first to obtain the optimal capacity using the +:py:class:`~oemof.solph.flows._investment_flow_block.InvestmentFlowBlock` and then implement the `min`/`max` loads using the +:py:class:`~oemof.solph.flows._non_convex_flow_block.NonConvexFlowBlock` class. The following duration curve would be obtained by applying +this method to the same diesel genset. + +.. image:: _files/diesel_genset_nonconvex_flow.svg + :width: 100 % + :alt: diesel_genset_nonconvex_flow.svg + :align: center + +Because of the oversized diesel genset obtained from this approach, the capacity of the PV and battery in the given case study +would be 13% and 43% smaller than the capacities obtained using the :py:class:`~oemof.solph.flows.NonConvexInvestmentFlow` class. +This results in a 15% reduction in the share of renewable energy sources to cover the given demand and a higher levelized +cost of electricity. Last but not least, apart from the nonreliable results, using :py:class:`~oemof.solph._options.Investment` +and :py:class:`~oemof.solph._options.NonConvex` classes for the dispatch and investment optimization of the given case study +increases the computation time by more than 9 times compared to the +:py:class:`~oemof.solph.flows.NonConvexInvestmentFlow` class. + + +Adding additional constraints +----------------------------- + +You can add additional constraints to your :py:class:`~oemof.solph.models.Model`. +See :ref:`custom_constraints_label` to learn how to do it. + +Some predefined additional constraints can be found in the +:py:mod:`~oemof.solph.constraints` module. + + * Emission limit for the model -> :func:`~.oemof.solph.constraints.emission_limit` + * Generic integral limit (general form of emission limit) -> :func:`~.oemof.solph.constraints.generic_integral_limit` + * Coupling of two variables e.g. investment variables) with a factor -> :func:`~.oemof.solph.constraints.equate_variables` + * Overall investment limit -> :func:`~.oemof.solph.constraints.investment_limit` + * Generic investment limit -> :func:`~.oemof.solph.constraints.additional_investment_flow_limit` + * Limit active flow count -> :func:`~.oemof.solph.constraints.limit_active_flow_count` + * Limit active flow count by keyword -> :func:`~.oemof.solph.constraints.limit_active_flow_count_by_keyword` + + +The Grouping module (Sets) +-------------------------- +To construct constraints, +variables and objective expressions inside all Block classes +and the :py:mod:`~oemof.solph.models` modules, so called groups are used. Consequently, +certain constraints are created for all elements of a specific group. Thus, +mathematically the groups depict sets of elements inside the model. + +The grouping is handled by the solph grouping module :py:mod:`~oemof.solph.groupings` +which is based on the groupings module functionality of oemof network. You +do not need to understand how the underlying functionality works. Instead, checkout +how the solph grouping module is used to create groups. + +The simplest form is a function that looks at every node of the energy system and +returns a key for the group depending e.g. on node attributes: + +.. code-block:: python + + def constraint_grouping(node): + if isinstance(node, Bus) and node.balanced: + return blocks.Bus + if isinstance(node, Converter): + return blocks.Converter + GROUPINGS = [constraint_grouping] + +This function can be passed in a list to `groupings` of +:class:`oemof.solph.network.energy_system.EnergySystem`. So that we end up with two groups, +one with all Converters and one with all Buses that are balanced. These +groups are simply stored in a dictionary. There are some advanced functionalities +to group two connected nodes with their connecting flow and others +(see for example: FlowsWithNodes class in the oemof.network package). + + +Using the Excel (csv) reader +---------------------------- + +Alternatively to a manual creation of energy system component objects as describe above, can also be created from a excel sheet (libreoffice, gnumeric...). + +The idea is to create different sheets within one spreadsheet file for different components. Afterwards you can loop over the rows with the attributes in the columns. The name of the columns may differ from the name of the attribute. You may even create two sheets for the GenericStorage class with attributes such as C-rate for batteries or capacity of turbine for a PHES. + +Once you have create your specific excel reader you can lower the entry barrier for other users. It is some sort of a GUI in form of platform independent spreadsheet software and to make data and models exchangeable in one archive. + +See :ref:`excel_reader_example_label` for an excel reader example. + + +.. _oemof_outputlib_label: + +Handling Results +-------------------- + +The main purpose of the processing module is to collect and organise results. +The views module will provide some typical representations of the results. +Plots are not part of solph, because plots are highly individual. However, the +provided pandas.DataFrames are a good start for plots. Some basic functions +for plotting of optimisation results can be found in the separate repository +`oemof_visio `_. + +The ``processing.results`` function gives back the results as a python +dictionary holding pandas Series for scalar values and pandas DataFrames for +all nodes and flows between them. This way we can make use of the full power +of the pandas package available to process the results. + +See the `pandas documentation `_ +to learn how to `visualise +`_, +`read or write +`_ or how to +`access parts of the DataFrame +`_ to +process them. + +The results chapter consists of three parts: + +.. contents:: + :depth: 1 + :local: + :backlinks: top + +The first step is the processing of the results (:ref:`results_collect_results_label`) +This is followed by basic examples of the general analysis of the results +(:ref:`res_general_approach_label`) and finally the use of functionality already included in solph +for providing a quick access to your results (:ref:`results_easy_access_label`). +Especially for larger energy systems the general approach will help you to +write your own results processing functions. + +.. _results_collect_results_label: + +Collecting results +^^^^^^^^^^^^^^^^^^ + +Collecting results can be done with the help of the processing module. A solved +model is needed: + +.. code-block:: python + + [...] + model.solve(solver=solver) + results = solph.processing.results(model) + +The scalars and sequences describe nodes (with keys like (node, None)) and +flows between nodes (with keys like (node_1, node_2)). You can directly extract +the data in the dictionary by using these keys, where "node" is the name of +the object you want to address. +Processing the results is the prerequisite for the examples in the following +sections. + +.. _res_general_approach_label: + +General approach +^^^^^^^^^^^^^^^^ + +As stated above, after processing you will get a dictionary with all result +data. +If you want to access your results directly via labels, you +can continue with :ref:`results_easy_access_label`. For a systematic analysis list comprehensions +are the easiest way of filtering and analysing your results. + +The keys of the results dictionary are tuples containing two nodes. Since flows +have a starting node and an ending node, you get a list of all flows by +filtering the results using the following expression: + +.. code-block:: python + + flows = [x for x in results.keys() if x[1] is not None] + +On the same way you can get a list of all nodes by applying: + +.. code-block:: python + + nodes = [x for x in results.keys() if x[1] is None] + +Probably you will just get storages as nodes, if you have some in your energy +system. Note, that just nodes containing decision variables are listed, e.g. a +Source or a Converter object does not have decision variables. These are in +the flows from or to the nodes. + +All items within the results dictionary are dictionaries and have two items +with 'scalars' and 'sequences' as keys: + +.. code-block:: python + + for flow in flows: + print(flow) + print(results[flow]['scalars']) + print(results[flow]['sequences']) + +There many options of filtering the flows and nodes as you prefer. +The following will give you all flows which are outputs of converter: + +.. code-block:: python + + flows_from_converter = [x for x in flows if isinstance( + x[0], solph.components.Converter)] + +You can filter your flows, if the label of in- or output contains a given +string, e.g.: + +.. code-block:: python + + flows_to_elec = [x for x in results.keys() if 'elec' in x[1].label] + +Getting all labels of the starting node of your investment flows: + +.. code-block:: python + + flows_invest = [x[0].label for x in flows if hasattr( + results[x]['scalars'], 'invest')] + + +.. _results_easy_access_label: + +Easy access +^^^^^^^^^^^ + +The solph package provides some functions which will help you to access your +results directly via labels, which is helpful especially for small energy +systems. +So, if you want to address objects by their label, you can convert the results +dictionary such that the keys are changed to strings given by the labels: + +.. code-block:: python + + views.convert_keys_to_strings(results) + print(results[('wind', 'bus_electricity')]['sequences'] + + +Another option is to access data belonging to a grouping by the name of the grouping +(`note also this section on groupings `_. +Given the label of an object, e.g. 'wind' you can access the grouping by its label +and use this to extract data from the results dictionary. + +.. code-block:: python + + node_wind = energysystem.groups['wind'] + print(results[(node_wind, bus_electricity)]) + + +However, in many situations it might be convenient to use the views module to +collect information on a specific node. You can request all data related to a +specific node by using either the node's variable name or its label: + +.. code-block:: python + + data_wind = solph.views.node(results, 'wind') + + +A function for collecting and printing meta results, i.e. information on the objective function, +the problem and the solver, is provided as well: + +.. code-block:: python + + meta_results = solph.processing.meta_results(om) + pp.pprint(meta_results) From 50bb40d20b3164c17a42d9274f57dce4c8349a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 26 Nov 2024 09:03:44 +0100 Subject: [PATCH 30/45] Update link to CBC solver --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 6d69c9571..8767b3afa 100644 --- a/README.rst +++ b/README.rst @@ -189,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/ From e6343d656ea58cbfcb63a7e8b691e20b314019c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 26 Nov 2024 09:08:21 +0100 Subject: [PATCH 31/45] Update to gh/actions/cache@v4 --- .github/workflows/tox_checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tox_checks.yml b/.github/workflows/tox_checks.yml index 5553b0acf..e5cf4fa76 100644 --- a/.github/workflows/tox_checks.yml +++ b/.github/workflows/tox_checks.yml @@ -39,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') }} From 0376a1051bc4e9a5ffa6bd91202ceedfe22031c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 26 Nov 2024 09:13:03 +0100 Subject: [PATCH 32/45] Update sites ignored in linkcheck --- docs/conf.py | 162 +++++++++++++++++++++++++-------------------------- 1 file changed, 79 insertions(+), 83 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index d81c66685..dd86a87a6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,83 +1,79 @@ -# -*- coding: utf-8 -*- - -import os -import sys - -import matplotlib -from sphinx.ext.autodoc import between - -from oemof.solph import __version__ - - -matplotlib.use("agg") -sys.path.append(os.path.join(os.path.dirname(__file__), "..", "examples")) - - -def setup(app): - # Register a sphinx.ext.autodoc.between listener to ignore everything - # between lines that contain the word IGNORE - app.connect("autodoc-process-docstring", between("^SPDX.*$", exclude=True)) - return app - - -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.autosummary", - "sphinx.ext.coverage", - "sphinx.ext.doctest", - "sphinx.ext.extlinks", - "sphinx.ext.ifconfig", - "sphinx.ext.napoleon", - "sphinx.ext.todo", - "sphinx.ext.viewcode", - "sphinx_copybutton", - "sphinx_design", -] -source_suffix = ".rst" -master_doc = "index" -project = "oemof.solph" -year = "2014-2023" -author = "oemof-developer-group" -copyright = "{0}, {1}".format(year, author) -version = release = __version__ - -pygments_style = "trac" -templates_path = ["."] -extlinks = { - "issue": ("https://github.com/oemof/oemof-solph/issues/%s", "#%s"), - "pr": ("https://github.com/oemof/oemof-solph/pull/%s", "PR #%s"), -} -# on_rtd is whether we are on readthedocs.org -on_rtd = os.environ.get("READTHEDOCS", None) == "True" - -if not on_rtd: # only set the theme if we're building docs locally - html_theme = "sphinx_rtd_theme" - -html_use_smartypants = True -html_last_updated_fmt = "%b %d, %Y" -html_split_index = False -html_sidebars = { - "**": ["searchbox.html", "globaltoc.html", "sourcelink.html"], -} -html_short_title = "%s-%s" % (project, version) -html_logo = "./_logo/logo_oemof_solph_COMPACT_bg.svg" - -napoleon_use_ivar = True -napoleon_use_rtype = False -napoleon_use_param = False -nitpicky = False - -exclude_patterns = ["_build", "whatsnew/*"] - -linkcheck_ignore = [ - 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 [] -) +# -*- coding: utf-8 -*- + +import os +import sys + +import matplotlib +from sphinx.ext.autodoc import between + +from oemof.solph import __version__ + + +matplotlib.use("agg") +sys.path.append(os.path.join(os.path.dirname(__file__), "..", "examples")) + + +def setup(app): + # Register a sphinx.ext.autodoc.between listener to ignore everything + # between lines that contain the word IGNORE + app.connect("autodoc-process-docstring", between("^SPDX.*$", exclude=True)) + return app + + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.coverage", + "sphinx.ext.doctest", + "sphinx.ext.extlinks", + "sphinx.ext.ifconfig", + "sphinx.ext.napoleon", + "sphinx.ext.todo", + "sphinx.ext.viewcode", + "sphinx_copybutton", + "sphinx_design", +] +source_suffix = ".rst" +master_doc = "index" +project = "oemof.solph" +year = "2014-2023" +author = "oemof-developer-group" +copyright = "{0}, {1}".format(year, author) +version = release = __version__ + +pygments_style = "trac" +templates_path = ["."] +extlinks = { + "issue": ("https://github.com/oemof/oemof-solph/issues/%s", "#%s"), + "pr": ("https://github.com/oemof/oemof-solph/pull/%s", "PR #%s"), +} +# on_rtd is whether we are on readthedocs.org +on_rtd = os.environ.get("READTHEDOCS", None) == "True" + +if not on_rtd: # only set the theme if we're building docs locally + html_theme = "sphinx_rtd_theme" + +html_use_smartypants = True +html_last_updated_fmt = "%b %d, %Y" +html_split_index = False +html_sidebars = { + "**": ["searchbox.html", "globaltoc.html", "sourcelink.html"], +} +html_short_title = "%s-%s" % (project, version) +html_logo = "./_logo/logo_oemof_solph_COMPACT_bg.svg" + +napoleon_use_ivar = True +napoleon_use_rtype = False +napoleon_use_param = False +nitpicky = False + +exclude_patterns = ["_build", "whatsnew/*"] + +linkcheck_ignore = [ + 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/*", + "https://www.sciencedirect.com/science/article/abs/pii/S036054421500331X", # 403 in CI pipeline +] From b5369c880116fd77048cdf349b3cda30d3ebc9e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 26 Nov 2024 09:17:14 +0100 Subject: [PATCH 33/45] Shorten lines --- docs/conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index dd86a87a6..630948655 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -75,5 +75,6 @@ def setup(app): r"https://forum.openmod-initiative.org/*", r"https://github.com/oemof/oemof-solph/issues/*", r"https://github.com/oemof/oemof-solph/pull/*", - "https://www.sciencedirect.com/science/article/abs/pii/S036054421500331X", # 403 in CI pipeline + # Due to traffic limitation, the folowwing creates a 403 in CI pipeline: + "https://www.sciencedirect.com/science/article/abs/pii/S036054421500331X", ] From 8b22e80493c69707e422682dc7313a9b3770743c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 26 Nov 2024 09:21:05 +0100 Subject: [PATCH 34/45] Fix line endings --- README.rst | 578 ++++---- docs/conf.py | 160 +-- docs/usage.rst | 3414 ++++++++++++++++++++++++------------------------ 3 files changed, 2076 insertions(+), 2076 deletions(-) diff --git a/README.rst b/README.rst index 8767b3afa..73819ba26 100644 --- a/README.rst +++ b/README.rst @@ -1,289 +1,289 @@ - -|tox-pytest| |tox-checks| |appveyor| |coveralls| |codecov| - -|scrutinizer| |codacy| |codeclimate| - -|wheel| |packaging| |supported-versions| - -|docs| |zenodo| - -|version| |commits-since| |chat| - - ------------------------------- - -.. |tox-pytest| image:: https://github.com/oemof/oemof-solph/workflows/tox%20pytests/badge.svg?branch=dev - :target: https://github.com/oemof/oemof-solph/actions?query=workflow%3A%22tox+checks%22 - -.. |tox-checks| image:: https://github.com/oemof/oemof-solph/workflows/tox%20checks/badge.svg?branch=dev - :target: https://github.com/oemof/oemof-solph/actions?query=workflow%3A%22tox+checks%22 - -.. |packaging| image:: https://github.com/oemof/oemof-solph/workflows/packaging/badge.svg?branch=dev - :target: https://github.com/oemof/oemof-solph/actions?query=workflow%3Apackaging - -.. |docs| image:: https://readthedocs.org/projects/oemof-solph/badge/?style=flat - :target: https://readthedocs.org/projects/oemof-solph - :alt: Documentation Status - -.. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/oemof/oemof-solph?branch=dev&svg=true - :alt: AppVeyor Build Status - :target: https://ci.appveyor.com/project/oemof-developer/oemof-solph - -.. |coveralls| image:: https://coveralls.io/repos/oemof/oemof-solph/badge.svg?branch=dev&service=github - :alt: Coverage Status - :target: https://coveralls.io/github/oemof/oemof-solph - -.. |codecov| image:: https://codecov.io/gh/oemof/oemof-solph/branch/dev/graphs/badge.svg?branch=dev - :alt: Coverage Status - :target: https://codecov.io/gh/oemof/oemof-solph - -.. |codacy| image:: https://api.codacy.com/project/badge/Grade/a6e5cb2dd2694c73895e142e4cf680d5 - :target: https://app.codacy.com/gh/oemof/oemof-solph/dashboard - :alt: Codacy Code Quality Status - -.. |codeclimate| image:: https://codeclimate.com/github/oemof/oemof-solph/badges/gpa.svg - :target: https://codeclimate.com/github/oemof/oemof-solph - :alt: CodeClimate Quality Status - -.. |version| image:: https://img.shields.io/pypi/v/oemof.solph.svg - :alt: PyPI Package latest release - :target: https://pypi.org/project/oemof.solph - -.. |wheel| image:: https://img.shields.io/pypi/wheel/oemof.solph.svg - :alt: PyPI Wheel - :target: https://pypi.org/project/oemof.solph - -.. |supported-versions| image:: https://img.shields.io/pypi/pyversions/oemof.solph.svg - :alt: Supported versions - :target: https://pypi.org/project/oemof.solph - -.. |supported-implementations| image:: https://img.shields.io/pypi/implementation/oemof.solph.svg - :alt: Supported implementations - :target: https://pypi.org/project/oemof.solph - -.. |commits-since| image:: https://img.shields.io/github/commits-since/oemof/oemof-solph/latest/dev - :alt: Commits since latest release - :target: https://github.com/oemof/oemof-solph/compare/master...dev - -.. |zenodo| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.596235.svg - :alt: Zenodo DOI - :target: https://doi.org/10.5281/zenodo.596235 - -.. |scrutinizer| image:: https://img.shields.io/scrutinizer/quality/g/oemof/oemof-solph/dev.svg - :alt: Scrutinizer Status - :target: https://scrutinizer-ci.com/g/oemof/oemof-solph/ - -.. |chat| image:: https://img.shields.io/badge/chat-oemof:matrix.org-%238ADCF7 - :alt: matrix-chat - :target: https://matrix.to/#/#oemof:matrix.org - - -.. figure:: https://raw.githubusercontent.com/oemof/oemof-solph/492e3f5a0dda7065be30d33a37b0625027847518/docs/_logo/logo_oemof_solph_FULL.svg - :align: center - ------------------------------- - -=========== -oemof.solph -=========== - -**A model generator for energy system modelling and optimisation (LP/MILP)** - -.. contents:: - :depth: 2 - :local: - :backlinks: top - - -Introduction -============ - -The oemof.solph package is part of the -`Open energy modelling framework (oemof) `_. -This is an organisational framework to bundle tools for energy (system) modelling. -oemof-solph is a model generator for energy system modelling and optimisation. - -The package ``oemof.solph`` is very often called just ``oemof``. -This is because installing the ``oemof`` meta package was once the best way to get ``oemof.solph``. -Notice that you should prefeably install ``oemof.solph`` instead of ``oemof`` -if you want to use ``solph``. - - -Everybody is welcome to use and/or develop oemof.solph. -Read our `contribution `_ section. - -Contribution is already possible on a low level by simply fixing typos in -oemof's documentation or rephrasing sections which are unclear. -If you want to support us that way please fork the oemof-solph repository to your own -GitHub account and make changes as described in the `github guidelines `_ - -If you have questions regarding the use of oemof including oemof.solph you can visit the openmod forum (`tag oemof `_ or `tag oemof-solph `_) and open a new thread if your questions hasn't been already answered. - -Keep in touch! - You can become a watcher at our `github site `_, -but this will bring you quite a few mails and might be more interesting for developers. -If you just want to get the latest news, like when is the next oemof meeting, -you can follow our news-blog at `oemof.org `_. - -Documentation -============= -The `oemof.solph documentation `_ is powered by readthedocs. Use the `project site `_ of oemof.solph to choose the version of the documentation. Go to the `download page `_ to download different versions and formats (pdf, html, epub) of the documentation. - - -.. _installation_label: - -Installation -============ - - -If you have a working Python installation, use pypi to install the latest version of oemof.solph. -Python >= 3.8 is recommended. Lower versions may work but are not tested. - -We highly recommend to use virtual environments. -Please refer to the documentation of your Python distribution (e.g. Anaconda, -Micromamba, or the version of Python that came with your Linux installation) -to learn how to set up and use virtual environments. - -:: - - (venv) pip install oemof.solph - -If you want to use the latest features, you might want to install the **developer version**. The developer version is not recommended for productive use:: - - (venv) pip install https://github.com/oemof/oemof-solph/archive/dev.zip - - -For running an oemof-solph optimisation model, you need to install a solver. -Following you will find guidelines for the installation process for different operating systems. - -.. _windows_solver_label: -.. _linux_solver_label: - -Installing a solver -------------------- - -There are several solvers that can work with oemof, both open source and commercial. -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. - -Check the solver installation by executing the test_installation example below (see section Installation Test). - -**Linux** - -To install the solvers have a look at the package repository of your Linux distribution or search for precompiled packages. GLPK and CBC ares available at Debian, Feodora, Ubuntu and others. - -**Windows** - - 1. Download `CBC `_ - 2. Download `GLPK (64/32 bit) `_ - 3. Unpack CBC/GLPK to any folder (e.g. C:/Users/Somebody/my_programs) - 4. Add the path of the executable files of both solvers to the PATH variable using `this tutorial `_ - 5. Restart Windows - -Check the solver installation by executing the test_installation example (see the `Installation test` section). - - -**Mac OSX** - -Please follow the installation instructions on the respective homepages for details. - -CBC-solver: https://github.com/coin-or/Cbc - -GLPK-solver: http://arnab-deka.com/posts/2010/02/installing-glpk-on-a-mac/ - -If you install the CBC solver via brew (highly recommended), it should work without additional configuration. - - -**conda** - -Provided you are using a Linux or MacOS, the CBC-solver can also be installed in a `conda` environment. Please note, that it is highly recommended to `use pip after conda `_, so: - -.. code:: console - - (venv) conda install -c conda-forge coincbc - (venv) pip install oemof.solph - - -.. _check_installation_label: - -Installation test ------------------ - -Test the installation and the installed solver by running the installation test -in your virtual environment: - -.. code:: console - - (venv) oemof_installation_test - -If the installation was successful, you will receive something like this: - -.. code:: console - - ********* - Solver installed with oemof: - glpk: working - cplex: not working - cbc: working - gurobi: not working - ********* - oemof.solph successfully installed. - -as an output. - -Contributing -============ - -A warm welcome to all who want to join the developers and contribute to -oemof.solph. - -Information on the details and how to approach us can be found -`in the oemof documentation `_ . - -Citing -====== - -For explicitly citing solph, you might want to refer to -`DOI:10.1016/j.simpa.2020.100028 `_, -which gives an overview over the capabilities of solph. -The core ideas of oemof as a whole are described in -`DOI:10.1016/j.esr.2018.07.001 `_ -(preprint at `arXiv:1808.0807 `_). -To allow citing specific versions, we use the zenodo project to get a DOI for each version. - -Example Applications -==================== - -The combination of specific modules (often including other packages) is called an -application (app). For example, it can depict a concrete energy system model. -You can find a large variety of helpful examples in the documentation. -The examples show the optimisation of different energy systems and are supposed -to help new users to understand the framework's structure. - -You are welcome to contribute your own examples via a `pull request `_ -or by e-mailing us (see `here `_ for contact information). - -License -======= - -Copyright (c) oemof developer group - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + +|tox-pytest| |tox-checks| |appveyor| |coveralls| |codecov| + +|scrutinizer| |codacy| |codeclimate| + +|wheel| |packaging| |supported-versions| + +|docs| |zenodo| + +|version| |commits-since| |chat| + + +------------------------------ + +.. |tox-pytest| image:: https://github.com/oemof/oemof-solph/workflows/tox%20pytests/badge.svg?branch=dev + :target: https://github.com/oemof/oemof-solph/actions?query=workflow%3A%22tox+checks%22 + +.. |tox-checks| image:: https://github.com/oemof/oemof-solph/workflows/tox%20checks/badge.svg?branch=dev + :target: https://github.com/oemof/oemof-solph/actions?query=workflow%3A%22tox+checks%22 + +.. |packaging| image:: https://github.com/oemof/oemof-solph/workflows/packaging/badge.svg?branch=dev + :target: https://github.com/oemof/oemof-solph/actions?query=workflow%3Apackaging + +.. |docs| image:: https://readthedocs.org/projects/oemof-solph/badge/?style=flat + :target: https://readthedocs.org/projects/oemof-solph + :alt: Documentation Status + +.. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/oemof/oemof-solph?branch=dev&svg=true + :alt: AppVeyor Build Status + :target: https://ci.appveyor.com/project/oemof-developer/oemof-solph + +.. |coveralls| image:: https://coveralls.io/repos/oemof/oemof-solph/badge.svg?branch=dev&service=github + :alt: Coverage Status + :target: https://coveralls.io/github/oemof/oemof-solph + +.. |codecov| image:: https://codecov.io/gh/oemof/oemof-solph/branch/dev/graphs/badge.svg?branch=dev + :alt: Coverage Status + :target: https://codecov.io/gh/oemof/oemof-solph + +.. |codacy| image:: https://api.codacy.com/project/badge/Grade/a6e5cb2dd2694c73895e142e4cf680d5 + :target: https://app.codacy.com/gh/oemof/oemof-solph/dashboard + :alt: Codacy Code Quality Status + +.. |codeclimate| image:: https://codeclimate.com/github/oemof/oemof-solph/badges/gpa.svg + :target: https://codeclimate.com/github/oemof/oemof-solph + :alt: CodeClimate Quality Status + +.. |version| image:: https://img.shields.io/pypi/v/oemof.solph.svg + :alt: PyPI Package latest release + :target: https://pypi.org/project/oemof.solph + +.. |wheel| image:: https://img.shields.io/pypi/wheel/oemof.solph.svg + :alt: PyPI Wheel + :target: https://pypi.org/project/oemof.solph + +.. |supported-versions| image:: https://img.shields.io/pypi/pyversions/oemof.solph.svg + :alt: Supported versions + :target: https://pypi.org/project/oemof.solph + +.. |supported-implementations| image:: https://img.shields.io/pypi/implementation/oemof.solph.svg + :alt: Supported implementations + :target: https://pypi.org/project/oemof.solph + +.. |commits-since| image:: https://img.shields.io/github/commits-since/oemof/oemof-solph/latest/dev + :alt: Commits since latest release + :target: https://github.com/oemof/oemof-solph/compare/master...dev + +.. |zenodo| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.596235.svg + :alt: Zenodo DOI + :target: https://doi.org/10.5281/zenodo.596235 + +.. |scrutinizer| image:: https://img.shields.io/scrutinizer/quality/g/oemof/oemof-solph/dev.svg + :alt: Scrutinizer Status + :target: https://scrutinizer-ci.com/g/oemof/oemof-solph/ + +.. |chat| image:: https://img.shields.io/badge/chat-oemof:matrix.org-%238ADCF7 + :alt: matrix-chat + :target: https://matrix.to/#/#oemof:matrix.org + + +.. figure:: https://raw.githubusercontent.com/oemof/oemof-solph/492e3f5a0dda7065be30d33a37b0625027847518/docs/_logo/logo_oemof_solph_FULL.svg + :align: center + +------------------------------ + +=========== +oemof.solph +=========== + +**A model generator for energy system modelling and optimisation (LP/MILP)** + +.. contents:: + :depth: 2 + :local: + :backlinks: top + + +Introduction +============ + +The oemof.solph package is part of the +`Open energy modelling framework (oemof) `_. +This is an organisational framework to bundle tools for energy (system) modelling. +oemof-solph is a model generator for energy system modelling and optimisation. + +The package ``oemof.solph`` is very often called just ``oemof``. +This is because installing the ``oemof`` meta package was once the best way to get ``oemof.solph``. +Notice that you should prefeably install ``oemof.solph`` instead of ``oemof`` +if you want to use ``solph``. + + +Everybody is welcome to use and/or develop oemof.solph. +Read our `contribution `_ section. + +Contribution is already possible on a low level by simply fixing typos in +oemof's documentation or rephrasing sections which are unclear. +If you want to support us that way please fork the oemof-solph repository to your own +GitHub account and make changes as described in the `github guidelines `_ + +If you have questions regarding the use of oemof including oemof.solph you can visit the openmod forum (`tag oemof `_ or `tag oemof-solph `_) and open a new thread if your questions hasn't been already answered. + +Keep in touch! - You can become a watcher at our `github site `_, +but this will bring you quite a few mails and might be more interesting for developers. +If you just want to get the latest news, like when is the next oemof meeting, +you can follow our news-blog at `oemof.org `_. + +Documentation +============= +The `oemof.solph documentation `_ is powered by readthedocs. Use the `project site `_ of oemof.solph to choose the version of the documentation. Go to the `download page `_ to download different versions and formats (pdf, html, epub) of the documentation. + + +.. _installation_label: + +Installation +============ + + +If you have a working Python installation, use pypi to install the latest version of oemof.solph. +Python >= 3.8 is recommended. Lower versions may work but are not tested. + +We highly recommend to use virtual environments. +Please refer to the documentation of your Python distribution (e.g. Anaconda, +Micromamba, or the version of Python that came with your Linux installation) +to learn how to set up and use virtual environments. + +:: + + (venv) pip install oemof.solph + +If you want to use the latest features, you might want to install the **developer version**. The developer version is not recommended for productive use:: + + (venv) pip install https://github.com/oemof/oemof-solph/archive/dev.zip + + +For running an oemof-solph optimisation model, you need to install a solver. +Following you will find guidelines for the installation process for different operating systems. + +.. _windows_solver_label: +.. _linux_solver_label: + +Installing a solver +------------------- + +There are several solvers that can work with oemof, both open source and commercial. +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. + +Check the solver installation by executing the test_installation example below (see section Installation Test). + +**Linux** + +To install the solvers have a look at the package repository of your Linux distribution or search for precompiled packages. GLPK and CBC ares available at Debian, Feodora, Ubuntu and others. + +**Windows** + + 1. Download `CBC `_ + 2. Download `GLPK (64/32 bit) `_ + 3. Unpack CBC/GLPK to any folder (e.g. C:/Users/Somebody/my_programs) + 4. Add the path of the executable files of both solvers to the PATH variable using `this tutorial `_ + 5. Restart Windows + +Check the solver installation by executing the test_installation example (see the `Installation test` section). + + +**Mac OSX** + +Please follow the installation instructions on the respective homepages for details. + +CBC-solver: https://github.com/coin-or/Cbc + +GLPK-solver: http://arnab-deka.com/posts/2010/02/installing-glpk-on-a-mac/ + +If you install the CBC solver via brew (highly recommended), it should work without additional configuration. + + +**conda** + +Provided you are using a Linux or MacOS, the CBC-solver can also be installed in a `conda` environment. Please note, that it is highly recommended to `use pip after conda `_, so: + +.. code:: console + + (venv) conda install -c conda-forge coincbc + (venv) pip install oemof.solph + + +.. _check_installation_label: + +Installation test +----------------- + +Test the installation and the installed solver by running the installation test +in your virtual environment: + +.. code:: console + + (venv) oemof_installation_test + +If the installation was successful, you will receive something like this: + +.. code:: console + + ********* + Solver installed with oemof: + glpk: working + cplex: not working + cbc: working + gurobi: not working + ********* + oemof.solph successfully installed. + +as an output. + +Contributing +============ + +A warm welcome to all who want to join the developers and contribute to +oemof.solph. + +Information on the details and how to approach us can be found +`in the oemof documentation `_ . + +Citing +====== + +For explicitly citing solph, you might want to refer to +`DOI:10.1016/j.simpa.2020.100028 `_, +which gives an overview over the capabilities of solph. +The core ideas of oemof as a whole are described in +`DOI:10.1016/j.esr.2018.07.001 `_ +(preprint at `arXiv:1808.0807 `_). +To allow citing specific versions, we use the zenodo project to get a DOI for each version. + +Example Applications +==================== + +The combination of specific modules (often including other packages) is called an +application (app). For example, it can depict a concrete energy system model. +You can find a large variety of helpful examples in the documentation. +The examples show the optimisation of different energy systems and are supposed +to help new users to understand the framework's structure. + +You are welcome to contribute your own examples via a `pull request `_ +or by e-mailing us (see `here `_ for contact information). + +License +======= + +Copyright (c) oemof developer group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/conf.py b/docs/conf.py index 630948655..e6b25a295 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,80 +1,80 @@ -# -*- coding: utf-8 -*- - -import os -import sys - -import matplotlib -from sphinx.ext.autodoc import between - -from oemof.solph import __version__ - - -matplotlib.use("agg") -sys.path.append(os.path.join(os.path.dirname(__file__), "..", "examples")) - - -def setup(app): - # Register a sphinx.ext.autodoc.between listener to ignore everything - # between lines that contain the word IGNORE - app.connect("autodoc-process-docstring", between("^SPDX.*$", exclude=True)) - return app - - -extensions = [ - "sphinx.ext.autodoc", - "sphinx.ext.autosummary", - "sphinx.ext.coverage", - "sphinx.ext.doctest", - "sphinx.ext.extlinks", - "sphinx.ext.ifconfig", - "sphinx.ext.napoleon", - "sphinx.ext.todo", - "sphinx.ext.viewcode", - "sphinx_copybutton", - "sphinx_design", -] -source_suffix = ".rst" -master_doc = "index" -project = "oemof.solph" -year = "2014-2023" -author = "oemof-developer-group" -copyright = "{0}, {1}".format(year, author) -version = release = __version__ - -pygments_style = "trac" -templates_path = ["."] -extlinks = { - "issue": ("https://github.com/oemof/oemof-solph/issues/%s", "#%s"), - "pr": ("https://github.com/oemof/oemof-solph/pull/%s", "PR #%s"), -} -# on_rtd is whether we are on readthedocs.org -on_rtd = os.environ.get("READTHEDOCS", None) == "True" - -if not on_rtd: # only set the theme if we're building docs locally - html_theme = "sphinx_rtd_theme" - -html_use_smartypants = True -html_last_updated_fmt = "%b %d, %Y" -html_split_index = False -html_sidebars = { - "**": ["searchbox.html", "globaltoc.html", "sourcelink.html"], -} -html_short_title = "%s-%s" % (project, version) -html_logo = "./_logo/logo_oemof_solph_COMPACT_bg.svg" - -napoleon_use_ivar = True -napoleon_use_rtype = False -napoleon_use_param = False -nitpicky = False - -exclude_patterns = ["_build", "whatsnew/*"] - -linkcheck_ignore = [ - 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/*", - # Due to traffic limitation, the folowwing creates a 403 in CI pipeline: - "https://www.sciencedirect.com/science/article/abs/pii/S036054421500331X", -] +# -*- coding: utf-8 -*- + +import os +import sys + +import matplotlib +from sphinx.ext.autodoc import between + +from oemof.solph import __version__ + + +matplotlib.use("agg") +sys.path.append(os.path.join(os.path.dirname(__file__), "..", "examples")) + + +def setup(app): + # Register a sphinx.ext.autodoc.between listener to ignore everything + # between lines that contain the word IGNORE + app.connect("autodoc-process-docstring", between("^SPDX.*$", exclude=True)) + return app + + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.coverage", + "sphinx.ext.doctest", + "sphinx.ext.extlinks", + "sphinx.ext.ifconfig", + "sphinx.ext.napoleon", + "sphinx.ext.todo", + "sphinx.ext.viewcode", + "sphinx_copybutton", + "sphinx_design", +] +source_suffix = ".rst" +master_doc = "index" +project = "oemof.solph" +year = "2014-2023" +author = "oemof-developer-group" +copyright = "{0}, {1}".format(year, author) +version = release = __version__ + +pygments_style = "trac" +templates_path = ["."] +extlinks = { + "issue": ("https://github.com/oemof/oemof-solph/issues/%s", "#%s"), + "pr": ("https://github.com/oemof/oemof-solph/pull/%s", "PR #%s"), +} +# on_rtd is whether we are on readthedocs.org +on_rtd = os.environ.get("READTHEDOCS", None) == "True" + +if not on_rtd: # only set the theme if we're building docs locally + html_theme = "sphinx_rtd_theme" + +html_use_smartypants = True +html_last_updated_fmt = "%b %d, %Y" +html_split_index = False +html_sidebars = { + "**": ["searchbox.html", "globaltoc.html", "sourcelink.html"], +} +html_short_title = "%s-%s" % (project, version) +html_logo = "./_logo/logo_oemof_solph_COMPACT_bg.svg" + +napoleon_use_ivar = True +napoleon_use_rtype = False +napoleon_use_param = False +nitpicky = False + +exclude_patterns = ["_build", "whatsnew/*"] + +linkcheck_ignore = [ + 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/*", + # 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 ff174706c..e85658bcc 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -1,1707 +1,1707 @@ -.. _oemof_solph_label: - -.. _using_oemof_label: - -~~~~~~~~~~~~ -User's guide -~~~~~~~~~~~~ - -Solph is an oemof-package, designed to create and solve linear or mixed-integer linear optimization problems. The package is based on pyomo. To create an energy system model generic and specific components are available. To get started with solph, checkout the examples in the :ref:`examples_label` section. - -This User's guide provides a user-friendly introduction into oemof-solph, -which includes small examples and nice illustrations. -However, the functionalities of oemof-solph go beyond the content of this User's guide section. -So, if you want to know all details of a certain component or a function, -please go the :ref:`api_reference_label`. There, you will find -a detailed and complete description of all oemof-solph modules. - -.. contents:: - :depth: 2 - :local: - :backlinks: top - - -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). -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. - -Once the examples work you are close to your first energy model. - - -Handling of Warnings -^^^^^^^^^^^^^^^^^^^^ - -The solph library is designed to be as generic as possible to make it possible -to use it in different use cases. This concept makes it difficult to raise -Errors or Warnings because sometimes untypical combinations of parameters are -allowed even though they might be wrong in over 99% of the use cases. - -Therefore, a SuspiciousUsageWarning was introduced. This warning will warn you -if you do something untypical. If you are sure that you know what you are doing -you can switch the warning off. - -See `the debugging module of oemof-tools `_ for more -information. - - -Set up an energy system -^^^^^^^^^^^^^^^^^^^^^^^ - -In most cases an EnergySystem object is defined when we start to build up an energy system model. The EnergySystem object will be the main container for the model's elements. - -The model time is defined by the number of intervals and the length of intervals. The length of each interval does not have to be the same. This can be defined in two ways: - -1. Define the length of each interval in an array/Series where the number of the elements is the number of intervals. -2. Define a `pandas.DatetimeIndex` with all time steps that encloses an interval. Be aware that you have to define n+1 time points to get n intervals. For non-leap year with hourly values that means 8761 time points to get 8760 interval e.g. 2018-01-01 00:00 to 2019-01-01 00:00. - -The index will also be used for the results. For a numeric index the resulting time series will indexed with a numeric index starting with 0. - -One can use the function -:py:func:`create_time_index` to create an equidistant datetime index. By default the function creates an hourly index for one year, so online the year has to be passed to the function. But it is also possible to change the length of the interval to quarter hours etc. The default number of intervals is the number needed to cover the given year but the value can be overwritten by the user. - -It is also possible to define the datetime index using pandas. See `pandas date_range guide `_ for more information. - -Both code blocks will create an hourly datetime index for 2011: - -.. code-block:: python - - from oemof.solph import create_time_index - my_index = create_time_index(2011) - -.. code-block:: python - - import pandas as pd - my_index = pd.date_range('1/1/2011', periods=8761, freq='h') - -This index can be used to define the EnergySystem: - -.. code-block:: python - - import oemof.solph as solph - my_energysystem = solph.EnergySystem(timeindex=my_index) - -Now you can start to add the components of the network. - - -Add components to the energy system -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -After defining an instance of the EnergySystem class, in the following you have to add all nodes you define to your EnergySystem. - -Basically, there are two types of *nodes* - *components* and *buses*. Every Component has to be connected with one or more *buses*. The connection between a *component* and a *bus* is the *flow*. - -All solph *components* can be used to set up an energy system model but you should read the documentation of each *component* to learn about usage and restrictions. For example it is not possible to combine every *component* with every *flow*. Furthermore, you can add your own *components* in your application (see below) but we would be pleased to integrate them into solph if they are of general interest (see :ref:`feature_requests_and_feedback`). - -An example of a simple energy system shows the usage of the nodes for -real world representations: - -.. image:: _files/oemof_solph_example.svg - :scale: 70 % - :alt: alternate text - :align: center - -The figure shows a simple energy system using the four basic network classes and the Bus class. -If you remove the transmission line (transport 1 and transport 2) you get two systems but they are still one energy system in terms of solph and will be optimised at once. - -There are different ways to add components to an *energy system*. The following line adds a *bus* object to the *energy system* defined above. - -.. code-block:: python - - my_energysystem.add(solph.buses.Bus()) - -It is also possible to assign the bus to a variable and add it afterwards. In that case it is easy to add as many objects as you like. - -.. code-block:: python - - my_bus1 = solph.buses.Bus() - my_bus2 = solph.buses.Bus() - my_energysystem.add(my_bus1, my_bus2) - -Therefore it is also possible to add lists or dictionaries with components but you have to dissolve them. - -.. code-block:: python - - # add a list - my_energysystem.add(*my_list) - - # add a dictionary - my_energysystem.add(*my_dictionary.values()) - - -Bus -+++ - -All flows into and out of a *bus* are balanced (by default). Therefore an instance of the Bus class represents a grid or network without losses. To define an instance of a Bus only a unique label is necessary. If you do not set a label a random label is used but this makes it difficult to get the results later on. - -To make it easier to connect the bus to a component you can optionally assign a variable for later use. - -.. code-block:: python - - solph.buses.Bus(label='natural_gas') - electricity_bus = solph.buses.Bus(label='electricity') - -.. note:: See the :py:class:`~oemof.solph.buses._bus.Bus` class for all parameters and the mathematical background. - - -Flow -++++ - -The flow class has to be used to connect nodes and buses. An instance of the Flow class is normally used in combination with the definition of a component. -A Flow can be limited by upper and lower bounds (constant or time-dependent) or by summarised limits. -For all parameters see the API documentation of the :py:class:`~oemof.solph.flows._flow.Flow` class or the examples of the nodes below. A basic flow can be defined without any parameter. - -.. code-block:: python - - solph.flows.Flow() - -oemof.solph has different types of *flows* but you should be aware that you cannot connect every *flow* type with every *component*. - -.. note:: See the :py:class:`~oemof.solph.flows._flow.Flow` class for all parameters and the mathematical background. - -Components -++++++++++ - -Components are divided in two categories. Well-tested components (solph.components) and experimental components (solph.components.experimental). The experimental section was created to lower the entry barrier for new components. Be aware that these components might not be properly documented or even sometimes do not even work as intended. Let us know if you have successfully used and tested these components. This is the first step to move them to the regular components section. - -See :ref:`oemof_solph_components_label` for a list of all components. - - -.. _oemof_solph_optimise_es_label: - -Optimise your energy system -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The typical optimisation of an energy system in solph is the dispatch optimisation, which means that the use of the sources is optimised to satisfy the demand at least costs. -Therefore, variable cost can be defined for all components. The cost for gas should be defined in the gas source while the variable costs of the gas power plant are caused by operating material. -The actual fuel cost in turn is calculated in the framework itself considering the efficiency of the power plant. -You can deviate from this scheme but you should keep it consistent to make it understandable for others. - -Costs do not have to be monetary costs but could be emissions or other variable units. - -Furthermore, it is possible to optimise the capacity of different components using the investment mode (see :ref:`investment_mode_label`). - -Since v0.5.1, there also is the possibility to have multi-period (i.e. dynamic) investments over longer-time horizon which is in experimental state (see :ref:`multi_period_mode_label`). - -.. code-block:: python - - # set up a simple least cost optimisation - om = solph.Model(my_energysystem) - - # solve the energy model using the CBC solver - om.solve(solver='cbc', solve_kwargs={'tee': True}) - -If you want to analyse the lp-file to see all equations and bounds you can write the file to you disc. In that case you should reduce the timesteps to 3. This will increase the readability of the file. - -.. code-block:: python - - # set up a simple least cost optimisation - om = solph.Model(my_energysystem) - - # write the lp file for debugging or other reasons - om.write('path/my_model.lp', io_options={'symbolic_solver_labels': True}) - -Analysing your results -^^^^^^^^^^^^^^^^^^^^^^ - -If you want to analyse your results, you should first dump your EnergySystem instance to permanently store results. Otherwise you would have to run the simulation again. - -.. code-block:: python - - my_energysystem.results = processing.results(om) - my_energysystem.dump('my_path', 'my_dump.oemof') - -If you need the meta results of the solver you can do the following: - -.. code-block:: python - - my_energysystem.results['main'] = processing.results(om) - my_energysystem.results['meta'] = processing.meta_results(om) - my_energysystem.dump('my_path', 'my_dump.oemof') - -To restore the dump you can simply create an EnergySystem instance and restore your dump into it. - -.. code-block:: python - - import oemof.solph as solph - my_energysystem = solph.EnergySystem() - my_energysystem.restore('my_path', 'my_dump.oemof') - results = my_energysystem.results - - # If you use meta results do the following instead of the previous line. - results = my_energysystem.results['main'] - meta = my_energysystem.results['meta'] - - -If you call dump/restore without any parameters, the dump will be stored as *'es_dump.oemof'* into the *'.oemof/dumps/'* folder created in your HOME directory. - -See :ref:`oemof_outputlib_label` to learn how to process, plot and analyse the results. - - -.. _oemof_solph_components_label: - -Solph components ----------------- - - * :ref:`oemof_solph_components_sink_label` - * :ref:`oemof_solph_components_source_label` - * :ref:`oemof_solph_components_converter_label` - * :ref:`oemof_solph_components_extraction_turbine_chp_label` - * :ref:`oemof_solph_components_generic_caes_label` - * :ref:`oemof_solph_components_generic_chp_label` - * :ref:`oemof_solph_components_generic_storage_label` - * :ref:`oemof_solph_custom_electrical_line_label` - * :ref:`oemof_solph_custom_link_label` - * :ref:`oemof_solph_custom_sinkdsm_label` - - -.. _oemof_solph_components_sink_label: - -Sink (basic) -^^^^^^^^^^^^ - -A sink is normally used to define the demand within an energy model but it can also be used to detect excesses. - -The example shows the electricity demand of the electricity_bus defined above. -The *'my_demand_series'* should be sequence of normalised valueswhile the *'nominal_value'* is the maximum demand the normalised sequence is multiplied with. -Giving *'my_demand_series'* as parameter *'fix'* means that the demand cannot be changed by the solver. - -.. code-block:: python - - solph.components.Sink(label='electricity_demand', inputs={electricity_bus: solph.flows.Flow( - fix=my_demand_series, nominal_value=nominal_demand)}) - -In contrast to the demand sink the excess sink has normally less restrictions but is open to take the whole excess. - -.. code-block:: python - - solph.components.Sink(label='electricity_excess', inputs={electricity_bus: solph.flows.Flow()}) - -.. note:: The Sink class is only a plug and provides no additional constraints or variables. - - -.. _oemof_solph_components_source_label: - -Source (basic) -^^^^^^^^^^^^^^ - -A source can represent a pv-system, a wind power plant, an import of natural gas or a slack variable to avoid creating an in-feasible model. - -While a wind power plant will have as feed-in depending on the weather conditions the natural_gas import might be restricted by maximum value (*nominal_value*) and an annual limit (*full_load_time_max*). -As we do have to pay for imported gas we should set variable costs. -Comparable to the demand series an *fix* is used to define a fixed the normalised output of a wind power plant. -Alternatively, you might use *max* to allow for easy curtailment. -The *nominal_value* sets the installed capacity. - -.. code-block:: python - - solph.components.Source( - label='import_natural_gas', - outputs={my_energysystem.groups['natural_gas']: solph.flows.Flow( - nominal_value=1000, full_load_time_max=1000000, variable_costs=50)}) - - solph.components.Source(label='wind', outputs={electricity_bus: solph.flows.Flow( - fix=wind_power_feedin_series, nominal_value=1000000)}) - -.. note:: The Source class is only a plug and provides no additional constraints or variables. - -.. _oemof_solph_components_converter_label: - -Converter (basic) -^^^^^^^^^^^^^^^^^ - -An instance of the Converter class can represent a node with multiple input and output flows such as a power plant, a transport line or any kind of a transforming process as electrolysis, a cooling device or a heat pump. -The efficiency has to be constant within one time step to get a linear transformation. -You can define a different efficiency for every time step (e.g. the thermal powerplant efficiency according to the ambient temperature) but this series has to be predefined and cannot be changed within the optimisation. - -A condensing power plant can be defined by a converter with one input (fuel) and one output (electricity). - -.. code-block:: python - - b_gas = solph.buses.Bus(label='natural_gas') - b_el = solph.buses.Bus(label='electricity') - - solph.components.Converter( - label="pp_gas", - inputs={bgas: solph.flows.Flow()}, - outputs={b_el: solph.flows.Flow(nominal_value=10e10)}, - conversion_factors={electricity_bus: 0.58}) - -A CHP power plant would be defined in the same manner but with two outputs: - -.. code-block:: python - - b_gas = solph.buses.Bus(label='natural_gas') - b_el = solph.buses.Bus(label='electricity') - b_th = solph.buses.Bus(label='heat') - - solph.components.Converter( - label='pp_chp', - inputs={b_gas: Flow()}, - outputs={b_el: Flow(nominal_value=30), - b_th: Flow(nominal_value=40)}, - conversion_factors={b_el: 0.3, b_th: 0.4}) - -A CHP power plant with 70% coal and 30% natural gas can be defined with two inputs and two outputs: - -.. code-block:: python - - b_gas = solph.buses.Bus(label='natural_gas') - b_coal = solph.buses.Bus(label='hard_coal') - b_el = solph.buses.Bus(label='electricity') - b_th = solph.buses.Bus(label='heat') - - solph.components.Converter( - label='pp_chp', - inputs={b_gas: Flow(), b_coal: Flow()}, - outputs={b_el: Flow(nominal_value=30), - b_th: Flow(nominal_value=40)}, - conversion_factors={b_el: 0.3, b_th: 0.4, - b_coal: 0.7, b_gas: 0.3}) - -A heat pump would be defined in the same manner. New buses are defined to make the code cleaner: - -.. code-block:: python - - b_el = solph.buses.Bus(label='electricity') - b_th_low = solph.buses.Bus(label='low_temp_heat') - b_th_high = solph.buses.Bus(label='high_temp_heat') - - # The cop (coefficient of performance) of the heat pump can be defined as - # a scalar or a sequence. - cop = 3 - - solph.components.Converter( - label='heat_pump', - inputs={b_el: Flow(), b_th_low: Flow()}, - outputs={b_th_high: Flow()}, - conversion_factors={b_el: 1/cop, - b_th_low: (cop-1)/cop}) - -If the low-temperature reservoir is nearly infinite (ambient air heat pump) the -low temperature bus is not needed and, therefore, a Converter with one input -is sufficient. - -.. note:: See the :py:class:`~oemof.solph.components.converter.Converter` class for all parameters and the mathematical background. - -.. _oemof_solph_components_extraction_turbine_chp_label: - -ExtractionTurbineCHP (component) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The :py:class:`~oemof.solph.components._extraction_turbine_chp.ExtractionTurbineCHP` -inherits from the :ref:`oemof_solph_components_converter_label` class. Like the name indicates, -the application example for the component is a flexible combined heat and power -(chp) plant. Of course, an instance of this class can represent also another -component with one input and two output flows and a flexible ratio between -these flows, with the following constraints: - -.. include:: ../src/oemof/solph/components/_extraction_turbine_chp.py - :start-after: _ETCHP-equations: - :end-before: """ - -These constraints are applied in addition to those of a standard -:class:`~oemof.solph.components.Converter`. The constraints limit the range of -the possible operation points, like the following picture shows. For a certain -flow of fuel, there is a line of operation points, whose slope is defined by -the power loss factor :math:`\beta` (in some contexts also referred to as -:math:`C_v`). The second constraint limits the decrease of electrical power and -incorporates the backpressure coefficient :math:`C_b`. - -.. image:: _files/ExtractionTurbine_range_of_operation.svg - :width: 70 % - :alt: variable_chp_plot.svg - :align: center - -For now, :py:class:`~oemof.solph.components._extraction_turbine_chp.ExtractionTurbineCHP` instances must -have one input and two output flows. The class allows the definition -of a different efficiency for every time step that can be passed as a series -of parameters that are fixed before the optimisation. In contrast to the -:py:class:`~oemof.solph.components.Converter`, a main flow and a tapped flow is -defined. For the main flow you can define a separate conversion factor that -applies when the second flow is zero (*`conversion_factor_full_condensation`*). - -.. code-block:: python - - solph.components._extractionTurbineCHP( - label='variable_chp_gas', - inputs={b_gas: solph.flows.Flow(nominal_value=10e10)}, - outputs={b_el: solph.flows.Flow(), b_th: solph.flows.Flow()}, - conversion_factors={b_el: 0.3, b_th: 0.5}, - conversion_factor_full_condensation={b_el: 0.5}) - -The key of the parameter *'conversion_factor_full_condensation'* defines which -of the two flows is the main flow. In the example above, the flow to the Bus -*'b_el'* is the main flow and the flow to the Bus *'b_th'* is the tapped flow. -The following plot shows how the variable chp (right) schedules it's electrical -and thermal power production in contrast to a fixed chp (left). The plot is the -output of an example in the `example directory -`_. - -.. image:: _files/variable_chp_plot.svg - :scale: 10 % - :alt: variable_chp_plot.svg - :align: center - -.. note:: See the :py:class:`~oemof.solph.components._extraction_turbine_chp.ExtractionTurbineCHP` class for all parameters and the mathematical background. - - -.. _oemof_solph_components_generic_chp_label: - -GenericCHP (component) -^^^^^^^^^^^^^^^^^^^^^^ - -With the GenericCHP class it is possible to model different types of CHP plants (combined cycle extraction turbines, -back pressure turbines and motoric CHP), which use different ranges of operation, as shown in the figure below. - -.. image:: _files/GenericCHP.svg - :scale: 70 % - :alt: scheme of GenericCHP operation range - :align: center - -Combined cycle extraction turbines: The minimal and maximal electric power without district heating -(red dots in the figure) define maximum load and minimum load of the plant. Beta defines electrical power loss through -heat extraction. The minimal thermal condenser load to cooling water and the share of flue gas losses -at maximal heat extraction determine the right boundary of the operation range. - -.. code-block:: python - - solph.components.GenericCHP( - label='combined_cycle_extraction_turbine', - fuel_input={bgas: solph.flows.Flow( - H_L_FG_share_max=[0.19 for p in range(0, periods)])}, - electrical_output={bel: solph.flows.Flow( - P_max_woDH=[200 for p in range(0, periods)], - P_min_woDH=[80 for p in range(0, periods)], - Eta_el_max_woDH=[0.53 for p in range(0, periods)], - Eta_el_min_woDH=[0.43 for p in range(0, periods)])}, - heat_output={bth: solph.flows.Flow( - Q_CW_min=[30 for p in range(0, periods)])}, - Beta=[0.19 for p in range(0, periods)], - back_pressure=False) - -For modeling a back pressure CHP, the attribute `back_pressure` has to be set to True. -The ratio of power and heat production in a back pressure plant is fixed, therefore the operation range -is just a line (see figure). Again, the `P_min_woDH` and `P_max_woDH`, the efficiencies at these points and the share of flue -gas losses at maximal heat extraction have to be specified. In this case “without district heating” is not to be taken -literally since an operation without heat production is not possible. It is advised to set `Beta` to zero, so the minimal and -maximal electric power without district heating are the same as in the operation point (see figure). The minimal -thermal condenser load to cooling water has to be zero, because there is no condenser besides the district heating unit. - - -.. code-block:: python - - solph.components.GenericCHP( - label='back_pressure_turbine', - fuel_input={bgas: solph.flows.Flow( - H_L_FG_share_max=[0.19 for p in range(0, periods)])}, - electrical_output={bel: solph.flows.Flow( - P_max_woDH=[200 for p in range(0, periods)], - P_min_woDH=[80 for p in range(0, periods)], - Eta_el_max_woDH=[0.53 for p in range(0, periods)], - Eta_el_min_woDH=[0.43 for p in range(0, periods)])}, - heat_output={bth: solph.flows.Flow( - Q_CW_min=[0 for p in range(0, periods)])}, - Beta=[0 for p in range(0, periods)], - back_pressure=True) - -A motoric chp has no condenser, so `Q_CW_min` is zero. Electrical power does not depend on the amount of heat used -so `Beta` is zero. The minimal and maximal electric power (without district heating) and the efficiencies at these -points are needed, whereas the use of electrical power without using thermal energy is not possible. -With `Beta=0` there is no difference between these points and the electrical output in the operation range. -As a consequence of the functionality of a motoric CHP, share of flue gas losses at maximal heat extraction but also -at minimal heat extraction have to be specified. - - -.. code-block:: python - - solph.components.GenericCHP( - label='motoric_chp', - fuel_input={bgas: solph.flows.Flow( - H_L_FG_share_max=[0.18 for p in range(0, periods)], - H_L_FG_share_min=[0.41 for p in range(0, periods)])}, - electrical_output={bel: solph.flows.Flow( - P_max_woDH=[200 for p in range(0, periods)], - P_min_woDH=[100 for p in range(0, periods)], - Eta_el_max_woDH=[0.44 for p in range(0, periods)], - Eta_el_min_woDH=[0.40 for p in range(0, periods)])}, - heat_output={bth: solph.flows.Flow( - Q_CW_min=[0 for p in range(0, periods)])}, - Beta=[0 for p in range(0, periods)], - back_pressure=False) - -Modeling different types of plants means telling the component to use different constraints. Constraint 1 to 9 -are active in all three cases. Constraint 10 depends on the attribute back_pressure. If true, the constraint is -an equality, if not it is a less or equal. Constraint 11 is only needed for modeling motoric CHP which is done by -setting the attribute `H_L_FG_share_min`. - -.. include:: ../src/oemof/solph/components/_generic_chp.py - :start-after: _GenericCHP-equations1-10: - :end-before: **For the attribute** - -If :math:`\dot{H}_{L,FG,min}` is given, e.g. for a motoric CHP: - -.. include:: ../src/oemof/solph/components/_generic_chp.py - :start-after: _GenericCHP-equations11: - :end-before: """ - -.. note:: See the :py:class:`~oemof.solph.components._generic_chp.GenericCHP` class for all parameters and the mathematical background. - - -.. _oemof_solph_components_generic_storage_label: - -GenericStorage (component) -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -A component to model a storage with its basic characteristics. The -GenericStorage is designed for one input and one output. -The ``nominal_storage_capacity`` of the storage signifies the storage capacity. You can either set it to the net capacity or to the gross capacity and limit it using the min/max attribute. -To limit the input and output flows, you can define the ``nominal_value`` in the Flow objects. -Furthermore, an efficiency for loading, unloading and a loss rate can be defined. - -.. code-block:: python - - solph.components.GenericStorage( - label='storage', - inputs={b_el: solph.flows.Flow(nominal_value=9, variable_costs=10)}, - outputs={b_el: solph.flows.Flow(nominal_value=25, variable_costs=10)}, - loss_rate=0.001, nominal_storage_capacity=50, - inflow_conversion_factor=0.98, outflow_conversion_factor=0.8) - -For initialising the state of charge before the first time step (time step zero) the parameter ``initial_storage_level`` (default value: ``None``) can be set by a numeric value as fraction of the storage capacity. -Additionally the parameter ``balanced`` (default value: ``True``) sets the relation of the state of charge of time step zero and the last time step. -If ``balanced=True``, the state of charge in the last time step is equal to initial value in time step zero. -Use ``balanced=False`` with caution as energy might be added to or taken from the energy system due to different states of charge in time step zero and the last time step. -Generally, with these two parameters four configurations are possible, which might result in different solutions of the same optimization model: - - * ``initial_storage_level=None``, ``balanced=True`` (default setting): The state of charge in time step zero is a result of the optimization. The state of charge of the last time step is equal to time step zero. Thus, the storage is not violating the energy conservation by adding or taking energy from the system due to different states of charge at the beginning and at the end of the optimization period. - * ``initial_storage_level=0.5``, ``balanced=True``: The state of charge in time step zero is fixed to 0.5 (50 % charged). The state of charge in the last time step is also constrained by 0.5 due to the coupling parameter ``balanced`` set to ``True``. - * ``initial_storage_level=None``, ``balanced=False``: Both, the state of charge in time step zero and the last time step are a result of the optimization and not coupled. - * ``initial_storage_level=0.5``, ``balanced=False``: The state of charge in time step zero is constrained by a given value. The state of charge of the last time step is a result of the optimization. - -The following code block shows an example of the storage parametrization for the second configuration: - -.. code-block:: python - - solph.components.GenericStorage( - label='storage', - inputs={b_el: solph.flows.Flow(nominal_value=9, variable_costs=10)}, - outputs={b_el: solph.flows.Flow(nominal_value=25, variable_costs=10)}, - loss_rate=0.001, nominal_storage_capacity=50, - initial_storage_level=0.5, balanced=True, - inflow_conversion_factor=0.98, outflow_conversion_factor=0.8) - -If you want to view the temporal course of the state of charge of your storage -after the optimisation, you need to check the ``storage_content`` in the results: - -.. code-block:: python - - from oemof.solph import processing, views - results = processing.results(om) - column_name = (('your_storage_label', 'None'), 'storage_content') - SC = views.node(results, 'your_storage_label')['sequences'][column_name] - -The ``storage_content`` is the absolute value of the current stored energy. -By calling: - -.. code-block:: python - - views.node(results, 'your_storage_label')['scalars'] - -you get the results of the scalar values of your storage, e.g. the initial -storage content before time step zero (``init_content``). - -For more information see the definition of the :py:class:`~oemof.solph.components._generic_storage.GenericStorage` class or check the :ref:`examples_label`. - - -Using an investment object with the GenericStorage component -+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -Based on the `GenericStorage` object the `GenericInvestmentStorageBlock` adds two main investment possibilities. - - * Invest into the flow parameters e.g. a turbine or a pump - * Invest into capacity of the storage e.g. a basin or a battery cell - -Investment in this context refers to the value of the variable for the 'nominal_value' (installed capacity) in the investment mode. - -As an addition to other flow-investments, the storage class implements the possibility to couple or decouple the flows -with the capacity of the storage. -Three parameters are responsible for connecting the flows and the capacity of the storage: - - * ``invest_relation_input_capacity`` fixes the input flow investment to the capacity investment. A ratio of 1 means that the storage can be filled within one time-period. - * ``invest_relation_output_capacity`` fixes the output flow investment to the capacity investment. A ratio of 1 means that the storage can be emptied within one period. - * ``invest_relation_input_output`` fixes the input flow investment to the output flow investment. For values <1, the input will be smaller and for values >1 the input flow will be larger. - -You should not set all 3 parameters at the same time, since it will lead to overdetermination. - -The following example pictures a Pumped Hydroelectric Energy Storage (PHES). Both flows and the storage itself (representing: pump, turbine, basin) are free in their investment. You can set the parameters to `None` or delete them as `None` is the default value. - -.. code-block:: python - - solph.components.GenericStorage( - label='PHES', - inputs={b_el: solph.flows.Flow(nominal_value=solph.Investment(ep_costs=500))}, - outputs={b_el: solph.flows.Flow(nominal_value=solph.Investment(ep_costs=500)}, - loss_rate=0.001, - inflow_conversion_factor=0.98, outflow_conversion_factor=0.8), - investment = solph.Investment(ep_costs=40)) - -The following example describes a battery with flows coupled to the capacity of the storage. - -.. code-block:: python - - solph.components.GenericStorage( - label='battery', - inputs={b_el: solph.flows.Flow()}, - outputs={b_el: solph.flows.Flow()}, - loss_rate=0.001, - inflow_conversion_factor=0.98, - outflow_conversion_factor=0.8, - invest_relation_input_capacity = 1/6, - invest_relation_output_capacity = 1/6, - investment = solph.Investment(ep_costs=400)) - - -.. note:: See the :py:class:`~oemof.solph.components._generic_storage.GenericStorage` class for all parameters and the mathematical background. - - -.. _oemof_solph_custom_link_label: - -Link -^^^^ - -The `Link` allows to model connections between two busses, e.g. modeling the transshipment of electric energy between two regions. - -.. note:: See the :py:class:`~oemof.solph.components.experimental._link.Link` class for all parameters and the mathematical background. - - - -OffsetConverter (component) -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The `OffsetConverter` object makes it possible to create a Converter with efficiencies depending on the part load condition. -For this it is necessary to define one flow as a nonconvex flow and to set a minimum load. -The following example illustrates how to define an OffsetConverter for given -information for an output, i.e. a combined heat and power plant. The plant -generates up to 100 kW electric energy at an efficiency of 40 %. In minimal -load the electric efficiency is at 30 %, and the minimum possible load is 50 % -of the nominal load. At the same time, heat is produced with a constant -efficiency. By using the `OffsetConverter` a linear relation of in- and output -power with a power dependent efficiency is generated. - -.. code-block:: python - - >>> from oemof import solph - - >>> eta_el_min = 0.3 # electrical efficiency at minimal operation point - >>> eta_el_max = 0.4 # electrical efficiency at nominal operation point - >>> eta_th_min = 0.5 # thermal efficiency at minimal operation point - >>> eta_th_max = 0.5 # thermal efficiency at nominal operation point - >>> P_out_min = 20 # absolute minimal output power - >>> P_out_max = 100 # absolute nominal output power - -As reference for our system we use the input and will mark that flow as -nonconvex respectively. The efficiencies for electricity and heat output have -therefore to be defined with respect to the input flow. The same is true for -the minimal and maximal load. Therefore, we first calculate the minimum and -maximum input of fuel and then derive the slope and offset for both outputs. - -.. code-block:: python - - >>> P_in_max = P_out_max / eta_el_max - >>> P_in_min = P_out_min / eta_el_min - >>> P_in_max - 250.0 - >>> round(P_in_min, 2) - 66.67 - -With that information, we can derive the normed minimal and maximal load of the -nonconvex flow, and calculate the slope and the offset for both outputs. Note, -that the offset for the heat output is 0, because the thermal heat output -efficiency is constant. - -.. code-block:: python - - >>> l_max = 1 - >>> l_min = P_in_min / P_in_max - >>> slope_el, offset_el = solph.components.slope_offset_from_nonconvex_input( - ... l_max, l_min, eta_el_max, eta_el_min - ... ) - >>> slope_th, offset_th = solph.components.slope_offset_from_nonconvex_input( - ... l_max, l_min, eta_th_max, eta_th_min - ... ) - >>> round(slope_el, 3) - 0.436 - >>> round(offset_el, 3) - -0.036 - >>> round(slope_th, 3) - 0.5 - >>> round(offset_th, 3) - 0.0 - -Then we can create our component with the buses attached to it. - -.. code-block:: python - - >>> bfuel = solph.Bus("fuel") - >>> bel = solph.Bus("electricity") - >>> bth = solph.Bus("heat") - - # define OffsetConverter - >>> diesel_genset = solph.components.OffsetConverter( - ... label='boiler', - ... inputs={ - ... bfuel: solph.flows.Flow( - ... nominal_value=P_out_max, - ... max=l_max, - ... min=l_min, - ... nonconvex=solph.NonConvex() - ... ) - ... }, - ... outputs={ - ... bel: solph.flows.Flow(), - ... bth: solph.flows.Flow(), - ... }, - ... conversion_factors={bel: slope_el, bth: slope_th}, - ... normed_offsets={bel: offset_el, bth: offset_th}, - ... ) - -.. note:: - - One of the inputs and outputs has to be a `NonConvex` flow and this flow - will serve as the reference for the `conversion_factors` and the - `normed_offsets`. The `NonConvex` flow also holds - - - the `nominal_value` (or `Investment` in case of investment optimization), - - the `min` and - - the `max` attributes. - - The `conversion_factors` and `normed_offsets` are specified similar to the - `Converter` API with dictionaries referencing the respective input and - output buses. Note, that you cannot have the `conversion_factors` or - `normed_offsets` point to the `NonConvex` flow. - -The following figures show the power at the electrical and the thermal output -and the resepctive ratios to the nonconvex flow (normalized). The efficiency -becomes non-linear. - -.. image:: _files/OffsetConverter_relations_1.svg - :width: 70 % - :alt: OffsetConverter_relations_1.svg - :align: center - - -.. image:: _files/OffsetConverter_relations_2.svg - :width: 70 % - :alt: OffsetConverter_relations_2.svg - :align: center - -.. math:: - - \eta = P(t) / P_\text{ref}(t) - -It also becomes clear, why the component has been named `OffsetConverter`. The -linear equation of inflow to electrical outflow does not hit the origin, but is -offset. By multiplying the offset :math:`y_\text{0,normed}` with the binary -status variable of the `NonConvex` flow, the origin (0, 0) becomes part of the -solution space and the boiler is allowed to switch off. - -.. include:: ../src/oemof/solph/components/_offset_converter.py - :start-after: _OffsetConverter-equations: - :end-before: """ - -The parameters :math:`y_\text{0,normed}` and :math:`m` can be given by scalars or by series in order to define a different efficiency equation for every timestep. -It is also possible to define multiple outputs. - -.. note:: See the :py:class:`~oemof.solph.components._offset_converter.OffsetConverter` class for all parameters and the mathematical background. - - -.. _oemof_solph_custom_electrical_line_label: - -ElectricalLine (experimental) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Electrical line. - -.. note:: See the :py:class:`~oemof.solph.flows.experimental._electrical_line.ElectricalLine` class for all parameters and the mathematical background. - - -.. _oemof_solph_components_generic_caes_label: - -GenericCAES (experimental) -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Compressed Air Energy Storage (CAES). -The following constraints describe the CAES: - -.. include:: ../src/oemof/solph/components/experimental/_generic_caes.py - :start-after: _GenericCAES-equations: - :end-before: """ - -.. note:: See the :py:class:`~oemof.solph.components.experimental._generic_caes.GenericCAES` class for all parameters and the mathematical background. - - -.. _oemof_solph_custom_sinkdsm_label: - -SinkDSM (experimental) -^^^^^^^^^^^^^^^^^^^^^^ - -:class:`~oemof.solph.custom.sink_dsm.SinkDSM` can used to represent flexibility in a demand time series. -It can represent both, load shifting or load shedding. -For load shifting, elasticity of the demand is described by upper (`~oemof.solph.custom.sink_dsm.SinkDSM.capacity_up`) and lower (`~oemof.solph.custom.SinkDSM.capacity_down`) bounds where within the demand is allowed to vary. -Upwards shifted demand is then balanced with downwards shifted demand. -For load shedding, shedding capability is described by `~oemof.solph.custom.SinkDSM.capacity_down`. -It both, load shifting and load shedding are allowed, `~oemof.solph.custom.SinkDSM.capacity_down` limits the sum of both downshift categories. - -:class:`~oemof.solph.custom.sink_dsm.SinkDSM` provides three approaches how the Demand-Side Management (DSM) flexibility is represented in constraints -It can be used for both, dispatch and investments modeling. - -* "DLR": Implementation of the DSM modeling approach from by Gils (2015): `Balancing of Intermittent Renewable Power Generation by Demand Response and Thermal Energy Storage, Stuttgart, `_, - Details: :class:`~oemof.solph.custom.sink_dsm.SinkDSMDLRBlock` and :class:`~oemof.solph.custom.sink_dsm.SinkDSMDLRInvestmentBlock` -* "DIW": Implementation of the DSM modeling approach by Zerrahn & Schill (2015): `On the representation of demand-side management in power system models `_, - in: Energy (84), pp. 840-845, 10.1016/j.energy.2015.03.037. Details: :class:`~oemof.solph.custom.sink_dsm.SinkDSMDIWBlock` and :class:`~oemof.solph.custom.sink_dsm.SinkDSMDIWInvestmentBlock` -* "oemof": Is a fairly simple approach. Within a defined windows of time steps, demand can be shifted within the defined bounds of elasticity. - The window sequentially moves forwards. Details: :class:`~oemof.solph.custom.sink_dsm.SinkDSMOemofBlock` and :class:`~oemof.solph.custom.sink_dsm.SinkDSMOemofInvestmentBlock` - -Cost can be associated to either demand up shifts or demand down shifts or both. - -This small example of PV, grid and SinkDSM shows how to use the component - -.. code-block:: python - - # Create some data - pv_day = [(-(1 / 6 * x ** 2) + 6) / 6 for x in range(-6, 7)] - pv_ts = [0] * 6 + pv_day + [0] * 6 - data_dict = {"demand_el": [3] * len(pv_ts), - "pv": pv_ts, - "Cap_up": [0.5] * len(pv_ts), - "Cap_do": [0.5] * len(pv_ts)} - data = pd.DataFrame.from_dict(data_dict) - - # Do timestamp stuff - datetimeindex = pd.date_range(start='1/1/2013', periods=len(data.index), freq='h') - data['timestamp'] = datetimeindex - data.set_index('timestamp', inplace=True) - - # Create Energy System - es = solph.EnergySystem(timeindex=datetimeindex) - - # Create bus representing electricity grid - b_elec = solph.buses.Bus(label='Electricity bus') - es.add(b_elec) - - # Create a back supply - grid = solph.components.Source(label='Grid', - outputs={ - b_elec: solph.flows.Flow( - nominal_value=10000, - variable_costs=50)} - ) - es.add(grid) - - # PV supply from time series - s_wind = solph.components.Source(label='wind', - outputs={ - b_elec: solph.flows.Flow( - fix=data['pv'], - nominal_value=3.5)} - ) - es.add(s_wind) - - # Create DSM Sink - demand_dsm = solph.custom.SinkDSM(label="DSM", - inputs={b_elec: solph.flows.Flow()}, - demand=data['demand_el'], - capacity_up=data["Cap_up"], - capacity_down=data["Cap_do"], - delay_time=6, - max_demand=1, - max_capacity_up=1, - max_capacity_down=1, - approach="DIW", - cost_dsm_down=5) - es.add(demand_dsm) - -Yielding the following results - -.. image:: _files/Plot_delay_2013-01-01.svg - :width: 85 % - :alt: Plot_delay_2013-01-01.svg - :align: center - - -.. note:: - * Keyword argument `method` from v0.4.1 has been renamed to `approach` in v0.4.2 and methods have been renamed. - * The parameters `demand`, `capacity_up` and `capacity_down` have been normalized to allow investments modeling. To retreive the original dispatch behaviour from v0.4.1, set `max_demand=1`, `max_capacity_up=1`, `max_capacity_down=1`. - * This component is a candidate component. It's implemented as a custom component for users that like to use and test the component at early stage. Please report issues to improve the component. - * See the :py:class:`~oemof.solph.custom.sink_dsm.SinkDSM` class for all parameters and the mathematical background. - - -.. _investment_mode_label: - -Investment optimisation -------------------------- - -As described in :ref:`oemof_solph_optimise_es_label` the typical way to optimise an energy system is the dispatch optimisation based on marginal costs. Solph also provides a combined dispatch and investment optimisation. -This standard investment mode is limited to one period where all investments happen at the start of the optimization time frame. If you want to optimize longer-term horizons and allow investments at the beginning -of each of multiple periods, also taking into account units lifetimes, you can try the :ref:`multi_period_mode_label`. Please be aware that the multi-period feature is experimental. If you experience any bugs or unexpected -behaviour, please report them. - -In the standard investment mode, based on investment costs you can compare the usage of existing components against building up new capacity. -The annual savings by building up new capacity must therefore compensate the annuity of the investment costs (the time period does not have to be one year, but depends on your Datetime index). - -See the API of the :py:class:`~oemof.solph.options.Investment` class to see all possible parameters. - -Basically, an instance of the Investment class can be added to a Flow, a -Storage or a DSM Sink. All parameters that usually refer to the *nominal_value/capacity* will -now refer to the investment variables and existing capacity. It is also -possible to set a maximum limit for the capacity that can be build. -If existing capacity is considered for a component with investment mode enabled, -the *ep_costs* still apply only to the newly built capacity, i.e. the existing capacity -comes at no costs. - -The investment object can be used in Flows and some components. See the -:ref:`oemof_solph_components_label` section for detailed information of each -component. Besides the flows, it can be invested into - -* :ref:`oemof_solph_components_generic_storage_label` and -* :ref:`oemof_solph_custom_sinkdsm_label` - -For example if you want to find out what would be the optimal capacity of a wind -power plant to decrease the costs of an existing energy system, you can define -this model and add an investment source. -The *wind_power_time_series* has to be a normalised feed-in time series of you -wind power plant. The maximum value might be caused by limited space for wind -turbines. - -.. code-block:: python - - solph.components.Source(label='new_wind_pp', outputs={electricity: solph.flows.Flow( - fix=wind_power_time_series, - nominal_value=solph.Investment(ep_costs=epc, maximum=50000))}) - -Let's slightly alter the case and consider for already existing wind power -capacity of 20,000 kW. We're still expecting the total wind power capacity, thus we -allow for 30,000 kW of new installations and formulate as follows. - -.. code-block:: python - - solph.components.Source(label='new_wind_pp', outputs={electricity: solph.flows.Flow( - fix=wind_power_time_series, - nominal_value=solph.Investment(ep_costs=epc, - maximum=30000, - existing=20000))}) - -The periodical costs (*ep_costs*) are typically calculated as annuities, i.e. as follows: - -.. code-block:: python - - capex = 1000 # investment cost - lifetime = 20 # life expectancy - wacc = 0.05 # weighted average of capital cost - epc = capex * (wacc * (1 + wacc) ** lifetime) / ((1 + wacc) ** lifetime - 1) - -This also implemented in the annuity function of the economics module in the oemof.tools package. The code above would look like this: - -.. code-block:: python - - from oemof.tools import economics - epc = economics.annuity(1000, 20, 0.05) - -So far, the investment costs and the installed capacity are mathematically a -line through origin. But what if there is a minimum threshold for doing an -investment, e.g. you cannot buy gas turbines lower than a certain -nominal power, or, the marginal costs of bigger plants -decrease. -Therefore, you can use the parameter *nonconvex* and *offset* of the -investment class. Both, work with investment in flows and storages. Here is an -example of a converter: - -.. code-block:: python - - trafo = solph.components.Converter( - label='converter_nonconvex', - inputs={bus_0: solph.flows.Flow()}, - outputs={bus_1: solph.flows.Flow( - nominal_value=solph.Investment( - ep_costs=4, - maximum=100, - minimum=20, - nonconvex=True, - offset=400))}, - conversion_factors={bus_1: 0.9}) - -In this examples, it is assumed, that independent of the size of the -converter, there are always fix investment costs of 400 (€). -The minimum investment size is 20 (kW) -and the costs per installed unit are 4 (€/kW). With this -option, you could theoretically approximate every cost function you want. But -be aware that for every nonconvex investment flow or storage you are using, -an additional binary variable is created. This might boost your computing time -into the limitless. - -The following figures illustrates the use of the nonconvex investment flow. -Here, :math:`c_{invest,fix}` is the *offset* value and :math:`c_{invest,var}` is -the *ep_costs* value: - -.. image:: _files/nonconvex_invest_investcosts_power.svg - :width: 70 % - :alt: nonconvex_invest_investcosts_power.svg - :align: center - -In case of a convex investment (which is the default setting -`nonconvex=False`), the *minimum* attribute leads to a forced investment, -whereas in the nonconvex case, the investment can become zero as well. - -The calculation of the specific costs per kilowatt installed capacity results -in the following relation for convex and nonconvex investments: - -.. image:: _files/nonconvex_invest_specific_costs.svg - :width: 70 % - :alt: nonconvex_invest_specific_costs.svg - :align: center - -See :py:class:`~oemof.solph.blocks.investment_flow.InvestmentFlow` and -:py:class:`~oemof.solph.components._generic_storage.GenericInvestmentStorageBlock` for all the -mathematical background, like variables and constraints, which are used. - -.. note:: At the moment the investment class is not compatible with the MIP classes :py:class:`~oemof.solph.options.NonConvex`. - - -.. _multi_period_mode_label: - -Multi-period (investment) mode (experimental) ---------------------------------------------- -Sometimes you might be interested in how energy systems could evolve in the longer-term, e.g. until 2045 or 2050 to meet some -carbon neutrality and climate protection or RES and energy efficiency targets. - -While in principle, you could try to model this in oemof.solph using the standard investment mode described above (see :ref:`investment_mode_label`), -you would make the implicit assumption that your entire system is built at the start of your optimization and doesn't change over time. -To address this shortcoming, the multi-period (investment) feature has been introduced. Be aware that it is still experimental. -So feel free to report any bugs or unexpected behaviour if you come across them. - -While in principle, you can define a dispatch-only multi-period system, this doesn't make much sense. The power of the multi-period feature -only unfolds if you look at long-term investments. Let's see how. - -First, you start by defining your energy system as you might have done before, but you - -* choose a longer-term time horizon (spanning multiple years, i.e. multiple periods) and -* explicitly define the `periods` attribute of your energy system which lists the time steps for each period. - -.. code-block:: python - - import pandas as pd - import oemof.solph as solph - - my_index = pd.date_range('1/1/2013', periods=17520, freq='h') - periods = [ - pd.date_range('1/1/2013', periods=8760, freq='h'), - pd.date_range('1/1/2014', periods=8760, freq='h'), - ] - my_energysystem = solph.EnergySystem(timeindex=my_index, periods=periods) - -If you want to use a multi-period model you have define periods of your energy system explicitly. This way, -you are forced to critically think, e.g. about handling leap years, and take some design decisions. It is possible to -define periods with different lengths, but remember that decommissioning of components is possible only at the -beginning of each period. This means that if the life of a component is just a little longer, it will remain for the -entire next period. This can have a particularly large impact the longer your periods are. - -To assist you, here is a plain python snippet that includes leap years which you can just copy -and adjust to your needs: - -.. code-block:: python - - def determine_periods(datetimeindex): - """Explicitly define and return periods of the energy system - - Leap years have 8784 hourly time steps, regular years 8760. - - Parameters - ---------- - datetimeindex : pd.date_range - DatetimeIndex of the model comprising all time steps - - Returns - ------- - periods : list - periods for the optimization run - """ - years = sorted(list(set(getattr(datetimeindex, "year")))) - periods = [] - filter_series = datetimeindex.to_series() - for number, year in enumerate(years): - start = filter_series.loc[filter_series.index.year == year].min() - end = filter_series.loc[filter_series.index.year == year].max() - periods.append(pd.date_range(start, end, freq=datetimeindex.freq)) - - return periods - -So if you want to use this, the above would simplify to: - -.. code-block:: python - - import pandas as pd - import oemof.solph as solph - - # Define your method (or import it from somewhere else) - def determine_periods(datetimeindex): - ... - - my_index = pd.date_range('1/1/2013', periods=17520, freq='h') - periods = determine_periods(my_index) # Make use of method - my_energysystem = solph.EnergySystem(timeindex=my_index, periods=periods) - - -Then you add all the *components* and *buses* to your energy system, just as you are used to with, but with few additions. - -.. code-block:: python - - hydrogen_bus = solph.buses.Bus(label="hydrogen") - coal_bus = solph.buses.Bus(label="coal") - electricity_bus = solph.buses.Bus(label="electricity") - - hydrogen_source = solph.components.Source( - label="green_hydrogen", - outputs={ - hydrogen_bus: solph.flows.Flow( - variable_costs=[25] * 8760 + [30] * 8760 - ) - }, - ) - - coal_source = solph.components.Source( - label="hardcoal", - outputs={ - coal_bus: solph.flows.Flow(variable_costs=[20] * 8760 + [24] * 8760) - }, - ) - - electrical_sink = solph.components.Sink( - label="electricity_demand", - inputs={ - electricity_bus: solph.flows.Flow( - nominal_value=1000, fix=[0.8] * len(my_index) - ) - }, - ) - -So defining buses is the same as for standard models. Also defining components that do not have any investments associated with -them or any lifetime limitations is the same. - -Now if you want to have components that can be invested into, you use the investment option, just as in :ref:`investment_mode_label`, -but with a few minor additions and modifications in the investment object itself which you specify by additional attributes: - -* You have to specify a `lifetime` attribute. This is the components assumed technical lifetime in years. If it is 20 years, - the model invests into it and your simulation has a 30 years horizon, the plant will be decommissioned. Now the model is - free to reinvest or choose another option to fill up the missing capacity. -* You can define an initial `age` if you have `existing` capacity. If you do not specify anything, the default value 0 will be used, - meaning your `existing` capacity has just been newly invested. -* You can define an `interest_rate` that the investor you model has, i.e. the return he desires expressed as the weighted - average osts of capital (wacc) and used for calculating annuities in the model itself. -* You also can define `fixed_costs`, i.e. costs that occur every period independent of the plants usage. - -Here is an example - -.. code-block:: python - - hydrogen_power_plant = solph.components.Converter( - label="hydrogen_pp", - inputs={hydrogen_bus: solph.flows.Flow()}, - outputs={ - electricity_bus: solph.flows.Flow( - nominal_value=solph.Investment( - maximum=1000, - ep_costs=1e6, - lifetime=30, - interest_rate=0.06, - fixed_costs=100, - ), - variable_costs=3, - ) - }, - conversion_factors={electricity_bus: 0.6}, - ) - -.. warning:: - - The `ep_costs` attribute for investments is used in a different way in a multi-period model. Instead - of periodical costs, it depicts (nominal or real) investment expenses, so actual Euros you have to pay per kW or MW - (or whatever power or energy unit) installed. Also, you can depict a change in investment expenses over time, - so instead of providing a scalar value, you could define a list with investment expenses with one value for each period modelled. - - Annuities are calculated within the model. You do not have to do that. - Also the model takes care of discounting future expenses / cashflows. - -Below is what it would look like if you altered `ep_costs` and `fixed_costs` per period. This can be done by simply -providing a list. Note that the length of the list must equal the number of periods of your model. -This would mean that for investments in the particular period, these values would be the one that are applied over their lifetime. - -.. code-block:: python - - hydrogen_power_plant = solph.components.Converter( - label="hydrogen_pp", - inputs={hydrogen_bus: solph.flows.Flow()}, - outputs={ - electricity_bus: solph.flows.Flow( - nominal_value=solph.Investment( - maximum=1000, - ep_costs=[1e6, 1.1e6], - lifetime=30, - interest_rate=0.06, - fixed_costs=[100, 110], - ), - variable_costs=3, - ) - }, - conversion_factors={electricity_bus: 0.6}, - ) - -For components that is not invested into, you also can specify some additional attributes for their inflows and outflows: - -* You can specify a `lifetime` attribute. This can be used to depict existing plants going offline when reaching their lifetime. -* You can define an initial `age`. Also, this can be used for existing plants. -* You also can define `fixed_costs`, i.e. costs that occur every period independent of the plants usage. How they are handled - depends on whether the flow has a limited or an unlimited lifetime. - -.. code-block:: python - - coal_power_plant = solph.components.Converter( - label="existing_coal_pp", - inputs={coal_bus: solph.flows.Flow()}, - outputs={ - electricity_bus: solph.flows.Flow( - nominal_value=600, - max=1, - min=0.4, - lifetime=50, - age=46, - fixed_costs=100, - variable_costs=3, - ) - }, - conversion_factors={electricity_bus: 0.36}, - ) - -To solve our model and retrieve results, you basically perform the same operations as for standard models. -So it works like this: - -.. code-block:: python - - my_energysystem.add( - hydrogen_bus, - coal_bus, - electricity_bus, - hydrogen_source, - coal_source, - electrical_sink, - hydrogen_power_plant, - coal_power_plant, - ) - - om = solph.Model(my_energysystem) - om.solve(solver="cbc", solve_kwargs={"tee": True}) - - # Obtain results - results = solph.processing.results(om) - hydrogen_results = solph.views.node(results, "hydrogen_pp") - - # Show investment plan for hydrogen power plants - print(hydrogen_results["period_scalars"]) - -The keys in the results dict in a multi-period model are "sequences" and "period_scalars". -So for sequences, it is all the same, while for scalar values, we now have values for each period. - -Besides the `invest` variable, new variables are introduced as well. These are: - -* `total`: The total capacity installed, i.e. how much is actually there in a given period. -* `old`: (Overall) capacity to be decommissioned in a given period. -* `old_end`: Endogenous capacity to be decommissioned in a given period. This is capacity that has been invested into - in the model itself. -* `old_exo`: Exogenous capacity to be decommissioned in a given period. This is capacity that was already existing and - given by the `existing` attribute. - -.. note:: - - * You can specify a `discount_rate` for the model. If you do not do so, 0.02 will be used as a default, corresponding - to sort of a social discount rate. If you work with costs in real terms, discounting is obsolete, so define - `discount_rate = 0` in that case. - * You can specify an `interest_rate` for every investment object. If you do not do so, it will be chosen the same - as the model's `discount_rate`. You could use this default to model a perfect competition administered by some sort of - social planner, but even in a social planner setting, you might want to deviate from the `discount_rate` - value and/or discriminate among technologies with different risk profiles and hence different interest requirements. - * For storage units, the `initial_content` is not allowed combined with multi-period investments. - The storage inflow and outflow are forced to zero until the storage unit is invested into. - * You can specify periods of different lengths, but the frequency of your timeindex needs to be consistent. Also, - you could use the `timeincrement` attribute of the energy system to model different weightings. Be aware that this - has not yet been tested. - * For now, both, the `timeindex` as well as the `timeincrement` of an energy system have to be defined since they - have to be of the same length for a multi-period model. - * You can choose whether to re-evaluate assets at the end of the optimization horizon. If you set attribute - `use_remaining_value` of the energy system to True (defaults to False), this leads to the model evaluating the - difference in the asset value at the end of the optimization horizon vs. at the time the investment was made. - The difference in value is added to or subtracted from the respective investment costs increment, - assuming assets are to be liquidated / re-evaluated at the end of the optimization horizon. - * Also please be aware, that periods correspond to years by default. You could also choose - monthly periods, but you would need to be very careful in parameterizing your energy system and your model and also, - this would mean monthly discounting (if applicable) as well as specifying your plants lifetimes in months. - - -Mixed Integer (Linear) Problems -------------------------------- - -Solph also allows you to model components with respect to more technical details, -such as minimum power production. This can be done in both possible combinations, -as dispatch optimization with fixed capacities or combined dispatch and investment optimization. - -Dispatch Optimization -^^^^^^^^^^^^^^^^^^^^^ -In dispatch optimization, it is assumed that the capacities of the assets are already known, -but the optimal dispatch strategy must be obtained. -For this purpose, the class :py:class:`~oemof.solph._options.NonConvex` should be used, as seen in the following example. - -Note that this flow class's usage is incompatible with the :py:mod:`~oemof.solph.options.Investment` option. This means that, -as stated before, the optimal capacity of the converter cannot be obtained using the :py:class:`~oemof.solph.flows.NonConvexFlow` -class, and only the optimal dispatch strategy of an existing asset with a given capacity can be optimized here. - -.. code-block:: python - - b_gas = solph.buses.Bus(label='natural_gas') - b_el = solph.buses.Bus(label='electricity') - b_th = solph.buses.Bus(label='heat') - - solph.components.Converter( - label='pp_chp', - inputs={b_gas: solph.flows.Flow()}, - outputs={b_el: solph.flows.Flow( - nonconvex=solph.NonConvex(), - nominal_value=30, - min=0.5), - b_th: solph.flows.Flow(nominal_value=40)}, - conversion_factors={b_el: 0.3, b_th: 0.4}) - -The class :py:class:`~oemof.solph.options.NonConvex` for the electrical output of the created Converter (i.e., CHP) -will create a 'status' variable for the flow. -This will be used to model, for example, minimal/maximal power production constraints if the -attributes `min`/`max` of the flow are set. It will also be used to include start-up constraints and costs -if corresponding attributes of the class are provided. For more information, see the API of the -:py:class:`~oemof.solph.flows.NonConvexFlow` class. - -.. note:: The usage of this class can sometimes be tricky as there are many interdenpendencies. So - check out the examples and do not hesitate to ask the developers if your model does - not work as expected. - -Combination of Dispatch and Investment Optimisation -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Since version 'v0.5', it is also possilbe to combine the investment and nonconvex option. -Therefore, a new constraint block for flows, called :py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` has been developed, -which combines both :py:class:`~oemof.solph._options.Investment` and :py:class:`~oemof.solph._options.NonConvex` classes. -The new class offers the possibility to perform the investment optimization of an asset considering `min`/`max` values of the flow -as fractions of the optimal capacity. Moreover, it obtains the optimal 'status' of the flow during the simulation period. - -It must be noted that in a straighforward implementation, a binary variable -representing the 'status' of the flow at each time is multiplied by the 'invest' parameter, -which is a continuous variable representing the capacity of the asset being optimized (i.e., :math:`status \times invest`). -This nonlinearity is linearised in the -:py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` - -.. code-block:: python - - b_diesel = solph.buses.Bus(label='diesel') - b_el = solph.buses.Bus(label='electricity') - - solph.components.Converter( - label='diesel_genset', - inputs={b_diesel: solph.flows.Flow()}, - outputs={ - b_el: solph.flows.Flow( - variable_costs=0.04, - min=0.2, - max=1, - nonconvex=solph.NonConvex(), - nominal_value=solph.Investment( - ep_costs=90, - maximum=150, # required for the linearization - ), - ) - }, - conversion_factors={b_el: 0.3}) - -The following diagram shows the duration curve of a typical diesel genset in a hybrid mini-grid system consisting of a diesel genset, -PV cells, battery, inverter, and rectifier. By using the :py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` class, -it is possible to obtain the optimal capacity of this component and simultaneously limit its operation between `min` and `max` loads. - -.. image:: _files/diesel_genset_nonconvex_invest_flow.svg - :width: 100 % - :alt: diesel_genset_nonconvex_invest_flow.svg - :align: center - -Without using the new :py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` class, if the same system is optimized again, but this -time using the :py:class:`~oemof.solph.flows._investment_flow_block.InvestmentFlowBlock`, the corresponding duration curve would be similar to the following -figure. However, assuming that the diesel genset has a minimum operation load of 20% (as seen in the figure), the -:py:class:`~oemof.solph.flows._investment_flow_block.InvestmentFlowBlock` cannot prevent operations at lower loads than 20%, and it would result in -an infeasible operation of this device for around 50% of its annual operation. - -Moreover, using the :py:class:`~oemof.solph.flows._investment_flow_block.InvestmentFlowBlock` class in the given case study would result in a significantly -oversized diesel genset, which has a 30% larger capacity compared with the optimal capacity obtained from the -:py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` class. - -.. image:: _files/diesel_genset_investment_flow.svg - :width: 100 % - :alt: diesel_genset_investment_flow.svg - :align: center - -Solving such an optimisation problem considering `min`/`max` loads without the :py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` class, the only possibility is first to obtain the optimal capacity using the -:py:class:`~oemof.solph.flows._investment_flow_block.InvestmentFlowBlock` and then implement the `min`/`max` loads using the -:py:class:`~oemof.solph.flows._non_convex_flow_block.NonConvexFlowBlock` class. The following duration curve would be obtained by applying -this method to the same diesel genset. - -.. image:: _files/diesel_genset_nonconvex_flow.svg - :width: 100 % - :alt: diesel_genset_nonconvex_flow.svg - :align: center - -Because of the oversized diesel genset obtained from this approach, the capacity of the PV and battery in the given case study -would be 13% and 43% smaller than the capacities obtained using the :py:class:`~oemof.solph.flows.NonConvexInvestmentFlow` class. -This results in a 15% reduction in the share of renewable energy sources to cover the given demand and a higher levelized -cost of electricity. Last but not least, apart from the nonreliable results, using :py:class:`~oemof.solph._options.Investment` -and :py:class:`~oemof.solph._options.NonConvex` classes for the dispatch and investment optimization of the given case study -increases the computation time by more than 9 times compared to the -:py:class:`~oemof.solph.flows.NonConvexInvestmentFlow` class. - - -Adding additional constraints ------------------------------ - -You can add additional constraints to your :py:class:`~oemof.solph.models.Model`. -See :ref:`custom_constraints_label` to learn how to do it. - -Some predefined additional constraints can be found in the -:py:mod:`~oemof.solph.constraints` module. - - * Emission limit for the model -> :func:`~.oemof.solph.constraints.emission_limit` - * Generic integral limit (general form of emission limit) -> :func:`~.oemof.solph.constraints.generic_integral_limit` - * Coupling of two variables e.g. investment variables) with a factor -> :func:`~.oemof.solph.constraints.equate_variables` - * Overall investment limit -> :func:`~.oemof.solph.constraints.investment_limit` - * Generic investment limit -> :func:`~.oemof.solph.constraints.additional_investment_flow_limit` - * Limit active flow count -> :func:`~.oemof.solph.constraints.limit_active_flow_count` - * Limit active flow count by keyword -> :func:`~.oemof.solph.constraints.limit_active_flow_count_by_keyword` - - -The Grouping module (Sets) --------------------------- -To construct constraints, -variables and objective expressions inside all Block classes -and the :py:mod:`~oemof.solph.models` modules, so called groups are used. Consequently, -certain constraints are created for all elements of a specific group. Thus, -mathematically the groups depict sets of elements inside the model. - -The grouping is handled by the solph grouping module :py:mod:`~oemof.solph.groupings` -which is based on the groupings module functionality of oemof network. You -do not need to understand how the underlying functionality works. Instead, checkout -how the solph grouping module is used to create groups. - -The simplest form is a function that looks at every node of the energy system and -returns a key for the group depending e.g. on node attributes: - -.. code-block:: python - - def constraint_grouping(node): - if isinstance(node, Bus) and node.balanced: - return blocks.Bus - if isinstance(node, Converter): - return blocks.Converter - GROUPINGS = [constraint_grouping] - -This function can be passed in a list to `groupings` of -:class:`oemof.solph.network.energy_system.EnergySystem`. So that we end up with two groups, -one with all Converters and one with all Buses that are balanced. These -groups are simply stored in a dictionary. There are some advanced functionalities -to group two connected nodes with their connecting flow and others -(see for example: FlowsWithNodes class in the oemof.network package). - - -Using the Excel (csv) reader ----------------------------- - -Alternatively to a manual creation of energy system component objects as describe above, can also be created from a excel sheet (libreoffice, gnumeric...). - -The idea is to create different sheets within one spreadsheet file for different components. Afterwards you can loop over the rows with the attributes in the columns. The name of the columns may differ from the name of the attribute. You may even create two sheets for the GenericStorage class with attributes such as C-rate for batteries or capacity of turbine for a PHES. - -Once you have create your specific excel reader you can lower the entry barrier for other users. It is some sort of a GUI in form of platform independent spreadsheet software and to make data and models exchangeable in one archive. - -See :ref:`excel_reader_example_label` for an excel reader example. - - -.. _oemof_outputlib_label: - -Handling Results --------------------- - -The main purpose of the processing module is to collect and organise results. -The views module will provide some typical representations of the results. -Plots are not part of solph, because plots are highly individual. However, the -provided pandas.DataFrames are a good start for plots. Some basic functions -for plotting of optimisation results can be found in the separate repository -`oemof_visio `_. - -The ``processing.results`` function gives back the results as a python -dictionary holding pandas Series for scalar values and pandas DataFrames for -all nodes and flows between them. This way we can make use of the full power -of the pandas package available to process the results. - -See the `pandas documentation `_ -to learn how to `visualise -`_, -`read or write -`_ or how to -`access parts of the DataFrame -`_ to -process them. - -The results chapter consists of three parts: - -.. contents:: - :depth: 1 - :local: - :backlinks: top - -The first step is the processing of the results (:ref:`results_collect_results_label`) -This is followed by basic examples of the general analysis of the results -(:ref:`res_general_approach_label`) and finally the use of functionality already included in solph -for providing a quick access to your results (:ref:`results_easy_access_label`). -Especially for larger energy systems the general approach will help you to -write your own results processing functions. - -.. _results_collect_results_label: - -Collecting results -^^^^^^^^^^^^^^^^^^ - -Collecting results can be done with the help of the processing module. A solved -model is needed: - -.. code-block:: python - - [...] - model.solve(solver=solver) - results = solph.processing.results(model) - -The scalars and sequences describe nodes (with keys like (node, None)) and -flows between nodes (with keys like (node_1, node_2)). You can directly extract -the data in the dictionary by using these keys, where "node" is the name of -the object you want to address. -Processing the results is the prerequisite for the examples in the following -sections. - -.. _res_general_approach_label: - -General approach -^^^^^^^^^^^^^^^^ - -As stated above, after processing you will get a dictionary with all result -data. -If you want to access your results directly via labels, you -can continue with :ref:`results_easy_access_label`. For a systematic analysis list comprehensions -are the easiest way of filtering and analysing your results. - -The keys of the results dictionary are tuples containing two nodes. Since flows -have a starting node and an ending node, you get a list of all flows by -filtering the results using the following expression: - -.. code-block:: python - - flows = [x for x in results.keys() if x[1] is not None] - -On the same way you can get a list of all nodes by applying: - -.. code-block:: python - - nodes = [x for x in results.keys() if x[1] is None] - -Probably you will just get storages as nodes, if you have some in your energy -system. Note, that just nodes containing decision variables are listed, e.g. a -Source or a Converter object does not have decision variables. These are in -the flows from or to the nodes. - -All items within the results dictionary are dictionaries and have two items -with 'scalars' and 'sequences' as keys: - -.. code-block:: python - - for flow in flows: - print(flow) - print(results[flow]['scalars']) - print(results[flow]['sequences']) - -There many options of filtering the flows and nodes as you prefer. -The following will give you all flows which are outputs of converter: - -.. code-block:: python - - flows_from_converter = [x for x in flows if isinstance( - x[0], solph.components.Converter)] - -You can filter your flows, if the label of in- or output contains a given -string, e.g.: - -.. code-block:: python - - flows_to_elec = [x for x in results.keys() if 'elec' in x[1].label] - -Getting all labels of the starting node of your investment flows: - -.. code-block:: python - - flows_invest = [x[0].label for x in flows if hasattr( - results[x]['scalars'], 'invest')] - - -.. _results_easy_access_label: - -Easy access -^^^^^^^^^^^ - -The solph package provides some functions which will help you to access your -results directly via labels, which is helpful especially for small energy -systems. -So, if you want to address objects by their label, you can convert the results -dictionary such that the keys are changed to strings given by the labels: - -.. code-block:: python - - views.convert_keys_to_strings(results) - print(results[('wind', 'bus_electricity')]['sequences'] - - -Another option is to access data belonging to a grouping by the name of the grouping -(`note also this section on groupings `_. -Given the label of an object, e.g. 'wind' you can access the grouping by its label -and use this to extract data from the results dictionary. - -.. code-block:: python - - node_wind = energysystem.groups['wind'] - print(results[(node_wind, bus_electricity)]) - - -However, in many situations it might be convenient to use the views module to -collect information on a specific node. You can request all data related to a -specific node by using either the node's variable name or its label: - -.. code-block:: python - - data_wind = solph.views.node(results, 'wind') - - -A function for collecting and printing meta results, i.e. information on the objective function, -the problem and the solver, is provided as well: - -.. code-block:: python - - meta_results = solph.processing.meta_results(om) - pp.pprint(meta_results) +.. _oemof_solph_label: + +.. _using_oemof_label: + +~~~~~~~~~~~~ +User's guide +~~~~~~~~~~~~ + +Solph is an oemof-package, designed to create and solve linear or mixed-integer linear optimization problems. The package is based on pyomo. To create an energy system model generic and specific components are available. To get started with solph, checkout the examples in the :ref:`examples_label` section. + +This User's guide provides a user-friendly introduction into oemof-solph, +which includes small examples and nice illustrations. +However, the functionalities of oemof-solph go beyond the content of this User's guide section. +So, if you want to know all details of a certain component or a function, +please go the :ref:`api_reference_label`. There, you will find +a detailed and complete description of all oemof-solph modules. + +.. contents:: + :depth: 2 + :local: + :backlinks: top + + +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). +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. + +Once the examples work you are close to your first energy model. + + +Handling of Warnings +^^^^^^^^^^^^^^^^^^^^ + +The solph library is designed to be as generic as possible to make it possible +to use it in different use cases. This concept makes it difficult to raise +Errors or Warnings because sometimes untypical combinations of parameters are +allowed even though they might be wrong in over 99% of the use cases. + +Therefore, a SuspiciousUsageWarning was introduced. This warning will warn you +if you do something untypical. If you are sure that you know what you are doing +you can switch the warning off. + +See `the debugging module of oemof-tools `_ for more +information. + + +Set up an energy system +^^^^^^^^^^^^^^^^^^^^^^^ + +In most cases an EnergySystem object is defined when we start to build up an energy system model. The EnergySystem object will be the main container for the model's elements. + +The model time is defined by the number of intervals and the length of intervals. The length of each interval does not have to be the same. This can be defined in two ways: + +1. Define the length of each interval in an array/Series where the number of the elements is the number of intervals. +2. Define a `pandas.DatetimeIndex` with all time steps that encloses an interval. Be aware that you have to define n+1 time points to get n intervals. For non-leap year with hourly values that means 8761 time points to get 8760 interval e.g. 2018-01-01 00:00 to 2019-01-01 00:00. + +The index will also be used for the results. For a numeric index the resulting time series will indexed with a numeric index starting with 0. + +One can use the function +:py:func:`create_time_index` to create an equidistant datetime index. By default the function creates an hourly index for one year, so online the year has to be passed to the function. But it is also possible to change the length of the interval to quarter hours etc. The default number of intervals is the number needed to cover the given year but the value can be overwritten by the user. + +It is also possible to define the datetime index using pandas. See `pandas date_range guide `_ for more information. + +Both code blocks will create an hourly datetime index for 2011: + +.. code-block:: python + + from oemof.solph import create_time_index + my_index = create_time_index(2011) + +.. code-block:: python + + import pandas as pd + my_index = pd.date_range('1/1/2011', periods=8761, freq='h') + +This index can be used to define the EnergySystem: + +.. code-block:: python + + import oemof.solph as solph + my_energysystem = solph.EnergySystem(timeindex=my_index) + +Now you can start to add the components of the network. + + +Add components to the energy system +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +After defining an instance of the EnergySystem class, in the following you have to add all nodes you define to your EnergySystem. + +Basically, there are two types of *nodes* - *components* and *buses*. Every Component has to be connected with one or more *buses*. The connection between a *component* and a *bus* is the *flow*. + +All solph *components* can be used to set up an energy system model but you should read the documentation of each *component* to learn about usage and restrictions. For example it is not possible to combine every *component* with every *flow*. Furthermore, you can add your own *components* in your application (see below) but we would be pleased to integrate them into solph if they are of general interest (see :ref:`feature_requests_and_feedback`). + +An example of a simple energy system shows the usage of the nodes for +real world representations: + +.. image:: _files/oemof_solph_example.svg + :scale: 70 % + :alt: alternate text + :align: center + +The figure shows a simple energy system using the four basic network classes and the Bus class. +If you remove the transmission line (transport 1 and transport 2) you get two systems but they are still one energy system in terms of solph and will be optimised at once. + +There are different ways to add components to an *energy system*. The following line adds a *bus* object to the *energy system* defined above. + +.. code-block:: python + + my_energysystem.add(solph.buses.Bus()) + +It is also possible to assign the bus to a variable and add it afterwards. In that case it is easy to add as many objects as you like. + +.. code-block:: python + + my_bus1 = solph.buses.Bus() + my_bus2 = solph.buses.Bus() + my_energysystem.add(my_bus1, my_bus2) + +Therefore it is also possible to add lists or dictionaries with components but you have to dissolve them. + +.. code-block:: python + + # add a list + my_energysystem.add(*my_list) + + # add a dictionary + my_energysystem.add(*my_dictionary.values()) + + +Bus ++++ + +All flows into and out of a *bus* are balanced (by default). Therefore an instance of the Bus class represents a grid or network without losses. To define an instance of a Bus only a unique label is necessary. If you do not set a label a random label is used but this makes it difficult to get the results later on. + +To make it easier to connect the bus to a component you can optionally assign a variable for later use. + +.. code-block:: python + + solph.buses.Bus(label='natural_gas') + electricity_bus = solph.buses.Bus(label='electricity') + +.. note:: See the :py:class:`~oemof.solph.buses._bus.Bus` class for all parameters and the mathematical background. + + +Flow +++++ + +The flow class has to be used to connect nodes and buses. An instance of the Flow class is normally used in combination with the definition of a component. +A Flow can be limited by upper and lower bounds (constant or time-dependent) or by summarised limits. +For all parameters see the API documentation of the :py:class:`~oemof.solph.flows._flow.Flow` class or the examples of the nodes below. A basic flow can be defined without any parameter. + +.. code-block:: python + + solph.flows.Flow() + +oemof.solph has different types of *flows* but you should be aware that you cannot connect every *flow* type with every *component*. + +.. note:: See the :py:class:`~oemof.solph.flows._flow.Flow` class for all parameters and the mathematical background. + +Components +++++++++++ + +Components are divided in two categories. Well-tested components (solph.components) and experimental components (solph.components.experimental). The experimental section was created to lower the entry barrier for new components. Be aware that these components might not be properly documented or even sometimes do not even work as intended. Let us know if you have successfully used and tested these components. This is the first step to move them to the regular components section. + +See :ref:`oemof_solph_components_label` for a list of all components. + + +.. _oemof_solph_optimise_es_label: + +Optimise your energy system +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The typical optimisation of an energy system in solph is the dispatch optimisation, which means that the use of the sources is optimised to satisfy the demand at least costs. +Therefore, variable cost can be defined for all components. The cost for gas should be defined in the gas source while the variable costs of the gas power plant are caused by operating material. +The actual fuel cost in turn is calculated in the framework itself considering the efficiency of the power plant. +You can deviate from this scheme but you should keep it consistent to make it understandable for others. + +Costs do not have to be monetary costs but could be emissions or other variable units. + +Furthermore, it is possible to optimise the capacity of different components using the investment mode (see :ref:`investment_mode_label`). + +Since v0.5.1, there also is the possibility to have multi-period (i.e. dynamic) investments over longer-time horizon which is in experimental state (see :ref:`multi_period_mode_label`). + +.. code-block:: python + + # set up a simple least cost optimisation + om = solph.Model(my_energysystem) + + # solve the energy model using the CBC solver + om.solve(solver='cbc', solve_kwargs={'tee': True}) + +If you want to analyse the lp-file to see all equations and bounds you can write the file to you disc. In that case you should reduce the timesteps to 3. This will increase the readability of the file. + +.. code-block:: python + + # set up a simple least cost optimisation + om = solph.Model(my_energysystem) + + # write the lp file for debugging or other reasons + om.write('path/my_model.lp', io_options={'symbolic_solver_labels': True}) + +Analysing your results +^^^^^^^^^^^^^^^^^^^^^^ + +If you want to analyse your results, you should first dump your EnergySystem instance to permanently store results. Otherwise you would have to run the simulation again. + +.. code-block:: python + + my_energysystem.results = processing.results(om) + my_energysystem.dump('my_path', 'my_dump.oemof') + +If you need the meta results of the solver you can do the following: + +.. code-block:: python + + my_energysystem.results['main'] = processing.results(om) + my_energysystem.results['meta'] = processing.meta_results(om) + my_energysystem.dump('my_path', 'my_dump.oemof') + +To restore the dump you can simply create an EnergySystem instance and restore your dump into it. + +.. code-block:: python + + import oemof.solph as solph + my_energysystem = solph.EnergySystem() + my_energysystem.restore('my_path', 'my_dump.oemof') + results = my_energysystem.results + + # If you use meta results do the following instead of the previous line. + results = my_energysystem.results['main'] + meta = my_energysystem.results['meta'] + + +If you call dump/restore without any parameters, the dump will be stored as *'es_dump.oemof'* into the *'.oemof/dumps/'* folder created in your HOME directory. + +See :ref:`oemof_outputlib_label` to learn how to process, plot and analyse the results. + + +.. _oemof_solph_components_label: + +Solph components +---------------- + + * :ref:`oemof_solph_components_sink_label` + * :ref:`oemof_solph_components_source_label` + * :ref:`oemof_solph_components_converter_label` + * :ref:`oemof_solph_components_extraction_turbine_chp_label` + * :ref:`oemof_solph_components_generic_caes_label` + * :ref:`oemof_solph_components_generic_chp_label` + * :ref:`oemof_solph_components_generic_storage_label` + * :ref:`oemof_solph_custom_electrical_line_label` + * :ref:`oemof_solph_custom_link_label` + * :ref:`oemof_solph_custom_sinkdsm_label` + + +.. _oemof_solph_components_sink_label: + +Sink (basic) +^^^^^^^^^^^^ + +A sink is normally used to define the demand within an energy model but it can also be used to detect excesses. + +The example shows the electricity demand of the electricity_bus defined above. +The *'my_demand_series'* should be sequence of normalised valueswhile the *'nominal_value'* is the maximum demand the normalised sequence is multiplied with. +Giving *'my_demand_series'* as parameter *'fix'* means that the demand cannot be changed by the solver. + +.. code-block:: python + + solph.components.Sink(label='electricity_demand', inputs={electricity_bus: solph.flows.Flow( + fix=my_demand_series, nominal_value=nominal_demand)}) + +In contrast to the demand sink the excess sink has normally less restrictions but is open to take the whole excess. + +.. code-block:: python + + solph.components.Sink(label='electricity_excess', inputs={electricity_bus: solph.flows.Flow()}) + +.. note:: The Sink class is only a plug and provides no additional constraints or variables. + + +.. _oemof_solph_components_source_label: + +Source (basic) +^^^^^^^^^^^^^^ + +A source can represent a pv-system, a wind power plant, an import of natural gas or a slack variable to avoid creating an in-feasible model. + +While a wind power plant will have as feed-in depending on the weather conditions the natural_gas import might be restricted by maximum value (*nominal_value*) and an annual limit (*full_load_time_max*). +As we do have to pay for imported gas we should set variable costs. +Comparable to the demand series an *fix* is used to define a fixed the normalised output of a wind power plant. +Alternatively, you might use *max* to allow for easy curtailment. +The *nominal_value* sets the installed capacity. + +.. code-block:: python + + solph.components.Source( + label='import_natural_gas', + outputs={my_energysystem.groups['natural_gas']: solph.flows.Flow( + nominal_value=1000, full_load_time_max=1000000, variable_costs=50)}) + + solph.components.Source(label='wind', outputs={electricity_bus: solph.flows.Flow( + fix=wind_power_feedin_series, nominal_value=1000000)}) + +.. note:: The Source class is only a plug and provides no additional constraints or variables. + +.. _oemof_solph_components_converter_label: + +Converter (basic) +^^^^^^^^^^^^^^^^^ + +An instance of the Converter class can represent a node with multiple input and output flows such as a power plant, a transport line or any kind of a transforming process as electrolysis, a cooling device or a heat pump. +The efficiency has to be constant within one time step to get a linear transformation. +You can define a different efficiency for every time step (e.g. the thermal powerplant efficiency according to the ambient temperature) but this series has to be predefined and cannot be changed within the optimisation. + +A condensing power plant can be defined by a converter with one input (fuel) and one output (electricity). + +.. code-block:: python + + b_gas = solph.buses.Bus(label='natural_gas') + b_el = solph.buses.Bus(label='electricity') + + solph.components.Converter( + label="pp_gas", + inputs={bgas: solph.flows.Flow()}, + outputs={b_el: solph.flows.Flow(nominal_value=10e10)}, + conversion_factors={electricity_bus: 0.58}) + +A CHP power plant would be defined in the same manner but with two outputs: + +.. code-block:: python + + b_gas = solph.buses.Bus(label='natural_gas') + b_el = solph.buses.Bus(label='electricity') + b_th = solph.buses.Bus(label='heat') + + solph.components.Converter( + label='pp_chp', + inputs={b_gas: Flow()}, + outputs={b_el: Flow(nominal_value=30), + b_th: Flow(nominal_value=40)}, + conversion_factors={b_el: 0.3, b_th: 0.4}) + +A CHP power plant with 70% coal and 30% natural gas can be defined with two inputs and two outputs: + +.. code-block:: python + + b_gas = solph.buses.Bus(label='natural_gas') + b_coal = solph.buses.Bus(label='hard_coal') + b_el = solph.buses.Bus(label='electricity') + b_th = solph.buses.Bus(label='heat') + + solph.components.Converter( + label='pp_chp', + inputs={b_gas: Flow(), b_coal: Flow()}, + outputs={b_el: Flow(nominal_value=30), + b_th: Flow(nominal_value=40)}, + conversion_factors={b_el: 0.3, b_th: 0.4, + b_coal: 0.7, b_gas: 0.3}) + +A heat pump would be defined in the same manner. New buses are defined to make the code cleaner: + +.. code-block:: python + + b_el = solph.buses.Bus(label='electricity') + b_th_low = solph.buses.Bus(label='low_temp_heat') + b_th_high = solph.buses.Bus(label='high_temp_heat') + + # The cop (coefficient of performance) of the heat pump can be defined as + # a scalar or a sequence. + cop = 3 + + solph.components.Converter( + label='heat_pump', + inputs={b_el: Flow(), b_th_low: Flow()}, + outputs={b_th_high: Flow()}, + conversion_factors={b_el: 1/cop, + b_th_low: (cop-1)/cop}) + +If the low-temperature reservoir is nearly infinite (ambient air heat pump) the +low temperature bus is not needed and, therefore, a Converter with one input +is sufficient. + +.. note:: See the :py:class:`~oemof.solph.components.converter.Converter` class for all parameters and the mathematical background. + +.. _oemof_solph_components_extraction_turbine_chp_label: + +ExtractionTurbineCHP (component) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :py:class:`~oemof.solph.components._extraction_turbine_chp.ExtractionTurbineCHP` +inherits from the :ref:`oemof_solph_components_converter_label` class. Like the name indicates, +the application example for the component is a flexible combined heat and power +(chp) plant. Of course, an instance of this class can represent also another +component with one input and two output flows and a flexible ratio between +these flows, with the following constraints: + +.. include:: ../src/oemof/solph/components/_extraction_turbine_chp.py + :start-after: _ETCHP-equations: + :end-before: """ + +These constraints are applied in addition to those of a standard +:class:`~oemof.solph.components.Converter`. The constraints limit the range of +the possible operation points, like the following picture shows. For a certain +flow of fuel, there is a line of operation points, whose slope is defined by +the power loss factor :math:`\beta` (in some contexts also referred to as +:math:`C_v`). The second constraint limits the decrease of electrical power and +incorporates the backpressure coefficient :math:`C_b`. + +.. image:: _files/ExtractionTurbine_range_of_operation.svg + :width: 70 % + :alt: variable_chp_plot.svg + :align: center + +For now, :py:class:`~oemof.solph.components._extraction_turbine_chp.ExtractionTurbineCHP` instances must +have one input and two output flows. The class allows the definition +of a different efficiency for every time step that can be passed as a series +of parameters that are fixed before the optimisation. In contrast to the +:py:class:`~oemof.solph.components.Converter`, a main flow and a tapped flow is +defined. For the main flow you can define a separate conversion factor that +applies when the second flow is zero (*`conversion_factor_full_condensation`*). + +.. code-block:: python + + solph.components._extractionTurbineCHP( + label='variable_chp_gas', + inputs={b_gas: solph.flows.Flow(nominal_value=10e10)}, + outputs={b_el: solph.flows.Flow(), b_th: solph.flows.Flow()}, + conversion_factors={b_el: 0.3, b_th: 0.5}, + conversion_factor_full_condensation={b_el: 0.5}) + +The key of the parameter *'conversion_factor_full_condensation'* defines which +of the two flows is the main flow. In the example above, the flow to the Bus +*'b_el'* is the main flow and the flow to the Bus *'b_th'* is the tapped flow. +The following plot shows how the variable chp (right) schedules it's electrical +and thermal power production in contrast to a fixed chp (left). The plot is the +output of an example in the `example directory +`_. + +.. image:: _files/variable_chp_plot.svg + :scale: 10 % + :alt: variable_chp_plot.svg + :align: center + +.. note:: See the :py:class:`~oemof.solph.components._extraction_turbine_chp.ExtractionTurbineCHP` class for all parameters and the mathematical background. + + +.. _oemof_solph_components_generic_chp_label: + +GenericCHP (component) +^^^^^^^^^^^^^^^^^^^^^^ + +With the GenericCHP class it is possible to model different types of CHP plants (combined cycle extraction turbines, +back pressure turbines and motoric CHP), which use different ranges of operation, as shown in the figure below. + +.. image:: _files/GenericCHP.svg + :scale: 70 % + :alt: scheme of GenericCHP operation range + :align: center + +Combined cycle extraction turbines: The minimal and maximal electric power without district heating +(red dots in the figure) define maximum load and minimum load of the plant. Beta defines electrical power loss through +heat extraction. The minimal thermal condenser load to cooling water and the share of flue gas losses +at maximal heat extraction determine the right boundary of the operation range. + +.. code-block:: python + + solph.components.GenericCHP( + label='combined_cycle_extraction_turbine', + fuel_input={bgas: solph.flows.Flow( + H_L_FG_share_max=[0.19 for p in range(0, periods)])}, + electrical_output={bel: solph.flows.Flow( + P_max_woDH=[200 for p in range(0, periods)], + P_min_woDH=[80 for p in range(0, periods)], + Eta_el_max_woDH=[0.53 for p in range(0, periods)], + Eta_el_min_woDH=[0.43 for p in range(0, periods)])}, + heat_output={bth: solph.flows.Flow( + Q_CW_min=[30 for p in range(0, periods)])}, + Beta=[0.19 for p in range(0, periods)], + back_pressure=False) + +For modeling a back pressure CHP, the attribute `back_pressure` has to be set to True. +The ratio of power and heat production in a back pressure plant is fixed, therefore the operation range +is just a line (see figure). Again, the `P_min_woDH` and `P_max_woDH`, the efficiencies at these points and the share of flue +gas losses at maximal heat extraction have to be specified. In this case “without district heating” is not to be taken +literally since an operation without heat production is not possible. It is advised to set `Beta` to zero, so the minimal and +maximal electric power without district heating are the same as in the operation point (see figure). The minimal +thermal condenser load to cooling water has to be zero, because there is no condenser besides the district heating unit. + + +.. code-block:: python + + solph.components.GenericCHP( + label='back_pressure_turbine', + fuel_input={bgas: solph.flows.Flow( + H_L_FG_share_max=[0.19 for p in range(0, periods)])}, + electrical_output={bel: solph.flows.Flow( + P_max_woDH=[200 for p in range(0, periods)], + P_min_woDH=[80 for p in range(0, periods)], + Eta_el_max_woDH=[0.53 for p in range(0, periods)], + Eta_el_min_woDH=[0.43 for p in range(0, periods)])}, + heat_output={bth: solph.flows.Flow( + Q_CW_min=[0 for p in range(0, periods)])}, + Beta=[0 for p in range(0, periods)], + back_pressure=True) + +A motoric chp has no condenser, so `Q_CW_min` is zero. Electrical power does not depend on the amount of heat used +so `Beta` is zero. The minimal and maximal electric power (without district heating) and the efficiencies at these +points are needed, whereas the use of electrical power without using thermal energy is not possible. +With `Beta=0` there is no difference between these points and the electrical output in the operation range. +As a consequence of the functionality of a motoric CHP, share of flue gas losses at maximal heat extraction but also +at minimal heat extraction have to be specified. + + +.. code-block:: python + + solph.components.GenericCHP( + label='motoric_chp', + fuel_input={bgas: solph.flows.Flow( + H_L_FG_share_max=[0.18 for p in range(0, periods)], + H_L_FG_share_min=[0.41 for p in range(0, periods)])}, + electrical_output={bel: solph.flows.Flow( + P_max_woDH=[200 for p in range(0, periods)], + P_min_woDH=[100 for p in range(0, periods)], + Eta_el_max_woDH=[0.44 for p in range(0, periods)], + Eta_el_min_woDH=[0.40 for p in range(0, periods)])}, + heat_output={bth: solph.flows.Flow( + Q_CW_min=[0 for p in range(0, periods)])}, + Beta=[0 for p in range(0, periods)], + back_pressure=False) + +Modeling different types of plants means telling the component to use different constraints. Constraint 1 to 9 +are active in all three cases. Constraint 10 depends on the attribute back_pressure. If true, the constraint is +an equality, if not it is a less or equal. Constraint 11 is only needed for modeling motoric CHP which is done by +setting the attribute `H_L_FG_share_min`. + +.. include:: ../src/oemof/solph/components/_generic_chp.py + :start-after: _GenericCHP-equations1-10: + :end-before: **For the attribute** + +If :math:`\dot{H}_{L,FG,min}` is given, e.g. for a motoric CHP: + +.. include:: ../src/oemof/solph/components/_generic_chp.py + :start-after: _GenericCHP-equations11: + :end-before: """ + +.. note:: See the :py:class:`~oemof.solph.components._generic_chp.GenericCHP` class for all parameters and the mathematical background. + + +.. _oemof_solph_components_generic_storage_label: + +GenericStorage (component) +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A component to model a storage with its basic characteristics. The +GenericStorage is designed for one input and one output. +The ``nominal_storage_capacity`` of the storage signifies the storage capacity. You can either set it to the net capacity or to the gross capacity and limit it using the min/max attribute. +To limit the input and output flows, you can define the ``nominal_value`` in the Flow objects. +Furthermore, an efficiency for loading, unloading and a loss rate can be defined. + +.. code-block:: python + + solph.components.GenericStorage( + label='storage', + inputs={b_el: solph.flows.Flow(nominal_value=9, variable_costs=10)}, + outputs={b_el: solph.flows.Flow(nominal_value=25, variable_costs=10)}, + loss_rate=0.001, nominal_storage_capacity=50, + inflow_conversion_factor=0.98, outflow_conversion_factor=0.8) + +For initialising the state of charge before the first time step (time step zero) the parameter ``initial_storage_level`` (default value: ``None``) can be set by a numeric value as fraction of the storage capacity. +Additionally the parameter ``balanced`` (default value: ``True``) sets the relation of the state of charge of time step zero and the last time step. +If ``balanced=True``, the state of charge in the last time step is equal to initial value in time step zero. +Use ``balanced=False`` with caution as energy might be added to or taken from the energy system due to different states of charge in time step zero and the last time step. +Generally, with these two parameters four configurations are possible, which might result in different solutions of the same optimization model: + + * ``initial_storage_level=None``, ``balanced=True`` (default setting): The state of charge in time step zero is a result of the optimization. The state of charge of the last time step is equal to time step zero. Thus, the storage is not violating the energy conservation by adding or taking energy from the system due to different states of charge at the beginning and at the end of the optimization period. + * ``initial_storage_level=0.5``, ``balanced=True``: The state of charge in time step zero is fixed to 0.5 (50 % charged). The state of charge in the last time step is also constrained by 0.5 due to the coupling parameter ``balanced`` set to ``True``. + * ``initial_storage_level=None``, ``balanced=False``: Both, the state of charge in time step zero and the last time step are a result of the optimization and not coupled. + * ``initial_storage_level=0.5``, ``balanced=False``: The state of charge in time step zero is constrained by a given value. The state of charge of the last time step is a result of the optimization. + +The following code block shows an example of the storage parametrization for the second configuration: + +.. code-block:: python + + solph.components.GenericStorage( + label='storage', + inputs={b_el: solph.flows.Flow(nominal_value=9, variable_costs=10)}, + outputs={b_el: solph.flows.Flow(nominal_value=25, variable_costs=10)}, + loss_rate=0.001, nominal_storage_capacity=50, + initial_storage_level=0.5, balanced=True, + inflow_conversion_factor=0.98, outflow_conversion_factor=0.8) + +If you want to view the temporal course of the state of charge of your storage +after the optimisation, you need to check the ``storage_content`` in the results: + +.. code-block:: python + + from oemof.solph import processing, views + results = processing.results(om) + column_name = (('your_storage_label', 'None'), 'storage_content') + SC = views.node(results, 'your_storage_label')['sequences'][column_name] + +The ``storage_content`` is the absolute value of the current stored energy. +By calling: + +.. code-block:: python + + views.node(results, 'your_storage_label')['scalars'] + +you get the results of the scalar values of your storage, e.g. the initial +storage content before time step zero (``init_content``). + +For more information see the definition of the :py:class:`~oemof.solph.components._generic_storage.GenericStorage` class or check the :ref:`examples_label`. + + +Using an investment object with the GenericStorage component ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +Based on the `GenericStorage` object the `GenericInvestmentStorageBlock` adds two main investment possibilities. + + * Invest into the flow parameters e.g. a turbine or a pump + * Invest into capacity of the storage e.g. a basin or a battery cell + +Investment in this context refers to the value of the variable for the 'nominal_value' (installed capacity) in the investment mode. + +As an addition to other flow-investments, the storage class implements the possibility to couple or decouple the flows +with the capacity of the storage. +Three parameters are responsible for connecting the flows and the capacity of the storage: + + * ``invest_relation_input_capacity`` fixes the input flow investment to the capacity investment. A ratio of 1 means that the storage can be filled within one time-period. + * ``invest_relation_output_capacity`` fixes the output flow investment to the capacity investment. A ratio of 1 means that the storage can be emptied within one period. + * ``invest_relation_input_output`` fixes the input flow investment to the output flow investment. For values <1, the input will be smaller and for values >1 the input flow will be larger. + +You should not set all 3 parameters at the same time, since it will lead to overdetermination. + +The following example pictures a Pumped Hydroelectric Energy Storage (PHES). Both flows and the storage itself (representing: pump, turbine, basin) are free in their investment. You can set the parameters to `None` or delete them as `None` is the default value. + +.. code-block:: python + + solph.components.GenericStorage( + label='PHES', + inputs={b_el: solph.flows.Flow(nominal_value=solph.Investment(ep_costs=500))}, + outputs={b_el: solph.flows.Flow(nominal_value=solph.Investment(ep_costs=500)}, + loss_rate=0.001, + inflow_conversion_factor=0.98, outflow_conversion_factor=0.8), + investment = solph.Investment(ep_costs=40)) + +The following example describes a battery with flows coupled to the capacity of the storage. + +.. code-block:: python + + solph.components.GenericStorage( + label='battery', + inputs={b_el: solph.flows.Flow()}, + outputs={b_el: solph.flows.Flow()}, + loss_rate=0.001, + inflow_conversion_factor=0.98, + outflow_conversion_factor=0.8, + invest_relation_input_capacity = 1/6, + invest_relation_output_capacity = 1/6, + investment = solph.Investment(ep_costs=400)) + + +.. note:: See the :py:class:`~oemof.solph.components._generic_storage.GenericStorage` class for all parameters and the mathematical background. + + +.. _oemof_solph_custom_link_label: + +Link +^^^^ + +The `Link` allows to model connections between two busses, e.g. modeling the transshipment of electric energy between two regions. + +.. note:: See the :py:class:`~oemof.solph.components.experimental._link.Link` class for all parameters and the mathematical background. + + + +OffsetConverter (component) +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The `OffsetConverter` object makes it possible to create a Converter with efficiencies depending on the part load condition. +For this it is necessary to define one flow as a nonconvex flow and to set a minimum load. +The following example illustrates how to define an OffsetConverter for given +information for an output, i.e. a combined heat and power plant. The plant +generates up to 100 kW electric energy at an efficiency of 40 %. In minimal +load the electric efficiency is at 30 %, and the minimum possible load is 50 % +of the nominal load. At the same time, heat is produced with a constant +efficiency. By using the `OffsetConverter` a linear relation of in- and output +power with a power dependent efficiency is generated. + +.. code-block:: python + + >>> from oemof import solph + + >>> eta_el_min = 0.3 # electrical efficiency at minimal operation point + >>> eta_el_max = 0.4 # electrical efficiency at nominal operation point + >>> eta_th_min = 0.5 # thermal efficiency at minimal operation point + >>> eta_th_max = 0.5 # thermal efficiency at nominal operation point + >>> P_out_min = 20 # absolute minimal output power + >>> P_out_max = 100 # absolute nominal output power + +As reference for our system we use the input and will mark that flow as +nonconvex respectively. The efficiencies for electricity and heat output have +therefore to be defined with respect to the input flow. The same is true for +the minimal and maximal load. Therefore, we first calculate the minimum and +maximum input of fuel and then derive the slope and offset for both outputs. + +.. code-block:: python + + >>> P_in_max = P_out_max / eta_el_max + >>> P_in_min = P_out_min / eta_el_min + >>> P_in_max + 250.0 + >>> round(P_in_min, 2) + 66.67 + +With that information, we can derive the normed minimal and maximal load of the +nonconvex flow, and calculate the slope and the offset for both outputs. Note, +that the offset for the heat output is 0, because the thermal heat output +efficiency is constant. + +.. code-block:: python + + >>> l_max = 1 + >>> l_min = P_in_min / P_in_max + >>> slope_el, offset_el = solph.components.slope_offset_from_nonconvex_input( + ... l_max, l_min, eta_el_max, eta_el_min + ... ) + >>> slope_th, offset_th = solph.components.slope_offset_from_nonconvex_input( + ... l_max, l_min, eta_th_max, eta_th_min + ... ) + >>> round(slope_el, 3) + 0.436 + >>> round(offset_el, 3) + -0.036 + >>> round(slope_th, 3) + 0.5 + >>> round(offset_th, 3) + 0.0 + +Then we can create our component with the buses attached to it. + +.. code-block:: python + + >>> bfuel = solph.Bus("fuel") + >>> bel = solph.Bus("electricity") + >>> bth = solph.Bus("heat") + + # define OffsetConverter + >>> diesel_genset = solph.components.OffsetConverter( + ... label='boiler', + ... inputs={ + ... bfuel: solph.flows.Flow( + ... nominal_value=P_out_max, + ... max=l_max, + ... min=l_min, + ... nonconvex=solph.NonConvex() + ... ) + ... }, + ... outputs={ + ... bel: solph.flows.Flow(), + ... bth: solph.flows.Flow(), + ... }, + ... conversion_factors={bel: slope_el, bth: slope_th}, + ... normed_offsets={bel: offset_el, bth: offset_th}, + ... ) + +.. note:: + + One of the inputs and outputs has to be a `NonConvex` flow and this flow + will serve as the reference for the `conversion_factors` and the + `normed_offsets`. The `NonConvex` flow also holds + + - the `nominal_value` (or `Investment` in case of investment optimization), + - the `min` and + - the `max` attributes. + + The `conversion_factors` and `normed_offsets` are specified similar to the + `Converter` API with dictionaries referencing the respective input and + output buses. Note, that you cannot have the `conversion_factors` or + `normed_offsets` point to the `NonConvex` flow. + +The following figures show the power at the electrical and the thermal output +and the resepctive ratios to the nonconvex flow (normalized). The efficiency +becomes non-linear. + +.. image:: _files/OffsetConverter_relations_1.svg + :width: 70 % + :alt: OffsetConverter_relations_1.svg + :align: center + + +.. image:: _files/OffsetConverter_relations_2.svg + :width: 70 % + :alt: OffsetConverter_relations_2.svg + :align: center + +.. math:: + + \eta = P(t) / P_\text{ref}(t) + +It also becomes clear, why the component has been named `OffsetConverter`. The +linear equation of inflow to electrical outflow does not hit the origin, but is +offset. By multiplying the offset :math:`y_\text{0,normed}` with the binary +status variable of the `NonConvex` flow, the origin (0, 0) becomes part of the +solution space and the boiler is allowed to switch off. + +.. include:: ../src/oemof/solph/components/_offset_converter.py + :start-after: _OffsetConverter-equations: + :end-before: """ + +The parameters :math:`y_\text{0,normed}` and :math:`m` can be given by scalars or by series in order to define a different efficiency equation for every timestep. +It is also possible to define multiple outputs. + +.. note:: See the :py:class:`~oemof.solph.components._offset_converter.OffsetConverter` class for all parameters and the mathematical background. + + +.. _oemof_solph_custom_electrical_line_label: + +ElectricalLine (experimental) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Electrical line. + +.. note:: See the :py:class:`~oemof.solph.flows.experimental._electrical_line.ElectricalLine` class for all parameters and the mathematical background. + + +.. _oemof_solph_components_generic_caes_label: + +GenericCAES (experimental) +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Compressed Air Energy Storage (CAES). +The following constraints describe the CAES: + +.. include:: ../src/oemof/solph/components/experimental/_generic_caes.py + :start-after: _GenericCAES-equations: + :end-before: """ + +.. note:: See the :py:class:`~oemof.solph.components.experimental._generic_caes.GenericCAES` class for all parameters and the mathematical background. + + +.. _oemof_solph_custom_sinkdsm_label: + +SinkDSM (experimental) +^^^^^^^^^^^^^^^^^^^^^^ + +:class:`~oemof.solph.custom.sink_dsm.SinkDSM` can used to represent flexibility in a demand time series. +It can represent both, load shifting or load shedding. +For load shifting, elasticity of the demand is described by upper (`~oemof.solph.custom.sink_dsm.SinkDSM.capacity_up`) and lower (`~oemof.solph.custom.SinkDSM.capacity_down`) bounds where within the demand is allowed to vary. +Upwards shifted demand is then balanced with downwards shifted demand. +For load shedding, shedding capability is described by `~oemof.solph.custom.SinkDSM.capacity_down`. +It both, load shifting and load shedding are allowed, `~oemof.solph.custom.SinkDSM.capacity_down` limits the sum of both downshift categories. + +:class:`~oemof.solph.custom.sink_dsm.SinkDSM` provides three approaches how the Demand-Side Management (DSM) flexibility is represented in constraints +It can be used for both, dispatch and investments modeling. + +* "DLR": Implementation of the DSM modeling approach from by Gils (2015): `Balancing of Intermittent Renewable Power Generation by Demand Response and Thermal Energy Storage, Stuttgart, `_, + Details: :class:`~oemof.solph.custom.sink_dsm.SinkDSMDLRBlock` and :class:`~oemof.solph.custom.sink_dsm.SinkDSMDLRInvestmentBlock` +* "DIW": Implementation of the DSM modeling approach by Zerrahn & Schill (2015): `On the representation of demand-side management in power system models `_, + in: Energy (84), pp. 840-845, 10.1016/j.energy.2015.03.037. Details: :class:`~oemof.solph.custom.sink_dsm.SinkDSMDIWBlock` and :class:`~oemof.solph.custom.sink_dsm.SinkDSMDIWInvestmentBlock` +* "oemof": Is a fairly simple approach. Within a defined windows of time steps, demand can be shifted within the defined bounds of elasticity. + The window sequentially moves forwards. Details: :class:`~oemof.solph.custom.sink_dsm.SinkDSMOemofBlock` and :class:`~oemof.solph.custom.sink_dsm.SinkDSMOemofInvestmentBlock` + +Cost can be associated to either demand up shifts or demand down shifts or both. + +This small example of PV, grid and SinkDSM shows how to use the component + +.. code-block:: python + + # Create some data + pv_day = [(-(1 / 6 * x ** 2) + 6) / 6 for x in range(-6, 7)] + pv_ts = [0] * 6 + pv_day + [0] * 6 + data_dict = {"demand_el": [3] * len(pv_ts), + "pv": pv_ts, + "Cap_up": [0.5] * len(pv_ts), + "Cap_do": [0.5] * len(pv_ts)} + data = pd.DataFrame.from_dict(data_dict) + + # Do timestamp stuff + datetimeindex = pd.date_range(start='1/1/2013', periods=len(data.index), freq='h') + data['timestamp'] = datetimeindex + data.set_index('timestamp', inplace=True) + + # Create Energy System + es = solph.EnergySystem(timeindex=datetimeindex) + + # Create bus representing electricity grid + b_elec = solph.buses.Bus(label='Electricity bus') + es.add(b_elec) + + # Create a back supply + grid = solph.components.Source(label='Grid', + outputs={ + b_elec: solph.flows.Flow( + nominal_value=10000, + variable_costs=50)} + ) + es.add(grid) + + # PV supply from time series + s_wind = solph.components.Source(label='wind', + outputs={ + b_elec: solph.flows.Flow( + fix=data['pv'], + nominal_value=3.5)} + ) + es.add(s_wind) + + # Create DSM Sink + demand_dsm = solph.custom.SinkDSM(label="DSM", + inputs={b_elec: solph.flows.Flow()}, + demand=data['demand_el'], + capacity_up=data["Cap_up"], + capacity_down=data["Cap_do"], + delay_time=6, + max_demand=1, + max_capacity_up=1, + max_capacity_down=1, + approach="DIW", + cost_dsm_down=5) + es.add(demand_dsm) + +Yielding the following results + +.. image:: _files/Plot_delay_2013-01-01.svg + :width: 85 % + :alt: Plot_delay_2013-01-01.svg + :align: center + + +.. note:: + * Keyword argument `method` from v0.4.1 has been renamed to `approach` in v0.4.2 and methods have been renamed. + * The parameters `demand`, `capacity_up` and `capacity_down` have been normalized to allow investments modeling. To retreive the original dispatch behaviour from v0.4.1, set `max_demand=1`, `max_capacity_up=1`, `max_capacity_down=1`. + * This component is a candidate component. It's implemented as a custom component for users that like to use and test the component at early stage. Please report issues to improve the component. + * See the :py:class:`~oemof.solph.custom.sink_dsm.SinkDSM` class for all parameters and the mathematical background. + + +.. _investment_mode_label: + +Investment optimisation +------------------------- + +As described in :ref:`oemof_solph_optimise_es_label` the typical way to optimise an energy system is the dispatch optimisation based on marginal costs. Solph also provides a combined dispatch and investment optimisation. +This standard investment mode is limited to one period where all investments happen at the start of the optimization time frame. If you want to optimize longer-term horizons and allow investments at the beginning +of each of multiple periods, also taking into account units lifetimes, you can try the :ref:`multi_period_mode_label`. Please be aware that the multi-period feature is experimental. If you experience any bugs or unexpected +behaviour, please report them. + +In the standard investment mode, based on investment costs you can compare the usage of existing components against building up new capacity. +The annual savings by building up new capacity must therefore compensate the annuity of the investment costs (the time period does not have to be one year, but depends on your Datetime index). + +See the API of the :py:class:`~oemof.solph.options.Investment` class to see all possible parameters. + +Basically, an instance of the Investment class can be added to a Flow, a +Storage or a DSM Sink. All parameters that usually refer to the *nominal_value/capacity* will +now refer to the investment variables and existing capacity. It is also +possible to set a maximum limit for the capacity that can be build. +If existing capacity is considered for a component with investment mode enabled, +the *ep_costs* still apply only to the newly built capacity, i.e. the existing capacity +comes at no costs. + +The investment object can be used in Flows and some components. See the +:ref:`oemof_solph_components_label` section for detailed information of each +component. Besides the flows, it can be invested into + +* :ref:`oemof_solph_components_generic_storage_label` and +* :ref:`oemof_solph_custom_sinkdsm_label` + +For example if you want to find out what would be the optimal capacity of a wind +power plant to decrease the costs of an existing energy system, you can define +this model and add an investment source. +The *wind_power_time_series* has to be a normalised feed-in time series of you +wind power plant. The maximum value might be caused by limited space for wind +turbines. + +.. code-block:: python + + solph.components.Source(label='new_wind_pp', outputs={electricity: solph.flows.Flow( + fix=wind_power_time_series, + nominal_value=solph.Investment(ep_costs=epc, maximum=50000))}) + +Let's slightly alter the case and consider for already existing wind power +capacity of 20,000 kW. We're still expecting the total wind power capacity, thus we +allow for 30,000 kW of new installations and formulate as follows. + +.. code-block:: python + + solph.components.Source(label='new_wind_pp', outputs={electricity: solph.flows.Flow( + fix=wind_power_time_series, + nominal_value=solph.Investment(ep_costs=epc, + maximum=30000, + existing=20000))}) + +The periodical costs (*ep_costs*) are typically calculated as annuities, i.e. as follows: + +.. code-block:: python + + capex = 1000 # investment cost + lifetime = 20 # life expectancy + wacc = 0.05 # weighted average of capital cost + epc = capex * (wacc * (1 + wacc) ** lifetime) / ((1 + wacc) ** lifetime - 1) + +This also implemented in the annuity function of the economics module in the oemof.tools package. The code above would look like this: + +.. code-block:: python + + from oemof.tools import economics + epc = economics.annuity(1000, 20, 0.05) + +So far, the investment costs and the installed capacity are mathematically a +line through origin. But what if there is a minimum threshold for doing an +investment, e.g. you cannot buy gas turbines lower than a certain +nominal power, or, the marginal costs of bigger plants +decrease. +Therefore, you can use the parameter *nonconvex* and *offset* of the +investment class. Both, work with investment in flows and storages. Here is an +example of a converter: + +.. code-block:: python + + trafo = solph.components.Converter( + label='converter_nonconvex', + inputs={bus_0: solph.flows.Flow()}, + outputs={bus_1: solph.flows.Flow( + nominal_value=solph.Investment( + ep_costs=4, + maximum=100, + minimum=20, + nonconvex=True, + offset=400))}, + conversion_factors={bus_1: 0.9}) + +In this examples, it is assumed, that independent of the size of the +converter, there are always fix investment costs of 400 (€). +The minimum investment size is 20 (kW) +and the costs per installed unit are 4 (€/kW). With this +option, you could theoretically approximate every cost function you want. But +be aware that for every nonconvex investment flow or storage you are using, +an additional binary variable is created. This might boost your computing time +into the limitless. + +The following figures illustrates the use of the nonconvex investment flow. +Here, :math:`c_{invest,fix}` is the *offset* value and :math:`c_{invest,var}` is +the *ep_costs* value: + +.. image:: _files/nonconvex_invest_investcosts_power.svg + :width: 70 % + :alt: nonconvex_invest_investcosts_power.svg + :align: center + +In case of a convex investment (which is the default setting +`nonconvex=False`), the *minimum* attribute leads to a forced investment, +whereas in the nonconvex case, the investment can become zero as well. + +The calculation of the specific costs per kilowatt installed capacity results +in the following relation for convex and nonconvex investments: + +.. image:: _files/nonconvex_invest_specific_costs.svg + :width: 70 % + :alt: nonconvex_invest_specific_costs.svg + :align: center + +See :py:class:`~oemof.solph.blocks.investment_flow.InvestmentFlow` and +:py:class:`~oemof.solph.components._generic_storage.GenericInvestmentStorageBlock` for all the +mathematical background, like variables and constraints, which are used. + +.. note:: At the moment the investment class is not compatible with the MIP classes :py:class:`~oemof.solph.options.NonConvex`. + + +.. _multi_period_mode_label: + +Multi-period (investment) mode (experimental) +--------------------------------------------- +Sometimes you might be interested in how energy systems could evolve in the longer-term, e.g. until 2045 or 2050 to meet some +carbon neutrality and climate protection or RES and energy efficiency targets. + +While in principle, you could try to model this in oemof.solph using the standard investment mode described above (see :ref:`investment_mode_label`), +you would make the implicit assumption that your entire system is built at the start of your optimization and doesn't change over time. +To address this shortcoming, the multi-period (investment) feature has been introduced. Be aware that it is still experimental. +So feel free to report any bugs or unexpected behaviour if you come across them. + +While in principle, you can define a dispatch-only multi-period system, this doesn't make much sense. The power of the multi-period feature +only unfolds if you look at long-term investments. Let's see how. + +First, you start by defining your energy system as you might have done before, but you + +* choose a longer-term time horizon (spanning multiple years, i.e. multiple periods) and +* explicitly define the `periods` attribute of your energy system which lists the time steps for each period. + +.. code-block:: python + + import pandas as pd + import oemof.solph as solph + + my_index = pd.date_range('1/1/2013', periods=17520, freq='h') + periods = [ + pd.date_range('1/1/2013', periods=8760, freq='h'), + pd.date_range('1/1/2014', periods=8760, freq='h'), + ] + my_energysystem = solph.EnergySystem(timeindex=my_index, periods=periods) + +If you want to use a multi-period model you have define periods of your energy system explicitly. This way, +you are forced to critically think, e.g. about handling leap years, and take some design decisions. It is possible to +define periods with different lengths, but remember that decommissioning of components is possible only at the +beginning of each period. This means that if the life of a component is just a little longer, it will remain for the +entire next period. This can have a particularly large impact the longer your periods are. + +To assist you, here is a plain python snippet that includes leap years which you can just copy +and adjust to your needs: + +.. code-block:: python + + def determine_periods(datetimeindex): + """Explicitly define and return periods of the energy system + + Leap years have 8784 hourly time steps, regular years 8760. + + Parameters + ---------- + datetimeindex : pd.date_range + DatetimeIndex of the model comprising all time steps + + Returns + ------- + periods : list + periods for the optimization run + """ + years = sorted(list(set(getattr(datetimeindex, "year")))) + periods = [] + filter_series = datetimeindex.to_series() + for number, year in enumerate(years): + start = filter_series.loc[filter_series.index.year == year].min() + end = filter_series.loc[filter_series.index.year == year].max() + periods.append(pd.date_range(start, end, freq=datetimeindex.freq)) + + return periods + +So if you want to use this, the above would simplify to: + +.. code-block:: python + + import pandas as pd + import oemof.solph as solph + + # Define your method (or import it from somewhere else) + def determine_periods(datetimeindex): + ... + + my_index = pd.date_range('1/1/2013', periods=17520, freq='h') + periods = determine_periods(my_index) # Make use of method + my_energysystem = solph.EnergySystem(timeindex=my_index, periods=periods) + + +Then you add all the *components* and *buses* to your energy system, just as you are used to with, but with few additions. + +.. code-block:: python + + hydrogen_bus = solph.buses.Bus(label="hydrogen") + coal_bus = solph.buses.Bus(label="coal") + electricity_bus = solph.buses.Bus(label="electricity") + + hydrogen_source = solph.components.Source( + label="green_hydrogen", + outputs={ + hydrogen_bus: solph.flows.Flow( + variable_costs=[25] * 8760 + [30] * 8760 + ) + }, + ) + + coal_source = solph.components.Source( + label="hardcoal", + outputs={ + coal_bus: solph.flows.Flow(variable_costs=[20] * 8760 + [24] * 8760) + }, + ) + + electrical_sink = solph.components.Sink( + label="electricity_demand", + inputs={ + electricity_bus: solph.flows.Flow( + nominal_value=1000, fix=[0.8] * len(my_index) + ) + }, + ) + +So defining buses is the same as for standard models. Also defining components that do not have any investments associated with +them or any lifetime limitations is the same. + +Now if you want to have components that can be invested into, you use the investment option, just as in :ref:`investment_mode_label`, +but with a few minor additions and modifications in the investment object itself which you specify by additional attributes: + +* You have to specify a `lifetime` attribute. This is the components assumed technical lifetime in years. If it is 20 years, + the model invests into it and your simulation has a 30 years horizon, the plant will be decommissioned. Now the model is + free to reinvest or choose another option to fill up the missing capacity. +* You can define an initial `age` if you have `existing` capacity. If you do not specify anything, the default value 0 will be used, + meaning your `existing` capacity has just been newly invested. +* You can define an `interest_rate` that the investor you model has, i.e. the return he desires expressed as the weighted + average osts of capital (wacc) and used for calculating annuities in the model itself. +* You also can define `fixed_costs`, i.e. costs that occur every period independent of the plants usage. + +Here is an example + +.. code-block:: python + + hydrogen_power_plant = solph.components.Converter( + label="hydrogen_pp", + inputs={hydrogen_bus: solph.flows.Flow()}, + outputs={ + electricity_bus: solph.flows.Flow( + nominal_value=solph.Investment( + maximum=1000, + ep_costs=1e6, + lifetime=30, + interest_rate=0.06, + fixed_costs=100, + ), + variable_costs=3, + ) + }, + conversion_factors={electricity_bus: 0.6}, + ) + +.. warning:: + + The `ep_costs` attribute for investments is used in a different way in a multi-period model. Instead + of periodical costs, it depicts (nominal or real) investment expenses, so actual Euros you have to pay per kW or MW + (or whatever power or energy unit) installed. Also, you can depict a change in investment expenses over time, + so instead of providing a scalar value, you could define a list with investment expenses with one value for each period modelled. + + Annuities are calculated within the model. You do not have to do that. + Also the model takes care of discounting future expenses / cashflows. + +Below is what it would look like if you altered `ep_costs` and `fixed_costs` per period. This can be done by simply +providing a list. Note that the length of the list must equal the number of periods of your model. +This would mean that for investments in the particular period, these values would be the one that are applied over their lifetime. + +.. code-block:: python + + hydrogen_power_plant = solph.components.Converter( + label="hydrogen_pp", + inputs={hydrogen_bus: solph.flows.Flow()}, + outputs={ + electricity_bus: solph.flows.Flow( + nominal_value=solph.Investment( + maximum=1000, + ep_costs=[1e6, 1.1e6], + lifetime=30, + interest_rate=0.06, + fixed_costs=[100, 110], + ), + variable_costs=3, + ) + }, + conversion_factors={electricity_bus: 0.6}, + ) + +For components that is not invested into, you also can specify some additional attributes for their inflows and outflows: + +* You can specify a `lifetime` attribute. This can be used to depict existing plants going offline when reaching their lifetime. +* You can define an initial `age`. Also, this can be used for existing plants. +* You also can define `fixed_costs`, i.e. costs that occur every period independent of the plants usage. How they are handled + depends on whether the flow has a limited or an unlimited lifetime. + +.. code-block:: python + + coal_power_plant = solph.components.Converter( + label="existing_coal_pp", + inputs={coal_bus: solph.flows.Flow()}, + outputs={ + electricity_bus: solph.flows.Flow( + nominal_value=600, + max=1, + min=0.4, + lifetime=50, + age=46, + fixed_costs=100, + variable_costs=3, + ) + }, + conversion_factors={electricity_bus: 0.36}, + ) + +To solve our model and retrieve results, you basically perform the same operations as for standard models. +So it works like this: + +.. code-block:: python + + my_energysystem.add( + hydrogen_bus, + coal_bus, + electricity_bus, + hydrogen_source, + coal_source, + electrical_sink, + hydrogen_power_plant, + coal_power_plant, + ) + + om = solph.Model(my_energysystem) + om.solve(solver="cbc", solve_kwargs={"tee": True}) + + # Obtain results + results = solph.processing.results(om) + hydrogen_results = solph.views.node(results, "hydrogen_pp") + + # Show investment plan for hydrogen power plants + print(hydrogen_results["period_scalars"]) + +The keys in the results dict in a multi-period model are "sequences" and "period_scalars". +So for sequences, it is all the same, while for scalar values, we now have values for each period. + +Besides the `invest` variable, new variables are introduced as well. These are: + +* `total`: The total capacity installed, i.e. how much is actually there in a given period. +* `old`: (Overall) capacity to be decommissioned in a given period. +* `old_end`: Endogenous capacity to be decommissioned in a given period. This is capacity that has been invested into + in the model itself. +* `old_exo`: Exogenous capacity to be decommissioned in a given period. This is capacity that was already existing and + given by the `existing` attribute. + +.. note:: + + * You can specify a `discount_rate` for the model. If you do not do so, 0.02 will be used as a default, corresponding + to sort of a social discount rate. If you work with costs in real terms, discounting is obsolete, so define + `discount_rate = 0` in that case. + * You can specify an `interest_rate` for every investment object. If you do not do so, it will be chosen the same + as the model's `discount_rate`. You could use this default to model a perfect competition administered by some sort of + social planner, but even in a social planner setting, you might want to deviate from the `discount_rate` + value and/or discriminate among technologies with different risk profiles and hence different interest requirements. + * For storage units, the `initial_content` is not allowed combined with multi-period investments. + The storage inflow and outflow are forced to zero until the storage unit is invested into. + * You can specify periods of different lengths, but the frequency of your timeindex needs to be consistent. Also, + you could use the `timeincrement` attribute of the energy system to model different weightings. Be aware that this + has not yet been tested. + * For now, both, the `timeindex` as well as the `timeincrement` of an energy system have to be defined since they + have to be of the same length for a multi-period model. + * You can choose whether to re-evaluate assets at the end of the optimization horizon. If you set attribute + `use_remaining_value` of the energy system to True (defaults to False), this leads to the model evaluating the + difference in the asset value at the end of the optimization horizon vs. at the time the investment was made. + The difference in value is added to or subtracted from the respective investment costs increment, + assuming assets are to be liquidated / re-evaluated at the end of the optimization horizon. + * Also please be aware, that periods correspond to years by default. You could also choose + monthly periods, but you would need to be very careful in parameterizing your energy system and your model and also, + this would mean monthly discounting (if applicable) as well as specifying your plants lifetimes in months. + + +Mixed Integer (Linear) Problems +------------------------------- + +Solph also allows you to model components with respect to more technical details, +such as minimum power production. This can be done in both possible combinations, +as dispatch optimization with fixed capacities or combined dispatch and investment optimization. + +Dispatch Optimization +^^^^^^^^^^^^^^^^^^^^^ +In dispatch optimization, it is assumed that the capacities of the assets are already known, +but the optimal dispatch strategy must be obtained. +For this purpose, the class :py:class:`~oemof.solph._options.NonConvex` should be used, as seen in the following example. + +Note that this flow class's usage is incompatible with the :py:mod:`~oemof.solph.options.Investment` option. This means that, +as stated before, the optimal capacity of the converter cannot be obtained using the :py:class:`~oemof.solph.flows.NonConvexFlow` +class, and only the optimal dispatch strategy of an existing asset with a given capacity can be optimized here. + +.. code-block:: python + + b_gas = solph.buses.Bus(label='natural_gas') + b_el = solph.buses.Bus(label='electricity') + b_th = solph.buses.Bus(label='heat') + + solph.components.Converter( + label='pp_chp', + inputs={b_gas: solph.flows.Flow()}, + outputs={b_el: solph.flows.Flow( + nonconvex=solph.NonConvex(), + nominal_value=30, + min=0.5), + b_th: solph.flows.Flow(nominal_value=40)}, + conversion_factors={b_el: 0.3, b_th: 0.4}) + +The class :py:class:`~oemof.solph.options.NonConvex` for the electrical output of the created Converter (i.e., CHP) +will create a 'status' variable for the flow. +This will be used to model, for example, minimal/maximal power production constraints if the +attributes `min`/`max` of the flow are set. It will also be used to include start-up constraints and costs +if corresponding attributes of the class are provided. For more information, see the API of the +:py:class:`~oemof.solph.flows.NonConvexFlow` class. + +.. note:: The usage of this class can sometimes be tricky as there are many interdenpendencies. So + check out the examples and do not hesitate to ask the developers if your model does + not work as expected. + +Combination of Dispatch and Investment Optimisation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Since version 'v0.5', it is also possilbe to combine the investment and nonconvex option. +Therefore, a new constraint block for flows, called :py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` has been developed, +which combines both :py:class:`~oemof.solph._options.Investment` and :py:class:`~oemof.solph._options.NonConvex` classes. +The new class offers the possibility to perform the investment optimization of an asset considering `min`/`max` values of the flow +as fractions of the optimal capacity. Moreover, it obtains the optimal 'status' of the flow during the simulation period. + +It must be noted that in a straighforward implementation, a binary variable +representing the 'status' of the flow at each time is multiplied by the 'invest' parameter, +which is a continuous variable representing the capacity of the asset being optimized (i.e., :math:`status \times invest`). +This nonlinearity is linearised in the +:py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` + +.. code-block:: python + + b_diesel = solph.buses.Bus(label='diesel') + b_el = solph.buses.Bus(label='electricity') + + solph.components.Converter( + label='diesel_genset', + inputs={b_diesel: solph.flows.Flow()}, + outputs={ + b_el: solph.flows.Flow( + variable_costs=0.04, + min=0.2, + max=1, + nonconvex=solph.NonConvex(), + nominal_value=solph.Investment( + ep_costs=90, + maximum=150, # required for the linearization + ), + ) + }, + conversion_factors={b_el: 0.3}) + +The following diagram shows the duration curve of a typical diesel genset in a hybrid mini-grid system consisting of a diesel genset, +PV cells, battery, inverter, and rectifier. By using the :py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` class, +it is possible to obtain the optimal capacity of this component and simultaneously limit its operation between `min` and `max` loads. + +.. image:: _files/diesel_genset_nonconvex_invest_flow.svg + :width: 100 % + :alt: diesel_genset_nonconvex_invest_flow.svg + :align: center + +Without using the new :py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` class, if the same system is optimized again, but this +time using the :py:class:`~oemof.solph.flows._investment_flow_block.InvestmentFlowBlock`, the corresponding duration curve would be similar to the following +figure. However, assuming that the diesel genset has a minimum operation load of 20% (as seen in the figure), the +:py:class:`~oemof.solph.flows._investment_flow_block.InvestmentFlowBlock` cannot prevent operations at lower loads than 20%, and it would result in +an infeasible operation of this device for around 50% of its annual operation. + +Moreover, using the :py:class:`~oemof.solph.flows._investment_flow_block.InvestmentFlowBlock` class in the given case study would result in a significantly +oversized diesel genset, which has a 30% larger capacity compared with the optimal capacity obtained from the +:py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` class. + +.. image:: _files/diesel_genset_investment_flow.svg + :width: 100 % + :alt: diesel_genset_investment_flow.svg + :align: center + +Solving such an optimisation problem considering `min`/`max` loads without the :py:class:`~oemof.solph.flows._invest_non_convex_flow_block.InvestNonConvexFlowBlock` class, the only possibility is first to obtain the optimal capacity using the +:py:class:`~oemof.solph.flows._investment_flow_block.InvestmentFlowBlock` and then implement the `min`/`max` loads using the +:py:class:`~oemof.solph.flows._non_convex_flow_block.NonConvexFlowBlock` class. The following duration curve would be obtained by applying +this method to the same diesel genset. + +.. image:: _files/diesel_genset_nonconvex_flow.svg + :width: 100 % + :alt: diesel_genset_nonconvex_flow.svg + :align: center + +Because of the oversized diesel genset obtained from this approach, the capacity of the PV and battery in the given case study +would be 13% and 43% smaller than the capacities obtained using the :py:class:`~oemof.solph.flows.NonConvexInvestmentFlow` class. +This results in a 15% reduction in the share of renewable energy sources to cover the given demand and a higher levelized +cost of electricity. Last but not least, apart from the nonreliable results, using :py:class:`~oemof.solph._options.Investment` +and :py:class:`~oemof.solph._options.NonConvex` classes for the dispatch and investment optimization of the given case study +increases the computation time by more than 9 times compared to the +:py:class:`~oemof.solph.flows.NonConvexInvestmentFlow` class. + + +Adding additional constraints +----------------------------- + +You can add additional constraints to your :py:class:`~oemof.solph.models.Model`. +See :ref:`custom_constraints_label` to learn how to do it. + +Some predefined additional constraints can be found in the +:py:mod:`~oemof.solph.constraints` module. + + * Emission limit for the model -> :func:`~.oemof.solph.constraints.emission_limit` + * Generic integral limit (general form of emission limit) -> :func:`~.oemof.solph.constraints.generic_integral_limit` + * Coupling of two variables e.g. investment variables) with a factor -> :func:`~.oemof.solph.constraints.equate_variables` + * Overall investment limit -> :func:`~.oemof.solph.constraints.investment_limit` + * Generic investment limit -> :func:`~.oemof.solph.constraints.additional_investment_flow_limit` + * Limit active flow count -> :func:`~.oemof.solph.constraints.limit_active_flow_count` + * Limit active flow count by keyword -> :func:`~.oemof.solph.constraints.limit_active_flow_count_by_keyword` + + +The Grouping module (Sets) +-------------------------- +To construct constraints, +variables and objective expressions inside all Block classes +and the :py:mod:`~oemof.solph.models` modules, so called groups are used. Consequently, +certain constraints are created for all elements of a specific group. Thus, +mathematically the groups depict sets of elements inside the model. + +The grouping is handled by the solph grouping module :py:mod:`~oemof.solph.groupings` +which is based on the groupings module functionality of oemof network. You +do not need to understand how the underlying functionality works. Instead, checkout +how the solph grouping module is used to create groups. + +The simplest form is a function that looks at every node of the energy system and +returns a key for the group depending e.g. on node attributes: + +.. code-block:: python + + def constraint_grouping(node): + if isinstance(node, Bus) and node.balanced: + return blocks.Bus + if isinstance(node, Converter): + return blocks.Converter + GROUPINGS = [constraint_grouping] + +This function can be passed in a list to `groupings` of +:class:`oemof.solph.network.energy_system.EnergySystem`. So that we end up with two groups, +one with all Converters and one with all Buses that are balanced. These +groups are simply stored in a dictionary. There are some advanced functionalities +to group two connected nodes with their connecting flow and others +(see for example: FlowsWithNodes class in the oemof.network package). + + +Using the Excel (csv) reader +---------------------------- + +Alternatively to a manual creation of energy system component objects as describe above, can also be created from a excel sheet (libreoffice, gnumeric...). + +The idea is to create different sheets within one spreadsheet file for different components. Afterwards you can loop over the rows with the attributes in the columns. The name of the columns may differ from the name of the attribute. You may even create two sheets for the GenericStorage class with attributes such as C-rate for batteries or capacity of turbine for a PHES. + +Once you have create your specific excel reader you can lower the entry barrier for other users. It is some sort of a GUI in form of platform independent spreadsheet software and to make data and models exchangeable in one archive. + +See :ref:`excel_reader_example_label` for an excel reader example. + + +.. _oemof_outputlib_label: + +Handling Results +-------------------- + +The main purpose of the processing module is to collect and organise results. +The views module will provide some typical representations of the results. +Plots are not part of solph, because plots are highly individual. However, the +provided pandas.DataFrames are a good start for plots. Some basic functions +for plotting of optimisation results can be found in the separate repository +`oemof_visio `_. + +The ``processing.results`` function gives back the results as a python +dictionary holding pandas Series for scalar values and pandas DataFrames for +all nodes and flows between them. This way we can make use of the full power +of the pandas package available to process the results. + +See the `pandas documentation `_ +to learn how to `visualise +`_, +`read or write +`_ or how to +`access parts of the DataFrame +`_ to +process them. + +The results chapter consists of three parts: + +.. contents:: + :depth: 1 + :local: + :backlinks: top + +The first step is the processing of the results (:ref:`results_collect_results_label`) +This is followed by basic examples of the general analysis of the results +(:ref:`res_general_approach_label`) and finally the use of functionality already included in solph +for providing a quick access to your results (:ref:`results_easy_access_label`). +Especially for larger energy systems the general approach will help you to +write your own results processing functions. + +.. _results_collect_results_label: + +Collecting results +^^^^^^^^^^^^^^^^^^ + +Collecting results can be done with the help of the processing module. A solved +model is needed: + +.. code-block:: python + + [...] + model.solve(solver=solver) + results = solph.processing.results(model) + +The scalars and sequences describe nodes (with keys like (node, None)) and +flows between nodes (with keys like (node_1, node_2)). You can directly extract +the data in the dictionary by using these keys, where "node" is the name of +the object you want to address. +Processing the results is the prerequisite for the examples in the following +sections. + +.. _res_general_approach_label: + +General approach +^^^^^^^^^^^^^^^^ + +As stated above, after processing you will get a dictionary with all result +data. +If you want to access your results directly via labels, you +can continue with :ref:`results_easy_access_label`. For a systematic analysis list comprehensions +are the easiest way of filtering and analysing your results. + +The keys of the results dictionary are tuples containing two nodes. Since flows +have a starting node and an ending node, you get a list of all flows by +filtering the results using the following expression: + +.. code-block:: python + + flows = [x for x in results.keys() if x[1] is not None] + +On the same way you can get a list of all nodes by applying: + +.. code-block:: python + + nodes = [x for x in results.keys() if x[1] is None] + +Probably you will just get storages as nodes, if you have some in your energy +system. Note, that just nodes containing decision variables are listed, e.g. a +Source or a Converter object does not have decision variables. These are in +the flows from or to the nodes. + +All items within the results dictionary are dictionaries and have two items +with 'scalars' and 'sequences' as keys: + +.. code-block:: python + + for flow in flows: + print(flow) + print(results[flow]['scalars']) + print(results[flow]['sequences']) + +There many options of filtering the flows and nodes as you prefer. +The following will give you all flows which are outputs of converter: + +.. code-block:: python + + flows_from_converter = [x for x in flows if isinstance( + x[0], solph.components.Converter)] + +You can filter your flows, if the label of in- or output contains a given +string, e.g.: + +.. code-block:: python + + flows_to_elec = [x for x in results.keys() if 'elec' in x[1].label] + +Getting all labels of the starting node of your investment flows: + +.. code-block:: python + + flows_invest = [x[0].label for x in flows if hasattr( + results[x]['scalars'], 'invest')] + + +.. _results_easy_access_label: + +Easy access +^^^^^^^^^^^ + +The solph package provides some functions which will help you to access your +results directly via labels, which is helpful especially for small energy +systems. +So, if you want to address objects by their label, you can convert the results +dictionary such that the keys are changed to strings given by the labels: + +.. code-block:: python + + views.convert_keys_to_strings(results) + print(results[('wind', 'bus_electricity')]['sequences'] + + +Another option is to access data belonging to a grouping by the name of the grouping +(`note also this section on groupings `_. +Given the label of an object, e.g. 'wind' you can access the grouping by its label +and use this to extract data from the results dictionary. + +.. code-block:: python + + node_wind = energysystem.groups['wind'] + print(results[(node_wind, bus_electricity)]) + + +However, in many situations it might be convenient to use the views module to +collect information on a specific node. You can request all data related to a +specific node by using either the node's variable name or its label: + +.. code-block:: python + + data_wind = solph.views.node(results, 'wind') + + +A function for collecting and printing meta results, i.e. information on the objective function, +the problem and the solver, is provided as well: + +.. code-block:: python + + meta_results = solph.processing.meta_results(om) + pp.pprint(meta_results) From 445ee9faaa9d735de293d4401d40da962685e836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 26 Nov 2024 09:48:41 +0100 Subject: [PATCH 35/45] Fix v0.6.0 whatsnew --- docs/whatsnew/v0-6-0.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/whatsnew/v0-6-0.rst b/docs/whatsnew/v0-6-0.rst index f5db57b34..aaf86994a 100644 --- a/docs/whatsnew/v0-6-0.rst +++ b/docs/whatsnew/v0-6-0.rst @@ -11,7 +11,7 @@ API changes initial storage level is defined, the costs just offset the objective value without changing anything else. * The parameters `GenericStorage.nominal_storage_capacity` and - `Flow.nominal_capacity` are now both called `nominal_capacity`. + `Flow.nominal_value` are now both called `nominal_capacity`. New features ############ @@ -36,3 +36,4 @@ Contributors ############ * Patrik Schönfeldt +* Johannes Kochems From c38a75dcf719f8fe7bb39fa23e6489283f67cd8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 26 Nov 2024 10:15:35 +0100 Subject: [PATCH 36/45] Use Python 3.10 for RTD Numpy2 is not compatible to legacy Python. --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index a5016d1e6..bd7141c26 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,7 +3,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.8" + python: "3.10" apt_packages: - coinor-cbc sphinx: From 37edbef6900ce6c799f05f17e44912cdce0e4828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 26 Nov 2024 10:15:47 +0100 Subject: [PATCH 37/45] Add v0.5.6 release date --- docs/whatsnew/v0-5-6.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/whatsnew/v0-5-6.rst b/docs/whatsnew/v0-5-6.rst index a6c92aebf..56c6c6a0b 100644 --- a/docs/whatsnew/v0-5-6.rst +++ b/docs/whatsnew/v0-5-6.rst @@ -1,5 +1,5 @@ -v0.5.6 ------- +v0.5.6 (November 26th, 2024) +---------------------------- Bug fixes ######### From 52462048174ea50d2d07269d8e2e5e63e92f8026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 26 Nov 2024 20:22:42 +0100 Subject: [PATCH 38/45] Set dependency to Python >= 3.10 Numpy 2 needs this, and we depend on that. So, it's a good idea to explicitly not support older versions instead of claiming something wrong. --- .github/workflows/lint.yml | 2 +- .github/workflows/packaging.yml | 2 +- .github/workflows/tox_checks.yml | 4 ++-- .github/workflows/tox_pytests.yml | 2 +- pyproject.toml | 5 +++-- tox.ini | 11 ++++++----- 6 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3e5f73159..50ff9e404 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,7 +20,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: "3.10" - name: Install Python dependencies run: pip install black flake8 diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml index a09540cfe..ce3c6808e 100644 --- a/.github/workflows/packaging.yml +++ b/.github/workflows/packaging.yml @@ -19,7 +19,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [3.9] + python-version: ["3.10"] steps: - uses: actions/checkout@v1 diff --git a/.github/workflows/tox_checks.yml b/.github/workflows/tox_checks.yml index 5553b0acf..9aa8c8b9a 100644 --- a/.github/workflows/tox_checks.yml +++ b/.github/workflows/tox_checks.yml @@ -33,10 +33,10 @@ jobs: - name: Git clone uses: actions/checkout@v2 - - name: Set up Python ${{ env.default_python || '3.9' }} + - name: Set up Python ${{ env.default_python || '3.10' }} uses: actions/setup-python@v5 with: - python-version: "${{ env.default_python || '3.9' }}" + python-version: "${{ env.default_python || '3.10' }}" - name: Pip cache uses: actions/cache@v2 diff --git a/.github/workflows/tox_pytests.yml b/.github/workflows/tox_pytests.yml index 240459535..6b5c46c3d 100644 --- a/.github/workflows/tox_pytests.yml +++ b/.github/workflows/tox_pytests.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.9, "3.10", "3.11"] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v1 diff --git a/pyproject.toml b/pyproject.toml index d50b4de2e..4c8cf1314 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,13 +47,14 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Utilities", ] -requires-python = ">=3.8" +requires-python = ">=3.10" dependencies = [ "blinker", "dill", diff --git a/tox.ini b/tox.ini index bbbaac9c0..21e29a9b4 100644 --- a/tox.ini +++ b/tox.ini @@ -11,9 +11,10 @@ envlist = [gh-actions] python = - 3.9: py39 3.10: py310 3.11: py311 + 3.12: py312 + 3.13: py313 [testenv] basepython = @@ -114,8 +115,8 @@ deps = pytest-cov -[testenv:py39] -basepython = {env:TOXPYTHON:python3.9} +[testenv:py312] +basepython = {env:TOXPYTHON:python3.12} setenv = {[testenv]setenv} usedevelop = true @@ -125,8 +126,8 @@ deps = {[testenv]deps} pytest-cov -[testenv:py38] -basepython = {env:TOXPYTHON:python3.8} +[testenv:py313] +basepython = {env:TOXPYTHON:python3.13} setenv = {[testenv]setenv} usedevelop = true From a83134b729c2822320dd9eb9042d4b20febffcac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 26 Nov 2024 20:43:05 +0100 Subject: [PATCH 39/45] Properly name GH workflow running pytest --- .github/workflows/tox_pytests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tox_pytests.yml b/.github/workflows/tox_pytests.yml index 1dea68e0e..76b50a9cf 100644 --- a/.github/workflows/tox_pytests.yml +++ b/.github/workflows/tox_pytests.yml @@ -13,7 +13,7 @@ on: - cron: "0 5 * * 6" # 5:00 UTC every Saturday jobs: - build: + pytest: runs-on: ubuntu-latest strategy: matrix: From 749e85aa7ecd6c73982e8479265cdc5164e67717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 26 Nov 2024 20:47:34 +0100 Subject: [PATCH 40/45] Use only Python 3.10, 3.11 in GH tests For some reason, reporting for Python 3.12 does not work. --- .github/workflows/tox_pytests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tox_pytests.yml b/.github/workflows/tox_pytests.yml index 76b50a9cf..79683e2cb 100644 --- a/.github/workflows/tox_pytests.yml +++ b/.github/workflows/tox_pytests.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11"] steps: - uses: actions/checkout@v1 From 0551cba842b3aa25ace9b83b5d81d0c1ba3abc35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Fri, 29 Nov 2024 14:25:42 +0100 Subject: [PATCH 41/45] Rename custom_attributes to custom_properties for Nodes Setting the oemof.network.Node.custom_properties using an argument called "custom_attributes" was rather confusing. Additionally, the class Bus already called the argument "custom_properties". --- docs/whatsnew/v0-6-0.rst | 5 +++++ src/oemof/solph/components/_converter.py | 9 ++++----- .../solph/components/_extraction_turbine_chp.py | 4 ++-- src/oemof/solph/components/_generic_chp.py | 14 +++++++------- src/oemof/solph/components/_offset_converter.py | 10 +++++----- src/oemof/solph/components/_sink.py | 8 ++++---- .../solph/components/experimental/_sink_dsm.py | 8 ++++---- tests/test_components/test_offset_converter.py | 2 +- 8 files changed, 32 insertions(+), 28 deletions(-) diff --git a/docs/whatsnew/v0-6-0.rst b/docs/whatsnew/v0-6-0.rst index c5700ae76..e2e31f858 100644 --- a/docs/whatsnew/v0-6-0.rst +++ b/docs/whatsnew/v0-6-0.rst @@ -10,6 +10,11 @@ API changes effectively the same) had double weight before. Also, if the initial storage level is defined, the costs just offset the objective value without changing anything else. +* Rename custom_attributes to custom_properties for all Nodes. + Setting the oemof.network.Node.custom_properties + using an argument called "custom_attributes" was rather confusing. + Additionally, the class Bus already called the argument + "custom_properties". New features ############ diff --git a/src/oemof/solph/components/_converter.py b/src/oemof/solph/components/_converter.py index ab660ea02..f3a0327d0 100644 --- a/src/oemof/solph/components/_converter.py +++ b/src/oemof/solph/components/_converter.py @@ -99,21 +99,20 @@ def __init__( inputs=None, outputs=None, conversion_factors=None, - custom_attributes=None, + custom_properties=None, ): if inputs is None: inputs = {} if outputs is None: outputs = {} - - if custom_attributes is None: - custom_attributes = {} + if custom_properties is None: + custom_properties = {} super().__init__( label=label, inputs=inputs, outputs=outputs, - custom_properties=custom_attributes, + custom_properties=custom_properties, ) if not inputs: warn_if_missing_attribute(self, "inputs") diff --git a/src/oemof/solph/components/_extraction_turbine_chp.py b/src/oemof/solph/components/_extraction_turbine_chp.py index dc8fb393b..9b502e462 100644 --- a/src/oemof/solph/components/_extraction_turbine_chp.py +++ b/src/oemof/solph/components/_extraction_turbine_chp.py @@ -77,14 +77,14 @@ def __init__( inputs=None, outputs=None, conversion_factors=None, - custom_attributes=None, + custom_properties=None, ): super().__init__( label=label, inputs=inputs, outputs=outputs, conversion_factors=conversion_factors, - custom_attributes=custom_attributes, + custom_properties=custom_properties, ) self.conversion_factor_full_condensation = { k: sequence(v) diff --git a/src/oemof/solph/components/_generic_chp.py b/src/oemof/solph/components/_generic_chp.py index 83246bb6c..195b9572a 100644 --- a/src/oemof/solph/components/_generic_chp.py +++ b/src/oemof/solph/components/_generic_chp.py @@ -104,16 +104,16 @@ class GenericCHP(Node): >>> ccet = solph.components.GenericCHP( ... label='combined_cycle_extraction_turbine', ... fuel_input={bgas: solph.flows.Flow( - ... custom_attributes={"H_L_FG_share_max": [0.183]})}, + ... custom_properties={"H_L_FG_share_max": [0.183]})}, ... electrical_output={bel: solph.flows.Flow( - ... custom_attributes={ + ... custom_properties={ ... "P_max_woDH": [155.946], ... "P_min_woDH": [68.787], ... "Eta_el_max_woDH": [0.525], ... "Eta_el_min_woDH": [0.444], ... })}, ... heat_output={bth: solph.flows.Flow( - ... custom_attributes={"Q_CW_min": [10.552]})}, + ... custom_properties={"Q_CW_min": [10.552]})}, ... beta=[0.122], back_pressure=False) >>> type(ccet) @@ -127,11 +127,11 @@ def __init__( beta, back_pressure, label=None, - custom_attributes=None, + custom_properties=None, ): - if custom_attributes is None: - custom_attributes = {} - super().__init__(label, custom_properties=custom_attributes) + if custom_properties is None: + custom_properties = {} + super().__init__(label, custom_properties=custom_properties) self.fuel_input = fuel_input self.electrical_output = electrical_output diff --git a/src/oemof/solph/components/_offset_converter.py b/src/oemof/solph/components/_offset_converter.py index b078c9ced..ea6005a9e 100644 --- a/src/oemof/solph/components/_offset_converter.py +++ b/src/oemof/solph/components/_offset_converter.py @@ -139,16 +139,16 @@ def __init__( conversion_factors=None, normed_offsets=None, coefficients=None, - custom_attributes=None, + custom_properties=None, ): - if custom_attributes is None: - custom_attributes = {} + if custom_properties is None: + custom_properties = {} super().__init__( inputs=inputs, outputs=outputs, label=label, - custom_properties=custom_attributes, + custom_properties=custom_properties, ) # this part is used for the transition phase from the old @@ -393,7 +393,7 @@ def __init__( inputs=inputs, outputs=outputs, coefficients=coefficients, - custom_attributes=custom_attributes, + custom_properties=custom_attributes, ) warn( "solph.components.OffsetTransformer has been renamed to" diff --git a/src/oemof/solph/components/_sink.py b/src/oemof/solph/components/_sink.py index 7644871fd..de9b78bad 100644 --- a/src/oemof/solph/components/_sink.py +++ b/src/oemof/solph/components/_sink.py @@ -41,14 +41,14 @@ class Sink(Node): """ - def __init__(self, label=None, *, inputs, custom_attributes=None): + def __init__(self, label=None, *, inputs, custom_properties=None): if inputs is None: inputs = {} - if custom_attributes is None: - custom_attributes = {} + if custom_properties is None: + custom_properties = {} super().__init__( - label=label, inputs=inputs, custom_properties=custom_attributes + label=label, inputs=inputs, custom_properties=custom_properties ) def constraint_group(self): diff --git a/src/oemof/solph/components/experimental/_sink_dsm.py b/src/oemof/solph/components/experimental/_sink_dsm.py index 4645a5899..1b1402537 100644 --- a/src/oemof/solph/components/experimental/_sink_dsm.py +++ b/src/oemof/solph/components/experimental/_sink_dsm.py @@ -271,12 +271,12 @@ def __init__( shift_eligibility=True, fixed_costs=0, investment=None, - custom_attributes=None, + custom_properties=None, ): - if custom_attributes is None: - custom_attributes = {} + if custom_properties is None: + custom_properties = {} super().__init__( - label=label, inputs=inputs, custom_attributes=custom_attributes + label=label, inputs=inputs, custom_properties=custom_properties ) self.capacity_up = sequence(capacity_up) diff --git a/tests/test_components/test_offset_converter.py b/tests/test_components/test_offset_converter.py index 997a1140c..c1b0a00f7 100644 --- a/tests/test_components/test_offset_converter.py +++ b/tests/test_components/test_offset_converter.py @@ -177,7 +177,7 @@ def test_custom_properties(): outputs={bus2: solph.Flow()}, conversion_factors={bus2: 2}, normed_offsets={bus2: -0.5}, - custom_attributes={"foo": "bar"}, + custom_properties={"foo": "bar"}, ) assert oc.custom_properties["foo"] == "bar" From 102dbb3e87eb895f6d9cfb5e2507e93563b3b04b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Fri, 29 Nov 2024 14:30:53 +0100 Subject: [PATCH 42/45] Fix Transformer wrapper --- src/oemof/solph/components/_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/oemof/solph/components/_converter.py b/src/oemof/solph/components/_converter.py index f3a0327d0..8395fac9a 100644 --- a/src/oemof/solph/components/_converter.py +++ b/src/oemof/solph/components/_converter.py @@ -152,7 +152,7 @@ def __init__( inputs=inputs, outputs=outputs, conversion_factors=conversion_factors, - custom_attributes=custom_attributes, + custom_properties=custom_attributes, ) warn( "solph.components.Transformer has been renamed to" From ce84bb34552565dc1cb0f7a271b4e38c811288a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Fri, 29 Nov 2024 14:40:41 +0100 Subject: [PATCH 43/45] Fix GenricCHP docstring --- src/oemof/solph/components/_generic_chp.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/oemof/solph/components/_generic_chp.py b/src/oemof/solph/components/_generic_chp.py index 195b9572a..516a047c1 100644 --- a/src/oemof/solph/components/_generic_chp.py +++ b/src/oemof/solph/components/_generic_chp.py @@ -104,16 +104,16 @@ class GenericCHP(Node): >>> ccet = solph.components.GenericCHP( ... label='combined_cycle_extraction_turbine', ... fuel_input={bgas: solph.flows.Flow( - ... custom_properties={"H_L_FG_share_max": [0.183]})}, + ... custom_attributes={"H_L_FG_share_max": [0.183]})}, ... electrical_output={bel: solph.flows.Flow( - ... custom_properties={ + ... custom_attributes={ ... "P_max_woDH": [155.946], ... "P_min_woDH": [68.787], ... "Eta_el_max_woDH": [0.525], ... "Eta_el_min_woDH": [0.444], ... })}, ... heat_output={bth: solph.flows.Flow( - ... custom_properties={"Q_CW_min": [10.552]})}, + ... custom_attributes={"Q_CW_min": [10.552]})}, ... beta=[0.122], back_pressure=False) >>> type(ccet) From 4e745f0c3a67cc95131db7d6570c47a72bc9bd25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 17 Dec 2024 14:12:13 +0100 Subject: [PATCH 44/45] Fix code removal comments --- src/oemof/solph/components/_generic_storage.py | 2 +- tests/test_components/test_storage.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/oemof/solph/components/_generic_storage.py b/src/oemof/solph/components/_generic_storage.py index cbbd25a17..81d41ba54 100644 --- a/src/oemof/solph/components/_generic_storage.py +++ b/src/oemof/solph/components/_generic_storage.py @@ -215,7 +215,7 @@ def __init__( warn(msg, FutureWarning) nominal_storage_capacity = investment # --- END --- - # --- BEGIN: The following code can be removed for versions >= v0.6 --- + # --- BEGIN: The following code can be removed for versions >= v0.7 --- if nominal_storage_capacity is not None: msg = ( "For backward compatibility," diff --git a/tests/test_components/test_storage.py b/tests/test_components/test_storage.py index ec6494456..4c45d4871 100644 --- a/tests/test_components/test_storage.py +++ b/tests/test_components/test_storage.py @@ -346,7 +346,7 @@ def test_invest_content_maximum(): ) -# --- BEGIN: The following code can be removed for versions >= v0.6 --- +# --- BEGIN: The following code can be removed for versions >= v0.7 --- def test_capacity_keyword_wrapper_warning(): with pytest.warns(FutureWarning, match="nominal_storage_capacity"): bus = solph.Bus() From 05885631b9dee67f2dec5bbf65aba8356482b6be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 17 Dec 2024 14:23:44 +0100 Subject: [PATCH 45/45] Adhere to Black --- .../solph/components/_generic_storage.py | 222 +++++++++--------- 1 file changed, 111 insertions(+), 111 deletions(-) diff --git a/src/oemof/solph/components/_generic_storage.py b/src/oemof/solph/components/_generic_storage.py index 46caed3a1..31493e6b9 100644 --- a/src/oemof/solph/components/_generic_storage.py +++ b/src/oemof/solph/components/_generic_storage.py @@ -44,117 +44,117 @@ class GenericStorage(Node): r""" - Component `GenericStorage` to model with basic characteristics of storages. - - The GenericStorage is designed for one input and one output. - - Parameters - ---------- - nominal_capacity : numeric, :math:`E_{nom}` or - :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 (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 (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 (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. - Values < 1 set the input flow lower than the output and > 1 will - set the input flow higher than the output flow. If None no relation - will be set: - :math:`\dot{E}_{in,invest} = \dot{E}_{out,invest} \cdot r_{in,out}` - initial_storage_level : numeric, :math:`c(-1)` - The relative storage content in the timestep before the first - time step of optimization (between 0 and 1). - - Note: When investment mode is used in a multi-period model, - `initial_storage_level` is not supported. - Storage output is forced to zero until the storage unit is invested in. - balanced : boolean - Couple storage level of first and last time step. - (Total inflow and total outflow are balanced.) - loss_rate : numeric (iterable or scalar) - The relative loss of the storage content per hour. - fixed_losses_relative : numeric (iterable or scalar), :math:`\gamma(t)` - Losses per hour that are independent of the storage content but - proportional to nominal storage capacity. - - Note: Fixed losses are not supported in investment mode. - fixed_losses_absolute : numeric (iterable or scalar), :math:`\delta(t)` - Losses per hour that are independent of storage content and independent - of nominal storage capacity. - - Note: Fixed losses are not supported in investment mode. - inflow_conversion_factor : numeric (iterable or scalar), :math:`\eta_i(t)` - The relative conversion factor, i.e. efficiency associated with the - inflow of the storage. - outflow_conversion_factor : numeric (iterable or scalar), :math:`\eta_o(t)` - see: inflow_conversion_factor - min_storage_level : numeric (iterable or scalar), :math:`c_{min}(t)` - The normed minimum storage content as fraction of the - nominal storage capacity or the capacity that has been invested into - (between 0 and 1). - To set different values in every time step use a sequence. - max_storage_level : numeric (iterable or scalar), :math:`c_{max}(t)` - see: min_storage_level - storage_costs : numeric (iterable or scalar), :math:`c_{storage}(t)` - Cost (per energy) for having energy in the storage, starting from - time point :math:`t_{1}`. - lifetime_inflow : int, :math:`n_{in}` - Determine the lifetime of an inflow; only applicable for multi-period - models which can invest in storage capacity and have an - invest_relation_input_capacity defined - lifetime_outflow : int, :math:`n_{in}` - Determine the lifetime of an outflow; only applicable for multi-period - models which can invest in storage capacity and have an - invest_relation_output_capacity defined - - Notes - ----- - The following sets, variables, constraints and objective parts are created - * :py:class:`~oemof.solph.components._generic_storage.GenericStorageBlock` - (if no Investment object present) - * :py:class:`~oemof.solph.components._generic_storage.GenericInvestmentStorageBlock` - (if Investment object present) - - Examples - -------- - Basic usage examples of the GenericStorage with a random selection of - attributes. See the Flow class for all Flow attributes. - - >>> from oemof import solph - - >>> my_bus = solph.buses.Bus('my_bus') - - >>> my_storage = solph.components.GenericStorage( - ... label='storage', - ... nominal_capacity=1000, - ... inputs={my_bus: solph.flows.Flow(nominal_capacity=200, variable_costs=10)}, - ... outputs={my_bus: solph.flows.Flow(nominal_capacity=200)}, - ... loss_rate=0.01, - ... initial_storage_level=0, - ... max_storage_level = 0.9, - ... inflow_conversion_factor=0.9, - ... outflow_conversion_factor=0.93) - - >>> my_investment_storage = solph.components.GenericStorage( - ... label='storage', - ... nominal_capacity=solph.Investment(ep_costs=50), - ... inputs={my_bus: solph.flows.Flow()}, - ... outputs={my_bus: solph.flows.Flow()}, - ... loss_rate=0.02, - ... initial_storage_level=None, - ... invest_relation_input_capacity=1/6, - ... invest_relation_output_capacity=1/6, - ... inflow_conversion_factor=1, - ... outflow_conversion_factor=0.8) + Component `GenericStorage` to model with basic characteristics of storages. + + The GenericStorage is designed for one input and one output. + + Parameters + ---------- + nominal_capacity : numeric, :math:`E_{nom}` or + :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 (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 (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 (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. + Values < 1 set the input flow lower than the output and > 1 will + set the input flow higher than the output flow. If None no relation + will be set: + :math:`\dot{E}_{in,invest} = \dot{E}_{out,invest} \cdot r_{in,out}` + initial_storage_level : numeric, :math:`c(-1)` + The relative storage content in the timestep before the first + time step of optimization (between 0 and 1). + + Note: When investment mode is used in a multi-period model, + `initial_storage_level` is not supported. + Storage output is forced to zero until the storage unit is invested in. + balanced : boolean + Couple storage level of first and last time step. + (Total inflow and total outflow are balanced.) + loss_rate : numeric (iterable or scalar) + The relative loss of the storage content per hour. + fixed_losses_relative : numeric (iterable or scalar), :math:`\gamma(t)` + Losses per hour that are independent of the storage content but + proportional to nominal storage capacity. + + Note: Fixed losses are not supported in investment mode. + fixed_losses_absolute : numeric (iterable or scalar), :math:`\delta(t)` + Losses per hour that are independent of storage content and independent + of nominal storage capacity. + + Note: Fixed losses are not supported in investment mode. + inflow_conversion_factor : numeric (iterable or scalar), :math:`\eta_i(t)` + The relative conversion factor, i.e. efficiency associated with the + inflow of the storage. + outflow_conversion_factor : numeric (iterable or scalar), :math:`\eta_o(t)` + see: inflow_conversion_factor + min_storage_level : numeric (iterable or scalar), :math:`c_{min}(t)` + The normed minimum storage content as fraction of the + nominal storage capacity or the capacity that has been invested into + (between 0 and 1). + To set different values in every time step use a sequence. + max_storage_level : numeric (iterable or scalar), :math:`c_{max}(t)` + see: min_storage_level + storage_costs : numeric (iterable or scalar), :math:`c_{storage}(t)` + Cost (per energy) for having energy in the storage, starting from + time point :math:`t_{1}`. + lifetime_inflow : int, :math:`n_{in}` + Determine the lifetime of an inflow; only applicable for multi-period + models which can invest in storage capacity and have an + invest_relation_input_capacity defined + lifetime_outflow : int, :math:`n_{in}` + Determine the lifetime of an outflow; only applicable for multi-period + models which can invest in storage capacity and have an + invest_relation_output_capacity defined + + Notes + ----- + The following sets, variables, constraints and objective parts are created + * :py:class:`~oemof.solph.components._generic_storage.GenericStorageBlock` + (if no Investment object present) + * :py:class:`~oemof.solph.components._generic_storage.GenericInvestmentStorageBlock` + (if Investment object present) + + Examples + -------- + Basic usage examples of the GenericStorage with a random selection of + attributes. See the Flow class for all Flow attributes. + + >>> from oemof import solph + + >>> my_bus = solph.buses.Bus('my_bus') + + >>> my_storage = solph.components.GenericStorage( + ... label='storage', + ... nominal_capacity=1000, + ... inputs={my_bus: solph.flows.Flow(nominal_capacity=200, variable_costs=10)}, + ... outputs={my_bus: solph.flows.Flow(nominal_capacity=200)}, + ... loss_rate=0.01, + ... initial_storage_level=0, + ... max_storage_level = 0.9, + ... inflow_conversion_factor=0.9, + ... outflow_conversion_factor=0.93) + + >>> my_investment_storage = solph.components.GenericStorage( + ... label='storage', + ... nominal_capacity=solph.Investment(ep_costs=50), + ... inputs={my_bus: solph.flows.Flow()}, + ... outputs={my_bus: solph.flows.Flow()}, + ... loss_rate=0.02, + ... initial_storage_level=None, + ... invest_relation_input_capacity=1/6, + ... invest_relation_output_capacity=1/6, + ... inflow_conversion_factor=1, + ... outflow_conversion_factor=0.8) """ # noqa: E501 def __init__(