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 f911b6646..4de6fa041 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -489,11 +489,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 - return False if (isinstance(self.size, InvestParameters) and not self.size.optional) else True - class FlowModel(ElementModel): element: Flow # Type hint @@ -678,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 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: diff --git a/flixopt/features.py b/flixopt/features.py index f266a1946..7be0c5f30 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -56,12 +56,13 @@ def _create_variables_and_constraints(self): size_min, size_max = (self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size) self.add_variables( short_name='size', - lower=0 if self.parameters.optional else size_min, + lower=0 if not self.parameters.mandatory else size_min, upper=size_max, coords=self._model.get_coords(['period', 'scenario']), ) - if self.parameters.optional: + # Optional (not mandatory) + if not self.parameters.mandatory: self.add_variables( binary=True, coords=self._model.get_coords(['period', 'scenario']), @@ -100,7 +101,7 @@ def _add_effects(self): target='periodic', ) - if self.parameters.effects_of_retirement and self.parameters.optional: + if self.parameters.effects_of_retirement and not self.parameters.mandatory: self._model.effects.add_share_to_effects( name=self.label_of_element, expressions={ diff --git a/flixopt/interface.py b/flixopt/interface.py index b985729e6..fb2f6e915 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -696,8 +696,9 @@ class InvestParameters(Interface): Ignored if fixed_size is specified. maximum_size: Upper bound for continuous sizing. Default: CONFIG.modeling.BIG. Ignored if fixed_size is specified. - optional: If True, can choose not to invest. If False, investment is mandatory. - Default: True. + 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. effects_of_investment: Fixed costs if investment is made, regardless of size. Dict: {'effect_name': value} (e.g., {'cost': 10000}). effects_of_investment_per_size: Variable costs proportional to size (per-unit costs). @@ -716,6 +717,8 @@ class InvestParameters(Interface): Will be removed in version 4.0. piecewise_effects: **Deprecated**. Use `piecewise_effects_of_investment` instead. Will be removed in version 4.0. + optional: DEPRECATED. Use `mandatory` instead. Opposite of `mandatory`. + Will be removed in version 4.0. Cost Annualization Requirements: All cost values must be properly weighted to match the optimization model's time horizon. @@ -733,7 +736,7 @@ class InvestParameters(Interface): ```python solar_investment = InvestParameters( fixed_size=100, # 100 kW system (binary decision) - optional=True, + mandatory=False, # Investment is optional effects_of_investment={ 'cost': 25000, # Installation and permitting costs 'CO2': -50000, # Avoided emissions over lifetime @@ -751,7 +754,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 effects_of_investment={ 'cost': 5000, # Grid connection and control system 'installation_time': 2, # Days for fixed components @@ -783,7 +786,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 effects_of_investment={ 'cost': 15000, # Installation costs 'disruption': 3, # Days of downtime @@ -867,7 +870,7 @@ def __init__( fixed_size: PeriodicDataUser | None = None, minimum_size: PeriodicDataUser | None = None, maximum_size: PeriodicDataUser | None = None, - optional: bool = True, # Investition ist weglassbar + mandatory: bool = False, effects_of_investment: PeriodicEffectsUser | None = None, effects_of_investment_per_size: PeriodicEffectsUser | None = None, effects_of_retirement: PeriodicEffectsUser | None = None, @@ -887,6 +890,16 @@ def __init__( piecewise_effects_of_investment = self._handle_deprecated_kwarg( kwargs, 'piecewise_effects', 'piecewise_effects_of_investment', piecewise_effects_of_investment ) + # 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" manually!', + DeprecationWarning, + stacklevel=2, + ) + 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) @@ -898,7 +911,7 @@ def __init__( effects_of_retirement if effects_of_retirement is not None else {} ) self.fixed_size = fixed_size - self.optional = optional + self.mandatory = mandatory self.effects_of_investment_per_size: PeriodicEffectsUser = ( effects_of_investment_per_size if effects_of_investment_per_size is not None else {} ) @@ -941,6 +954,20 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None f'{name_prefix}|fixed_size', self.fixed_size, dims=['period', 'scenario'] ) + @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'.""" + warnings.warn("Property 'optional' is deprecated. Use 'mandatory' instead.", DeprecationWarning, stacklevel=2) + self.mandatory = not value + @property def fix_effects(self) -> PeriodicEffectsUser: """Deprecated property. Use effects_of_investment instead.""" 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/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_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 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' )