flixopt 2.2.0b0__py3-none-any.whl → 3.0.0__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 (63) hide show
  1. flixopt/__init__.py +35 -1
  2. flixopt/aggregation.py +60 -81
  3. flixopt/calculation.py +381 -196
  4. flixopt/components.py +1022 -359
  5. flixopt/config.py +553 -191
  6. flixopt/core.py +475 -1315
  7. flixopt/effects.py +477 -214
  8. flixopt/elements.py +591 -344
  9. flixopt/features.py +403 -957
  10. flixopt/flow_system.py +781 -293
  11. flixopt/interface.py +1159 -189
  12. flixopt/io.py +50 -55
  13. flixopt/linear_converters.py +384 -92
  14. flixopt/modeling.py +759 -0
  15. flixopt/network_app.py +789 -0
  16. flixopt/plotting.py +273 -135
  17. flixopt/results.py +639 -383
  18. flixopt/solvers.py +25 -21
  19. flixopt/structure.py +928 -442
  20. flixopt/utils.py +34 -5
  21. flixopt-3.0.0.dist-info/METADATA +209 -0
  22. flixopt-3.0.0.dist-info/RECORD +26 -0
  23. {flixopt-2.2.0b0.dist-info → flixopt-3.0.0.dist-info}/WHEEL +1 -1
  24. flixopt-3.0.0.dist-info/top_level.txt +1 -0
  25. docs/examples/00-Minimal Example.md +0 -5
  26. docs/examples/01-Basic Example.md +0 -5
  27. docs/examples/02-Complex Example.md +0 -10
  28. docs/examples/03-Calculation Modes.md +0 -5
  29. docs/examples/index.md +0 -5
  30. docs/faq/contribute.md +0 -49
  31. docs/faq/index.md +0 -3
  32. docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
  33. docs/images/architecture_flixOpt.png +0 -0
  34. docs/images/flixopt-icon.svg +0 -1
  35. docs/javascripts/mathjax.js +0 -18
  36. docs/release-notes/_template.txt +0 -32
  37. docs/release-notes/index.md +0 -7
  38. docs/release-notes/v2.0.0.md +0 -93
  39. docs/release-notes/v2.0.1.md +0 -12
  40. docs/release-notes/v2.1.0.md +0 -31
  41. docs/release-notes/v2.2.0.md +0 -55
  42. docs/user-guide/Mathematical Notation/Bus.md +0 -33
  43. docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +0 -132
  44. docs/user-guide/Mathematical Notation/Flow.md +0 -26
  45. docs/user-guide/Mathematical Notation/Investment.md +0 -115
  46. docs/user-guide/Mathematical Notation/LinearConverter.md +0 -21
  47. docs/user-guide/Mathematical Notation/Piecewise.md +0 -49
  48. docs/user-guide/Mathematical Notation/Storage.md +0 -44
  49. docs/user-guide/Mathematical Notation/index.md +0 -22
  50. docs/user-guide/Mathematical Notation/others.md +0 -3
  51. docs/user-guide/index.md +0 -124
  52. flixopt/config.yaml +0 -10
  53. flixopt-2.2.0b0.dist-info/METADATA +0 -146
  54. flixopt-2.2.0b0.dist-info/RECORD +0 -59
  55. flixopt-2.2.0b0.dist-info/top_level.txt +0 -5
  56. pics/architecture_flixOpt-pre2.0.0.png +0 -0
  57. pics/architecture_flixOpt.png +0 -0
  58. pics/flixOpt_plotting.jpg +0 -0
  59. pics/flixopt-icon.svg +0 -1
  60. pics/pics.pptx +0 -0
  61. scripts/gen_ref_pages.py +0 -54
  62. tests/ressources/Zeitreihen2020.csv +0 -35137
  63. {flixopt-2.2.0b0.dist-info → flixopt-3.0.0.dist-info}/licenses/LICENSE +0 -0
flixopt/effects.py CHANGED
@@ -5,19 +5,24 @@ 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, Set, Tuple, Union
12
+ from collections import deque
13
+ from typing import TYPE_CHECKING, Literal
11
14
 
12
15
  import linopy
13
16
  import numpy as np
14
17
  import xarray as xr
15
18
 
16
- from .core import NumericDataTS, ScenarioData, TimeSeries, TimeSeriesCollection, TimestepData, extract_data
19
+ from .core import PeriodicDataUser, Scalar, TemporalData, TemporalDataUser
17
20
  from .features import ShareAllocationModel
18
- from .structure import Element, ElementModel, Interface, Model, SystemModel, register_class_for_io
21
+ from .structure import Element, ElementModel, FlowSystemModel, Submodel, register_class_for_io
19
22
 
20
23
  if TYPE_CHECKING:
24
+ from collections.abc import Iterator
25
+
21
26
  from .flow_system import FlowSystem
22
27
 
23
28
  logger = logging.getLogger('flixopt')
@@ -26,8 +31,132 @@ logger = logging.getLogger('flixopt')
26
31
  @register_class_for_io
27
32
  class Effect(Element):
