From 5cd173344cb8c9e1e37498a0c5766a1715fa0bf8 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:34:11 +0100 Subject: [PATCH 01/13] Change default size to None. Raise if not set and Status is used --- flixopt/elements.py | 34 +++++++++++++++++++++------------- flixopt/io.py | 2 +- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index d2ebf7ac0..2e6a76bf8 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -341,7 +341,7 @@ class Flow(Element): Args: label: Unique flow identifier within its component. bus: Bus label this flow connects to. - size: Flow capacity. Scalar, InvestParameters, or None (uses CONFIG.Modeling.big). + size: Flow capacity. Scalar, InvestParameters, or None (unbounded). relative_minimum: Minimum flow rate as fraction of size (0-1). Default: 0. relative_maximum: Maximum flow rate as fraction of size. Default: 1. load_factor_min: Minimum average utilization (0-1). Default: 0. @@ -442,7 +442,8 @@ class Flow(Element): `relative_maximum` for upper bounds on optimization variables. Notes: - - Default size (CONFIG.Modeling.big) is used when size=None + - size=None means unbounded (no capacity constraint) + - size must be set when using status_parameters or fixed_relative_profile - list inputs for previous_flow_rate are converted to NumPy arrays - Flow direction is determined by component input/output designation @@ -473,7 +474,7 @@ def __init__( meta_data: dict | None = None, ): super().__init__(label, meta_data=meta_data) - self.size = CONFIG.Modeling.big if size is None else size + self.size = size self.relative_minimum = relative_minimum self.relative_maximum = relative_maximum self.fixed_relative_profile = fixed_relative_profile @@ -542,7 +543,7 @@ def transform_data(self, name_prefix: str = '') -> None: self.status_parameters.transform_data(prefix) if isinstance(self.size, InvestParameters): self.size.transform_data(prefix) - else: + elif self.size is not None: self.size = self._fit_coords(f'{prefix}|size', self.size, dims=['period', 'scenario']) def _plausibility_checks(self) -> None: @@ -550,13 +551,17 @@ def _plausibility_checks(self) -> None: if (self.relative_minimum > self.relative_maximum).any(): raise PlausibilityError(self.label_full + ': Take care, that relative_minimum <= relative_maximum!') - if not isinstance(self.size, InvestParameters) and ( - np.any(self.size == CONFIG.Modeling.big) and self.fixed_relative_profile is not None - ): # Default Size --> Most likely by accident - logger.warning( - f'Flow "{self.label_full}" has no size assigned, but a "fixed_relative_profile". ' - f'The default size is {CONFIG.Modeling.big}. As "flow_rate = size * fixed_relative_profile", ' - f'the resulting flow_rate will be very high. To fix this, assign a size to the Flow {self}.' + # Size is required when using StatusParameters (for big-M constraints) + if self.status_parameters is not None and self.size is None: + raise PlausibilityError( + f'Flow "{self.label_full}" has status_parameters but no size defined. ' + f'A size is required when using status_parameters to bound the flow rate.' + ) + + if self.size is None and self.fixed_relative_profile is not None: + raise PlausibilityError( + f'Flow "{self.label_full}" has a fixed_relative_profile but no size defined. ' + f'A size is required because flow_rate = size * fixed_relative_profile.' ) if self.fixed_relative_profile is not None and self.status_parameters is not None: @@ -822,15 +827,18 @@ def absolute_flow_rate_bounds(self) -> tuple[xr.DataArray, xr.DataArray]: if not self.with_status: if not self.with_investment: # Basic case without investment and without Status - lb = lb_relative * self.element.size + if self.element.size is not None: + lb = lb_relative * self.element.size elif self.with_investment and self.element.size.mandatory: # With mandatory Investment lb = lb_relative * self.element.size.minimum_or_fixed_size if self.with_investment: ub = ub_relative * self.element.size.maximum_or_fixed_size - else: + elif self.element.size is not None: ub = ub_relative * self.element.size + else: + ub = np.inf # Unbounded when size is None return lb, ub diff --git a/flixopt/io.py b/flixopt/io.py index 27bc242ff..7e36b268a 100644 --- a/flixopt/io.py +++ b/flixopt/io.py @@ -801,7 +801,7 @@ def build_repr_from_init( excluded_params: Set of parameter names to exclude (e.g., {'self', 'inputs', 'outputs'}) Default excludes 'self', 'label', and 'kwargs' label_as_positional: If True and 'label' param exists, show it as first positional arg - skip_default_size: If True, skip 'size' parameter when it equals CONFIG.Modeling.big + skip_default_size: Deprecated. Previously skipped size=CONFIG.Modeling.big, now size=None is default. Returns: Formatted repr string like: ClassName("label", param=value) From 7d0d7154641b1dbc8b9f5a0c4eebb88fad42bc1d Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 8 Dec 2025 17:03:54 +0100 Subject: [PATCH 02/13] Make maximum size mandatory (or fixed size). Update tests accordingly --- flixopt/interface.py | 11 +++++++++-- tests/deprecated/test_component.py | 6 ++++-- tests/deprecated/test_effect.py | 4 +++- tests/deprecated/test_functional.py | 18 ++++++++++++++---- tests/deprecated/test_storage.py | 1 + tests/test_component.py | 6 ++++-- tests/test_effect.py | 4 +++- tests/test_functional.py | 18 ++++++++++++++---- tests/test_storage.py | 1 + 9 files changed, 53 insertions(+), 16 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index 7995d5e78..fa23292ae 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -718,7 +718,7 @@ class InvestParameters(Interface): fixed_size: Creates binary decision at this exact size. None allows continuous sizing. minimum_size: Lower bound for continuous sizing. Default: CONFIG.Modeling.epsilon. Ignored if fixed_size is specified. - maximum_size: Upper bound for continuous sizing. Default: CONFIG.Modeling.big. + maximum_size: Upper bound for continuous sizing. Required if fixed_size is not set. Ignored if fixed_size is specified. mandatory: Controls whether investment is required. When True, forces investment to occur (useful for mandatory upgrades or replacement decisions). @@ -901,7 +901,7 @@ def __init__( ) self.piecewise_effects_of_investment = piecewise_effects_of_investment self.minimum_size = minimum_size if minimum_size is not None else CONFIG.Modeling.epsilon - self.maximum_size = maximum_size if maximum_size is not None else CONFIG.Modeling.big # default maximum + self.maximum_size = maximum_size self.linked_periods = linked_periods def _set_flow_system(self, flow_system) -> None: @@ -911,6 +911,13 @@ def _set_flow_system(self, flow_system) -> None: self.piecewise_effects_of_investment._set_flow_system(flow_system) def transform_data(self, name_prefix: str = '') -> None: + # Validate that either fixed_size or maximum_size is set + if self.fixed_size is None and self.maximum_size is None: + raise ValueError( + f'InvestParameters in "{name_prefix}" requires either fixed_size or maximum_size to be set. ' + f'An upper bound is needed to properly scale the optimization model.' + ) + self.effects_of_investment = self._fit_effect_coords( prefix=name_prefix, effect_values=self.effects_of_investment, diff --git a/tests/deprecated/test_component.py b/tests/deprecated/test_component.py index 8cde784c9..bf71a9c4c 100644 --- a/tests/deprecated/test_component.py +++ b/tests/deprecated/test_component.py @@ -498,7 +498,7 @@ def test_transmission_balanced(self, basic_flow_system, highs_solver): size=fx.InvestParameters(effects_of_investment_per_size=5, maximum_size=1000), ), out1=fx.Flow('Rohr1b', 'Fernwärme', size=1000), - in2=fx.Flow('Rohr2a', 'Fernwärme', size=fx.InvestParameters()), + in2=fx.Flow('Rohr2a', 'Fernwärme', size=fx.InvestParameters(maximum_size=1000)), out2=fx.Flow('Rohr2b', bus='Wärme lokal', size=1000), balanced=True, ) @@ -578,7 +578,9 @@ def test_transmission_unbalanced(self, basic_flow_system, highs_solver): in2=fx.Flow( 'Rohr2a', 'Fernwärme', - size=fx.InvestParameters(effects_of_investment_per_size=100, minimum_size=10, mandatory=True), + size=fx.InvestParameters( + effects_of_investment_per_size=100, minimum_size=10, maximum_size=1000, mandatory=True + ), ), out2=fx.Flow('Rohr2b', bus='Wärme lokal', size=1000), balanced=False, diff --git a/tests/deprecated/test_effect.py b/tests/deprecated/test_effect.py index 10ae59bcc..b3bb278f0 100644 --- a/tests/deprecated/test_effect.py +++ b/tests/deprecated/test_effect.py @@ -253,7 +253,9 @@ def test_shares(self, basic_flow_system_linopy_coords, coords_config): thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(effects_of_investment_per_size=10, minimum_size=20, mandatory=True), + size=fx.InvestParameters( + effects_of_investment_per_size=10, minimum_size=20, maximum_size=200, mandatory=True + ), ), fuel_flow=fx.Flow('Q_fu', bus='Gas'), ), diff --git a/tests/deprecated/test_functional.py b/tests/deprecated/test_functional.py index 34d16819c..a60e3a20a 100644 --- a/tests/deprecated/test_functional.py +++ b/tests/deprecated/test_functional.py @@ -187,7 +187,7 @@ def test_optimize_size(solver_fixture, time_steps_fixture): thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(effects_of_investment=10, effects_of_investment_per_size=1), + size=fx.InvestParameters(effects_of_investment=10, effects_of_investment_per_size=1, maximum_size=100), ), ) ) @@ -228,7 +228,9 @@ def test_size_bounds(solver_fixture, time_steps_fixture): thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(minimum_size=40, effects_of_investment=10, effects_of_investment_per_size=1), + size=fx.InvestParameters( + minimum_size=40, maximum_size=100, effects_of_investment=10, effects_of_investment_per_size=1 + ), ), ) ) @@ -270,7 +272,11 @@ def test_optional_invest(solver_fixture, time_steps_fixture): 'Q_th', bus='Fernwärme', size=fx.InvestParameters( - mandatory=False, minimum_size=40, effects_of_investment=10, effects_of_investment_per_size=1 + mandatory=False, + minimum_size=40, + maximum_size=100, + effects_of_investment=10, + effects_of_investment_per_size=1, ), ), ), @@ -282,7 +288,11 @@ def test_optional_invest(solver_fixture, time_steps_fixture): 'Q_th', bus='Fernwärme', size=fx.InvestParameters( - mandatory=False, minimum_size=50, effects_of_investment=10, effects_of_investment_per_size=1 + mandatory=False, + minimum_size=50, + maximum_size=100, + effects_of_investment=10, + effects_of_investment_per_size=1, ), ), ), diff --git a/tests/deprecated/test_storage.py b/tests/deprecated/test_storage.py index a5d2c7a19..15170a321 100644 --- a/tests/deprecated/test_storage.py +++ b/tests/deprecated/test_storage.py @@ -451,6 +451,7 @@ def test_investment_parameters( 'effects_of_investment': 100, 'effects_of_investment_per_size': 10, 'mandatory': mandatory, + 'maximum_size': 100, } if minimum_size is not None: invest_params['minimum_size'] = minimum_size diff --git a/tests/test_component.py b/tests/test_component.py index 741f6390e..2204d09cd 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -498,7 +498,7 @@ def test_transmission_balanced(self, basic_flow_system, highs_solver): size=fx.InvestParameters(effects_of_investment_per_size=5, maximum_size=1000), ), out1=fx.Flow('Rohr1b', 'Fernwärme', size=1000), - in2=fx.Flow('Rohr2a', 'Fernwärme', size=fx.InvestParameters()), + in2=fx.Flow('Rohr2a', 'Fernwärme', size=fx.InvestParameters(maximum_size=1000)), out2=fx.Flow('Rohr2b', bus='Wärme lokal', size=1000), balanced=True, ) @@ -574,7 +574,9 @@ def test_transmission_unbalanced(self, basic_flow_system, highs_solver): in2=fx.Flow( 'Rohr2a', 'Fernwärme', - size=fx.InvestParameters(effects_of_investment_per_size=100, minimum_size=10, mandatory=True), + size=fx.InvestParameters( + effects_of_investment_per_size=100, minimum_size=10, maximum_size=1000, mandatory=True + ), ), out2=fx.Flow('Rohr2b', bus='Wärme lokal', size=1000), balanced=False, diff --git a/tests/test_effect.py b/tests/test_effect.py index 7dcac9e1c..015e054eb 100644 --- a/tests/test_effect.py +++ b/tests/test_effect.py @@ -251,7 +251,9 @@ def test_shares(self, basic_flow_system_linopy_coords, coords_config, highs_solv thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(effects_of_investment_per_size=10, minimum_size=20, mandatory=True), + size=fx.InvestParameters( + effects_of_investment_per_size=10, minimum_size=20, maximum_size=200, mandatory=True + ), ), fuel_flow=fx.Flow('Q_fu', bus='Gas'), ), diff --git a/tests/test_functional.py b/tests/test_functional.py index 24322a6e0..de7407a87 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -185,7 +185,7 @@ def test_optimize_size(solver_fixture, time_steps_fixture): thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(effects_of_investment=10, effects_of_investment_per_size=1), + size=fx.InvestParameters(effects_of_investment=10, effects_of_investment_per_size=1, maximum_size=100), ), ) ) @@ -224,7 +224,9 @@ def test_size_bounds(solver_fixture, time_steps_fixture): thermal_flow=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(minimum_size=40, effects_of_investment=10, effects_of_investment_per_size=1), + size=fx.InvestParameters( + minimum_size=40, maximum_size=100, effects_of_investment=10, effects_of_investment_per_size=1 + ), ), ) ) @@ -264,7 +266,11 @@ def test_optional_invest(solver_fixture, time_steps_fixture): 'Q_th', bus='Fernwärme', size=fx.InvestParameters( - mandatory=False, minimum_size=40, effects_of_investment=10, effects_of_investment_per_size=1 + mandatory=False, + minimum_size=40, + maximum_size=100, + effects_of_investment=10, + effects_of_investment_per_size=1, ), ), ), @@ -276,7 +282,11 @@ def test_optional_invest(solver_fixture, time_steps_fixture): 'Q_th', bus='Fernwärme', size=fx.InvestParameters( - mandatory=False, minimum_size=50, effects_of_investment=10, effects_of_investment_per_size=1 + mandatory=False, + minimum_size=50, + maximum_size=100, + effects_of_investment=10, + effects_of_investment_per_size=1, ), ), ), diff --git a/tests/test_storage.py b/tests/test_storage.py index a5d2c7a19..15170a321 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -451,6 +451,7 @@ def test_investment_parameters( 'effects_of_investment': 100, 'effects_of_investment_per_size': 10, 'mandatory': mandatory, + 'maximum_size': 100, } if minimum_size is not None: invest_params['minimum_size'] = minimum_size From 9e9b0a1c20160ebcc6f433901d321cf84cda82ad Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 8 Dec 2025 17:40:09 +0100 Subject: [PATCH 03/13] Adding maximum sizes for FLows which get Status variables (also if only through component status --- tests/conftest.py | 8 ++++---- tests/deprecated/conftest.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9d76097ee..c15c483e2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -212,10 +212,10 @@ def piecewise(): """Piecewise converter from flow_system_piecewise_conversion""" return fx.LinearConverter( 'KWK', - inputs=[fx.Flow('Q_fu', bus='Gas')], + inputs=[fx.Flow('Q_fu', bus='Gas', size=200)], outputs=[ fx.Flow('P_el', bus='Strom', size=60, relative_maximum=55, previous_flow_rate=10), - fx.Flow('Q_th', bus='Fernwärme'), + fx.Flow('Q_th', bus='Fernwärme', size=100), ], piecewise_conversion=fx.PiecewiseConversion( { @@ -232,10 +232,10 @@ def segments(timesteps_length): """Segments converter with time-varying piecewise conversion""" return fx.LinearConverter( 'KWK', - inputs=[fx.Flow('Q_fu', bus='Gas')], + inputs=[fx.Flow('Q_fu', bus='Gas', size=200)], outputs=[ fx.Flow('P_el', bus='Strom', size=60, relative_maximum=55, previous_flow_rate=10), - fx.Flow('Q_th', bus='Fernwärme'), + fx.Flow('Q_th', bus='Fernwärme', size=100), ], piecewise_conversion=fx.PiecewiseConversion( { diff --git a/tests/deprecated/conftest.py b/tests/deprecated/conftest.py index e24dcaa72..1a448182b 100644 --- a/tests/deprecated/conftest.py +++ b/tests/deprecated/conftest.py @@ -215,10 +215,10 @@ def piecewise(): """Piecewise converter from flow_system_piecewise_conversion""" return fx.LinearConverter( 'KWK', - inputs=[fx.Flow('Q_fu', bus='Gas')], + inputs=[fx.Flow('Q_fu', bus='Gas', size=200)], outputs=[ fx.Flow('P_el', bus='Strom', size=60, relative_maximum=55, previous_flow_rate=10), - fx.Flow('Q_th', bus='Fernwärme'), + fx.Flow('Q_th', bus='Fernwärme', size=100), ], piecewise_conversion=fx.PiecewiseConversion( { @@ -235,10 +235,10 @@ def segments(timesteps_length): """Segments converter with time-varying piecewise conversion""" return fx.LinearConverter( 'KWK', - inputs=[fx.Flow('Q_fu', bus='Gas')], + inputs=[fx.Flow('Q_fu', bus='Gas', size=200)], outputs=[ fx.Flow('P_el', bus='Strom', size=60, relative_maximum=55, previous_flow_rate=10), - fx.Flow('Q_th', bus='Fernwärme'), + fx.Flow('Q_th', bus='Fernwärme', size=100), ], piecewise_conversion=fx.PiecewiseConversion( { From 0208ca533e5fe2136896f91e53e450947fb33446 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:27:48 +0100 Subject: [PATCH 04/13] Update more tests --- tests/deprecated/test_scenarios.py | 4 ++-- tests/test_scenarios.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/deprecated/test_scenarios.py b/tests/deprecated/test_scenarios.py index 366429831..b4a1cd161 100644 --- a/tests/deprecated/test_scenarios.py +++ b/tests/deprecated/test_scenarios.py @@ -212,10 +212,10 @@ def flow_system_piecewise_conversion_scenarios(flow_system_complex_scenarios) -> flow_system.add_elements( fx.LinearConverter( 'KWK', - inputs=[fx.Flow('Q_fu', bus='Gas')], + inputs=[fx.Flow('Q_fu', bus='Gas', size=200)], outputs=[ fx.Flow('P_el', bus='Strom', size=60, relative_maximum=55, previous_flow_rate=10), - fx.Flow('Q_th', bus='Fernwärme'), + fx.Flow('Q_th', bus='Fernwärme', size=100), ], piecewise_conversion=fx.PiecewiseConversion( { diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 366429831..b4a1cd161 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -212,10 +212,10 @@ def flow_system_piecewise_conversion_scenarios(flow_system_complex_scenarios) -> flow_system.add_elements( fx.LinearConverter( 'KWK', - inputs=[fx.Flow('Q_fu', bus='Gas')], + inputs=[fx.Flow('Q_fu', bus='Gas', size=200)], outputs=[ fx.Flow('P_el', bus='Strom', size=60, relative_maximum=55, previous_flow_rate=10), - fx.Flow('Q_th', bus='Fernwärme'), + fx.Flow('Q_th', bus='Fernwärme', size=100), ], piecewise_conversion=fx.PiecewiseConversion( { From c6990ddfdeddef4f3dd6ccff6813df8e63de8a8a Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Tue, 9 Dec 2025 08:10:36 +0100 Subject: [PATCH 05/13] BUGFIX: Minimum or foixed size in storages --- flixopt/components.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 0cfed39eb..f6e8a47df 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -518,13 +518,13 @@ def _plausibility_checks(self) -> None: f'Balancing charging and discharging Flows in {self.label_full} is only possible with Investments.' ) - if (self.charging.size.minimum_size > self.discharging.size.maximum_size).any() or ( - self.charging.size.maximum_size < self.discharging.size.minimum_size + if (self.charging.size.minimum_or_fixed_size > self.discharging.size.maximum_or_fixed_size).any() or ( + self.charging.size.maximum_or_fixed_size < self.discharging.size.minimum_or_fixed_size ).any(): raise PlausibilityError( f'Balancing charging and discharging Flows in {self.label_full} need compatible minimum and maximum sizes.' - f'Got: {self.charging.size.minimum_size=}, {self.charging.size.maximum_size=} and ' - f'{self.discharging.size.minimum_size=}, {self.discharging.size.maximum_size=}.' + f'Got: {self.charging.size.minimum_or_fixed_size=}, {self.charging.size.maximum_or_fixed_size=} and ' + f'{self.discharging.size.minimum_or_fixed_size=}, {self.discharging.size.maximum_or_fixed_size=}.' ) def __repr__(self) -> str: @@ -705,8 +705,8 @@ def _plausibility_checks(self): ).any(): raise ValueError( f'Balanced Transmission needs compatible minimum and maximum sizes.' - f'Got: {self.in1.size.minimum_size=}, {self.in1.size.maximum_size=}, {self.in1.size.fixed_size=} and ' - f'{self.in2.size.minimum_size=}, {self.in2.size.maximum_size=}, {self.in2.size.fixed_size=}.' + f'Got: {self.in1.size.minimum_or_fixed_size=}, {self.in1.size.maximum_or_fixed_size=} and ' + f'{self.in2.size.minimum_or_fixed_size=}, {self.in2.size.maximum_or_fixed_size=}.' ) def create_model(self, model) -> TransmissionModel: @@ -943,8 +943,8 @@ def _absolute_charge_state_bounds(self) -> tuple[xr.DataArray, xr.DataArray]: ) else: return ( - relative_lower_bound * self.element.capacity_in_flow_hours.minimum_size, - relative_upper_bound * self.element.capacity_in_flow_hours.maximum_size, + relative_lower_bound * self.element.capacity_in_flow_hours.minimum_or_fixed_size, + relative_upper_bound * self.element.capacity_in_flow_hours.maximum_or_fixed_size, ) @property From 816297c09cc510044c7c68312a842ab212544873 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 11 Dec 2025 08:20:01 +0100 Subject: [PATCH 06/13] Added validation: 5. Component with status_parameters now validates that all flows have explicit sizes --- flixopt/components.py | 74 +++++++++++++++++++++++++------------------ flixopt/elements.py | 11 +++++++ 2 files changed, 54 insertions(+), 31 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 53322ef5a..427c6d82c 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -265,7 +265,9 @@ class Storage(Component): charging: Incoming flow for loading the storage. discharging: Outgoing flow for unloading the storage. capacity_in_flow_hours: Storage capacity in flow-hours (kWh, m³, kg). - Scalar for fixed size or InvestParameters for optimization. + Scalar for fixed size, InvestParameters for optimization, or None (unbounded). + Default: None (unbounded capacity). When using InvestParameters, + maximum_size (or fixed_size) must be explicitly set for proper model scaling. relative_minimum_charge_state: Minimum charge state (0-1). Default: 0. relative_maximum_charge_state: Maximum charge state (0-1). Default: 1. initial_charge_state: Charge at start. Numeric or 'equals_final'. Default: 0. @@ -366,6 +368,11 @@ class Storage(Component): variables enforce mutual exclusivity, increasing solution time but preventing unrealistic simultaneous charging and discharging. + **Unbounded capacity**: When capacity_in_flow_hours is None (default), the storage has + unlimited capacity. Note that prevent_simultaneous_charge_and_discharge requires the + charging and discharging flows to have explicit sizes. Use prevent_simultaneous_charge_and_discharge=False + with unbounded storages, or set flow sizes explicitly. + **Units**: Flow rates and charge states are related by the concept of 'flow hours' (=flow_rate * time). With flow rates in kW, the charge state is therefore (usually) kWh. With flow rates in m3/h, the charge state is therefore in m3. @@ -378,7 +385,7 @@ def __init__( label: str, charging: Flow, discharging: Flow, - capacity_in_flow_hours: Numeric_PS | InvestParameters, + capacity_in_flow_hours: Numeric_PS | InvestParameters | None = None, relative_minimum_charge_state: Numeric_TPS = 0, relative_maximum_charge_state: Numeric_TPS = 1, initial_charge_state: Numeric_PS | Literal['equals_final'] = 0, @@ -485,30 +492,32 @@ def _plausibility_checks(self) -> None: raise PlausibilityError(f'initial_charge_state has undefined value: {self.initial_charge_state}') initial_equals_final = True - # Use new InvestParameters methods to get capacity bounds - if isinstance(self.capacity_in_flow_hours, InvestParameters): - minimum_capacity = self.capacity_in_flow_hours.minimum_or_fixed_size - maximum_capacity = self.capacity_in_flow_hours.maximum_or_fixed_size - else: - maximum_capacity = self.capacity_in_flow_hours - minimum_capacity = self.capacity_in_flow_hours - - # Initial capacity should not constraint investment decision - minimum_initial_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=0) - maximum_initial_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=0) - - # Only perform numeric comparisons if not using 'equals_final' - if not initial_equals_final: - if (self.initial_charge_state > maximum_initial_capacity).any(): - raise PlausibilityError( - f'{self.label_full}: {self.initial_charge_state=} ' - f'is constraining the investment decision. Chosse a value above {maximum_initial_capacity}' - ) - if (self.initial_charge_state < minimum_initial_capacity).any(): - raise PlausibilityError( - f'{self.label_full}: {self.initial_charge_state=} ' - f'is constraining the investment decision. Chosse a value below {minimum_initial_capacity}' - ) + # Skip capacity-related checks if capacity is None (unbounded) + if self.capacity_in_flow_hours is not None: + # Use new InvestParameters methods to get capacity bounds + if isinstance(self.capacity_in_flow_hours, InvestParameters): + minimum_capacity = self.capacity_in_flow_hours.minimum_or_fixed_size + maximum_capacity = self.capacity_in_flow_hours.maximum_or_fixed_size + else: + maximum_capacity = self.capacity_in_flow_hours + minimum_capacity = self.capacity_in_flow_hours + + # Initial capacity should not constraint investment decision + minimum_initial_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=0) + maximum_initial_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=0) + + # Only perform numeric comparisons if not using 'equals_final' + if not initial_equals_final: + if (self.initial_charge_state > maximum_initial_capacity).any(): + raise PlausibilityError( + f'{self.label_full}: {self.initial_charge_state=} ' + f'is constraining the investment decision. Chosse a value above {maximum_initial_capacity}' + ) + if (self.initial_charge_state < minimum_initial_capacity).any(): + raise PlausibilityError( + f'{self.label_full}: {self.initial_charge_state=} ' + f'is constraining the investment decision. Chosse a value below {minimum_initial_capacity}' + ) if self.balanced: if not isinstance(self.charging.size, InvestParameters) or not isinstance( @@ -938,15 +947,18 @@ def _initial_and_final_charge_state(self): @property def _absolute_charge_state_bounds(self) -> tuple[xr.DataArray, xr.DataArray]: relative_lower_bound, relative_upper_bound = self._relative_charge_state_bounds - if not isinstance(self.element.capacity_in_flow_hours, InvestParameters): + if self.element.capacity_in_flow_hours is None: + # Unbounded storage: lower bound is 0, upper bound is infinite + return (0, np.inf) + elif isinstance(self.element.capacity_in_flow_hours, InvestParameters): return ( - relative_lower_bound * self.element.capacity_in_flow_hours, - relative_upper_bound * self.element.capacity_in_flow_hours, + relative_lower_bound * self.element.capacity_in_flow_hours.minimum_or_fixed_size, + relative_upper_bound * self.element.capacity_in_flow_hours.maximum_or_fixed_size, ) else: return ( - relative_lower_bound * self.element.capacity_in_flow_hours.minimum_or_fixed_size, - relative_upper_bound * self.element.capacity_in_flow_hours.maximum_or_fixed_size, + relative_lower_bound * self.element.capacity_in_flow_hours, + relative_upper_bound * self.element.capacity_in_flow_hours, ) @property diff --git a/flixopt/elements.py b/flixopt/elements.py index 82d071e09..71fc2d744 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -138,6 +138,17 @@ def _check_unique_flow_labels(self): def _plausibility_checks(self) -> None: self._check_unique_flow_labels() + # Component with status_parameters requires all flows to have sizes set + # (status_parameters are propagated to flows in _do_modeling, which need sizes for big-M constraints) + if self.status_parameters is not None: + flows_without_size = [flow.label for flow in self.inputs + self.outputs if flow.size is None] + if flows_without_size: + raise PlausibilityError( + f'Component "{self.label_full}" has status_parameters, but the following flows have no size: ' + f'{flows_without_size}. All flows need explicit sizes when the component uses status_parameters ' + f'(required for big-M constraints).' + ) + def _connect_flows(self): # Inputs for flow in self.inputs: From 22a299f6ca72b53b0a43c35aa5bf2bd6e66ef954 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 11 Dec 2025 08:32:10 +0100 Subject: [PATCH 07/13] In flixopt/elements.py - Flow._plausibility_checks(): - relative_minimum > 0 requires size (lines 585-590) - relative_maximum < 1 requires size (lines 592-596) - load_factor_min requires size (lines 598-603) - load_factor_max requires size (lines 605-609) --- flixopt/elements.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/flixopt/elements.py b/flixopt/elements.py index 71fc2d744..c25a4dd13 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -582,6 +582,32 @@ def _plausibility_checks(self) -> None: f'A size is required because flow_rate = size * fixed_relative_profile.' ) + # Size is required when using non-default relative bounds (flow_rate = size * relative_bound) + if self.size is None and np.any(self.relative_minimum > 0): + raise PlausibilityError( + f'Flow "{self.label_full}" has relative_minimum > 0 but no size defined. ' + f'A size is required because the lower bound is size * relative_minimum.' + ) + + if self.size is None and np.any(self.relative_maximum < 1): + raise PlausibilityError( + f'Flow "{self.label_full}" has relative_maximum != 1 but no size defined. ' + f'A size is required because the upper bound is size * relative_maximum.' + ) + + # Size is required for load factor constraints (total_flow_hours / size) + if self.size is None and self.load_factor_min is not None: + raise PlausibilityError( + f'Flow "{self.label_full}" has load_factor_min but no size defined. ' + f'A size is required because the constraint is total_flow_hours >= size * load_factor_min * hours.' + ) + + if self.size is None and self.load_factor_max is not None: + raise PlausibilityError( + f'Flow "{self.label_full}" has load_factor_max but no size defined. ' + f'A size is required because the constraint is total_flow_hours <= size * load_factor_max * hours.' + ) + if self.fixed_relative_profile is not None and self.status_parameters is not None: logger.warning( f'Flow {self.label_full} has both a fixed_relative_profile and status_parameters.' From b46f04557650714e6e9ddb7b6b6e76dae76e79fa Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 11 Dec 2025 09:03:29 +0100 Subject: [PATCH 08/13] Added more validations --- flixopt/components.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/flixopt/components.py b/flixopt/components.py index 427c6d82c..977eb74e1 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -492,6 +492,29 @@ def _plausibility_checks(self) -> None: raise PlausibilityError(f'initial_charge_state has undefined value: {self.initial_charge_state}') initial_equals_final = True + # Capacity is required when using non-default relative bounds + if self.capacity_in_flow_hours is None: + if np.any(self.relative_minimum_charge_state > 0): + raise PlausibilityError( + f'Storage "{self.label_full}" has relative_minimum_charge_state > 0 but no capacity_in_flow_hours. ' + f'A capacity is required because the lower bound is capacity * relative_minimum_charge_state.' + ) + if np.any(self.relative_maximum_charge_state < 1): + raise PlausibilityError( + f'Storage "{self.label_full}" has relative_maximum_charge_state < 1 but no capacity_in_flow_hours. ' + f'A capacity is required because the upper bound is capacity * relative_maximum_charge_state.' + ) + if self.relative_minimum_final_charge_state is not None: + raise PlausibilityError( + f'Storage "{self.label_full}" has relative_minimum_final_charge_state but no capacity_in_flow_hours. ' + f'A capacity is required for relative final charge state constraints.' + ) + if self.relative_maximum_final_charge_state is not None: + raise PlausibilityError( + f'Storage "{self.label_full}" has relative_maximum_final_charge_state but no capacity_in_flow_hours. ' + f'A capacity is required for relative final charge state constraints.' + ) + # Skip capacity-related checks if capacity is None (unbounded) if self.capacity_in_flow_hours is not None: # Use new InvestParameters methods to get capacity bounds From b2b65a5ad8be6ac87f4b125ffc39b06f1565df4c Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 11 Dec 2025 09:10:13 +0100 Subject: [PATCH 09/13] Fix test --- tests/test_component.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_component.py b/tests/test_component.py index 2204d09cd..e6b82cbb0 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -464,7 +464,9 @@ def test_transmission_balanced(self, basic_flow_system, highs_solver): boiler = fx.linear_converters.Boiler( 'Boiler_Standard', thermal_efficiency=0.9, - thermal_flow=fx.Flow('Q_th', bus='Fernwärme', relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1])), + thermal_flow=fx.Flow( + 'Q_th', bus='Fernwärme', relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1]), size=1000 + ), fuel_flow=fx.Flow('Q_fu', bus='Gas'), ) @@ -537,7 +539,9 @@ def test_transmission_unbalanced(self, basic_flow_system, highs_solver): boiler = fx.linear_converters.Boiler( 'Boiler_Standard', thermal_efficiency=0.9, - thermal_flow=fx.Flow('Q_th', bus='Fernwärme', relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1])), + thermal_flow=fx.Flow( + 'Q_th', bus='Fernwärme', relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1]), size=1000 + ), fuel_flow=fx.Flow('Q_fu', bus='Gas'), ) From 8edd7c221070d2dd9afce33b794e77e0d17b038f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:50:11 +0100 Subject: [PATCH 10/13] Fix tests to specify size if needed --- tests/conftest.py | 4 ++-- tests/deprecated/conftest.py | 4 ++-- tests/deprecated/test_component.py | 16 ++++++++++------ tests/deprecated/test_flow.py | 1 + tests/test_component.py | 12 ++++++------ tests/test_flow.py | 1 + 6 files changed, 22 insertions(+), 16 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7243105af..e8ad3a7cf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -617,8 +617,8 @@ def flow_system_long(): thermal_efficiency=0.58, electrical_efficiency=0.22, status_parameters=fx.StatusParameters(effects_per_startup=24000), - electrical_flow=fx.Flow('P_el', bus='Strom'), - thermal_flow=fx.Flow('Q_th', bus='Fernwärme'), + electrical_flow=fx.Flow('P_el', bus='Strom', size=288 * 0.22), + thermal_flow=fx.Flow('Q_th', bus='Fernwärme', size=288 * 0.58), fuel_flow=fx.Flow('Q_fu', bus='Kohle', size=288, relative_minimum=87 / 288), ), fx.Storage( diff --git a/tests/deprecated/conftest.py b/tests/deprecated/conftest.py index 6eb75e768..5a06e046b 100644 --- a/tests/deprecated/conftest.py +++ b/tests/deprecated/conftest.py @@ -616,8 +616,8 @@ def flow_system_long(): thermal_efficiency=0.58, electrical_efficiency=0.22, status_parameters=fx.StatusParameters(effects_per_startup=24000), - electrical_flow=fx.Flow('P_el', bus='Strom'), - thermal_flow=fx.Flow('Q_th', bus='Fernwärme'), + electrical_flow=fx.Flow('P_el', bus='Strom', size=288 * 0.22), + thermal_flow=fx.Flow('Q_th', bus='Fernwärme', size=288 * 0.58), fuel_flow=fx.Flow('Q_fu', bus='Kohle', size=288, relative_minimum=87 / 288), ), fx.Storage( diff --git a/tests/deprecated/test_component.py b/tests/deprecated/test_component.py index bf71a9c4c..497a5c3aa 100644 --- a/tests/deprecated/test_component.py +++ b/tests/deprecated/test_component.py @@ -31,12 +31,12 @@ def test_component(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config inputs = [ - fx.Flow('In1', 'Fernwärme', relative_minimum=np.ones(10) * 0.1), - fx.Flow('In2', 'Fernwärme', relative_minimum=np.ones(10) * 0.1), + fx.Flow('In1', 'Fernwärme', size=100, relative_minimum=np.ones(10) * 0.1), + fx.Flow('In2', 'Fernwärme', size=100, relative_minimum=np.ones(10) * 0.1), ] outputs = [ - fx.Flow('Out1', 'Gas', relative_minimum=np.ones(10) * 0.01), - fx.Flow('Out2', 'Gas', relative_minimum=np.ones(10) * 0.01), + fx.Flow('Out1', 'Gas', size=100, relative_minimum=np.ones(10) * 0.01), + fx.Flow('Out2', 'Gas', size=100, relative_minimum=np.ones(10) * 0.01), ] comp = flixopt.elements.Component('TestComponent', inputs=inputs, outputs=outputs) flow_system.add_elements(comp) @@ -464,7 +464,9 @@ def test_transmission_balanced(self, basic_flow_system, highs_solver): boiler = fx.linear_converters.Boiler( 'Boiler_Standard', thermal_efficiency=0.9, - thermal_flow=fx.Flow('Q_th', bus='Fernwärme', relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1])), + thermal_flow=fx.Flow( + 'Q_th', bus='Fernwärme', size=1000, relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1]) + ), fuel_flow=fx.Flow('Q_fu', bus='Gas'), ) @@ -541,7 +543,9 @@ def test_transmission_unbalanced(self, basic_flow_system, highs_solver): boiler = fx.linear_converters.Boiler( 'Boiler_Standard', thermal_efficiency=0.9, - thermal_flow=fx.Flow('Q_th', bus='Fernwärme', relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1])), + thermal_flow=fx.Flow( + 'Q_th', bus='Fernwärme', size=1000, relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1]) + ), fuel_flow=fx.Flow('Q_fu', bus='Gas'), ) diff --git a/tests/deprecated/test_flow.py b/tests/deprecated/test_flow.py index 0a1a03341..594bc1fbb 100644 --- a/tests/deprecated/test_flow.py +++ b/tests/deprecated/test_flow.py @@ -593,6 +593,7 @@ def test_effects_per_active_hour(self, basic_flow_system_linopy_coords, coords_c flow = fx.Flow( 'Wärme', bus='Fernwärme', + size=100, status_parameters=fx.StatusParameters( effects_per_active_hour={'costs': costs_per_running_hour, 'CO2': co2_per_running_hour} ), diff --git a/tests/test_component.py b/tests/test_component.py index e6b82cbb0..66d09aaee 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -31,12 +31,12 @@ def test_component(self, basic_flow_system_linopy_coords, coords_config): """Test that flow model constraints are correctly generated.""" flow_system, coords_config = basic_flow_system_linopy_coords, coords_config inputs = [ - fx.Flow('In1', 'Fernwärme', relative_minimum=np.ones(10) * 0.1), - fx.Flow('In2', 'Fernwärme', relative_minimum=np.ones(10) * 0.1), + fx.Flow('In1', 'Fernwärme', size=100, relative_minimum=np.ones(10) * 0.1), + fx.Flow('In2', 'Fernwärme', size=100, relative_minimum=np.ones(10) * 0.1), ] outputs = [ - fx.Flow('Out1', 'Gas', relative_minimum=np.ones(10) * 0.01), - fx.Flow('Out2', 'Gas', relative_minimum=np.ones(10) * 0.01), + fx.Flow('Out1', 'Gas', size=100, relative_minimum=np.ones(10) * 0.01), + fx.Flow('Out2', 'Gas', size=100, relative_minimum=np.ones(10) * 0.01), ] comp = flixopt.elements.Component('TestComponent', inputs=inputs, outputs=outputs) flow_system.add_elements(comp) @@ -465,7 +465,7 @@ def test_transmission_balanced(self, basic_flow_system, highs_solver): 'Boiler_Standard', thermal_efficiency=0.9, thermal_flow=fx.Flow( - 'Q_th', bus='Fernwärme', relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1]), size=1000 + 'Q_th', bus='Fernwärme', size=1000, relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1]) ), fuel_flow=fx.Flow('Q_fu', bus='Gas'), ) @@ -540,7 +540,7 @@ def test_transmission_unbalanced(self, basic_flow_system, highs_solver): 'Boiler_Standard', thermal_efficiency=0.9, thermal_flow=fx.Flow( - 'Q_th', bus='Fernwärme', relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1]), size=1000 + 'Q_th', bus='Fernwärme', size=1000, relative_maximum=np.array([0, 0, 0, 1, 1, 1, 1, 1, 1, 1]) ), fuel_flow=fx.Flow('Q_fu', bus='Gas'), ) diff --git a/tests/test_flow.py b/tests/test_flow.py index 0a1a03341..594bc1fbb 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -593,6 +593,7 @@ def test_effects_per_active_hour(self, basic_flow_system_linopy_coords, coords_c flow = fx.Flow( 'Wärme', bus='Fernwärme', + size=100, status_parameters=fx.StatusParameters( effects_per_active_hour={'costs': costs_per_running_hour, 'CO2': co2_per_running_hour} ), From bd9c1d0f2912c21e457caa3de0417cac294f3ec5 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Dec 2025 11:28:18 +0100 Subject: [PATCH 11/13] Improve check verbosity --- flixopt/components.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 977eb74e1..267c144af 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -525,21 +525,23 @@ def _plausibility_checks(self) -> None: maximum_capacity = self.capacity_in_flow_hours minimum_capacity = self.capacity_in_flow_hours - # Initial capacity should not constraint investment decision - minimum_initial_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=0) - maximum_initial_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=0) + # Initial charge state should not constrain investment decision + # If initial > (min_cap * rel_max), investment is forced to increase capacity + # If initial < (max_cap * rel_min), investment is forced to decrease capacity + min_initial_at_max_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=0) + max_initial_at_min_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=0) # Only perform numeric comparisons if not using 'equals_final' if not initial_equals_final: - if (self.initial_charge_state > maximum_initial_capacity).any(): + if (self.initial_charge_state > max_initial_at_min_capacity).any(): raise PlausibilityError( f'{self.label_full}: {self.initial_charge_state=} ' - f'is constraining the investment decision. Chosse a value above {maximum_initial_capacity}' + f'is constraining the investment decision. Choose a value <= {max_initial_at_min_capacity}.' ) - if (self.initial_charge_state < minimum_initial_capacity).any(): + if (self.initial_charge_state < min_initial_at_max_capacity).any(): raise PlausibilityError( f'{self.label_full}: {self.initial_charge_state=} ' - f'is constraining the investment decision. Chosse a value below {minimum_initial_capacity}' + f'is constraining the investment decision. Choose a value >= {min_initial_at_max_capacity}.' ) if self.balanced: From 421125f69e64daeb6be9d05aaa9595db509eeb0f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:30:30 +0100 Subject: [PATCH 12/13] Fix type hint --- flixopt/elements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index c25a4dd13..2933eb95a 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -472,7 +472,7 @@ def __init__( self, label: str, bus: str, - size: Numeric_PS | InvestParameters = None, + size: Numeric_PS | InvestParameters | None = None, fixed_relative_profile: Numeric_TPS | None = None, relative_minimum: Numeric_TPS = 0, relative_maximum: Numeric_TPS = 1, From 16d2d24045e41c49bedaeb283c00a79839976ce5 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:30:46 +0100 Subject: [PATCH 13/13] Improve conftest.py --- tests/conftest.py | 10 +++++----- tests/deprecated/conftest.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e8ad3a7cf..ee2c0f4e2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -614,12 +614,12 @@ def flow_system_long(): ), fx.linear_converters.CHP( 'BHKW2', - thermal_efficiency=0.58, - electrical_efficiency=0.22, + thermal_efficiency=(eta_th := 0.58), + electrical_efficiency=(eta_el := 0.22), status_parameters=fx.StatusParameters(effects_per_startup=24000), - electrical_flow=fx.Flow('P_el', bus='Strom', size=288 * 0.22), - thermal_flow=fx.Flow('Q_th', bus='Fernwärme', size=288 * 0.58), - fuel_flow=fx.Flow('Q_fu', bus='Kohle', size=288, relative_minimum=87 / 288), + fuel_flow=fx.Flow('Q_fu', bus='Kohle', size=(fuel_size := 288), relative_minimum=87 / fuel_size), + electrical_flow=fx.Flow('P_el', bus='Strom', size=fuel_size * eta_el), + thermal_flow=fx.Flow('Q_th', bus='Fernwärme', size=fuel_size * eta_th), ), fx.Storage( 'Speicher', diff --git a/tests/deprecated/conftest.py b/tests/deprecated/conftest.py index 5a06e046b..65434f04c 100644 --- a/tests/deprecated/conftest.py +++ b/tests/deprecated/conftest.py @@ -613,12 +613,12 @@ def flow_system_long(): ), fx.linear_converters.CHP( 'BHKW2', - thermal_efficiency=0.58, - electrical_efficiency=0.22, + thermal_efficiency=(eta_th := 0.58), + electrical_efficiency=(eta_el := 0.22), status_parameters=fx.StatusParameters(effects_per_startup=24000), - electrical_flow=fx.Flow('P_el', bus='Strom', size=288 * 0.22), - thermal_flow=fx.Flow('Q_th', bus='Fernwärme', size=288 * 0.58), - fuel_flow=fx.Flow('Q_fu', bus='Kohle', size=288, relative_minimum=87 / 288), + fuel_flow=fx.Flow('Q_fu', bus='Kohle', size=(fuel_size := 288), relative_minimum=87 / fuel_size), + electrical_flow=fx.Flow('P_el', bus='Strom', size=fuel_size * eta_el), + thermal_flow=fx.Flow('Q_th', bus='Fernwärme', size=fuel_size * eta_th), ), fx.Storage( 'Speicher',