flixopt 2.1.7__py3-none-any.whl → 2.1.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of flixopt might be problematic. Click here for more details.

flixopt/effects.py CHANGED
@@ -5,19 +5,22 @@ Different Datatypes are used to represent the effects with assigned values by th
5
5
  which are then transformed into the internal data structure.
6
6
  """
7
7
 
8
+ from __future__ import annotations
9
+
8
10
  import logging
9
11
  import warnings
10
- from typing import TYPE_CHECKING, Dict, Iterator, List, Literal, Optional, Union
12
+ from typing import TYPE_CHECKING, Literal
11
13
 
12
14
  import linopy
13
15
  import numpy as np
14
- import pandas as pd
15
16
 
16
- from .core import NumericData, NumericDataTS, Scalar, TimeSeries, TimeSeriesCollection
17
+ from .core import NumericDataTS, Scalar, TimeSeries
17
18
  from .features import ShareAllocationModel
18
- from .structure import Element, ElementModel, Interface, Model, SystemModel, register_class_for_io
19
+ from .structure import Element, ElementModel, Model, SystemModel, register_class_for_io
19
20
 
20
21
  if TYPE_CHECKING:
22
+ from collections.abc import Iterator
23
+
21
24
  from .flow_system import FlowSystem
22
25
 
23
26
  logger = logging.getLogger('flixopt')
@@ -26,8 +29,107 @@ logger = logging.getLogger('flixopt')
26
29
  @register_class_for_io
27
30
  class Effect(Element):
28
31
  """