28
33
  """
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
34
+ Represents system-wide impacts like costs, emissions, resource consumption, or other effects.
35
+
36
+ Effects capture the broader impacts of system operation and investment decisions beyond
37
+ the primary energy/material flows. Each Effect accumulates contributions from Components,
38
+ Flows, and other system elements. One Effect is typically chosen as the optimization
39
+ objective, while others can serve as constraints or tracking metrics.
40
+
41
+ Effects support comprehensive modeling including operational and investment contributions,
42
+ cross-effect relationships (e.g., carbon pricing), and flexible constraint formulation.
43
+
44
+ Args:
45
+ label: The label of the Element. Used to identify it in the FlowSystem.
46
+ unit: The unit of the effect (e.g., '€', 'kg_CO2', 'kWh_primary', 'm²').
47
+ This is informative only and does not affect optimization calculations.
48
+ description: Descriptive name explaining what this effect represents.
49
+ is_standard: If True, this is a standard effect allowing direct value input
50
+ without effect dictionaries. Used for simplified effect specification (and less boilerplate code).
51
+ is_objective: If True, this effect serves as the optimization objective function.
52
+ Only one effect can be marked as objective per optimization.
53
+ share_from_temporal: Temporal cross-effect contributions.
54
+ Maps temporal contributions from other effects to this effect
55
+ share_from_periodic: Periodic cross-effect contributions.
56
+ Maps periodic contributions from other effects to this effect.
57
+ minimum_temporal: Minimum allowed total contribution across all timesteps.
58
+ maximum_temporal: Maximum allowed total contribution across all timesteps.
59
+ minimum_per_hour: Minimum allowed contribution per hour.
60
+ maximum_per_hour: Maximum allowed contribution per hour.
61
+ minimum_periodic: Minimum allowed total periodic contribution.
62
+ maximum_periodic: Maximum allowed total periodic contribution.
63
+ minimum_total: Minimum allowed total effect (temporal + periodic combined).
64
+ maximum_total: Maximum allowed total effect (temporal + periodic combined).
65
+ meta_data: Used to store additional information. Not used internally but saved
66
+ in results. Only use Python native types.
67
+
68
+ **Deprecated Parameters** (for backwards compatibility):
69
+ minimum_operation: Use `minimum_temporal` instead.
70
+ maximum_operation: Use `maximum_temporal` instead.
71
+ minimum_invest: Use `minimum_periodic` instead.
72
+ maximum_invest: Use `maximum_periodic` instead.
73
+ minimum_operation_per_hour: Use `minimum_per_hour` instead.
74
+ maximum_operation_per_hour: Use `maximum_per_hour` instead.
75
+
76
+ Examples:
77
+ Basic cost objective:
78
+
79
+ ```python
80
+ cost_effect = Effect(
81
+ label='system_costs',
82
+ unit='€',
83
+ description='Total system costs',
84
+ is_objective=True,
85
+ )
86
+ ```
87
+
88
+ CO2 emissions:
89
+
90
+ ```python
91
+ co2_effect = Effect(
92
+ label='CO2',
93
+ unit='kg_CO2',
94
+ description='Carbon dioxide emissions',
95
+ maximum_total=1_000_000, # 1000 t CO2 annual limit
96
+ )
97
+ ```
98
+
99
+ Land use constraint:
100
+
101
+ ```python
102
+ land_use = Effect(
103
+ label='land_usage',
104
+ unit='m²',
105
+ description='Land area requirement',
106
+ maximum_total=50_000, # Maximum 5 hectares available
107
+ )
108
+ ```
109
+
110
+ Primary energy tracking:
111
+
112
+ ```python
113
+ primary_energy = Effect(
114
+ label='primary_energy',
115
+ unit='kWh_primary',
116
+ description='Primary energy consumption',
117
+ )
118
+ ```
119
+
120
+ Cost objective with carbon and primary energy pricing:
121
+
122
+ ```python
123
+ cost_effect = Effect(
124
+ label='system_costs',
125
+ unit='€',
126
+ description='Total system costs',
127
+ is_objective=True,
128
+ share_from_temporal={
129
+ 'primary_energy': 0.08, # 0.08 €/kWh_primary
130
+ 'CO2': 0.2, # Carbon pricing: 0.2 €/kg_CO2 into costs if used on a cost effect
131
+ },
132
+ )
133
+ ```
134
+
135
+ Water consumption with tiered constraints:
136
+
137
+ ```python
138
+ water_usage = Effect(
139
+ label='water_consumption',
140
+ unit='m³',
141
+ description='Industrial water usage',
142
+ minimum_per_hour=10, # Minimum 10 m³/h for process stability
143
+ maximum_per_hour=500, # Maximum 500 m³/h capacity limit
144
+ maximum_total=100_000, # Annual permit limit: 100,000 m³
145
+ )
146
+ ```
147
+
148
+ Note:
149
+ Effect bounds can be None to indicate no constraint in that direction.
150
+
151
+ Cross-effect relationships enable sophisticated modeling like carbon pricing,
152
+ resource valuation, or multi-criteria optimization with weighted objectives.
153
+
154
+ The unit field is purely informational - ensure dimensional consistency
155
+ across all contributions to each effect manually.
156
+
157
+ Effects are accumulated as:
158
+ - Total = Σ(temporal contributions) + Σ(periodic contributions)
159
+
31
160
  """
32
161
 
33
162
  def __init__(
@@ -35,100 +164,223 @@ class Effect(Element):
35
164
  label: str,
36
165
  unit: str,
37
166
  description: str,
38
- meta_data: Optional[Dict] = None,
167
+ meta_data: dict | None = None,
39
168
  is_standard: bool = False,
40
169
  is_objective: bool = False,
41
- specific_share_to_other_effects_operation: Optional['EffectValuesUserTimestep'] = None,
42
- specific_share_to_other_effects_invest: Optional['EffectValuesUserScenario'] = None,
43
- minimum_operation: Optional[ScenarioData] = None,
44
- maximum_operation: Optional[ScenarioData] = None,
45
- minimum_invest: Optional[ScenarioData] = None,
46
- maximum_invest: Optional[ScenarioData] = None,
47
- minimum_operation_per_hour: Optional[NumericDataTS] = None,
48
- maximum_operation_per_hour: Optional[NumericDataTS] = None,
49
- minimum_total: Optional[ScenarioData] = None,
50
- maximum_total: Optional[ScenarioData] = None,
170
+ share_from_temporal: TemporalEffectsUser | None = None,
171
+ share_from_periodic: PeriodicEffectsUser | None = None,
172
+ minimum_temporal: PeriodicEffectsUser | None = None,
173
+ maximum_temporal: PeriodicEffectsUser | None = None,
174
+ minimum_periodic: PeriodicEffectsUser | None = None,
175
+ maximum_periodic: PeriodicEffectsUser | None = None,
176
+ minimum_per_hour: TemporalDataUser | None = None,
177
+ maximum_per_hour: TemporalDataUser | None = None,
178
+ minimum_total: Scalar | None = None,
179
+ maximum_total: Scalar | None = None,
180
+ **kwargs,
51
181
  ):
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
182
  super().__init__(label, meta_data=meta_data)
