flixopt 2.0.1__py3-none-any.whl → 2.1.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.
- docs/release-notes/v2.0.1.md +1 -1
- docs/release-notes/v2.1.0.md +31 -0
- flixopt/components.py +13 -9
- flixopt/elements.py +54 -14
- flixopt/features.py +407 -331
- flixopt/interface.py +5 -5
- flixopt/structure.py +5 -5
- {flixopt-2.0.1.dist-info → flixopt-2.1.0.dist-info}/METADATA +7 -6
- {flixopt-2.0.1.dist-info → flixopt-2.1.0.dist-info}/RECORD +12 -11
- {flixopt-2.0.1.dist-info → flixopt-2.1.0.dist-info}/WHEEL +0 -0
- {flixopt-2.0.1.dist-info → flixopt-2.1.0.dist-info}/licenses/LICENSE +0 -0
- {flixopt-2.0.1.dist-info → flixopt-2.1.0.dist-info}/top_level.txt +0 -0
flixopt/features.py
CHANGED
|
@@ -4,7 +4,7 @@ Features extend the functionality of Elements.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import logging
|
|
7
|
-
from typing import
|
|
7
|
+
from typing import Dict, List, Optional, Tuple, Union
|
|
8
8
|
|
|
9
9
|
import linopy
|
|
10
10
|
import numpy as np
|
|
@@ -12,7 +12,7 @@ import numpy as np
|
|
|
12
12
|
from . import utils
|
|
13
13
|
from .config import CONFIG
|
|
14
14
|
from .core import NumericData, Scalar, TimeSeries
|
|
15
|
-
from .interface import InvestParameters, OnOffParameters,
|
|
15
|
+
from .interface import InvestParameters, OnOffParameters, Piecewise
|
|
16
16
|
from .structure import Model, SystemModel
|
|
17
17
|
|
|
18
18
|
logger = logging.getLogger('flixopt')
|
|
@@ -90,7 +90,7 @@ class InvestmentModel(Model):
|
|
|
90
90
|
# share: divest_effects - isInvested * divest_effects
|
|
91
91
|
self._model.effects.add_share_to_effects(
|
|
92
92
|
name=self.label_of_element,
|
|
93
|
-
expressions={effect: -self.is_invested * factor + factor for effect, factor in
|
|
93
|
+
expressions={effect: -self.is_invested * factor + factor for effect, factor in self.parameters.divest_effects.items()},
|
|
94
94
|
target='invest',
|
|
95
95
|
)
|
|
96
96
|
|
|
@@ -155,7 +155,7 @@ class InvestmentModel(Model):
|
|
|
155
155
|
)
|
|
156
156
|
if self._on_variable is not None:
|
|
157
157
|
raise ValueError(
|
|
158
|
-
f'Flow {self.
|
|
158
|
+
f'Flow {self.label_full} has a fixed relative flow rate and an on_variable.'
|
|
159
159
|
f'This combination is currently not supported.'
|
|
160
160
|
)
|
|
161
161
|
return
|
|
@@ -193,53 +193,52 @@ class InvestmentModel(Model):
|
|
|
193
193
|
# anmerkung: Glg bei Spezialfall relative_minimum = 0 redundant zu OnOff ??
|
|
194
194
|
|
|
195
195
|
|
|
196
|
-
class
|
|
196
|
+
class StateModel(Model):
|
|
197
197
|
"""
|
|
198
|
-
|
|
199
|
-
If defining_bounds are given, creates sufficient lower bounds
|
|
198
|
+
Handles basic on/off binary states for defining variables
|
|
200
199
|
"""
|
|
201
200
|
|
|
202
201
|
def __init__(
|
|
203
202
|
self,
|
|
204
203
|
model: SystemModel,
|
|
205
|
-
on_off_parameters: OnOffParameters,
|
|
206
204
|
label_of_element: str,
|
|
207
205
|
defining_variables: List[linopy.Variable],
|
|
208
206
|
defining_bounds: List[Tuple[NumericData, NumericData]],
|
|
209
|
-
previous_values: List[Optional[NumericData]],
|
|
207
|
+
previous_values: List[Optional[NumericData]] = None,
|
|
208
|
+
use_off: bool = True,
|
|
209
|
+
on_hours_total_min: Optional[NumericData] = 0,
|
|
210
|
+
on_hours_total_max: Optional[NumericData] = None,
|
|
211
|
+
effects_per_running_hour: Dict[str, NumericData] = None,
|
|
210
212
|
label: Optional[str] = None,
|
|
211
213
|
):
|
|
212
214
|
"""
|
|
213
|
-
|
|
215
|
+
Models binary state variables based on a continous variable.
|
|
214
216
|
|
|
215
217
|
Args:
|
|
216
|
-
model:
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
defining_variables: List of Variables that are used to define the OnOffModel
|
|
218
|
+
model: The SystemModel that is used to create the model.
|
|
219
|
+
label_of_element: The label of the parent (Element). Used to construct the full label of the model.
|
|
220
|
+
defining_variables: List of Variables that are used to define the state
|
|
220
221
|
defining_bounds: List of Tuples, defining the absolute bounds of each defining variable
|
|
221
222
|
previous_values: List of previous values of the defining variables
|
|
223
|
+
use_off: Whether to use the off state or not
|
|
224
|
+
on_hours_total_min: min. overall sum of operating hours.
|
|
225
|
+
on_hours_total_max: max. overall sum of operating hours.
|
|
226
|
+
effects_per_running_hour: Costs per operating hours
|
|
222
227
|
label: Label of the OnOffModel
|
|
223
228
|
"""
|
|
224
229
|
super().__init__(model, label_of_element, label)
|
|
225
230
|
assert len(defining_variables) == len(defining_bounds), 'Every defining Variable needs bounds to Model OnOff'
|
|
226
|
-
self.parameters = on_off_parameters
|
|
227
231
|
self._defining_variables = defining_variables
|
|
228
|
-
|
|
229
|
-
self.
|
|
230
|
-
self.
|
|
231
|
-
|
|
232
|
-
self.
|
|
232
|
+
self._defining_bounds = defining_bounds
|
|
233
|
+
self._previous_values = previous_values or []
|
|
234
|
+
self._on_hours_total_min = on_hours_total_min if on_hours_total_min is not None else 0
|
|
235
|
+
self._on_hours_total_max = on_hours_total_max if on_hours_total_max is not None else np.inf
|
|
236
|
+
self._use_off = use_off
|
|
237
|
+
self._effects_per_running_hour = effects_per_running_hour or {}
|
|
238
|
+
|
|
239
|
+
self.on = None
|
|
233
240
|
self.total_on_hours: Optional[linopy.Variable] = None
|
|
234
|
-
|
|
235
|
-
self.consecutive_on_hours: Optional[linopy.Variable] = None
|
|
236
|
-
self.consecutive_off_hours: Optional[linopy.Variable] = None
|
|
237
|
-
|
|
238
|
-
self.off: Optional[linopy.Variable] = None
|
|
239
|
-
|
|
240
|
-
self.switch_on: Optional[linopy.Variable] = None
|
|
241
|
-
self.switch_off: Optional[linopy.Variable] = None
|
|
242
|
-
self.switch_on_nr: Optional[linopy.Variable] = None
|
|
241
|
+
self.off = None
|
|
243
242
|
|
|
244
243
|
def do_modeling(self):
|
|
245
244
|
self.on = self.add(
|
|
@@ -253,8 +252,9 @@ class OnOffModel(Model):
|
|
|
253
252
|
|
|
254
253
|
self.total_on_hours = self.add(
|
|
255
254
|
self._model.add_variables(
|
|
256
|
-
lower=self.
|
|
257
|
-
upper=self.
|
|
255
|
+
lower=self._on_hours_total_min,
|
|
256
|
+
upper=self._on_hours_total_max,
|
|
257
|
+
coords=None,
|
|
258
258
|
name=f'{self.label_full}|on_hours_total',
|
|
259
259
|
),
|
|
260
260
|
'on_hours_total',
|
|
@@ -268,9 +268,10 @@ class OnOffModel(Model):
|
|
|
268
268
|
'on_hours_total',
|
|
269
269
|
)
|
|
270
270
|
|
|
271
|
-
|
|
271
|
+
# Add defining constraints for each variable
|
|
272
|
+
self._add_defining_constraints()
|
|
272
273
|
|
|
273
|
-
if self.
|
|
274
|
+
if self._use_off:
|
|
274
275
|
self.off = self.add(
|
|
275
276
|
self._model.add_variables(
|
|
276
277
|
name=f'{self.label_full}|off',
|
|
@@ -280,68 +281,21 @@ class OnOffModel(Model):
|
|
|
280
281
|
'off',
|
|
281
282
|
)
|
|
282
283
|
|
|
283
|
-
#
|
|
284
|
+
# Constraint: on + off = 1
|
|
284
285
|
self.add(self._model.add_constraints(self.on + self.off == 1, name=f'{self.label_full}|off'), 'off')
|
|
285
286
|
|
|
286
|
-
|
|
287
|
-
self.consecutive_on_hours = self._get_duration_in_hours(
|
|
288
|
-
'consecutive_on_hours',
|
|
289
|
-
self.on,
|
|
290
|
-
self.previous_consecutive_on_hours,
|
|
291
|
-
self.parameters.consecutive_on_hours_min,
|
|
292
|
-
self.parameters.consecutive_on_hours_max,
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
if self.parameters.use_consecutive_off_hours:
|
|
296
|
-
self.consecutive_off_hours = self._get_duration_in_hours(
|
|
297
|
-
'consecutive_off_hours',
|
|
298
|
-
self.off,
|
|
299
|
-
self.previous_consecutive_off_hours,
|
|
300
|
-
self.parameters.consecutive_off_hours_min,
|
|
301
|
-
self.parameters.consecutive_off_hours_max,
|
|
302
|
-
)
|
|
303
|
-
|
|
304
|
-
if self.parameters.use_switch_on:
|
|
305
|
-
self.switch_on = self.add(
|
|
306
|
-
self._model.add_variables(binary=True, name=f'{self.label_full}|switch_on', coords=self._model.coords),
|
|
307
|
-
'switch_on',
|
|
308
|
-
)
|
|
309
|
-
|
|
310
|
-
self.switch_off = self.add(
|
|
311
|
-
self._model.add_variables(binary=True, name=f'{self.label_full}|switch_off', coords=self._model.coords),
|
|
312
|
-
'switch_off',
|
|
313
|
-
)
|
|
314
|
-
|
|
315
|
-
self.switch_on_nr = self.add(
|
|
316
|
-
self._model.add_variables(
|
|
317
|
-
upper=self.parameters.switch_on_total_max
|
|
318
|
-
if self.parameters.switch_on_total_max is not None
|
|
319
|
-
else np.inf,
|
|
320
|
-
name=f'{self.label_full}|switch_on_nr',
|
|
321
|
-
),
|
|
322
|
-
'switch_on_nr',
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
self._add_switch_constraints()
|
|
326
|
-
|
|
327
|
-
self._create_shares()
|
|
328
|
-
|
|
329
|
-
def _add_on_constraints(self):
|
|
330
|
-
assert self.on is not None, f'On variable of {self.label_full} must be defined to add constraints'
|
|
331
|
-
# % Bedingungen 1) und 2) müssen erfüllt sein:
|
|
332
|
-
|
|
333
|
-
# % Anmerkung: Falls "abschnittsweise linear" gewählt, dann ist eigentlich nur Bedingung 1) noch notwendig
|
|
334
|
-
# % (und dann auch nur wenn erstes Piece bei Q_th=0 beginnt. Dann soll bei Q_th=0 (d.h. die Maschine ist Aus) On = 0 und segment1.onSeg = 0):)
|
|
335
|
-
# % Fazit: Wenn kein Performance-Verlust durch mehr Gleichungen, dann egal!
|
|
287
|
+
return self
|
|
336
288
|
|
|
289
|
+
def _add_defining_constraints(self):
|
|
290
|
+
"""Add constraints that link defining variables to the on state"""
|
|
337
291
|
nr_of_def_vars = len(self._defining_variables)
|
|
338
|
-
assert nr_of_def_vars > 0, 'Achtung: mindestens 1 Flow notwendig'
|
|
339
292
|
|
|
340
293
|
if nr_of_def_vars == 1:
|
|
294
|
+
# Case for a single defining variable
|
|
341
295
|
def_var = self._defining_variables[0]
|
|
342
296
|
lb, ub = self._defining_bounds[0]
|
|
343
297
|
|
|
344
|
-
#
|
|
298
|
+
# Constraint: on * lower_bound <= def_var
|
|
345
299
|
self.add(
|
|
346
300
|
self._model.add_constraints(
|
|
347
301
|
self.on * np.maximum(CONFIG.modeling.EPSILON, lb) <= def_var, name=f'{self.label_full}|on_con1'
|
|
@@ -349,20 +303,16 @@ class OnOffModel(Model):
|
|
|
349
303
|
'on_con1',
|
|
350
304
|
)
|
|
351
305
|
|
|
352
|
-
#
|
|
306
|
+
# Constraint: on * upper_bound >= def_var
|
|
353
307
|
self.add(
|
|
354
|
-
self._model.add_constraints(
|
|
355
|
-
self.on * np.maximum(CONFIG.modeling.EPSILON, ub) >= def_var, name=f'{self.label_full}|on_con2'
|
|
356
|
-
),
|
|
357
|
-
'on_con2',
|
|
308
|
+
self._model.add_constraints(self.on * ub >= def_var, name=f'{self.label_full}|on_con2'), 'on_con2'
|
|
358
309
|
)
|
|
310
|
+
else:
|
|
311
|
+
# Case for multiple defining variables
|
|
312
|
+
ub = sum(bound[1] for bound in self._defining_bounds) / nr_of_def_vars
|
|
313
|
+
lb = CONFIG.modeling.EPSILON #TODO: Can this be a bigger value? (maybe the smallest bound?)
|
|
359
314
|
|
|
360
|
-
|
|
361
|
-
ub = sum(bound[1] for bound in self._defining_bounds)
|
|
362
|
-
lb = CONFIG.modeling.EPSILON
|
|
363
|
-
|
|
364
|
-
# When all defining variables are 0, On is 0
|
|
365
|
-
# eq: On(t) * Epsilon <= sum(alle Leistungen(t))
|
|
315
|
+
# Constraint: on * epsilon <= sum(all_defining_variables)
|
|
366
316
|
self.add(
|
|
367
317
|
self._model.add_constraints(
|
|
368
318
|
self.on * lb <= sum(self._defining_variables), name=f'{self.label_full}|on_con1'
|
|
@@ -370,10 +320,8 @@ class OnOffModel(Model):
|
|
|
370
320
|
'on_con1',
|
|
371
321
|
)
|
|
372
322
|
|
|
373
|
-
|
|
374
|
-
#
|
|
375
|
-
# --> damit Gleichungswerte nicht zu groß werden, noch durch nr_of_flows geteilt:
|
|
376
|
-
# eq: sum( Leistung(t,i) / nr_of_flows ) - sum(Leistung_max(i)) / nr_of_flows * On(t) <= 0
|
|
323
|
+
# Constraint to ensure all variables are zero when off.
|
|
324
|
+
# Divide by nr_of_def_vars to improve numerical stability (smaller factors)
|
|
377
325
|
self.add(
|
|
378
326
|
self._model.add_constraints(
|
|
379
327
|
self.on * ub >= sum([def_var / nr_of_def_vars for def_var in self._defining_variables]),
|
|
@@ -382,282 +330,261 @@ class OnOffModel(Model):
|
|
|
382
330
|
'on_con2',
|
|
383
331
|
)
|
|
384
332
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
f'Avoid this warning by reducing the size of {self.label_full} '
|
|
390
|
-
f'(or the maximum_size of the corresponding InvestParameters). '
|
|
391
|
-
f'If its a Component, you might need to adjust the sizes of all of its flows.'
|
|
392
|
-
)
|
|
333
|
+
@property
|
|
334
|
+
def previous_states(self) -> np.ndarray:
|
|
335
|
+
"""Computes the previous states {0, 1} of defining variables as a binary array from their previous values."""
|
|
336
|
+
return StateModel.compute_previous_states(self._previous_values, epsilon=CONFIG.modeling.EPSILON)
|
|
393
337
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
binary_variable: linopy.Variable,
|
|
398
|
-
previous_duration: Scalar,
|
|
399
|
-
minimum_duration: Optional[TimeSeries],
|
|
400
|
-
maximum_duration: Optional[TimeSeries],
|
|
401
|
-
) -> linopy.Variable:
|
|
402
|
-
"""
|
|
403
|
-
creates duration variable and adds constraints to a time-series variable to enforce duration limits based on
|
|
404
|
-
binary activity.
|
|
405
|
-
The minimum duration in the last time step is not restricted.
|
|
406
|
-
Previous values before t=0 are not recognised!
|
|
338
|
+
@property
|
|
339
|
+
def previous_on_states(self) -> np.ndarray:
|
|
340
|
+
return self.previous_states
|
|
407
341
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
minimum_duration: Minimum duration the activity must remain active once started.
|
|
412
|
-
If None, no minimum duration constraint is applied.
|
|
413
|
-
maximum_duration: Maximum duration the activity can remain active.
|
|
414
|
-
If None, the maximum duration is set to the total available time.
|
|
342
|
+
@property
|
|
343
|
+
def previous_off_states(self):
|
|
344
|
+
return 1 - self.previous_states
|
|
415
345
|
|
|
416
|
-
|
|
417
|
-
|
|
346
|
+
@staticmethod
|
|
347
|
+
def compute_previous_states(previous_values: List[NumericData], epsilon: float = 1e-5) -> np.ndarray:
|
|
348
|
+
"""Computes the previous states {0, 1} of defining variables as a binary array from their previous values."""
|
|
349
|
+
if not previous_values or all([val is None for val in previous_values]):
|
|
350
|
+
return np.array([0])
|
|
418
351
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
352
|
+
# Convert to 2D-array and compute binary on/off states
|
|
353
|
+
previous_values = np.array([values for values in previous_values if values is not None]) # Filter out None
|
|
354
|
+
if previous_values.ndim > 1:
|
|
355
|
+
return np.any(~np.isclose(previous_values, 0, atol=epsilon), axis=0).astype(int)
|
|
422
356
|
|
|
423
|
-
|
|
424
|
-
can be enforced to constrain how long the activity remains active.
|
|
357
|
+
return (~np.isclose(previous_values, 0, atol=epsilon)).astype(int)
|
|
425
358
|
|
|
426
|
-
Notes:
|
|
427
|
-
- To count consecutive zeros instead of ones, use a transformed binary variable
|
|
428
|
-
(e.g., `1 - binary_variable`).
|
|
429
|
-
- Constraints ensure the duration variable properly resets or increments based on activity.
|
|
430
359
|
|
|
431
|
-
|
|
432
|
-
|
|
360
|
+
class SwitchStateModel(Model):
|
|
361
|
+
"""
|
|
362
|
+
Handles switch on/off transitions
|
|
363
|
+
"""
|
|
433
364
|
|
|
434
|
-
|
|
435
|
-
|
|
365
|
+
def __init__(
|
|
366
|
+
self,
|
|
367
|
+
model: SystemModel,
|
|
368
|
+
label_of_element: str,
|
|
369
|
+
state_variable: linopy.Variable,
|
|
370
|
+
previous_state=0,
|
|
371
|
+
switch_on_max: Optional[Scalar] = None,
|
|
372
|
+
label: Optional[str] = None,
|
|
373
|
+
):
|
|
374
|
+
super().__init__(model, label_of_element, label)
|
|
375
|
+
self._state_variable = state_variable
|
|
376
|
+
self.previous_state = previous_state
|
|
377
|
+
self._switch_on_max = switch_on_max if switch_on_max is not None else np.inf
|
|
436
378
|
|
|
437
|
-
|
|
379
|
+
self.switch_on = None
|
|
380
|
+
self.switch_off = None
|
|
381
|
+
self.switch_on_nr = None
|
|
438
382
|
|
|
439
|
-
|
|
440
|
-
|
|
383
|
+
def do_modeling(self):
|
|
384
|
+
"""Create switch variables and constraints"""
|
|
441
385
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
386
|
+
# Create switch variables
|
|
387
|
+
self.switch_on = self.add(
|
|
388
|
+
self._model.add_variables(binary=True, name=f'{self.label_full}|switch_on', coords=self._model.coords),
|
|
389
|
+
'switch_on',
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
self.switch_off = self.add(
|
|
393
|
+
self._model.add_variables(binary=True, name=f'{self.label_full}|switch_off', coords=self._model.coords),
|
|
394
|
+
'switch_off',
|
|
395
|
+
)
|
|
449
396
|
|
|
450
|
-
|
|
397
|
+
# Create count variable for number of switches
|
|
398
|
+
self.switch_on_nr = self.add(
|
|
451
399
|
self._model.add_variables(
|
|
400
|
+
upper=self._switch_on_max,
|
|
452
401
|
lower=0,
|
|
453
|
-
|
|
454
|
-
coords=self._model.coords,
|
|
455
|
-
name=f'{self.label_full}|{variable_name}',
|
|
402
|
+
name=f'{self.label_full}|switch_on_nr',
|
|
456
403
|
),
|
|
457
|
-
|
|
404
|
+
'switch_on_nr',
|
|
458
405
|
)
|
|
459
406
|
|
|
460
|
-
#
|
|
407
|
+
# Add switch constraints for all entries after the first timestep
|
|
461
408
|
self.add(
|
|
462
409
|
self._model.add_constraints(
|
|
463
|
-
|
|
410
|
+
self.switch_on.isel(time=slice(1, None)) - self.switch_off.isel(time=slice(1, None))
|
|
411
|
+
== self._state_variable.isel(time=slice(1, None)) - self._state_variable.isel(time=slice(None, -1)),
|
|
412
|
+
name=f'{self.label_full}|switch_con',
|
|
464
413
|
),
|
|
465
|
-
|
|
414
|
+
'switch_con',
|
|
466
415
|
)
|
|
467
416
|
|
|
468
|
-
#
|
|
469
|
-
# on(t)=1 -> duration(t) - duration(t-1) <= dt(t)
|
|
470
|
-
# on(t)=0 -> duration(t-1) >= negat. value
|
|
417
|
+
# Initial switch constraint
|
|
471
418
|
self.add(
|
|
472
419
|
self._model.add_constraints(
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
name=f'{self.label_full}|
|
|
420
|
+
self.switch_on.isel(time=0) - self.switch_off.isel(time=0)
|
|
421
|
+
== self._state_variable.isel(time=0) - self.previous_state,
|
|
422
|
+
name=f'{self.label_full}|initial_switch_con',
|
|
476
423
|
),
|
|
477
|
-
|
|
424
|
+
'initial_switch_con',
|
|
478
425
|
)
|
|
479
426
|
|
|
480
|
-
#
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
427
|
+
# Mutual exclusivity constraint
|
|
428
|
+
self.add(
|
|
429
|
+
self._model.add_constraints(self.switch_on + self.switch_off <= 1.1, name=f'{self.label_full}|switch_on_or_off'),
|
|
430
|
+
'switch_on_or_off',
|
|
431
|
+
)
|
|
485
432
|
|
|
433
|
+
# Total switch-on count constraint
|
|
486
434
|
self.add(
|
|
487
435
|
self._model.add_constraints(
|
|
488
|
-
|
|
489
|
-
>= duration_in_hours.isel(time=slice(None, -1))
|
|
490
|
-
+ self._model.hours_per_step.isel(time=slice(None, -1))
|
|
491
|
-
+ (binary_variable.isel(time=slice(1, None)) - 1) * mega,
|
|
492
|
-
name=f'{self.label_full}|{variable_name}_con2b',
|
|
436
|
+
self.switch_on_nr == self.switch_on.sum('time'), name=f'{self.label_full}|switch_on_nr'
|
|
493
437
|
),
|
|
494
|
-
|
|
438
|
+
'switch_on_nr',
|
|
495
439
|
)
|
|
496
440
|
|
|
497
|
-
|
|
441
|
+
return self
|
|
498
442
|
|
|
499
|
-
if minimum_duration is not None:
|
|
500
|
-
# Note: switchOff-step is when: On(t) - On(t+1) == 1
|
|
501
|
-
# Note: (last on-time period (with last timestep of period t=n) is not checked and can be shorter)
|
|
502
|
-
# Note: (previous values before t=1 are not recognised!)
|
|
503
|
-
# eq: duration(t) >= minimum_duration(t) * [On(t) - On(t+1)] for t=1..(n-1)
|
|
504
|
-
# eq: -duration(t) + minimum_duration(t) * On(t) - minimum_duration(t) * On(t+1) <= 0
|
|
505
|
-
self.add(
|
|
506
|
-
self._model.add_constraints(
|
|
507
|
-
duration_in_hours
|
|
508
|
-
>= (binary_variable.isel(time=slice(None, -1)) - binary_variable.isel(time=slice(1, None)))
|
|
509
|
-
* minimum_duration.isel(time=slice(None, -1)),
|
|
510
|
-
name=f'{self.label_full}|{variable_name}_minimum_duration',
|
|
511
|
-
),
|
|
512
|
-
f'{variable_name}_minimum_duration',
|
|
513
|
-
)
|
|
514
443
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
# eq: On(t=0) = 1
|
|
520
|
-
self.add(
|
|
521
|
-
self._model.add_constraints(
|
|
522
|
-
binary_variable.isel(time=0) == 1, name=f'{self.label_full}|{variable_name}_minimum_inital'
|
|
523
|
-
),
|
|
524
|
-
f'{variable_name}_minimum_inital',
|
|
525
|
-
)
|
|
444
|
+
class ConsecutiveStateModel(Model):
|
|
445
|
+
"""
|
|
446
|
+
Handles tracking consecutive durations in a state
|
|
447
|
+
"""
|
|
526
448
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
449
|
+
def __init__(
|
|
450
|
+
self,
|
|
451
|
+
model: SystemModel,
|
|
452
|
+
label_of_element: str,
|
|
453
|
+
state_variable: linopy.Variable,
|
|
454
|
+
minimum_duration: Optional[NumericData] = None,
|
|
455
|
+
maximum_duration: Optional[NumericData] = None,
|
|
456
|
+
previous_states: Optional[NumericData] = None,
|
|
457
|
+
label: Optional[str] = None,
|
|
458
|
+
):
|
|
459
|
+
"""
|
|
460
|
+
Model and constraint the consecutive duration of a state variable.
|
|
537
461
|
|
|
538
|
-
|
|
462
|
+
Args:
|
|
463
|
+
model: The SystemModel that is used to create the model.
|
|
464
|
+
label_of_element: The label of the parent (Element). Used to construct the full label of the model.
|
|
465
|
+
state_variable: The state variable that is used to model the duration. state = {0, 1}
|
|
466
|
+
minimum_duration: The minimum duration of the state variable.
|
|
467
|
+
maximum_duration: The maximum duration of the state variable.
|
|
468
|
+
previous_states: The previous states of the state variable.
|
|
469
|
+
label: The label of the model. Used to construct the full label of the model.
|
|
470
|
+
"""
|
|
471
|
+
super().__init__(model, label_of_element, label)
|
|
472
|
+
self._state_variable = state_variable
|
|
473
|
+
self._previous_states = previous_states
|
|
474
|
+
self._minimum_duration = minimum_duration
|
|
475
|
+
self._maximum_duration = maximum_duration
|
|
539
476
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
#
|
|
550
|
-
|
|
551
|
-
self.
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
477
|
+
if isinstance(self._minimum_duration, TimeSeries):
|
|
478
|
+
self._minimum_duration = self._minimum_duration.active_data
|
|
479
|
+
if isinstance(self._maximum_duration, TimeSeries):
|
|
480
|
+
self._maximum_duration = self._maximum_duration.active_data
|
|
481
|
+
|
|
482
|
+
self.duration = None
|
|
483
|
+
|
|
484
|
+
def do_modeling(self):
|
|
485
|
+
"""Create consecutive duration variables and constraints"""
|
|
486
|
+
# Get the hours per step
|
|
487
|
+
hours_per_step = self._model.hours_per_step
|
|
488
|
+
mega = hours_per_step.sum('time') + self.previous_duration
|
|
489
|
+
|
|
490
|
+
# Create the duration variable
|
|
491
|
+
self.duration = self.add(
|
|
492
|
+
self._model.add_variables(
|
|
493
|
+
lower=0,
|
|
494
|
+
upper=self._maximum_duration if self._maximum_duration is not None else mega,
|
|
495
|
+
coords=self._model.coords,
|
|
496
|
+
name=f'{self.label_full}|hours',
|
|
556
497
|
),
|
|
557
|
-
'
|
|
498
|
+
'hours',
|
|
558
499
|
)
|
|
559
|
-
|
|
560
|
-
#
|
|
500
|
+
|
|
501
|
+
# Add constraints
|
|
502
|
+
|
|
503
|
+
# Upper bound constraint
|
|
561
504
|
self.add(
|
|
562
505
|
self._model.add_constraints(
|
|
563
|
-
self.
|
|
564
|
-
== self.on.isel(time=0) - self.previous_on_values[-1],
|
|
565
|
-
name=f'{self.label_full}|initial_switch_con',
|
|
506
|
+
self.duration <= self._state_variable * mega, name=f'{self.label_full}|con1'
|
|
566
507
|
),
|
|
567
|
-
'
|
|
508
|
+
'con1',
|
|
568
509
|
)
|
|
569
|
-
|
|
570
|
-
#
|
|
510
|
+
|
|
511
|
+
# Forward constraint
|
|
571
512
|
self.add(
|
|
572
513
|
self._model.add_constraints(
|
|
573
|
-
self.
|
|
514
|
+
self.duration.isel(time=slice(1, None))
|
|
515
|
+
<= self.duration.isel(time=slice(None, -1)) + hours_per_step.isel(time=slice(None, -1)),
|
|
516
|
+
name=f'{self.label_full}|con2a',
|
|
574
517
|
),
|
|
575
|
-
'
|
|
518
|
+
'con2a',
|
|
576
519
|
)
|
|
577
520
|
|
|
578
|
-
|
|
579
|
-
# eq: nrSwitchOn = sum(SwitchOn(t))
|
|
521
|
+
# Backward constraint
|
|
580
522
|
self.add(
|
|
581
523
|
self._model.add_constraints(
|
|
582
|
-
self.
|
|
524
|
+
self.duration.isel(time=slice(1, None))
|
|
525
|
+
>= self.duration.isel(time=slice(None, -1))
|
|
526
|
+
+ hours_per_step.isel(time=slice(None, -1))
|
|
527
|
+
+ (self._state_variable.isel(time=slice(1, None)) - 1) * mega,
|
|
528
|
+
name=f'{self.label_full}|con2b',
|
|
583
529
|
),
|
|
584
|
-
'
|
|
530
|
+
'con2b',
|
|
585
531
|
)
|
|
586
532
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
if effects_per_running_hour != {}:
|
|
600
|
-
self._model.effects.add_share_to_effects(
|
|
601
|
-
name=self.label_of_element,
|
|
602
|
-
expressions={
|
|
603
|
-
effect: self.on * factor * self._model.hours_per_step
|
|
604
|
-
for effect, factor in effects_per_running_hour.items()
|
|
605
|
-
},
|
|
606
|
-
target='operation',
|
|
533
|
+
# Add minimum duration constraints if specified
|
|
534
|
+
if self._minimum_duration is not None:
|
|
535
|
+
self.add(
|
|
536
|
+
self._model.add_constraints(
|
|
537
|
+
self.duration
|
|
538
|
+
>= (
|
|
539
|
+
self._state_variable.isel(time=slice(None, -1)) - self._state_variable.isel(time=slice(1, None))
|
|
540
|
+
)
|
|
541
|
+
* self._minimum_duration.isel(time=slice(None, -1)),
|
|
542
|
+
name=f'{self.label_full}|minimum',
|
|
543
|
+
),
|
|
544
|
+
'minimum',
|
|
607
545
|
)
|
|
608
546
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
547
|
+
# Handle initial condition
|
|
548
|
+
if 0 < self.previous_duration < self._minimum_duration.isel(time=0):
|
|
549
|
+
self.add(
|
|
550
|
+
self._model.add_constraints(
|
|
551
|
+
self._state_variable.isel(time=0) == 1, name=f'{self.label_full}|initial_minimum'
|
|
552
|
+
),
|
|
553
|
+
'initial_minimum',
|
|
554
|
+
)
|
|
612
555
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
556
|
+
# Set initial value
|
|
557
|
+
self.add(
|
|
558
|
+
self._model.add_constraints(
|
|
559
|
+
self.duration.isel(time=0) ==
|
|
560
|
+
(hours_per_step.isel(time=0) + self.previous_duration) * self._state_variable.isel(time=0),
|
|
561
|
+
name=f'{self.label_full}|initial',
|
|
562
|
+
),
|
|
563
|
+
'initial',
|
|
564
|
+
)
|
|
616
565
|
|
|
617
|
-
|
|
618
|
-
def previous_consecutive_on_hours(self) -> Scalar:
|
|
619
|
-
return self.compute_consecutive_duration(self.previous_on_values, self._model.hours_per_step)
|
|
566
|
+
return self
|
|
620
567
|
|
|
621
568
|
@property
|
|
622
|
-
def
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
Computes the previous 'on' states {0, 1} of defining variables as a binary array from their previous values.
|
|
629
|
-
|
|
630
|
-
Args:
|
|
631
|
-
previous_values: List of previous values of the defining variables. In Range [0, inf] or None (ignored)
|
|
632
|
-
epsilon: Tolerance for equality to determine "off" state, default is 1e-5.
|
|
633
|
-
|
|
634
|
-
Returns:
|
|
635
|
-
A binary array (0 and 1) indicating the previous on/off states of the variables.
|
|
636
|
-
Returns `array([0])` if no previous values are available.
|
|
637
|
-
"""
|
|
638
|
-
|
|
639
|
-
if not previous_values or all([val is None for val in previous_values]):
|
|
640
|
-
return np.array([0])
|
|
641
|
-
else: # Convert to 2D-array and compute binary on/off states
|
|
642
|
-
previous_values = np.array([values for values in previous_values if values is not None]) # Filter out None
|
|
643
|
-
if previous_values.ndim > 1:
|
|
644
|
-
return np.any(~np.isclose(previous_values, 0, atol=epsilon), axis=0).astype(int)
|
|
645
|
-
else:
|
|
646
|
-
return (~np.isclose(previous_values, 0, atol=epsilon)).astype(int)
|
|
569
|
+
def previous_duration(self) -> Scalar:
|
|
570
|
+
"""Computes the previous duration of the state variable"""
|
|
571
|
+
#TODO: Allow for other/dynamic timestep resolutions
|
|
572
|
+
return ConsecutiveStateModel.compute_consecutive_hours_in_state(
|
|
573
|
+
self._previous_states, self._model.hours_per_step.isel(time=0).item()
|
|
574
|
+
)
|
|
647
575
|
|
|
648
576
|
@staticmethod
|
|
649
|
-
def
|
|
577
|
+
def compute_consecutive_hours_in_state(
|
|
650
578
|
binary_values: NumericData, hours_per_timestep: Union[int, float, np.ndarray]
|
|
651
579
|
) -> Scalar:
|
|
652
580
|
"""
|
|
653
|
-
Computes the final consecutive duration in
|
|
654
|
-
|
|
655
|
-
hours_per_timestep is handled in a way, that maximizes compatability.
|
|
656
|
-
Its length must only be as long as the last consecutive duration in binary_values.
|
|
581
|
+
Computes the final consecutive duration in state 'on' (=1) in hours, from a binary array.
|
|
657
582
|
|
|
658
583
|
Args:
|
|
659
584
|
binary_values: An int or 1D binary array containing only `0`s and `1`s.
|
|
660
585
|
hours_per_timestep: The duration of each timestep in hours.
|
|
586
|
+
If a scalar is provided, it is used for all timesteps.
|
|
587
|
+
If an array is provided, it must be as long as the last consecutive duration in binary_values.
|
|
661
588
|
|
|
662
589
|
Returns:
|
|
663
590
|
The duration of the binary variable in hours.
|
|
@@ -672,28 +599,177 @@ class OnOffModel(Model):
|
|
|
672
599
|
elif np.isscalar(binary_values) and not np.isscalar(hours_per_timestep):
|
|
673
600
|
return binary_values * hours_per_timestep[-1]
|
|
674
601
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
length_of_last_duration = zero_indices[-1] + 1 if zero_indices.size > 0 else len(binary_values)
|
|
678
|
-
|
|
679
|
-
if not np.isscalar(binary_values) and np.isscalar(hours_per_timestep):
|
|
680
|
-
return np.sum(binary_values[-length_of_last_duration:] * hours_per_timestep)
|
|
602
|
+
if np.isclose(binary_values[-1], 0, atol=CONFIG.modeling.EPSILON):
|
|
603
|
+
return 0
|
|
681
604
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
f'When trying to calculate the consecutive duration, the length of the last duration '
|
|
686
|
-
f'({len(length_of_last_duration)}) is longer than the hours_per_timestep ({len(hours_per_timestep)}), '
|
|
687
|
-
f'as {binary_values=}'
|
|
688
|
-
)
|
|
689
|
-
return np.sum(binary_values[-length_of_last_duration:] * hours_per_timestep[-length_of_last_duration:])
|
|
605
|
+
if np.isscalar(hours_per_timestep):
|
|
606
|
+
hours_per_timestep = np.ones(len(binary_values)) * hours_per_timestep
|
|
607
|
+
hours_per_timestep: np.ndarray
|
|
690
608
|
|
|
609
|
+
indexes_with_zero_values = np.where(np.isclose(binary_values, 0, atol=CONFIG.modeling.EPSILON))[0]
|
|
610
|
+
if len(indexes_with_zero_values) == 0:
|
|
611
|
+
nr_of_indexes_with_consecutive_ones = len(binary_values)
|
|
691
612
|
else:
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
613
|
+
nr_of_indexes_with_consecutive_ones = len(binary_values) - indexes_with_zero_values[-1] - 1
|
|
614
|
+
|
|
615
|
+
if len(hours_per_timestep) < nr_of_indexes_with_consecutive_ones:
|
|
616
|
+
raise ValueError(
|
|
617
|
+
f'When trying to calculate the consecutive duration, the length of the last duration '
|
|
618
|
+
f'({len(nr_of_indexes_with_consecutive_ones)}) is longer than the provided hours_per_timestep ({len(hours_per_timestep)}), '
|
|
619
|
+
f'as {binary_values=}'
|
|
695
620
|
)
|
|
696
621
|
|
|
622
|
+
return np.sum(binary_values[-nr_of_indexes_with_consecutive_ones:] * hours_per_timestep[-nr_of_indexes_with_consecutive_ones:])
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
class OnOffModel(Model):
|
|
626
|
+
"""
|
|
627
|
+
Class for modeling the on and off state of a variable
|
|
628
|
+
Uses component models to create a modular implementation
|
|
629
|
+
"""
|
|
630
|
+
|
|
631
|
+
def __init__(
|
|
632
|
+
self,
|
|
633
|
+
model: SystemModel,
|
|
634
|
+
on_off_parameters: OnOffParameters,
|
|
635
|
+
label_of_element: str,
|
|
636
|
+
defining_variables: List[linopy.Variable],
|
|
637
|
+
defining_bounds: List[Tuple[NumericData, NumericData]],
|
|
638
|
+
previous_values: List[Optional[NumericData]],
|
|
639
|
+
label: Optional[str] = None,
|
|
640
|
+
):
|
|
641
|
+
"""
|
|
642
|
+
Constructor for OnOffModel
|
|
643
|
+
|
|
644
|
+
Args:
|
|
645
|
+
model: Reference to the SystemModel
|
|
646
|
+
on_off_parameters: Parameters for the OnOffModel
|
|
647
|
+
label_of_element: Label of the Parent
|
|
648
|
+
defining_variables: List of Variables that are used to define the OnOffModel
|
|
649
|
+
defining_bounds: List of Tuples, defining the absolute bounds of each defining variable
|
|
650
|
+
previous_values: List of previous values of the defining variables
|
|
651
|
+
label: Label of the OnOffModel
|
|
652
|
+
"""
|
|
653
|
+
super().__init__(model, label_of_element, label)
|
|
654
|
+
self.parameters = on_off_parameters
|
|
655
|
+
self._defining_variables = defining_variables
|
|
656
|
+
self._defining_bounds = defining_bounds
|
|
657
|
+
self._previous_values = previous_values
|
|
658
|
+
|
|
659
|
+
self.state_model = None
|
|
660
|
+
self.switch_state_model = None
|
|
661
|
+
self.consecutive_on_model = None
|
|
662
|
+
self.consecutive_off_model = None
|
|
663
|
+
|
|
664
|
+
def do_modeling(self):
|
|
665
|
+
"""Create all variables and constraints for the OnOffModel"""
|
|
666
|
+
|
|
667
|
+
# Create binary state component
|
|
668
|
+
self.state_model = StateModel(
|
|
669
|
+
model=self._model,
|
|
670
|
+
label_of_element=self.label_of_element,
|
|
671
|
+
defining_variables=self._defining_variables,
|
|
672
|
+
defining_bounds=self._defining_bounds,
|
|
673
|
+
previous_values=self._previous_values,
|
|
674
|
+
use_off=self.parameters.use_off,
|
|
675
|
+
on_hours_total_min=self.parameters.on_hours_total_min,
|
|
676
|
+
on_hours_total_max=self.parameters.on_hours_total_max,
|
|
677
|
+
effects_per_running_hour=self.parameters.effects_per_running_hour,
|
|
678
|
+
)
|
|
679
|
+
self.add(self.state_model)
|
|
680
|
+
self.state_model.do_modeling()
|
|
681
|
+
|
|
682
|
+
# Create switch component if needed
|
|
683
|
+
if self.parameters.use_switch_on:
|
|
684
|
+
self.switch_state_model = SwitchStateModel(
|
|
685
|
+
model=self._model,
|
|
686
|
+
label_of_element=self.label_of_element,
|
|
687
|
+
state_variable=self.state_model.on,
|
|
688
|
+
previous_state=self.state_model.previous_on_states[-1],
|
|
689
|
+
switch_on_max=self.parameters.switch_on_total_max,
|
|
690
|
+
)
|
|
691
|
+
self.add(self.switch_state_model)
|
|
692
|
+
self.switch_state_model.do_modeling()
|
|
693
|
+
|
|
694
|
+
# Create consecutive on hours component if needed
|
|
695
|
+
if self.parameters.use_consecutive_on_hours:
|
|
696
|
+
self.consecutive_on_model = ConsecutiveStateModel(
|
|
697
|
+
model=self._model,
|
|
698
|
+
label_of_element=self.label_of_element,
|
|
699
|
+
state_variable=self.state_model.on,
|
|
700
|
+
minimum_duration=self.parameters.consecutive_on_hours_min,
|
|
701
|
+
maximum_duration=self.parameters.consecutive_on_hours_max,
|
|
702
|
+
previous_states=self.state_model.previous_on_states,
|
|
703
|
+
label='ConsecutiveOn',
|
|
704
|
+
)
|
|
705
|
+
self.add(self.consecutive_on_model)
|
|
706
|
+
self.consecutive_on_model.do_modeling()
|
|
707
|
+
|
|
708
|
+
# Create consecutive off hours component if needed
|
|
709
|
+
if self.parameters.use_consecutive_off_hours:
|
|
710
|
+
self.consecutive_off_model = ConsecutiveStateModel(
|
|
711
|
+
model=self._model,
|
|
712
|
+
label_of_element=self.label_of_element,
|
|
713
|
+
state_variable=self.state_model.off,
|
|
714
|
+
minimum_duration=self.parameters.consecutive_off_hours_min,
|
|
715
|
+
maximum_duration=self.parameters.consecutive_off_hours_max,
|
|
716
|
+
previous_states=self.state_model.previous_off_states,
|
|
717
|
+
label='ConsecutiveOff',
|
|
718
|
+
)
|
|
719
|
+
self.add(self.consecutive_off_model)
|
|
720
|
+
self.consecutive_off_model.do_modeling()
|
|
721
|
+
|
|
722
|
+
self._create_shares()
|
|
723
|
+
|
|
724
|
+
def _create_shares(self):
|
|
725
|
+
if self.parameters.effects_per_running_hour:
|
|
726
|
+
self._model.effects.add_share_to_effects(
|
|
727
|
+
name=self.label_of_element,
|
|
728
|
+
expressions={
|
|
729
|
+
effect: self.state_model.on * factor * self._model.hours_per_step
|
|
730
|
+
for effect, factor in self.parameters.effects_per_running_hour.items()
|
|
731
|
+
},
|
|
732
|
+
target='operation',
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
if self.parameters.effects_per_switch_on:
|
|
736
|
+
self._model.effects.add_share_to_effects(
|
|
737
|
+
name=self.label_of_element,
|
|
738
|
+
expressions={
|
|
739
|
+
effect: self.switch_state_model.switch_on * factor
|
|
740
|
+
for effect, factor in self.parameters.effects_per_switch_on.items()
|
|
741
|
+
},
|
|
742
|
+
target='operation',
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
@property
|
|
746
|
+
def on(self):
|
|
747
|
+
return self.state_model.on
|
|
748
|
+
|
|
749
|
+
@property
|
|
750
|
+
def off(self):
|
|
751
|
+
return self.state_model.off
|
|
752
|
+
|
|
753
|
+
@property
|
|
754
|
+
def switch_on(self):
|
|
755
|
+
return self.switch_state_model.switch_on
|
|
756
|
+
|
|
757
|
+
@property
|
|
758
|
+
def switch_off(self):
|
|
759
|
+
return self.switch_state_model.switch_off
|
|
760
|
+
|
|
761
|
+
@property
|
|
762
|
+
def switch_on_nr(self):
|
|
763
|
+
return self.switch_state_model.switch_on_nr
|
|
764
|
+
|
|
765
|
+
@property
|
|
766
|
+
def consecutive_on_hours(self):
|
|
767
|
+
return self.consecutive_on_model.duration
|
|
768
|
+
|
|
769
|
+
@property
|
|
770
|
+
def consecutive_off_hours(self):
|
|
771
|
+
return self.consecutive_off_model.duration
|
|
772
|
+
|
|
697
773
|
|
|
698
774
|
class PieceModel(Model):
|
|
699
775
|
"""Class for modeling a linear piece of one or more variables in parallel"""
|
|
@@ -755,10 +831,10 @@ class PiecewiseModel(Model):
|
|
|
755
831
|
self,
|
|
756
832
|
model: SystemModel,
|
|
757
833
|
label_of_element: str,
|
|
758
|
-
label: str,
|
|
759
834
|
piecewise_variables: Dict[str, Piecewise],
|
|
760
835
|
zero_point: Optional[Union[bool, linopy.Variable]],
|
|
761
836
|
as_time_series: bool,
|
|
837
|
+
label: str = '',
|
|
762
838
|
):
|
|
763
839
|
"""
|
|
764
840
|
Modeling a Piecewise relation between miultiple variables.
|
|
@@ -807,9 +883,9 @@ class PiecewiseModel(Model):
|
|
|
807
883
|
)
|
|
808
884
|
]
|
|
809
885
|
),
|
|
810
|
-
name=f'{self.label_full}|{var_name}
|
|
886
|
+
name=f'{self.label_full}|{var_name}|lambda',
|
|
811
887
|
),
|
|
812
|
-
f'{var_name}
|
|
888
|
+
f'{var_name}|lambda',
|
|
813
889
|
)
|
|
814
890
|
|
|
815
891
|
# a) eq: Segment1.onSeg(t) + Segment2.onSeg(t) + ... = 1 Aufenthalt nur in Segmenten erlaubt
|
|
@@ -831,9 +907,9 @@ class PiecewiseModel(Model):
|
|
|
831
907
|
self.add(
|
|
832
908
|
self._model.add_constraints(
|
|
833
909
|
sum([piece.inside_piece for piece in self.pieces]) <= rhs,
|
|
834
|
-
name=f'{self.label_full}|{variable.name}
|
|
910
|
+
name=f'{self.label_full}|{variable.name}|single_segment',
|
|
835
911
|
),
|
|
836
|
-
'single_segment',
|
|
912
|
+
f'{var_name}|single_segment',
|
|
837
913
|
)
|
|
838
914
|
|
|
839
915
|
|
|
@@ -982,10 +1058,10 @@ class PiecewiseEffectsModel(Model):
|
|
|
982
1058
|
PiecewiseModel(
|
|
983
1059
|
model=self._model,
|
|
984
1060
|
label_of_element=self.label_of_element,
|
|
985
|
-
label=f'{self.label_full}|PiecewiseModel',
|
|
986
1061
|
piecewise_variables=piecewise_variables,
|
|
987
1062
|
zero_point=self._zero_point,
|
|
988
1063
|
as_time_series=False,
|
|
1064
|
+
label='PiecewiseEffects',
|
|
989
1065
|
)
|
|
990
1066
|
)
|
|
991
1067
|
|