29
- Effect, i.g. costs, CO2 emissions, area, ...
30
- Components, FLows, and so on can contribute to an Effect. One Effect is chosen as the Objective of the Optimization
32
+ Represents system-wide impacts like costs, emissions, resource consumption, or other effects.
33
+
34
+ Effects capture the broader impacts of system operation and investment decisions beyond
35
+ the primary energy/material flows. Each Effect accumulates contributions from Components,
36
+ Flows, and other system elements. One Effect is typically chosen as the optimization
37
+ objective, while others can serve as constraints or tracking metrics.
38
+
39
+ Effects support comprehensive modeling including operational and investment contributions,
40
+ cross-effect relationships (e.g., carbon pricing), and flexible constraint formulation.
41
+
42
+ Args:
43
+ label: The label of the Element. Used to identify it in the FlowSystem.
44
+ unit: The unit of the effect (e.g., '€', 'kg_CO2', 'kWh_primary', 'm²').
45
+ This is informative only and does not affect optimization calculations.
46
+ description: Descriptive name explaining what this effect represents.
47
+ is_standard: If True, this is a standard effect allowing direct value input
48
+ without effect dictionaries. Used for simplified effect specification (and less boilerplate code).
49
+ is_objective: If True, this effect serves as the optimization objective function.
50
+ Only one effect can be marked as objective per optimization.
51
+ specific_share_to_other_effects_operation: Operational cross-effect contributions.
52
+ Maps this effect's operational values to contributions to other effects
53
+ specific_share_to_other_effects_invest: Investment cross-effect contributions.
54
+ Maps this effect's investment values to contributions to other effects.
55
+ minimum_operation: Minimum allowed total operational contribution across all timesteps.
56
+ maximum_operation: Maximum allowed total operational contribution across all timesteps.
57
+ minimum_operation_per_hour: Minimum allowed operational contribution per timestep.
58
+ maximum_operation_per_hour: Maximum allowed operational contribution per timestep.
59
+ minimum_invest: Minimum allowed total investment contribution.
60
+ maximum_invest: Maximum allowed total investment contribution.
61
+ minimum_total: Minimum allowed total effect (operation + investment combined).
62
+ maximum_total: Maximum allowed total effect (operation + investment combined).
63
+ meta_data: Used to store additional information. Not used internally but saved
64
+ in results. Only use Python native types.
65
+
66
+ Examples:
67
+ Basic cost objective:
68
+
69
+ ```python
70
+ cost_effect = Effect(label='system_costs', unit='€', description='Total system costs', is_objective=True)
71
+ ```
72
+
73
+ CO2 emissions with carbon pricing:
74
+
75
+ ```python
76
+ co2_effect = Effect(
77
+ label='co2_emissions',
78
+ unit='kg_CO2',
79
+ description='Carbon dioxide emissions',
80
+ specific_share_to_other_effects_operation={'costs': 50}, # €50/t_CO2
81
+ maximum_total=1_000_000, # 1000 t CO2 annual limit
82
+ )
83
+ ```
84
+
85
+ Land use constraint:
86
+
87
+ ```python
88
+ land_use = Effect(
89
+ label='land_usage',
90
+ unit='m²',
91
+ description='Land area requirement',
92
+ maximum_total=50_000, # Maximum 5 hectares available
93
+ )
94
+ ```
95
+
96
+ Primary energy tracking:
97
+
98
+ ```python
99
+ primary_energy = Effect(
100
+ label='primary_energy',
101
+ unit='kWh_primary',
102
+ description='Primary energy consumption',
103
+ specific_share_to_other_effects_operation={'costs': 0.08}, # €0.08/kWh
104
+ )
105
+ ```
106
+
107
+ Water consumption with tiered constraints:
108
+
109
+ ```python
110
+ water_usage = Effect(
111
+ label='water_consumption',
112
+ unit='m³',
113
+ description='Industrial water usage',
114
+ minimum_operation_per_hour=10, # Minimum 10 m³/h for process stability
115
+ maximum_operation_per_hour=500, # Maximum 500 m³/h capacity limit
116
+ maximum_total=100_000, # Annual permit limit: 100,000 m³
117
+ )
118
+ ```
119
+
120
+ Note:
121
+ Effect bounds can be None to indicate no constraint in that direction.
122
+
123
+ Cross-effect relationships enable sophisticated modeling like carbon pricing,
124
+ resource valuation, or multi-criteria optimization with weighted objectives.
125
+
126
+ The unit field is purely informational - ensure dimensional consistency
127
+ across all contributions to each effect manually.
128
+
129
+ Effects are accumulated as:
130
+ - Total = Σ(operational contributions) + Σ(investment contributions)
131
+ - Cross-effects add to target effects based on specific_share ratios
132
+
31
133
  """
32
134
 
33
135
  def __init__(
@@ -35,41 +137,20 @@ class Effect(Element):
35
137
  label: str,
36
138
  unit: str,
37
139
  description: str,
38
- meta_data: Optional[Dict] = None,
140
+ meta_data: dict | None = None,
39
141
  is_standard: bool = False,
40
142
  is_objective: bool = False,
41
- specific_share_to_other_effects_operation: Optional['EffectValuesUser'] = None,
42
- specific_share_to_other_effects_invest: Optional['EffectValuesUser'] = None,
43
- minimum_operation: Optional[Scalar] = None,
44
- maximum_operation: Optional[Scalar] = None,
45
- minimum_invest: Optional[Scalar] = None,
46
- maximum_invest: Optional[Scalar] = None,
47
- minimum_operation_per_hour: Optional[NumericDataTS] = None,
48
- maximum_operation_per_hour: Optional[NumericDataTS] = None,
49
- minimum_total: Optional[Scalar] = None,
50
- maximum_total: Optional[Scalar] = None,
143
+ specific_share_to_other_effects_operation: EffectValuesUser | None = None,
144
+ specific_share_to_other_effects_invest: EffectValuesUser | None = None,
145
+ minimum_operation: Scalar | None = None,
146
+ maximum_operation: Scalar | None = None,
147
+ minimum_invest: Scalar | None = None,
148
+ maximum_invest: Scalar | None = None,
149
+ minimum_operation_per_hour: NumericDataTS | None = None,
150
+ maximum_operation_per_hour: NumericDataTS | None = None,
151
+ minimum_total: Scalar | None = None,
152
+ maximum_total: Scalar | None = None,
51
153
  ):
52
- """
53
- Args:
54
- label: The label of the Element. Used to identify it in the FlowSystem
55
- unit: The unit of effect, i.g. €, kg_CO2, kWh_primaryEnergy
56
- description: The long name
57
- is_standard: true, if Standard-Effect (for direct input of value without effect (alternatively to dict)) , else false
58
- is_objective: true, if optimization target
59
- specific_share_to_other_effects_operation: {effectType: TS, ...}, i.g. 180 €/t_CO2, input as {costs: 180}, optional
60
- share to other effects (only operation)
61
- specific_share_to_other_effects_invest: {effectType: TS, ...}, i.g. 180 €/t_CO2, input as {costs: 180}, optional
62
- share to other effects (only invest).
63
- minimum_operation: minimal sum (only operation) of the effect.
64
- maximum_operation: maximal sum (nur operation) of the effect.
65
- minimum_operation_per_hour: max. value per hour (only operation) of effect (=sum of all effect-shares) for each timestep!
66
- maximum_operation_per_hour: min. value per hour (only operation) of effect (=sum of all effect-shares) for each timestep!
67
- minimum_invest: minimal sum (only invest) of the effect
68
- maximum_invest: maximal sum (only invest) of the effect
69
- minimum_total: min sum of effect (invest+operation).
70
- maximum_total: max sum of effect (invest+operation).
71
- meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
72
- """
73
154
  super().__init__(label, meta_data=meta_data)
74
155
  self.label = label
75
156
  self.unit = unit
@@ -82,14 +163,14 @@ class Effect(Element):
82
163
  self.specific_share_to_other_effects_invest: EffectValuesUser = specific_share_to_other_effects_invest or {}
83
164
  self.minimum_operation = minimum_operation
84
165
  self.maximum_operation = maximum_operation
85
- self.minimum_operation_per_hour: NumericDataTS = minimum_operation_per_hour
86
- self.maximum_operation_per_hour: NumericDataTS = maximum_operation_per_hour
166
+ self.minimum_operation_per_hour = minimum_operation_per_hour
167
+ self.maximum_operation_per_hour = maximum_operation_per_hour
87
168
  self.minimum_invest = minimum_invest
88
169
  self.maximum_invest = maximum_invest
89
170
  self.minimum_total = minimum_total
90
171
  self.maximum_total = maximum_total
91
172
 
92
- def transform_data(self, flow_system: 'FlowSystem'):
173
+ def transform_data(self, flow_system: FlowSystem):
93
174
  self.minimum_operation_per_hour = flow_system.create_time_series(
94
175
  f'{self.label_full}|minimum_operation_per_hour', self.minimum_operation_per_hour
95
176
  )
@@ -102,7 +183,7 @@ class Effect(Element):
102
183
  f'{self.label_full}|operation->', self.specific_share_to_other_effects_operation, 'operation'
103
184
  )
104
185
 
105
- def create_model(self, model: SystemModel) -> 'EffectModel':
186
+ def create_model(self, model: SystemModel) -> EffectModel:
106
187
  self._plausibility_checks()
107
188
  self.model = EffectModel(model, self)
108
189
  return self.model
@@ -116,7 +197,7 @@ class EffectModel(ElementModel):
116
197
  def __init__(self, model: SystemModel, element: Effect):
117
198
  super().__init__(model, element)
118
199
  self.element: Effect = element
119
- self.total: Optional[linopy.Variable] = None
200
+ self.total: linopy.Variable | None = None
120
201
  self.invest: ShareAllocationModel = self.add(
121
202
  ShareAllocationModel(
122
203
  self._model,
@@ -169,13 +250,13 @@ class EffectModel(ElementModel):
169
250
  )
170
251
 
171
252
 
172
- EffectValuesExpr = Dict[str, linopy.LinearExpression] # Used to create Shares
173
- EffectTimeSeries = Dict[str, TimeSeries] # Used internally to index values
174
- EffectValuesDict = Dict[str, NumericDataTS] # How effect values are stored
175
- EffectValuesUser = Union[NumericDataTS, Dict[str, NumericDataTS]] # User-specified Shares to Effects
253
+ EffectValuesExpr = dict[str, linopy.LinearExpression] # Used to create Shares
254
+ EffectTimeSeries = dict[str, TimeSeries] # Used internally to index values
255
+ EffectValuesDict = dict[str, NumericDataTS] # How effect values are stored
256
+ EffectValuesUser = NumericDataTS | dict[str, NumericDataTS] # User-specified Shares to Effects
176
257
  """ This datatype is used to define the share to an effect by a certain attribute. """
177
258
 
178
- EffectValuesUserScalar = Union[Scalar, Dict[str, Scalar]] # User-specified Shares to Effects
259
+ EffectValuesUserScalar = Scalar | dict[str, Scalar] # User-specified Shares to Effects
179
260
  """ This datatype is used to define the share to an effect by a certain attribute. Only scalars are allowed. """
180
261
 
181
262
 
@@ -184,15 +265,15 @@ class EffectCollection:
184
265
  Handling all Effects
185
266
  """
