diff --git a/CHANGELOG.md b/CHANGELOG.md index 46ab6270c..8c2520589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,13 @@ Please keep the format of the changelog consistent with the other releases, so t ### ♻️ Changed ### 🗑️ Deprecated +- Renamed `Effect` parameters: + - `minimum_investment` → `minimum_nontemporal` + - `maximum_investment` → `maximum_nontemporal` + - `minimum_operation` → `minimum_temporal` + - `maximum_operation` → `maximum_temporal` + - `minimum_operation_per_hour` → `minimum_per_hour` + - `maximum_operation_per_hour` → `maximum_per_hour` ### 🔥 Removed diff --git a/examples/03_Calculation_types/example_calculation_types.py b/examples/03_Calculation_types/example_calculation_types.py index a92a20163..3413ddc09 100644 --- a/examples/03_Calculation_types/example_calculation_types.py +++ b/examples/03_Calculation_types/example_calculation_types.py @@ -204,14 +204,14 @@ def get_solutions(calcs: list, variable: str) -> xr.Dataset: ).write_html('results/BHKW2 Thermal Power.html') fx.plotting.with_plotly( - get_solutions(calculations, 'costs(operation)|total_per_timestep').to_dataframe(), + get_solutions(calculations, 'costs(temporal)|per_timestep').to_dataframe(), mode='line', title='Operation Cost Comparison', ylabel='Costs [€]', ).write_html('results/Operation Costs.html') fx.plotting.with_plotly( - pd.DataFrame(get_solutions(calculations, 'costs(operation)|total_per_timestep').to_dataframe().sum()).T, + pd.DataFrame(get_solutions(calculations, 'costs(temporal)|per_timestep').to_dataframe().sum()).T, mode='bar', title='Total Cost Comparison', ylabel='Costs [€]', diff --git a/flixopt/calculation.py b/flixopt/calculation.py index c912b083b..6ab4c7cbd 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -80,8 +80,8 @@ def main_results(self) -> dict[str, Scalar | dict]: 'Penalty': float(self.model.effects.penalty.total.solution.values), 'Effects': { f'{effect.label} [{effect.unit}]': { - 'operation': float(effect.model.operation.total.solution.values), - 'invest': float(effect.model.invest.total.solution.values), + 'temporal': float(effect.model.temporal.total.solution.values), + 'nontemporal': float(effect.model.nontemporal.total.solution.values), 'total': float(effect.model.total.solution.values), } for effect in self.flow_system.effects diff --git a/flixopt/components.py b/flixopt/components.py index 9dd0fc52b..2f60f2759 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -975,7 +975,7 @@ def __init__( 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.', + 'The use of the "source" argument is deprecated. Use the "outputs" argument instead.', DeprecationWarning, stacklevel=2, ) @@ -985,7 +985,7 @@ def __init__( if sink is not None: warnings.warn( - 'The use of the sink argument is deprecated. Use the inputs argument instead.', + 'The use of the "sink" argument is deprecated. Use the "inputs" argument instead.', DeprecationWarning, stacklevel=2, ) @@ -995,12 +995,15 @@ def __init__( 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.', + '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 + # Validate any remaining unexpected kwargs + self._validate_kwargs(kwargs) + super().__init__( label, inputs=inputs, @@ -1125,7 +1128,7 @@ def __init__( 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.', + 'The use of the "source" argument is deprecated. Use the "outputs" argument instead.', DeprecationWarning, stacklevel=2, ) @@ -1133,6 +1136,9 @@ def __init__( raise ValueError('Either source or outputs can be specified, but not both.') outputs = [source] + # Validate any remaining unexpected kwargs + self._validate_kwargs(kwargs) + self.prevent_simultaneous_flow_rates = prevent_simultaneous_flow_rates super().__init__( label, @@ -1253,7 +1259,7 @@ def __init__( 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.', + 'The use of the "sink" argument is deprecated. Use the "inputs" argument instead.', DeprecationWarning, stacklevel=2, ) @@ -1261,6 +1267,9 @@ def __init__( raise ValueError('Either sink or inputs can be specified, but not both.') inputs = [sink] + # Validate any remaining unexpected kwargs + self._validate_kwargs(kwargs) + self.prevent_simultaneous_flow_rates = prevent_simultaneous_flow_rates super().__init__( label, diff --git a/flixopt/effects.py b/flixopt/effects.py index 31c941e11..ce6145401 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -52,17 +52,25 @@ class Effect(Element): Maps this effect's operational values to contributions to other effects specific_share_to_other_effects_invest: Investment cross-effect contributions. Maps this effect's investment values to contributions to other effects. - minimum_operation: Minimum allowed total operational contribution across all timesteps. - maximum_operation: Maximum allowed total operational contribution across all timesteps. - minimum_operation_per_hour: Minimum allowed operational contribution per timestep. - maximum_operation_per_hour: Maximum allowed operational contribution per timestep. - minimum_invest: Minimum allowed total investment contribution. - maximum_invest: Maximum allowed total investment contribution. - minimum_total: Minimum allowed total effect (operation + investment combined). - maximum_total: Maximum allowed total effect (operation + investment combined). + minimum_temporal: Minimum allowed total contribution across all timesteps. + maximum_temporal: Maximum allowed total contribution across all timesteps. + minimum_per_hour: Minimum allowed contribution per hour. + maximum_per_hour: Maximum allowed contribution per hour. + minimum_nontemporal: Minimum allowed total nontemporal contribution. + maximum_nontemporal: Maximum allowed total nontemporal contribution. + minimum_total: Minimum allowed total effect (temporal + nontemporal combined). + maximum_total: Maximum allowed total effect (temporal + nontemporal combined). meta_data: Used to store additional information. Not used internally but saved in results. Only use Python native types. + **Deprecated Parameters** (for backwards compatibility): + minimum_operation: Use `minimum_temporal` instead. + maximum_operation: Use `maximum_temporal` instead. + minimum_invest: Use `minimum_nontemporal` instead. + maximum_invest: Use `maximum_nontemporal` instead. + minimum_operation_per_hour: Use `minimum_per_hour` instead. + maximum_operation_per_hour: Use `maximum_per_hour` instead. + Examples: Basic cost objective: @@ -111,8 +119,8 @@ class Effect(Element): label='water_consumption', unit='m³', description='Industrial water usage', - minimum_operation_per_hour=10, # Minimum 10 m³/h for process stability - maximum_operation_per_hour=500, # Maximum 500 m³/h capacity limit + minimum_per_hour=10, # Minimum 10 m³/h for process stability + maximum_per_hour=500, # Maximum 500 m³/h capacity limit maximum_total=100_000, # Annual permit limit: 100,000 m³ ) ``` @@ -142,14 +150,15 @@ def __init__( is_objective: bool = False, specific_share_to_other_effects_operation: EffectValuesUser | None = None, specific_share_to_other_effects_invest: EffectValuesUser | None = None, - minimum_operation: Scalar | None = None, - maximum_operation: Scalar | None = None, - minimum_invest: Scalar | None = None, - maximum_invest: Scalar | None = None, - minimum_operation_per_hour: NumericDataTS | None = None, - maximum_operation_per_hour: NumericDataTS | None = None, + minimum_temporal: Scalar | None = None, + maximum_temporal: Scalar | None = None, + minimum_nontemporal: Scalar | None = None, + maximum_nontemporal: Scalar | None = None, + minimum_per_hour: NumericDataTS | None = None, + maximum_per_hour: NumericDataTS | None = None, minimum_total: Scalar | None = None, maximum_total: Scalar | None = None, + **kwargs, ): super().__init__(label, meta_data=meta_data) self.label = label @@ -161,22 +170,227 @@ def __init__( specific_share_to_other_effects_operation or {} ) self.specific_share_to_other_effects_invest: EffectValuesUser = specific_share_to_other_effects_invest or {} - self.minimum_operation = minimum_operation - self.maximum_operation = maximum_operation - self.minimum_operation_per_hour = minimum_operation_per_hour - self.maximum_operation_per_hour = maximum_operation_per_hour - self.minimum_invest = minimum_invest - self.maximum_invest = maximum_invest + + # 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_nontemporal + if minimum_invest is not None: + warnings.warn( + "Parameter 'minimum_invest' is deprecated. Use 'minimum_nontemporal' instead.", + DeprecationWarning, + stacklevel=2, + ) + if minimum_nontemporal is not None: + raise ValueError('Either minimum_invest or minimum_nontemporal can be specified, but not both.') + minimum_nontemporal = minimum_invest + + # Handle maximum_nontemporal + if maximum_invest is not None: + warnings.warn( + "Parameter 'maximum_invest' is deprecated. Use 'maximum_nontemporal' instead.", + DeprecationWarning, + stacklevel=2, + ) + if maximum_nontemporal is not None: + raise ValueError('Either maximum_invest or maximum_nontemporal can be specified, but not both.') + maximum_nontemporal = 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 + + # Validate any remaining unexpected kwargs + self._validate_kwargs(kwargs) + + # Set attributes directly + self.minimum_temporal = minimum_temporal + self.maximum_temporal = maximum_temporal + self.minimum_nontemporal = minimum_nontemporal + self.maximum_nontemporal = maximum_nontemporal + self.minimum_per_hour = minimum_per_hour + self.maximum_per_hour = maximum_per_hour self.minimum_total = minimum_total self.maximum_total = maximum_total + # Backwards compatible properties (deprecated) + @property + def minimum_operation(self): + """DEPRECATED: Use 'minimum_temporal' property instead.""" + warnings.warn( + "Property 'minimum_operation' is deprecated. Use 'minimum_temporal' instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.minimum_temporal + + @minimum_operation.setter + def minimum_operation(self, value): + """DEPRECATED: Use 'minimum_temporal' property instead.""" + warnings.warn( + "Property 'minimum_operation' is deprecated. Use 'minimum_temporal' instead.", + DeprecationWarning, + stacklevel=2, + ) + self.minimum_temporal = value + + @property + def maximum_operation(self): + """DEPRECATED: Use 'maximum_temporal' property instead.""" + warnings.warn( + "Property 'maximum_operation' is deprecated. Use 'maximum_temporal' instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.maximum_temporal + + @maximum_operation.setter + def maximum_operation(self, value): + """DEPRECATED: Use 'maximum_temporal' property instead.""" + warnings.warn( + "Property 'maximum_operation' is deprecated. Use 'maximum_temporal' instead.", + DeprecationWarning, + stacklevel=2, + ) + self.maximum_temporal = value + + @property + def minimum_invest(self): + """DEPRECATED: Use 'minimum_nontemporal' property instead.""" + warnings.warn( + "Property 'minimum_invest' is deprecated. Use 'minimum_nontemporal' instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.minimum_nontemporal + + @minimum_invest.setter + def minimum_invest(self, value): + """DEPRECATED: Use 'minimum_nontemporal' property instead.""" + warnings.warn( + "Property 'minimum_invest' is deprecated. Use 'minimum_nontemporal' instead.", + DeprecationWarning, + stacklevel=2, + ) + self.minimum_nontemporal = value + + @property + def maximum_invest(self): + """DEPRECATED: Use 'maximum_nontemporal' property instead.""" + warnings.warn( + "Property 'maximum_invest' is deprecated. Use 'maximum_nontemporal' instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.maximum_nontemporal + + @maximum_invest.setter + def maximum_invest(self, value): + """DEPRECATED: Use 'maximum_nontemporal' property instead.""" + warnings.warn( + "Property 'maximum_invest' is deprecated. Use 'maximum_nontemporal' instead.", + DeprecationWarning, + stacklevel=2, + ) + self.maximum_nontemporal = value + + @property + def minimum_operation_per_hour(self): + """DEPRECATED: Use 'minimum_per_hour' property instead.""" + warnings.warn( + "Property 'minimum_operation_per_hour' is deprecated. Use 'minimum_per_hour' instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.minimum_per_hour + + @minimum_operation_per_hour.setter + def minimum_operation_per_hour(self, value): + """DEPRECATED: Use 'minimum_per_hour' property instead.""" + warnings.warn( + "Property 'minimum_operation_per_hour' is deprecated. Use 'minimum_per_hour' instead.", + DeprecationWarning, + stacklevel=2, + ) + self.minimum_per_hour = value + + @property + def maximum_operation_per_hour(self): + """DEPRECATED: Use 'maximum_per_hour' property instead.""" + warnings.warn( + "Property 'maximum_operation_per_hour' is deprecated. Use 'maximum_per_hour' instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.maximum_per_hour + + @maximum_operation_per_hour.setter + def maximum_operation_per_hour(self, value): + """DEPRECATED: Use 'maximum_per_hour' property instead.""" + warnings.warn( + "Property 'maximum_operation_per_hour' is deprecated. Use 'maximum_per_hour' instead.", + DeprecationWarning, + stacklevel=2, + ) + self.maximum_per_hour = value + def transform_data(self, flow_system: FlowSystem): - self.minimum_operation_per_hour = flow_system.create_time_series( - f'{self.label_full}|minimum_operation_per_hour', self.minimum_operation_per_hour + self.minimum_per_hour = flow_system.create_time_series( + f'{self.label_full}|minimum_per_hour', self.minimum_per_hour ) - self.maximum_operation_per_hour = flow_system.create_time_series( - f'{self.label_full}|maximum_operation_per_hour', - self.maximum_operation_per_hour, + self.maximum_per_hour = flow_system.create_time_series( + f'{self.label_full}|maximum_per_hour', + self.maximum_per_hour, ) self.specific_share_to_other_effects_operation = flow_system.create_effect_time_series( @@ -198,32 +412,32 @@ def __init__(self, model: SystemModel, element: Effect): super().__init__(model, element) self.element: Effect = element self.total: linopy.Variable | None = None - self.invest: ShareAllocationModel = self.add( + self.nontemporal: ShareAllocationModel = self.add( ShareAllocationModel( self._model, False, self.label_of_element, - 'invest', - label_full=f'{self.label_full}(invest)', - total_max=self.element.maximum_invest, - total_min=self.element.minimum_invest, + 'nontemporal', + label_full=f'{self.label_full}(nontemporal)', + total_max=self.element.maximum_nontemporal, + total_min=self.element.minimum_nontemporal, ) ) - self.operation: ShareAllocationModel = self.add( + self.temporal: ShareAllocationModel = self.add( ShareAllocationModel( self._model, True, self.label_of_element, - 'operation', - label_full=f'{self.label_full}(operation)', - total_max=self.element.maximum_operation, - total_min=self.element.minimum_operation, - min_per_hour=self.element.minimum_operation_per_hour.active_data - if self.element.minimum_operation_per_hour is not None + 'temporal', + label_full=f'{self.label_full}(temporal)', + total_max=self.element.maximum_temporal, + total_min=self.element.minimum_temporal, + min_per_hour=self.element.minimum_per_hour.active_data + if self.element.minimum_per_hour is not None else None, - max_per_hour=self.element.maximum_operation_per_hour.active_data - if self.element.maximum_operation_per_hour is not None + max_per_hour=self.element.maximum_per_hour.active_data + if self.element.maximum_per_hour is not None else None, ) ) @@ -237,14 +451,14 @@ def do_modeling(self): lower=self.element.minimum_total if self.element.minimum_total is not None else -np.inf, upper=self.element.maximum_total if self.element.maximum_total is not None else np.inf, coords=None, - name=f'{self.label_full}|total', + name=f'{self.label_full}', ), 'total', ) self.add( self._model.add_constraints( - self.total == self.operation.total.sum() + self.invest.total.sum(), name=f'{self.label_full}|total' + self.total == self.temporal.total.sum() + self.nontemporal.total.sum(), name=f'{self.label_full}' ), 'total', ) @@ -424,13 +638,13 @@ def add_share_to_effects( self, name: str, expressions: EffectValuesExpr, - target: Literal['operation', 'invest'], + target: Literal['temporal', 'nontemporal'], ) -> None: for effect, expression in expressions.items(): - if target == 'operation': - self.effects[effect].model.operation.add_share(name, expression) - elif target == 'invest': - self.effects[effect].model.invest.add_share(name, expression) + if target == 'temporal': + self.effects[effect].model.temporal.add_share(name, expression) + elif target == 'nontemporal': + self.effects[effect].model.nontemporal.add_share(name, expression) else: raise ValueError(f'Target {target} not supported!') @@ -454,15 +668,15 @@ def do_modeling(self): def _add_share_between_effects(self): for origin_effect in self.effects: - # 1. operation: -> hier sind es Zeitreihen (share_TS) + # 1. temporal: -> hier sind es Zeitreihen (share_TS) for target_effect, time_series in origin_effect.specific_share_to_other_effects_operation.items(): - self.effects[target_effect].model.operation.add_share( - origin_effect.model.operation.label_full, - origin_effect.model.operation.total_per_timestep * time_series.active_data, + self.effects[target_effect].model.temporal.add_share( + origin_effect.model.temporal.label_full, + origin_effect.model.temporal.total_per_timestep * time_series.active_data, ) - # 2. invest: -> hier ist es Scalar (share) + # 2. nontemporal: -> hier ist es Scalar (share) for target_effect, factor in origin_effect.specific_share_to_other_effects_invest.items(): - self.effects[target_effect].model.invest.add_share( - origin_effect.model.invest.label_full, - origin_effect.model.invest.total * factor, + self.effects[target_effect].model.nontemporal.add_share( + origin_effect.model.nontemporal.label_full, + origin_effect.model.nontemporal.total * factor, ) diff --git a/flixopt/elements.py b/flixopt/elements.py index 22256b636..8a611109d 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -580,7 +580,7 @@ def _create_shares(self): effect: self.flow_rate * self._model.hours_per_step * factor.active_data for effect, factor in self.element.effects_per_flow_hour.items() }, - target='operation', + target='temporal', ) def _create_bounds_for_load_factor(self): diff --git a/flixopt/features.py b/flixopt/features.py index 5528917e0..0c22e739f 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -86,7 +86,7 @@ def _create_shares(self): effect: self.is_invested * factor if self.is_invested is not None else factor for effect, factor in fix_effects.items() }, - target='invest', + target='nontemporal', ) if self.parameters.divest_effects != {} and self.parameters.optional: @@ -97,14 +97,14 @@ def _create_shares(self): effect: -self.is_invested * factor + factor for effect, factor in self.parameters.divest_effects.items() }, - target='invest', + target='nontemporal', ) if self.parameters.specific_effects != {}: 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()}, - target='invest', + target='nontemporal', ) if self.parameters.piecewise_effects: @@ -736,7 +736,7 @@ def _create_shares(self): effect: self.state_model.on * factor * self._model.hours_per_step for effect, factor in self.parameters.effects_per_running_hour.items() }, - target='operation', + target='temporal', ) if self.parameters.effects_per_switch_on: @@ -746,7 +746,7 @@ def _create_shares(self): effect: self.switch_state_model.switch_on * factor for effect, factor in self.parameters.effects_per_switch_on.items() }, - target='operation', + target='temporal', ) @property @@ -956,14 +956,12 @@ def __init__( def do_modeling(self): self.total = self.add( self._model.add_variables( - lower=self._total_min, upper=self._total_max, coords=None, name=f'{self.label_full}|total' + lower=self._total_min, upper=self._total_max, coords=None, name=f'{self.label_full}' ), 'total', ) # eq: sum = sum(share_i) # skalar - self._eq_total = self.add( - self._model.add_constraints(self.total == 0, name=f'{self.label_full}|total'), 'total' - ) + self._eq_total = self.add(self._model.add_constraints(self.total == 0, name=f'{self.label_full}')) if self._shares_are_time_series: self.total_per_timestep = self.add( @@ -975,14 +973,14 @@ def do_modeling(self): if (self._max_per_hour is None) else np.multiply(self._max_per_hour, self._model.hours_per_step), coords=self._model.coords, - name=f'{self.label_full}|total_per_timestep', + name=f'{self.label_full}|per_timestep', ), - 'total_per_timestep', + 'per_timestep', ) self._eq_total_per_timestep = self.add( - self._model.add_constraints(self.total_per_timestep == 0, name=f'{self.label_full}|total_per_timestep'), - 'total_per_timestep', + self._model.add_constraints(self.total_per_timestep == 0, name=f'{self.label_full}|per_timestep'), + 'per_timestep', ) # Add it to the total @@ -1078,7 +1076,7 @@ def do_modeling(self): self._model.effects.add_share_to_effects( name=self.label_of_element, expressions={effect: variable * 1 for effect, variable in self.shares.items()}, - target='invest', + target='nontemporal', ) diff --git a/flixopt/structure.py b/flixopt/structure.py index c5519066c..12cd99c13 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -202,6 +202,36 @@ def _serialize_dict(self, d): """Serialize a dictionary of items.""" return {k: self._serialize_value(v) for k, v in d.items()} + def _validate_kwargs(self, kwargs: dict, class_name: str = None) -> None: + """ + Validate that no unexpected keyword arguments are present in kwargs. + + This method uses inspect to get the actual function signature and filters out + any parameters that are not defined in the __init__ method, while also + handling the special case of 'kwargs' itself which can appear during deserialization. + + Args: + kwargs: Dictionary of keyword arguments to validate + class_name: Optional class name for error messages. If None, uses self.__class__.__name__ + + Raises: + TypeError: If unexpected keyword arguments are found + """ + if not kwargs: + return + + import inspect + + sig = inspect.signature(self.__init__) + known_params = set(sig.parameters.keys()) - {'self', 'kwargs'} + # Also filter out 'kwargs' itself which can appear during deserialization + extra_kwargs = {k: v for k, v in kwargs.items() if k not in known_params and k != 'kwargs'} + + if extra_kwargs: + class_name = class_name or self.__class__.__name__ + unexpected_params = ', '.join(f"'{param}'" for param in extra_kwargs.keys()) + raise TypeError(f'{class_name}.__init__() got unexpected keyword argument(s): {unexpected_params}') + @classmethod def _deserialize_dict(cls, data: dict) -> dict | Interface: if '__class__' in data: diff --git a/tests/conftest.py b/tests/conftest.py index ac2bab5f4..a0b196e4d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -62,7 +62,7 @@ def simple_flow_system() -> fx.FlowSystem: 'kg', 'CO2_e-Emissionen', specific_share_to_other_effects_operation={costs.label: 0.2}, - maximum_operation_per_hour=1000, + maximum_per_hour=1000, ) # Create components diff --git a/tests/test_effect.py b/tests/test_effect.py index b4a618ea6..b5fec64dd 100644 --- a/tests/test_effect.py +++ b/tests/test_effect.py @@ -15,39 +15,36 @@ def test_minimal(self, basic_flow_system_linopy): model = create_linopy_model(flow_system) assert set(effect.model.variables) == { - 'Effect1(invest)|total', - 'Effect1(operation)|total', - 'Effect1(operation)|total_per_timestep', - 'Effect1|total', + 'Effect1(nontemporal)', + 'Effect1(temporal)', + 'Effect1(temporal)|per_timestep', + 'Effect1', } assert set(effect.model.constraints) == { - 'Effect1(invest)|total', - 'Effect1(operation)|total', - 'Effect1(operation)|total_per_timestep', - 'Effect1|total', + 'Effect1(nontemporal)', + 'Effect1(temporal)', + 'Effect1(temporal)|per_timestep', + 'Effect1', } - assert_var_equal(model.variables['Effect1|total'], model.add_variables()) - assert_var_equal(model.variables['Effect1(invest)|total'], model.add_variables()) - assert_var_equal(model.variables['Effect1(operation)|total'], model.add_variables()) - assert_var_equal( - model.variables['Effect1(operation)|total_per_timestep'], model.add_variables(coords=(timesteps,)) - ) + assert_var_equal(model.variables['Effect1'], model.add_variables()) + assert_var_equal(model.variables['Effect1(nontemporal)'], model.add_variables()) + assert_var_equal(model.variables['Effect1(temporal)'], model.add_variables()) + assert_var_equal(model.variables['Effect1(temporal)|per_timestep'], model.add_variables(coords=(timesteps,))) assert_conequal( - model.constraints['Effect1|total'], - model.variables['Effect1|total'] - == model.variables['Effect1(operation)|total'] + model.variables['Effect1(invest)|total'], + model.constraints['Effect1'], + model.variables['Effect1'] + == model.variables['Effect1(temporal)'] + model.variables['Effect1(nontemporal)'], ) - assert_conequal(model.constraints['Effect1(invest)|total'], model.variables['Effect1(invest)|total'] == 0) + assert_conequal(model.constraints['Effect1(nontemporal)'], model.variables['Effect1(nontemporal)'] == 0) assert_conequal( - model.constraints['Effect1(operation)|total'], - model.variables['Effect1(operation)|total'] - == model.variables['Effect1(operation)|total_per_timestep'].sum(), + model.constraints['Effect1(temporal)'], + model.variables['Effect1(temporal)'] == model.variables['Effect1(temporal)|per_timestep'].sum(), ) assert_conequal( - model.constraints['Effect1(operation)|total_per_timestep'], - model.variables['Effect1(operation)|total_per_timestep'] == 0, + model.constraints['Effect1(temporal)|per_timestep'], + model.variables['Effect1(temporal)|per_timestep'] == 0, ) def test_bounds(self, basic_flow_system_linopy): @@ -57,56 +54,55 @@ def test_bounds(self, basic_flow_system_linopy): 'Effect1', '€', 'Testing Effect', - minimum_operation=1.0, - maximum_operation=1.1, - minimum_invest=2.0, - maximum_invest=2.1, + minimum_temporal=1.0, + maximum_temporal=1.1, + minimum_nontemporal=2.0, + maximum_nontemporal=2.1, minimum_total=3.0, maximum_total=3.1, - minimum_operation_per_hour=4.0, - maximum_operation_per_hour=4.1, + minimum_per_hour=4.0, + maximum_per_hour=4.1, ) flow_system.add_elements(effect) model = create_linopy_model(flow_system) assert set(effect.model.variables) == { - 'Effect1(invest)|total', - 'Effect1(operation)|total', - 'Effect1(operation)|total_per_timestep', - 'Effect1|total', + 'Effect1(nontemporal)', + 'Effect1(temporal)', + 'Effect1(temporal)|per_timestep', + 'Effect1', } assert set(effect.model.constraints) == { - 'Effect1(invest)|total', - 'Effect1(operation)|total', - 'Effect1(operation)|total_per_timestep', - 'Effect1|total', + 'Effect1(nontemporal)', + 'Effect1(temporal)', + 'Effect1(temporal)|per_timestep', + 'Effect1', } - assert_var_equal(model.variables['Effect1|total'], model.add_variables(lower=3.0, upper=3.1)) - assert_var_equal(model.variables['Effect1(invest)|total'], model.add_variables(lower=2.0, upper=2.1)) - assert_var_equal(model.variables['Effect1(operation)|total'], model.add_variables(lower=1.0, upper=1.1)) + assert_var_equal(model.variables['Effect1'], model.add_variables(lower=3.0, upper=3.1)) + assert_var_equal(model.variables['Effect1(nontemporal)'], model.add_variables(lower=2.0, upper=2.1)) + assert_var_equal(model.variables['Effect1(temporal)'], model.add_variables(lower=1.0, upper=1.1)) assert_var_equal( - model.variables['Effect1(operation)|total_per_timestep'], + model.variables['Effect1(temporal)|per_timestep'], model.add_variables( lower=4.0 * model.hours_per_step, upper=4.1 * model.hours_per_step, coords=(timesteps,) ), ) assert_conequal( - model.constraints['Effect1|total'], - model.variables['Effect1|total'] - == model.variables['Effect1(operation)|total'] + model.variables['Effect1(invest)|total'], + model.constraints['Effect1'], + model.variables['Effect1'] + == model.variables['Effect1(temporal)'] + model.variables['Effect1(nontemporal)'], ) - assert_conequal(model.constraints['Effect1(invest)|total'], model.variables['Effect1(invest)|total'] == 0) + assert_conequal(model.constraints['Effect1(nontemporal)'], model.variables['Effect1(nontemporal)'] == 0) assert_conequal( - model.constraints['Effect1(operation)|total'], - model.variables['Effect1(operation)|total'] - == model.variables['Effect1(operation)|total_per_timestep'].sum(), + model.constraints['Effect1(temporal)'], + model.variables['Effect1(temporal)'] == model.variables['Effect1(temporal)|per_timestep'].sum(), ) assert_conequal( - model.constraints['Effect1(operation)|total_per_timestep'], - model.variables['Effect1(operation)|total_per_timestep'] == 0, + model.constraints['Effect1(temporal)|per_timestep'], + model.variables['Effect1(temporal)|per_timestep'] == 0, ) def test_shares(self, basic_flow_system_linopy): @@ -124,40 +120,41 @@ def test_shares(self, basic_flow_system_linopy): model = create_linopy_model(flow_system) assert set(effect2.model.variables) == { - 'Effect2(invest)|total', - 'Effect2(operation)|total', - 'Effect2(operation)|total_per_timestep', - 'Effect2|total', - 'Effect1(invest)->Effect2(invest)', - 'Effect1(operation)->Effect2(operation)', + 'Effect2(nontemporal)', + 'Effect2(temporal)', + 'Effect2(temporal)|per_timestep', + 'Effect2', + 'Effect1(nontemporal)->Effect2(nontemporal)', + 'Effect1(temporal)->Effect2(temporal)', } assert set(effect2.model.constraints) == { - 'Effect2(invest)|total', - 'Effect2(operation)|total', - 'Effect2(operation)|total_per_timestep', - 'Effect2|total', - 'Effect1(invest)->Effect2(invest)', - 'Effect1(operation)->Effect2(operation)', + 'Effect2(nontemporal)', + 'Effect2(temporal)', + 'Effect2(temporal)|per_timestep', + 'Effect2', + 'Effect1(nontemporal)->Effect2(nontemporal)', + 'Effect1(temporal)->Effect2(temporal)', } assert_conequal( - model.constraints['Effect2(invest)|total'], - model.variables['Effect2(invest)|total'] == model.variables['Effect1(invest)->Effect2(invest)'], + model.constraints['Effect2(nontemporal)'], + model.variables['Effect2(nontemporal)'] == model.variables['Effect1(nontemporal)->Effect2(nontemporal)'], ) assert_conequal( - model.constraints['Effect2(operation)|total_per_timestep'], - model.variables['Effect2(operation)|total_per_timestep'] - == model.variables['Effect1(operation)->Effect2(operation)'], + model.constraints['Effect2(temporal)|per_timestep'], + model.variables['Effect2(temporal)|per_timestep'] + == model.variables['Effect1(temporal)->Effect2(temporal)'], ) assert_conequal( - model.constraints['Effect1(operation)->Effect2(operation)'], - model.variables['Effect1(operation)->Effect2(operation)'] - == model.variables['Effect1(operation)|total_per_timestep'] * 1.1, + model.constraints['Effect1(temporal)->Effect2(temporal)'], + model.variables['Effect1(temporal)->Effect2(temporal)'] + == model.variables['Effect1(temporal)|per_timestep'] * 1.1, ) assert_conequal( - model.constraints['Effect1(invest)->Effect2(invest)'], - model.variables['Effect1(invest)->Effect2(invest)'] == model.variables['Effect1(invest)|total'] * 2.1, + model.constraints['Effect1(nontemporal)->Effect2(nontemporal)'], + model.variables['Effect1(nontemporal)->Effect2(nontemporal)'] + == model.variables['Effect1(nontemporal)'] * 2.1, ) diff --git a/tests/test_flow.py b/tests/test_flow.py index f7c5d8a69..29c53968b 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -99,18 +99,18 @@ def test_effects_per_flow_hour(self, basic_flow_system_linopy): assert set(flow.model.variables) == {'Sink(Wärme)|total_flow_hours', 'Sink(Wärme)|flow_rate'} assert set(flow.model.constraints) == {'Sink(Wärme)|total_flow_hours'} - assert 'Sink(Wärme)->Costs(operation)' in set(costs.model.constraints) - assert 'Sink(Wärme)->CO2(operation)' in set(co2.model.constraints) + assert 'Sink(Wärme)->Costs(temporal)' in set(costs.model.constraints) + assert 'Sink(Wärme)->CO2(temporal)' in set(co2.model.constraints) assert_conequal( - model.constraints['Sink(Wärme)->Costs(operation)'], - model.variables['Sink(Wärme)->Costs(operation)'] + model.constraints['Sink(Wärme)->Costs(temporal)'], + model.variables['Sink(Wärme)->Costs(temporal)'] == flow.model.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step * costs_per_flow_hour, ) assert_conequal( - model.constraints['Sink(Wärme)->CO2(operation)'], - model.variables['Sink(Wärme)->CO2(operation)'] + model.constraints['Sink(Wärme)->CO2(temporal)'], + model.variables['Sink(Wärme)->CO2(temporal)'] == flow.model.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step * co2_per_flow_hour, ) @@ -402,19 +402,19 @@ def test_flow_invest_with_effects(self, basic_flow_system_linopy): model = create_linopy_model(flow_system) # Check investment effects - assert 'Sink(Wärme)->Costs(invest)' in model.variables - assert 'Sink(Wärme)->CO2(invest)' in model.variables + assert 'Sink(Wärme)->Costs(nontemporal)' in model.variables + assert 'Sink(Wärme)->CO2(nontemporal)' in model.variables # Check fix effects (applied only when is_invested=1) assert_conequal( - model.constraints['Sink(Wärme)->Costs(invest)'], - model.variables['Sink(Wärme)->Costs(invest)'] + model.constraints['Sink(Wärme)->Costs(nontemporal)'], + model.variables['Sink(Wärme)->Costs(nontemporal)'] == flow.model.variables['Sink(Wärme)|is_invested'] * 1000 + flow.model.variables['Sink(Wärme)|size'] * 500, ) assert_conequal( - model.constraints['Sink(Wärme)->CO2(invest)'], - model.variables['Sink(Wärme)->CO2(invest)'] + model.constraints['Sink(Wärme)->CO2(nontemporal)'], + model.variables['Sink(Wärme)->CO2(nontemporal)'] == flow.model.variables['Sink(Wärme)|is_invested'] * 5 + flow.model.variables['Sink(Wärme)|size'] * 0.1, ) @@ -437,11 +437,12 @@ def test_flow_invest_divest_effects(self, basic_flow_system_linopy): model = create_linopy_model(flow_system) # Check divestment effects - assert 'Sink(Wärme)->Costs(invest)' in model.constraints + assert 'Sink(Wärme)->Costs(nontemporal)' in model.constraints assert_conequal( - model.constraints['Sink(Wärme)->Costs(invest)'], - model.variables['Sink(Wärme)->Costs(invest)'] + (model.variables['Sink(Wärme)|is_invested'] - 1) * 500 == 0, + model.constraints['Sink(Wärme)->Costs(nontemporal)'], + model.variables['Sink(Wärme)->Costs(nontemporal)'] + (model.variables['Sink(Wärme)|is_invested'] - 1) * 500 + == 0, ) @@ -539,18 +540,18 @@ def test_effects_per_running_hour(self, basic_flow_system_linopy): 'Sink(Wärme)|on_hours_total', } - assert 'Sink(Wärme)->Costs(operation)' in set(costs.model.constraints) - assert 'Sink(Wärme)->CO2(operation)' in set(co2.model.constraints) + assert 'Sink(Wärme)->Costs(temporal)' in set(costs.model.constraints) + assert 'Sink(Wärme)->CO2(temporal)' in set(co2.model.constraints) assert_conequal( - model.constraints['Sink(Wärme)->Costs(operation)'], - model.variables['Sink(Wärme)->Costs(operation)'] + model.constraints['Sink(Wärme)->Costs(temporal)'], + model.variables['Sink(Wärme)->Costs(temporal)'] == flow.model.variables['Sink(Wärme)|on'] * model.hours_per_step * costs_per_running_hour, ) assert_conequal( - model.constraints['Sink(Wärme)->CO2(operation)'], - model.variables['Sink(Wärme)->CO2(operation)'] + model.constraints['Sink(Wärme)->CO2(temporal)'], + model.variables['Sink(Wärme)->CO2(temporal)'] == flow.model.variables['Sink(Wärme)|on'] * model.hours_per_step * co2_per_running_hour, ) @@ -885,12 +886,12 @@ def test_switch_on_constraints(self, basic_flow_system_linopy): ) # Check that startup cost effect constraint exists - assert 'Sink(Wärme)->Costs(operation)' in model.constraints + assert 'Sink(Wärme)->Costs(temporal)' in model.constraints # Verify the startup cost effect constraint assert_conequal( - model.constraints['Sink(Wärme)->Costs(operation)'], - model.variables['Sink(Wärme)->Costs(operation)'] == flow.model.variables['Sink(Wärme)|switch_on'] * 100, + model.constraints['Sink(Wärme)->Costs(temporal)'], + model.variables['Sink(Wärme)->Costs(temporal)'] == flow.model.variables['Sink(Wärme)|switch_on'] * 100, ) def test_on_hours_limits(self, basic_flow_system_linopy): diff --git a/tests/test_functional.py b/tests/test_functional.py index 5db83f656..e542dd265 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -112,7 +112,7 @@ def test_solve_and_load(solver_fixture, time_steps_fixture): def test_minimal_model(solver_fixture, time_steps_fixture): results = solve_and_load(flow_system_minimal(time_steps_fixture), solver_fixture) - assert_allclose(results.model.variables['costs|total'].solution.values, 80, rtol=1e-5, atol=1e-10) + assert_allclose(results.model.variables['costs'].solution.values, 80, rtol=1e-5, atol=1e-10) assert_allclose( results.model.variables['Boiler(Q_th)|flow_rate'].solution.values, @@ -122,14 +122,14 @@ def test_minimal_model(solver_fixture, time_steps_fixture): ) assert_allclose( - results.model.variables['costs(operation)|total_per_timestep'].solution.values, + results.model.variables['costs(temporal)|per_timestep'].solution.values, [-0.0, 20.0, 40.0, -0.0, 20.0], rtol=1e-5, atol=1e-10, ) assert_allclose( - results.model.variables['Gastarif(Gas)->costs(operation)'].solution.values, + results.model.variables['Gastarif(Gas)->costs(temporal)'].solution.values, [-0.0, 20.0, 40.0, -0.0, 20.0], rtol=1e-5, atol=1e-10, diff --git a/tests/test_integration.py b/tests/test_integration.py index 42fb5f0b7..1d331df00 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -63,11 +63,11 @@ def test_results_persistence(self, simple_flow_system, highs_solver): # Verify key variables from loaded results assert_almost_equal_numeric( - results.solution['costs|total'].values, + results.solution['costs'].values, 81.88394666666667, 'costs doesnt match expected value', ) - assert_almost_equal_numeric(results.solution['CO2|total'].values, 255.09184, 'CO2 doesnt match expected value') + assert_almost_equal_numeric(results.solution['CO2'].values, 255.09184, 'CO2 doesnt match expected value') class TestComponents: @@ -179,13 +179,13 @@ def test_basic_flow_system(self, flow_system_base, highs_solver): # Assertions assert_almost_equal_numeric( - calculation.results.model['costs|total'].solution.item(), + calculation.results.model['costs'].solution.item(), -11597.873624489237, 'costs doesnt match expected value', ) assert_almost_equal_numeric( - calculation.results.model['costs(operation)|total_per_timestep'].solution.values, + calculation.results.model['costs(temporal)|per_timestep'].solution.values, [ -2.38500000e03, -2.21681333e03, @@ -201,55 +201,55 @@ def test_basic_flow_system(self, flow_system_base, highs_solver): ) assert_almost_equal_numeric( - sum(calculation.results.model['CO2(operation)->costs(operation)'].solution.values), + sum(calculation.results.model['CO2(temporal)->costs(temporal)'].solution.values), 258.63729669618675, 'costs doesnt match expected value', ) assert_almost_equal_numeric( - sum(calculation.results.model['Kessel(Q_th)->costs(operation)'].solution.values), + sum(calculation.results.model['Kessel(Q_th)->costs(temporal)'].solution.values), 0.01, 'costs doesnt match expected value', ) assert_almost_equal_numeric( - sum(calculation.results.model['Kessel->costs(operation)'].solution.values), + sum(calculation.results.model['Kessel->costs(temporal)'].solution.values), -0.0, 'costs doesnt match expected value', ) assert_almost_equal_numeric( - sum(calculation.results.model['Gastarif(Q_Gas)->costs(operation)'].solution.values), + sum(calculation.results.model['Gastarif(Q_Gas)->costs(temporal)'].solution.values), 39.09153113079115, 'costs doesnt match expected value', ) assert_almost_equal_numeric( - sum(calculation.results.model['Einspeisung(P_el)->costs(operation)'].solution.values), + sum(calculation.results.model['Einspeisung(P_el)->costs(temporal)'].solution.values), -14196.61245231646, 'costs doesnt match expected value', ) assert_almost_equal_numeric( - sum(calculation.results.model['KWK->costs(operation)'].solution.values), + sum(calculation.results.model['KWK->costs(temporal)'].solution.values), 0.0, 'costs doesnt match expected value', ) assert_almost_equal_numeric( - calculation.results.model['Kessel(Q_th)->costs(invest)'].solution.values, + calculation.results.model['Kessel(Q_th)->costs(nontemporal)'].solution.values, 1000 + 500, 'costs doesnt match expected value', ) assert_almost_equal_numeric( - calculation.results.model['Speicher->costs(invest)'].solution.values, + calculation.results.model['Speicher->costs(nontemporal)'].solution.values, 800 + 1, 'costs doesnt match expected value', ) assert_almost_equal_numeric( - calculation.results.model['CO2(operation)|total'].solution.values, + calculation.results.model['CO2(temporal)'].solution.values, 1293.1864834809337, 'CO2 doesnt match expected value', ) assert_almost_equal_numeric( - calculation.results.model['CO2(invest)|total'].solution.values, + calculation.results.model['CO2(nontemporal)'].solution.values, 0.9999999999999994, 'CO2 doesnt match expected value', ) @@ -407,13 +407,13 @@ def test_modeling_types_costs(self, modeling_calculation): if modeling_type in ['full', 'aggregated']: assert_almost_equal_numeric( - calc.results.model['costs|total'].solution.item(), + calc.results.model['costs'].solution.item(), expected_costs[modeling_type], f'Costs do not match for {modeling_type} modeling type', ) else: assert_almost_equal_numeric( - calc.results.solution_without_overlap('costs(operation)|total_per_timestep').sum(), + calc.results.solution_without_overlap('costs(temporal)|per_timestep').sum(), expected_costs[modeling_type], f'Costs do not match for {modeling_type} modeling type', ) diff --git a/tests/test_io.py b/tests/test_io.py index 2ec74955f..93bcbb1d0 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -42,8 +42,8 @@ def test_flow_system_file_io(flow_system, highs_solver): ) assert_almost_equal_numeric( - calculation_0.results.solution['costs|total'].values, - calculation_1.results.solution['costs|total'].values, + calculation_0.results.solution['costs'].values, + calculation_1.results.solution['costs'].values, 'costs doesnt match expected value', ) diff --git a/tests/test_linear_converter.py b/tests/test_linear_converter.py index 93ace3e78..100ce5bee 100644 --- a/tests/test_linear_converter.py +++ b/tests/test_linear_converter.py @@ -184,10 +184,10 @@ def test_linear_converter_with_on_off(self, basic_flow_system_linopy): ) # Check on_off effects - assert 'Converter->Costs(operation)' in model.constraints + assert 'Converter->Costs(temporal)' in model.constraints assert_conequal( - model.constraints['Converter->Costs(operation)'], - model.variables['Converter->Costs(operation)'] + model.constraints['Converter->Costs(temporal)'], + model.variables['Converter->Costs(temporal)'] == converter.model.on_off.variables['Converter|on'] * model.hours_per_step * 5, ) @@ -488,10 +488,10 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy): ) # Verify that the costs effect is applied - assert 'Converter->Costs(operation)' in model.constraints + assert 'Converter->Costs(temporal)' in model.constraints assert_conequal( - model.constraints['Converter->Costs(operation)'], - model.variables['Converter->Costs(operation)'] + model.constraints['Converter->Costs(temporal)'], + model.variables['Converter->Costs(temporal)'] == converter.model.on_off.variables['Converter|on'] * model.hours_per_step * 5, )