From 6757f4a86d7a160560eae40ac34a837de91a8d64 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 9 Oct 2025 21:50:26 +0200 Subject: [PATCH 01/13] FIrst Try --- examples/01_Simple/simple_example.py | 2 +- flixopt/interface.py | 145 ++++++++++++++++++++------- 2 files changed, 112 insertions(+), 35 deletions(-) diff --git a/examples/01_Simple/simple_example.py b/examples/01_Simple/simple_example.py index c39d85c7a..924d165d5 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(fix_effects=20, fixed_size=30, optional=False), + capacity_in_flow_hours=fx.InvestParameters(effects_of_investment=20, fixed_size=30, optional=False), 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/flixopt/interface.py b/flixopt/interface.py index c3247ee50..44418eb34 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, Literal, Optional from .config import CONFIG @@ -697,15 +698,25 @@ class InvestParameters(Interface): Ignored if fixed_size is specified. optional: If True, can choose not to invest. If False, investment is mandatory. Default: True. - fix_effects: Fixed costs if investment is made, regardless of size. + effects_of_investment: Fixed costs if investment is made, regardless of size. Dict: {'effect_name': value} (e.g., {'cost': 10000}). - specific_effects: Variable costs proportional to size (per-unit costs). + effects_of_investment_per_size: Variable costs proportional to size (per-unit costs). Dict: {'effect_name': value/unit} (e.g., {'cost': 1200}). - piecewise_effects: Non-linear costs using PiecewiseEffects. - Combinable with fix_effects and specific_effects. - divest_effects: Costs incurred if NOT investing (demolition, penalties). + piecewise_effects_of_investment: Non-linear costs using PiecewiseEffects. + Combinable with effects_of_investment and effects_of_investment_per_size. + effects_of_retirement: Costs incurred if NOT investing (demolition, penalties). Dict: {'effect_name': value}. + Deprecated Args: + fix_effects: **Deprecated**. Use `effects_of_investment` instead. + Will be removed in version 4.0. + specific_effects: **Deprecated**. Use `effects_of_investment_per_size` instead. + Will be removed in version 4.0. + divest_effects: **Deprecated**. Use `effects_of_retirement` instead. + Will be removed in version 4.0. + piecewise_effects: **Deprecated**. Use `piecewise_effects_of_investment` instead. + 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. For long-term investments, the cost values should be annualized to the corresponding operation time (annuity). @@ -723,11 +734,11 @@ class InvestParameters(Interface): solar_investment = InvestParameters( fixed_size=100, # 100 kW system (binary decision) optional=True, - fix_effects={ + effects_of_investment={ 'cost': 25000, # Installation and permitting costs 'CO2': -50000, # Avoided emissions over lifetime }, - specific_effects={ + effects_of_investment_per_size={ 'cost': 1200, # €1200/kW for panels (annualized) 'CO2': -800, # kg CO2 avoided per kW annually }, @@ -741,11 +752,11 @@ class InvestParameters(Interface): minimum_size=10, # Minimum viable system size (kWh) maximum_size=1000, # Maximum installable capacity optional=True, - fix_effects={ + effects_of_investment={ 'cost': 5000, # Grid connection and control system 'installation_time': 2, # Days for fixed components }, - piecewise_effects=PiecewiseEffects( + piecewise_effects_of_investment=PiecewiseEffects( piecewise_origin=Piecewise( [ Piece(0, 100), # Small systems @@ -766,22 +777,22 @@ class InvestParameters(Interface): ) ``` - Mandatory replacement with divestment costs: + Mandatory replacement with retirement costs: ```python boiler_replacement = InvestParameters( minimum_size=50, maximum_size=200, optional=True, # Can choose not to replace - fix_effects={ + effects_of_investment={ 'cost': 15000, # Installation costs 'disruption': 3, # Days of downtime }, - specific_effects={ + effects_of_investment_per_size={ 'cost': 400, # €400/kW capacity 'maintenance': 25, # Annual maintenance per kW }, - divest_effects={ + effects_of_retirement={ 'cost': 8000, # Demolition if not replaced 'environmental': 100, # Disposal fees }, @@ -794,16 +805,16 @@ class InvestParameters(Interface): # Gas turbine option gas_turbine = InvestParameters( fixed_size=50, # MW - fix_effects={'cost': 2500000, 'CO2': 1250000}, - specific_effects={'fuel_cost': 45, 'maintenance': 12}, + effects_of_investment={'cost': 2500000, 'CO2': 1250000}, + effects_of_investment_per_size={'fuel_cost': 45, 'maintenance': 12}, ) # Wind farm option wind_farm = InvestParameters( minimum_size=20, maximum_size=100, - fix_effects={'cost': 1000000, 'CO2': -5000000}, - specific_effects={'cost': 1800000, 'land_use': 0.5}, + effects_of_investment={'cost': 1000000, 'CO2': -5000000}, + effects_of_investment_per_size={'cost': 1800000, 'land_use': 0.5}, ) ``` @@ -813,7 +824,7 @@ class InvestParameters(Interface): hydrogen_electrolyzer = InvestParameters( minimum_size=1, maximum_size=50, # MW - piecewise_effects=PiecewiseEffects( + piecewise_effects_of_investment=PiecewiseEffects( piecewise_origin=Piecewise( [ Piece(0, 5), # Small scale: early adoption @@ -861,38 +872,104 @@ def __init__( specific_effects: PeriodicEffectsUser | None = None, # costs per Flow-Unit/Storage-Size/... piecewise_effects: PiecewiseEffects | None = None, divest_effects: PeriodicEffectsUser | None = None, + # New parameter names + effects_of_investment: PeriodicEffectsUser | None = None, + effects_of_investment_per_size: PeriodicEffectsUser | None = None, + effects_of_retirement: PeriodicEffectsUser | None = None, + piecewise_effects_of_investment: PiecewiseEffects | None = None, + **kwargs, ): - self.fix_effects: PeriodicEffectsUser = fix_effects or {} - self.divest_effects: PeriodicEffectsUser = divest_effects or {} + # Handle deprecated 'fix_effects' parameter + if fix_effects is not None: + warnings.warn( + "Parameter 'fix_effects' is deprecated and will be removed in version 4.0. " + "Use 'effects_of_investment' instead.", + DeprecationWarning, + stacklevel=2, + ) + if effects_of_investment is None: + effects_of_investment = fix_effects + + # Handle deprecated 'specific_effects' parameter + if specific_effects is not None: + warnings.warn( + "Parameter 'specific_effects' is deprecated and will be removed in version 4.0. " + "Use 'effects_of_investment_per_size' instead.", + DeprecationWarning, + stacklevel=2, + ) + if effects_of_investment_per_size is None: + effects_of_investment_per_size = specific_effects + + # Handle deprecated 'divest_effects' parameter + if divest_effects is not None: + warnings.warn( + "Parameter 'divest_effects' is deprecated and will be removed in version 4.0. " + "Use 'effects_of_retirement' instead.", + DeprecationWarning, + stacklevel=2, + ) + if effects_of_retirement is None: + effects_of_retirement = divest_effects + + # Handle deprecated 'piecewise_effects' parameter + if piecewise_effects is not None: + warnings.warn( + "Parameter 'piecewise_effects' is deprecated and will be removed in version 4.0. " + "Use 'piecewise_effects_of_investment' instead.", + DeprecationWarning, + stacklevel=2, + ) + if piecewise_effects_of_investment is None: + piecewise_effects_of_investment = piecewise_effects + + # Assign to internal attributes using new names + self.effects_of_investment: PeriodicEffectsUser = effects_of_investment or {} + self.effects_of_retirement: PeriodicEffectsUser = effects_of_retirement or {} self.fixed_size = fixed_size self.optional = optional - self.specific_effects: PeriodicEffectsUser = specific_effects if specific_effects is not None else {} - self.piecewise_effects = piecewise_effects + self.effects_of_investment_per_size: PeriodicEffectsUser = ( + effects_of_investment_per_size if effects_of_investment_per_size is not None else {} + ) + 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 + # Keep old attribute names for backward compatibility (deprecated) + self.fix_effects = self.effects_of_investment + self.specific_effects = self.effects_of_investment_per_size + self.divest_effects = self.effects_of_retirement + self.piecewise_effects = self.piecewise_effects_of_investment + def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: - self.fix_effects = flow_system.fit_effects_to_model_coords( + self.effects_of_investment = flow_system.fit_effects_to_model_coords( label_prefix=name_prefix, - effect_values=self.fix_effects, - label_suffix='fix_effects', + effect_values=self.effects_of_investment, + label_suffix='effects_of_investment', dims=['period', 'scenario'], ) - self.divest_effects = flow_system.fit_effects_to_model_coords( + self.effects_of_retirement = flow_system.fit_effects_to_model_coords( label_prefix=name_prefix, - effect_values=self.divest_effects, - label_suffix='divest_effects', + effect_values=self.effects_of_retirement, + label_suffix='effects_of_retirement', dims=['period', 'scenario'], ) - self.specific_effects = flow_system.fit_effects_to_model_coords( + self.effects_of_investment_per_size = flow_system.fit_effects_to_model_coords( label_prefix=name_prefix, - effect_values=self.specific_effects, - label_suffix='specific_effects', + effect_values=self.effects_of_investment_per_size, + label_suffix='effects_of_investment_per_size', dims=['period', 'scenario'], ) - if self.piecewise_effects is not None: - self.piecewise_effects.has_time_dim = False - self.piecewise_effects.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects') + + # Update deprecated attributes to maintain backward compatibility + self.fix_effects = self.effects_of_investment + self.divest_effects = self.effects_of_retirement + self.specific_effects = self.effects_of_investment_per_size + self.piecewise_effects = self.piecewise_effects_of_investment + + if self.piecewise_effects_of_investment is not None: + self.piecewise_effects_of_investment.has_time_dim = False + self.piecewise_effects_of_investment.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects') self.minimum_size = flow_system.fit_to_model_coords( f'{name_prefix}|minimum_size', self.minimum_size, dims=['period', 'scenario'] From ad262b8455e8f6d7a6b1885b119c205cd227e028 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 9 Oct 2025 22:16:09 +0200 Subject: [PATCH 02/13] Improve deprecation --- flixopt/interface.py | 61 ++++-- tests/test_invest_parameters_deprecation.py | 227 ++++++++++++++++++++ 2 files changed, 271 insertions(+), 17 deletions(-) create mode 100644 tests/test_invest_parameters_deprecation.py diff --git a/flixopt/interface.py b/flixopt/interface.py index 44418eb34..51606378d 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -868,11 +868,6 @@ def __init__( minimum_size: PeriodicDataUser | None = None, maximum_size: PeriodicDataUser | None = None, optional: bool = True, # Investition ist weglassbar - fix_effects: PeriodicEffectsUser | None = None, - specific_effects: PeriodicEffectsUser | None = None, # costs per Flow-Unit/Storage-Size/... - piecewise_effects: PiecewiseEffects | None = None, - divest_effects: PeriodicEffectsUser | None = None, - # New parameter names effects_of_investment: PeriodicEffectsUser | None = None, effects_of_investment_per_size: PeriodicEffectsUser | None = None, effects_of_retirement: PeriodicEffectsUser | None = None, @@ -880,6 +875,7 @@ def __init__( **kwargs, ): # Handle deprecated 'fix_effects' parameter + fix_effects = kwargs.pop('fix_effects', None) if fix_effects is not None: warnings.warn( "Parameter 'fix_effects' is deprecated and will be removed in version 4.0. " @@ -891,6 +887,7 @@ def __init__( effects_of_investment = fix_effects # Handle deprecated 'specific_effects' parameter + specific_effects = kwargs.pop('specific_effects', None) if specific_effects is not None: warnings.warn( "Parameter 'specific_effects' is deprecated and will be removed in version 4.0. " @@ -902,6 +899,7 @@ def __init__( effects_of_investment_per_size = specific_effects # Handle deprecated 'divest_effects' parameter + divest_effects = kwargs.pop('divest_effects', None) if divest_effects is not None: warnings.warn( "Parameter 'divest_effects' is deprecated and will be removed in version 4.0. " @@ -913,6 +911,7 @@ def __init__( effects_of_retirement = divest_effects # Handle deprecated 'piecewise_effects' parameter + piecewise_effects = kwargs.pop('piecewise_effects', None) if piecewise_effects is not None: warnings.warn( "Parameter 'piecewise_effects' is deprecated and will be removed in version 4.0. " @@ -935,12 +934,6 @@ def __init__( 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 - # Keep old attribute names for backward compatibility (deprecated) - self.fix_effects = self.effects_of_investment - self.specific_effects = self.effects_of_investment_per_size - self.divest_effects = self.effects_of_retirement - self.piecewise_effects = self.piecewise_effects_of_investment - def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: self.effects_of_investment = flow_system.fit_effects_to_model_coords( label_prefix=name_prefix, @@ -961,12 +954,6 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None dims=['period', 'scenario'], ) - # Update deprecated attributes to maintain backward compatibility - self.fix_effects = self.effects_of_investment - self.divest_effects = self.effects_of_retirement - self.specific_effects = self.effects_of_investment_per_size - self.piecewise_effects = self.piecewise_effects_of_investment - if self.piecewise_effects_of_investment is not None: self.piecewise_effects_of_investment.has_time_dim = False self.piecewise_effects_of_investment.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects') @@ -982,6 +969,46 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None f'{name_prefix}|fixed_size', self.fixed_size, dims=['period', 'scenario'] ) + @property + def fix_effects(self) -> PeriodicEffectsUser: + """Deprecated property. Use effects_of_investment instead.""" + warnings.warn( + 'The fix_effects property is deprecated. Use effects_of_investment instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.effects_of_investment + + @property + def specific_effects(self) -> PeriodicEffectsUser: + """Deprecated property. Use effects_of_investment_per_size instead.""" + warnings.warn( + 'The specific_effects property is deprecated. Use effects_of_investment_per_size instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.effects_of_investment_per_size + + @property + def divest_effects(self) -> PeriodicEffectsUser: + """Deprecated property. Use effects_of_retirement instead.""" + warnings.warn( + 'The divest_effects property is deprecated. Use effects_of_retirement instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.effects_of_retirement + + @property + def piecewise_effects(self) -> PiecewiseEffects | None: + """Deprecated property. Use piecewise_effects_of_investment instead.""" + warnings.warn( + 'The piecewise_effects property is deprecated. Use piecewise_effects_of_investment instead.', + DeprecationWarning, + stacklevel=2, + ) + return self.piecewise_effects_of_investment + @property def minimum_or_fixed_size(self) -> PeriodicData: return self.fixed_size if self.fixed_size is not None else self.minimum_size diff --git a/tests/test_invest_parameters_deprecation.py b/tests/test_invest_parameters_deprecation.py new file mode 100644 index 000000000..36ee99739 --- /dev/null +++ b/tests/test_invest_parameters_deprecation.py @@ -0,0 +1,227 @@ +""" +Test backward compatibility and deprecation warnings for InvestParameters. + +This test verifies that: +1. Old parameter names (fix_effects, specific_effects, divest_effects, piecewise_effects) still work with warnings +2. New parameter names (effects_of_investment, effects_of_investment_per_size, effects_of_retirement, piecewise_effects_of_investment) work correctly +3. Both old and new approaches produce equivalent results +""" + +import warnings + +import pytest + +from flixopt.interface import InvestParameters + + +class TestInvestParametersDeprecation: + """Test suite for InvestParameters parameter deprecation.""" + + def test_new_parameters_no_warnings(self): + """Test that new parameter names don't trigger warnings.""" + with warnings.catch_warnings(): + warnings.simplefilter('error', DeprecationWarning) + # Should not raise DeprecationWarning + params = InvestParameters( + fixed_size=100, + effects_of_investment={'cost': 25000}, + effects_of_investment_per_size={'cost': 1200}, + effects_of_retirement={'cost': 5000}, + ) + assert params.effects_of_investment == {'cost': 25000} + assert params.effects_of_investment_per_size == {'cost': 1200} + assert params.effects_of_retirement == {'cost': 5000} + + def test_old_fix_effects_deprecation_warning(self): + """Test that fix_effects triggers deprecation warning.""" + with pytest.warns(DeprecationWarning, match='fix_effects.*deprecated.*effects_of_investment'): + params = InvestParameters(fix_effects={'cost': 25000}) + # Verify backward compatibility + assert params.effects_of_investment == {'cost': 25000} + + # Accessing the property also triggers warning + with pytest.warns(DeprecationWarning, match='fix_effects.*deprecated.*effects_of_investment'): + assert params.fix_effects == {'cost': 25000} + + def test_old_specific_effects_deprecation_warning(self): + """Test that specific_effects triggers deprecation warning.""" + with pytest.warns(DeprecationWarning, match='specific_effects.*deprecated.*effects_of_investment_per_size'): + params = InvestParameters(specific_effects={'cost': 1200}) + # Verify backward compatibility + assert params.effects_of_investment_per_size == {'cost': 1200} + + # Accessing the property also triggers warning + with pytest.warns(DeprecationWarning, match='specific_effects.*deprecated.*effects_of_investment_per_size'): + assert params.specific_effects == {'cost': 1200} + + def test_old_divest_effects_deprecation_warning(self): + """Test that divest_effects triggers deprecation warning.""" + with pytest.warns(DeprecationWarning, match='divest_effects.*deprecated.*effects_of_retirement'): + params = InvestParameters(divest_effects={'cost': 5000}) + # Verify backward compatibility + assert params.effects_of_retirement == {'cost': 5000} + + # Accessing the property also triggers warning + with pytest.warns(DeprecationWarning, match='divest_effects.*deprecated.*effects_of_retirement'): + assert params.divest_effects == {'cost': 5000} + + def test_old_piecewise_effects_deprecation_warning(self): + """Test that piecewise_effects triggers deprecation warning.""" + from flixopt.interface import Piece, Piecewise, PiecewiseEffects + + test_piecewise = PiecewiseEffects( + piecewise_origin=Piecewise([Piece(0, 100)]), + piecewise_shares={'cost': Piecewise([Piece(800, 600)])}, + ) + with pytest.warns(DeprecationWarning, match='piecewise_effects.*deprecated.*piecewise_effects_of_investment'): + params = InvestParameters(piecewise_effects=test_piecewise) + # Verify backward compatibility + assert params.piecewise_effects_of_investment is test_piecewise + + # Accessing the property also triggers warning + with pytest.warns(DeprecationWarning, match='piecewise_effects.*deprecated.*piecewise_effects_of_investment'): + assert params.piecewise_effects is test_piecewise + + def test_all_old_parameters_together(self): + """Test all old parameters work together with warnings.""" + from flixopt.interface import Piece, Piecewise, PiecewiseEffects + + test_piecewise = PiecewiseEffects( + piecewise_origin=Piecewise([Piece(0, 100)]), + piecewise_shares={'cost': Piecewise([Piece(800, 600)])}, + ) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always', DeprecationWarning) + params = InvestParameters( + fixed_size=100, + fix_effects={'cost': 25000}, + specific_effects={'cost': 1200}, + divest_effects={'cost': 5000}, + piecewise_effects=test_piecewise, + ) + + # Should trigger 4 deprecation warnings (from kwargs) + assert len([warning for warning in w if issubclass(warning.category, DeprecationWarning)]) == 4 + + # Verify all mappings work (accessing new properties - no warnings) + assert params.effects_of_investment == {'cost': 25000} + assert params.effects_of_investment_per_size == {'cost': 1200} + assert params.effects_of_retirement == {'cost': 5000} + assert params.piecewise_effects_of_investment is test_piecewise + + # Verify old attributes still work (accessing deprecated properties - triggers warnings) + with pytest.warns(DeprecationWarning): + assert params.fix_effects == {'cost': 25000} + with pytest.warns(DeprecationWarning): + assert params.specific_effects == {'cost': 1200} + with pytest.warns(DeprecationWarning): + assert params.divest_effects == {'cost': 5000} + with pytest.warns(DeprecationWarning): + assert params.piecewise_effects is test_piecewise + + def test_new_parameters_override_old(self): + """Test that new parameters take precedence when both are provided.""" + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always', DeprecationWarning) + params = InvestParameters( + fix_effects={'cost': 10000}, # Old parameter + effects_of_investment={'cost': 25000}, # New parameter (should take precedence) + ) + + # Should still warn about deprecated parameter + assert len([warning for warning in w if issubclass(warning.category, DeprecationWarning)]) == 1 + + # New parameter should take precedence + assert params.effects_of_investment == {'cost': 25000} + + def test_piecewise_effects_of_investment_new_parameter(self): + """Test that piecewise_effects_of_investment works correctly.""" + from flixopt.interface import Piece, Piecewise, PiecewiseEffects + + test_piecewise = PiecewiseEffects( + piecewise_origin=Piecewise([Piece(0, 100)]), + piecewise_shares={'cost': Piecewise([Piece(800, 600)])}, + ) + + with warnings.catch_warnings(): + warnings.simplefilter('error', DeprecationWarning) + # Should not raise DeprecationWarning when using new parameter + params = InvestParameters(piecewise_effects_of_investment=test_piecewise) + assert params.piecewise_effects_of_investment is test_piecewise + + # Accessing deprecated property triggers warning + with pytest.warns(DeprecationWarning): + assert params.piecewise_effects is test_piecewise + + def test_backward_compatibility_with_features(self): + """Test that old attribute names remain accessible for features.py compatibility.""" + from flixopt.interface import Piece, Piecewise, PiecewiseEffects + + test_piecewise = PiecewiseEffects( + piecewise_origin=Piecewise([Piece(0, 100)]), + piecewise_shares={'cost': Piecewise([Piece(800, 600)])}, + ) + + params = InvestParameters( + effects_of_investment={'cost': 25000}, + effects_of_investment_per_size={'cost': 1200}, + effects_of_retirement={'cost': 5000}, + piecewise_effects_of_investment=test_piecewise, + ) + + # Old properties should still be accessible (for features.py) but with warnings + with pytest.warns(DeprecationWarning): + assert params.fix_effects == {'cost': 25000} + with pytest.warns(DeprecationWarning): + assert params.specific_effects == {'cost': 1200} + with pytest.warns(DeprecationWarning): + assert params.divest_effects == {'cost': 5000} + with pytest.warns(DeprecationWarning): + assert params.piecewise_effects is test_piecewise + + # Properties should return the same objects as the new attributes + with pytest.warns(DeprecationWarning): + assert params.fix_effects is params.effects_of_investment + with pytest.warns(DeprecationWarning): + assert params.specific_effects is params.effects_of_investment_per_size + with pytest.warns(DeprecationWarning): + assert params.divest_effects is params.effects_of_retirement + with pytest.warns(DeprecationWarning): + assert params.piecewise_effects is params.piecewise_effects_of_investment + + def test_empty_parameters(self): + """Test that empty/None parameters work correctly.""" + params = InvestParameters() + + assert params.effects_of_investment == {} + assert params.effects_of_investment_per_size == {} + assert params.effects_of_retirement == {} + assert params.piecewise_effects_of_investment is None + + # Old properties should also be empty (but with warnings) + with pytest.warns(DeprecationWarning): + assert params.fix_effects == {} + with pytest.warns(DeprecationWarning): + assert params.specific_effects == {} + with pytest.warns(DeprecationWarning): + assert params.divest_effects == {} + with pytest.warns(DeprecationWarning): + assert params.piecewise_effects is None + + def test_mixed_old_and_new_parameters(self): + """Test mixing old and new parameter names (not recommended but should work).""" + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always', DeprecationWarning) + params = InvestParameters( + effects_of_investment={'cost': 25000}, # New + specific_effects={'cost': 1200}, # Old + effects_of_retirement={'cost': 5000}, # New + ) + + # Should only warn about the old parameter + assert len([warning for warning in w if issubclass(warning.category, DeprecationWarning)]) == 1 + + # All should work correctly + assert params.effects_of_investment == {'cost': 25000} + assert params.effects_of_investment_per_size == {'cost': 1200} + assert params.effects_of_retirement == {'cost': 5000} From 073804f2c3b6b5f5e9c8fe33a10c3a142815e1fa Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Thu, 9 Oct 2025 22:34:57 +0200 Subject: [PATCH 03/13] Update usage of deprectated parameters --- examples/04_Scenarios/scenario_example.py | 2 +- .../two_stage_optimization.py | 10 ++++++--- flixopt/features.py | 21 +++++++++++-------- tests/conftest.py | 13 +++++++----- tests/test_component.py | 20 ++++++++++++++---- tests/test_effect.py | 2 +- tests/test_functional.py | 14 ++++++++----- 7 files changed, 54 insertions(+), 28 deletions(-) diff --git a/examples/04_Scenarios/scenario_example.py b/examples/04_Scenarios/scenario_example.py index 601074dc4..c48ee56a8 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(fix_effects=20, fixed_size=30, optional=False), + capacity_in_flow_hours=fx.InvestParameters(effects_of_investment=20, fixed_size=30, optional=False), 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/examples/05_Two-stage-optimization/two_stage_optimization.py b/examples/05_Two-stage-optimization/two_stage_optimization.py index c8d709b8d..30b8a5532 100644 --- a/examples/05_Two-stage-optimization/two_stage_optimization.py +++ b/examples/05_Two-stage-optimization/two_stage_optimization.py @@ -48,7 +48,9 @@ Q_fu=fx.Flow( label='Q_fu', bus='Gas', - size=fx.InvestParameters(specific_effects={'costs': 1_000}, minimum_size=10, maximum_size=500), + size=fx.InvestParameters( + effects_of_investment_per_size={'costs': 1_000}, minimum_size=10, maximum_size=500 + ), relative_minimum=0.2, previous_flow_rate=20, on_off_parameters=fx.OnOffParameters(effects_per_switch_on=300), @@ -66,7 +68,9 @@ Q_fu=fx.Flow( 'Q_fu', bus='Kohle', - size=fx.InvestParameters(specific_effects={'costs': 3_000}, minimum_size=10, maximum_size=500), + size=fx.InvestParameters( + effects_of_investment_per_size={'costs': 3_000}, minimum_size=10, maximum_size=500 + ), relative_minimum=0.3, previous_flow_rate=100, ), @@ -74,7 +78,7 @@ fx.Storage( 'Speicher', capacity_in_flow_hours=fx.InvestParameters( - minimum_size=10, maximum_size=1000, specific_effects={'costs': 60} + minimum_size=10, maximum_size=1000, effects_of_investment_per_size={'costs': 60} ), initial_charge_state='lastValueOfSim', eta_charge=1, diff --git a/flixopt/features.py b/flixopt/features.py index 9843a7f57..f266a1946 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -75,14 +75,14 @@ def _create_variables_and_constraints(self): bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size), ) - if self.parameters.piecewise_effects: + if self.parameters.piecewise_effects_of_investment: self.piecewise_effects = self.add_submodels( PiecewiseEffectsModel( model=self._model, label_of_element=self.label_of_element, label_of_model=f'{self.label_of_element}|PiecewiseEffects', - piecewise_origin=(self.size.name, self.parameters.piecewise_effects.piecewise_origin), - piecewise_shares=self.parameters.piecewise_effects.piecewise_shares, + piecewise_origin=(self.size.name, self.parameters.piecewise_effects_of_investment.piecewise_origin), + piecewise_shares=self.parameters.piecewise_effects_of_investment.piecewise_shares, zero_point=self.is_invested, ), short_name='segments', @@ -90,30 +90,33 @@ def _create_variables_and_constraints(self): def _add_effects(self): """Add investment effects""" - if self.parameters.fix_effects: + if self.parameters.effects_of_investment: self._model.effects.add_share_to_effects( name=self.label_of_element, expressions={ effect: self.is_invested * factor if self.is_invested is not None else factor - for effect, factor in self.parameters.fix_effects.items() + for effect, factor in self.parameters.effects_of_investment.items() }, target='periodic', ) - if self.parameters.divest_effects and self.parameters.optional: + if self.parameters.effects_of_retirement and self.parameters.optional: self._model.effects.add_share_to_effects( name=self.label_of_element, expressions={ effect: -self.is_invested * factor + factor - for effect, factor in self.parameters.divest_effects.items() + for effect, factor in self.parameters.effects_of_retirement.items() }, target='periodic', ) - if self.parameters.specific_effects: + if self.parameters.effects_of_investment_per_size: self._model.effects.add_share_to_effects( name=self.label_of_element, - expressions={effect: self.size * factor for effect, factor in self.parameters.specific_effects.items()}, + expressions={ + effect: self.size * factor + for effect, factor in self.parameters.effects_of_investment_per_size.items() + }, target='periodic', ) diff --git a/tests/conftest.py b/tests/conftest.py index 069d5c1ee..9dd8e6d0d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -159,7 +159,10 @@ def complex(): relative_maximum=1, previous_flow_rate=50, size=fx.InvestParameters( - fix_effects=1000, fixed_size=50, optional=False, specific_effects={'costs': 10, 'PE': 2} + effects_of_investment=1000, + fixed_size=50, + optional=False, + effects_of_investment_per_size={'costs': 10, 'PE': 2}, ), on_off_parameters=fx.OnOffParameters( on_hours_total_min=0, @@ -264,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(fix_effects=20, fixed_size=30, optional=False), + capacity_in_flow_hours=fx.InvestParameters(effects_of_investment=20, fixed_size=30, optional=False), initial_charge_state=0, relative_maximum_charge_state=1 / 100 * np.array(charge_state_values), relative_maximum_final_charge_state=0.8, @@ -278,8 +281,8 @@ def simple(timesteps_length=9): def complex(): """Complex storage with piecewise investment from flow_system_complex""" invest_speicher = fx.InvestParameters( - fix_effects=0, - piecewise_effects=fx.PiecewiseEffects( + effects_of_investment=0, + piecewise_effects_of_investment=fx.PiecewiseEffects( piecewise_origin=fx.Piecewise([fx.Piece(5, 25), fx.Piece(25, 100)]), piecewise_shares={ 'costs': fx.Piecewise([fx.Piece(50, 250), fx.Piece(250, 800)]), @@ -287,7 +290,7 @@ def complex(): }, ), optional=False, - specific_effects={'costs': 0.01, 'CO2': 0.01}, + 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 4a7c4afd8..98e2ad7db 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -423,7 +423,9 @@ def test_transmission_basic(self, basic_flow_system, highs_solver): 'Rohr', relative_losses=0.2, absolute_losses=20, - in1=fx.Flow('Rohr1', 'Wärme lokal', size=fx.InvestParameters(specific_effects=5, maximum_size=1e6)), + in1=fx.Flow( + 'Rohr1', 'Wärme lokal', size=fx.InvestParameters(effects_of_investment_per_size=5, maximum_size=1e6) + ), out1=fx.Flow('Rohr2', 'Fernwärme', size=1000), ) @@ -477,7 +479,11 @@ def test_transmission_balanced(self, basic_flow_system, highs_solver): 'Rohr', relative_losses=0.2, absolute_losses=20, - in1=fx.Flow('Rohr1a', bus='Wärme lokal', size=fx.InvestParameters(specific_effects=5, maximum_size=1000)), + in1=fx.Flow( + 'Rohr1a', + bus='Wärme lokal', + 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()), out2=fx.Flow('Rohr2b', bus='Wärme lokal', size=1000), @@ -547,10 +553,16 @@ def test_transmission_unbalanced(self, basic_flow_system, highs_solver): 'Rohr', relative_losses=0.2, absolute_losses=20, - in1=fx.Flow('Rohr1a', bus='Wärme lokal', size=fx.InvestParameters(specific_effects=50, maximum_size=1000)), + in1=fx.Flow( + 'Rohr1a', + bus='Wärme lokal', + size=fx.InvestParameters(effects_of_investment_per_size=50, maximum_size=1000), + ), out1=fx.Flow('Rohr1b', 'Fernwärme', size=1000), in2=fx.Flow( - 'Rohr2a', 'Fernwärme', size=fx.InvestParameters(specific_effects=100, minimum_size=10, optional=False) + 'Rohr2a', + 'Fernwärme', + size=fx.InvestParameters(effects_of_investment_per_size=100, minimum_size=10, optional=False), ), 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 09ac6e418..b855ec8d0 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(specific_effects=10, minimum_size=20, optional=False), + size=fx.InvestParameters(effects_of_investment_per_size=10, minimum_size=20, optional=False), ), Q_fu=fx.Flow('Q_fu', bus='Gas'), ), diff --git a/tests/test_functional.py b/tests/test_functional.py index 17caea2b5..94172eccf 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -146,7 +146,7 @@ def test_fixed_size(solver_fixture, time_steps_fixture): Q_th=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(fixed_size=1000, fix_effects=10, specific_effects=1), + size=fx.InvestParameters(fixed_size=1000, effects_of_investment=10, effects_of_investment_per_size=1), ), ) ) @@ -187,7 +187,7 @@ def test_optimize_size(solver_fixture, time_steps_fixture): Q_th=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(fix_effects=10, specific_effects=1), + size=fx.InvestParameters(effects_of_investment=10, effects_of_investment_per_size=1), ), ) ) @@ -228,7 +228,7 @@ def test_size_bounds(solver_fixture, time_steps_fixture): Q_th=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(minimum_size=40, fix_effects=10, specific_effects=1), + size=fx.InvestParameters(minimum_size=40, effects_of_investment=10, effects_of_investment_per_size=1), ), ) ) @@ -269,7 +269,9 @@ def test_optional_invest(solver_fixture, time_steps_fixture): Q_th=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(optional=True, minimum_size=40, fix_effects=10, specific_effects=1), + size=fx.InvestParameters( + optional=True, minimum_size=40, effects_of_investment=10, effects_of_investment_per_size=1 + ), ), ), fx.linear_converters.Boiler( @@ -279,7 +281,9 @@ def test_optional_invest(solver_fixture, time_steps_fixture): Q_th=fx.Flow( 'Q_th', bus='Fernwärme', - size=fx.InvestParameters(optional=True, minimum_size=50, fix_effects=10, specific_effects=1), + size=fx.InvestParameters( + optional=True, minimum_size=50, effects_of_investment=10, effects_of_investment_per_size=1 + ), ), ), ) From 9164b01f9eca16bc1d2dc37af6a98577c45b8bfd Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 10 Oct 2025 08:47:45 +0200 Subject: [PATCH 04/13] Improve None handling --- flixopt/interface.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index 51606378d..391932291 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -922,9 +922,12 @@ def __init__( if piecewise_effects_of_investment is None: piecewise_effects_of_investment = piecewise_effects - # Assign to internal attributes using new names - self.effects_of_investment: PeriodicEffectsUser = effects_of_investment or {} - self.effects_of_retirement: PeriodicEffectsUser = effects_of_retirement or {} + self.effects_of_investment: PeriodicEffectsUser = ( + effects_of_investment if effects_of_investment is not None else {} + ) + self.effects_of_retirement: PeriodicEffectsUser = ( + effects_of_retirement if effects_of_retirement is not None else {} + ) self.fixed_size = fixed_size self.optional = optional self.effects_of_investment_per_size: PeriodicEffectsUser = ( From 14006f8002d58777cb89eabbfca038d3401ebc5f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 10 Oct 2025 08:49:54 +0200 Subject: [PATCH 05/13] Add extra kwargs handling --- flixopt/interface.py | 5 +++++ tests/test_invest_parameters_deprecation.py | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/flixopt/interface.py b/flixopt/interface.py index 391932291..dd5f842e1 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -922,6 +922,11 @@ def __init__( if piecewise_effects_of_investment is None: piecewise_effects_of_investment = piecewise_effects + # Check for unexpected keyword arguments + if kwargs: + unexpected = ', '.join(f"'{k}'" for k in kwargs.keys()) + raise TypeError(f'InvestParameters() got unexpected keyword arguments: {unexpected}') + self.effects_of_investment: PeriodicEffectsUser = ( effects_of_investment if effects_of_investment is not None else {} ) diff --git a/tests/test_invest_parameters_deprecation.py b/tests/test_invest_parameters_deprecation.py index 36ee99739..b1c907c68 100644 --- a/tests/test_invest_parameters_deprecation.py +++ b/tests/test_invest_parameters_deprecation.py @@ -225,3 +225,19 @@ def test_mixed_old_and_new_parameters(self): assert params.effects_of_investment == {'cost': 25000} assert params.effects_of_investment_per_size == {'cost': 1200} assert params.effects_of_retirement == {'cost': 5000} + + def test_unexpected_keyword_arguments(self): + """Test that unexpected keyword arguments raise TypeError.""" + # Single unexpected argument + with pytest.raises(TypeError, match="InvestParameters\\(\\) got unexpected keyword arguments: 'invalid_param'"): + InvestParameters(invalid_param='value') + + # Multiple unexpected arguments + with pytest.raises( + TypeError, match="InvestParameters\\(\\) got unexpected keyword arguments: 'param1', 'param2'" + ): + InvestParameters(param1='value1', param2='value2') + + # Mix of valid and invalid arguments + with pytest.raises(TypeError, match="InvestParameters\\(\\) got unexpected keyword arguments: 'typo'"): + InvestParameters(effects_of_investment={'cost': 100}, typo='value') From 063333b62481e8821753a9be30dddeba667ed2d6 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 10 Oct 2025 08:56:03 +0200 Subject: [PATCH 06/13] Improve deprecation --- flixopt/interface.py | 38 ++++++++------ tests/test_invest_parameters_deprecation.py | 57 +++++++++++++++++---- 2 files changed, 67 insertions(+), 28 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index dd5f842e1..57c80b84f 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -878,51 +878,55 @@ def __init__( fix_effects = kwargs.pop('fix_effects', None) if fix_effects is not None: warnings.warn( - "Parameter 'fix_effects' is deprecated and will be removed in version 4.0. " - "Use 'effects_of_investment' instead.", + 'The use of the "fix_effects" argument is deprecated. Use the "effects_of_investment" argument instead.', DeprecationWarning, stacklevel=2, ) - if effects_of_investment is None: - effects_of_investment = fix_effects + if effects_of_investment is not None: + raise ValueError('Either fix_effects or effects_of_investment can be specified, but not both.') + effects_of_investment = fix_effects # Handle deprecated 'specific_effects' parameter specific_effects = kwargs.pop('specific_effects', None) if specific_effects is not None: warnings.warn( - "Parameter 'specific_effects' is deprecated and will be removed in version 4.0. " - "Use 'effects_of_investment_per_size' instead.", + 'The use of the "specific_effects" argument is deprecated. Use the "effects_of_investment_per_size" argument instead.', DeprecationWarning, stacklevel=2, ) - if effects_of_investment_per_size is None: - effects_of_investment_per_size = specific_effects + if effects_of_investment_per_size is not None: + raise ValueError( + 'Either specific_effects or effects_of_investment_per_size can be specified, but not both.' + ) + effects_of_investment_per_size = specific_effects # Handle deprecated 'divest_effects' parameter divest_effects = kwargs.pop('divest_effects', None) if divest_effects is not None: warnings.warn( - "Parameter 'divest_effects' is deprecated and will be removed in version 4.0. " - "Use 'effects_of_retirement' instead.", + 'The use of the "divest_effects" argument is deprecated. Use the "effects_of_retirement" argument instead.', DeprecationWarning, stacklevel=2, ) - if effects_of_retirement is None: - effects_of_retirement = divest_effects + if effects_of_retirement is not None: + raise ValueError('Either divest_effects or effects_of_retirement can be specified, but not both.') + effects_of_retirement = divest_effects # Handle deprecated 'piecewise_effects' parameter piecewise_effects = kwargs.pop('piecewise_effects', None) if piecewise_effects is not None: warnings.warn( - "Parameter 'piecewise_effects' is deprecated and will be removed in version 4.0. " - "Use 'piecewise_effects_of_investment' instead.", + 'The use of the "piecewise_effects" argument is deprecated. Use the "piecewise_effects_of_investment" argument instead.', DeprecationWarning, stacklevel=2, ) - if piecewise_effects_of_investment is None: - piecewise_effects_of_investment = piecewise_effects + if piecewise_effects_of_investment is not None: + raise ValueError( + 'Either piecewise_effects or piecewise_effects_of_investment can be specified, but not both.' + ) + piecewise_effects_of_investment = piecewise_effects - # Check for unexpected keyword arguments + # Validate any remaining unexpected kwargs if kwargs: unexpected = ', '.join(f"'{k}'" for k in kwargs.keys()) raise TypeError(f'InvestParameters() got unexpected keyword arguments: {unexpected}') diff --git a/tests/test_invest_parameters_deprecation.py b/tests/test_invest_parameters_deprecation.py index b1c907c68..117d5fdd7 100644 --- a/tests/test_invest_parameters_deprecation.py +++ b/tests/test_invest_parameters_deprecation.py @@ -119,20 +119,55 @@ def test_all_old_parameters_together(self): with pytest.warns(DeprecationWarning): assert params.piecewise_effects is test_piecewise - def test_new_parameters_override_old(self): - """Test that new parameters take precedence when both are provided.""" - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always', DeprecationWarning) - params = InvestParameters( - fix_effects={'cost': 10000}, # Old parameter - effects_of_investment={'cost': 25000}, # New parameter (should take precedence) + def test_both_old_and_new_raises_error(self): + """Test that specifying both old and new parameter names raises ValueError.""" + # fix_effects + effects_of_investment + with pytest.raises( + ValueError, match='Either fix_effects or effects_of_investment can be specified, but not both' + ): + InvestParameters( + fix_effects={'cost': 10000}, + effects_of_investment={'cost': 25000}, ) - # Should still warn about deprecated parameter - assert len([warning for warning in w if issubclass(warning.category, DeprecationWarning)]) == 1 + # specific_effects + effects_of_investment_per_size + with pytest.raises( + ValueError, + match='Either specific_effects or effects_of_investment_per_size can be specified, but not both', + ): + InvestParameters( + specific_effects={'cost': 1200}, + effects_of_investment_per_size={'cost': 1500}, + ) - # New parameter should take precedence - assert params.effects_of_investment == {'cost': 25000} + # divest_effects + effects_of_retirement + with pytest.raises( + ValueError, match='Either divest_effects or effects_of_retirement can be specified, but not both' + ): + InvestParameters( + divest_effects={'cost': 5000}, + effects_of_retirement={'cost': 6000}, + ) + + # piecewise_effects + piecewise_effects_of_investment + from flixopt.interface import Piece, Piecewise, PiecewiseEffects + + test_piecewise1 = PiecewiseEffects( + piecewise_origin=Piecewise([Piece(0, 100)]), + piecewise_shares={'cost': Piecewise([Piece(800, 600)])}, + ) + test_piecewise2 = PiecewiseEffects( + piecewise_origin=Piecewise([Piece(0, 200)]), + piecewise_shares={'cost': Piecewise([Piece(900, 700)])}, + ) + with pytest.raises( + ValueError, + match='Either piecewise_effects or piecewise_effects_of_investment can be specified, but not both', + ): + InvestParameters( + piecewise_effects=test_piecewise1, + piecewise_effects_of_investment=test_piecewise2, + ) def test_piecewise_effects_of_investment_new_parameter(self): """Test that piecewise_effects_of_investment works correctly.""" From 98ac8c4d6092f62b7e0b166e7d4749e14d78288b Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 10 Oct 2025 09:00:03 +0200 Subject: [PATCH 07/13] Use custom method for kwargs --- flixopt/interface.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index 57c80b84f..3abe321fc 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -927,9 +927,7 @@ def __init__( piecewise_effects_of_investment = piecewise_effects # Validate any remaining unexpected kwargs - if kwargs: - unexpected = ', '.join(f"'{k}'" for k in kwargs.keys()) - raise TypeError(f'InvestParameters() got unexpected keyword arguments: {unexpected}') + self._validate_kwargs(kwargs) self.effects_of_investment: PeriodicEffectsUser = ( effects_of_investment if effects_of_investment is not None else {} From 81504e15eb8bb123845b17e21a5a0cee91b00fe1 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 10 Oct 2025 09:21:41 +0200 Subject: [PATCH 08/13] Add deprecation method --- flixopt/interface.py | 64 +++++---------------- flixopt/structure.py | 46 +++++++++++++++ tests/test_invest_parameters_deprecation.py | 11 +++- 3 files changed, 67 insertions(+), 54 deletions(-) diff --git a/flixopt/interface.py b/flixopt/interface.py index 3abe321fc..b985729e6 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -874,57 +874,19 @@ def __init__( piecewise_effects_of_investment: PiecewiseEffects | None = None, **kwargs, ): - # Handle deprecated 'fix_effects' parameter - fix_effects = kwargs.pop('fix_effects', None) - if fix_effects is not None: - warnings.warn( - 'The use of the "fix_effects" argument is deprecated. Use the "effects_of_investment" argument instead.', - DeprecationWarning, - stacklevel=2, - ) - if effects_of_investment is not None: - raise ValueError('Either fix_effects or effects_of_investment can be specified, but not both.') - effects_of_investment = fix_effects - - # Handle deprecated 'specific_effects' parameter - specific_effects = kwargs.pop('specific_effects', None) - if specific_effects is not None: - warnings.warn( - 'The use of the "specific_effects" argument is deprecated. Use the "effects_of_investment_per_size" argument instead.', - DeprecationWarning, - stacklevel=2, - ) - if effects_of_investment_per_size is not None: - raise ValueError( - 'Either specific_effects or effects_of_investment_per_size can be specified, but not both.' - ) - effects_of_investment_per_size = specific_effects - - # Handle deprecated 'divest_effects' parameter - divest_effects = kwargs.pop('divest_effects', None) - if divest_effects is not None: - warnings.warn( - 'The use of the "divest_effects" argument is deprecated. Use the "effects_of_retirement" argument instead.', - DeprecationWarning, - stacklevel=2, - ) - if effects_of_retirement is not None: - raise ValueError('Either divest_effects or effects_of_retirement can be specified, but not both.') - effects_of_retirement = divest_effects - - # Handle deprecated 'piecewise_effects' parameter - piecewise_effects = kwargs.pop('piecewise_effects', None) - if piecewise_effects is not None: - warnings.warn( - 'The use of the "piecewise_effects" argument is deprecated. Use the "piecewise_effects_of_investment" argument instead.', - DeprecationWarning, - stacklevel=2, - ) - if piecewise_effects_of_investment is not None: - raise ValueError( - 'Either piecewise_effects or piecewise_effects_of_investment can be specified, but not both.' - ) - piecewise_effects_of_investment = piecewise_effects + # Handle deprecated parameters using centralized helper + effects_of_investment = self._handle_deprecated_kwarg( + kwargs, 'fix_effects', 'effects_of_investment', effects_of_investment + ) + effects_of_investment_per_size = self._handle_deprecated_kwarg( + kwargs, 'specific_effects', 'effects_of_investment_per_size', effects_of_investment_per_size + ) + effects_of_retirement = self._handle_deprecated_kwarg( + kwargs, 'divest_effects', 'effects_of_retirement', effects_of_retirement + ) + piecewise_effects_of_investment = self._handle_deprecated_kwarg( + kwargs, 'piecewise_effects', 'piecewise_effects_of_investment', piecewise_effects_of_investment + ) # Validate any remaining unexpected kwargs self._validate_kwargs(kwargs) diff --git a/flixopt/structure.py b/flixopt/structure.py index 91d5a53c1..0a4d158ed 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -367,6 +367,52 @@ def _extract_dataarrays_recursive(self, obj, context_name: str = '') -> tuple[An else: return self._serialize_to_basic_types(obj), extracted_arrays + def _handle_deprecated_kwarg( + self, + kwargs: dict, + old_name: str, + new_name: str, + current_value: Any = None, + transform: callable = None, + check_conflict: bool = True, + ) -> Any: + """ + Handle a deprecated keyword argument by issuing a warning and returning the appropriate value. + + This centralizes the deprecation pattern used across multiple classes (Source, Sink, InvestParameters, etc.). + + Args: + kwargs: Dictionary of keyword arguments to check and modify + old_name: Name of the deprecated parameter + 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) + + 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 + """ + import warnings + + old_value = kwargs.pop(old_name, None) + if old_value is not None: + warnings.warn( + f'The use of the "{old_name}" argument is deprecated. Use the "{new_name}" argument instead.', + DeprecationWarning, + stacklevel=3, # Stack: this method -> __init__ -> caller + ) + if check_conflict and current_value is not None: + raise ValueError(f'Either {old_name} or {new_name} can be specified, but not both.') + + # Apply transformation if provided + 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: """ Validate that no unexpected keyword arguments are present in kwargs. diff --git a/tests/test_invest_parameters_deprecation.py b/tests/test_invest_parameters_deprecation.py index 117d5fdd7..5d7cf49b9 100644 --- a/tests/test_invest_parameters_deprecation.py +++ b/tests/test_invest_parameters_deprecation.py @@ -264,15 +264,20 @@ def test_mixed_old_and_new_parameters(self): def test_unexpected_keyword_arguments(self): """Test that unexpected keyword arguments raise TypeError.""" # Single unexpected argument - with pytest.raises(TypeError, match="InvestParameters\\(\\) got unexpected keyword arguments: 'invalid_param'"): + with pytest.raises( + TypeError, match="InvestParameters.__init__\\(\\) got unexpected keyword argument\\(s\\): 'invalid_param'" + ): InvestParameters(invalid_param='value') # Multiple unexpected arguments with pytest.raises( - TypeError, match="InvestParameters\\(\\) got unexpected keyword arguments: 'param1', 'param2'" + TypeError, + match="InvestParameters.__init__\\(\\) got unexpected keyword argument\\(s\\): 'param1', 'param2'", ): InvestParameters(param1='value1', param2='value2') # Mix of valid and invalid arguments - with pytest.raises(TypeError, match="InvestParameters\\(\\) got unexpected keyword arguments: 'typo'"): + with pytest.raises( + TypeError, match="InvestParameters.__init__\\(\\) got unexpected keyword argument\\(s\\): 'typo'" + ): InvestParameters(effects_of_investment={'cost': 100}, typo='value') From bfdd325d674c0010b29a460c89b00983af16f55e Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 10 Oct 2025 09:22:32 +0200 Subject: [PATCH 09/13] Apply deprecation method to other classes --- flixopt/components.py | 64 ++++++++++--------------------------------- 1 file changed, 14 insertions(+), 50 deletions(-) diff --git a/flixopt/components.py b/flixopt/components.py index 9af6be153..d0ef8cf16 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -1060,36 +1060,16 @@ def __init__( meta_data: dict | None = None, **kwargs, ): - source = kwargs.pop('source', None) - sink = kwargs.pop('sink', None) - prevent_simultaneous_sink_and_source = kwargs.pop('prevent_simultaneous_sink_and_source', None) - if source is not None: - warnings.warn( - 'The use of the "source" argument is deprecated. Use the "outputs" argument instead.', - DeprecationWarning, - stacklevel=2, - ) - if outputs is not None: - raise ValueError('Either source or outputs can be specified, but not both.') - outputs = [source] - - if sink is not None: - warnings.warn( - 'The use of the "sink" argument is deprecated. Use the "inputs" argument instead.', - DeprecationWarning, - stacklevel=2, - ) - if inputs is not None: - raise ValueError('Either sink or inputs can be specified, but not both.') - inputs = [sink] - - if prevent_simultaneous_sink_and_source is not None: - warnings.warn( - 'The use of the "prevent_simultaneous_sink_and_source" argument is deprecated. Use the "prevent_simultaneous_flow_rates" argument instead.', - DeprecationWarning, - stacklevel=2, - ) - prevent_simultaneous_flow_rates = prevent_simultaneous_sink_and_source + # Handle deprecated parameters using centralized helper + outputs = self._handle_deprecated_kwarg(kwargs, 'source', 'outputs', outputs, transform=lambda x: [x]) + inputs = self._handle_deprecated_kwarg(kwargs, 'sink', 'inputs', inputs, transform=lambda x: [x]) + prevent_simultaneous_flow_rates = self._handle_deprecated_kwarg( + kwargs, + 'prevent_simultaneous_sink_and_source', + 'prevent_simultaneous_flow_rates', + prevent_simultaneous_flow_rates, + check_conflict=False, + ) # Validate any remaining unexpected kwargs self._validate_kwargs(kwargs) @@ -1215,16 +1195,8 @@ def __init__( prevent_simultaneous_flow_rates: bool = False, **kwargs, ): - source = kwargs.pop('source', None) - if source is not None: - warnings.warn( - 'The use of the "source" argument is deprecated. Use the "outputs" argument instead.', - DeprecationWarning, - stacklevel=2, - ) - if outputs is not None: - raise ValueError('Either source or outputs can be specified, but not both.') - outputs = [source] + # Handle deprecated parameter using centralized helper + outputs = self._handle_deprecated_kwarg(kwargs, 'source', 'outputs', outputs, transform=lambda x: [x]) # Validate any remaining unexpected kwargs self._validate_kwargs(kwargs) @@ -1346,16 +1318,8 @@ def __init__( Note: The deprecated `sink` kwarg is accepted for compatibility but will be removed in future releases. """ - sink = kwargs.pop('sink', None) - if sink is not None: - warnings.warn( - 'The use of the "sink" argument is deprecated. Use the "inputs" argument instead.', - DeprecationWarning, - stacklevel=2, - ) - if inputs is not None: - raise ValueError('Either sink or inputs can be specified, but not both.') - inputs = [sink] + # Handle deprecated parameter using centralized helper + inputs = self._handle_deprecated_kwarg(kwargs, 'sink', 'inputs', inputs, transform=lambda x: [x]) # Validate any remaining unexpected kwargs self._validate_kwargs(kwargs) From 66d867a5e57c5f4984f995460fffa4c83bbae773 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 10 Oct 2025 09:27:44 +0200 Subject: [PATCH 10/13] Apply to effects.py as well --- flixopt/effects.py | 93 ++++++++-------------------------------------- 1 file changed, 15 insertions(+), 78 deletions(-) diff --git a/flixopt/effects.py b/flixopt/effects.py index cc7564191..2c7607b02 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -187,84 +187,21 @@ def __init__( self.share_from_temporal: TemporalEffectsUser = share_from_temporal if share_from_temporal is not None else {} self.share_from_periodic: PeriodicEffectsUser = share_from_periodic if share_from_periodic is not None else {} - # Handle backwards compatibility for deprecated parameters - # Extract deprecated parameters from kwargs - minimum_operation = kwargs.pop('minimum_operation', None) - maximum_operation = kwargs.pop('maximum_operation', None) - minimum_invest = kwargs.pop('minimum_invest', None) - maximum_invest = kwargs.pop('maximum_invest', None) - minimum_operation_per_hour = kwargs.pop('minimum_operation_per_hour', None) - maximum_operation_per_hour = kwargs.pop('maximum_operation_per_hour', None) - - # Handle minimum_temporal - if minimum_operation is not None: - warnings.warn( - "Parameter 'minimum_operation' is deprecated. Use 'minimum_temporal' instead.", - DeprecationWarning, - stacklevel=2, - ) - if minimum_temporal is not None: - raise ValueError('Either minimum_operation or minimum_temporal can be specified, but not both.') - minimum_temporal = minimum_operation - - # Handle maximum_temporal - if maximum_operation is not None: - warnings.warn( - "Parameter 'maximum_operation' is deprecated. Use 'maximum_temporal' instead.", - DeprecationWarning, - stacklevel=2, - ) - if maximum_temporal is not None: - raise ValueError('Either maximum_operation or maximum_temporal can be specified, but not both.') - maximum_temporal = maximum_operation - - # Handle minimum_periodic - if minimum_invest is not None: - warnings.warn( - "Parameter 'minimum_invest' is deprecated. Use 'minimum_periodic' instead.", - DeprecationWarning, - stacklevel=2, - ) - if minimum_periodic is not None: - raise ValueError('Either minimum_invest or minimum_periodic can be specified, but not both.') - minimum_periodic = minimum_invest - - # Handle maximum_periodic - if maximum_invest is not None: - warnings.warn( - "Parameter 'maximum_invest' is deprecated. Use 'maximum_periodic' instead.", - DeprecationWarning, - stacklevel=2, - ) - if maximum_periodic is not None: - raise ValueError('Either maximum_invest or maximum_periodic can be specified, but not both.') - maximum_periodic = maximum_invest - - # Handle minimum_per_hour - if minimum_operation_per_hour is not None: - warnings.warn( - "Parameter 'minimum_operation_per_hour' is deprecated. Use 'minimum_per_hour' instead.", - DeprecationWarning, - stacklevel=2, - ) - if minimum_per_hour is not None: - raise ValueError( - 'Either minimum_operation_per_hour or minimum_per_hour can be specified, but not both.' - ) - minimum_per_hour = minimum_operation_per_hour - - # Handle maximum_per_hour - if maximum_operation_per_hour is not None: - warnings.warn( - "Parameter 'maximum_operation_per_hour' is deprecated. Use 'maximum_per_hour' instead.", - DeprecationWarning, - stacklevel=2, - ) - if maximum_per_hour is not None: - raise ValueError( - 'Either maximum_operation_per_hour or maximum_per_hour can be specified, but not both.' - ) - maximum_per_hour = maximum_operation_per_hour + # Handle backwards compatibility for deprecated parameters using centralized helper + minimum_temporal = self._handle_deprecated_kwarg( + kwargs, 'minimum_operation', 'minimum_temporal', minimum_temporal + ) + maximum_temporal = self._handle_deprecated_kwarg( + kwargs, 'maximum_operation', 'maximum_temporal', maximum_temporal + ) + minimum_periodic = self._handle_deprecated_kwarg(kwargs, 'minimum_invest', 'minimum_periodic', minimum_periodic) + maximum_periodic = self._handle_deprecated_kwarg(kwargs, 'maximum_invest', 'maximum_periodic', maximum_periodic) + minimum_per_hour = self._handle_deprecated_kwarg( + kwargs, 'minimum_operation_per_hour', 'minimum_per_hour', minimum_per_hour + ) + maximum_per_hour = self._handle_deprecated_kwarg( + kwargs, 'maximum_operation_per_hour', 'maximum_per_hour', maximum_per_hour + ) # Validate any remaining unexpected kwargs self._validate_kwargs(kwargs) From f42b449326ec39f7a347af09faa81c1846ad1602 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 10 Oct 2025 10:08:37 +0200 Subject: [PATCH 11/13] Update usage of deprectaed parameters --- examples/02_Complex/complex_example.py | 6 +++--- tests/test_flow.py | 6 +++--- tests/test_scenarios.py | 15 +++++++++------ tests/test_storage.py | 10 +++++++--- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/examples/02_Complex/complex_example.py b/examples/02_Complex/complex_example.py index 5bfd6f37e..5c46f650e 100644 --- a/examples/02_Complex/complex_example.py +++ b/examples/02_Complex/complex_example.py @@ -57,10 +57,10 @@ label='Q_th', # Thermal output bus='Fernwärme', # Linked bus size=fx.InvestParameters( - fix_effects=1000, # Fixed investment costs + effects_of_investment=1000, # Fixed investment costs fixed_size=50, # Fixed size optional=False, # Forced investment - specific_effects={Costs.label: 10, PE.label: 2}, # Specific costs + effects_of_investment_per_size={Costs.label: 10, PE.label: 2}, # Specific costs ), load_factor_max=1.0, # Maximum load factor (50 kW) load_factor_min=0.1, # Minimum load factor (5 kW) @@ -130,7 +130,7 @@ 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( - piecewise_effects=segmented_investment_effects, # Investment effects + piecewise_effects_of_investment=segmented_investment_effects, # Investment effects optional=False, # Forced investment minimum_size=0, maximum_size=1000, # Optimizing between 0 and 1000 kWh diff --git a/tests/test_flow.py b/tests/test_flow.py index bf1723a7d..5f0343bc1 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -459,8 +459,8 @@ def test_flow_invest_with_effects(self, basic_flow_system_linopy_coords, coords_ minimum_size=20, maximum_size=100, optional=True, - fix_effects={'costs': 1000, 'CO2': 5}, # Fixed investment effects - specific_effects={'costs': 500, 'CO2': 0.1}, # Specific investment effects + effects_of_investment={'costs': 1000, 'CO2': 5}, # Fixed investment effects + effects_of_investment_per_size={'costs': 500, 'CO2': 0.1}, # Specific investment effects ), ) @@ -497,7 +497,7 @@ def test_flow_invest_divest_effects(self, basic_flow_system_linopy_coords, coord minimum_size=20, maximum_size=100, optional=True, - divest_effects={'costs': 500}, # Cost incurred when NOT investing + effects_of_retirement={'costs': 500}, # Cost incurred when NOT investing ), ) diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index 56c3169b7..b8f8bc45e 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -65,7 +65,7 @@ def test_system(): size=InvestParameters( minimum_size=0, maximum_size=20, - specific_effects={'costs': 100}, # €/kW + effects_of_investment_per_size={'costs': 100}, # €/kW ), effects_per_flow_hour={'costs': 20}, # €/MWh ) @@ -81,7 +81,7 @@ def test_system(): capacity_in_flow_hours=InvestParameters( minimum_size=0, maximum_size=50, - specific_effects={'costs': 50}, # €/kWh + effects_of_investment_per_size={'costs': 50}, # €/kWh ), eta_charge=0.95, eta_discharge=0.95, @@ -150,7 +150,10 @@ def flow_system_complex_scenarios() -> fx.FlowSystem: relative_maximum=1, previous_flow_rate=50, size=fx.InvestParameters( - fix_effects=1000, fixed_size=50, optional=False, specific_effects={'costs': 10, 'PE': 2} + effects_of_investment=1000, + fixed_size=50, + optional=False, + effects_of_investment_per_size={'costs': 10, 'PE': 2}, ), on_off_parameters=fx.OnOffParameters( on_hours_total_min=0, @@ -167,8 +170,8 @@ def flow_system_complex_scenarios() -> fx.FlowSystem: ) invest_speicher = fx.InvestParameters( - fix_effects=0, - piecewise_effects=fx.PiecewiseEffects( + effects_of_investment=0, + piecewise_effects_of_investment=fx.PiecewiseEffects( piecewise_origin=fx.Piecewise([fx.Piece(5, 25), fx.Piece(25, 100)]), piecewise_shares={ 'costs': fx.Piecewise([fx.Piece(50, 250), fx.Piece(250, 800)]), @@ -176,7 +179,7 @@ def flow_system_complex_scenarios() -> fx.FlowSystem: }, ), optional=False, - specific_effects={'costs': 0.01, 'CO2': 0.01}, + 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 2b2bc07e4..252c7d228 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -261,7 +261,11 @@ def test_storage_with_investment(self, basic_flow_system_linopy_coords, coords_c charging=fx.Flow('Q_th_in', bus='Fernwärme', size=20), discharging=fx.Flow('Q_th_out', bus='Fernwärme', size=20), capacity_in_flow_hours=fx.InvestParameters( - fix_effects=100, specific_effects=10, minimum_size=20, maximum_size=100, optional=True + effects_of_investment=100, + effects_of_investment_per_size=10, + minimum_size=20, + maximum_size=100, + optional=True, ), initial_charge_state=0, eta_charge=0.9, @@ -443,8 +447,8 @@ def test_investment_parameters( # Create investment parameters invest_params = { - 'fix_effects': 100, - 'specific_effects': 10, + 'effects_of_investment': 100, + 'effects_of_investment_per_size': 10, 'optional': optional, } if minimum_size is not None: From 2440adc61bb8b04af3e8efa03d8b54772ba9d14f Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 10 Oct 2025 10:09:33 +0200 Subject: [PATCH 12/13] Update CHANGELOG.md --- CHANGELOG.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 604769b4f..7d05e1b60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,13 +101,24 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir - The `active_timesteps` parameter of `Calculation` is deprecated and will be removed in a future version. Use the new `sel(time=...)` method on the FlowSystem instead. - The assignment of Bus Objects to Flow.bus is deprecated and will be removed in a future version. Use the label of the Bus instead. - The usage of Effects objects in Dicts to assign shares to Effects is deprecated and will be removed in a future version. Use the label of the Effect instead. -- Effect parameters renamed: +- **InvestParameters** parameters renamed for improved clarity around investment and retirement effects: + - `fix_effects` → `effects_of_investment` + - `specific_effects` → `effects_of_investment_per_size` + - `divest_effects` → `effects_of_retirement` + - `piecewise_effects` → `piecewise_effects_of_investment` +- **Effect** parameters renamed: - `minimum_investment` → `minimum_periodic` - `maximum_investment` → `maximum_periodic` - `minimum_operation` → `minimum_temporal` - `maximum_operation` → `maximum_temporal` - `minimum_operation_per_hour` → `minimum_per_hour` - `maximum_operation_per_hour` → `maximum_per_hour` +- **Component** parameters renamed: + - `Source.source` → `Source.outputs` + - `Sink.sink` → `Sink.inputs` + - `SourceAndSink.source` → `SourceAndSink.outputs` + - `SourceAndSink.sink` → `SourceAndSink.inputs` + - `SourceAndSink.prevent_simultaneous_sink_and_source` → `SourceAndSink.prevent_simultaneous_flow_rates` ### 🔥 Removed @@ -137,6 +148,7 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir ### 👷 Development +- **Centralized deprecation pattern**: Added `_handle_deprecated_kwarg()` helper method to `Interface` base class that provides reusable deprecation handling with consistent warnings, conflict detection, and optional value transformation. Applied across 5 classes (InvestParameters, Source, Sink, SourceAndSink, Effect) reducing deprecation boilerplate by 72%. - FlowSystem data management simplified - removed `time_series_collection` pattern in favor of direct timestep properties - Change modeling hierarchy to allow for more flexibility in future development. This leads to minimal changes in the access and creation of Submodels and their variables. - Added new module `.modeling` that contains Modelling primitives and utilities From ab279c59e806dcbe666d65de92a9ef4ead9b29fb Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Fri, 10 Oct 2025 10:14:21 +0200 Subject: [PATCH 13/13] Update Docs --- .../features/InvestParameters.md | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/docs/user-guide/mathematical-notation/features/InvestParameters.md b/docs/user-guide/mathematical-notation/features/InvestParameters.md index 45c338630..21d071dc8 100644 --- a/docs/user-guide/mathematical-notation/features/InvestParameters.md +++ b/docs/user-guide/mathematical-notation/features/InvestParameters.md @@ -137,24 +137,25 @@ See [Piecewise](../features/Piecewise.md) for detailed mathematical formulation. --- -### Divestment Effects +### Retirement Effects -Costs incurred if investment is NOT made: +Effects incurred if investment is NOT made (when retiring/not replacing existing equipment): -$$\label{eq:invest_divest_effects} -E_{e,\text{divest}} = (1 - s_\text{invest}) \cdot \text{divest}_e +$$\label{eq:invest_retirement_effects} +E_{e,\text{retirement}} = (1 - s_\text{invest}) \cdot \text{retirement}_e $$ With: -- $E_{e,\text{divest}}$ being the divestment contribution to effect $e$ -- $\text{divest}_e$ being the divestment effect value +- $E_{e,\text{retirement}}$ being the retirement contribution to effect $e$ +- $\text{retirement}_e$ being the retirement effect value **Behavior:** -- $s_\text{invest} = 0$: divestment effects are incurred -- $s_\text{invest} = 1$: no divestment effects +- $s_\text{invest} = 0$: retirement effects are incurred +- $s_\text{invest} = 1$: no retirement effects **Examples:** - Demolition or disposal costs +- Decommissioning expenses - Contractual penalties for not investing - Opportunity costs or lost revenues @@ -165,7 +166,7 @@ With: The total contribution to effect $e$ from an investment is: $$\label{eq:invest_total_effects} -E_{e,\text{invest}} = E_{e,\text{fix}} + E_{e,\text{spec}} + E_{e,\text{pw}} + E_{e,\text{divest}} +E_{e,\text{invest}} = E_{e,\text{fix}} + E_{e,\text{spec}} + E_{e,\text{pw}} + E_{e,\text{retirement}} $$ Effects integrate into the overall system effects as described in [Effects, Penalty & Objective](../effects-penalty-objective.md). @@ -228,10 +229,10 @@ $$ - `fixed_size`: For binary investments (mutually exclusive with continuous sizing) - `minimum_size`, `maximum_size`: For continuous sizing - `optional`: Whether investment can be skipped -- `fix_effects`: Fixed costs dictionary -- `specific_effects`: Per-unit costs dictionary -- `piecewise_effects`: Non-linear cost modeling -- `divest_effects`: Costs for not investing +- `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`) +- `effects_of_retirement`: Effects for not investing (replaces deprecated `divest_effects`) See the [`InvestParameters`][flixopt.interface.InvestParameters] API documentation for complete parameter list and usage examples. @@ -250,8 +251,8 @@ See the [`InvestParameters`][flixopt.interface.InvestParameters] API documentati solar_investment = InvestParameters( fixed_size=100, # 100 kW system optional=True, - fix_effects={'cost': 25000}, # Installation costs - specific_effects={'cost': 1200}, # €1200/kW + effects_of_investment={'cost': 25000}, # Installation costs + effects_of_investment_per_size={'cost': 1200}, # €1200/kW ) ``` @@ -261,20 +262,20 @@ battery_investment = InvestParameters( minimum_size=10, # kWh maximum_size=1000, optional=True, - fix_effects={'cost': 5000}, # Grid connection - specific_effects={'cost': 600}, # €600/kWh + effects_of_investment={'cost': 5000}, # Grid connection + effects_of_investment_per_size={'cost': 600}, # €600/kWh ) ``` -### With Divestment Costs (Replacement) +### With Retirement Costs (Replacement) ```python boiler_replacement = InvestParameters( minimum_size=50, # kW maximum_size=200, optional=True, - fix_effects={'cost': 15000}, - specific_effects={'cost': 400}, - divest_effects={'cost': 8000}, # Demolition if not replaced + effects_of_investment={'cost': 15000}, + effects_of_investment_per_size={'cost': 400}, + effects_of_retirement={'cost': 8000}, # Demolition if not replaced ) ``` @@ -283,7 +284,7 @@ boiler_replacement = InvestParameters( battery_investment = InvestParameters( minimum_size=10, maximum_size=1000, - piecewise_effects=PiecewiseEffects( + piecewise_effects_of_investment=PiecewiseEffects( piecewise_origin=Piecewise([ Piece(0, 100), # Small Piece(100, 500), # Medium