diff --git a/flixopt/elements.py b/flixopt/elements.py index 22256b636..f9dcf0cee 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -486,11 +486,6 @@ def size_is_fixed(self) -> bool: # Wenn kein InvestParameters existiert --> True; Wenn Investparameter, den Wert davon nehmen return False if (isinstance(self.size, InvestParameters) and self.size.fixed_size is None) else True - @property - def invest_is_optional(self) -> bool: - # Wenn kein InvestParameters existiert: # Investment ist nicht optional -> Keine Variable --> False - return False if (isinstance(self.size, InvestParameters) and not self.size.optional) else True - class FlowModel(ElementModel): def __init__(self, model: SystemModel, element: Flow): @@ -650,7 +645,7 @@ def flow_rate_lower_bound(self) -> NumericData: if self.element.on_off_parameters is not None: return 0 if isinstance(self.element.size, InvestParameters): - if self.element.size.optional: + if not self.element.size.mandatory: return 0 return self.flow_rate_lower_bound_relative * self.element.size.minimum_size return self.flow_rate_lower_bound_relative * self.element.size diff --git a/flixopt/features.py b/flixopt/features.py index 5528917e0..985250a51 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -46,7 +46,7 @@ def __init__( self.parameters = parameters def do_modeling(self): - if self.parameters.fixed_size and not self.parameters.optional: + if self.parameters.fixed_size and self.parameters.mandatory: self.size = self.add( self._model.add_variables( lower=self.parameters.fixed_size, upper=self.parameters.fixed_size, name=f'{self.label_full}|size' @@ -56,15 +56,15 @@ def do_modeling(self): else: self.size = self.add( self._model.add_variables( - lower=0 if self.parameters.optional else self.parameters.minimum_size, + lower=0 if not self.parameters.mandatory else self.parameters.minimum_size, upper=self.parameters.maximum_size, name=f'{self.label_full}|size', ), 'size', ) - # Optional - if self.parameters.optional: + # Optional (not mandatory) + if not self.parameters.mandatory: self.is_invested = self.add( self._model.add_variables(binary=True, name=f'{self.label_full}|is_invested'), 'is_invested' ) @@ -89,7 +89,7 @@ def _create_shares(self): target='invest', ) - if self.parameters.divest_effects != {} and self.parameters.optional: + if self.parameters.divest_effects != {} and not self.parameters.mandatory: # share: divest_effects - isInvested * divest_effects self._model.effects.add_share_to_effects( name=self.label_of_element, diff --git a/flixopt/interface.py b/flixopt/interface.py index e72e28b90..aa113acc3 100644 --- a/flixopt/interface.py +++ b/flixopt/interface.py @@ -6,6 +6,7 @@ from __future__ import annotations import logging +import warnings from typing import TYPE_CHECKING from .config import CONFIG @@ -655,9 +656,10 @@ class InvestParameters(Interface): maximum_size: Upper bound for continuous sizing decisions. Defaults to a large value (CONFIG.modeling.BIG) representing unlimited capacity. Ignored when fixed_size is specified. - optional: Controls whether investment is required. When True (default), - optimization can choose not to invest. When False, forces investment + mandatory: Controls whether investment is required. When True, forces investment to occur (useful for mandatory upgrades or replacement decisions). + When False (default), optimization can choose not to invest. + optional: DEPRECATED. Use `mandatory` instead. Opposite of `mandatory`. fix_effects: Fixed costs incurred once if investment is made, regardless of size. Dictionary mapping effect names to values (e.g., {'cost': 10000, 'CO2_construction': 500}). @@ -687,7 +689,7 @@ class InvestParameters(Interface): ```python solar_investment = InvestParameters( fixed_size=100, # 100 kW system (binary decision) - optional=True, + mandatory=False, # Investment is optional fix_effects={ 'cost': 25000, # Installation and permitting costs 'CO2': -50000, # Avoided emissions over lifetime @@ -705,7 +707,7 @@ class InvestParameters(Interface): battery_investment = InvestParameters( minimum_size=10, # Minimum viable system size (kWh) maximum_size=1000, # Maximum installable capacity - optional=True, + mandatory=False, # Investment is optional fix_effects={ 'cost': 5000, # Grid connection and control system 'installation_time': 2, # Days for fixed components @@ -737,7 +739,7 @@ class InvestParameters(Interface): boiler_replacement = InvestParameters( minimum_size=50, maximum_size=200, - optional=True, # Can choose not to replace + mandatory=False, # Can choose not to replace fix_effects={ 'cost': 15000, # Installation costs 'disruption': 3, # Days of downtime @@ -821,26 +823,46 @@ def __init__( fixed_size: int | float | None = None, minimum_size: int | float | None = None, maximum_size: int | float | None = None, - optional: bool = True, # Investition ist weglassbar + mandatory: bool = False, fix_effects: EffectValuesUserScalar | None = None, specific_effects: EffectValuesUserScalar | None = None, # costs per Flow-Unit/Storage-Size/... piecewise_effects: PiecewiseEffects | None = None, divest_effects: EffectValuesUserScalar | None = None, + # Backwards compatibility - deprecated parameter + optional: bool | None = None, ): self.fix_effects: EffectValuesUserScalar = fix_effects or {} self.divest_effects: EffectValuesUserScalar = divest_effects or {} self.fixed_size = fixed_size - self.optional = optional self.specific_effects: EffectValuesUserScalar = specific_effects or {} self.piecewise_effects = piecewise_effects self._minimum_size = minimum_size if minimum_size is not None else CONFIG.modeling.EPSILON self._maximum_size = maximum_size if maximum_size is not None else CONFIG.modeling.BIG # default maximum + self.mandatory = mandatory + + # Handle backwards compatibility for optional parameter + if optional is not None: + self.optional = optional def transform_data(self, flow_system: FlowSystem): self.fix_effects = flow_system.effects.create_effect_values_dict(self.fix_effects) self.divest_effects = flow_system.effects.create_effect_values_dict(self.divest_effects) self.specific_effects = flow_system.effects.create_effect_values_dict(self.specific_effects) + @property + def 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 minimum_size(self): return self.fixed_size or self._minimum_size