74
- self.label = label
75
183
  self.unit = unit
76
184
  self.description = description
77
185
  self.is_standard = is_standard
78
186
  self.is_objective = is_objective
79
- self.specific_share_to_other_effects_operation: EffectValuesUserTimestep = (
80
- specific_share_to_other_effects_operation or {}
81
- )
82
- self.specific_share_to_other_effects_invest: EffectValuesUserTimestep = (
83
- specific_share_to_other_effects_invest or {}
84
- )
85
- self.minimum_operation = minimum_operation
86
- self.maximum_operation = maximum_operation
87
- self.minimum_operation_per_hour: NumericDataTS = minimum_operation_per_hour
88
- self.maximum_operation_per_hour: NumericDataTS = maximum_operation_per_hour
89
- self.minimum_invest = minimum_invest
90
- self.maximum_invest = maximum_invest
187
+ self.share_from_temporal: TemporalEffectsUser = share_from_temporal if share_from_temporal is not None else {}
188
+ self.share_from_periodic: PeriodicEffectsUser = share_from_periodic if share_from_periodic is not None else {}
189
+
190
+ # Handle backwards compatibility for deprecated parameters using centralized helper
191
+ minimum_temporal = self._handle_deprecated_kwarg(
192
+ kwargs, 'minimum_operation', 'minimum_temporal', minimum_temporal
193
+ )
194
+ maximum_temporal = self._handle_deprecated_kwarg(
195
+ kwargs, 'maximum_operation', 'maximum_temporal', maximum_temporal
196
+ )
197
+ minimum_periodic = self._handle_deprecated_kwarg(kwargs, 'minimum_invest', 'minimum_periodic', minimum_periodic)
198
+ maximum_periodic = self._handle_deprecated_kwarg(kwargs, 'maximum_invest', 'maximum_periodic', maximum_periodic)
199
+ minimum_per_hour = self._handle_deprecated_kwarg(
200
+ kwargs, 'minimum_operation_per_hour', 'minimum_per_hour', minimum_per_hour
201
+ )
202
+ maximum_per_hour = self._handle_deprecated_kwarg(
203
+ kwargs, 'maximum_operation_per_hour', 'maximum_per_hour', maximum_per_hour
204
+ )
205
+
206
+ # Validate any remaining unexpected kwargs
207
+ self._validate_kwargs(kwargs)
208
+
209
+ # Set attributes directly
210
+ self.minimum_temporal = minimum_temporal
211
+ self.maximum_temporal = maximum_temporal
212
+ self.minimum_periodic = minimum_periodic
213
+ self.maximum_periodic = maximum_periodic
214
+ self.minimum_per_hour = minimum_per_hour
215
+ self.maximum_per_hour = maximum_per_hour
91
216
  self.minimum_total = minimum_total
92
217
  self.maximum_total = maximum_total
93
218
 
