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/features.py
CHANGED
|
@@ -5,33 +5,39 @@ Features extend the functionality of Elements.
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
import logging
|
|
9
8
|
from typing import TYPE_CHECKING
|
|
10
9
|
|
|
11
10
|
import linopy
|
|
12
11
|
import numpy as np
|
|
13
12
|
|
|
14
13
|
from .modeling import BoundingPatterns, ModelingPrimitives, ModelingUtilities
|
|
15
|
-
from .structure import FlowSystemModel, Submodel
|
|
14
|
+
from .structure import FlowSystemModel, Submodel, VariableCategory
|
|
16
15
|
|
|
17
16
|
if TYPE_CHECKING:
|
|
18
|
-
from .
|
|
19
|
-
from .interface import InvestParameters, OnOffParameters, Piecewise
|
|
17
|
+
from collections.abc import Collection
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
import xarray as xr
|
|
20
|
+
|
|
21
|
+
from .core import FlowSystemDimensions
|
|
22
|
+
from .interface import InvestParameters, Piecewise, StatusParameters
|
|
23
|
+
from .types import Numeric_PS, Numeric_TPS
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
class InvestmentModel(Submodel):
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
"""Mathematical model implementation for investment decisions.
|
|
28
|
+
|
|
29
|
+
Creates optimization variables and constraints for investment sizing decisions,
|
|
30
|
+
supporting both binary and continuous sizing with comprehensive effect modeling.
|
|
31
|
+
|
|
32
|
+
Mathematical Formulation:
|
|
33
|
+
See <https://flixopt.github.io/flixopt/latest/user-guide/mathematical-notation/features/InvestParameters/>
|
|
28
34
|
|
|
29
35
|
Args:
|
|
30
36
|
model: The optimization model instance
|
|
31
37
|
label_of_element: The label of the parent (Element). Used to construct the full label of the model.
|
|
32
38
|
parameters: The parameters of the feature model.
|
|
33
39
|
label_of_model: The label of the model. This is needed to construct the full label of the model.
|
|
34
|
-
|
|
40
|
+
size_category: Category for the size variable (FLOW_SIZE, STORAGE_SIZE, or SIZE for generic).
|
|
35
41
|
"""
|
|
36
42
|
|
|
37
43
|
parameters: InvestParameters
|
|
@@ -42,9 +48,11 @@ class InvestmentModel(Submodel):
|
|
|
42
48
|
label_of_element: str,
|
|
43
49
|
parameters: InvestParameters,
|
|
44
50
|
label_of_model: str | None = None,
|
|
51
|
+
size_category: VariableCategory = VariableCategory.SIZE,
|
|
45
52
|
):
|
|
46
53
|
self.piecewise_effects: PiecewiseEffectsModel | None = None
|
|
47
54
|
self.parameters = parameters
|
|
55
|
+
self._size_category = size_category
|
|
48
56
|
super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model)
|
|
49
57
|
|
|
50
58
|
def _do_modeling(self):
|
|
@@ -64,6 +72,7 @@ class InvestmentModel(Submodel):
|
|
|
64
72
|
lower=size_min if self.parameters.mandatory else 0,
|
|
65
73
|
upper=size_max,
|
|
66
74
|
coords=self._model.get_coords(['period', 'scenario']),
|
|
75
|
+
category=self._size_category,
|
|
67
76
|
)
|
|
68
77
|
|
|
69
78
|
if not self.parameters.mandatory:
|
|
@@ -71,11 +80,12 @@ class InvestmentModel(Submodel):
|
|
|
71
80
|
binary=True,
|
|
72
81
|
coords=self._model.get_coords(['period', 'scenario']),
|
|
73
82
|
short_name='invested',
|
|
83
|
+
category=VariableCategory.INVESTED,
|
|
74
84
|
)
|
|
75
85
|
BoundingPatterns.bounds_with_state(
|
|
76
86
|
self,
|
|
77
87
|
variable=self.size,
|
|
78
|
-
|
|
88
|
+
state=self._variables['invested'],
|
|
79
89
|
bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size),
|
|
80
90
|
)
|
|
81
91
|
|
|
@@ -144,124 +154,168 @@ class InvestmentModel(Submodel):
|
|
|
144
154
|
return self._variables['invested']
|
|
145
155
|
|
|
146
156
|
|
|
147
|
-
class
|
|
148
|
-
"""
|
|
157
|
+
class StatusModel(Submodel):
|
|
158
|
+
"""Mathematical model implementation for binary status.
|
|
159
|
+
|
|
160
|
+
Creates optimization variables and constraints for binary status modeling,
|
|
161
|
+
state transitions, duration tracking, and operational effects.
|
|
162
|
+
|
|
163
|
+
Mathematical Formulation:
|
|
164
|
+
See <https://flixopt.github.io/flixopt/latest/user-guide/mathematical-notation/features/StatusParameters/>
|
|
165
|
+
"""
|
|
149
166
|
|
|
150
167
|
def __init__(
|
|
151
168
|
self,
|
|
152
169
|
model: FlowSystemModel,
|
|
153
170
|
label_of_element: str,
|
|
154
|
-
parameters:
|
|
155
|
-
|
|
156
|
-
|
|
171
|
+
parameters: StatusParameters,
|
|
172
|
+
status: linopy.Variable,
|
|
173
|
+
previous_status: xr.DataArray | None,
|
|
157
174
|
label_of_model: str | None = None,
|
|
158
175
|
):
|
|
159
176
|
"""
|
|
160
|
-
This feature model is used to model the
|
|
161
|
-
bounded by a size variable or by a hard bound.
|
|
177
|
+
This feature model is used to model the status (active/inactive) state of flow_rate(s).
|
|
178
|
+
It does not matter if the flow_rates are bounded by a size variable or by a hard bound.
|
|
179
|
+
The used bound here is the absolute highest/lowest bound!
|
|
162
180
|
|
|
163
181
|
Args:
|
|
164
182
|
model: The optimization model instance
|
|
165
183
|
label_of_element: The label of the parent (Element). Used to construct the full label of the model.
|
|
166
184
|
parameters: The parameters of the feature model.
|
|
167
|
-
|
|
168
|
-
|
|
185
|
+
status: The variable that determines the active state
|
|
186
|
+
previous_status: The previous flow_rates
|
|
169
187
|
label_of_model: The label of the model. This is needed to construct the full label of the model.
|
|
170
188
|
"""
|
|
171
|
-
self.
|
|
172
|
-
self.
|
|
189
|
+
self.status = status
|
|
190
|
+
self._previous_status = previous_status
|
|
173
191
|
self.parameters = parameters
|
|
174
192
|
super().__init__(model, label_of_element, label_of_model=label_of_model)
|
|
175
193
|
|
|
176
194
|
def _do_modeling(self):
|
|
195
|
+
"""Create variables, constraints, and nested submodels"""
|
|
177
196
|
super()._do_modeling()
|
|
178
197
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
198
|
+
# Create a separate binary 'inactive' variable when needed for downtime tracking or explicit use
|
|
199
|
+
# When not needed, the expression (1 - self.status) can be used instead
|
|
200
|
+
if self.parameters.use_downtime_tracking:
|
|
201
|
+
inactive = self.add_variables(
|
|
202
|
+
binary=True,
|
|
203
|
+
short_name='inactive',
|
|
204
|
+
coords=self._model.get_coords(),
|
|
205
|
+
category=VariableCategory.INACTIVE,
|
|
206
|
+
)
|
|
207
|
+
self.add_constraints(self.status + inactive == 1, short_name='complementary')
|
|
182
208
|
|
|
183
|
-
# 3. Total duration tracking
|
|
209
|
+
# 3. Total duration tracking
|
|
210
|
+
total_hours = self._model.temporal_weight.sum(self._model.temporal_dims)
|
|
184
211
|
ModelingPrimitives.expression_tracking_variable(
|
|
185
212
|
self,
|
|
186
|
-
tracked_expression=
|
|
213
|
+
tracked_expression=self._model.sum_temporal(self.status),
|
|
187
214
|
bounds=(
|
|
188
|
-
self.parameters.
|
|
189
|
-
self.parameters.
|
|
190
|
-
),
|
|
191
|
-
short_name='
|
|
215
|
+
self.parameters.active_hours_min if self.parameters.active_hours_min is not None else 0,
|
|
216
|
+
self.parameters.active_hours_max if self.parameters.active_hours_max is not None else total_hours,
|
|
217
|
+
),
|
|
218
|
+
short_name='active_hours',
|
|
192
219
|
coords=['period', 'scenario'],
|
|
220
|
+
category=VariableCategory.TOTAL,
|
|
193
221
|
)
|
|
194
222
|
|
|
195
223
|
# 4. Switch tracking using existing pattern
|
|
196
|
-
if self.parameters.
|
|
197
|
-
self.add_variables(
|
|
198
|
-
|
|
224
|
+
if self.parameters.use_startup_tracking:
|
|
225
|
+
self.add_variables(
|
|
226
|
+
binary=True,
|
|
227
|
+
short_name='startup',
|
|
228
|
+
coords=self.get_coords(),
|
|
229
|
+
category=VariableCategory.STARTUP,
|
|
230
|
+
)
|
|
231
|
+
self.add_variables(
|
|
232
|
+
binary=True,
|
|
233
|
+
short_name='shutdown',
|
|
234
|
+
coords=self.get_coords(),
|
|
235
|
+
category=VariableCategory.SHUTDOWN,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Determine previous_state: None means relaxed (no constraint at t=0)
|
|
239
|
+
previous_state = self._previous_status.isel(time=-1) if self._previous_status is not None else None
|
|
199
240
|
|
|
200
241
|
BoundingPatterns.state_transition_bounds(
|
|
201
242
|
self,
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
243
|
+
state=self.status,
|
|
244
|
+
activate=self.startup,
|
|
245
|
+
deactivate=self.shutdown,
|
|
205
246
|
name=f'{self.label_of_model}|switch',
|
|
206
|
-
previous_state=
|
|
247
|
+
previous_state=previous_state,
|
|
207
248
|
coord='time',
|
|
208
249
|
)
|
|
209
250
|
|
|
210
|
-
if self.parameters.
|
|
251
|
+
if self.parameters.startup_limit is not None:
|
|
211
252
|
count = self.add_variables(
|
|
212
253
|
lower=0,
|
|
213
|
-
upper=self.parameters.
|
|
254
|
+
upper=self.parameters.startup_limit,
|
|
214
255
|
coords=self._model.get_coords(('period', 'scenario')),
|
|
215
|
-
short_name='
|
|
256
|
+
short_name='startup_count',
|
|
257
|
+
category=VariableCategory.STARTUP_COUNT,
|
|
216
258
|
)
|
|
217
|
-
|
|
259
|
+
# Sum over all temporal dimensions (time, and cluster if present)
|
|
260
|
+
startup_temporal_dims = [d for d in self.startup.dims if d not in ('period', 'scenario')]
|
|
261
|
+
self.add_constraints(count == self.startup.sum(startup_temporal_dims), short_name='startup_count')
|
|
218
262
|
|
|
219
|
-
# 5. Consecutive
|
|
220
|
-
if self.parameters.
|
|
263
|
+
# 5. Consecutive active duration (uptime) using existing pattern
|
|
264
|
+
if self.parameters.use_uptime_tracking:
|
|
221
265
|
ModelingPrimitives.consecutive_duration_tracking(
|
|
222
266
|
self,
|
|
223
|
-
|
|
224
|
-
short_name='
|
|
225
|
-
minimum_duration=self.parameters.
|
|
226
|
-
maximum_duration=self.parameters.
|
|
227
|
-
duration_per_step=self.
|
|
267
|
+
state=self.status,
|
|
268
|
+
short_name='uptime',
|
|
269
|
+
minimum_duration=self.parameters.min_uptime,
|
|
270
|
+
maximum_duration=self.parameters.max_uptime,
|
|
271
|
+
duration_per_step=self.timestep_duration,
|
|
228
272
|
duration_dim='time',
|
|
229
|
-
previous_duration=self.
|
|
273
|
+
previous_duration=self._get_previous_uptime(),
|
|
230
274
|
)
|
|
231
275
|
|
|
232
|
-
# 6. Consecutive
|
|
233
|
-
if self.parameters.
|
|
276
|
+
# 6. Consecutive inactive duration (downtime) using existing pattern
|
|
277
|
+
if self.parameters.use_downtime_tracking:
|
|
234
278
|
ModelingPrimitives.consecutive_duration_tracking(
|
|
235
279
|
self,
|
|
236
|
-
|
|
237
|
-
short_name='
|
|
238
|
-
minimum_duration=self.parameters.
|
|
239
|
-
maximum_duration=self.parameters.
|
|
240
|
-
duration_per_step=self.
|
|
280
|
+
state=self.inactive,
|
|
281
|
+
short_name='downtime',
|
|
282
|
+
minimum_duration=self.parameters.min_downtime,
|
|
283
|
+
maximum_duration=self.parameters.max_downtime,
|
|
284
|
+
duration_per_step=self.timestep_duration,
|
|
241
285
|
duration_dim='time',
|
|
242
|
-
previous_duration=self.
|
|
286
|
+
previous_duration=self._get_previous_downtime(),
|
|
243
287
|
)
|
|
244
|
-
|
|
288
|
+
|
|
289
|
+
# 7. Cyclic constraint for clustered systems
|
|
290
|
+
self._add_cluster_cyclic_constraint()
|
|
245
291
|
|
|
246
292
|
self._add_effects()
|
|
247
293
|
|
|
294
|
+
def _add_cluster_cyclic_constraint(self):
|
|
295
|
+
"""For 'cyclic' cluster mode: each cluster's start status equals its end status."""
|
|
296
|
+
if self._model.flow_system.clusters is not None and self.parameters.cluster_mode == 'cyclic':
|
|
297
|
+
self.add_constraints(
|
|
298
|
+
self.status.isel(time=0) == self.status.isel(time=-1),
|
|
299
|
+
short_name='cluster_cyclic',
|
|
300
|
+
)
|
|
301
|
+
|
|
248
302
|
def _add_effects(self):
|
|
249
|
-
"""Add operational effects"""
|
|
250
|
-
if self.parameters.
|
|
303
|
+
"""Add operational effects (use timestep_duration only, cluster_weight is applied when summing to total)"""
|
|
304
|
+
if self.parameters.effects_per_active_hour:
|
|
251
305
|
self._model.effects.add_share_to_effects(
|
|
252
306
|
name=self.label_of_element,
|
|
253
307
|
expressions={
|
|
254
|
-
effect: self.
|
|
255
|
-
for effect, factor in self.parameters.
|
|
308
|
+
effect: self.status * factor * self._model.timestep_duration
|
|
309
|
+
for effect, factor in self.parameters.effects_per_active_hour.items()
|
|
256
310
|
},
|
|
257
311
|
target='temporal',
|
|
258
312
|
)
|
|
259
313
|
|
|
260
|
-
if self.parameters.
|
|
314
|
+
if self.parameters.effects_per_startup:
|
|
261
315
|
self._model.effects.add_share_to_effects(
|
|
262
316
|
name=self.label_of_element,
|
|
263
317
|
expressions={
|
|
264
|
-
effect: self.
|
|
318
|
+
effect: self.startup * factor for effect, factor in self.parameters.effects_per_startup.items()
|
|
265
319
|
},
|
|
266
320
|
target='temporal',
|
|
267
321
|
)
|
|
@@ -269,55 +323,64 @@ class OnOffModel(Submodel):
|
|
|
269
323
|
# Properties access variables from Submodel's tracking system
|
|
270
324
|
|
|
271
325
|
@property
|
|
272
|
-
def
|
|
273
|
-
"""Total
|
|
274
|
-
return self['
|
|
326
|
+
def active_hours(self) -> linopy.Variable:
|
|
327
|
+
"""Total active hours variable"""
|
|
328
|
+
return self['active_hours']
|
|
275
329
|
|
|
276
330
|
@property
|
|
277
|
-
def
|
|
278
|
-
"""Binary
|
|
279
|
-
|
|
331
|
+
def inactive(self) -> linopy.Variable | None:
|
|
332
|
+
"""Binary inactive state variable.
|
|
333
|
+
|
|
334
|
+
Note:
|
|
335
|
+
Only created when downtime tracking is enabled (min_downtime or max_downtime set).
|
|
336
|
+
For general use, prefer the expression `1 - status` instead of this variable.
|
|
337
|
+
"""
|
|
338
|
+
return self.get('inactive')
|
|
280
339
|
|
|
281
340
|
@property
|
|
282
|
-
def
|
|
283
|
-
"""
|
|
284
|
-
return self.get('
|
|
341
|
+
def startup(self) -> linopy.Variable | None:
|
|
342
|
+
"""Startup variable"""
|
|
343
|
+
return self.get('startup')
|
|
285
344
|
|
|
286
345
|
@property
|
|
287
|
-
def
|
|
288
|
-
"""
|
|
289
|
-
return self.get('
|
|
346
|
+
def shutdown(self) -> linopy.Variable | None:
|
|
347
|
+
"""Shutdown variable"""
|
|
348
|
+
return self.get('shutdown')
|
|
290
349
|
|
|
291
350
|
@property
|
|
292
|
-
def
|
|
293
|
-
"""Number of
|
|
294
|
-
return self.get('
|
|
351
|
+
def startup_count(self) -> linopy.Variable | None:
|
|
352
|
+
"""Number of startups variable"""
|
|
353
|
+
return self.get('startup_count')
|
|
295
354
|
|
|
296
355
|
@property
|
|
297
|
-
def
|
|
298
|
-
"""Consecutive
|
|
299
|
-
return self.get('
|
|
356
|
+
def uptime(self) -> linopy.Variable | None:
|
|
357
|
+
"""Consecutive active hours (uptime) variable"""
|
|
358
|
+
return self.get('uptime')
|
|
300
359
|
|
|
301
360
|
@property
|
|
302
|
-
def
|
|
303
|
-
"""Consecutive
|
|
304
|
-
return self.get('
|
|
305
|
-
|
|
306
|
-
def _get_previous_on_duration(self):
|
|
307
|
-
"""Get previous on duration. Previously OFF by default, for one timestep"""
|
|
308
|
-
hours_per_step = self._model.hours_per_step.isel(time=0).min().item()
|
|
309
|
-
if self._previous_states is None:
|
|
310
|
-
return 0
|
|
311
|
-
else:
|
|
312
|
-
return ModelingUtilities.compute_consecutive_hours_in_state(self._previous_states, hours_per_step)
|
|
361
|
+
def downtime(self) -> linopy.Variable | None:
|
|
362
|
+
"""Consecutive inactive hours (downtime) variable"""
|
|
363
|
+
return self.get('downtime')
|
|
313
364
|
|
|
314
|
-
def
|
|
315
|
-
"""Get previous
|
|
316
|
-
|
|
317
|
-
if
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
return
|
|
365
|
+
def _get_previous_uptime(self):
|
|
366
|
+
"""Get previous uptime (consecutive active hours).
|
|
367
|
+
|
|
368
|
+
Returns None if no previous status is provided (relaxed mode - no constraint at t=0).
|
|
369
|
+
"""
|
|
370
|
+
if self._previous_status is None:
|
|
371
|
+
return None # Relaxed mode
|
|
372
|
+
hours_per_step = self._model.timestep_duration.isel(time=0).min().item()
|
|
373
|
+
return ModelingUtilities.compute_consecutive_hours_in_state(self._previous_status, hours_per_step)
|
|
374
|
+
|
|
375
|
+
def _get_previous_downtime(self):
|
|
376
|
+
"""Get previous downtime (consecutive inactive hours).
|
|
377
|
+
|
|
378
|
+
Returns None if no previous status is provided (relaxed mode - no constraint at t=0).
|
|
379
|
+
"""
|
|
380
|
+
if self._previous_status is None:
|
|
381
|
+
return None # Relaxed mode
|
|
382
|
+
hours_per_step = self._model.timestep_duration.isel(time=0).min().item()
|
|
383
|
+
return ModelingUtilities.compute_consecutive_hours_in_state(1 - self._previous_status, hours_per_step)
|
|
321
384
|
|
|
322
385
|
|
|
323
386
|
class PieceModel(Submodel):
|
|
@@ -328,7 +391,7 @@ class PieceModel(Submodel):
|
|
|
328
391
|
model: FlowSystemModel,
|
|
329
392
|
label_of_element: str,
|
|
330
393
|
label_of_model: str,
|
|
331
|
-
dims: FlowSystemDimensions | None,
|
|
394
|
+
dims: Collection[FlowSystemDimensions] | None,
|
|
332
395
|
):
|
|
333
396
|
self.inside_piece: linopy.Variable | None = None
|
|
334
397
|
self.lambda0: linopy.Variable | None = None
|
|
@@ -338,17 +401,22 @@ class PieceModel(Submodel):
|
|
|
338
401
|
super().__init__(model, label_of_element, label_of_model)
|
|
339
402
|
|
|
340
403
|
def _do_modeling(self):
|
|
404
|
+
"""Create variables, constraints, and nested submodels"""
|
|
341
405
|
super()._do_modeling()
|
|
406
|
+
|
|
407
|
+
# Create variables
|
|
342
408
|
self.inside_piece = self.add_variables(
|
|
343
409
|
binary=True,
|
|
344
410
|
short_name='inside_piece',
|
|
345
411
|
coords=self._model.get_coords(dims=self.dims),
|
|
412
|
+
category=VariableCategory.INSIDE_PIECE,
|
|
346
413
|
)
|
|
347
414
|
self.lambda0 = self.add_variables(
|
|
348
415
|
lower=0,
|
|
349
416
|
upper=1,
|
|
350
417
|
short_name='lambda0',
|
|
351
418
|
coords=self._model.get_coords(dims=self.dims),
|
|
419
|
+
category=VariableCategory.LAMBDA0,
|
|
352
420
|
)
|
|
353
421
|
|
|
354
422
|
self.lambda1 = self.add_variables(
|
|
@@ -356,13 +424,24 @@ class PieceModel(Submodel):
|
|
|
356
424
|
upper=1,
|
|
357
425
|
short_name='lambda1',
|
|
358
426
|
coords=self._model.get_coords(dims=self.dims),
|
|
427
|
+
category=VariableCategory.LAMBDA1,
|
|
359
428
|
)
|
|
360
429
|
|
|
430
|
+
# Create constraints
|
|
361
431
|
# eq: lambda0(t) + lambda1(t) = inside_piece(t)
|
|
362
432
|
self.add_constraints(self.inside_piece == self.lambda0 + self.lambda1, short_name='inside_piece')
|
|
363
433
|
|
|
364
434
|
|
|
365
435
|
class PiecewiseModel(Submodel):
|
|
436
|
+
"""Mathematical model implementation for piecewise linear approximations.
|
|
437
|
+
|
|
438
|
+
Creates optimization variables and constraints for piecewise linear relationships,
|
|
439
|
+
including lambda variables, piece activation binaries, and coupling constraints.
|
|
440
|
+
|
|
441
|
+
Mathematical Formulation:
|
|
442
|
+
See <https://flixopt.github.io/flixopt/latest/user-guide/mathematical-notation/features/Piecewise/>
|
|
443
|
+
"""
|
|
444
|
+
|
|
366
445
|
def __init__(
|
|
367
446
|
self,
|
|
368
447
|
model: FlowSystemModel,
|
|
@@ -370,7 +449,7 @@ class PiecewiseModel(Submodel):
|
|
|
370
449
|
label_of_model: str,
|
|
371
450
|
piecewise_variables: dict[str, Piecewise],
|
|
372
451
|
zero_point: bool | linopy.Variable | None,
|
|
373
|
-
dims: FlowSystemDimensions | None,
|
|
452
|
+
dims: Collection[FlowSystemDimensions] | None,
|
|
374
453
|
):
|
|
375
454
|
"""
|
|
376
455
|
Modeling a Piecewise relation between miultiple variables.
|
|
@@ -394,12 +473,15 @@ class PiecewiseModel(Submodel):
|
|
|
394
473
|
super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model)
|
|
395
474
|
|
|
396
475
|
def _do_modeling(self):
|
|
476
|
+
"""Create variables, constraints, and nested submodels"""
|
|
397
477
|
super()._do_modeling()
|
|
478
|
+
|
|
398
479
|
# Validate all piecewise variables have the same number of segments
|
|
399
480
|
segment_counts = [len(pw) for pw in self._piecewise_variables.values()]
|
|
400
481
|
if not all(count == segment_counts[0] for count in segment_counts):
|
|
401
482
|
raise ValueError(f'All piecewises must have the same number of pieces, got {segment_counts}')
|
|
402
483
|
|
|
484
|
+
# Create PieceModel submodels (which creates their variables and constraints)
|
|
403
485
|
for i in range(len(list(self._piecewise_variables.values())[0])):
|
|
404
486
|
new_piece = self.add_submodels(
|
|
405
487
|
PieceModel(
|
|
@@ -438,11 +520,16 @@ class PiecewiseModel(Submodel):
|
|
|
438
520
|
coords=self._model.get_coords(self.dims),
|
|
439
521
|
binary=True,
|
|
440
522
|
short_name='zero_point',
|
|
523
|
+
category=VariableCategory.ZERO_POINT,
|
|
441
524
|
)
|
|
442
525
|
rhs = self.zero_point
|
|
443
526
|
else:
|
|
444
527
|
rhs = 1
|
|
445
528
|
|
|
529
|
+
# This constraint ensures at most one segment is active at a time.
|
|
530
|
+
# When zero_point is a binary variable, it acts as a gate:
|
|
531
|
+
# - zero_point=1: at most one segment can be active (normal piecewise operation)
|
|
532
|
+
# - zero_point=0: all segments must be inactive (effectively disables the piecewise)
|
|
446
533
|
self.add_constraints(
|
|
447
534
|
sum([piece.inside_piece for piece in self.pieces]) <= rhs,
|
|
448
535
|
name=f'{self.label_full}|{variable.name}|single_segment',
|
|
@@ -477,6 +564,10 @@ class PiecewiseEffectsModel(Submodel):
|
|
|
477
564
|
super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model)
|
|
478
565
|
|
|
479
566
|
def _do_modeling(self):
|
|
567
|
+
"""Create variables, constraints, and nested submodels"""
|
|
568
|
+
super()._do_modeling()
|
|
569
|
+
|
|
570
|
+
# Create variables
|
|
480
571
|
self.shares = {
|
|
481
572
|
effect: self.add_variables(coords=self._model.get_coords(['period', 'scenario']), short_name=effect)
|
|
482
573
|
for effect in self._piecewise_shares
|
|
@@ -490,6 +581,7 @@ class PiecewiseEffectsModel(Submodel):
|
|
|
490
581
|
},
|
|
491
582
|
}
|
|
492
583
|
|
|
584
|
+
# Create piecewise model (which creates its variables and constraints)
|
|
493
585
|
self.piecewise_model = self.add_submodels(
|
|
494
586
|
PiecewiseModel(
|
|
495
587
|
model=self._model,
|
|
@@ -502,7 +594,7 @@ class PiecewiseEffectsModel(Submodel):
|
|
|
502
594
|
short_name='PiecewiseEffects',
|
|
503
595
|
)
|
|
504
596
|
|
|
505
|
-
#
|
|
597
|
+
# Add shares to effects
|
|
506
598
|
self._model.effects.add_share_to_effects(
|
|
507
599
|
name=self.label_of_element,
|
|
508
600
|
expressions={effect: variable * 1 for effect, variable in self.shares.items()},
|
|
@@ -517,13 +609,13 @@ class ShareAllocationModel(Submodel):
|
|
|
517
609
|
dims: list[FlowSystemDimensions],
|
|
518
610
|
label_of_element: str | None = None,
|
|
519
611
|
label_of_model: str | None = None,
|
|
520
|
-
total_max:
|
|
521
|
-
total_min:
|
|
522
|
-
max_per_hour:
|
|
523
|
-
min_per_hour:
|
|
612
|
+
total_max: Numeric_PS | None = None,
|
|
613
|
+
total_min: Numeric_PS | None = None,
|
|
614
|
+
max_per_hour: Numeric_TPS | None = None,
|
|
615
|
+
min_per_hour: Numeric_TPS | None = None,
|
|
524
616
|
):
|
|
525
617
|
if 'time' not in dims and (max_per_hour is not None or min_per_hour is not None):
|
|
526
|
-
raise ValueError(
|
|
618
|
+
raise ValueError("max_per_hour and min_per_hour require 'time' dimension in dims")
|
|
527
619
|
|
|
528
620
|
self._dims = dims
|
|
529
621
|
self.total_per_timestep: linopy.Variable | None = None
|
|
@@ -535,37 +627,44 @@ class ShareAllocationModel(Submodel):
|
|
|
535
627
|
self._eq_total: linopy.Constraint | None = None
|
|
536
628
|
|
|
537
629
|
# Parameters
|
|
538
|
-
self._total_max = total_max
|
|
539
|
-
self._total_min = total_min
|
|
540
|
-
self._max_per_hour = max_per_hour
|
|
541
|
-
self._min_per_hour = min_per_hour
|
|
630
|
+
self._total_max = total_max
|
|
631
|
+
self._total_min = total_min
|
|
632
|
+
self._max_per_hour = max_per_hour
|
|
633
|
+
self._min_per_hour = min_per_hour
|
|
542
634
|
|
|
543
635
|
super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model)
|
|
544
636
|
|
|
545
637
|
def _do_modeling(self):
|
|
638
|
+
"""Create variables, constraints, and nested submodels"""
|
|
546
639
|
super()._do_modeling()
|
|
640
|
+
|
|
641
|
+
# Create variables
|
|
547
642
|
self.total = self.add_variables(
|
|
548
|
-
lower=self._total_min,
|
|
549
|
-
upper=self._total_max,
|
|
643
|
+
lower=self._total_min if self._total_min is not None else -np.inf,
|
|
644
|
+
upper=self._total_max if self._total_max is not None else np.inf,
|
|
550
645
|
coords=self._model.get_coords([dim for dim in self._dims if dim != 'time']),
|
|
551
646
|
name=self.label_full,
|
|
552
647
|
short_name='total',
|
|
648
|
+
category=VariableCategory.TOTAL,
|
|
553
649
|
)
|
|
554
650
|
# eq: sum = sum(share_i) # skalar
|
|
555
651
|
self._eq_total = self.add_constraints(self.total == 0, name=self.label_full)
|
|
556
652
|
|
|
557
653
|
if 'time' in self._dims:
|
|
558
654
|
self.total_per_timestep = self.add_variables(
|
|
559
|
-
lower=-np.inf if (self._min_per_hour is None) else self._min_per_hour * self._model.
|
|
560
|
-
upper=np.inf if (self._max_per_hour is None) else self._max_per_hour * self._model.
|
|
655
|
+
lower=-np.inf if (self._min_per_hour is None) else self._min_per_hour * self._model.timestep_duration,
|
|
656
|
+
upper=np.inf if (self._max_per_hour is None) else self._max_per_hour * self._model.timestep_duration,
|
|
561
657
|
coords=self._model.get_coords(self._dims),
|
|
562
658
|
short_name='per_timestep',
|
|
659
|
+
category=VariableCategory.PER_TIMESTEP,
|
|
563
660
|
)
|
|
564
661
|
|
|
565
662
|
self._eq_total_per_timestep = self.add_constraints(self.total_per_timestep == 0, short_name='per_timestep')
|
|
566
663
|
|
|
567
|
-
# Add it to the total
|
|
568
|
-
|
|
664
|
+
# Add it to the total (cluster_weight handles cluster representation, defaults to 1.0)
|
|
665
|
+
# Sum over all temporal dimensions (time, and cluster if present)
|
|
666
|
+
weighted_per_timestep = self.total_per_timestep * self._model.weights.get('cluster', 1.0)
|
|
667
|
+
self._eq_total.lhs -= weighted_per_timestep.sum(dim=self._model.temporal_dims)
|
|
569
668
|
|
|
570
669
|
def add_share(
|
|
571
670
|
self,
|
|
@@ -597,10 +696,13 @@ class ShareAllocationModel(Submodel):
|
|
|
597
696
|
if name in self.shares:
|
|
598
697
|
self.share_constraints[name].lhs -= expression
|
|
599
698
|
else:
|
|
699
|
+
# Temporal shares (with 'time' dim) are segment totals that need division
|
|
700
|
+
category = VariableCategory.SHARE if 'time' in dims else None
|
|
600
701
|
self.shares[name] = self.add_variables(
|
|
601
702
|
coords=self._model.get_coords(dims),
|
|
602
703
|
name=f'{name}->{self.label_full}',
|
|
603
704
|
short_name=name,
|
|
705
|
+
category=category,
|
|
604
706
|
)
|
|
605
707
|
|
|
606
708
|
self.share_constraints[name] = self.add_constraints(
|