flixopt 2.1.1__py3-none-any.whl → 2.2.0b0__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.2.0.md +55 -0
- docs/user-guide/Mathematical Notation/Investment.md +115 -0
- flixopt/calculation.py +65 -37
- flixopt/components.py +115 -73
- flixopt/core.py +966 -451
- flixopt/effects.py +269 -65
- flixopt/elements.py +79 -49
- flixopt/features.py +134 -85
- flixopt/flow_system.py +99 -16
- flixopt/interface.py +142 -51
- flixopt/io.py +56 -27
- flixopt/linear_converters.py +3 -3
- flixopt/plotting.py +34 -16
- flixopt/results.py +806 -108
- flixopt/structure.py +64 -10
- flixopt/utils.py +6 -9
- {flixopt-2.1.1.dist-info → flixopt-2.2.0b0.dist-info}/METADATA +1 -1
- {flixopt-2.1.1.dist-info → flixopt-2.2.0b0.dist-info}/RECORD +21 -21
- {flixopt-2.1.1.dist-info → flixopt-2.2.0b0.dist-info}/WHEEL +1 -1
- {flixopt-2.1.1.dist-info → flixopt-2.2.0b0.dist-info}/top_level.txt +0 -1
- docs/release-notes/v2.1.1.md +0 -11
- site/release-notes/_template.txt +0 -32
- {flixopt-2.1.1.dist-info → flixopt-2.2.0b0.dist-info}/licenses/LICENSE +0 -0
flixopt/features.py
CHANGED
|
@@ -9,9 +9,8 @@ from typing import Dict, List, Optional, Tuple, Union
|
|
|
9
9
|
import linopy
|
|
10
10
|
import numpy as np
|
|
11
11
|
|
|
12
|
-
from . import utils
|
|
13
12
|
from .config import CONFIG
|
|
14
|
-
from .core import
|
|
13
|
+
from .core import Scalar, ScenarioData, TimeSeries, TimestepData, extract_data
|
|
15
14
|
from .interface import InvestParameters, OnOffParameters, Piecewise
|
|
16
15
|
from .structure import Model, SystemModel
|
|
17
16
|
|
|
@@ -27,13 +26,14 @@ class InvestmentModel(Model):
|
|
|
27
26
|
label_of_element: str,
|
|
28
27
|
parameters: InvestParameters,
|
|
29
28
|
defining_variable: [linopy.Variable],
|
|
30
|
-
relative_bounds_of_defining_variable: Tuple[
|
|
29
|
+
relative_bounds_of_defining_variable: Tuple[TimestepData, TimestepData],
|
|
31
30
|
label: Optional[str] = None,
|
|
32
31
|
on_variable: Optional[linopy.Variable] = None,
|
|
33
32
|
):
|
|
34
33
|
super().__init__(model, label_of_element, label)
|
|
35
34
|
self.size: Optional[Union[Scalar, linopy.Variable]] = None
|
|
36
35
|
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,31 +43,32 @@ class InvestmentModel(Model):
|
|
|
43
43
|
self.parameters = parameters
|
|
44
44
|
|
|
45
45
|
def do_modeling(self):
|
|
46
|
-
|
|
47
|
-
self.
|
|
48
|
-
self.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
)
|
|
46
|
+
self.size = self.add(
|
|
47
|
+
self._model.add_variables(
|
|
48
|
+
lower=0 if self.parameters.optional else extract_data(self.parameters.minimum_size),
|
|
49
|
+
upper=extract_data(self.parameters.maximum_size),
|
|
50
|
+
name=f'{self.label_full}|size',
|
|
51
|
+
coords=self._model.get_coords(time_dim=False),
|
|
52
|
+
),
|
|
53
|
+
'size',
|
|
54
|
+
)
|
|
62
55
|
|
|
63
56
|
# Optional
|
|
64
57
|
if self.parameters.optional:
|
|
65
58
|
self.is_invested = self.add(
|
|
66
|
-
self._model.add_variables(
|
|
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',
|
|
67
65
|
)
|
|
68
66
|
|
|
69
67
|
self._create_bounds_for_optional_investment()
|
|
70
68
|
|
|
69
|
+
if self._model.time_series_collection.scenarios is not None:
|
|
70
|
+
self._create_bounds_for_scenarios()
|
|
71
|
+
|
|
71
72
|
# Bounds for defining variable
|
|
72
73
|
self._create_bounds_for_defining_variable()
|
|
73
74
|
|
|
@@ -153,11 +154,6 @@ class InvestmentModel(Model):
|
|
|
153
154
|
),
|
|
154
155
|
f'fix_{variable.name}',
|
|
155
156
|
)
|
|
156
|
-
if self._on_variable is not None:
|
|
157
|
-
raise ValueError(
|
|
158
|
-
f'Flow {self.label_full} has a fixed relative flow rate and an on_variable.'
|
|
159
|
-
f'This combination is currently not supported.'
|
|
160
|
-
)
|
|
161
157
|
return
|
|
162
158
|
|
|
163
159
|
# eq: defining_variable(t) <= size * upper_bound(t)
|
|
@@ -182,7 +178,7 @@ class InvestmentModel(Model):
|
|
|
182
178
|
# ... mit mega = relative_maximum * maximum_size
|
|
183
179
|
# äquivalent zu:.
|
|
184
180
|
# eq: - defining_variable(t) + mega * On(t) + size * relative_minimum(t) <= + mega
|
|
185
|
-
mega =
|
|
181
|
+
mega = self.parameters.maximum_size * lb_relative
|
|
186
182
|
on = self._on_variable
|
|
187
183
|
self.add(
|
|
188
184
|
self._model.add_constraints(
|
|
@@ -192,6 +188,50 @@ class InvestmentModel(Model):
|
|
|
192
188
|
)
|
|
193
189
|
# anmerkung: Glg bei Spezialfall relative_minimum = 0 redundant zu OnOff ??
|
|
194
190
|
|
|
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
|
+
|
|
195
235
|
|
|
196
236
|
class StateModel(Model):
|
|
197
237
|
"""
|
|
@@ -203,12 +243,12 @@ class StateModel(Model):
|
|
|
203
243
|
model: SystemModel,
|
|
204
244
|
label_of_element: str,
|
|
205
245
|
defining_variables: List[linopy.Variable],
|
|
206
|
-
defining_bounds: List[Tuple[
|
|
207
|
-
previous_values: List[Optional[
|
|
246
|
+
defining_bounds: List[Tuple[TimestepData, TimestepData]],
|
|
247
|
+
previous_values: List[Optional[TimestepData]] = None,
|
|
208
248
|
use_off: bool = True,
|
|
209
|
-
on_hours_total_min: Optional[
|
|
210
|
-
on_hours_total_max: Optional[
|
|
211
|
-
effects_per_running_hour: Dict[str,
|
|
249
|
+
on_hours_total_min: Optional[ScenarioData] = 0,
|
|
250
|
+
on_hours_total_max: Optional[ScenarioData] = None,
|
|
251
|
+
effects_per_running_hour: Dict[str, TimestepData] = None,
|
|
212
252
|
label: Optional[str] = None,
|
|
213
253
|
):
|
|
214
254
|
"""
|
|
@@ -245,16 +285,16 @@ class StateModel(Model):
|
|
|
245
285
|
self._model.add_variables(
|
|
246
286
|
name=f'{self.label_full}|on',
|
|
247
287
|
binary=True,
|
|
248
|
-
coords=self._model.
|
|
288
|
+
coords=self._model.get_coords(),
|
|
249
289
|
),
|
|
250
290
|
'on',
|
|
251
291
|
)
|
|
252
292
|
|
|
253
293
|
self.total_on_hours = self.add(
|
|
254
294
|
self._model.add_variables(
|
|
255
|
-
lower=self._on_hours_total_min,
|
|
256
|
-
upper=self._on_hours_total_max,
|
|
257
|
-
coords=
|
|
295
|
+
lower=extract_data(self._on_hours_total_min),
|
|
296
|
+
upper=extract_data(self._on_hours_total_max),
|
|
297
|
+
coords=self._model.get_coords(time_dim=False),
|
|
258
298
|
name=f'{self.label_full}|on_hours_total',
|
|
259
299
|
),
|
|
260
300
|
'on_hours_total',
|
|
@@ -262,7 +302,7 @@ class StateModel(Model):
|
|
|
262
302
|
|
|
263
303
|
self.add(
|
|
264
304
|
self._model.add_constraints(
|
|
265
|
-
self.total_on_hours == (self.on * self._model.hours_per_step).sum(),
|
|
305
|
+
self.total_on_hours == (self.on * self._model.hours_per_step).sum('time'),
|
|
266
306
|
name=f'{self.label_full}|on_hours_total',
|
|
267
307
|
),
|
|
268
308
|
'on_hours_total',
|
|
@@ -276,7 +316,7 @@ class StateModel(Model):
|
|
|
276
316
|
self._model.add_variables(
|
|
277
317
|
name=f'{self.label_full}|off',
|
|
278
318
|
binary=True,
|
|
279
|
-
coords=self._model.
|
|
319
|
+
coords=self._model.get_coords(),
|
|
280
320
|
),
|
|
281
321
|
'off',
|
|
282
322
|
)
|
|
@@ -344,7 +384,7 @@ class StateModel(Model):
|
|
|
344
384
|
return 1 - self.previous_states
|
|
345
385
|
|
|
346
386
|
@staticmethod
|
|
347
|
-
def compute_previous_states(previous_values: List[
|
|
387
|
+
def compute_previous_states(previous_values: List[TimestepData], epsilon: float = 1e-5) -> np.ndarray:
|
|
348
388
|
"""Computes the previous states {0, 1} of defining variables as a binary array from their previous values."""
|
|
349
389
|
if not previous_values or all([val is None for val in previous_values]):
|
|
350
390
|
return np.array([0])
|
|
@@ -385,19 +425,19 @@ class SwitchStateModel(Model):
|
|
|
385
425
|
|
|
386
426
|
# Create switch variables
|
|
387
427
|
self.switch_on = self.add(
|
|
388
|
-
self._model.add_variables(binary=True, name=f'{self.label_full}|switch_on', coords=self._model.
|
|
428
|
+
self._model.add_variables(binary=True, name=f'{self.label_full}|switch_on', coords=self._model.get_coords()),
|
|
389
429
|
'switch_on',
|
|
390
430
|
)
|
|
391
431
|
|
|
392
432
|
self.switch_off = self.add(
|
|
393
|
-
self._model.add_variables(binary=True, name=f'{self.label_full}|switch_off', coords=self._model.
|
|
433
|
+
self._model.add_variables(binary=True, name=f'{self.label_full}|switch_off', coords=self._model.get_coords()),
|
|
394
434
|
'switch_off',
|
|
395
435
|
)
|
|
396
436
|
|
|
397
437
|
# Create count variable for number of switches
|
|
398
438
|
self.switch_on_nr = self.add(
|
|
399
439
|
self._model.add_variables(
|
|
400
|
-
upper=self._switch_on_max,
|
|
440
|
+
upper=extract_data(self._switch_on_max),
|
|
401
441
|
lower=0,
|
|
402
442
|
name=f'{self.label_full}|switch_on_nr',
|
|
403
443
|
),
|
|
@@ -451,9 +491,9 @@ class ConsecutiveStateModel(Model):
|
|
|
451
491
|
model: SystemModel,
|
|
452
492
|
label_of_element: str,
|
|
453
493
|
state_variable: linopy.Variable,
|
|
454
|
-
minimum_duration: Optional[
|
|
455
|
-
maximum_duration: Optional[
|
|
456
|
-
previous_states: Optional[
|
|
494
|
+
minimum_duration: Optional[TimestepData] = None,
|
|
495
|
+
maximum_duration: Optional[TimestepData] = None,
|
|
496
|
+
previous_states: Optional[TimestepData] = None,
|
|
457
497
|
label: Optional[str] = None,
|
|
458
498
|
):
|
|
459
499
|
"""
|
|
@@ -475,9 +515,9 @@ class ConsecutiveStateModel(Model):
|
|
|
475
515
|
self._maximum_duration = maximum_duration
|
|
476
516
|
|
|
477
517
|
if isinstance(self._minimum_duration, TimeSeries):
|
|
478
|
-
self._minimum_duration = self._minimum_duration.
|
|
518
|
+
self._minimum_duration = self._minimum_duration.selected_data
|
|
479
519
|
if isinstance(self._maximum_duration, TimeSeries):
|
|
480
|
-
self._maximum_duration = self._maximum_duration.
|
|
520
|
+
self._maximum_duration = self._maximum_duration.selected_data
|
|
481
521
|
|
|
482
522
|
self.duration = None
|
|
483
523
|
|
|
@@ -491,8 +531,8 @@ class ConsecutiveStateModel(Model):
|
|
|
491
531
|
self.duration = self.add(
|
|
492
532
|
self._model.add_variables(
|
|
493
533
|
lower=0,
|
|
494
|
-
upper=self._maximum_duration
|
|
495
|
-
coords=self._model.
|
|
534
|
+
upper=extract_data(self._maximum_duration, mega),
|
|
535
|
+
coords=self._model.get_coords(),
|
|
496
536
|
name=f'{self.label_full}|hours',
|
|
497
537
|
),
|
|
498
538
|
'hours',
|
|
@@ -545,7 +585,7 @@ class ConsecutiveStateModel(Model):
|
|
|
545
585
|
)
|
|
546
586
|
|
|
547
587
|
# Handle initial condition
|
|
548
|
-
if 0 < self.previous_duration < self._minimum_duration.isel(time=0):
|
|
588
|
+
if 0 < self.previous_duration < self._minimum_duration.isel(time=0).max():
|
|
549
589
|
self.add(
|
|
550
590
|
self._model.add_constraints(
|
|
551
591
|
self._state_variable.isel(time=0) == 1, name=f'{self.label_full}|initial_minimum'
|
|
@@ -570,12 +610,12 @@ class ConsecutiveStateModel(Model):
|
|
|
570
610
|
"""Computes the previous duration of the state variable"""
|
|
571
611
|
#TODO: Allow for other/dynamic timestep resolutions
|
|
572
612
|
return ConsecutiveStateModel.compute_consecutive_hours_in_state(
|
|
573
|
-
self._previous_states, self._model.hours_per_step.isel(time=0).
|
|
613
|
+
self._previous_states, self._model.hours_per_step.isel(time=0).values.flatten()[0]
|
|
574
614
|
)
|
|
575
615
|
|
|
576
616
|
@staticmethod
|
|
577
617
|
def compute_consecutive_hours_in_state(
|
|
578
|
-
binary_values:
|
|
618
|
+
binary_values: TimestepData, hours_per_timestep: Union[int, float, np.ndarray]
|
|
579
619
|
) -> Scalar:
|
|
580
620
|
"""
|
|
581
621
|
Computes the final consecutive duration in state 'on' (=1) in hours, from a binary array.
|
|
@@ -634,8 +674,8 @@ class OnOffModel(Model):
|
|
|
634
674
|
on_off_parameters: OnOffParameters,
|
|
635
675
|
label_of_element: str,
|
|
636
676
|
defining_variables: List[linopy.Variable],
|
|
637
|
-
defining_bounds: List[Tuple[
|
|
638
|
-
previous_values: List[Optional[
|
|
677
|
+
defining_bounds: List[Tuple[TimestepData, TimestepData]],
|
|
678
|
+
previous_values: List[Optional[TimestepData]],
|
|
639
679
|
label: Optional[str] = None,
|
|
640
680
|
):
|
|
641
681
|
"""
|
|
@@ -672,8 +712,8 @@ class OnOffModel(Model):
|
|
|
672
712
|
defining_bounds=self._defining_bounds,
|
|
673
713
|
previous_values=self._previous_values,
|
|
674
714
|
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,
|
|
715
|
+
on_hours_total_min=extract_data(self.parameters.on_hours_total_min),
|
|
716
|
+
on_hours_total_max=extract_data(self.parameters.on_hours_total_max),
|
|
677
717
|
effects_per_running_hour=self.parameters.effects_per_running_hour,
|
|
678
718
|
)
|
|
679
719
|
self.add(self.state_model)
|
|
@@ -792,7 +832,7 @@ class PieceModel(Model):
|
|
|
792
832
|
self._model.add_variables(
|
|
793
833
|
binary=True,
|
|
794
834
|
name=f'{self.label_full}|inside_piece',
|
|
795
|
-
coords=self._model.
|
|
835
|
+
coords=self._model.get_coords(time_dim=self._as_time_series),
|
|
796
836
|
),
|
|
797
837
|
'inside_piece',
|
|
798
838
|
)
|
|
@@ -802,7 +842,7 @@ class PieceModel(Model):
|
|
|
802
842
|
lower=0,
|
|
803
843
|
upper=1,
|
|
804
844
|
name=f'{self.label_full}|lambda0',
|
|
805
|
-
coords=self._model.
|
|
845
|
+
coords=self._model.get_coords(time_dim=self._as_time_series),
|
|
806
846
|
),
|
|
807
847
|
'lambda0',
|
|
808
848
|
)
|
|
@@ -812,7 +852,7 @@ class PieceModel(Model):
|
|
|
812
852
|
lower=0,
|
|
813
853
|
upper=1,
|
|
814
854
|
name=f'{self.label_full}|lambda1',
|
|
815
|
-
coords=self._model.
|
|
855
|
+
coords=self._model.get_coords(time_dim=self._as_time_series),
|
|
816
856
|
),
|
|
817
857
|
'lambda1',
|
|
818
858
|
)
|
|
@@ -896,7 +936,7 @@ class PiecewiseModel(Model):
|
|
|
896
936
|
elif self._zero_point is True:
|
|
897
937
|
self.zero_point = self.add(
|
|
898
938
|
self._model.add_variables(
|
|
899
|
-
coords=self._model.
|
|
939
|
+
coords=self._model.get_coords(), binary=True, name=f'{self.label_full}|zero_point'
|
|
900
940
|
),
|
|
901
941
|
'zero_point',
|
|
902
942
|
)
|
|
@@ -917,19 +957,20 @@ class ShareAllocationModel(Model):
|
|
|
917
957
|
def __init__(
|
|
918
958
|
self,
|
|
919
959
|
model: SystemModel,
|
|
920
|
-
|
|
960
|
+
has_time_dim: bool,
|
|
961
|
+
has_scenario_dim: bool,
|
|
921
962
|
label_of_element: Optional[str] = None,
|
|
922
963
|
label: Optional[str] = None,
|
|
923
964
|
label_full: Optional[str] = None,
|
|
924
|
-
total_max: Optional[
|
|
925
|
-
total_min: Optional[
|
|
926
|
-
max_per_hour: Optional[
|
|
927
|
-
min_per_hour: Optional[
|
|
965
|
+
total_max: Optional[ScenarioData] = None,
|
|
966
|
+
total_min: Optional[ScenarioData] = None,
|
|
967
|
+
max_per_hour: Optional[TimestepData] = None,
|
|
968
|
+
min_per_hour: Optional[TimestepData] = None,
|
|
928
969
|
):
|
|
929
970
|
super().__init__(model, label_of_element=label_of_element, label=label, label_full=label_full)
|
|
930
|
-
if not
|
|
971
|
+
if not has_time_dim: # If the condition is True
|
|
931
972
|
assert max_per_hour is None and min_per_hour is None, (
|
|
932
|
-
'Both max_per_hour and min_per_hour cannot be used when
|
|
973
|
+
'Both max_per_hour and min_per_hour cannot be used when has_time_dim is False'
|
|
933
974
|
)
|
|
934
975
|
self.total_per_timestep: Optional[linopy.Variable] = None
|
|
935
976
|
self.total: Optional[linopy.Variable] = None
|
|
@@ -940,8 +981,9 @@ class ShareAllocationModel(Model):
|
|
|
940
981
|
self._eq_total: Optional[linopy.Constraint] = None
|
|
941
982
|
|
|
942
983
|
# Parameters
|
|
943
|
-
self.
|
|
944
|
-
self.
|
|
984
|
+
self._has_time_dim = has_time_dim
|
|
985
|
+
self._has_scenario_dim = has_scenario_dim
|
|
986
|
+
self._total_max = total_max if total_max is not None else np.inf
|
|
945
987
|
self._total_min = total_min if total_min is not None else -np.inf
|
|
946
988
|
self._max_per_hour = max_per_hour if max_per_hour is not None else np.inf
|
|
947
989
|
self._min_per_hour = min_per_hour if min_per_hour is not None else -np.inf
|
|
@@ -949,7 +991,10 @@ class ShareAllocationModel(Model):
|
|
|
949
991
|
def do_modeling(self):
|
|
950
992
|
self.total = self.add(
|
|
951
993
|
self._model.add_variables(
|
|
952
|
-
lower=self._total_min,
|
|
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',
|
|
953
998
|
),
|
|
954
999
|
'total',
|
|
955
1000
|
)
|
|
@@ -958,16 +1003,12 @@ class ShareAllocationModel(Model):
|
|
|
958
1003
|
self._model.add_constraints(self.total == 0, name=f'{self.label_full}|total'), 'total'
|
|
959
1004
|
)
|
|
960
1005
|
|
|
961
|
-
if self.
|
|
1006
|
+
if self._has_time_dim:
|
|
962
1007
|
self.total_per_timestep = self.add(
|
|
963
1008
|
self._model.add_variables(
|
|
964
|
-
lower=-np.inf
|
|
965
|
-
if (self.
|
|
966
|
-
|
|
967
|
-
upper=np.inf
|
|
968
|
-
if (self._max_per_hour is None)
|
|
969
|
-
else np.multiply(self._max_per_hour, self._model.hours_per_step),
|
|
970
|
-
coords=self._model.coords,
|
|
1009
|
+
lower=-np.inf if (self._min_per_hour is None) else extract_data(self._min_per_hour) * self._model.hours_per_step,
|
|
1010
|
+
upper=np.inf if (self._max_per_hour is None) else extract_data(self._max_per_hour) * self._model.hours_per_step,
|
|
1011
|
+
coords=self._model.get_coords(time_dim=True, scenario_dim=self._has_scenario_dim),
|
|
971
1012
|
name=f'{self.label_full}|total_per_timestep',
|
|
972
1013
|
),
|
|
973
1014
|
'total_per_timestep',
|
|
@@ -979,12 +1020,14 @@ class ShareAllocationModel(Model):
|
|
|
979
1020
|
)
|
|
980
1021
|
|
|
981
1022
|
# Add it to the total
|
|
982
|
-
self._eq_total.lhs -= self.total_per_timestep.sum()
|
|
1023
|
+
self._eq_total.lhs -= self.total_per_timestep.sum(dim='time')
|
|
983
1024
|
|
|
984
1025
|
def add_share(
|
|
985
1026
|
self,
|
|
986
1027
|
name: str,
|
|
987
1028
|
expression: linopy.LinearExpression,
|
|
1029
|
+
has_time_dim: bool,
|
|
1030
|
+
has_scenario_dim: bool,
|
|
988
1031
|
):
|
|
989
1032
|
"""
|
|
990
1033
|
Add a share to the share allocation model. If the share already exists, the expression is added to the existing share.
|
|
@@ -996,16 +1039,17 @@ class ShareAllocationModel(Model):
|
|
|
996
1039
|
name: The name of the share.
|
|
997
1040
|
expression: The expression of the share. Added to the right hand side of the constraint.
|
|
998
1041
|
"""
|
|
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
|
+
|
|
999
1047
|
if name in self.shares:
|
|
1000
1048
|
self.share_constraints[name].lhs -= expression
|
|
1001
1049
|
else:
|
|
1002
1050
|
self.shares[name] = self.add(
|
|
1003
1051
|
self._model.add_variables(
|
|
1004
|
-
coords=
|
|
1005
|
-
if isinstance(expression, linopy.LinearExpression)
|
|
1006
|
-
and expression.ndim == 0
|
|
1007
|
-
or not isinstance(expression, linopy.LinearExpression)
|
|
1008
|
-
else self._model.coords,
|
|
1052
|
+
coords=self._model.get_coords(time_dim=has_time_dim, scenario_dim=has_scenario_dim),
|
|
1009
1053
|
name=f'{name}->{self.label_full}',
|
|
1010
1054
|
),
|
|
1011
1055
|
name,
|
|
@@ -1013,7 +1057,7 @@ class ShareAllocationModel(Model):
|
|
|
1013
1057
|
self.share_constraints[name] = self.add(
|
|
1014
1058
|
self._model.add_constraints(self.shares[name] == expression, name=f'{name}->{self.label_full}'), name
|
|
1015
1059
|
)
|
|
1016
|
-
if
|
|
1060
|
+
if not has_time_dim:
|
|
1017
1061
|
self._eq_total.lhs -= self.shares[name]
|
|
1018
1062
|
else:
|
|
1019
1063
|
self._eq_total_per_timestep.lhs -= self.shares[name]
|
|
@@ -1042,7 +1086,12 @@ class PiecewiseEffectsModel(Model):
|
|
|
1042
1086
|
|
|
1043
1087
|
def do_modeling(self):
|
|
1044
1088
|
self.shares = {
|
|
1045
|
-
effect: self.add(
|
|
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
|
+
)
|
|
1046
1095
|
for effect in self._piecewise_shares
|
|
1047
1096
|
}
|
|
1048
1097
|
|