flixopt 2.2.0b0__py3-none-any.whl → 2.2.0rc2__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/examples/00-Minimal Example.md +1 -1
- docs/examples/01-Basic Example.md +1 -1
- docs/examples/02-Complex Example.md +1 -1
- docs/examples/index.md +1 -1
- docs/faq/contribute.md +26 -14
- docs/faq/index.md +1 -1
- docs/javascripts/mathjax.js +1 -1
- docs/user-guide/Mathematical Notation/Bus.md +1 -1
- docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +13 -13
- docs/user-guide/Mathematical Notation/Flow.md +1 -1
- docs/user-guide/Mathematical Notation/LinearConverter.md +2 -2
- docs/user-guide/Mathematical Notation/Piecewise.md +1 -1
- docs/user-guide/Mathematical Notation/Storage.md +1 -1
- docs/user-guide/Mathematical Notation/index.md +1 -1
- docs/user-guide/Mathematical Notation/others.md +1 -1
- docs/user-guide/index.md +2 -2
- flixopt/__init__.py +5 -0
- flixopt/aggregation.py +0 -1
- flixopt/calculation.py +40 -72
- flixopt/commons.py +10 -1
- flixopt/components.py +326 -154
- flixopt/core.py +459 -966
- flixopt/effects.py +67 -270
- flixopt/elements.py +76 -84
- flixopt/features.py +172 -154
- flixopt/flow_system.py +70 -99
- flixopt/interface.py +315 -147
- flixopt/io.py +27 -56
- flixopt/linear_converters.py +3 -3
- flixopt/network_app.py +755 -0
- flixopt/plotting.py +16 -34
- flixopt/results.py +108 -806
- flixopt/structure.py +11 -67
- flixopt/utils.py +9 -6
- {flixopt-2.2.0b0.dist-info → flixopt-2.2.0rc2.dist-info}/METADATA +63 -42
- flixopt-2.2.0rc2.dist-info/RECORD +54 -0
- {flixopt-2.2.0b0.dist-info → flixopt-2.2.0rc2.dist-info}/WHEEL +1 -1
- scripts/extract_release_notes.py +45 -0
- docs/release-notes/_template.txt +0 -32
- docs/release-notes/index.md +0 -7
- docs/release-notes/v2.0.0.md +0 -93
- docs/release-notes/v2.0.1.md +0 -12
- docs/release-notes/v2.1.0.md +0 -31
- docs/release-notes/v2.2.0.md +0 -55
- docs/user-guide/Mathematical Notation/Investment.md +0 -115
- flixopt-2.2.0b0.dist-info/RECORD +0 -59
- {flixopt-2.2.0b0.dist-info → flixopt-2.2.0rc2.dist-info}/licenses/LICENSE +0 -0
- {flixopt-2.2.0b0.dist-info → flixopt-2.2.0rc2.dist-info}/top_level.txt +0 -0
flixopt/features.py
CHANGED
|
@@ -9,9 +9,10 @@ from typing import Dict, List, Optional, Tuple, Union
|
|
|
9
9
|
import linopy
|
|
10
10
|
import numpy as np
|
|
11
11
|
|
|
12
|
+
from . import utils
|
|
12
13
|
from .config import CONFIG
|
|
13
|
-
from .core import
|
|
14
|
-
from .interface import InvestParameters, OnOffParameters, Piecewise
|
|
14
|
+
from .core import NumericData, Scalar, TimeSeries
|
|
15
|
+
from .interface import InvestParameters, OnOffParameters, Piece, Piecewise
|
|
15
16
|
from .structure import Model, SystemModel
|
|
16
17
|
|
|
17
18
|
logger = logging.getLogger('flixopt')
|
|
@@ -26,14 +27,13 @@ class InvestmentModel(Model):
|
|
|
26
27
|
label_of_element: str,
|
|
27
28
|
parameters: InvestParameters,
|
|
28
29
|
defining_variable: [linopy.Variable],
|
|
29
|
-
relative_bounds_of_defining_variable: Tuple[
|
|
30
|
+
relative_bounds_of_defining_variable: Tuple[NumericData, NumericData],
|
|
30
31
|
label: Optional[str] = None,
|
|
31
32
|
on_variable: Optional[linopy.Variable] = None,
|
|
32
33
|
):
|
|
33
34
|
super().__init__(model, label_of_element, label)
|
|
34
35
|
self.size: Optional[Union[Scalar, linopy.Variable]] = None
|
|
35
36
|
self.is_invested: Optional[linopy.Variable] = None
|
|
36
|
-
self.scenario_of_investment: Optional[linopy.Variable] = None
|
|
37
37
|
|
|
38
38
|
self.piecewise_effects: Optional[PiecewiseEffectsModel] = None
|
|
39
39
|
|
|
@@ -43,32 +43,31 @@ class InvestmentModel(Model):
|
|
|
43
43
|
self.parameters = parameters
|
|
44
44
|
|
|
45
45
|
def do_modeling(self):
|
|
46
|
-
self.
|
|
47
|
-
self.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
if self.parameters.fixed_size and not self.parameters.optional:
|
|
47
|
+
self.size = self.add(
|
|
48
|
+
self._model.add_variables(
|
|
49
|
+
lower=self.parameters.fixed_size, upper=self.parameters.fixed_size, name=f'{self.label_full}|size'
|
|
50
|
+
),
|
|
51
|
+
'size',
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
self.size = self.add(
|
|
55
|
+
self._model.add_variables(
|
|
56
|
+
lower=0 if self.parameters.optional else self.parameters.minimum_size,
|
|
57
|
+
upper=self.parameters.maximum_size,
|
|
58
|
+
name=f'{self.label_full}|size',
|
|
59
|
+
),
|
|
60
|
+
'size',
|
|
61
|
+
)
|
|
55
62
|
|
|
56
63
|
# Optional
|
|
57
64
|
if self.parameters.optional:
|
|
58
65
|
self.is_invested = self.add(
|
|
59
|
-
self._model.add_variables(
|
|
60
|
-
binary=True,
|
|
61
|
-
name=f'{self.label_full}|is_invested',
|
|
62
|
-
coords=self._model.get_coords(time_dim=False),
|
|
63
|
-
),
|
|
64
|
-
'is_invested',
|
|
66
|
+
self._model.add_variables(binary=True, name=f'{self.label_full}|is_invested'), 'is_invested'
|
|
65
67
|
)
|
|
66
68
|
|
|
67
69
|
self._create_bounds_for_optional_investment()
|
|
68
70
|
|
|
69
|
-
if self._model.time_series_collection.scenarios is not None:
|
|
70
|
-
self._create_bounds_for_scenarios()
|
|
71
|
-
|
|
72
71
|
# Bounds for defining variable
|
|
73
72
|
self._create_bounds_for_defining_variable()
|
|
74
73
|
|
|
@@ -91,7 +90,10 @@ class InvestmentModel(Model):
|
|
|
91
90
|
# share: divest_effects - isInvested * divest_effects
|
|
92
91
|
self._model.effects.add_share_to_effects(
|
|
93
92
|
name=self.label_of_element,
|
|
94
|
-
expressions={
|
|
93
|
+
expressions={
|
|
94
|
+
effect: -self.is_invested * factor + factor
|
|
95
|
+
for effect, factor in self.parameters.divest_effects.items()
|
|
96
|
+
},
|
|
95
97
|
target='invest',
|
|
96
98
|
)
|
|
97
99
|
|
|
@@ -154,6 +156,11 @@ class InvestmentModel(Model):
|
|
|
154
156
|
),
|
|
155
157
|
f'fix_{variable.name}',
|
|
156
158
|
)
|
|
159
|
+
if self._on_variable is not None:
|
|
160
|
+
raise ValueError(
|
|
161
|
+
f'Flow {self.label_full} has a fixed relative flow rate and an on_variable.'
|
|
162
|
+
f'This combination is currently not supported.'
|
|
163
|
+
)
|
|
157
164
|
return
|
|
158
165
|
|
|
159
166
|
# eq: defining_variable(t) <= size * upper_bound(t)
|
|
@@ -178,7 +185,7 @@ class InvestmentModel(Model):
|
|
|
178
185
|
# ... mit mega = relative_maximum * maximum_size
|
|
179
186
|
# äquivalent zu:.
|
|
180
187
|
# eq: - defining_variable(t) + mega * On(t) + size * relative_minimum(t) <= + mega
|
|
181
|
-
mega = self.parameters.maximum_size
|
|
188
|
+
mega = lb_relative * self.parameters.maximum_size
|
|
182
189
|
on = self._on_variable
|
|
183
190
|
self.add(
|
|
184
191
|
self._model.add_constraints(
|
|
@@ -188,50 +195,6 @@ class InvestmentModel(Model):
|
|
|
188
195
|
)
|
|
189
196
|
# anmerkung: Glg bei Spezialfall relative_minimum = 0 redundant zu OnOff ??
|
|
190
197
|
|
|
191
|
-
def _create_bounds_for_scenarios(self):
|
|
192
|
-
if isinstance(self.parameters.investment_scenarios, str):
|
|
193
|
-
if self.parameters.investment_scenarios == 'individual':
|
|
194
|
-
return
|
|
195
|
-
raise ValueError(f'Invalid value for investment_scenarios: {self.parameters.investment_scenarios}')
|
|
196
|
-
|
|
197
|
-
if self.parameters.investment_scenarios is None:
|
|
198
|
-
self.add(
|
|
199
|
-
self._model.add_constraints(
|
|
200
|
-
self.size.isel(scenario=slice(None, -1)) == self.size.isel(scenario=slice(1, None)),
|
|
201
|
-
name=f'{self.label_full}|equalize_size_per_scenario',
|
|
202
|
-
),
|
|
203
|
-
'equalize_size_per_scenario',
|
|
204
|
-
)
|
|
205
|
-
return
|
|
206
|
-
if not isinstance(self.parameters.investment_scenarios, list):
|
|
207
|
-
raise ValueError(f'Invalid value for investment_scenarios: {self.parameters.investment_scenarios}')
|
|
208
|
-
if not all(scenario in self._model.time_series_collection.scenarios for scenario in self.parameters.investment_scenarios):
|
|
209
|
-
raise ValueError(f'Some scenarios in investment_scenarios are not present in the time_series_collection: '
|
|
210
|
-
f'{self.parameters.investment_scenarios}. This might be due to selecting a subset of '
|
|
211
|
-
f'all scenarios, which is not yet supported.')
|
|
212
|
-
|
|
213
|
-
investment_scenarios = self._model.time_series_collection.scenarios.intersection(self.parameters.investment_scenarios)
|
|
214
|
-
no_investment_scenarios = self._model.time_series_collection.scenarios.difference(self.parameters.investment_scenarios)
|
|
215
|
-
|
|
216
|
-
# eq: size(s) = size(s') for s, s' in investment_scenarios
|
|
217
|
-
if len(investment_scenarios) > 1:
|
|
218
|
-
self.add(
|
|
219
|
-
self._model.add_constraints(
|
|
220
|
-
self.size.sel(scenario=investment_scenarios[:-1]) == self.size.sel(scenario=investment_scenarios[1:]),
|
|
221
|
-
name=f'{self.label_full}|investment_scenarios',
|
|
222
|
-
),
|
|
223
|
-
'investment_scenarios',
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
if len(no_investment_scenarios) >= 1:
|
|
227
|
-
self.add(
|
|
228
|
-
self._model.add_constraints(
|
|
229
|
-
self.size.sel(scenario=no_investment_scenarios) == 0,
|
|
230
|
-
name=f'{self.label_full}|no_investment_scenarios',
|
|
231
|
-
),
|
|
232
|
-
'no_investment_scenarios',
|
|
233
|
-
)
|
|
234
|
-
|
|
235
198
|
|
|
236
199
|
class StateModel(Model):
|
|
237
200
|
"""
|
|
@@ -243,12 +206,12 @@ class StateModel(Model):
|
|
|
243
206
|
model: SystemModel,
|
|
244
207
|
label_of_element: str,
|
|
245
208
|
defining_variables: List[linopy.Variable],
|
|
246
|
-
defining_bounds: List[Tuple[
|
|
247
|
-
previous_values: List[Optional[
|
|
209
|
+
defining_bounds: List[Tuple[NumericData, NumericData]],
|
|
210
|
+
previous_values: List[Optional[NumericData]] = None,
|
|
248
211
|
use_off: bool = True,
|
|
249
|
-
on_hours_total_min: Optional[
|
|
250
|
-
on_hours_total_max: Optional[
|
|
251
|
-
effects_per_running_hour: Dict[str,
|
|
212
|
+
on_hours_total_min: Optional[NumericData] = 0,
|
|
213
|
+
on_hours_total_max: Optional[NumericData] = None,
|
|
214
|
+
effects_per_running_hour: Dict[str, NumericData] = None,
|
|
252
215
|
label: Optional[str] = None,
|
|
253
216
|
):
|
|
254
217
|
"""
|
|
@@ -285,16 +248,16 @@ class StateModel(Model):
|
|
|
285
248
|
self._model.add_variables(
|
|
286
249
|
name=f'{self.label_full}|on',
|
|
287
250
|
binary=True,
|
|
288
|
-
coords=self._model.
|
|
251
|
+
coords=self._model.coords,
|
|
289
252
|
),
|
|
290
253
|
'on',
|
|
291
254
|
)
|
|
292
255
|
|
|
293
256
|
self.total_on_hours = self.add(
|
|
294
257
|
self._model.add_variables(
|
|
295
|
-
lower=
|
|
296
|
-
upper=
|
|
297
|
-
coords=
|
|
258
|
+
lower=self._on_hours_total_min,
|
|
259
|
+
upper=self._on_hours_total_max,
|
|
260
|
+
coords=None,
|
|
298
261
|
name=f'{self.label_full}|on_hours_total',
|
|
299
262
|
),
|
|
300
263
|
'on_hours_total',
|
|
@@ -302,7 +265,7 @@ class StateModel(Model):
|
|
|
302
265
|
|
|
303
266
|
self.add(
|
|
304
267
|
self._model.add_constraints(
|
|
305
|
-
self.total_on_hours == (self.on * self._model.hours_per_step).sum(
|
|
268
|
+
self.total_on_hours == (self.on * self._model.hours_per_step).sum(),
|
|
306
269
|
name=f'{self.label_full}|on_hours_total',
|
|
307
270
|
),
|
|
308
271
|
'on_hours_total',
|
|
@@ -316,7 +279,7 @@ class StateModel(Model):
|
|
|
316
279
|
self._model.add_variables(
|
|
317
280
|
name=f'{self.label_full}|off',
|
|
318
281
|
binary=True,
|
|
319
|
-
coords=self._model.
|
|
282
|
+
coords=self._model.coords,
|
|
320
283
|
),
|
|
321
284
|
'off',
|
|
322
285
|
)
|
|
@@ -344,13 +307,11 @@ class StateModel(Model):
|
|
|
344
307
|
)
|
|
345
308
|
|
|
346
309
|
# Constraint: on * upper_bound >= def_var
|
|
347
|
-
self.add(
|
|
348
|
-
self._model.add_constraints(self.on * ub >= def_var, name=f'{self.label_full}|on_con2'), 'on_con2'
|
|
349
|
-
)
|
|
310
|
+
self.add(self._model.add_constraints(self.on * ub >= def_var, name=f'{self.label_full}|on_con2'), 'on_con2')
|
|
350
311
|
else:
|
|
351
312
|
# Case for multiple defining variables
|
|
352
313
|
ub = sum(bound[1] for bound in self._defining_bounds) / nr_of_def_vars
|
|
353
|
-
lb = CONFIG.modeling.EPSILON #TODO: Can this be a bigger value? (maybe the smallest bound?)
|
|
314
|
+
lb = CONFIG.modeling.EPSILON # TODO: Can this be a bigger value? (maybe the smallest bound?)
|
|
354
315
|
|
|
355
316
|
# Constraint: on * epsilon <= sum(all_defining_variables)
|
|
356
317
|
self.add(
|
|
@@ -384,7 +345,7 @@ class StateModel(Model):
|
|
|
384
345
|
return 1 - self.previous_states
|
|
385
346
|
|
|
386
347
|
@staticmethod
|
|
387
|
-
def compute_previous_states(previous_values: List[
|
|
348
|
+
def compute_previous_states(previous_values: List[NumericData], epsilon: float = 1e-5) -> np.ndarray:
|
|
388
349
|
"""Computes the previous states {0, 1} of defining variables as a binary array from their previous values."""
|
|
389
350
|
if not previous_values or all([val is None for val in previous_values]):
|
|
390
351
|
return np.array([0])
|
|
@@ -425,19 +386,19 @@ class SwitchStateModel(Model):
|
|
|
425
386
|
|
|
426
387
|
# Create switch variables
|
|
427
388
|
self.switch_on = self.add(
|
|
428
|
-
self._model.add_variables(binary=True, name=f'{self.label_full}|switch_on', coords=self._model.
|
|
389
|
+
self._model.add_variables(binary=True, name=f'{self.label_full}|switch_on', coords=self._model.coords),
|
|
429
390
|
'switch_on',
|
|
430
391
|
)
|
|
431
392
|
|
|
432
393
|
self.switch_off = self.add(
|
|
433
|
-
self._model.add_variables(binary=True, name=f'{self.label_full}|switch_off', coords=self._model.
|
|
394
|
+
self._model.add_variables(binary=True, name=f'{self.label_full}|switch_off', coords=self._model.coords),
|
|
434
395
|
'switch_off',
|
|
435
396
|
)
|
|
436
397
|
|
|
437
398
|
# Create count variable for number of switches
|
|
438
399
|
self.switch_on_nr = self.add(
|
|
439
400
|
self._model.add_variables(
|
|
440
|
-
upper=
|
|
401
|
+
upper=self._switch_on_max,
|
|
441
402
|
lower=0,
|
|
442
403
|
name=f'{self.label_full}|switch_on_nr',
|
|
443
404
|
),
|
|
@@ -466,7 +427,9 @@ class SwitchStateModel(Model):
|
|
|
466
427
|
|
|
467
428
|
# Mutual exclusivity constraint
|
|
468
429
|
self.add(
|
|
469
|
-
self._model.add_constraints(
|
|
430
|
+
self._model.add_constraints(
|
|
431
|
+
self.switch_on + self.switch_off <= 1.1, name=f'{self.label_full}|switch_on_or_off'
|
|
432
|
+
),
|
|
470
433
|
'switch_on_or_off',
|
|
471
434
|
)
|
|
472
435
|
|
|
@@ -491,9 +454,9 @@ class ConsecutiveStateModel(Model):
|
|
|
491
454
|
model: SystemModel,
|
|
492
455
|
label_of_element: str,
|
|
493
456
|
state_variable: linopy.Variable,
|
|
494
|
-
minimum_duration: Optional[
|
|
495
|
-
maximum_duration: Optional[
|
|
496
|
-
previous_states: Optional[
|
|
457
|
+
minimum_duration: Optional[NumericData] = None,
|
|
458
|
+
maximum_duration: Optional[NumericData] = None,
|
|
459
|
+
previous_states: Optional[NumericData] = None,
|
|
497
460
|
label: Optional[str] = None,
|
|
498
461
|
):
|
|
499
462
|
"""
|
|
@@ -515,9 +478,9 @@ class ConsecutiveStateModel(Model):
|
|
|
515
478
|
self._maximum_duration = maximum_duration
|
|
516
479
|
|
|
517
480
|
if isinstance(self._minimum_duration, TimeSeries):
|
|
518
|
-
self._minimum_duration = self._minimum_duration.
|
|
481
|
+
self._minimum_duration = self._minimum_duration.active_data
|
|
519
482
|
if isinstance(self._maximum_duration, TimeSeries):
|
|
520
|
-
self._maximum_duration = self._maximum_duration.
|
|
483
|
+
self._maximum_duration = self._maximum_duration.active_data
|
|
521
484
|
|
|
522
485
|
self.duration = None
|
|
523
486
|
|
|
@@ -531,8 +494,8 @@ class ConsecutiveStateModel(Model):
|
|
|
531
494
|
self.duration = self.add(
|
|
532
495
|
self._model.add_variables(
|
|
533
496
|
lower=0,
|
|
534
|
-
upper=
|
|
535
|
-
coords=self._model.
|
|
497
|
+
upper=self._maximum_duration if self._maximum_duration is not None else mega,
|
|
498
|
+
coords=self._model.coords,
|
|
536
499
|
name=f'{self.label_full}|hours',
|
|
537
500
|
),
|
|
538
501
|
'hours',
|
|
@@ -542,9 +505,7 @@ class ConsecutiveStateModel(Model):
|
|
|
542
505
|
|
|
543
506
|
# Upper bound constraint
|
|
544
507
|
self.add(
|
|
545
|
-
self._model.add_constraints(
|
|
546
|
-
self.duration <= self._state_variable * mega, name=f'{self.label_full}|con1'
|
|
547
|
-
),
|
|
508
|
+
self._model.add_constraints(self.duration <= self._state_variable * mega, name=f'{self.label_full}|con1'),
|
|
548
509
|
'con1',
|
|
549
510
|
)
|
|
550
511
|
|
|
@@ -585,7 +546,7 @@ class ConsecutiveStateModel(Model):
|
|
|
585
546
|
)
|
|
586
547
|
|
|
587
548
|
# Handle initial condition
|
|
588
|
-
if 0 < self.previous_duration < self._minimum_duration.isel(time=0)
|
|
549
|
+
if 0 < self.previous_duration < self._minimum_duration.isel(time=0):
|
|
589
550
|
self.add(
|
|
590
551
|
self._model.add_constraints(
|
|
591
552
|
self._state_variable.isel(time=0) == 1, name=f'{self.label_full}|initial_minimum'
|
|
@@ -596,8 +557,8 @@ class ConsecutiveStateModel(Model):
|
|
|
596
557
|
# Set initial value
|
|
597
558
|
self.add(
|
|
598
559
|
self._model.add_constraints(
|
|
599
|
-
self.duration.isel(time=0)
|
|
600
|
-
(hours_per_step.isel(time=0) + self.previous_duration) * self._state_variable.isel(time=0),
|
|
560
|
+
self.duration.isel(time=0)
|
|
561
|
+
== (hours_per_step.isel(time=0) + self.previous_duration) * self._state_variable.isel(time=0),
|
|
601
562
|
name=f'{self.label_full}|initial',
|
|
602
563
|
),
|
|
603
564
|
'initial',
|
|
@@ -608,14 +569,14 @@ class ConsecutiveStateModel(Model):
|
|
|
608
569
|
@property
|
|
609
570
|
def previous_duration(self) -> Scalar:
|
|
610
571
|
"""Computes the previous duration of the state variable"""
|
|
611
|
-
#TODO: Allow for other/dynamic timestep resolutions
|
|
572
|
+
# TODO: Allow for other/dynamic timestep resolutions
|
|
612
573
|
return ConsecutiveStateModel.compute_consecutive_hours_in_state(
|
|
613
|
-
self._previous_states, self._model.hours_per_step.isel(time=0).
|
|
574
|
+
self._previous_states, self._model.hours_per_step.isel(time=0).item()
|
|
614
575
|
)
|
|
615
576
|
|
|
616
577
|
@staticmethod
|
|
617
578
|
def compute_consecutive_hours_in_state(
|
|
618
|
-
binary_values:
|
|
579
|
+
binary_values: NumericData, hours_per_timestep: Union[int, float, np.ndarray]
|
|
619
580
|
) -> Scalar:
|
|
620
581
|
"""
|
|
621
582
|
Computes the final consecutive duration in state 'on' (=1) in hours, from a binary array.
|
|
@@ -659,7 +620,10 @@ class ConsecutiveStateModel(Model):
|
|
|
659
620
|
f'as {binary_values=}'
|
|
660
621
|
)
|
|
661
622
|
|
|
662
|
-
return np.sum(
|
|
623
|
+
return np.sum(
|
|
624
|
+
binary_values[-nr_of_indexes_with_consecutive_ones:]
|
|
625
|
+
* hours_per_timestep[-nr_of_indexes_with_consecutive_ones:]
|
|
626
|
+
)
|
|
663
627
|
|
|
664
628
|
|
|
665
629
|
class OnOffModel(Model):
|
|
@@ -674,8 +638,8 @@ class OnOffModel(Model):
|
|
|
674
638
|
on_off_parameters: OnOffParameters,
|
|
675
639
|
label_of_element: str,
|
|
676
640
|
defining_variables: List[linopy.Variable],
|
|
677
|
-
defining_bounds: List[Tuple[
|
|
678
|
-
previous_values: List[Optional[
|
|
641
|
+
defining_bounds: List[Tuple[NumericData, NumericData]],
|
|
642
|
+
previous_values: List[Optional[NumericData]],
|
|
679
643
|
label: Optional[str] = None,
|
|
680
644
|
):
|
|
681
645
|
"""
|
|
@@ -712,8 +676,8 @@ class OnOffModel(Model):
|
|
|
712
676
|
defining_bounds=self._defining_bounds,
|
|
713
677
|
previous_values=self._previous_values,
|
|
714
678
|
use_off=self.parameters.use_off,
|
|
715
|
-
on_hours_total_min=
|
|
716
|
-
on_hours_total_max=
|
|
679
|
+
on_hours_total_min=self.parameters.on_hours_total_min,
|
|
680
|
+
on_hours_total_max=self.parameters.on_hours_total_max,
|
|
717
681
|
effects_per_running_hour=self.parameters.effects_per_running_hour,
|
|
718
682
|
)
|
|
719
683
|
self.add(self.state_model)
|
|
@@ -832,7 +796,7 @@ class PieceModel(Model):
|
|
|
832
796
|
self._model.add_variables(
|
|
833
797
|
binary=True,
|
|
834
798
|
name=f'{self.label_full}|inside_piece',
|
|
835
|
-
coords=self._model.
|
|
799
|
+
coords=self._model.coords if self._as_time_series else None,
|
|
836
800
|
),
|
|
837
801
|
'inside_piece',
|
|
838
802
|
)
|
|
@@ -842,7 +806,7 @@ class PieceModel(Model):
|
|
|
842
806
|
lower=0,
|
|
843
807
|
upper=1,
|
|
844
808
|
name=f'{self.label_full}|lambda0',
|
|
845
|
-
coords=self._model.
|
|
809
|
+
coords=self._model.coords if self._as_time_series else None,
|
|
846
810
|
),
|
|
847
811
|
'lambda0',
|
|
848
812
|
)
|
|
@@ -852,7 +816,7 @@ class PieceModel(Model):
|
|
|
852
816
|
lower=0,
|
|
853
817
|
upper=1,
|
|
854
818
|
name=f'{self.label_full}|lambda1',
|
|
855
|
-
coords=self._model.
|
|
819
|
+
coords=self._model.coords if self._as_time_series else None,
|
|
856
820
|
),
|
|
857
821
|
'lambda1',
|
|
858
822
|
)
|
|
@@ -877,7 +841,7 @@ class PiecewiseModel(Model):
|
|
|
877
841
|
label: str = '',
|
|
878
842
|
):
|
|
879
843
|
"""
|
|
880
|
-
Modeling a Piecewise relation between
|
|
844
|
+
Modeling a Piecewise relation between multiple variables.
|
|
881
845
|
The relation is defined by a list of Pieces, which are assigned to the variables.
|
|
882
846
|
Each Piece is a tuple of (start, end).
|
|
883
847
|
|
|
@@ -886,7 +850,9 @@ class PiecewiseModel(Model):
|
|
|
886
850
|
label_of_element: The label of the parent (Element). Used to construct the full label of the model.
|
|
887
851
|
label: The label of the model. Used to construct the full label of the model.
|
|
888
852
|
piecewise_variables: The variables to which the Pieces are assigned.
|
|
889
|
-
zero_point: A variable that can be used to define a zero point for the Piecewise relation.
|
|
853
|
+
zero_point: A variable that can be used to define a zero point for the Piecewise relation.
|
|
854
|
+
If None or False, no zero point is defined. THis leads to 0 not being possible,
|
|
855
|
+
unless its its contained in a Piece.
|
|
890
856
|
as_time_series: Whether the Piecewise relation is defined for a TimeSeries or a single variable.
|
|
891
857
|
"""
|
|
892
858
|
super().__init__(model, label_of_element, label)
|
|
@@ -932,21 +898,23 @@ class PiecewiseModel(Model):
|
|
|
932
898
|
# b) eq: -On(t) + Segment1.onSeg(t) + Segment2.onSeg(t) + ... = 0 zusätzlich kann alles auch Null sein
|
|
933
899
|
if isinstance(self._zero_point, linopy.Variable):
|
|
934
900
|
self.zero_point = self._zero_point
|
|
935
|
-
rhs = self.zero_point
|
|
901
|
+
sign, rhs = '<=', self.zero_point
|
|
936
902
|
elif self._zero_point is True:
|
|
937
903
|
self.zero_point = self.add(
|
|
938
904
|
self._model.add_variables(
|
|
939
|
-
coords=self._model.
|
|
905
|
+
coords=self._model.coords, binary=True, name=f'{self.label_full}|zero_point'
|
|
940
906
|
),
|
|
941
907
|
'zero_point',
|
|
942
908
|
)
|
|
943
|
-
rhs = self.zero_point
|
|
909
|
+
sign, rhs = '<=', self.zero_point
|
|
944
910
|
else:
|
|
945
|
-
rhs = 1
|
|
911
|
+
sign, rhs = '=', 1
|
|
946
912
|
|
|
947
913
|
self.add(
|
|
948
914
|
self._model.add_constraints(
|
|
949
|
-
sum([piece.inside_piece for piece in self.pieces])
|
|
915
|
+
sum([piece.inside_piece for piece in self.pieces]),
|
|
916
|
+
sign,
|
|
917
|
+
rhs,
|
|
950
918
|
name=f'{self.label_full}|{variable.name}|single_segment',
|
|
951
919
|
),
|
|
952
920
|
f'{var_name}|single_segment',
|
|
@@ -957,20 +925,19 @@ class ShareAllocationModel(Model):
|
|
|
957
925
|
def __init__(
|
|
958
926
|
self,
|
|
959
927
|
model: SystemModel,
|
|
960
|
-
|
|
961
|
-
has_scenario_dim: bool,
|
|
928
|
+
shares_are_time_series: bool,
|
|
962
929
|
label_of_element: Optional[str] = None,
|
|
963
930
|
label: Optional[str] = None,
|
|
964
931
|
label_full: Optional[str] = None,
|
|
965
|
-
total_max: Optional[
|
|
966
|
-
total_min: Optional[
|
|
967
|
-
max_per_hour: Optional[
|
|
968
|
-
min_per_hour: Optional[
|
|
932
|
+
total_max: Optional[Scalar] = None,
|
|
933
|
+
total_min: Optional[Scalar] = None,
|
|
934
|
+
max_per_hour: Optional[NumericData] = None,
|
|
935
|
+
min_per_hour: Optional[NumericData] = None,
|
|
969
936
|
):
|
|
970
937
|
super().__init__(model, label_of_element=label_of_element, label=label, label_full=label_full)
|
|
971
|
-
if not
|
|
938
|
+
if not shares_are_time_series: # If the condition is True
|
|
972
939
|
assert max_per_hour is None and min_per_hour is None, (
|
|
973
|
-
'Both max_per_hour and min_per_hour cannot be used when
|
|
940
|
+
'Both max_per_hour and min_per_hour cannot be used when shares_are_time_series is False'
|
|
974
941
|
)
|
|
975
942
|
self.total_per_timestep: Optional[linopy.Variable] = None
|
|
976
943
|
self.total: Optional[linopy.Variable] = None
|
|
@@ -981,9 +948,8 @@ class ShareAllocationModel(Model):
|
|
|
981
948
|
self._eq_total: Optional[linopy.Constraint] = None
|
|
982
949
|
|
|
983
950
|
# Parameters
|
|
984
|
-
self.
|
|
985
|
-
self.
|
|
986
|
-
self._total_max = total_max if total_max is not None else np.inf
|
|
951
|
+
self._shares_are_time_series = shares_are_time_series
|
|
952
|
+
self._total_max = total_max if total_min is not None else np.inf
|
|
987
953
|
self._total_min = total_min if total_min is not None else -np.inf
|
|
988
954
|
self._max_per_hour = max_per_hour if max_per_hour is not None else np.inf
|
|
989
955
|
self._min_per_hour = min_per_hour if min_per_hour is not None else -np.inf
|
|
@@ -991,10 +957,7 @@ class ShareAllocationModel(Model):
|
|
|
991
957
|
def do_modeling(self):
|
|
992
958
|
self.total = self.add(
|
|
993
959
|
self._model.add_variables(
|
|
994
|
-
lower=self._total_min,
|
|
995
|
-
upper=self._total_max,
|
|
996
|
-
coords=self._model.get_coords(time_dim=False, scenario_dim=self._has_scenario_dim),
|
|
997
|
-
name=f'{self.label_full}|total',
|
|
960
|
+
lower=self._total_min, upper=self._total_max, coords=None, name=f'{self.label_full}|total'
|
|
998
961
|
),
|
|
999
962
|
'total',
|
|
1000
963
|
)
|
|
@@ -1003,12 +966,16 @@ class ShareAllocationModel(Model):
|
|
|
1003
966
|
self._model.add_constraints(self.total == 0, name=f'{self.label_full}|total'), 'total'
|
|
1004
967
|
)
|
|
1005
968
|
|
|
1006
|
-
if self.
|
|
969
|
+
if self._shares_are_time_series:
|
|
1007
970
|
self.total_per_timestep = self.add(
|
|
1008
971
|
self._model.add_variables(
|
|
1009
|
-
lower=-np.inf
|
|
1010
|
-
|
|
1011
|
-
|
|
972
|
+
lower=-np.inf
|
|
973
|
+
if (self._min_per_hour is None)
|
|
974
|
+
else np.multiply(self._min_per_hour, self._model.hours_per_step),
|
|
975
|
+
upper=np.inf
|
|
976
|
+
if (self._max_per_hour is None)
|
|
977
|
+
else np.multiply(self._max_per_hour, self._model.hours_per_step),
|
|
978
|
+
coords=self._model.coords,
|
|
1012
979
|
name=f'{self.label_full}|total_per_timestep',
|
|
1013
980
|
),
|
|
1014
981
|
'total_per_timestep',
|
|
@@ -1020,14 +987,12 @@ class ShareAllocationModel(Model):
|
|
|
1020
987
|
)
|
|
1021
988
|
|
|
1022
989
|
# Add it to the total
|
|
1023
|
-
self._eq_total.lhs -= self.total_per_timestep.sum(
|
|
990
|
+
self._eq_total.lhs -= self.total_per_timestep.sum()
|
|
1024
991
|
|
|
1025
992
|
def add_share(
|
|
1026
993
|
self,
|
|
1027
994
|
name: str,
|
|
1028
995
|
expression: linopy.LinearExpression,
|
|
1029
|
-
has_time_dim: bool,
|
|
1030
|
-
has_scenario_dim: bool,
|
|
1031
996
|
):
|
|
1032
997
|
"""
|
|
1033
998
|
Add a share to the share allocation model. If the share already exists, the expression is added to the existing share.
|
|
@@ -1039,17 +1004,16 @@ class ShareAllocationModel(Model):
|
|
|
1039
1004
|
name: The name of the share.
|
|
1040
1005
|
expression: The expression of the share. Added to the right hand side of the constraint.
|
|
1041
1006
|
"""
|
|
1042
|
-
if has_time_dim and not self._has_time_dim:
|
|
1043
|
-
raise ValueError('Cannot add share with time_dim=True to a model without time_dim')
|
|
1044
|
-
if has_scenario_dim and not self._has_scenario_dim:
|
|
1045
|
-
raise ValueError('Cannot add share with scenario_dim=True to a model without scenario_dim')
|
|
1046
|
-
|
|
1047
1007
|
if name in self.shares:
|
|
1048
1008
|
self.share_constraints[name].lhs -= expression
|
|
1049
1009
|
else:
|
|
1050
1010
|
self.shares[name] = self.add(
|
|
1051
1011
|
self._model.add_variables(
|
|
1052
|
-
coords=
|
|
1012
|
+
coords=None
|
|
1013
|
+
if isinstance(expression, linopy.LinearExpression)
|
|
1014
|
+
and expression.ndim == 0
|
|
1015
|
+
or not isinstance(expression, linopy.LinearExpression)
|
|
1016
|
+
else self._model.coords,
|
|
1053
1017
|
name=f'{name}->{self.label_full}',
|
|
1054
1018
|
),
|
|
1055
1019
|
name,
|
|
@@ -1057,7 +1021,7 @@ class ShareAllocationModel(Model):
|
|
|
1057
1021
|
self.share_constraints[name] = self.add(
|
|
1058
1022
|
self._model.add_constraints(self.shares[name] == expression, name=f'{name}->{self.label_full}'), name
|
|
1059
1023
|
)
|
|
1060
|
-
if
|
|
1024
|
+
if self.shares[name].ndim == 0:
|
|
1061
1025
|
self._eq_total.lhs -= self.shares[name]
|
|
1062
1026
|
else:
|
|
1063
1027
|
self._eq_total_per_timestep.lhs -= self.shares[name]
|
|
@@ -1086,12 +1050,7 @@ class PiecewiseEffectsModel(Model):
|
|
|
1086
1050
|
|
|
1087
1051
|
def do_modeling(self):
|
|
1088
1052
|
self.shares = {
|
|
1089
|
-
effect: self.add(
|
|
1090
|
-
self._model.add_variables(
|
|
1091
|
-
coords=self._model.get_coords(time_dim=False), name=f'{self.label_full}|{effect}'
|
|
1092
|
-
),
|
|
1093
|
-
f'{effect}',
|
|
1094
|
-
)
|
|
1053
|
+
effect: self.add(self._model.add_variables(coords=None, name=f'{self.label_full}|{effect}'), f'{effect}')
|
|
1095
1054
|
for effect in self._piecewise_shares
|
|
1096
1055
|
}
|
|
1097
1056
|
|
|
@@ -1124,6 +1083,65 @@ class PiecewiseEffectsModel(Model):
|
|
|
1124
1083
|
)
|
|
1125
1084
|
|
|
1126
1085
|
|
|
1086
|
+
class PiecewiseEffectsPerFlowHourModel(Model):
|
|
1087
|
+
def __init__(
|
|
1088
|
+
self,
|
|
1089
|
+
model: SystemModel,
|
|
1090
|
+
label_of_element: str,
|
|
1091
|
+
piecewise_origin: Tuple[str, Piecewise],
|
|
1092
|
+
piecewise_shares: Dict[str, Piecewise],
|
|
1093
|
+
zero_point: Optional[Union[bool, linopy.Variable]],
|
|
1094
|
+
label: str = 'PiecewiseEffectsPerFlowHour',
|
|
1095
|
+
):
|
|
1096
|
+
super().__init__(model, label_of_element, label)
|
|
1097
|
+
assert len(piecewise_origin[1]) == len(list(piecewise_shares.values())[0]), (
|
|
1098
|
+
'Piece length of variable_segments and share_segments must be equal'
|
|
1099
|
+
)
|
|
1100
|
+
self._zero_point = zero_point
|
|
1101
|
+
self._piecewise_origin = piecewise_origin
|
|
1102
|
+
self._piecewise_shares = piecewise_shares
|
|
1103
|
+
|
|
1104
|
+
self.shares: Dict[str, linopy.Variable] = {}
|
|
1105
|
+
|
|
1106
|
+
self.piecewise_model: Optional[PiecewiseModel] = None
|
|
1107
|
+
|
|
1108
|
+
def do_modeling(self):
|
|
1109
|
+
self.shares = {
|
|
1110
|
+
effect: self.add(
|
|
1111
|
+
self._model.add_variables(coords=self._model.coords, name=f'{self.label_full}|{effect}'), f'{effect}'
|
|
1112
|
+
)
|
|
1113
|
+
for effect in self._piecewise_shares
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
piecewise_variables = {
|
|
1117
|
+
self._piecewise_origin[0]: self._piecewise_origin[1],
|
|
1118
|
+
**{
|
|
1119
|
+
self.shares[effect_label].name: self._piecewise_shares[effect_label]
|
|
1120
|
+
for effect_label in self._piecewise_shares
|
|
1121
|
+
},
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
self.piecewise_model = self.add(
|
|
1125
|
+
PiecewiseModel(
|
|
1126
|
+
model=self._model,
|
|
1127
|
+
label_of_element=self.label_of_element,
|
|
1128
|
+
piecewise_variables=piecewise_variables,
|
|
1129
|
+
zero_point=self._zero_point,
|
|
1130
|
+
as_time_series=True,
|
|
1131
|
+
label='PiecewiseEffectsPerFlowHour',
|
|
1132
|
+
)
|
|
1133
|
+
)
|
|
1134
|
+
|
|
1135
|
+
self.piecewise_model.do_modeling()
|
|
1136
|
+
|
|
1137
|
+
# Shares
|
|
1138
|
+
self._model.effects.add_share_to_effects(
|
|
1139
|
+
name=self.label_of_element,
|
|
1140
|
+
expressions={effect: variable * self._model.hours_per_step for effect, variable in self.shares.items()},
|
|
1141
|
+
target='operation',
|
|
1142
|
+
)
|
|
1143
|
+
|
|
1144
|
+
|
|
1127
1145
|
class PreventSimultaneousUsageModel(Model):
|
|
1128
1146
|
"""
|
|
1129
1147
|
Prevents multiple Multiple Binary variables from being 1 at the same time
|