diff --git a/CHANGELOG.md b/CHANGELOG.md index a1e98dbf0..7ecf091eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,6 +108,13 @@ The weighted sum of the total objective effect of each scenario is used as the o * 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. +- 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` ### 🐛 Fixed diff --git a/examples/03_Calculation_types/example_calculation_types.py b/examples/03_Calculation_types/example_calculation_types.py index f449485de..c4f30be46 100644 --- a/examples/03_Calculation_types/example_calculation_types.py +++ b/examples/03_Calculation_types/example_calculation_types.py @@ -207,14 +207,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(), style='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, style='stacked_bar', title='Total Cost Comparison', ylabel='Costs [€]', diff --git a/examples/05_Two-stage-optimization/two_stage_optimization.py b/examples/05_Two-stage-optimization/two_stage_optimization.py index 7701f6056..b6a0bfa67 100644 --- a/examples/05_Two-stage-optimization/two_stage_optimization.py +++ b/examples/05_Two-stage-optimization/two_stage_optimization.py @@ -145,9 +145,9 @@ comparison_main = comparison[ [ 'Duration [s]', - 'costs|total', - 'costs(invest)|total', - 'costs(operation)|total', + 'costs', + 'costs(nontemporal)', + 'costs(temporal)', 'BHKW2(Q_fu)|size', 'Kessel(Q_fu)|size', 'Speicher|size', diff --git a/flixopt/calculation.py b/flixopt/calculation.py index 966d31e1e..82aa476a0 100644 --- a/flixopt/calculation.py +++ b/flixopt/calculation.py @@ -107,8 +107,8 @@ def main_results(self) -> dict[str, Scalar | dict]: 'Penalty': self.model.effects.penalty.total.solution.values, 'Effects': { f'{effect.label} [{effect.unit}]': { - 'operation': effect.submodel.operation.total.solution.values, - 'invest': effect.submodel.invest.total.solution.values, + 'temporal': effect.submodel.temporal.total.solution.values, + 'nontemporal': effect.submodel.nontemporal.total.solution.values, 'total': effect.submodel.total.solution.values, } for effect in self.flow_system.effects diff --git a/flixopt/components.py b/flixopt/components.py index 516cb2b12..5bf76afaf 100644 --- a/flixopt/components.py +++ b/flixopt/components.py @@ -1056,7 +1056,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, ) @@ -1066,7 +1066,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, ) @@ -1076,12 +1076,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, @@ -1206,7 +1209,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, ) @@ -1214,6 +1217,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, @@ -1334,7 +1340,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, ) @@ -1342,6 +1348,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 fb9674b3e..9b3f97227 100644 --- a/flixopt/effects.py +++ b/flixopt/effects.py @@ -54,17 +54,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: @@ -113,8 +121,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³ ) ``` @@ -144,14 +152,15 @@ def __init__( is_objective: bool = False, specific_share_to_other_effects_operation: TemporalEffectsUser | None = None, specific_share_to_other_effects_invest: NonTemporalEffectsUser | 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: TemporalDataUser | None = None, - maximum_operation_per_hour: TemporalDataUser | None = None, + minimum_temporal: NonTemporalEffectsUser | None = None, + maximum_temporal: NonTemporalEffectsUser | None = None, + minimum_nontemporal: NonTemporalEffectsUser | None = None, + maximum_nontemporal: NonTemporalEffectsUser | None = None, + minimum_per_hour: TemporalDataUser | None = None, + maximum_per_hour: TemporalDataUser | None = None, minimum_total: Scalar | None = None, maximum_total: Scalar | None = None, + **kwargs, ): super().__init__(label, meta_data=meta_data) self.label = label @@ -165,40 +174,241 @@ def __init__( self.specific_share_to_other_effects_invest: NonTemporalEffectsUser = ( specific_share_to_other_effects_invest if specific_share_to_other_effects_invest is not None else {} ) - 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 - def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None: - prefix = '|'.join(filter(None, [name_prefix, self.label_full])) - self.minimum_operation_per_hour = flow_system.fit_to_model_coords( - f'{prefix}|minimum_operation_per_hour', self.minimum_operation_per_hour + # 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 - self.maximum_operation_per_hour = flow_system.fit_to_model_coords( - f'{prefix}|maximum_operation_per_hour', self.maximum_operation_per_hour + @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, name_prefix: str = '') -> None: + prefix = '|'.join(filter(None, [name_prefix, self.label_full])) + self.minimum_per_hour = flow_system.fit_to_model_coords(f'{prefix}|minimum_per_hour', self.minimum_per_hour) + + self.maximum_per_hour = flow_system.fit_to_model_coords(f'{prefix}|maximum_per_hour', self.maximum_per_hour) self.specific_share_to_other_effects_operation = flow_system.fit_effects_to_model_coords( - f'{prefix}|operation->', self.specific_share_to_other_effects_operation, 'operation' + f'{prefix}|operation->', self.specific_share_to_other_effects_operation, 'temporal' ) self.minimum_operation = flow_system.fit_to_model_coords( - f'{prefix}|minimum_operation', self.minimum_operation, dims=['year', 'scenario'] + f'{prefix}|minimum_temporal', self.minimum_temporal, dims=['year', 'scenario'] ) self.maximum_operation = flow_system.fit_to_model_coords( - f'{prefix}|maximum_operation', self.maximum_operation, dims=['year', 'scenario'] + f'{prefix}|maximum_temporal', self.maximum_temporal, dims=['year', 'scenario'] ) self.minimum_invest = flow_system.fit_to_model_coords( - f'{prefix}|minimum_invest', self.minimum_invest, dims=['year', 'scenario'] + f'{prefix}|minimum_nontemporal', self.minimum_nontemporal, dims=['year', 'scenario'] ) self.maximum_invest = flow_system.fit_to_model_coords( - f'{prefix}|maximum_invest', self.maximum_invest, dims=['year', 'scenario'] + f'{prefix}|maximum_nontemporal', self.maximum_nontemporal, dims=['year', 'scenario'] ) self.minimum_total = flow_system.fit_to_model_coords( f'{prefix}|minimum_total', @@ -209,9 +419,9 @@ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None f'{prefix}|maximum_total', self.maximum_total, dims=['year', 'scenario'] ) self.specific_share_to_other_effects_invest = flow_system.fit_effects_to_model_coords( - f'{prefix}|invest->', + f'{prefix}|operation->', self.specific_share_to_other_effects_invest, - 'invest', + 'operation', dims=['year', 'scenario'], ) @@ -233,44 +443,42 @@ def __init__(self, model: FlowSystemModel, element: Effect): def _do_modeling(self): self.total: linopy.Variable | None = None - self.invest: ShareAllocationModel = self.add_submodels( + self.nontemporal: ShareAllocationModel = self.add_submodels( ShareAllocationModel( model=self._model, dims=('year', 'scenario'), label_of_element=self.label_of_element, - label_of_model=f'{self.label_of_model}(invest)', - total_max=self.element.maximum_invest, - total_min=self.element.minimum_invest, + label_of_model=f'{self.label_of_model}(nontemporal)', + total_max=self.element.maximum_nontemporal, + total_min=self.element.minimum_nontemporal, ), - short_name='invest', + short_name='nontemporal', ) - self.operation: ShareAllocationModel = self.add_submodels( + self.temporal: ShareAllocationModel = self.add_submodels( ShareAllocationModel( model=self._model, dims=('time', 'year', 'scenario'), label_of_element=self.label_of_element, - label_of_model=f'{self.label_of_model}(operation)', - total_max=self.element.maximum_operation, - total_min=self.element.minimum_operation, - min_per_hour=self.element.minimum_operation_per_hour - if self.element.minimum_operation_per_hour is not None - else None, - max_per_hour=self.element.maximum_operation_per_hour - if self.element.maximum_operation_per_hour is not None - else None, + label_of_model=f'{self.label_of_model}(temporal)', + total_max=self.element.maximum_temporal, + total_min=self.element.minimum_temporal, + min_per_hour=self.element.minimum_per_hour if self.element.minimum_per_hour is not None else None, + max_per_hour=self.element.maximum_per_hour if self.element.maximum_per_hour is not None else None, ), - short_name='operation', + short_name='temporal', ) self.total = self.add_variables( 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=self._model.get_coords(['year', 'scenario']), - short_name='total', + name=self.label_full, ) - self.add_constraints(self.total == self.operation.total + self.invest.total, short_name='total') + self.add_constraints( + self.total == self.temporal.total + self.nontemporal.total, name=self.label_full, short_name='total' + ) TemporalEffectsUser = TemporalDataUser | dict[str, TemporalDataUser] # User-specified Shares to Effects @@ -365,18 +573,18 @@ def get_effect_label(eff: Effect | str) -> str: def _plausibility_checks(self) -> None: # Check circular loops in effects: - operation, invest = self.calculate_effect_share_factors() + temporal, nontemporal = self.calculate_effect_share_factors() - operation_cycles = detect_cycles(tuples_to_adjacency_list([key for key in operation])) - invest_cycles = detect_cycles(tuples_to_adjacency_list([key for key in invest])) + temporal_cycles = detect_cycles(tuples_to_adjacency_list([key for key in temporal])) + nontemporal_cycles = detect_cycles(tuples_to_adjacency_list([key for key in nontemporal])) - if operation_cycles: - cycle_str = '\n'.join([' -> '.join(cycle) for cycle in operation_cycles]) - raise ValueError(f'Error: circular operation-shares detected:\n{cycle_str}') + if temporal_cycles: + cycle_str = '\n'.join([' -> '.join(cycle) for cycle in temporal_cycles]) + raise ValueError(f'Error: circular temporal-shares detected:\n{cycle_str}') - if invest_cycles: - cycle_str = '\n'.join([' -> '.join(cycle) for cycle in invest_cycles]) - raise ValueError(f'Error: circular invest-shares detected:\n{cycle_str}') + if nontemporal_cycles: + cycle_str = '\n'.join([' -> '.join(cycle) for cycle in nontemporal_cycles]) + raise ValueError(f'Error: circular nontemporal-shares detected:\n{cycle_str}') def __getitem__(self, effect: str | Effect | None) -> Effect: """ @@ -449,23 +657,23 @@ def calculate_effect_share_factors( dict[tuple[str, str], xr.DataArray], dict[tuple[str, str], xr.DataArray], ]: - shares_invest = {} + shares_nontemporal = {} for name, effect in self.effects.items(): if effect.specific_share_to_other_effects_invest: - shares_invest[name] = { + shares_nontemporal[name] = { target: data for target, data in effect.specific_share_to_other_effects_invest.items() } - shares_invest = calculate_all_conversion_paths(shares_invest) + shares_nontemporal = calculate_all_conversion_paths(shares_nontemporal) - shares_operation = {} + shares_temporal = {} for name, effect in self.effects.items(): if effect.specific_share_to_other_effects_operation: - shares_operation[name] = { + shares_temporal[name] = { target: data for target, data in effect.specific_share_to_other_effects_operation.items() } - shares_operation = calculate_all_conversion_paths(shares_operation) + shares_temporal = calculate_all_conversion_paths(shares_temporal) - return shares_operation, shares_invest + return shares_temporal, shares_nontemporal class EffectCollectionModel(Submodel): @@ -482,17 +690,17 @@ def add_share_to_effects( self, name: str, expressions: EffectExpr, - target: Literal['operation', 'invest'], + target: Literal['temporal', 'nontemporal'], ) -> None: for effect, expression in expressions.items(): - if target == 'operation': - self.effects[effect].submodel.operation.add_share( + if target == 'temporal': + self.effects[effect].submodel.temporal.add_share( name, expression, dims=('time', 'year', 'scenario'), ) - elif target == 'invest': - self.effects[effect].submodel.invest.add_share( + elif target == 'nontemporal': + self.effects[effect].submodel.nontemporal.add_share( name, expression, dims=('year', 'scenario'), @@ -522,18 +730,18 @@ 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].submodel.operation.add_share( - origin_effect.submodel.operation.label_full, - origin_effect.submodel.operation.total_per_timestep * time_series, + self.effects[target_effect].submodel.temporal.add_share( + origin_effect.submodel.temporal.label_full, + origin_effect.submodel.temporal.total_per_timestep * time_series, dims=('time', 'year', 'scenario'), ) - # 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].submodel.invest.add_share( - origin_effect.submodel.invest.label_full, - origin_effect.submodel.invest.total * factor, + self.effects[target_effect].submodel.nontemporal.add_share( + origin_effect.submodel.nontemporal.label_full, + origin_effect.submodel.nontemporal.total * factor, dims=('year', 'scenario'), ) diff --git a/flixopt/elements.py b/flixopt/elements.py index 04bc7a599..42d46c8ce 100644 --- a/flixopt/elements.py +++ b/flixopt/elements.py @@ -643,7 +643,7 @@ def _create_shares(self): effect: self.flow_rate * self._model.hours_per_step * factor 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 51a4832b7..160a32b33 100644 --- a/flixopt/features.py +++ b/flixopt/features.py @@ -97,7 +97,7 @@ def _add_effects(self): effect: self.is_invested * factor if self.is_invested is not None else factor for effect, factor in self.parameters.fix_effects.items() }, - target='invest', + target='nontemporal', ) if self.parameters.divest_effects and self.parameters.optional: @@ -107,14 +107,14 @@ def _add_effects(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', ) @property @@ -240,7 +240,7 @@ def _add_effects(self): effect: self.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: @@ -249,7 +249,7 @@ def _add_effects(self): expressions={ effect: self.switch_on * factor for effect, factor in self.parameters.effects_per_switch_on.items() }, - target='operation', + target='temporal', ) # Properties access variables from Submodel's tracking system @@ -484,7 +484,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', ) @@ -526,22 +526,21 @@ def _do_modeling(self): lower=self._total_min, upper=self._total_max, coords=self._model.get_coords([dim for dim in self._dims if dim != 'time']), + name=self.label_full, short_name='total', ) # eq: sum = sum(share_i) # skalar - self._eq_total = self.add_constraints(self.total == 0, short_name='total') + self._eq_total = self.add_constraints(self.total == 0, name=self.label_full) if 'time' in self._dims: self.total_per_timestep = self.add_variables( lower=-np.inf if (self._min_per_hour is None) else self._min_per_hour * self._model.hours_per_step, upper=np.inf if (self._max_per_hour is None) else self._max_per_hour * self._model.hours_per_step, coords=self._model.get_coords(self._dims), - short_name='total_per_timestep', + short_name='per_timestep', ) - self._eq_total_per_timestep = self.add_constraints( - self.total_per_timestep == 0, short_name='total_per_timestep' - ) + self._eq_total_per_timestep = self.add_constraints(self.total_per_timestep == 0, short_name='per_timestep') # Add it to the total self._eq_total.lhs -= self.total_per_timestep.sum(dim='time') diff --git a/flixopt/results.py b/flixopt/results.py index e153178be..596f6870e 100644 --- a/flixopt/results.py +++ b/flixopt/results.py @@ -283,7 +283,7 @@ def constraints(self) -> linopy.Constraints: def effect_share_factors(self): if self._effect_share_factors is None: effect_share_factors = self.flow_system.effects.calculate_effect_share_factors() - self._effect_share_factors = {'operation': effect_share_factors[0], 'invest': effect_share_factors[1]} + self._effect_share_factors = {'temporal': effect_share_factors[0], 'nontemporal': effect_share_factors[1]} return self._effect_share_factors @property @@ -356,7 +356,7 @@ def effects_per_component(self) -> xr.Dataset: self._effects_per_component = xr.Dataset( { mode: self._create_effects_dataset(mode).to_dataarray('effect', name=mode) - for mode in ['operation', 'invest', 'total'] + for mode in ['temporal', 'nontemporal', 'total'] } ) dim_order = ['time', 'year', 'scenario', 'component', 'effect'] @@ -477,22 +477,22 @@ def get_effect_shares( self, element: str, effect: str, - mode: Literal['operation', 'invest'] | None = None, + mode: Literal['temporal', 'nontemporal'] | None = None, include_flows: bool = False, ) -> xr.Dataset: """Retrieves individual effect shares for a specific element and effect. - Either for operation, investment, or both modes combined. + Either for temporal, investment, or both modes combined. Only includes the direct shares. Args: element: The element identifier for which to retrieve effect shares. effect: The effect identifier for which to retrieve shares. - mode: Optional. The mode to retrieve shares for. Can be 'operation', 'invest', + mode: Optional. The mode to retrieve shares for. Can be 'temporal', 'nontemporal', or None to retrieve both. Defaults to None. Returns: An xarray Dataset containing the requested effect shares. If mode is None, - returns a merged Dataset containing both operation and investment shares. + returns a merged Dataset containing both temporal and investment shares. Raises: ValueError: If the specified effect is not available or if mode is invalid. @@ -504,14 +504,16 @@ def get_effect_shares( return xr.merge( [ self.get_effect_shares( - element=element, effect=effect, mode='operation', include_flows=include_flows + element=element, effect=effect, mode='temporal', include_flows=include_flows + ), + self.get_effect_shares( + element=element, effect=effect, mode='nontemporal', include_flows=include_flows ), - self.get_effect_shares(element=element, effect=effect, mode='invest', include_flows=include_flows), ] ) - if mode not in ['operation', 'invest']: - raise ValueError(f'Mode {mode} is not available. Choose between "operation" and "invest".') + if mode not in ['temporal', 'nontemporal']: + raise ValueError(f'Mode {mode} is not available. Choose between "temporal" and "nontemporal".') ds = xr.Dataset() @@ -539,7 +541,7 @@ def _compute_effect_total( self, element: str, effect: str, - mode: Literal['operation', 'invest', 'total'] = 'total', + mode: Literal['temporal', 'nontemporal', 'total'] = 'total', include_flows: bool = False, ) -> xr.DataArray: """Calculates the total effect for a specific element and effect. @@ -551,10 +553,9 @@ def _compute_effect_total( element: The element identifier for which to calculate total effects. effect: The effect identifier to calculate. mode: The calculation mode. Options are: - 'operation': Returns operation-specific effects. - 'invest': Returns investment-specific effects. - 'total': Returns the sum of operation effects (across all timesteps) - and investment effects. Defaults to 'total'. + 'temporal': Returns temporal effects. + 'nontemporal': Returns investment-specific effects. + 'total': Returns the sum of temporal effects and non-temporal effects. Defaults to 'total'. include_flows: Whether to include effects from flows connected to this element. Returns: @@ -569,22 +570,22 @@ def _compute_effect_total( raise ValueError(f'Effect {effect} is not available.') if mode == 'total': - operation = self._compute_effect_total( - element=element, effect=effect, mode='operation', include_flows=include_flows + temporal = self._compute_effect_total( + element=element, effect=effect, mode='temporal', include_flows=include_flows ) - invest = self._compute_effect_total( - element=element, effect=effect, mode='invest', include_flows=include_flows + nontemporal = self._compute_effect_total( + element=element, effect=effect, mode='nontemporal', include_flows=include_flows ) - if invest.isnull().all() and operation.isnull().all(): + if nontemporal.isnull().all() and temporal.isnull().all(): return xr.DataArray(np.nan) - if operation.isnull().all(): - return invest.rename(f'{element}->{effect}') - operation = operation.sum('time') - if invest.isnull().all(): - return operation.rename(f'{element}->{effect}') - if 'time' in operation.indexes: - operation = operation.sum('time') - return invest + operation + if temporal.isnull().all(): + return nontemporal.rename(f'{element}->{effect}') + temporal = temporal.sum('time') + if nontemporal.isnull().all(): + return temporal.rename(f'{element}->{effect}') + if 'time' in temporal.indexes: + temporal = temporal.sum('time') + return nontemporal + temporal total = xr.DataArray(0) share_exists = False @@ -617,12 +618,12 @@ def _compute_effect_total( total = xr.DataArray(np.nan) return total.rename(f'{element}->{effect}({mode})') - def _create_effects_dataset(self, mode: Literal['operation', 'invest', 'total']) -> xr.Dataset: + def _create_effects_dataset(self, mode: Literal['temporal', 'nontemporal', 'total']) -> xr.Dataset: """Creates a dataset containing effect totals for all components (including their flows). The dataset does contain the direct as well as the indirect effects of each component. Args: - mode: The calculation mode ('operation', 'invest', or 'total'). + mode: The calculation mode ('temporal', 'nontemporal', or 'total'). Returns: An xarray Dataset with components as dimension and effects as variables. @@ -668,9 +669,9 @@ def _create_effects_dataset(self, mode: Literal['operation', 'invest', 'total']) # For now include a test to ensure correctness suffix = { - 'operation': '(operation)|total_per_timestep', - 'invest': '(invest)|total', - 'total': '|total', + 'temporal': '(temporal)|per_timestep', + 'nontemporal': '(nontemporal)', + 'total': '', } for effect in self.effects: label = f'{effect}{suffix[mode]}' diff --git a/flixopt/structure.py b/flixopt/structure.py index 01529aba0..76ed57c1d 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -367,6 +367,36 @@ def _extract_dataarrays_recursive(self, obj, context_name: str = '') -> tuple[An else: return self._serialize_to_basic_types(obj), extracted_arrays + 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 _resolve_dataarray_reference( cls, reference: str, arrays_dict: dict[str, xr.DataArray] diff --git a/tests/test_effect.py b/tests/test_effect.py index cf64eb723..e0b737771 100644 --- a/tests/test_effect.py +++ b/tests/test_effect.py @@ -25,10 +25,10 @@ def test_minimal(self, basic_flow_system_linopy_coords, coords_config): assert_sets_equal( set(effect.submodel.variables), { - 'Effect1(invest)|total', - 'Effect1(operation)|total', - 'Effect1(operation)|total_per_timestep', - 'Effect1|total', + 'Effect1(nontemporal)', + 'Effect1(temporal)', + 'Effect1(temporal)|per_timestep', + 'Effect1', }, msg='Incorrect variables', ) @@ -36,42 +36,39 @@ def test_minimal(self, basic_flow_system_linopy_coords, coords_config): assert_sets_equal( set(effect.submodel.constraints), { - 'Effect1(invest)|total', - 'Effect1(operation)|total', - 'Effect1(operation)|total_per_timestep', - 'Effect1|total', + 'Effect1(nontemporal)', + 'Effect1(temporal)', + 'Effect1(temporal)|per_timestep', + 'Effect1', }, msg='Incorrect constraints', ) + assert_var_equal(model.variables['Effect1'], model.add_variables(coords=model.get_coords(['year', 'scenario']))) assert_var_equal( - model.variables['Effect1|total'], model.add_variables(coords=model.get_coords(['year', 'scenario'])) - ) - assert_var_equal( - model.variables['Effect1(invest)|total'], model.add_variables(coords=model.get_coords(['year', 'scenario'])) + model.variables['Effect1(nontemporal)'], model.add_variables(coords=model.get_coords(['year', 'scenario'])) ) assert_var_equal( - model.variables['Effect1(operation)|total'], + model.variables['Effect1(temporal)'], model.add_variables(coords=model.get_coords(['year', 'scenario'])), ) assert_var_equal( - model.variables['Effect1(operation)|total_per_timestep'], model.add_variables(coords=model.get_coords()) + model.variables['Effect1(temporal)|per_timestep'], model.add_variables(coords=model.get_coords()) ) 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('time'), + model.constraints['Effect1(temporal)'], + model.variables['Effect1(temporal)'] == model.variables['Effect1(temporal)|per_timestep'].sum('time'), ) 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_coords, coords_config): @@ -80,14 +77,14 @@ def test_bounds(self, basic_flow_system_linopy_coords, coords_config): '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) @@ -96,10 +93,10 @@ def test_bounds(self, basic_flow_system_linopy_coords, coords_config): assert_sets_equal( set(effect.submodel.variables), { - 'Effect1(invest)|total', - 'Effect1(operation)|total', - 'Effect1(operation)|total_per_timestep', - 'Effect1|total', + 'Effect1(nontemporal)', + 'Effect1(temporal)', + 'Effect1(temporal)|per_timestep', + 'Effect1', }, msg='Incorrect variables', ) @@ -107,28 +104,28 @@ def test_bounds(self, basic_flow_system_linopy_coords, coords_config): assert_sets_equal( set(effect.submodel.constraints), { - 'Effect1(invest)|total', - 'Effect1(operation)|total', - 'Effect1(operation)|total_per_timestep', - 'Effect1|total', + 'Effect1(nontemporal)', + 'Effect1(temporal)', + 'Effect1(temporal)|per_timestep', + 'Effect1', }, msg='Incorrect constraints', ) assert_var_equal( - model.variables['Effect1|total'], + model.variables['Effect1'], model.add_variables(lower=3.0, upper=3.1, coords=model.get_coords(['year', 'scenario'])), ) assert_var_equal( - model.variables['Effect1(invest)|total'], + model.variables['Effect1(nontemporal)'], model.add_variables(lower=2.0, upper=2.1, coords=model.get_coords(['year', 'scenario'])), ) assert_var_equal( - model.variables['Effect1(operation)|total'], + model.variables['Effect1(temporal)'], model.add_variables(lower=1.0, upper=1.1, coords=model.get_coords(['year', 'scenario'])), ) 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, @@ -137,19 +134,18 @@ def test_bounds(self, basic_flow_system_linopy_coords, coords_config): ) 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('time'), + model.constraints['Effect1(temporal)'], + model.variables['Effect1(temporal)'] == model.variables['Effect1(temporal)|per_timestep'].sum('time'), ) 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_coords, coords_config): @@ -169,12 +165,12 @@ def test_shares(self, basic_flow_system_linopy_coords, coords_config): assert_sets_equal( set(effect2.submodel.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)', }, msg='Incorrect variables for effect2', ) @@ -182,36 +178,37 @@ def test_shares(self, basic_flow_system_linopy_coords, coords_config): assert_sets_equal( set(effect2.submodel.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)', }, msg='Incorrect constraints for effect2', ) 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, ) @@ -244,7 +241,7 @@ def test_shares(self, basic_flow_system_linopy_coords, coords_config): results = create_calculation_and_solve(flow_system, fx.solvers.HighsSolver(0.01, 60), 'Sim1').results effect_share_factors = { - 'operation': { + 'temporal': { ('costs', 'Effect1'): 0.5, ('costs', 'Effect2'): 0.5 * 1.1, ('costs', 'Effect3'): 0.5 * 1.1 * 5 + 0.5 * 1.2, # This is where the issue lies @@ -252,75 +249,75 @@ def test_shares(self, basic_flow_system_linopy_coords, coords_config): ('Effect1', 'Effect3'): 1.2 + 1.1 * 5, ('Effect2', 'Effect3'): 5, }, - 'invest': { + 'nontemporal': { ('Effect1', 'Effect2'): 2.1, ('Effect1', 'Effect3'): 2.2, }, } - for key, value in effect_share_factors['operation'].items(): - np.testing.assert_allclose(results.effect_share_factors['operation'][key].values, value) + for key, value in effect_share_factors['temporal'].items(): + np.testing.assert_allclose(results.effect_share_factors['temporal'][key].values, value) - for key, value in effect_share_factors['invest'].items(): - np.testing.assert_allclose(results.effect_share_factors['invest'][key].values, value) + for key, value in effect_share_factors['nontemporal'].items(): + np.testing.assert_allclose(results.effect_share_factors['nontemporal'][key].values, value) xr.testing.assert_allclose( - results.effects_per_component['operation'].sum('component').sel(effect='costs', drop=True), - results.solution['costs(operation)|total_per_timestep'].fillna(0), + results.effects_per_component['temporal'].sum('component').sel(effect='costs', drop=True), + results.solution['costs(temporal)|per_timestep'].fillna(0), ) xr.testing.assert_allclose( - results.effects_per_component['operation'].sum('component').sel(effect='Effect1', drop=True), - results.solution['Effect1(operation)|total_per_timestep'].fillna(0), + results.effects_per_component['temporal'].sum('component').sel(effect='Effect1', drop=True), + results.solution['Effect1(temporal)|per_timestep'].fillna(0), ) xr.testing.assert_allclose( - results.effects_per_component['operation'].sum('component').sel(effect='Effect2', drop=True), - results.solution['Effect2(operation)|total_per_timestep'].fillna(0), + results.effects_per_component['temporal'].sum('component').sel(effect='Effect2', drop=True), + results.solution['Effect2(temporal)|per_timestep'].fillna(0), ) xr.testing.assert_allclose( - results.effects_per_component['operation'].sum('component').sel(effect='Effect3', drop=True), - results.solution['Effect3(operation)|total_per_timestep'].fillna(0), + results.effects_per_component['temporal'].sum('component').sel(effect='Effect3', drop=True), + results.solution['Effect3(temporal)|per_timestep'].fillna(0), ) - # Invest mode checks + # nontemporal mode checks xr.testing.assert_allclose( - results.effects_per_component['invest'].sum('component').sel(effect='costs', drop=True), - results.solution['costs(invest)|total'], + results.effects_per_component['nontemporal'].sum('component').sel(effect='costs', drop=True), + results.solution['costs(nontemporal)'], ) xr.testing.assert_allclose( - results.effects_per_component['invest'].sum('component').sel(effect='Effect1', drop=True), - results.solution['Effect1(invest)|total'], + results.effects_per_component['nontemporal'].sum('component').sel(effect='Effect1', drop=True), + results.solution['Effect1(nontemporal)'], ) xr.testing.assert_allclose( - results.effects_per_component['invest'].sum('component').sel(effect='Effect2', drop=True), - results.solution['Effect2(invest)|total'], + results.effects_per_component['nontemporal'].sum('component').sel(effect='Effect2', drop=True), + results.solution['Effect2(nontemporal)'], ) xr.testing.assert_allclose( - results.effects_per_component['invest'].sum('component').sel(effect='Effect3', drop=True), - results.solution['Effect3(invest)|total'], + results.effects_per_component['nontemporal'].sum('component').sel(effect='Effect3', drop=True), + results.solution['Effect3(nontemporal)'], ) # Total mode checks xr.testing.assert_allclose( results.effects_per_component['total'].sum('component').sel(effect='costs', drop=True), - results.solution['costs|total'], + results.solution['costs'], ) xr.testing.assert_allclose( results.effects_per_component['total'].sum('component').sel(effect='Effect1', drop=True), - results.solution['Effect1|total'], + results.solution['Effect1'], ) xr.testing.assert_allclose( results.effects_per_component['total'].sum('component').sel(effect='Effect2', drop=True), - results.solution['Effect2|total'], + results.solution['Effect2'], ) xr.testing.assert_allclose( results.effects_per_component['total'].sum('component').sel(effect='Effect3', drop=True), - results.solution['Effect3|total'], + results.solution['Effect3'], ) diff --git a/tests/test_flow.py b/tests/test_flow.py index 88ce329e5..3c393e965 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -122,18 +122,18 @@ def test_effects_per_flow_hour(self, basic_flow_system_linopy_coords, coords_con ) assert_sets_equal(set(flow.submodel.constraints), {'Sink(Wärme)|total_flow_hours'}, msg='Incorrect constraints') - assert 'Sink(Wärme)->costs(operation)' in set(costs.submodel.constraints) - assert 'Sink(Wärme)->CO2(operation)' in set(co2.submodel.constraints) + assert 'Sink(Wärme)->costs(temporal)' in set(costs.submodel.constraints) + assert 'Sink(Wärme)->CO2(temporal)' in set(co2.submodel.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.submodel.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.submodel.variables['Sink(Wärme)|flow_rate'] * model.hours_per_step * co2_per_flow_hour, ) @@ -467,20 +467,20 @@ def test_flow_invest_with_effects(self, basic_flow_system_linopy_coords, coords_ 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.submodel.variables['Sink(Wärme)|is_invested'] * 1000 + flow.submodel.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.submodel.variables['Sink(Wärme)|is_invested'] * 5 + flow.submodel.variables['Sink(Wärme)|size'] * 0.1, ) @@ -504,11 +504,12 @@ def test_flow_invest_divest_effects(self, basic_flow_system_linopy_coords, coord 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, ) @@ -618,8 +619,8 @@ def test_effects_per_running_hour(self, basic_flow_system_linopy_coords, coords_ msg='Incorrect constraints', ) - assert 'Sink(Wärme)->costs(operation)' in set(costs.submodel.constraints) - assert 'Sink(Wärme)->CO2(operation)' in set(co2.submodel.constraints) + assert 'Sink(Wärme)->costs(temporal)' in set(costs.submodel.constraints) + assert 'Sink(Wärme)->CO2(temporal)' in set(co2.submodel.constraints) costs_per_running_hour = flow.on_off_parameters.effects_per_running_hour['costs'] co2_per_running_hour = flow.on_off_parameters.effects_per_running_hour['CO2'] @@ -628,14 +629,14 @@ def test_effects_per_running_hour(self, basic_flow_system_linopy_coords, coords_ assert co2_per_running_hour.dims == tuple(model.get_coords()) 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.submodel.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.submodel.variables['Sink(Wärme)|on'] * model.hours_per_step * co2_per_running_hour, ) @@ -1021,12 +1022,12 @@ def test_switch_on_constraints(self, basic_flow_system_linopy_coords, coords_con ) # 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.submodel.variables['Sink(Wärme)|switch|on'] * 100, + model.constraints['Sink(Wärme)->costs(temporal)'], + model.variables['Sink(Wärme)->costs(temporal)'] == flow.submodel.variables['Sink(Wärme)|switch|on'] * 100, ) def test_on_hours_limits(self, basic_flow_system_linopy_coords, coords_config): diff --git a/tests/test_functional.py b/tests/test_functional.py index 2315867f1..54dd2e99f 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 a99557e92..76143a1f7 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 TestComplex: @@ -76,13 +76,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, @@ -98,55 +98,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', ) @@ -304,13 +304,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 7b7f4b1b6..f5ca2174a 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -53,8 +53,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 e3d6af18a..1884c8d72 100644 --- a/tests/test_linear_converter.py +++ b/tests/test_linear_converter.py @@ -184,11 +184,10 @@ def test_linear_converter_with_on_off(self, basic_flow_system_linopy_coords, coo ) # 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.variables['Converter|on'] * model.hours_per_step * 5, + model.constraints['Converter->costs(temporal)'], + model.variables['Converter->costs(temporal)'] == model.variables['Converter|on'] * model.hours_per_step * 5, ) def test_linear_converter_multidimensional(self, basic_flow_system_linopy_coords, coords_config): @@ -489,11 +488,10 @@ def test_piecewise_conversion_with_onoff(self, basic_flow_system_linopy_coords, ) # 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.variables['Converter|on'] * model.hours_per_step * 5, + model.constraints['Converter->costs(temporal)'], + model.variables['Converter->costs(temporal)'] == model.variables['Converter|on'] * model.hours_per_step * 5, ) diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py index dea90e34f..ebc633a04 100644 --- a/tests/test_scenarios.py +++ b/tests/test_scenarios.py @@ -239,9 +239,7 @@ def test_weights(flow_system_piecewise_conversion_scenarios): flow_system_piecewise_conversion_scenarios.weights = weights model = create_linopy_model(flow_system_piecewise_conversion_scenarios) np.testing.assert_allclose(model.weights.values, weights) - assert_linequal( - model.objective.expression, (model.variables['costs|total'] * weights).sum() + model.variables['Penalty|total'] - ) + assert_linequal(model.objective.expression, (model.variables['costs'] * weights).sum() + model.variables['Penalty']) assert np.isclose(model.weights.sum().item(), 2.25) @@ -252,9 +250,7 @@ def test_weights_io(flow_system_piecewise_conversion_scenarios): flow_system_piecewise_conversion_scenarios.weights = weights model = create_linopy_model(flow_system_piecewise_conversion_scenarios) np.testing.assert_allclose(model.weights.values, weights) - assert_linequal( - model.objective.expression, (model.variables['costs|total'] * weights).sum() + model.variables['Penalty|total'] - ) + assert_linequal(model.objective.expression, (model.variables['costs'] * weights).sum() + model.variables['Penalty']) assert np.isclose(model.weights.sum().item(), 1.0) @@ -329,9 +325,7 @@ def test_scenarios_selection(flow_system_piecewise_conversion_scenarios): np.testing.assert_allclose( calc.results.objective, - ( - (calc.results.solution['costs|total'] * flow_system.weights).sum() + calc.results.solution['Penalty|total'] - ).item(), + ((calc.results.solution['costs'] * flow_system.weights).sum() + calc.results.solution['Penalty']).item(), ) ## Acount for rounding errors assert calc.results.solution.indexes['scenario'].equals(flow_system_full.scenarios[0:2])