94
- def transform_data(self, flow_system: 'FlowSystem'):
95
- self.minimum_operation_per_hour = flow_system.create_time_series(
96
- f'{self.label_full}|minimum_operation_per_hour', self.minimum_operation_per_hour
219
+ # Backwards compatible properties (deprecated)
220
+ @property
221
+ def minimum_operation(self):
222
+ """DEPRECATED: Use 'minimum_temporal' property instead."""
223
+ warnings.warn(
224
+ "Property 'minimum_operation' is deprecated. Use 'minimum_temporal' instead.",
225
+ DeprecationWarning,
226
+ stacklevel=2,
227
+ )
228
+ return self.minimum_temporal
229
+
230
+ @minimum_operation.setter
231
+ def minimum_operation(self, value):
232
+ """DEPRECATED: Use 'minimum_temporal' property instead."""
233
+ warnings.warn(
234
+ "Property 'minimum_operation' is deprecated. Use 'minimum_temporal' instead.",
235
+ DeprecationWarning,
236
+ stacklevel=2,
237
+ )
238
+ self.minimum_temporal = value
239
+
240
+ @property
241
+ def maximum_operation(self):
242
+ """DEPRECATED: Use 'maximum_temporal' property instead."""
243
+ warnings.warn(
244
+ "Property 'maximum_operation' is deprecated. Use 'maximum_temporal' instead.",
245
+ DeprecationWarning,
246
+ stacklevel=2,
247
+ )
248
+ return self.maximum_temporal
249
+
250
+ @maximum_operation.setter
251
+ def maximum_operation(self, value):
252
+ """DEPRECATED: Use 'maximum_temporal' property instead."""
253
+ warnings.warn(
254
+ "Property 'maximum_operation' is deprecated. Use 'maximum_temporal' instead.",
255
+ DeprecationWarning,
256
+ stacklevel=2,
257
+ )
258
+ self.maximum_temporal = value
259
+
260
+ @property
261
+ def minimum_invest(self):
262
+ """DEPRECATED: Use 'minimum_periodic' property instead."""
263
+ warnings.warn(
264
+ "Property 'minimum_invest' is deprecated. Use 'minimum_periodic' instead.",
265
+ DeprecationWarning,
266
+ stacklevel=2,
267
+ )
268
+ return self.minimum_periodic
269
+
270
+ @minimum_invest.setter
271
+ def minimum_invest(self, value):
272
+ """DEPRECATED: Use 'minimum_periodic' property instead."""
273
+ warnings.warn(
274
+ "Property 'minimum_invest' is deprecated. Use 'minimum_periodic' instead.",
275
+ DeprecationWarning,
276
+ stacklevel=2,
97
277
  )
98
- self.maximum_operation_per_hour = flow_system.create_time_series(
99
- f'{self.label_full}|maximum_operation_per_hour', self.maximum_operation_per_hour, flow_system
278
+ self.minimum_periodic = value
279
+
280
+ @property
281
+ def maximum_invest(self):
282
+ """DEPRECATED: Use 'maximum_periodic' property instead."""
283
+ warnings.warn(
284
+ "Property 'maximum_invest' is deprecated. Use 'maximum_periodic' instead.",
285
+ DeprecationWarning,
286
+ stacklevel=2,
100
287
  )
101
- self.specific_share_to_other_effects_operation = flow_system.create_effect_time_series(
102
- f'{self.label_full}|operation->', self.specific_share_to_other_effects_operation, 'operation'
288
+ return self.maximum_periodic
289
+
290
+ @maximum_invest.setter
291
+ def maximum_invest(self, value):
292
+ """DEPRECATED: Use 'maximum_periodic' property instead."""
293
+ warnings.warn(
294
+ "Property 'maximum_invest' is deprecated. Use 'maximum_periodic' instead.",
295
+ DeprecationWarning,
296
+ stacklevel=2,
103
297
  )
298
+ self.maximum_periodic = value
104
299
 
105
- self.minimum_operation = flow_system.create_time_series(
106
- f'{self.label_full}|minimum_operation', self.minimum_operation, has_time_dim=False
300
+ @property
301
+ def minimum_operation_per_hour(self):
302
+ """DEPRECATED: Use 'minimum_per_hour' property instead."""
303
+ warnings.warn(
304
+ "Property 'minimum_operation_per_hour' is deprecated. Use 'minimum_per_hour' instead.",
305
+ DeprecationWarning,
306
+ stacklevel=2,
107
307
  )
108
- self.maximum_operation = flow_system.create_time_series(
109
- f'{self.label_full}|maximum_operation', self.maximum_operation, has_time_dim=False
308
+ return self.minimum_per_hour
309
+
310
+ @minimum_operation_per_hour.setter
311
+ def minimum_operation_per_hour(self, value):
312
+ """DEPRECATED: Use 'minimum_per_hour' property instead."""
313
+ warnings.warn(
314
+ "Property 'minimum_operation_per_hour' is deprecated. Use 'minimum_per_hour' instead.",
315
+ DeprecationWarning,
316
+ stacklevel=2,
317
+ )
318
+ self.minimum_per_hour = value
319
+
320
+ @property
321
+ def maximum_operation_per_hour(self):
322
+ """DEPRECATED: Use 'maximum_per_hour' property instead."""
323
+ warnings.warn(
324
+ "Property 'maximum_operation_per_hour' is deprecated. Use 'maximum_per_hour' instead.",
325
+ DeprecationWarning,
326
+ stacklevel=2,
327
+ )
328
+ return self.maximum_per_hour
329
+
330
+ @maximum_operation_per_hour.setter
331
+ def maximum_operation_per_hour(self, value):
332
+ """DEPRECATED: Use 'maximum_per_hour' property instead."""
333
+ warnings.warn(
334
+ "Property 'maximum_operation_per_hour' is deprecated. Use 'maximum_per_hour' instead.",
335
+ DeprecationWarning,
336
+ stacklevel=2,
337
+ )
338
+ self.maximum_per_hour = value
339
+
340
+ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:
341
+ prefix = '|'.join(filter(None, [name_prefix, self.label_full]))
342
+ self.minimum_per_hour = flow_system.fit_to_model_coords(f'{prefix}|minimum_per_hour', self.minimum_per_hour)
343
+
344
+ self.maximum_per_hour = flow_system.fit_to_model_coords(f'{prefix}|maximum_per_hour', self.maximum_per_hour)
345
+
346
+ self.share_from_temporal = flow_system.fit_effects_to_model_coords(
347
+ label_prefix=None,
348
+ effect_values=self.share_from_temporal,
349
+ label_suffix=f'(temporal)->{prefix}(temporal)',
350
+ dims=['time', 'period', 'scenario'],
351
+ )
352
+ self.share_from_periodic = flow_system.fit_effects_to_model_coords(
353
+ label_prefix=None,
354
+ effect_values=self.share_from_periodic,
355
+ label_suffix=f'(periodic)->{prefix}(periodic)',
356
+ dims=['period', 'scenario'],
357
+ )
358
+
359
+ self.minimum_temporal = flow_system.fit_to_model_coords(
360
+ f'{prefix}|minimum_temporal', self.minimum_temporal, dims=['period', 'scenario']
110
361
  )
111
- self.minimum_invest = flow_system.create_time_series(
112
- f'{self.label_full}|minimum_invest', self.minimum_invest, has_time_dim=False
362
+ self.maximum_temporal = flow_system.fit_to_model_coords(
363
+ f'{prefix}|maximum_temporal', self.maximum_temporal, dims=['period', 'scenario']
113
364
  )
114
- self.maximum_invest = flow_system.create_time_series(
115
- f'{self.label_full}|maximum_invest', self.maximum_invest, has_time_dim=False
365
+ self.minimum_periodic = flow_system.fit_to_model_coords(
366
+ f'{prefix}|minimum_periodic', self.minimum_periodic, dims=['period', 'scenario']
116
367
  )
117
- self.minimum_total = flow_system.create_time_series(
118
- f'{self.label_full}|minimum_total', self.minimum_total, has_time_dim=False,
368
+ self.maximum_periodic = flow_system.fit_to_model_coords(
369
+ f'{prefix}|maximum_periodic', self.maximum_periodic, dims=['period', 'scenario']
119
370
  )
120
- self.maximum_total = flow_system.create_time_series(
121
- f'{self.label_full}|maximum_total', self.maximum_total, has_time_dim=False
371
+ self.minimum_total = flow_system.fit_to_model_coords(
372
+ f'{prefix}|minimum_total',
373
+ self.minimum_total,
374
+ dims=['period', 'scenario'],
122
375
  )
123
- self.specific_share_to_other_effects_invest = flow_system.create_effect_time_series(
124
- f'{self.label_full}|invest->', self.specific_share_to_other_effects_invest, 'invest',
125
- has_time_dim=False
376
+ self.maximum_total = flow_system.fit_to_model_coords(
377
+ f'{prefix}|maximum_total', self.maximum_total, dims=['period', 'scenario']
126
378
  )
127
379
 
128
- def create_model(self, model: SystemModel) -> 'EffectModel':
380
+ def create_model(self, model: FlowSystemModel) -> EffectModel:
129
381
  self._plausibility_checks()
130
- self.model = EffectModel(model, self)
131
- return self.model
382
+ self.submodel = EffectModel(model, self)
383
+ return self.submodel
132
384
 
133
385
  def _plausibility_checks(self) -> None:
134
386
  # TODO: Check for plausibility
@@ -136,69 +388,64 @@ class Effect(Element):
136
388
 
137
389
 
138
390
  class EffectModel(ElementModel):
139
- def __init__(self, model: SystemModel, element: Effect):
391
+ element: Effect # Type hint
392
+
393
+ def __init__(self, model: FlowSystemModel, element: Effect):
140
394
  super().__init__(model, element)
141
- self.element: Effect = element
142
- self.total: Optional[linopy.Variable] = None
143
- self.invest: ShareAllocationModel = self.add(
395
+
396
+ def _do_modeling(self):
397
+ self.total: linopy.Variable | None = None
398
+ self.periodic: ShareAllocationModel = self.add_submodels(
144
399
  ShareAllocationModel(
145
400
  model=self._model,
146
- has_time_dim=False,
147
- has_scenario_dim=True,
401
+ dims=('period', 'scenario'),
148
402
  label_of_element=self.label_of_element,
149
- label='invest',
150
- label_full=f'{self.label_full}(invest)',
151
- total_max=extract_data(self.element.maximum_invest),
152
- total_min=extract_data(self.element.minimum_invest),
153
- )
403
+ label_of_model=f'{self.label_of_model}(periodic)',
404
+ total_max=self.element.maximum_periodic,
405
+ total_min=self.element.minimum_periodic,
406
+ ),
407
+ short_name='periodic',
154
408
  )
155
409
 
156
- self.operation: ShareAllocationModel = self.add(
410
+ self.temporal: ShareAllocationModel = self.add_submodels(
157
411
  ShareAllocationModel(
158
412
  model=self._model,
159
- has_time_dim=True,
160
- has_scenario_dim=True,
413
+ dims=('time', 'period', 'scenario'),
161
414
  label_of_element=self.label_of_element,
162
- label='operation',
163
- label_full=f'{self.label_full}(operation)',
164
- total_max=extract_data(self.element.maximum_operation),
165
- total_min=extract_data(self.element.minimum_operation),
166
- min_per_hour=extract_data(self.element.minimum_operation_per_hour),
167
- max_per_hour=extract_data(self.element.maximum_operation_per_hour),
168
- )
415
+ label_of_model=f'{self.label_of_model}(temporal)',
416
+ total_max=self.element.maximum_temporal,
417
+ total_min=self.element.minimum_temporal,
418
+ min_per_hour=self.element.minimum_per_hour if self.element.minimum_per_hour is not None else None,
419
+ max_per_hour=self.element.maximum_per_hour if self.element.maximum_per_hour is not None else None,
420
+ ),
421
+ short_name='temporal',
169
422
  )
170
423
 
171
- def do_modeling(self):
172
- for model in self.sub_models:
173
- model.do_modeling()
174
-
175
- self.total = self.add(
176
- self._model.add_variables(
177
- lower=extract_data(self.element.minimum_total, if_none=-np.inf),
178
- upper=extract_data(self.element.maximum_total, if_none=np.inf),
179
- coords=self._model.get_coords(time_dim=False),
180
- name=f'{self.label_full}|total',
181
- ),
182
- 'total',
424
+ self.total = self.add_variables(
425
+ lower=self.element.minimum_total if self.element.minimum_total is not None else -np.inf,
426
+ upper=self.element.maximum_total if self.element.maximum_total is not None else np.inf,
427
+ coords=self._model.get_coords(['period', 'scenario']),
428
+ name=self.label_full,
183
429
  )
184
430
 
185
- self.add(
186
- self._model.add_constraints(
187
- self.total == self.operation.total + self.invest.total, name=f'{self.label_full}|total'
188
- ),
189
- 'total',
431
+ self.add_constraints(
432
+ self.total == self.temporal.total + self.periodic.total, name=self.label_full, short_name='total'
190
433
  )
191
434
 
192
435
 
193
- EffectValuesExpr = Dict[str, linopy.LinearExpression] # Used to create Shares
194
- EffectTimeSeries = Dict[str, TimeSeries] # Used internally to index values
195
- EffectValuesDict = Dict[str, NumericDataTS] # How effect values are stored
436
+ TemporalEffectsUser = TemporalDataUser | dict[str, TemporalDataUser] # User-specified Shares to Effects
437
+ """ This datatype is used to define a temporal share to an effect by a certain attribute. """
196
438
 
197
- EffectValuesUserScenario = Union[ScenarioData, Dict[str, ScenarioData]]
198
- """ This datatype is used to define the share to an effect for every scenario. """
439
+ PeriodicEffectsUser = PeriodicDataUser | dict[str, PeriodicDataUser] # User-specified Shares to Effects
440
+ """ This datatype is used to define a scalar share to an effect by a certain attribute. """
199
441
 
200
- EffectValuesUserTimestep = Union[TimestepData, Dict[str, TimestepData]]
201
- """ This datatype is used to define the share to an effect for every timestep. """
442
+ TemporalEffects = dict[str, TemporalData] # User-specified Shares to Effects
443
+ """ This datatype is used internally to handle temporal shares to an effect. """
444
+
445
+ PeriodicEffects = dict[str, Scalar]
446
+ """ This datatype is used internally to handle scalar shares to an effect. """
447
+
448
+ EffectExpr = dict[str, linopy.LinearExpression] # Used to create Shares
202
449
 
203
450
 
204
451
  class EffectCollection:
@@ -206,18 +453,18 @@ class EffectCollection:
206
453
  Handling all Effects
207
454
  """
208
455
 
209
- def __init__(self, *effects: List[Effect]):
456
+ def __init__(self, *effects: Effect):
210
457
  self._effects = {}
211
- self._standard_effect: Optional[Effect] = None
212
- self._objective_effect: Optional[Effect] = None
458
+ self._standard_effect: Effect | None = None
459
+ self._objective_effect: Effect | None = None
213
460
 
214
- self.model: Optional[EffectCollectionModel] = None
461
+ self.submodel: EffectCollectionModel | None = None
215
462
  self.add_effects(*effects)
216
463
 
217
- def create_model(self, model: SystemModel) -> 'EffectCollectionModel':
464
+ def create_model(self, model: FlowSystemModel) -> EffectCollectionModel:
218
465
  self._plausibility_checks()
219
- self.model = EffectCollectionModel(model, self)
220
- return self.model
466
+ self.submodel = EffectCollectionModel(model, self)
467
+ return self.submodel
221
468
 
222
469
  def add_effects(self, *effects: Effect) -> None:
223
470
  for effect in list(effects):
@@ -231,24 +478,26 @@ class EffectCollection:
231
478
  logger.info(f'Registered new Effect: {effect.label}')
232
479
 
233
480
  def create_effect_values_dict(
234
- self, effect_values_user: Union[EffectValuesUserScenario, EffectValuesUserTimestep]
235
- ) -> Optional[EffectValuesDict]:
481
+ self, effect_values_user: PeriodicEffectsUser | TemporalEffectsUser
482
+ ) -> dict[str, Scalar | TemporalDataUser] | None:
236
483
  """
237
484
  Converts effect values into a dictionary. If a scalar is provided, it is associated with a default effect type.
238
485
 
239
486
  Examples
240
487
  --------
241
- effect_values_user = 20 -> {None: 20}
242
- effect_values_user = None -> None
243
- effect_values_user = {effect1: 20, effect2: 0.3} -> {effect1: 20, effect2: 0.3}
488
+ effect_values_user = 20 -> {'<standard_effect_label>': 20}
489
+ effect_values_user = {None: 20} -> {'<standard_effect_label>': 20}
490
+ effect_values_user = None -> None
491
+ effect_values_user = {'effect1': 20, 'effect2': 0.3} -> {'effect1': 20, 'effect2': 0.3}
244
492
 
245
493
  Returns
246
494
  -------
247
495
  dict or None
248
- A dictionary with None or Effect as the key, or None if input is None.
496
+ A dictionary keyed by effect label, or None if input is None.
497
+ Note: a standard effect must be defined when passing scalars or None labels.
249
498
  """
250
499
 
251
- def get_effect_label(eff: Union[Effect, str]) -> str:
500
+ def get_effect_label(eff: Effect | str) -> str:
252
501
  """Temporary function to get the label of an effect and warn for deprecation"""
253
502
  if isinstance(eff, Effect):
254
503
  warnings.warn(
@@ -257,7 +506,9 @@ class EffectCollection:
257
506
  UserWarning,
258
507
  stacklevel=2,
259
508
  )
260
- return eff.label_full
509
+ return eff.label
510
+ elif eff is None:
511
+ return self.standard_effect.label
261
512
  else:
262
513
  return eff
263
514
 
@@ -265,24 +516,29 @@ class EffectCollection:
265
516
  return None
266
517
  if isinstance(effect_values_user, dict):
267
518
  return {get_effect_label(effect): value for effect, value in effect_values_user.items()}
268
- return {self.standard_effect.label_full: effect_values_user}
519
+ return {self.standard_effect.label: effect_values_user}
269
520
 
270
521
  def _plausibility_checks(self) -> None:
271
522
  # Check circular loops in effects:
272
- operation, invest = self.calculate_effect_share_factors()
523
+ temporal, periodic = self.calculate_effect_share_factors()
524
+
525
+ # Validate all referenced sources exist
526
+ unknown = {src for src, _ in list(temporal.keys()) + list(periodic.keys()) if src not in self.effects}
527
+ if unknown:
528
+ raise KeyError(f'Unknown effects used in in effect share mappings: {sorted(unknown)}')
273
529
 
274
- operation_cycles = detect_cycles(tuples_to_adjacency_list([key for key in operation]))
275
- invest_cycles = detect_cycles(tuples_to_adjacency_list([key for key in invest]))
530
+ temporal_cycles = detect_cycles(tuples_to_adjacency_list([key for key in temporal]))
531
+ periodic_cycles = detect_cycles(tuples_to_adjacency_list([key for key in periodic]))
276
532
 
277
- if operation_cycles:
278
- cycle_str = "\n".join([" -> ".join(cycle) for cycle in operation_cycles])
279
- raise ValueError(f'Error: circular operation-shares detected:\n{cycle_str}')
533
+ if temporal_cycles:
534
+ cycle_str = '\n'.join([' -> '.join(cycle) for cycle in temporal_cycles])
535
+ raise ValueError(f'Error: circular temporal-shares detected:\n{cycle_str}')
280
536
 
281
- if invest_cycles:
282
- cycle_str = "\n".join([" -> ".join(cycle) for cycle in invest_cycles])
283
- raise ValueError(f'Error: circular invest-shares detected:\n{cycle_str}')
537
+ if periodic_cycles:
538
+ cycle_str = '\n'.join([' -> '.join(cycle) for cycle in periodic_cycles])
539
+ raise ValueError(f'Error: circular periodic-shares detected:\n{cycle_str}')
284
540
 
285
- def __getitem__(self, effect: Union[str, Effect]) -> 'Effect':
541
+ def __getitem__(self, effect: str | Effect | None) -> Effect:
286
542
  """
287
543
  Get an effect by label, or return the standard effect if None is passed
288
544
 
@@ -308,22 +564,28 @@ class EffectCollection:
308
564
  def __len__(self) -> int:
309
565
  return len(self._effects)
310
566
 
311
- def __contains__(self, item: Union[str, 'Effect']) -> bool:
567
+ def __contains__(self, item: str | Effect) -> bool:
312
568
  """Check if the effect exists. Checks for label or object"""
313
569
  if isinstance(item, str):
314
570
  return item in self.effects # Check if the label exists
315
571
  elif isinstance(item, Effect):
316
- return item in self.effects.values() # Check if the object exists
572
+ if item.label_full in self.effects:
573
+ return True
574
+ if item in self.effects.values(): # Check if the object exists
575
+ return True
317
576
  return False
318
577
 
319
578
  @property
320
- def effects(self) -> Dict[str, Effect]:
579
+ def effects(self) -> dict[str, Effect]:
321
580
  return self._effects
322
581
 
323
582
  @property
324
583
  def standard_effect(self) -> Effect:
325
584
  if self._standard_effect is None:
326
- raise KeyError('No standard-effect specified!')
585
+ raise KeyError(
586
+ 'No standard-effect specified! Either set an effect through is_standard=True '
587
+ 'or pass a mapping when specifying effect values: {effect_label: value}.'
588
+ )
327
589
  return self._standard_effect
328
590
 
329
591
  @standard_effect.setter
@@ -344,61 +606,61 @@ class EffectCollection:
344
606
  raise ValueError(f'An objective-effect already exists! ({self._objective_effect.label=})')
345
607
  self._objective_effect = value
346
608
 
347
- def calculate_effect_share_factors(self) -> Tuple[
348
- Dict[Tuple[str, str], xr.DataArray],
349
- Dict[Tuple[str, str], xr.DataArray],
609
+ def calculate_effect_share_factors(
610
+ self,
611
+ ) -> tuple[
612
+ dict[tuple[str, str], xr.DataArray],
613
+ dict[tuple[str, str], xr.DataArray],
350
614
  ]:
351
- shares_invest = {}
615
+ shares_periodic = {}
352
616
  for name, effect in self.effects.items():
353
- if effect.specific_share_to_other_effects_invest:
354
- shares_invest[name] = {
355
- target: extract_data(data)
356
- for target, data in effect.specific_share_to_other_effects_invest.items()
357
- }
358
- shares_invest = calculate_all_conversion_paths(shares_invest)
359
-
360
- shares_operation = {}
617
+ if effect.share_from_periodic:
618
+ for source, data in effect.share_from_periodic.items():
619
+ if source not in shares_periodic:
620
+ shares_periodic[source] = {}
621
+ shares_periodic[source][name] = data
622
+ shares_periodic = calculate_all_conversion_paths(shares_periodic)
623
+
624
+ shares_temporal = {}
361
625
  for name, effect in self.effects.items():
362
- if effect.specific_share_to_other_effects_operation:
363
- shares_operation[name] = {
364
- target: extract_data(data)
365
- for target, data in effect.specific_share_to_other_effects_operation.items()
366
- }
367
- shares_operation = calculate_all_conversion_paths(shares_operation)
626
+ if effect.share_from_temporal:
627
+ for source, data in effect.share_from_temporal.items():
628
+ if source not in shares_temporal:
629
+ shares_temporal[source] = {}
630
+ shares_temporal[source][name] = data
631
+ shares_temporal = calculate_all_conversion_paths(shares_temporal)
368
632
 
369
- return shares_operation, shares_invest
633
+ return shares_temporal, shares_periodic
370
634
 
371
635
 
372
- class EffectCollectionModel(Model):
636
+ class EffectCollectionModel(Submodel):
373
637
  """
374
638
  Handling all Effects
375
639
  """
376
640
 
377
- def __init__(self, model: SystemModel, effects: EffectCollection):
378
- super().__init__(model, label_of_element='Effects')
641
+ def __init__(self, model: FlowSystemModel, effects: EffectCollection):
379
642
  self.effects = effects
380
- self.penalty: Optional[ShareAllocationModel] = None
643
+ self.penalty: ShareAllocationModel | None = None
644
+ super().__init__(model, label_of_element='Effects')
381
645
 
382
646
  def add_share_to_effects(
383
647
  self,
384
648
  name: str,
385
- expressions: EffectValuesExpr,
386
- target: Literal['operation', 'invest'],
649
+ expressions: EffectExpr,
650
+ target: Literal['temporal', 'periodic'],
387
651
  ) -> None:
388
652
  for effect, expression in expressions.items():
389
- if target == 'operation':
390
- self.effects[effect].model.operation.add_share(
653
+ if target == 'temporal':
654
+ self.effects[effect].submodel.temporal.add_share(
391
655
  name,
392
656
  expression,
393
- has_time_dim=True,
394
- has_scenario_dim=True,
657
+ dims=('time', 'period', 'scenario'),
395
658
  )
396
- elif target == 'invest':
397
- self.effects[effect].model.invest.add_share(
659
+ elif target == 'periodic':
660
+ self.effects[effect].submodel.periodic.add_share(
398
661
  name,
399
662
  expression,
400
- has_time_dim=False,
401
- has_scenario_dim=True,
663
+ dims=('period', 'scenario'),
402
664
  )
403
665
  else:
404
666
  raise ValueError(f'Target {target} not supported!')
@@ -406,47 +668,44 @@ class EffectCollectionModel(Model):
406
668
  def add_share_to_penalty(self, name: str, expression: linopy.LinearExpression) -> None:
407
669
  if expression.ndim != 0:
408
670
  raise TypeError(f'Penalty shares must be scalar expressions! ({expression.ndim=})')
409
- self.penalty.add_share(name, expression, has_time_dim=False, has_scenario_dim=False)
671
+ self.penalty.add_share(name, expression, dims=())
410
672
 
411
- def do_modeling(self):
673
+ def _do_modeling(self):
674
+ super()._do_modeling()
412
675
  for effect in self.effects:
413
676
  effect.create_model(self._model)
414
- self.penalty = self.add(
415
- ShareAllocationModel(self._model, has_time_dim=False, has_scenario_dim=False, label_of_element='Penalty')
677
+ self.penalty = self.add_submodels(
678
+ ShareAllocationModel(self._model, dims=(), label_of_element='Penalty'),
679
+ short_name='penalty',
416
680
  )
417
- for model in [effect.model for effect in self.effects] + [self.penalty]:
418
- model.do_modeling()
419
681
 
420
682
  self._add_share_between_effects()
421
683
 
422
684
  self._model.add_objective(
423
- (self.effects.objective_effect.model.total * self._model.scenario_weights).sum()
424
- + self.penalty.total.sum()
685
+ (self.effects.objective_effect.submodel.total * self._model.weights).sum() + self.penalty.total.sum()
425
686
  )
426
687
 
427
688
  def _add_share_between_effects(self):
428
- for origin_effect in self.effects:
429
- # 1. operation: -> hier sind es Zeitreihen (share_TS)
430
- for target_effect, time_series in origin_effect.specific_share_to_other_effects_operation.items():
431
- self.effects[target_effect].model.operation.add_share(
432
- origin_effect.model.operation.label_full,
433
- origin_effect.model.operation.total_per_timestep * time_series.selected_data,
434
- has_time_dim=True,
435
- has_scenario_dim=True,
689
+ for target_effect in self.effects:
690
+ # 1. temporal: <- receiving temporal shares from other effects
691
+ for source_effect, time_series in target_effect.share_from_temporal.items():
692
+ target_effect.submodel.temporal.add_share(
693
+ self.effects[source_effect].submodel.temporal.label_full,
694
+ self.effects[source_effect].submodel.temporal.total_per_timestep * time_series,
695
+ dims=('time', 'period', 'scenario'),
436
696
  )
437
- # 2. invest: -> hier ist es Scalar (share)
438
- for target_effect, factor in origin_effect.specific_share_to_other_effects_invest.items():
439
- self.effects[target_effect].model.invest.add_share(
440
- origin_effect.model.invest.label_full,
441
- origin_effect.model.invest.total * factor,
442
- has_time_dim=False,
443
- has_scenario_dim=True,
697
+ # 2. periodic: <- receiving periodic shares from other effects
698
+ for source_effect, factor in target_effect.share_from_periodic.items():
699
+ target_effect.submodel.periodic.add_share(
700
+ self.effects[source_effect].submodel.periodic.label_full,
701
+ self.effects[source_effect].submodel.periodic.total * factor,
702
+ dims=('period', 'scenario'),
444
703
  )
445
704
 
446
705
 
447
706
  def calculate_all_conversion_paths(
448
- conversion_dict: Dict[str, Dict[str, xr.DataArray]],
449
- ) -> Dict[Tuple[str, str], xr.DataArray]:
707
+ conversion_dict: dict[str, dict[str, Scalar | xr.DataArray]],
708
+ ) -> dict[tuple[str, str], xr.DataArray]:
450
709
  """
451
710
  Calculates all possible direct and indirect conversion factors between units/domains.
452
711
  This function uses Breadth-First Search (BFS) to find all possible conversion paths
@@ -477,10 +736,10 @@ def calculate_all_conversion_paths(
477
736
  # Keep track of visited paths to avoid repeating calculations
478
737
  processed_paths = set()
479
738
  # Use a queue with (current_domain, factor, path_history)
480
- queue = [(origin, 1, [origin])]
739
+ queue = deque([(origin, 1, [origin])])
481
740
 
482
741
  while queue:
483
- current_domain, factor, path = queue.pop(0)
742
+ current_domain, factor, path = queue.popleft()
484
743
 
485
744
  # Skip if we've processed this exact path before
486
745
  path_key = tuple(path)
@@ -510,13 +769,12 @@ def calculate_all_conversion_paths(
510
769
  queue.append((target, indirect_factor, new_path))
511
770
 
512
771
  # Convert all values to DataArrays
513
- result = {key: value if isinstance(value, xr.DataArray) else xr.DataArray(value)
514
- for key, value in result.items()}
772
+ result = {key: value if isinstance(value, xr.DataArray) else xr.DataArray(value) for key, value in result.items()}
515
773
 
516
774
  return result
517
775
 
518
776
 
519
- def detect_cycles(graph: Dict[str, List[str]]) -> List[List[str]]:
777
+ def detect_cycles(graph: dict[str, list[str]]) -> list[list[str]]:
520
778
  """
521
779
  Detects cycles in a directed graph using DFS.
522
780
 
@@ -566,7 +824,7 @@ def detect_cycles(graph: Dict[str, List[str]]) -> List[List[str]]:
566
824
  return cycles
567
825
 
568
826
 
569
- def tuples_to_adjacency_list(edges: List[Tuple[str, str]]) -> Dict[str, List[str]]:
827
+ def tuples_to_adjacency_list(edges: list[tuple[str, str]]) -> dict[str, list[str]]:
570
828
  """
571
829
  Converts a list of edge tuples (source, target) to an adjacency list representation.
572
830
 
@@ -588,3 +846,8 @@ def tuples_to_adjacency_list(edges: List[Tuple[str, str]]) -> Dict[str, List[str
588
846
  graph[target] = []
589
847
 
590
848
  return graph
849
+
850
+
851
+ # Backward compatibility aliases
852
+ NonTemporalEffectsUser = PeriodicEffectsUser
853
+ NonTemporalEffects = PeriodicEffects