186
267
 
187
- def __init__(self, *effects: List[Effect]):
268
+ def __init__(self, *effects: Effect):
188
269
  self._effects = {}
189
- self._standard_effect: Optional[Effect] = None
190
- self._objective_effect: Optional[Effect] = None
270
+ self._standard_effect: Effect | None = None
271
+ self._objective_effect: Effect | None = None
191
272
 
192
- self.model: Optional[EffectCollectionModel] = None
273
+ self.model: EffectCollectionModel | None = None
193
274
  self.add_effects(*effects)
194
275
 
195
- def create_model(self, model: SystemModel) -> 'EffectCollectionModel':
276
+ def create_model(self, model: SystemModel) -> EffectCollectionModel:
196
277
  self._plausibility_checks()
197
278
  self.model = EffectCollectionModel(model, self)
198
279
  return self.model
@@ -208,7 +289,7 @@ class EffectCollection:
208
289
  self._effects[effect.label] = effect
209
290
  logger.info(f'Registered new Effect: {effect.label}')
210
291
 
211
- def create_effect_values_dict(self, effect_values_user: EffectValuesUser) -> Optional[EffectValuesDict]:
292
+ def create_effect_values_dict(self, effect_values_user: EffectValuesUser) -> EffectValuesDict | None:
212
293
  """
213
294
  Converts effect values into a dictionary. If a scalar is provided, it is associated with a default effect type.
214
295
 
@@ -224,7 +305,7 @@ class EffectCollection:
224
305
  A dictionary with None or Effect as the key, or None if input is None.
225
306
  """
