flixopt 2.1.6__py3-none-any.whl → 2.1.8__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.

Files changed (45) hide show
  1. docs/examples/00-Minimal Example.md +1 -1
  2. docs/examples/01-Basic Example.md +1 -1
  3. docs/examples/02-Complex Example.md +1 -1
  4. docs/examples/index.md +1 -1
  5. docs/faq/contribute.md +26 -14
  6. docs/faq/index.md +1 -1
  7. docs/javascripts/mathjax.js +1 -1
  8. docs/user-guide/Mathematical Notation/Bus.md +1 -1
  9. docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +21 -21
  10. docs/user-guide/Mathematical Notation/Flow.md +3 -3
  11. docs/user-guide/Mathematical Notation/InvestParameters.md +3 -0
  12. docs/user-guide/Mathematical Notation/LinearConverter.md +5 -5
  13. docs/user-guide/Mathematical Notation/OnOffParameters.md +3 -0
  14. docs/user-guide/Mathematical Notation/Piecewise.md +1 -1
  15. docs/user-guide/Mathematical Notation/Storage.md +2 -2
  16. docs/user-guide/Mathematical Notation/index.md +1 -1
  17. docs/user-guide/Mathematical Notation/others.md +1 -1
  18. docs/user-guide/index.md +2 -2
  19. flixopt/__init__.py +4 -0
  20. flixopt/aggregation.py +33 -32
  21. flixopt/calculation.py +161 -65
  22. flixopt/components.py +687 -154
  23. flixopt/config.py +17 -8
  24. flixopt/core.py +69 -60
  25. flixopt/effects.py +146 -64
  26. flixopt/elements.py +297 -110
  27. flixopt/features.py +78 -71
  28. flixopt/flow_system.py +72 -50
  29. flixopt/interface.py +952 -113
  30. flixopt/io.py +15 -10
  31. flixopt/linear_converters.py +373 -81
  32. flixopt/network_app.py +445 -266
  33. flixopt/plotting.py +215 -87
  34. flixopt/results.py +382 -209
  35. flixopt/solvers.py +25 -21
  36. flixopt/structure.py +41 -39
  37. flixopt/utils.py +10 -7
  38. {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/METADATA +64 -53
  39. flixopt-2.1.8.dist-info/RECORD +56 -0
  40. scripts/extract_release_notes.py +5 -5
  41. scripts/gen_ref_pages.py +1 -1
  42. flixopt-2.1.6.dist-info/RECORD +0 -54
  43. {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/WHEEL +0 -0
  44. {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/licenses/LICENSE +0 -0
  45. {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/top_level.txt +0 -0
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,26 +163,27 @@ 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
  )
96
177
  self.maximum_operation_per_hour = flow_system.create_time_series(
97
- f'{self.label_full}|maximum_operation_per_hour', self.maximum_operation_per_hour,
178
+ f'{self.label_full}|maximum_operation_per_hour',
179
+ self.maximum_operation_per_hour,
98
180
  )
99
181
 
100
182
  self.specific_share_to_other_effects_operation = flow_system.create_effect_time_series(
101
183
  f'{self.label_full}|operation->', self.specific_share_to_other_effects_operation, 'operation'
102
184
  )
103
185
 
104
- def create_model(self, model: SystemModel) -> 'EffectModel':
186
+ def create_model(self, model: SystemModel) -> EffectModel:
105
187
  self._plausibility_checks()
106
188
  self.model = EffectModel(model, self)
107
189
  return self.model
@@ -115,7 +197,7 @@ class EffectModel(ElementModel):
115
197
  def __init__(self, model: SystemModel, element: Effect):
116
198
  super().__init__(model, element)
117
199
  self.element: Effect = element
118
- self.total: Optional[linopy.Variable] = None
200
+ self.total: linopy.Variable | None = None
119
201
  self.invest: ShareAllocationModel = self.add(
120
202
  ShareAllocationModel(
121
203
  self._model,
@@ -168,13 +250,13 @@ class EffectModel(ElementModel):
168
250
  )
169
251
 
170
252
 
171
- EffectValuesExpr = Dict[str, linopy.LinearExpression] # Used to create Shares
172
- EffectTimeSeries = Dict[str, TimeSeries] # Used internally to index values
173
- EffectValuesDict = Dict[str, NumericDataTS] # How effect values are stored
174
- 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
175
257
  """ This datatype is used to define the share to an effect by a certain attribute. """
176
258
 
177
- EffectValuesUserScalar = Union[Scalar, Dict[str, Scalar]] # User-specified Shares to Effects
259
+ EffectValuesUserScalar = Scalar | dict[str, Scalar] # User-specified Shares to Effects
178
260
  """ This datatype is used to define the share to an effect by a certain attribute. Only scalars are allowed. """
179
261
 
180
262
 
@@ -183,15 +265,15 @@ class EffectCollection:
183
265
  Handling all Effects
184
266
  """
185
267
 
186
- def __init__(self, *effects: List[Effect]):
268
+ def __init__(self, *effects: Effect):
187
269
  self._effects = {}
188
- self._standard_effect: Optional[Effect] = None
189
- self._objective_effect: Optional[Effect] = None
270
+ self._standard_effect: Effect | None = None
271
+ self._objective_effect: Effect | None = None
190
272
 
191
- self.model: Optional[EffectCollectionModel] = None
273
+ self.model: EffectCollectionModel | None = None
192
274
  self.add_effects(*effects)
193
275
 
194
- def create_model(self, model: SystemModel) -> 'EffectCollectionModel':
276
+ def create_model(self, model: SystemModel) -> EffectCollectionModel:
195
277
  self._plausibility_checks()
196
278
  self.model = EffectCollectionModel(model, self)
197
279
  return self.model
@@ -207,7 +289,7 @@ class EffectCollection:
207
289
  self._effects[effect.label] = effect
208
290
  logger.info(f'Registered new Effect: {effect.label}')
209
291
 
210
- 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:
211
293
  """
212
294
  Converts effect values into a dictionary. If a scalar is provided, it is associated with a default effect type.
213
295
 
@@ -223,7 +305,7 @@ class EffectCollection:
223
305
  A dictionary with None or Effect as the key, or None if input is None.
224
306
  """
225
307
 
226
- def get_effect_label(eff: Union[Effect, str]) -> str:
308
+ def get_effect_label(eff: Effect | str) -> str:
227
309
  """Temporary function to get the label of an effect and warn for deprecation"""
228
310
  if isinstance(eff, Effect):
229
311
  warnings.warn(
@@ -232,7 +314,7 @@ class EffectCollection:
232
314
  UserWarning,
233
315
  stacklevel=2,
234
316
  )
235
- return eff.label_full
317
+ return eff.label
236
318
  else:
237
319
  return eff
238
320
 
@@ -240,7 +322,7 @@ class EffectCollection:
240
322
  return None
241
323
  if isinstance(effect_values_user, dict):
242
324
  return {get_effect_label(effect): value for effect, value in effect_values_user.items()}
243
- return {self.standard_effect.label_full: effect_values_user}
325
+ return {self.standard_effect.label: effect_values_user}
244
326
 
245
327
  def _plausibility_checks(self) -> None:
246
328
  # Check circular loops in effects:
@@ -257,15 +339,15 @@ class EffectCollection:
257
339
  # operation:
258
340
  for target_effect in effect.specific_share_to_other_effects_operation.keys():
259
341
  assert effect not in self[target_effect].specific_share_to_other_effects_operation.keys(), (
260
- 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)}'
261
343
  )
262
344
  # invest:
263
345
  for target_effect in effect.specific_share_to_other_effects_invest.keys():
264
346
  assert effect not in self[target_effect].specific_share_to_other_effects_invest.keys(), (
265
- 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)}'
266
348
  )
267
349
 
268
- def __getitem__(self, effect: Union[str, Effect]) -> 'Effect':
350
+ def __getitem__(self, effect: str | Effect | None) -> Effect:
269
351
  """
270
352
  Get an effect by label, or return the standard effect if None is passed
271
353
 
@@ -291,7 +373,7 @@ class EffectCollection:
291
373
  def __len__(self) -> int:
292
374
  return len(self._effects)
293
375
 
294
- def __contains__(self, item: Union[str, 'Effect']) -> bool:
376
+ def __contains__(self, item: str | Effect) -> bool:
295
377
  """Check if the effect exists. Checks for label or object"""
296
378
  if isinstance(item, str):
297
379
  return item in self.effects # Check if the label exists
@@ -300,7 +382,7 @@ class EffectCollection:
300
382
  return False
301
383
 
302
384
  @property
303
- def effects(self) -> Dict[str, Effect]:
385
+ def effects(self) -> dict[str, Effect]:
304
386
  return self._effects
305
387
 
306
388
  @property
@@ -336,7 +418,7 @@ class EffectCollectionModel(Model):
336
418
  def __init__(self, model: SystemModel, effects: EffectCollection):
337
419
  super().__init__(model, label_of_element='Effects')
338
420
  self.effects = effects
339
- self.penalty: Optional[ShareAllocationModel] = None
421
+ self.penalty: ShareAllocationModel | None = None
340
422
 
341
423
  def add_share_to_effects(
342
424
  self,