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.
- flixopt/__init__.py +35 -1
- flixopt/aggregation.py +60 -81
- flixopt/calculation.py +381 -196
- flixopt/components.py +1022 -359
- flixopt/config.py +553 -191
- flixopt/core.py +475 -1315
- flixopt/effects.py +477 -214
- flixopt/elements.py +591 -344
- flixopt/features.py +403 -957
- flixopt/flow_system.py +781 -293
- flixopt/interface.py +1159 -189
- flixopt/io.py +50 -55
- flixopt/linear_converters.py +384 -92
- flixopt/modeling.py +759 -0
- flixopt/network_app.py +789 -0
- flixopt/plotting.py +273 -135
- flixopt/results.py +639 -383
- flixopt/solvers.py +25 -21
- flixopt/structure.py +928 -442
- flixopt/utils.py +34 -5
- flixopt-3.0.0.dist-info/METADATA +209 -0
- flixopt-3.0.0.dist-info/RECORD +26 -0
- {flixopt-2.2.0b0.dist-info → flixopt-3.0.0.dist-info}/WHEEL +1 -1
- flixopt-3.0.0.dist-info/top_level.txt +1 -0
- docs/examples/00-Minimal Example.md +0 -5
- docs/examples/01-Basic Example.md +0 -5
- docs/examples/02-Complex Example.md +0 -10
- docs/examples/03-Calculation Modes.md +0 -5
- docs/examples/index.md +0 -5
- docs/faq/contribute.md +0 -49
- docs/faq/index.md +0 -3
- docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
- docs/images/architecture_flixOpt.png +0 -0
- docs/images/flixopt-icon.svg +0 -1
- docs/javascripts/mathjax.js +0 -18
- docs/release-notes/_template.txt +0 -32
- docs/release-notes/index.md +0 -7
- docs/release-notes/v2.0.0.md +0 -93
- docs/release-notes/v2.0.1.md +0 -12
- docs/release-notes/v2.1.0.md +0 -31
- docs/release-notes/v2.2.0.md +0 -55
- docs/user-guide/Mathematical Notation/Bus.md +0 -33
- docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +0 -132
- docs/user-guide/Mathematical Notation/Flow.md +0 -26
- docs/user-guide/Mathematical Notation/Investment.md +0 -115
- docs/user-guide/Mathematical Notation/LinearConverter.md +0 -21
- docs/user-guide/Mathematical Notation/Piecewise.md +0 -49
- docs/user-guide/Mathematical Notation/Storage.md +0 -44
- docs/user-guide/Mathematical Notation/index.md +0 -22
- docs/user-guide/Mathematical Notation/others.md +0 -3
- docs/user-guide/index.md +0 -124
- flixopt/config.yaml +0 -10
- flixopt-2.2.0b0.dist-info/METADATA +0 -146
- flixopt-2.2.0b0.dist-info/RECORD +0 -59
- flixopt-2.2.0b0.dist-info/top_level.txt +0 -5
- pics/architecture_flixOpt-pre2.0.0.png +0 -0
- pics/architecture_flixOpt.png +0 -0
- pics/flixOpt_plotting.jpg +0 -0
- pics/flixopt-icon.svg +0 -1
- pics/pics.pptx +0 -0
- scripts/gen_ref_pages.py +0 -54
- tests/ressources/Zeitreihen2020.csv +0 -35137
- {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
|
|
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
|
|
19
|
+
from .core import PeriodicDataUser, Scalar, TemporalData, TemporalDataUser
|
|
17
20
|
from .features import ShareAllocationModel
|
|
18
|
-
from .structure import Element, ElementModel,
|
|
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
|
-
|
|
30
|
-
|
|
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:
|
|
167
|
+
meta_data: dict | None = None,
|
|
39
168
|
is_standard: bool = False,
|
|
40
169
|
is_objective: bool = False,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
minimum_total:
|
|
50
|
-
maximum_total:
|
|
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.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
self.minimum_invest
|
|
90
|
-
self.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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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.
|
|
99
|
-
|
|
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.
|
|
102
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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.
|
|
109
|
-
|
|
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.
|
|
112
|
-
f'{
|
|
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.
|
|
115
|
-
f'{
|
|
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.
|
|
118
|
-
f'{
|
|
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.
|
|
121
|
-
f'{
|
|
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.
|
|
124
|
-
f'{
|
|
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:
|
|
380
|
+
def create_model(self, model: FlowSystemModel) -> EffectModel:
|
|
129
381
|
self._plausibility_checks()
|
|
130
|
-
self.
|
|
131
|
-
return self.
|
|
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
|
-
|
|
391
|
+
element: Effect # Type hint
|
|
392
|
+
|
|
393
|
+
def __init__(self, model: FlowSystemModel, element: Effect):
|
|
140
394
|
super().__init__(model, element)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
self.
|
|
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
|
-
|
|
147
|
-
has_scenario_dim=True,
|
|
401
|
+
dims=('period', 'scenario'),
|
|
148
402
|
label_of_element=self.label_of_element,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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.
|
|
410
|
+
self.temporal: ShareAllocationModel = self.add_submodels(
|
|
157
411
|
ShareAllocationModel(
|
|
158
412
|
model=self._model,
|
|
159
|
-
|
|
160
|
-
has_scenario_dim=True,
|
|
413
|
+
dims=('time', 'period', 'scenario'),
|
|
161
414
|
label_of_element=self.label_of_element,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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.
|
|
186
|
-
self.
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
|
|
198
|
-
""" This datatype is used to define
|
|
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
|
-
|
|
201
|
-
""" This datatype is used to
|
|
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:
|
|
456
|
+
def __init__(self, *effects: Effect):
|
|
210
457
|
self._effects = {}
|
|
211
|
-
self._standard_effect:
|
|
212
|
-
self._objective_effect:
|
|
458
|
+
self._standard_effect: Effect | None = None
|
|
459
|
+
self._objective_effect: Effect | None = None
|
|
213
460
|
|
|
214
|
-
self.
|
|
461
|
+
self.submodel: EffectCollectionModel | None = None
|
|
215
462
|
self.add_effects(*effects)
|
|
216
463
|
|
|
217
|
-
def create_model(self, model:
|
|
464
|
+
def create_model(self, model: FlowSystemModel) -> EffectCollectionModel:
|
|
218
465
|
self._plausibility_checks()
|
|
219
|
-
self.
|
|
220
|
-
return self.
|
|
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:
|
|
235
|
-
) ->
|
|
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
|
|
242
|
-
effect_values_user = None
|
|
243
|
-
effect_values_user =
|
|
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
|
|
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:
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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
|
|
278
|
-
cycle_str =
|
|
279
|
-
raise ValueError(f'Error: circular
|
|
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
|
|
282
|
-
cycle_str =
|
|
283
|
-
raise ValueError(f'Error: circular
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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) ->
|
|
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(
|
|
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(
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
615
|
+
shares_periodic = {}
|
|
352
616
|
for name, effect in self.effects.items():
|
|
353
|
-
if effect.
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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.
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
|
633
|
+
return shares_temporal, shares_periodic
|
|
370
634
|
|
|
371
635
|
|
|
372
|
-
class EffectCollectionModel(
|
|
636
|
+
class EffectCollectionModel(Submodel):
|
|
373
637
|
"""
|
|
374
638
|
Handling all Effects
|
|
375
639
|
"""
|
|
376
640
|
|
|
377
|
-
def __init__(self, model:
|
|
378
|
-
super().__init__(model, label_of_element='Effects')
|
|
641
|
+
def __init__(self, model: FlowSystemModel, effects: EffectCollection):
|
|
379
642
|
self.effects = effects
|
|
380
|
-
self.penalty:
|
|
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:
|
|
386
|
-
target: Literal['
|
|
649
|
+
expressions: EffectExpr,
|
|
650
|
+
target: Literal['temporal', 'periodic'],
|
|
387
651
|
) -> None:
|
|
388
652
|
for effect, expression in expressions.items():
|
|
389
|
-
if target == '
|
|
390
|
-
self.effects[effect].
|
|
653
|
+
if target == 'temporal':
|
|
654
|
+
self.effects[effect].submodel.temporal.add_share(
|
|
391
655
|
name,
|
|
392
656
|
expression,
|
|
393
|
-
|
|
394
|
-
has_scenario_dim=True,
|
|
657
|
+
dims=('time', 'period', 'scenario'),
|
|
395
658
|
)
|
|
396
|
-
elif target == '
|
|
397
|
-
self.effects[effect].
|
|
659
|
+
elif target == 'periodic':
|
|
660
|
+
self.effects[effect].submodel.periodic.add_share(
|
|
398
661
|
name,
|
|
399
662
|
expression,
|
|
400
|
-
|
|
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,
|
|
671
|
+
self.penalty.add_share(name, expression, dims=())
|
|
410
672
|
|
|
411
|
-
def
|
|
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.
|
|
415
|
-
ShareAllocationModel(self._model,
|
|
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.
|
|
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
|
|
429
|
-
# 1.
|
|
430
|
-
for
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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.
|
|
438
|
-
for
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
449
|
-
) ->
|
|
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.
|
|
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:
|
|
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:
|
|
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
|