226
307
 
227
- def get_effect_label(eff: Union[Effect, str]) -> str:
308
+ def get_effect_label(eff: Effect | str) -> str:
228
309
  """Temporary function to get the label of an effect and warn for deprecation"""
229
310
  if isinstance(eff, Effect):
230
311
  warnings.warn(
@@ -233,7 +314,7 @@ class EffectCollection:
233
314
  UserWarning,
234
315
  stacklevel=2,
235
316
  )
236
- return eff.label_full
317
+ return eff.label
237
318
  else:
238
319
  return eff
239
320
 
@@ -241,7 +322,7 @@ class EffectCollection:
241
322
  return None
242
323
  if isinstance(effect_values_user, dict):
243
324
  return {get_effect_label(effect): value for effect, value in effect_values_user.items()}
244
- return {self.standard_effect.label_full: effect_values_user}
325
+ return {self.standard_effect.label: effect_values_user}
245
326
 
246
327
  def _plausibility_checks(self) -> None:
247
328
  # Check circular loops in effects:
@@ -258,15 +339,15 @@ class EffectCollection:
258
339
  # operation:
259
340
  for target_effect in effect.specific_share_to_other_effects_operation.keys():
260
341
  assert effect not in self[target_effect].specific_share_to_other_effects_operation.keys(), (
261
- f'Error: circular operation-shares \n{error_str(target_effect.label, target_effect.label)}'
342
+ f'Error: circular operation-shares \n{error_str(effect.label, self[target_effect].label)}'
262
343
  )
263
344
  # invest:
264
345
  for target_effect in effect.specific_share_to_other_effects_invest.keys():
265
346
  assert effect not in self[target_effect].specific_share_to_other_effects_invest.keys(), (
266
- f'Error: circular invest-shares \n{error_str(target_effect.label, target_effect.label)}'
347
+ f'Error: circular invest-shares \n{error_str(effect.label, self[target_effect].label)}'
267
348
  )
268
349
 
269
- def __getitem__(self, effect: Union[str, Effect]) -> 'Effect':
350
+ def __getitem__(self, effect: str | Effect | None) -> Effect:
270
351
  """
271
352
  Get an effect by label, or return the standard effect if None is passed
272
353
 
@@ -292,7 +373,7 @@ class EffectCollection:
292
373
  def __len__(self) -> int:
293
374
  return len(self._effects)
294
375
 
295
- def __contains__(self, item: Union[str, 'Effect']) -> bool:
376
+ def __contains__(self, item: str | Effect) -> bool:
296
377
  """Check if the effect exists. Checks for label or object"""
297
378
  if isinstance(item, str):
298
379
  return item in self.effects # Check if the label exists
@@ -301,7 +382,7 @@ class EffectCollection:
301
382
  return False
302
383
 
303
384
  @property
304
- def effects(self) -> Dict[str, Effect]:
385
+ def effects(self) -> dict[str, Effect]:
305
386
  return self._effects
306
387
 
307
388
  @property
@@ -337,7 +418,7 @@ class EffectCollectionModel(Model):
337
418
  def __init__(self, model: SystemModel, effects: EffectCollection):
338
419
  super().__init__(model, label_of_element='Effects')
339
420
  self.effects = effects
340
- self.penalty: Optional[ShareAllocationModel] = None
421
+ self.penalty: ShareAllocationModel | None = None
341
422
 
342
423
  def add_share_to_effects(
343
424
  self,