From ea0b2a019efa70c805f745fe7f078a099ab71ee1 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 26 Sep 2025 00:41:28 +0200 Subject: [PATCH 1/8] Change .optional to .mandatory --- flixopt/elements.py | 4 +-- flixopt/features.py | 10 ++++---- flixopt/interface.py | 60 ++++++++++++++++++++++++++++++++++++++------ 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 22256b636..1f4048a2c 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -489,7 +489,7 @@ def size_is_fixed(self) -> bool: @property def invest_is_optional(self) -> bool: # Wenn kein InvestParameters existiert: # Investment ist nicht optional -> Keine Variable --> False - return False if (isinstance(self.size, InvestParameters) and not self.size.optional) else True + return False if (isinstance(self.size, InvestParameters) and self.size.mandatory) else True class FlowModel(ElementModel): @@ -650,7 +650,7 @@ def flow_rate_lower_bound(self) -> NumericData: if self.element.on_off_parameters is not None: return 0 if isinstance(self.element.size, InvestParameters): - if self.element.size.optional: + if not self.element.size.mandatory: return 0 return self.flow_rate_lower_bound_relative * self.element.size.minimum_size return self.flow_rate_lower_bound_relative * self.element.size diff --git a/flixopt/features.py b/flixopt/features.py index 5528917e0..985250a51 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -46,7 +46,7 @@ def __init__( self.parameters = parameters def do_modeling(self): - if self.parameters.fixed_size and not self.parameters.optional: + if self.parameters.fixed_size and self.parameters.mandatory: self.size = self.add( self._model.add_variables( lower=self.parameters.fixed_size, upper=self.parameters.fixed_size, name=f'{self.label_full}|size' @@ -56,15 +56,15 @@ def do_modeling(self): else: self.size = self.add( self._model.add_variables( - lower=0 if self.parameters.optional else self.parameters.minimum_size, + lower=0 if not self.parameters.mandatory else self.parameters.minimum_size, upper=self.parameters.maximum_size, name=f'{self.label_full}|size', ), 'size', ) - # Optional - if self.parameters.optional: + # Optional (not mandatory) + if not self.parameters.mandatory: self.is_invested = self.add( self._model.add_variables(binary=True, name=f'{self.label_full}|is_invested'), 'is_invested' ) @@ -89,7 +89,7 @@ def _create_shares(self): target='invest', ) - if self.parameters.divest_effects != {} and self.parameters.optional: + if self.parameters.divest_effects != {} and not self.parameters.mandatory: # share: divest_effects - isInvested * divest_effects self._model.effects.add_share_to_effects( name=self.label_of_element, diff --git a/flixopt/interface.py b/flixopt/interface.py index e72e28b90..a713e759d 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -655,9 +655,12 @@ class InvestParameters(Interface): maximum_size: Upper bound for continuous sizing decisions. Defaults to a large value (CONFIG.modeling.BIG) representing unlimited capacity. Ignored when fixed_size is specified. - optional: Controls whether investment is required. When True (default), - optimization can choose not to invest. When False, forces investment + mandatory: Controls whether investment is required. When True, forces investment to occur (useful for mandatory upgrades or replacement decisions). + When False (default), optimization can choose not to invest. + optional: DEPRECATED. Use `mandatory` instead. Controls whether investment is required. + When True (default), optimization can choose not to invest. When False, forces + investment to occur. This parameter is maintained for backwards compatibility. fix_effects: Fixed costs incurred once if investment is made, regardless of size. Dictionary mapping effect names to values (e.g., {'cost': 10000, 'CO2_construction': 500}). @@ -687,7 +690,7 @@ class InvestParameters(Interface): ```python solar_investment = InvestParameters( fixed_size=100, # 100 kW system (binary decision) - optional=True, + mandatory=False, # Investment is optional fix_effects={ 'cost': 25000, # Installation and permitting costs 'CO2': -50000, # Avoided emissions over lifetime @@ -705,7 +708,7 @@ class InvestParameters(Interface): battery_investment = InvestParameters( minimum_size=10, # Minimum viable system size (kWh) maximum_size=1000, # Maximum installable capacity - optional=True, + mandatory=False, # Investment is optional fix_effects={ 'cost': 5000, # Grid connection and control system 'installation_time': 2, # Days for fixed components @@ -737,7 +740,7 @@ class InvestParameters(Interface): boiler_replacement = InvestParameters( minimum_size=50, maximum_size=200, - optional=True, # Can choose not to replace + mandatory=False, # Can choose not to replace fix_effects={ 'cost': 15000, # Installation costs 'disruption': 3, # Days of downtime @@ -821,7 +824,8 @@ def __init__( fixed_size: int | float | None = None, minimum_size: int | float | None = None, maximum_size: int | float | None = None, - optional: bool = True, # Investition ist weglassbar + mandatory: bool | None = None, + optional: bool | None = None, # DEPRECATED: use mandatory instead fix_effects: EffectValuesUserScalar | None = None, specific_effects: EffectValuesUserScalar | None = None, # costs per Flow-Unit/Storage-Size/... piecewise_effects: PiecewiseEffects | None = None, @@ -830,17 +834,59 @@ def __init__( self.fix_effects: EffectValuesUserScalar = fix_effects or {} self.divest_effects: EffectValuesUserScalar = divest_effects or {} self.fixed_size = fixed_size - self.optional = optional self.specific_effects: EffectValuesUserScalar = specific_effects or {} self.piecewise_effects = piecewise_effects 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 + # Handle backwards compatibility between mandatory and optional + if mandatory is not None and optional is not None: + raise ValueError("Cannot specify both 'mandatory' and 'optional' parameters. Use 'mandatory' instead.") + elif mandatory is not None: + self._mandatory = mandatory + elif optional is not None: + import warnings + + warnings.warn( + "Parameter 'optional' is deprecated. Use 'mandatory=not optional' instead.", + DeprecationWarning, + stacklevel=2, + ) + self._mandatory = not optional + else: + self._mandatory = False # Default: not mandatory (i.e., optional) + def transform_data(self, flow_system: FlowSystem): self.fix_effects = flow_system.effects.create_effect_values_dict(self.fix_effects) self.divest_effects = flow_system.effects.create_effect_values_dict(self.divest_effects) self.specific_effects = flow_system.effects.create_effect_values_dict(self.specific_effects) + @property + def mandatory(self) -> bool: + """Controls whether investment is required.""" + return self._mandatory + + @mandatory.setter + def mandatory(self, value: bool): + """Set whether investment is required.""" + self._mandatory = value + + @property + def optional(self) -> bool: + """DEPRECATED: Use 'mandatory' property instead. Returns the opposite of 'mandatory'.""" + import warnings + + warnings.warn("Property 'optional' is deprecated. Use 'mandatory' instead.", DeprecationWarning, stacklevel=2) + return not self._mandatory + + @optional.setter + def optional(self, value: bool): + """DEPRECATED: Use 'mandatory' property instead. Sets the opposite of the given value to 'mandatory'.""" + import warnings + + warnings.warn("Property 'optional' is deprecated. Use 'mandatory' instead.", DeprecationWarning, stacklevel=2) + self._mandatory = not value + @property def minimum_size(self): return self.fixed_size or self._minimum_size From 823b221f7397c2a220a29e6fd00f0544bc80e4b4 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 26 Sep 2025 09:52:47 +0200 Subject: [PATCH 2/8] Change .optional to .mandatory --- flixopt/elements.py | 9 +++++++++ flixopt/interface.py | 46 +++++++++++--------------------------------- 2 files changed, 20 insertions(+), 35 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 1f4048a2c..80b9384dc 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -489,8 +489,17 @@ def size_is_fixed(self) -> bool: @property def invest_is_optional(self) -> bool: # Wenn kein InvestParameters existiert: # Investment ist nicht optional -> Keine Variable --> False + warnings.warn( + "The 'invest_is_optional' property is deprecated. Use 'invest_is_mandatory' instead.", + DeprecationWarning, + stacklevel=2, + ) return False if (isinstance(self.size, InvestParameters) and self.size.mandatory) else True + @property + def invest_is_mandatory(self) -> bool: + return False if (isinstance(self.size, InvestParameters) and not self.size.mandatory) else True + class FlowModel(ElementModel): def __init__(self, model: SystemModel, element: Flow): diff --git a/flixopt/interface.py b/flixopt/interface.py index a713e759d..aa113acc3 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -6,6 +6,7 @@ from __future__ import annotations import logging +import warnings from typing import TYPE_CHECKING from .config import CONFIG @@ -658,9 +659,7 @@ class InvestParameters(Interface): mandatory: Controls whether investment is required. When True, forces investment to occur (useful for mandatory upgrades or replacement decisions). When False (default), optimization can choose not to invest. - optional: DEPRECATED. Use `mandatory` instead. Controls whether investment is required. - When True (default), optimization can choose not to invest. When False, forces - investment to occur. This parameter is maintained for backwards compatibility. + optional: DEPRECATED. Use `mandatory` instead. Opposite of `mandatory`. fix_effects: Fixed costs incurred once if investment is made, regardless of size. Dictionary mapping effect names to values (e.g., {'cost': 10000, 'CO2_construction': 500}). @@ -824,12 +823,13 @@ def __init__( fixed_size: int | float | None = None, minimum_size: int | float | None = None, maximum_size: int | float | None = None, - mandatory: bool | None = None, - optional: bool | None = None, # DEPRECATED: use mandatory instead + mandatory: bool = False, fix_effects: EffectValuesUserScalar | None = None, specific_effects: EffectValuesUserScalar | None = None, # costs per Flow-Unit/Storage-Size/... piecewise_effects: PiecewiseEffects | None = None, divest_effects: EffectValuesUserScalar | None = None, + # Backwards compatibility - deprecated parameter + optional: bool | None = None, ): self.fix_effects: EffectValuesUserScalar = fix_effects or {} self.divest_effects: EffectValuesUserScalar = divest_effects or {} @@ -838,54 +838,30 @@ def __init__( self.piecewise_effects = piecewise_effects 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.mandatory = mandatory - # Handle backwards compatibility between mandatory and optional - if mandatory is not None and optional is not None: - raise ValueError("Cannot specify both 'mandatory' and 'optional' parameters. Use 'mandatory' instead.") - elif mandatory is not None: - self._mandatory = mandatory - elif optional is not None: - import warnings - - warnings.warn( - "Parameter 'optional' is deprecated. Use 'mandatory=not optional' instead.", - DeprecationWarning, - stacklevel=2, - ) - self._mandatory = not optional - else: - self._mandatory = False # Default: not mandatory (i.e., optional) + # Handle backwards compatibility for optional parameter + if optional is not None: + self.optional = optional def transform_data(self, flow_system: FlowSystem): self.fix_effects = flow_system.effects.create_effect_values_dict(self.fix_effects) self.divest_effects = flow_system.effects.create_effect_values_dict(self.divest_effects) self.specific_effects = flow_system.effects.create_effect_values_dict(self.specific_effects) - @property - def mandatory(self) -> bool: - """Controls whether investment is required.""" - return self._mandatory - - @mandatory.setter - def mandatory(self, value: bool): - """Set whether investment is required.""" - self._mandatory = value - @property def optional(self) -> bool: """DEPRECATED: Use 'mandatory' property instead. Returns the opposite of 'mandatory'.""" import warnings warnings.warn("Property 'optional' is deprecated. Use 'mandatory' instead.", DeprecationWarning, stacklevel=2) - return not self._mandatory + return not self.mandatory @optional.setter def optional(self, value: bool): """DEPRECATED: Use 'mandatory' property instead. Sets the opposite of the given value to 'mandatory'.""" - import warnings - warnings.warn("Property 'optional' is deprecated. Use 'mandatory' instead.", DeprecationWarning, stacklevel=2) - self._mandatory = not value + self.mandatory = not value @property def minimum_size(self): From 4eefd294db6796445e8435d5d2eeec316411feab Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 26 Sep 2025 10:06:39 +0200 Subject: [PATCH 3/8] Remove not needed properties --- flixopt/elements.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index 80b9384dc..f9dcf0cee 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -486,20 +486,6 @@ def size_is_fixed(self) -> bool: # Wenn kein InvestParameters existiert --> True; Wenn Investparameter, den Wert davon nehmen return False if (isinstance(self.size, InvestParameters) and self.size.fixed_size is None) else True - @property - def invest_is_optional(self) -> bool: - # Wenn kein InvestParameters existiert: # Investment ist nicht optional -> Keine Variable --> False - warnings.warn( - "The 'invest_is_optional' property is deprecated. Use 'invest_is_mandatory' instead.", - DeprecationWarning, - stacklevel=2, - ) - return False if (isinstance(self.size, InvestParameters) and self.size.mandatory) else True - - @property - def invest_is_mandatory(self) -> bool: - return False if (isinstance(self.size, InvestParameters) and not self.size.mandatory) else True - class FlowModel(ElementModel): def __init__(self, model: SystemModel, element: Flow): From 938c346a6556b86744c51f77dae118f5b2d90ea6 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 10 Oct 2025 11:27:06 +0200 Subject: [PATCH 4/8] Improve deprectation warnings --- flixopt/interface.py | 6 +- flixopt/structure.py | 17 +++++- tests/test_invest_parameters_deprecation.py | 61 +++++++++++++++++++++ 3 files changed, 82 insertions(+), 2 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index 1b3b30fa9..5ebbadb0d 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -890,7 +890,11 @@ def __init__( piecewise_effects_of_investment = self._handle_deprecated_kwarg( kwargs, 'piecewise_effects', 'piecewise_effects_of_investment', piecewise_effects_of_investment ) - mandatory = self._handle_deprecated_kwarg(kwargs, 'optional', 'mandatory', mandatory, transform=lambda x: not x) + # For mandatory parameter with non-None default, disable conflict checking + # (cannot distinguish between explicit mandatory=False and default value) + mandatory = self._handle_deprecated_kwarg( + kwargs, 'optional', 'mandatory', mandatory, transform=lambda x: not x, check_conflict=False + ) # Validate any remaining unexpected kwargs self._validate_kwargs(kwargs) diff --git a/flixopt/structure.py b/flixopt/structure.py index 0a4d158ed..3ddfb8bbe 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -387,13 +387,26 @@ def _handle_deprecated_kwarg( new_name: Name of the replacement parameter current_value: Current value of the new parameter (if already set) transform: Optional callable to transform the old value before returning (e.g., lambda x: [x] to wrap in list) - check_conflict: Whether to check if both old and new parameters are specified (default: True) + check_conflict: Whether to check if both old and new parameters are specified (default: True). + Note: For parameters with non-None default values (e.g., bool parameters with default=False), + set check_conflict=False since we cannot distinguish between an explicit value and the default. Returns: The value to use (either from old parameter or current_value) Raises: ValueError: If both old and new parameters are specified and check_conflict is True + + Example: + # For parameters where None is the default (conflict checking works): + value = self._handle_deprecated_kwarg(kwargs, 'old_param', 'new_param', current_value) + + # For parameters with non-None defaults (disable conflict checking): + mandatory = self._handle_deprecated_kwarg( + kwargs, 'optional', 'mandatory', mandatory, + transform=lambda x: not x, + check_conflict=False # Cannot detect if mandatory was explicitly passed + ) """ import warnings @@ -404,6 +417,7 @@ def _handle_deprecated_kwarg( DeprecationWarning, stacklevel=3, # Stack: this method -> __init__ -> caller ) + # Check for conflicts: only raise error if both were explicitly provided if check_conflict and current_value is not None: raise ValueError(f'Either {old_name} or {new_name} can be specified, but not both.') @@ -411,6 +425,7 @@ def _handle_deprecated_kwarg( if transform is not None: return transform(old_value) return old_value + return current_value def _validate_kwargs(self, kwargs: dict, class_name: str = None) -> None: diff --git a/tests/test_invest_parameters_deprecation.py b/tests/test_invest_parameters_deprecation.py index 5d7cf49b9..438d7f4b8 100644 --- a/tests/test_invest_parameters_deprecation.py +++ b/tests/test_invest_parameters_deprecation.py @@ -281,3 +281,64 @@ def test_unexpected_keyword_arguments(self): TypeError, match="InvestParameters.__init__\\(\\) got unexpected keyword argument\\(s\\): 'typo'" ): InvestParameters(effects_of_investment={'cost': 100}, typo='value') + + def test_optional_parameter_deprecation(self): + """Test that optional parameter triggers deprecation warning and maps to mandatory.""" + # Test optional=True (should map to mandatory=False) + with pytest.warns(DeprecationWarning, match='optional.*deprecated.*mandatory'): + params = InvestParameters(optional=True) + assert params.mandatory is False + + # Test optional=False (should map to mandatory=True) + with pytest.warns(DeprecationWarning, match='optional.*deprecated.*mandatory'): + params = InvestParameters(optional=False) + assert params.mandatory is True + + def test_mandatory_parameter_no_warning(self): + """Test that mandatory parameter doesn't trigger warnings.""" + with warnings.catch_warnings(): + warnings.simplefilter('error', DeprecationWarning) + # Test mandatory=True + params = InvestParameters(mandatory=True) + assert params.mandatory is True + + # Test mandatory=False (explicit) + params = InvestParameters(mandatory=False) + assert params.mandatory is False + + def test_mandatory_default_value(self): + """Test that default value of mandatory is False when neither optional nor mandatory is specified.""" + params = InvestParameters() + assert params.mandatory is False + + def test_both_optional_and_mandatory_no_error(self): + """Test that specifying both optional and mandatory doesn't raise error. + + Note: Conflict checking is disabled for mandatory/optional because mandatory has + a non-None default value (False), making it impossible to distinguish between + an explicit mandatory=False and the default value. The deprecated optional + parameter will take precedence when both are specified. + """ + # When both are specified, optional takes precedence (with deprecation warning) + with pytest.warns(DeprecationWarning, match='optional.*deprecated.*mandatory'): + params = InvestParameters(optional=True, mandatory=False) + # optional=True should result in mandatory=False + assert params.mandatory is False + + with pytest.warns(DeprecationWarning, match='optional.*deprecated.*mandatory'): + params = InvestParameters(optional=False, mandatory=True) + # optional=False should result in mandatory=True (optional takes precedence) + assert params.mandatory is True + + def test_optional_property_deprecation(self): + """Test that accessing optional property triggers deprecation warning.""" + params = InvestParameters(mandatory=True) + + # Reading the property triggers warning + with pytest.warns(DeprecationWarning, match="Property 'optional' is deprecated"): + assert params.optional is False + + # Setting the property triggers warning + with pytest.warns(DeprecationWarning, match="Property 'optional' is deprecated"): + params.optional = True + assert params.mandatory is False From 24f9312519f8aa6f0a0fb33702e80e9a0ef191a4 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 10 Oct 2025 11:35:24 +0200 Subject: [PATCH 5/8] Improve deprectation of "optional" --- flixopt/interface.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index 5ebbadb0d..48ed42264 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -891,7 +891,12 @@ def __init__( kwargs, 'piecewise_effects', 'piecewise_effects_of_investment', piecewise_effects_of_investment ) # For mandatory parameter with non-None default, disable conflict checking - # (cannot distinguish between explicit mandatory=False and default value) + if 'optional' in kwargs: + warnings.warn( + 'Deprecated parameter "optional" used. Check conflicts with new parameter "mandatory" wmanually!', + DeprecationWarning, + stacklevel=2, + ) mandatory = self._handle_deprecated_kwarg( kwargs, 'optional', 'mandatory', mandatory, transform=lambda x: not x, check_conflict=False ) From 2f8df2410bf51ea1193a72512451b47375a6b93f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 10 Oct 2025 11:48:55 +0200 Subject: [PATCH 6/8] Remove all usages of old "optional" parameter in code --- .../features/InvestParameters.md | 14 +++++----- examples/01_Simple/simple_example.py | 2 +- examples/02_Complex/complex_example.py | 4 +-- examples/04_Scenarios/scenario_example.py | 2 +- flixopt/elements.py | 4 +-- tests/conftest.py | 6 ++--- tests/test_component.py | 2 +- tests/test_effect.py | 2 +- tests/test_flow.py | 20 +++++++------- tests/test_functional.py | 4 +-- tests/test_scenarios.py | 4 +-- tests/test_storage.py | 26 +++++++++---------- 12 files changed, 45 insertions(+), 45 deletions(-) diff --git a/docs/user-guide/mathematical-notation/features/InvestParameters.md b/docs/user-guide/mathematical-notation/features/InvestParameters.md index 21d071dc8..14fe02c79 100644 --- a/docs/user-guide/mathematical-notation/features/InvestParameters.md +++ b/docs/user-guide/mathematical-notation/features/InvestParameters.md @@ -49,16 +49,16 @@ This uses the **bounds with state** pattern described in [Bounds and States](../ ### Optional vs. Mandatory Investment -The `optional` parameter controls whether investment is required: +The `mandatory` parameter controls whether investment is required: -**Optional Investment** (`optional=True`): +**Optional Investment** (`mandatory=False`, default): $$\label{eq:invest_optional} s_\text{invest} \in \{0, 1\} $$ The optimization can freely choose to invest or not. -**Mandatory Investment** (`optional=False`): +**Mandatory Investment** (`mandatory=True`): $$\label{eq:invest_mandatory} s_\text{invest} = 1 $$ @@ -228,7 +228,7 @@ $$ **Key Parameters:** - `fixed_size`: For binary investments (mutually exclusive with continuous sizing) - `minimum_size`, `maximum_size`: For continuous sizing -- `optional`: Whether investment can be skipped +- `mandatory`: Whether investment is required (default: `False`) - `effects_of_investment`: Fixed effects incurred when investing (replaces deprecated `fix_effects`) - `effects_of_investment_per_size`: Per-unit effects proportional to size (replaces deprecated `specific_effects`) - `piecewise_effects_of_investment`: Non-linear effect modeling (replaces deprecated `piecewise_effects`) @@ -250,7 +250,7 @@ See the [`InvestParameters`][flixopt.interface.InvestParameters] API documentati ```python solar_investment = InvestParameters( fixed_size=100, # 100 kW system - optional=True, + mandatory=False, # Optional investment (default) effects_of_investment={'cost': 25000}, # Installation costs effects_of_investment_per_size={'cost': 1200}, # €1200/kW ) @@ -261,7 +261,7 @@ solar_investment = InvestParameters( battery_investment = InvestParameters( minimum_size=10, # kWh maximum_size=1000, - optional=True, + mandatory=False, # Optional investment (default) effects_of_investment={'cost': 5000}, # Grid connection effects_of_investment_per_size={'cost': 600}, # €600/kWh ) @@ -272,7 +272,7 @@ battery_investment = InvestParameters( boiler_replacement = InvestParameters( minimum_size=50, # kW maximum_size=200, - optional=True, + mandatory=False, # Optional investment (default) effects_of_investment={'cost': 15000}, effects_of_investment_per_size={'cost': 400}, effects_of_retirement={'cost': 8000}, # Demolition if not replaced diff --git a/examples/01_Simple/simple_example.py b/examples/01_Simple/simple_example.py index 924d165d5..a6db595ef 100644 --- a/examples/01_Simple/simple_example.py +++ b/examples/01_Simple/simple_example.py @@ -64,7 +64,7 @@ label='Storage', charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1000), discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1000), - capacity_in_flow_hours=fx.InvestParameters(effects_of_investment=20, fixed_size=30, optional=False), + capacity_in_flow_hours=fx.InvestParameters(effects_of_investment=20, fixed_size=30, mandatory=True), initial_charge_state=0, # Initial storage state: empty relative_maximum_charge_state=1 / 100 * np.array([80, 70, 80, 80, 80, 80, 80, 80, 80]), relative_maximum_final_charge_state=0.8, diff --git a/examples/02_Complex/complex_example.py b/examples/02_Complex/complex_example.py index 5c46f650e..81d5035f6 100644 --- a/examples/02_Complex/complex_example.py +++ b/examples/02_Complex/complex_example.py @@ -59,7 +59,7 @@ size=fx.InvestParameters( effects_of_investment=1000, # Fixed investment costs fixed_size=50, # Fixed size - optional=False, # Forced investment + mandatory=True, # Forced investment effects_of_investment_per_size={Costs.label: 10, PE.label: 2}, # Specific costs ), load_factor_max=1.0, # Maximum load factor (50 kW) @@ -131,7 +131,7 @@ discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1e4), capacity_in_flow_hours=fx.InvestParameters( piecewise_effects_of_investment=segmented_investment_effects, # Investment effects - optional=False, # Forced investment + mandatory=True, # Forced investment minimum_size=0, maximum_size=1000, # Optimizing between 0 and 1000 kWh ), diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py index c48ee56a8..eeb97005c 100644 --- a/examples/04_Scenarios/scenario_example.py +++ b/examples/04_Scenarios/scenario_example.py @@ -77,7 +77,7 @@ label='Storage', charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1000), discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1000), - capacity_in_flow_hours=fx.InvestParameters(effects_of_investment=20, fixed_size=30, optional=False), + capacity_in_flow_hours=fx.InvestParameters(effects_of_investment=20, fixed_size=30, mandatory=True), initial_charge_state=0, # Initial storage state: empty relative_maximum_charge_state=np.array([80, 70, 80, 80, 80, 80, 80, 80, 80]) * 0.01, relative_maximum_final_charge_state=0.8, diff --git a/flixopt/elements.py b/flixopt/elements.py index d39cf3e2f..d96cf3a81 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -673,8 +673,8 @@ def absolute_flow_rate_bounds(self) -> tuple[TemporalData, TemporalData]: if not self.with_investment: # Basic case without investment and without OnOff lb = lb_relative * self.element.size - elif isinstance(self.element.size, InvestParameters) and not self.element.size.optional: - # With non-optional Investment + elif isinstance(self.element.size, InvestParameters) and self.element.size.mandatory: + # With mandatory Investment lb = lb_relative * self.element.size.minimum_or_fixed_size if self.with_investment: diff --git a/tests/conftest.py b/tests/conftest.py index 9dd8e6d0d..f352dc7cc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -161,7 +161,7 @@ def complex(): size=fx.InvestParameters( effects_of_investment=1000, fixed_size=50, - optional=False, + mandatory=True, effects_of_investment_per_size={'costs': 10, 'PE': 2}, ), on_off_parameters=fx.OnOffParameters( @@ -267,7 +267,7 @@ def simple(timesteps_length=9): 'Speicher', charging=fx.Flow('Q_th_load', bus='Fernwärme', size=1e4), discharging=fx.Flow('Q_th_unload', bus='Fernwärme', size=1e4), - capacity_in_flow_hours=fx.InvestParameters(effects_of_investment=20, fixed_size=30, optional=False), + capacity_in_flow_hours=fx.InvestParameters(effects_of_investment=20, fixed_size=30, mandatory=True), initial_charge_state=0, relative_maximum_charge_state=1 / 100 * np.array(charge_state_values), relative_maximum_final_charge_state=0.8, @@ -289,7 +289,7 @@ def complex(): 'PE': fx.Piecewise([fx.Piece(5, 25), fx.Piece(25, 100)]), }, ), - optional=False, + mandatory=True, effects_of_investment_per_size={'costs': 0.01, 'CO2': 0.01}, minimum_size=0, maximum_size=1000, diff --git a/tests/test_component.py b/tests/test_component.py index 98e2ad7db..be1eecf3b 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -562,7 +562,7 @@ 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, optional=False), + size=fx.InvestParameters(effects_of_investment_per_size=100, minimum_size=10, 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 b855ec8d0..cd3edc537 100644 --- a/tests/test_effect.py +++ b/tests/test_effect.py @@ -251,7 +251,7 @@ def test_shares(self, basic_flow_system_linopy_coords, coords_config): Q_th=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(effects_of_investment_per_size=10, minimum_size=20, optional=False), + size=fx.InvestParameters(effects_of_investment_per_size=10, minimum_size=20, mandatory=True), ), Q_fu=fx.Flow('Q_fu', bus='Gas'), ), diff --git a/tests/test_flow.py b/tests/test_flow.py index 5f0343bc1..5c8420137 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -149,7 +149,7 @@ def test_flow_invest(self, basic_flow_system_linopy_coords, coords_config): flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(minimum_size=20, maximum_size=100, optional=False), + size=fx.InvestParameters(minimum_size=20, maximum_size=100, mandatory=True), relative_minimum=np.linspace(0.1, 0.5, timesteps.size), relative_maximum=np.linspace(0.5, 1, timesteps.size), ) @@ -212,7 +212,7 @@ def test_flow_invest_optional(self, basic_flow_system_linopy_coords, coords_conf flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(minimum_size=20, maximum_size=100, optional=True), + size=fx.InvestParameters(minimum_size=20, maximum_size=100, mandatory=False), relative_minimum=np.linspace(0.1, 0.5, timesteps.size), relative_maximum=np.linspace(0.5, 1, timesteps.size), ) @@ -287,7 +287,7 @@ def test_flow_invest_optional_wo_min_size(self, basic_flow_system_linopy_coords, flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(maximum_size=100, optional=True), + size=fx.InvestParameters(maximum_size=100, mandatory=False), relative_minimum=np.linspace(0.1, 0.5, timesteps.size), relative_maximum=np.linspace(0.5, 1, timesteps.size), ) @@ -362,7 +362,7 @@ def test_flow_invest_wo_min_size_non_optional(self, basic_flow_system_linopy_coo flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(maximum_size=100, optional=False), + size=fx.InvestParameters(maximum_size=100, mandatory=True), relative_minimum=np.linspace(0.1, 0.5, timesteps.size), relative_maximum=np.linspace(0.5, 1, timesteps.size), ) @@ -420,7 +420,7 @@ def test_flow_invest_fixed_size(self, basic_flow_system_linopy_coords, coords_co flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(fixed_size=75, optional=False), + size=fx.InvestParameters(fixed_size=75, mandatory=True), relative_minimum=0.2, relative_maximum=0.9, ) @@ -458,7 +458,7 @@ def test_flow_invest_with_effects(self, basic_flow_system_linopy_coords, coords_ size=fx.InvestParameters( minimum_size=20, maximum_size=100, - optional=True, + mandatory=False, effects_of_investment={'costs': 1000, 'CO2': 5}, # Fixed investment effects effects_of_investment_per_size={'costs': 500, 'CO2': 0.1}, # Specific investment effects ), @@ -496,7 +496,7 @@ def test_flow_invest_divest_effects(self, basic_flow_system_linopy_coords, coord size=fx.InvestParameters( minimum_size=20, maximum_size=100, - optional=True, + mandatory=False, effects_of_retirement={'costs': 500}, # Cost incurred when NOT investing ), ) @@ -1076,7 +1076,7 @@ def test_flow_on_invest_optional(self, basic_flow_system_linopy_coords, coords_c flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(minimum_size=20, maximum_size=200, optional=True), + size=fx.InvestParameters(minimum_size=20, maximum_size=200, mandatory=False), relative_minimum=0.2, relative_maximum=0.8, on_off_parameters=fx.OnOffParameters(), @@ -1177,7 +1177,7 @@ def test_flow_on_invest_non_optional(self, basic_flow_system_linopy_coords, coor flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(minimum_size=20, maximum_size=200, optional=False), + size=fx.InvestParameters(minimum_size=20, maximum_size=200, mandatory=True), relative_minimum=0.2, relative_maximum=0.8, on_off_parameters=fx.OnOffParameters(), @@ -1304,7 +1304,7 @@ def test_fixed_profile_with_investment(self, basic_flow_system_linopy_coords, co flow = fx.Flow( 'Wärme', bus='Fernwärme', - size=fx.InvestParameters(minimum_size=50, maximum_size=200, optional=True), + size=fx.InvestParameters(minimum_size=50, maximum_size=200, mandatory=False), fixed_relative_profile=profile, ) diff --git a/tests/test_functional.py b/tests/test_functional.py index 94172eccf..cd872e84c 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -270,7 +270,7 @@ def test_optional_invest(solver_fixture, time_steps_fixture): 'Q_th', bus='Fernwärme', size=fx.InvestParameters( - optional=True, minimum_size=40, effects_of_investment=10, effects_of_investment_per_size=1 + mandatory=False, minimum_size=40, effects_of_investment=10, effects_of_investment_per_size=1 ), ), ), @@ -282,7 +282,7 @@ def test_optional_invest(solver_fixture, time_steps_fixture): 'Q_th', bus='Fernwärme', size=fx.InvestParameters( - optional=True, minimum_size=50, effects_of_investment=10, effects_of_investment_per_size=1 + mandatory=False, minimum_size=50, effects_of_investment=10, effects_of_investment_per_size=1 ), ), ), diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index b8f8bc45e..1ff9e9cea 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -152,7 +152,7 @@ def flow_system_complex_scenarios() -> fx.FlowSystem: size=fx.InvestParameters( effects_of_investment=1000, fixed_size=50, - optional=False, + mandatory=True, effects_of_investment_per_size={'costs': 10, 'PE': 2}, ), on_off_parameters=fx.OnOffParameters( @@ -178,7 +178,7 @@ def flow_system_complex_scenarios() -> fx.FlowSystem: 'PE': fx.Piecewise([fx.Piece(5, 25), fx.Piece(25, 100)]), }, ), - optional=False, + mandatory=True, effects_of_investment_per_size={'costs': 0.01, 'CO2': 0.01}, minimum_size=0, maximum_size=1000, diff --git a/tests/test_storage.py b/tests/test_storage.py index 252c7d228..02db3f09a 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -265,7 +265,7 @@ def test_storage_with_investment(self, basic_flow_system_linopy_coords, coords_c effects_of_investment_per_size=10, minimum_size=20, maximum_size=100, - optional=True, + mandatory=False, ), initial_charge_state=0, eta_charge=0.9, @@ -425,19 +425,19 @@ def test_simultaneous_charge_discharge(self, basic_flow_system_linopy_coords, co ) @pytest.mark.parametrize( - 'optional,minimum_size,expected_vars,expected_constraints', + 'mandatory,minimum_size,expected_vars,expected_constraints', [ - (True, None, {'InvestStorage|is_invested'}, {'InvestStorage|size|lb'}), - (True, 20, {'InvestStorage|is_invested'}, {'InvestStorage|size|lb'}), - (False, None, set(), set()), - (False, 20, set(), set()), + (False, None, {'InvestStorage|is_invested'}, {'InvestStorage|size|lb'}), + (False, 20, {'InvestStorage|is_invested'}, {'InvestStorage|size|lb'}), + (True, None, set(), set()), + (True, 20, set(), set()), ], ) def test_investment_parameters( self, basic_flow_system_linopy_coords, coords_config, - optional, + mandatory, minimum_size, expected_vars, expected_constraints, @@ -449,7 +449,7 @@ def test_investment_parameters( invest_params = { 'effects_of_investment': 100, 'effects_of_investment_per_size': 10, - 'optional': optional, + 'mandatory': mandatory, } if minimum_size is not None: invest_params['minimum_size'] = minimum_size @@ -471,20 +471,20 @@ def test_investment_parameters( # Check that expected variables exist for var_name in expected_vars: - if optional: + if not mandatory: # Optional investment (mandatory=False) assert var_name in model.variables, f'Expected variable {var_name} not found' # Check that expected constraints exist for constraint_name in expected_constraints: - if optional: + if not mandatory: # Optional investment (mandatory=False) assert constraint_name in model.constraints, f'Expected constraint {constraint_name} not found' - # If optional is False, is_invested should be fixed to 1 - if not optional: + # If mandatory is True, is_invested should be fixed to 1 + if mandatory: # Check that the is_invested variable exists and is fixed to 1 if 'InvestStorage|is_invested' in model.variables: var = model.variables['InvestStorage|is_invested'] # Check if the lower and upper bounds are both 1 assert var.upper == 1 and var.lower == 1, ( - 'is_invested variable should be fixed to 1 when optional=False' + 'is_invested variable should be fixed to 1 when mandatory=True' ) From 3d631eac70f2d7dc652dc27e628a31df0f020465 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 10 Oct 2025 11:55:37 +0200 Subject: [PATCH 7/8] Typo --- flixopt/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index 48ed42264..fb2f6e915 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -893,7 +893,7 @@ def __init__( # For mandatory parameter with non-None default, disable conflict checking if 'optional' in kwargs: warnings.warn( - 'Deprecated parameter "optional" used. Check conflicts with new parameter "mandatory" wmanually!', + 'Deprecated parameter "optional" used. Check conflicts with new parameter "mandatory" manually!', DeprecationWarning, stacklevel=2, ) From 6c0c2fd3293fa2f6214bf153cb66c4ba2ebf0727 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 10 Oct 2025 12:55:50 +0200 Subject: [PATCH 8/8] Imrpove readability --- flixopt/elements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flixopt/elements.py b/flixopt/elements.py index d96cf3a81..4de6fa041 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -673,7 +673,7 @@ def absolute_flow_rate_bounds(self) -> tuple[TemporalData, TemporalData]: if not self.with_investment: # Basic case without investment and without OnOff lb = lb_relative * self.element.size - elif isinstance(self.element.size, InvestParameters) and self.element.size.mandatory: + elif self.with_investment and self.element.size.mandatory: # With mandatory Investment lb = lb_relative * self.element.size.minimum_or_fixed_size