flixopt 3.0.1__py3-none-any.whl → 6.0.0rc7__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.
- flixopt/__init__.py +57 -49
- flixopt/carrier.py +159 -0
- flixopt/clustering/__init__.py +51 -0
- flixopt/clustering/base.py +1746 -0
- flixopt/clustering/intercluster_helpers.py +201 -0
- flixopt/color_processing.py +372 -0
- flixopt/comparison.py +819 -0
- flixopt/components.py +848 -270
- flixopt/config.py +853 -496
- flixopt/core.py +111 -98
- flixopt/effects.py +294 -284
- flixopt/elements.py +484 -223
- flixopt/features.py +220 -118
- flixopt/flow_system.py +2026 -389
- flixopt/interface.py +504 -286
- flixopt/io.py +1718 -55
- flixopt/linear_converters.py +291 -230
- flixopt/modeling.py +304 -181
- flixopt/network_app.py +2 -1
- flixopt/optimization.py +788 -0
- flixopt/optimize_accessor.py +373 -0
- flixopt/plot_result.py +143 -0
- flixopt/plotting.py +1177 -1034
- flixopt/results.py +1331 -372
- flixopt/solvers.py +12 -4
- flixopt/statistics_accessor.py +2412 -0
- flixopt/stats_accessor.py +75 -0
- flixopt/structure.py +954 -120
- flixopt/topology_accessor.py +676 -0
- flixopt/transform_accessor.py +2277 -0
- flixopt/types.py +120 -0
- flixopt-6.0.0rc7.dist-info/METADATA +290 -0
- flixopt-6.0.0rc7.dist-info/RECORD +36 -0
- {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/WHEEL +1 -1
- flixopt/aggregation.py +0 -382
- flixopt/calculation.py +0 -672
- flixopt/commons.py +0 -51
- flixopt/utils.py +0 -86
- flixopt-3.0.1.dist-info/METADATA +0 -209
- flixopt-3.0.1.dist-info/RECORD +0 -26
- {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/licenses/LICENSE +0 -0
- {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/top_level.txt +0 -0
flixopt/effects.py
CHANGED
|
@@ -8,7 +8,6 @@ which are then transformed into the internal data structure.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import logging
|
|
11
|
-
import warnings
|
|
12
11
|
from collections import deque
|
|
13
12
|
from typing import TYPE_CHECKING, Literal
|
|
14
13
|
|
|
@@ -16,52 +15,72 @@ import linopy
|
|
|
16
15
|
import numpy as np
|
|
17
16
|
import xarray as xr
|
|
18
17
|
|
|
19
|
-
from .core import
|
|
18
|
+
from .core import PlausibilityError
|
|
20
19
|
from .features import ShareAllocationModel
|
|
21
|
-
from .structure import
|
|
20
|
+
from .structure import (
|
|
21
|
+
Element,
|
|
22
|
+
ElementContainer,
|
|
23
|
+
ElementModel,
|
|
24
|
+
FlowSystemModel,
|
|
25
|
+
Submodel,
|
|
26
|
+
VariableCategory,
|
|
27
|
+
register_class_for_io,
|
|
28
|
+
)
|
|
22
29
|
|
|
23
30
|
if TYPE_CHECKING:
|
|
24
31
|
from collections.abc import Iterator
|
|
25
32
|
|
|
26
|
-
from .
|
|
33
|
+
from .types import Effect_PS, Effect_TPS, Numeric_PS, Numeric_S, Numeric_TPS, Scalar
|
|
27
34
|
|
|
28
35
|
logger = logging.getLogger('flixopt')
|
|
29
36
|
|
|
37
|
+
# Penalty effect label constant
|
|
38
|
+
PENALTY_EFFECT_LABEL = 'Penalty'
|
|
39
|
+
|
|
30
40
|
|
|
31
41
|
@register_class_for_io
|
|
32
42
|
class Effect(Element):
|
|
33
|
-
"""
|
|
34
|
-
Represents system-wide impacts like costs, emissions, resource consumption, or other effects.
|
|
43
|
+
"""Represents system-wide impacts like costs, emissions, or resource consumption.
|
|
35
44
|
|
|
36
|
-
Effects
|
|
37
|
-
the
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
Effects quantify impacts aggregating contributions from Elements across the FlowSystem.
|
|
46
|
+
One Effect serves as the optimization objective, while others can be constrained or tracked.
|
|
47
|
+
Supports operational and investment contributions, cross-effect relationships (e.g., carbon
|
|
48
|
+
pricing), and flexible constraint formulation.
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
Mathematical Formulation:
|
|
51
|
+
See <https://flixopt.github.io/flixopt/latest/user-guide/mathematical-notation/effects-and-dimensions/>
|
|
43
52
|
|
|
44
53
|
Args:
|
|
45
54
|
label: The label of the Element. Used to identify it in the FlowSystem.
|
|
46
55
|
unit: The unit of the effect (e.g., '€', 'kg_CO2', 'kWh_primary', 'm²').
|
|
47
|
-
This is informative only and does not affect optimization
|
|
56
|
+
This is informative only and does not affect optimization.
|
|
48
57
|
description: Descriptive name explaining what this effect represents.
|
|
49
58
|
is_standard: If True, this is a standard effect allowing direct value input
|
|
50
59
|
without effect dictionaries. Used for simplified effect specification (and less boilerplate code).
|
|
51
60
|
is_objective: If True, this effect serves as the optimization objective function.
|
|
52
61
|
Only one effect can be marked as objective per optimization.
|
|
62
|
+
period_weights: Optional custom weights for periods and scenarios (Numeric_PS).
|
|
63
|
+
If provided, overrides the FlowSystem's default period weights for this effect.
|
|
64
|
+
Useful for effect-specific weighting (e.g., discounting for costs vs equal weights for CO2).
|
|
65
|
+
If None, uses FlowSystem's default weights.
|
|
53
66
|
share_from_temporal: Temporal cross-effect contributions.
|
|
54
|
-
Maps temporal contributions from other effects to this effect
|
|
67
|
+
Maps temporal contributions from other effects to this effect.
|
|
55
68
|
share_from_periodic: Periodic cross-effect contributions.
|
|
56
69
|
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.
|
|
70
|
+
minimum_temporal: Minimum allowed total contribution across all timesteps (per period).
|
|
71
|
+
maximum_temporal: Maximum allowed total contribution across all timesteps (per period).
|
|
59
72
|
minimum_per_hour: Minimum allowed contribution per hour.
|
|
60
73
|
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).
|
|
74
|
+
minimum_periodic: Minimum allowed total periodic contribution (per period).
|
|
75
|
+
maximum_periodic: Maximum allowed total periodic contribution (per period).
|
|
76
|
+
minimum_total: Minimum allowed total effect (temporal + periodic combined) per period.
|
|
77
|
+
maximum_total: Maximum allowed total effect (temporal + periodic combined) per period.
|
|
78
|
+
minimum_over_periods: Minimum allowed weighted sum of total effect across ALL periods.
|
|
79
|
+
Weighted by effect-specific weights if defined, otherwise by FlowSystem period weights.
|
|
80
|
+
Requires FlowSystem to have a 'period' dimension (i.e., periods must be defined).
|
|
81
|
+
maximum_over_periods: Maximum allowed weighted sum of total effect across ALL periods.
|
|
82
|
+
Weighted by effect-specific weights if defined, otherwise by FlowSystem period weights.
|
|
83
|
+
Requires FlowSystem to have a 'period' dimension (i.e., periods must be defined).
|
|
65
84
|
meta_data: Used to store additional information. Not used internally but saved
|
|
66
85
|
in results. Only use Python native types.
|
|
67
86
|
|
|
@@ -85,14 +104,25 @@ class Effect(Element):
|
|
|
85
104
|
)
|
|
86
105
|
```
|
|
87
106
|
|
|
88
|
-
CO2 emissions:
|
|
107
|
+
CO2 emissions with per-period limit:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
co2_effect = Effect(
|
|
111
|
+
label='CO2',
|
|
112
|
+
unit='kg_CO2',
|
|
113
|
+
description='Carbon dioxide emissions',
|
|
114
|
+
maximum_total=100_000, # 100 t CO2 per period
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
CO2 emissions with total limit across all periods:
|
|
89
119
|
|
|
90
120
|
```python
|
|
91
121
|
co2_effect = Effect(
|
|
92
122
|
label='CO2',
|
|
93
123
|
unit='kg_CO2',
|
|
94
124
|
description='Carbon dioxide emissions',
|
|
95
|
-
|
|
125
|
+
maximum_over_periods=1_000_000, # 1000 t CO2 total across all periods
|
|
96
126
|
)
|
|
97
127
|
```
|
|
98
128
|
|
|
@@ -103,7 +133,7 @@ class Effect(Element):
|
|
|
103
133
|
label='land_usage',
|
|
104
134
|
unit='m²',
|
|
105
135
|
description='Land area requirement',
|
|
106
|
-
maximum_total=50_000, # Maximum 5 hectares
|
|
136
|
+
maximum_total=50_000, # Maximum 5 hectares per period
|
|
107
137
|
)
|
|
108
138
|
```
|
|
109
139
|
|
|
@@ -141,7 +171,7 @@ class Effect(Element):
|
|
|
141
171
|
description='Industrial water usage',
|
|
142
172
|
minimum_per_hour=10, # Minimum 10 m³/h for process stability
|
|
143
173
|
maximum_per_hour=500, # Maximum 500 m³/h capacity limit
|
|
144
|
-
|
|
174
|
+
maximum_over_periods=100_000, # Annual permit limit: 100,000 m³
|
|
145
175
|
)
|
|
146
176
|
```
|
|
147
177
|
|
|
@@ -159,52 +189,49 @@ class Effect(Element):
|
|
|
159
189
|
|
|
160
190
|
"""
|
|
161
191
|
|
|
192
|
+
submodel: EffectModel | None
|
|
193
|
+
|
|
162
194
|
def __init__(
|
|
163
195
|
self,
|
|
164
196
|
label: str,
|
|
165
197
|
unit: str,
|
|
166
|
-
description: str,
|
|
198
|
+
description: str = '',
|
|
167
199
|
meta_data: dict | None = None,
|
|
168
200
|
is_standard: bool = False,
|
|
169
201
|
is_objective: bool = False,
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
202
|
+
period_weights: Numeric_PS | None = None,
|
|
203
|
+
share_from_temporal: Effect_TPS | Numeric_TPS | None = None,
|
|
204
|
+
share_from_periodic: Effect_PS | Numeric_PS | None = None,
|
|
205
|
+
minimum_temporal: Numeric_PS | None = None,
|
|
206
|
+
maximum_temporal: Numeric_PS | None = None,
|
|
207
|
+
minimum_periodic: Numeric_PS | None = None,
|
|
208
|
+
maximum_periodic: Numeric_PS | None = None,
|
|
209
|
+
minimum_per_hour: Numeric_TPS | None = None,
|
|
210
|
+
maximum_per_hour: Numeric_TPS | None = None,
|
|
211
|
+
minimum_total: Numeric_PS | None = None,
|
|
212
|
+
maximum_total: Numeric_PS | None = None,
|
|
213
|
+
minimum_over_periods: Numeric_S | None = None,
|
|
214
|
+
maximum_over_periods: Numeric_S | None = None,
|
|
181
215
|
):
|
|
182
216
|
super().__init__(label, meta_data=meta_data)
|
|
183
217
|
self.unit = unit
|
|
184
218
|
self.description = description
|
|
185
219
|
self.is_standard = is_standard
|
|
186
|
-
self.is_objective = is_objective
|
|
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
220
|
|
|
190
|
-
#
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
)
|
|
221
|
+
# Validate that Penalty cannot be set as objective
|
|
222
|
+
if is_objective and label == PENALTY_EFFECT_LABEL:
|
|
223
|
+
raise ValueError(
|
|
224
|
+
f'The Penalty effect ("{PENALTY_EFFECT_LABEL}") cannot be set as the objective effect. '
|
|
225
|
+
f'Please use a different effect as the optimization objective.'
|
|
226
|
+
)
|
|
205
227
|
|
|
206
|
-
|
|
207
|
-
self.
|
|
228
|
+
self.is_objective = is_objective
|
|
229
|
+
self.period_weights = period_weights
|
|
230
|
+
# Share parameters accept Effect_* | Numeric_* unions (dict or single value).
|
|
231
|
+
# Store as-is here; transform_data() will normalize via fit_effects_to_model_coords().
|
|
232
|
+
# Default to {} when None (no shares defined).
|
|
233
|
+
self.share_from_temporal = share_from_temporal if share_from_temporal is not None else {}
|
|
234
|
+
self.share_from_periodic = share_from_periodic if share_from_periodic is not None else {}
|
|
208
235
|
|
|
209
236
|
# Set attributes directly
|
|
210
237
|
self.minimum_temporal = minimum_temporal
|
|
@@ -215,166 +242,58 @@ class Effect(Element):
|
|
|
215
242
|
self.maximum_per_hour = maximum_per_hour
|
|
216
243
|
self.minimum_total = minimum_total
|
|
217
244
|
self.maximum_total = maximum_total
|
|
245
|
+
self.minimum_over_periods = minimum_over_periods
|
|
246
|
+
self.maximum_over_periods = maximum_over_periods
|
|
218
247
|
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
248
|
+
def link_to_flow_system(self, flow_system, prefix: str = '') -> None:
|
|
249
|
+
"""Link this effect to a FlowSystem.
|
|
239
250
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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,
|
|
277
|
-
)
|
|
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,
|
|
287
|
-
)
|
|
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,
|
|
297
|
-
)
|
|
298
|
-
self.maximum_periodic = value
|
|
299
|
-
|
|
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,
|
|
307
|
-
)
|
|
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)
|
|
251
|
+
Elements use their label_full as prefix by default, ignoring the passed prefix.
|
|
252
|
+
"""
|
|
253
|
+
super().link_to_flow_system(flow_system, self.label_full)
|
|
343
254
|
|
|
344
|
-
|
|
255
|
+
def transform_data(self) -> None:
|
|
256
|
+
self.minimum_per_hour = self._fit_coords(f'{self.prefix}|minimum_per_hour', self.minimum_per_hour)
|
|
257
|
+
self.maximum_per_hour = self._fit_coords(f'{self.prefix}|maximum_per_hour', self.maximum_per_hour)
|
|
345
258
|
|
|
346
|
-
self.share_from_temporal =
|
|
347
|
-
|
|
259
|
+
self.share_from_temporal = self._fit_effect_coords(
|
|
260
|
+
prefix=None,
|
|
348
261
|
effect_values=self.share_from_temporal,
|
|
349
|
-
|
|
350
|
-
dims=['time', 'period', 'scenario'],
|
|
262
|
+
suffix=f'(temporal)->{self.prefix}(temporal)',
|
|
351
263
|
)
|
|
352
|
-
self.share_from_periodic =
|
|
353
|
-
|
|
264
|
+
self.share_from_periodic = self._fit_effect_coords(
|
|
265
|
+
prefix=None,
|
|
354
266
|
effect_values=self.share_from_periodic,
|
|
355
|
-
|
|
267
|
+
suffix=f'(periodic)->{self.prefix}(periodic)',
|
|
356
268
|
dims=['period', 'scenario'],
|
|
357
269
|
)
|
|
358
270
|
|
|
359
|
-
self.minimum_temporal =
|
|
360
|
-
f'{prefix}|minimum_temporal', self.minimum_temporal, dims=['period', 'scenario']
|
|
271
|
+
self.minimum_temporal = self._fit_coords(
|
|
272
|
+
f'{self.prefix}|minimum_temporal', self.minimum_temporal, dims=['period', 'scenario']
|
|
361
273
|
)
|
|
362
|
-
self.maximum_temporal =
|
|
363
|
-
f'{prefix}|maximum_temporal', self.maximum_temporal, dims=['period', 'scenario']
|
|
274
|
+
self.maximum_temporal = self._fit_coords(
|
|
275
|
+
f'{self.prefix}|maximum_temporal', self.maximum_temporal, dims=['period', 'scenario']
|
|
364
276
|
)
|
|
365
|
-
self.minimum_periodic =
|
|
366
|
-
f'{prefix}|minimum_periodic', self.minimum_periodic, dims=['period', 'scenario']
|
|
277
|
+
self.minimum_periodic = self._fit_coords(
|
|
278
|
+
f'{self.prefix}|minimum_periodic', self.minimum_periodic, dims=['period', 'scenario']
|
|
367
279
|
)
|
|
368
|
-
self.maximum_periodic =
|
|
369
|
-
f'{prefix}|maximum_periodic', self.maximum_periodic, dims=['period', 'scenario']
|
|
280
|
+
self.maximum_periodic = self._fit_coords(
|
|
281
|
+
f'{self.prefix}|maximum_periodic', self.maximum_periodic, dims=['period', 'scenario']
|
|
370
282
|
)
|
|
371
|
-
self.minimum_total =
|
|
372
|
-
f'{prefix}|minimum_total',
|
|
373
|
-
|
|
374
|
-
|
|
283
|
+
self.minimum_total = self._fit_coords(
|
|
284
|
+
f'{self.prefix}|minimum_total', self.minimum_total, dims=['period', 'scenario']
|
|
285
|
+
)
|
|
286
|
+
self.maximum_total = self._fit_coords(
|
|
287
|
+
f'{self.prefix}|maximum_total', self.maximum_total, dims=['period', 'scenario']
|
|
288
|
+
)
|
|
289
|
+
self.minimum_over_periods = self._fit_coords(
|
|
290
|
+
f'{self.prefix}|minimum_over_periods', self.minimum_over_periods, dims=['scenario']
|
|
291
|
+
)
|
|
292
|
+
self.maximum_over_periods = self._fit_coords(
|
|
293
|
+
f'{self.prefix}|maximum_over_periods', self.maximum_over_periods, dims=['scenario']
|
|
375
294
|
)
|
|
376
|
-
self.
|
|
377
|
-
f'{prefix}|
|
|
295
|
+
self.period_weights = self._fit_coords(
|
|
296
|
+
f'{self.prefix}|period_weights', self.period_weights, dims=['period', 'scenario']
|
|
378
297
|
)
|
|
379
298
|
|
|
380
299
|
def create_model(self, model: FlowSystemModel) -> EffectModel:
|
|
@@ -383,17 +302,57 @@ class Effect(Element):
|
|
|
383
302
|
return self.submodel
|
|
384
303
|
|
|
385
304
|
def _plausibility_checks(self) -> None:
|
|
386
|
-
#
|
|
387
|
-
|
|
305
|
+
# Check that minimum_over_periods and maximum_over_periods require a period dimension
|
|
306
|
+
if (
|
|
307
|
+
self.minimum_over_periods is not None or self.maximum_over_periods is not None
|
|
308
|
+
) and self.flow_system.periods is None:
|
|
309
|
+
raise PlausibilityError(
|
|
310
|
+
f"Effect '{self.label}': minimum_over_periods and maximum_over_periods require "
|
|
311
|
+
f"the FlowSystem to have a 'period' dimension. Please define periods when creating "
|
|
312
|
+
f'the FlowSystem, or remove these constraints.'
|
|
313
|
+
)
|
|
388
314
|
|
|
389
315
|
|
|
390
316
|
class EffectModel(ElementModel):
|
|
317
|
+
"""Mathematical model implementation for Effects.
|
|
318
|
+
|
|
319
|
+
Creates optimization variables and constraints for effect aggregation,
|
|
320
|
+
including periodic and temporal tracking, cross-effect contributions,
|
|
321
|
+
and effect bounds.
|
|
322
|
+
|
|
323
|
+
Mathematical Formulation:
|
|
324
|
+
See <https://flixopt.github.io/flixopt/latest/user-guide/mathematical-notation/effects-and-dimensions/>
|
|
325
|
+
"""
|
|
326
|
+
|
|
391
327
|
element: Effect # Type hint
|
|
392
328
|
|
|
393
329
|
def __init__(self, model: FlowSystemModel, element: Effect):
|
|
394
330
|
super().__init__(model, element)
|
|
395
331
|
|
|
332
|
+
@property
|
|
333
|
+
def period_weights(self) -> xr.DataArray:
|
|
334
|
+
"""
|
|
335
|
+
Get period weights for this effect.
|
|
336
|
+
|
|
337
|
+
Returns effect-specific weights if defined, otherwise falls back to FlowSystem period weights.
|
|
338
|
+
This allows different effects to have different weighting schemes over periods (e.g., discounting for costs,
|
|
339
|
+
equal weights for CO2 emissions).
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
Weights with period dimensions (if applicable)
|
|
343
|
+
"""
|
|
344
|
+
effect_weights = self.element.period_weights
|
|
345
|
+
default_weights = self.element._flow_system.period_weights
|
|
346
|
+
if effect_weights is not None: # Use effect-specific weights
|
|
347
|
+
return effect_weights
|
|
348
|
+
elif default_weights is not None: # Fall back to FlowSystem weights
|
|
349
|
+
return default_weights
|
|
350
|
+
return self.element._fit_coords(name='period_weights', data=1, dims=['period'])
|
|
351
|
+
|
|
396
352
|
def _do_modeling(self):
|
|
353
|
+
"""Create variables, constraints, and nested submodels"""
|
|
354
|
+
super()._do_modeling()
|
|
355
|
+
|
|
397
356
|
self.total: linopy.Variable | None = None
|
|
398
357
|
self.periodic: ShareAllocationModel = self.add_submodels(
|
|
399
358
|
ShareAllocationModel(
|
|
@@ -426,39 +385,54 @@ class EffectModel(ElementModel):
|
|
|
426
385
|
upper=self.element.maximum_total if self.element.maximum_total is not None else np.inf,
|
|
427
386
|
coords=self._model.get_coords(['period', 'scenario']),
|
|
428
387
|
name=self.label_full,
|
|
388
|
+
category=VariableCategory.TOTAL,
|
|
429
389
|
)
|
|
430
390
|
|
|
431
391
|
self.add_constraints(
|
|
432
392
|
self.total == self.temporal.total + self.periodic.total, name=self.label_full, short_name='total'
|
|
433
393
|
)
|
|
434
394
|
|
|
395
|
+
# Add weighted sum over all periods constraint if minimum_over_periods or maximum_over_periods is defined
|
|
396
|
+
if self.element.minimum_over_periods is not None or self.element.maximum_over_periods is not None:
|
|
397
|
+
# Calculate weighted sum over all periods
|
|
398
|
+
weighted_total = (self.total * self.period_weights).sum('period')
|
|
435
399
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
400
|
+
# Create tracking variable for the weighted sum
|
|
401
|
+
self.total_over_periods = self.add_variables(
|
|
402
|
+
lower=self.element.minimum_over_periods if self.element.minimum_over_periods is not None else -np.inf,
|
|
403
|
+
upper=self.element.maximum_over_periods if self.element.maximum_over_periods is not None else np.inf,
|
|
404
|
+
coords=self._model.get_coords(['scenario']),
|
|
405
|
+
short_name='total_over_periods',
|
|
406
|
+
category=VariableCategory.TOTAL_OVER_PERIODS,
|
|
407
|
+
)
|
|
441
408
|
|
|
442
|
-
|
|
443
|
-
""" This datatype is used internally to handle temporal shares to an effect. """
|
|
409
|
+
self.add_constraints(self.total_over_periods == weighted_total, short_name='total_over_periods')
|
|
444
410
|
|
|
445
|
-
PeriodicEffects = dict[str, Scalar]
|
|
446
|
-
""" This datatype is used internally to handle scalar shares to an effect. """
|
|
447
411
|
|
|
448
412
|
EffectExpr = dict[str, linopy.LinearExpression] # Used to create Shares
|
|
449
413
|
|
|
450
414
|
|
|
451
|
-
class EffectCollection:
|
|
415
|
+
class EffectCollection(ElementContainer[Effect]):
|
|
452
416
|
"""
|
|
453
417
|
Handling all Effects
|
|
454
418
|
"""
|
|
455
419
|
|
|
456
|
-
|
|
457
|
-
|
|
420
|
+
submodel: EffectCollectionModel | None
|
|
421
|
+
|
|
422
|
+
def __init__(self, *effects: Effect, truncate_repr: int | None = None):
|
|
423
|
+
"""
|
|
424
|
+
Initialize the EffectCollection.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
*effects: Effects to register in the collection.
|
|
428
|
+
truncate_repr: Maximum number of items to show in repr. If None, show all items. Default: None
|
|
429
|
+
"""
|
|
430
|
+
super().__init__(element_type_name='effects', truncate_repr=truncate_repr)
|
|
458
431
|
self._standard_effect: Effect | None = None
|
|
459
432
|
self._objective_effect: Effect | None = None
|
|
433
|
+
self._penalty_effect: Effect | None = None
|
|
460
434
|
|
|
461
|
-
self.submodel
|
|
435
|
+
self.submodel = None
|
|
462
436
|
self.add_effects(*effects)
|
|
463
437
|
|
|
464
438
|
def create_model(self, model: FlowSystemModel) -> EffectCollectionModel:
|
|
@@ -466,6 +440,29 @@ class EffectCollection:
|
|
|
466
440
|
self.submodel = EffectCollectionModel(model, self)
|
|
467
441
|
return self.submodel
|
|
468
442
|
|
|
443
|
+
def _create_penalty_effect(self) -> Effect:
|
|
444
|
+
"""
|
|
445
|
+
Create and register the penalty effect (called internally by FlowSystem).
|
|
446
|
+
Only creates if user hasn't already defined a Penalty effect.
|
|
447
|
+
"""
|
|
448
|
+
# Check if user has already defined a Penalty effect
|
|
449
|
+
if PENALTY_EFFECT_LABEL in self:
|
|
450
|
+
self._penalty_effect = self[PENALTY_EFFECT_LABEL]
|
|
451
|
+
logger.info(f'Using user-defined Penalty Effect: {PENALTY_EFFECT_LABEL}')
|
|
452
|
+
return self._penalty_effect
|
|
453
|
+
|
|
454
|
+
# Auto-create penalty effect
|
|
455
|
+
self._penalty_effect = Effect(
|
|
456
|
+
label=PENALTY_EFFECT_LABEL,
|
|
457
|
+
unit='penalty_units',
|
|
458
|
+
description='Penalty for constraint violations and modeling artifacts',
|
|
459
|
+
is_standard=False,
|
|
460
|
+
is_objective=False,
|
|
461
|
+
)
|
|
462
|
+
self.add(self._penalty_effect) # Add to container
|
|
463
|
+
logger.info(f'Auto-created Penalty Effect: {PENALTY_EFFECT_LABEL}')
|
|
464
|
+
return self._penalty_effect
|
|
465
|
+
|
|
469
466
|
def add_effects(self, *effects: Effect) -> None:
|
|
470
467
|
for effect in list(effects):
|
|
471
468
|
if effect in self:
|
|
@@ -474,43 +471,35 @@ class EffectCollection:
|
|
|
474
471
|
self.standard_effect = effect
|
|
475
472
|
if effect.is_objective:
|
|
476
473
|
self.objective_effect = effect
|
|
477
|
-
self.
|
|
474
|
+
self.add(effect) # Use the inherited add() method from ElementContainer
|
|
478
475
|
logger.info(f'Registered new Effect: {effect.label}')
|
|
479
476
|
|
|
480
|
-
def create_effect_values_dict(
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
Returns
|
|
494
|
-
-------
|
|
495
|
-
dict or None
|
|
477
|
+
def create_effect_values_dict(self, effect_values_user: Numeric_TPS | Effect_TPS | None) -> Effect_TPS | None:
|
|
478
|
+
"""Converts effect values into a dictionary. If a scalar is provided, it is associated with a default effect type.
|
|
479
|
+
|
|
480
|
+
Examples:
|
|
481
|
+
```python
|
|
482
|
+
effect_values_user = 20 -> {'<standard_effect_label>': 20}
|
|
483
|
+
effect_values_user = {None: 20} -> {'<standard_effect_label>': 20}
|
|
484
|
+
effect_values_user = None -> None
|
|
485
|
+
effect_values_user = {'effect1': 20, 'effect2': 0.3} -> {'effect1': 20, 'effect2': 0.3}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
496
489
|
A dictionary keyed by effect label, or None if input is None.
|
|
497
490
|
Note: a standard effect must be defined when passing scalars or None labels.
|
|
498
491
|
"""
|
|
499
492
|
|
|
500
|
-
def get_effect_label(eff:
|
|
501
|
-
"""
|
|
493
|
+
def get_effect_label(eff: str | None) -> str:
|
|
494
|
+
"""Get the label of an effect"""
|
|
495
|
+
if eff is None:
|
|
496
|
+
return self.standard_effect.label
|
|
502
497
|
if isinstance(eff, Effect):
|
|
503
|
-
|
|
504
|
-
f'
|
|
505
|
-
f'Use the label
|
|
506
|
-
UserWarning,
|
|
507
|
-
stacklevel=2,
|
|
498
|
+
raise TypeError(
|
|
499
|
+
f'Effect objects are no longer accepted when specifying EffectValues. '
|
|
500
|
+
f'Use the label string instead. Got: {eff.label_full}'
|
|
508
501
|
)
|
|
509
|
-
|
|
510
|
-
elif eff is None:
|
|
511
|
-
return self.standard_effect.label
|
|
512
|
-
else:
|
|
513
|
-
return eff
|
|
502
|
+
return eff
|
|
514
503
|
|
|
515
504
|
if effect_values_user is None:
|
|
516
505
|
return None
|
|
@@ -522,10 +511,13 @@ class EffectCollection:
|
|
|
522
511
|
# Check circular loops in effects:
|
|
523
512
|
temporal, periodic = self.calculate_effect_share_factors()
|
|
524
513
|
|
|
525
|
-
# Validate all referenced sources exist
|
|
526
|
-
|
|
514
|
+
# Validate all referenced effects (both sources and targets) exist
|
|
515
|
+
edges = list(temporal.keys()) + list(periodic.keys())
|
|
516
|
+
unknown_sources = {src for src, _ in edges if src not in self}
|
|
517
|
+
unknown_targets = {tgt for _, tgt in edges if tgt not in self}
|
|
518
|
+
unknown = unknown_sources | unknown_targets
|
|
527
519
|
if unknown:
|
|
528
|
-
raise KeyError(f'Unknown effects used in
|
|
520
|
+
raise KeyError(f'Unknown effects used in effect share mappings: {sorted(unknown)}')
|
|
529
521
|
|
|
530
522
|
temporal_cycles = detect_cycles(tuples_to_adjacency_list([key for key in temporal]))
|
|
531
523
|
periodic_cycles = detect_cycles(tuples_to_adjacency_list([key for key in periodic]))
|
|
@@ -554,31 +546,23 @@ class EffectCollection:
|
|
|
554
546
|
else:
|
|
555
547
|
raise KeyError(f'Effect {effect} not found!')
|
|
556
548
|
try:
|
|
557
|
-
return
|
|
549
|
+
return super().__getitem__(effect) # Leverage ContainerMixin suggestions
|
|
558
550
|
except KeyError as e:
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
return iter(self._effects.values())
|
|
551
|
+
# Extract the original message and append context for cleaner output
|
|
552
|
+
original_msg = str(e).strip('\'"')
|
|
553
|
+
raise KeyError(f'{original_msg} Add the effect to the FlowSystem first.') from None
|
|
563
554
|
|
|
564
|
-
def
|
|
565
|
-
return
|
|
555
|
+
def __iter__(self) -> Iterator[str]:
|
|
556
|
+
return iter(self.keys()) # Iterate over keys like a normal dict
|
|
566
557
|
|
|
567
558
|
def __contains__(self, item: str | Effect) -> bool:
|
|
568
559
|
"""Check if the effect exists. Checks for label or object"""
|
|
569
560
|
if isinstance(item, str):
|
|
570
|
-
return item
|
|
561
|
+
return super().__contains__(item) # Check if the label exists
|
|
571
562
|
elif isinstance(item, Effect):
|
|
572
|
-
|
|
573
|
-
return True
|
|
574
|
-
if item in self.effects.values(): # Check if the object exists
|
|
575
|
-
return True
|
|
563
|
+
return item.label_full in self and self[item.label_full] is item
|
|
576
564
|
return False
|
|
577
565
|
|
|
578
|
-
@property
|
|
579
|
-
def effects(self) -> dict[str, Effect]:
|
|
580
|
-
return self._effects
|
|
581
|
-
|
|
582
566
|
@property
|
|
583
567
|
def standard_effect(self) -> Effect:
|
|
584
568
|
if self._standard_effect is None:
|
|
@@ -602,10 +586,38 @@ class EffectCollection:
|
|
|
602
586
|
|
|
603
587
|
@objective_effect.setter
|
|
604
588
|
def objective_effect(self, value: Effect) -> None:
|
|
589
|
+
# Check Penalty first to give users a more specific error message
|
|
590
|
+
if value.label == PENALTY_EFFECT_LABEL:
|
|
591
|
+
raise ValueError(
|
|
592
|
+
f'The Penalty effect ("{PENALTY_EFFECT_LABEL}") cannot be set as the objective effect. '
|
|
593
|
+
f'Please use a different effect as the optimization objective.'
|
|
594
|
+
)
|
|
605
595
|
if self._objective_effect is not None:
|
|
606
596
|
raise ValueError(f'An objective-effect already exists! ({self._objective_effect.label=})')
|
|
607
597
|
self._objective_effect = value
|
|
608
598
|
|
|
599
|
+
@property
|
|
600
|
+
def penalty_effect(self) -> Effect:
|
|
601
|
+
"""
|
|
602
|
+
The penalty effect (auto-created during modeling if not user-defined).
|
|
603
|
+
|
|
604
|
+
Returns the Penalty effect whether user-defined or auto-created.
|
|
605
|
+
"""
|
|
606
|
+
# If already set, return it
|
|
607
|
+
if self._penalty_effect is not None:
|
|
608
|
+
return self._penalty_effect
|
|
609
|
+
|
|
610
|
+
# Check if user has defined a Penalty effect
|
|
611
|
+
if PENALTY_EFFECT_LABEL in self:
|
|
612
|
+
self._penalty_effect = self[PENALTY_EFFECT_LABEL]
|
|
613
|
+
return self._penalty_effect
|
|
614
|
+
|
|
615
|
+
# Not yet created - will be created during modeling
|
|
616
|
+
raise KeyError(
|
|
617
|
+
f'Penalty effect not yet created. It will be auto-created during modeling, '
|
|
618
|
+
f'or you can define your own using: Effect("{PENALTY_EFFECT_LABEL}", ...)'
|
|
619
|
+
)
|
|
620
|
+
|
|
609
621
|
def calculate_effect_share_factors(
|
|
610
622
|
self,
|
|
611
623
|
) -> tuple[
|
|
@@ -613,7 +625,7 @@ class EffectCollection:
|
|
|
613
625
|
dict[tuple[str, str], xr.DataArray],
|
|
614
626
|
]:
|
|
615
627
|
shares_periodic = {}
|
|
616
|
-
for name, effect in self.
|
|
628
|
+
for name, effect in self.items():
|
|
617
629
|
if effect.share_from_periodic:
|
|
618
630
|
for source, data in effect.share_from_periodic.items():
|
|
619
631
|
if source not in shares_periodic:
|
|
@@ -622,7 +634,7 @@ class EffectCollection:
|
|
|
622
634
|
shares_periodic = calculate_all_conversion_paths(shares_periodic)
|
|
623
635
|
|
|
624
636
|
shares_temporal = {}
|
|
625
|
-
for name, effect in self.
|
|
637
|
+
for name, effect in self.items():
|
|
626
638
|
if effect.share_from_temporal:
|
|
627
639
|
for source, data in effect.share_from_temporal.items():
|
|
628
640
|
if source not in shares_temporal:
|
|
@@ -640,7 +652,6 @@ class EffectCollectionModel(Submodel):
|
|
|
640
652
|
|
|
641
653
|
def __init__(self, model: FlowSystemModel, effects: EffectCollection):
|
|
642
654
|
self.effects = effects
|
|
643
|
-
self.penalty: ShareAllocationModel | None = None
|
|
644
655
|
super().__init__(model, label_of_element='Effects')
|
|
645
656
|
|
|
646
657
|
def add_share_to_effects(
|
|
@@ -665,28 +676,32 @@ class EffectCollectionModel(Submodel):
|
|
|
665
676
|
else:
|
|
666
677
|
raise ValueError(f'Target {target} not supported!')
|
|
667
678
|
|
|
668
|
-
def add_share_to_penalty(self, name: str, expression: linopy.LinearExpression) -> None:
|
|
669
|
-
if expression.ndim != 0:
|
|
670
|
-
raise TypeError(f'Penalty shares must be scalar expressions! ({expression.ndim=})')
|
|
671
|
-
self.penalty.add_share(name, expression, dims=())
|
|
672
|
-
|
|
673
679
|
def _do_modeling(self):
|
|
680
|
+
"""Create variables, constraints, and nested submodels"""
|
|
674
681
|
super()._do_modeling()
|
|
675
|
-
|
|
682
|
+
|
|
683
|
+
# Ensure penalty effect exists (auto-create if user hasn't defined one)
|
|
684
|
+
if self.effects._penalty_effect is None:
|
|
685
|
+
penalty_effect = self.effects._create_penalty_effect()
|
|
686
|
+
# Link to FlowSystem (should already be linked, but ensure it)
|
|
687
|
+
if penalty_effect._flow_system is None:
|
|
688
|
+
penalty_effect.link_to_flow_system(self._model.flow_system)
|
|
689
|
+
|
|
690
|
+
# Create EffectModel for each effect
|
|
691
|
+
for effect in self.effects.values():
|
|
676
692
|
effect.create_model(self._model)
|
|
677
|
-
self.penalty = self.add_submodels(
|
|
678
|
-
ShareAllocationModel(self._model, dims=(), label_of_element='Penalty'),
|
|
679
|
-
short_name='penalty',
|
|
680
|
-
)
|
|
681
693
|
|
|
694
|
+
# Add cross-effect shares
|
|
682
695
|
self._add_share_between_effects()
|
|
683
696
|
|
|
697
|
+
# Use objective weights with objective effect and penalty effect
|
|
684
698
|
self._model.add_objective(
|
|
685
|
-
(self.effects.objective_effect.submodel.total * self._model.
|
|
699
|
+
(self.effects.objective_effect.submodel.total * self._model.objective_weights).sum()
|
|
700
|
+
+ (self.effects.penalty_effect.submodel.total * self._model.objective_weights).sum()
|
|
686
701
|
)
|
|
687
702
|
|
|
688
703
|
def _add_share_between_effects(self):
|
|
689
|
-
for target_effect in self.effects:
|
|
704
|
+
for target_effect in self.effects.values():
|
|
690
705
|
# 1. temporal: <- receiving temporal shares from other effects
|
|
691
706
|
for source_effect, time_series in target_effect.share_from_temporal.items():
|
|
692
707
|
target_effect.submodel.temporal.add_share(
|
|
@@ -846,8 +861,3 @@ def tuples_to_adjacency_list(edges: list[tuple[str, str]]) -> dict[str, list[str
|
|
|
846
861
|
graph[target] = []
|
|
847
862
|
|
|
848
863
|
return graph
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
# Backward compatibility aliases
|
|
852
|
-
NonTemporalEffectsUser = PeriodicEffectsUser
|
|
853
|
-
NonTemporalEffects = PeriodicEffects
|