flixopt 2.2.0rc2__py3-none-any.whl → 3.0.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.
- flixopt/__init__.py +33 -4
- flixopt/aggregation.py +60 -80
- flixopt/calculation.py +395 -178
- flixopt/commons.py +1 -10
- flixopt/components.py +939 -448
- flixopt/config.py +553 -191
- flixopt/core.py +513 -846
- flixopt/effects.py +644 -178
- flixopt/elements.py +610 -355
- flixopt/features.py +394 -966
- flixopt/flow_system.py +736 -219
- flixopt/interface.py +1104 -302
- flixopt/io.py +103 -79
- flixopt/linear_converters.py +387 -95
- flixopt/modeling.py +759 -0
- flixopt/network_app.py +73 -39
- flixopt/plotting.py +294 -138
- flixopt/results.py +1253 -299
- flixopt/solvers.py +25 -21
- flixopt/structure.py +938 -396
- flixopt/utils.py +38 -12
- flixopt-3.0.0.dist-info/METADATA +209 -0
- flixopt-3.0.0.dist-info/RECORD +26 -0
- flixopt-3.0.0.dist-info/top_level.txt +1 -0
- docs/examples/00-Minimal Example.md +0 -5
- docs/examples/01-Basic Example.md +0 -5
- docs/examples/02-Complex Example.md +0 -10
- docs/examples/03-Calculation Modes.md +0 -5
- docs/examples/index.md +0 -5
- docs/faq/contribute.md +0 -61
- docs/faq/index.md +0 -3
- docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
- docs/images/architecture_flixOpt.png +0 -0
- docs/images/flixopt-icon.svg +0 -1
- docs/javascripts/mathjax.js +0 -18
- docs/user-guide/Mathematical Notation/Bus.md +0 -33
- docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +0 -132
- docs/user-guide/Mathematical Notation/Flow.md +0 -26
- docs/user-guide/Mathematical Notation/LinearConverter.md +0 -21
- docs/user-guide/Mathematical Notation/Piecewise.md +0 -49
- docs/user-guide/Mathematical Notation/Storage.md +0 -44
- docs/user-guide/Mathematical Notation/index.md +0 -22
- docs/user-guide/Mathematical Notation/others.md +0 -3
- docs/user-guide/index.md +0 -124
- flixopt/config.yaml +0 -10
- flixopt-2.2.0rc2.dist-info/METADATA +0 -167
- flixopt-2.2.0rc2.dist-info/RECORD +0 -54
- flixopt-2.2.0rc2.dist-info/top_level.txt +0 -5
- pics/architecture_flixOpt-pre2.0.0.png +0 -0
- pics/architecture_flixOpt.png +0 -0
- pics/flixOpt_plotting.jpg +0 -0
- pics/flixopt-icon.svg +0 -1
- pics/pics.pptx +0 -0
- scripts/extract_release_notes.py +0 -45
- scripts/gen_ref_pages.py +0 -54
- tests/ressources/Zeitreihen2020.csv +0 -35137
- {flixopt-2.2.0rc2.dist-info → flixopt-3.0.0.dist-info}/WHEEL +0 -0
- {flixopt-2.2.0rc2.dist-info → flixopt-3.0.0.dist-info}/licenses/LICENSE +0 -0
flixopt/features.py
CHANGED
|
@@ -3,1054 +3,482 @@ This module contains the features of the flixopt framework.
|
|
|
3
3
|
Features extend the functionality of Elements.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
6
8
|
import logging
|
|
7
|
-
from typing import
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
8
10
|
|
|
9
11
|
import linopy
|
|
10
12
|
import numpy as np
|
|
11
13
|
|
|
12
|
-
from . import
|
|
13
|
-
from .
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
from .
|
|
14
|
+
from .modeling import BoundingPatterns, ModelingPrimitives, ModelingUtilities
|
|
15
|
+
from .structure import FlowSystemModel, Submodel
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from .core import FlowSystemDimensions, Scalar, TemporalData
|
|
19
|
+
from .interface import InvestParameters, OnOffParameters, Piecewise
|
|
17
20
|
|
|
18
21
|
logger = logging.getLogger('flixopt')
|
|
19
22
|
|
|
20
23
|
|
|
21
|
-
class InvestmentModel(
|
|
22
|
-
"""
|
|
24
|
+
class InvestmentModel(Submodel):
|
|
25
|
+
"""
|
|
26
|
+
This feature model is used to model the investment of a variable.
|
|
27
|
+
It applies the corresponding bounds to the variable and the on/off state of the variable.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
model: The optimization model instance
|
|
31
|
+
label_of_element: The label of the parent (Element). Used to construct the full label of the model.
|
|
32
|
+
parameters: The parameters of the feature model.
|
|
33
|
+
label_of_model: The label of the model. This is needed to construct the full label of the model.
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
parameters: InvestParameters
|
|
23
38
|
|
|
24
39
|
def __init__(
|
|
25
40
|
self,
|
|
26
|
-
model:
|
|
41
|
+
model: FlowSystemModel,
|
|
27
42
|
label_of_element: str,
|
|
28
43
|
parameters: InvestParameters,
|
|
29
|
-
|
|
30
|
-
relative_bounds_of_defining_variable: Tuple[NumericData, NumericData],
|
|
31
|
-
label: Optional[str] = None,
|
|
32
|
-
on_variable: Optional[linopy.Variable] = None,
|
|
44
|
+
label_of_model: str | None = None,
|
|
33
45
|
):
|
|
34
|
-
|
|
35
|
-
self.
|
|
36
|
-
|
|
46
|
+
self.piecewise_effects: PiecewiseEffectsModel | None = None
|
|
47
|
+
self.parameters = parameters
|
|
48
|
+
super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model)
|
|
37
49
|
|
|
38
|
-
|
|
50
|
+
def _do_modeling(self):
|
|
51
|
+
super()._do_modeling()
|
|
52
|
+
self._create_variables_and_constraints()
|
|
53
|
+
self._add_effects()
|
|
39
54
|
|
|
40
|
-
|
|
41
|
-
self.
|
|
42
|
-
self.
|
|
43
|
-
|
|
55
|
+
def _create_variables_and_constraints(self):
|
|
56
|
+
size_min, size_max = (self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size)
|
|
57
|
+
if self.parameters.linked_periods is not None:
|
|
58
|
+
# Mask size bounds: linked_periods is a binary DataArray that zeros out non-linked periods
|
|
59
|
+
size_min = size_min * self.parameters.linked_periods
|
|
60
|
+
size_max = size_max * self.parameters.linked_periods
|
|
44
61
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
self.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
self.add_variables(
|
|
63
|
+
short_name='size',
|
|
64
|
+
lower=size_min if self.parameters.mandatory else 0,
|
|
65
|
+
upper=size_max,
|
|
66
|
+
coords=self._model.get_coords(['period', 'scenario']),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if not self.parameters.mandatory:
|
|
70
|
+
self.add_variables(
|
|
71
|
+
binary=True,
|
|
72
|
+
coords=self._model.get_coords(['period', 'scenario']),
|
|
73
|
+
short_name='invested',
|
|
52
74
|
)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
self.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
name=f'{self.label_full}|size',
|
|
59
|
-
),
|
|
60
|
-
'size',
|
|
75
|
+
BoundingPatterns.bounds_with_state(
|
|
76
|
+
self,
|
|
77
|
+
variable=self.size,
|
|
78
|
+
variable_state=self._variables['invested'],
|
|
79
|
+
bounds=(self.parameters.minimum_or_fixed_size, self.parameters.maximum_or_fixed_size),
|
|
61
80
|
)
|
|
62
81
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
self.
|
|
66
|
-
|
|
82
|
+
if self.parameters.linked_periods is not None:
|
|
83
|
+
masked_size = self.size.where(self.parameters.linked_periods, drop=True)
|
|
84
|
+
self.add_constraints(
|
|
85
|
+
masked_size.isel(period=slice(None, -1)) == masked_size.isel(period=slice(1, None)),
|
|
86
|
+
short_name='linked_periods',
|
|
67
87
|
)
|
|
68
88
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
self._create_bounds_for_defining_variable()
|
|
73
|
-
|
|
74
|
-
self._create_shares()
|
|
75
|
-
|
|
76
|
-
def _create_shares(self):
|
|
77
|
-
# fix_effects:
|
|
78
|
-
fix_effects = self.parameters.fix_effects
|
|
79
|
-
if fix_effects != {}:
|
|
89
|
+
def _add_effects(self):
|
|
90
|
+
"""Add investment effects"""
|
|
91
|
+
if self.parameters.effects_of_investment:
|
|
80
92
|
self._model.effects.add_share_to_effects(
|
|
81
93
|
name=self.label_of_element,
|
|
82
94
|
expressions={
|
|
83
|
-
effect: self.
|
|
84
|
-
for effect, factor in
|
|
95
|
+
effect: self.invested * factor if self.invested is not None else factor
|
|
96
|
+
for effect, factor in self.parameters.effects_of_investment.items()
|
|
85
97
|
},
|
|
86
|
-
target='
|
|
98
|
+
target='periodic',
|
|
87
99
|
)
|
|
88
100
|
|
|
89
|
-
if self.parameters.
|
|
90
|
-
# share: divest_effects - isInvested * divest_effects
|
|
101
|
+
if self.parameters.effects_of_retirement and not self.parameters.mandatory:
|
|
91
102
|
self._model.effects.add_share_to_effects(
|
|
92
103
|
name=self.label_of_element,
|
|
93
104
|
expressions={
|
|
94
|
-
effect: -self.
|
|
95
|
-
for effect, factor in self.parameters.
|
|
105
|
+
effect: -self.invested * factor + factor
|
|
106
|
+
for effect, factor in self.parameters.effects_of_retirement.items()
|
|
96
107
|
},
|
|
97
|
-
target='
|
|
108
|
+
target='periodic',
|
|
98
109
|
)
|
|
99
110
|
|
|
100
|
-
if self.parameters.
|
|
111
|
+
if self.parameters.effects_of_investment_per_size:
|
|
101
112
|
self._model.effects.add_share_to_effects(
|
|
102
113
|
name=self.label_of_element,
|
|
103
|
-
expressions={
|
|
104
|
-
|
|
114
|
+
expressions={
|
|
115
|
+
effect: self.size * factor
|
|
116
|
+
for effect, factor in self.parameters.effects_of_investment_per_size.items()
|
|
117
|
+
},
|
|
118
|
+
target='periodic',
|
|
105
119
|
)
|
|
106
120
|
|
|
107
|
-
if self.parameters.
|
|
108
|
-
self.piecewise_effects = self.
|
|
121
|
+
if self.parameters.piecewise_effects_of_investment:
|
|
122
|
+
self.piecewise_effects = self.add_submodels(
|
|
109
123
|
PiecewiseEffectsModel(
|
|
110
124
|
model=self._model,
|
|
111
125
|
label_of_element=self.label_of_element,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
'segments',
|
|
117
|
-
)
|
|
118
|
-
self.piecewise_effects.do_modeling()
|
|
119
|
-
|
|
120
|
-
def _create_bounds_for_optional_investment(self):
|
|
121
|
-
if self.parameters.fixed_size:
|
|
122
|
-
# eq: investment_size = isInvested * fixed_size
|
|
123
|
-
self.add(
|
|
124
|
-
self._model.add_constraints(
|
|
125
|
-
self.size == self.is_invested * self.parameters.fixed_size, name=f'{self.label_full}|is_invested'
|
|
126
|
-
),
|
|
127
|
-
'is_invested',
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
else:
|
|
131
|
-
# eq1: P_invest <= isInvested * investSize_max
|
|
132
|
-
self.add(
|
|
133
|
-
self._model.add_constraints(
|
|
134
|
-
self.size <= self.is_invested * self.parameters.maximum_size,
|
|
135
|
-
name=f'{self.label_full}|is_invested_ub',
|
|
136
|
-
),
|
|
137
|
-
'is_invested_ub',
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
# eq2: P_invest >= isInvested * max(epsilon, investSize_min)
|
|
141
|
-
self.add(
|
|
142
|
-
self._model.add_constraints(
|
|
143
|
-
self.size >= self.is_invested * np.maximum(CONFIG.modeling.EPSILON, self.parameters.minimum_size),
|
|
144
|
-
name=f'{self.label_full}|is_invested_lb',
|
|
145
|
-
),
|
|
146
|
-
'is_invested_lb',
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
def _create_bounds_for_defining_variable(self):
|
|
150
|
-
variable = self._defining_variable
|
|
151
|
-
lb_relative, ub_relative = self._relative_bounds_of_defining_variable
|
|
152
|
-
if np.all(lb_relative == ub_relative):
|
|
153
|
-
self.add(
|
|
154
|
-
self._model.add_constraints(
|
|
155
|
-
variable == self.size * ub_relative, name=f'{self.label_full}|fix_{variable.name}'
|
|
156
|
-
),
|
|
157
|
-
f'fix_{variable.name}',
|
|
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
|
-
)
|
|
164
|
-
return
|
|
165
|
-
|
|
166
|
-
# eq: defining_variable(t) <= size * upper_bound(t)
|
|
167
|
-
self.add(
|
|
168
|
-
self._model.add_constraints(
|
|
169
|
-
variable <= self.size * ub_relative, name=f'{self.label_full}|ub_{variable.name}'
|
|
170
|
-
),
|
|
171
|
-
f'ub_{variable.name}',
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
if self._on_variable is None:
|
|
175
|
-
# eq: defining_variable(t) >= investment_size * relative_minimum(t)
|
|
176
|
-
self.add(
|
|
177
|
-
self._model.add_constraints(
|
|
178
|
-
variable >= self.size * lb_relative, name=f'{self.label_full}|lb_{variable.name}'
|
|
179
|
-
),
|
|
180
|
-
f'lb_{variable.name}',
|
|
181
|
-
)
|
|
182
|
-
else:
|
|
183
|
-
## 2. Gleichung: Minimum durch Investmentgröße und On
|
|
184
|
-
# eq: defining_variable(t) >= mega * (On(t)-1) + size * relative_minimum(t)
|
|
185
|
-
# ... mit mega = relative_maximum * maximum_size
|
|
186
|
-
# äquivalent zu:.
|
|
187
|
-
# eq: - defining_variable(t) + mega * On(t) + size * relative_minimum(t) <= + mega
|
|
188
|
-
mega = lb_relative * self.parameters.maximum_size
|
|
189
|
-
on = self._on_variable
|
|
190
|
-
self.add(
|
|
191
|
-
self._model.add_constraints(
|
|
192
|
-
variable >= mega * (on - 1) + self.size * lb_relative, name=f'{self.label_full}|lb_{variable.name}'
|
|
193
|
-
),
|
|
194
|
-
f'lb_{variable.name}',
|
|
195
|
-
)
|
|
196
|
-
# anmerkung: Glg bei Spezialfall relative_minimum = 0 redundant zu OnOff ??
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
class StateModel(Model):
|
|
200
|
-
"""
|
|
201
|
-
Handles basic on/off binary states for defining variables
|
|
202
|
-
"""
|
|
203
|
-
|
|
204
|
-
def __init__(
|
|
205
|
-
self,
|
|
206
|
-
model: SystemModel,
|
|
207
|
-
label_of_element: str,
|
|
208
|
-
defining_variables: List[linopy.Variable],
|
|
209
|
-
defining_bounds: List[Tuple[NumericData, NumericData]],
|
|
210
|
-
previous_values: List[Optional[NumericData]] = None,
|
|
211
|
-
use_off: bool = True,
|
|
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,
|
|
215
|
-
label: Optional[str] = None,
|
|
216
|
-
):
|
|
217
|
-
"""
|
|
218
|
-
Models binary state variables based on a continous variable.
|
|
219
|
-
|
|
220
|
-
Args:
|
|
221
|
-
model: The SystemModel that is used to create the model.
|
|
222
|
-
label_of_element: The label of the parent (Element). Used to construct the full label of the model.
|
|
223
|
-
defining_variables: List of Variables that are used to define the state
|
|
224
|
-
defining_bounds: List of Tuples, defining the absolute bounds of each defining variable
|
|
225
|
-
previous_values: List of previous values of the defining variables
|
|
226
|
-
use_off: Whether to use the off state or not
|
|
227
|
-
on_hours_total_min: min. overall sum of operating hours.
|
|
228
|
-
on_hours_total_max: max. overall sum of operating hours.
|
|
229
|
-
effects_per_running_hour: Costs per operating hours
|
|
230
|
-
label: Label of the OnOffModel
|
|
231
|
-
"""
|
|
232
|
-
super().__init__(model, label_of_element, label)
|
|
233
|
-
assert len(defining_variables) == len(defining_bounds), 'Every defining Variable needs bounds to Model OnOff'
|
|
234
|
-
self._defining_variables = defining_variables
|
|
235
|
-
self._defining_bounds = defining_bounds
|
|
236
|
-
self._previous_values = previous_values or []
|
|
237
|
-
self._on_hours_total_min = on_hours_total_min if on_hours_total_min is not None else 0
|
|
238
|
-
self._on_hours_total_max = on_hours_total_max if on_hours_total_max is not None else np.inf
|
|
239
|
-
self._use_off = use_off
|
|
240
|
-
self._effects_per_running_hour = effects_per_running_hour or {}
|
|
241
|
-
|
|
242
|
-
self.on = None
|
|
243
|
-
self.total_on_hours: Optional[linopy.Variable] = None
|
|
244
|
-
self.off = None
|
|
245
|
-
|
|
246
|
-
def do_modeling(self):
|
|
247
|
-
self.on = self.add(
|
|
248
|
-
self._model.add_variables(
|
|
249
|
-
name=f'{self.label_full}|on',
|
|
250
|
-
binary=True,
|
|
251
|
-
coords=self._model.coords,
|
|
252
|
-
),
|
|
253
|
-
'on',
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
self.total_on_hours = self.add(
|
|
257
|
-
self._model.add_variables(
|
|
258
|
-
lower=self._on_hours_total_min,
|
|
259
|
-
upper=self._on_hours_total_max,
|
|
260
|
-
coords=None,
|
|
261
|
-
name=f'{self.label_full}|on_hours_total',
|
|
262
|
-
),
|
|
263
|
-
'on_hours_total',
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
self.add(
|
|
267
|
-
self._model.add_constraints(
|
|
268
|
-
self.total_on_hours == (self.on * self._model.hours_per_step).sum(),
|
|
269
|
-
name=f'{self.label_full}|on_hours_total',
|
|
270
|
-
),
|
|
271
|
-
'on_hours_total',
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
# Add defining constraints for each variable
|
|
275
|
-
self._add_defining_constraints()
|
|
276
|
-
|
|
277
|
-
if self._use_off:
|
|
278
|
-
self.off = self.add(
|
|
279
|
-
self._model.add_variables(
|
|
280
|
-
name=f'{self.label_full}|off',
|
|
281
|
-
binary=True,
|
|
282
|
-
coords=self._model.coords,
|
|
283
|
-
),
|
|
284
|
-
'off',
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
# Constraint: on + off = 1
|
|
288
|
-
self.add(self._model.add_constraints(self.on + self.off == 1, name=f'{self.label_full}|off'), 'off')
|
|
289
|
-
|
|
290
|
-
return self
|
|
291
|
-
|
|
292
|
-
def _add_defining_constraints(self):
|
|
293
|
-
"""Add constraints that link defining variables to the on state"""
|
|
294
|
-
nr_of_def_vars = len(self._defining_variables)
|
|
295
|
-
|
|
296
|
-
if nr_of_def_vars == 1:
|
|
297
|
-
# Case for a single defining variable
|
|
298
|
-
def_var = self._defining_variables[0]
|
|
299
|
-
lb, ub = self._defining_bounds[0]
|
|
300
|
-
|
|
301
|
-
# Constraint: on * lower_bound <= def_var
|
|
302
|
-
self.add(
|
|
303
|
-
self._model.add_constraints(
|
|
304
|
-
self.on * np.maximum(CONFIG.modeling.EPSILON, lb) <= def_var, name=f'{self.label_full}|on_con1'
|
|
305
|
-
),
|
|
306
|
-
'on_con1',
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
# Constraint: on * upper_bound >= def_var
|
|
310
|
-
self.add(self._model.add_constraints(self.on * ub >= def_var, name=f'{self.label_full}|on_con2'), 'on_con2')
|
|
311
|
-
else:
|
|
312
|
-
# Case for multiple defining variables
|
|
313
|
-
ub = sum(bound[1] for bound in self._defining_bounds) / nr_of_def_vars
|
|
314
|
-
lb = CONFIG.modeling.EPSILON # TODO: Can this be a bigger value? (maybe the smallest bound?)
|
|
315
|
-
|
|
316
|
-
# Constraint: on * epsilon <= sum(all_defining_variables)
|
|
317
|
-
self.add(
|
|
318
|
-
self._model.add_constraints(
|
|
319
|
-
self.on * lb <= sum(self._defining_variables), name=f'{self.label_full}|on_con1'
|
|
320
|
-
),
|
|
321
|
-
'on_con1',
|
|
322
|
-
)
|
|
323
|
-
|
|
324
|
-
# Constraint to ensure all variables are zero when off.
|
|
325
|
-
# Divide by nr_of_def_vars to improve numerical stability (smaller factors)
|
|
326
|
-
self.add(
|
|
327
|
-
self._model.add_constraints(
|
|
328
|
-
self.on * ub >= sum([def_var / nr_of_def_vars for def_var in self._defining_variables]),
|
|
329
|
-
name=f'{self.label_full}|on_con2',
|
|
126
|
+
label_of_model=f'{self.label_of_element}|PiecewiseEffects',
|
|
127
|
+
piecewise_origin=(self.size.name, self.parameters.piecewise_effects_of_investment.piecewise_origin),
|
|
128
|
+
piecewise_shares=self.parameters.piecewise_effects_of_investment.piecewise_shares,
|
|
129
|
+
zero_point=self.invested,
|
|
330
130
|
),
|
|
331
|
-
'
|
|
131
|
+
short_name='segments',
|
|
332
132
|
)
|
|
333
133
|
|
|
334
134
|
@property
|
|
335
|
-
def
|
|
336
|
-
"""
|
|
337
|
-
return
|
|
135
|
+
def size(self) -> linopy.Variable:
|
|
136
|
+
"""Investment size variable"""
|
|
137
|
+
return self._variables['size']
|
|
338
138
|
|
|
339
139
|
@property
|
|
340
|
-
def
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
return 1 - self.previous_states
|
|
346
|
-
|
|
347
|
-
@staticmethod
|
|
348
|
-
def compute_previous_states(previous_values: List[NumericData], epsilon: float = 1e-5) -> np.ndarray:
|
|
349
|
-
"""Computes the previous states {0, 1} of defining variables as a binary array from their previous values."""
|
|
350
|
-
if not previous_values or all([val is None for val in previous_values]):
|
|
351
|
-
return np.array([0])
|
|
352
|
-
|
|
353
|
-
# Convert to 2D-array and compute binary on/off states
|
|
354
|
-
previous_values = np.array([values for values in previous_values if values is not None]) # Filter out None
|
|
355
|
-
if previous_values.ndim > 1:
|
|
356
|
-
return np.any(~np.isclose(previous_values, 0, atol=epsilon), axis=0).astype(int)
|
|
357
|
-
|
|
358
|
-
return (~np.isclose(previous_values, 0, atol=epsilon)).astype(int)
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
class SwitchStateModel(Model):
|
|
362
|
-
"""
|
|
363
|
-
Handles switch on/off transitions
|
|
364
|
-
"""
|
|
365
|
-
|
|
366
|
-
def __init__(
|
|
367
|
-
self,
|
|
368
|
-
model: SystemModel,
|
|
369
|
-
label_of_element: str,
|
|
370
|
-
state_variable: linopy.Variable,
|
|
371
|
-
previous_state=0,
|
|
372
|
-
switch_on_max: Optional[Scalar] = None,
|
|
373
|
-
label: Optional[str] = None,
|
|
374
|
-
):
|
|
375
|
-
super().__init__(model, label_of_element, label)
|
|
376
|
-
self._state_variable = state_variable
|
|
377
|
-
self.previous_state = previous_state
|
|
378
|
-
self._switch_on_max = switch_on_max if switch_on_max is not None else np.inf
|
|
379
|
-
|
|
380
|
-
self.switch_on = None
|
|
381
|
-
self.switch_off = None
|
|
382
|
-
self.switch_on_nr = None
|
|
383
|
-
|
|
384
|
-
def do_modeling(self):
|
|
385
|
-
"""Create switch variables and constraints"""
|
|
386
|
-
|
|
387
|
-
# Create switch variables
|
|
388
|
-
self.switch_on = self.add(
|
|
389
|
-
self._model.add_variables(binary=True, name=f'{self.label_full}|switch_on', coords=self._model.coords),
|
|
390
|
-
'switch_on',
|
|
391
|
-
)
|
|
140
|
+
def invested(self) -> linopy.Variable | None:
|
|
141
|
+
"""Binary investment decision variable"""
|
|
142
|
+
if 'invested' not in self._variables:
|
|
143
|
+
return None
|
|
144
|
+
return self._variables['invested']
|
|
392
145
|
|
|
393
|
-
self.switch_off = self.add(
|
|
394
|
-
self._model.add_variables(binary=True, name=f'{self.label_full}|switch_off', coords=self._model.coords),
|
|
395
|
-
'switch_off',
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
# Create count variable for number of switches
|
|
399
|
-
self.switch_on_nr = self.add(
|
|
400
|
-
self._model.add_variables(
|
|
401
|
-
upper=self._switch_on_max,
|
|
402
|
-
lower=0,
|
|
403
|
-
name=f'{self.label_full}|switch_on_nr',
|
|
404
|
-
),
|
|
405
|
-
'switch_on_nr',
|
|
406
|
-
)
|
|
407
|
-
|
|
408
|
-
# Add switch constraints for all entries after the first timestep
|
|
409
|
-
self.add(
|
|
410
|
-
self._model.add_constraints(
|
|
411
|
-
self.switch_on.isel(time=slice(1, None)) - self.switch_off.isel(time=slice(1, None))
|
|
412
|
-
== self._state_variable.isel(time=slice(1, None)) - self._state_variable.isel(time=slice(None, -1)),
|
|
413
|
-
name=f'{self.label_full}|switch_con',
|
|
414
|
-
),
|
|
415
|
-
'switch_con',
|
|
416
|
-
)
|
|
417
|
-
|
|
418
|
-
# Initial switch constraint
|
|
419
|
-
self.add(
|
|
420
|
-
self._model.add_constraints(
|
|
421
|
-
self.switch_on.isel(time=0) - self.switch_off.isel(time=0)
|
|
422
|
-
== self._state_variable.isel(time=0) - self.previous_state,
|
|
423
|
-
name=f'{self.label_full}|initial_switch_con',
|
|
424
|
-
),
|
|
425
|
-
'initial_switch_con',
|
|
426
|
-
)
|
|
427
146
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
self._model.add_constraints(
|
|
431
|
-
self.switch_on + self.switch_off <= 1.1, name=f'{self.label_full}|switch_on_or_off'
|
|
432
|
-
),
|
|
433
|
-
'switch_on_or_off',
|
|
434
|
-
)
|
|
435
|
-
|
|
436
|
-
# Total switch-on count constraint
|
|
437
|
-
self.add(
|
|
438
|
-
self._model.add_constraints(
|
|
439
|
-
self.switch_on_nr == self.switch_on.sum('time'), name=f'{self.label_full}|switch_on_nr'
|
|
440
|
-
),
|
|
441
|
-
'switch_on_nr',
|
|
442
|
-
)
|
|
443
|
-
|
|
444
|
-
return self
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
class ConsecutiveStateModel(Model):
|
|
448
|
-
"""
|
|
449
|
-
Handles tracking consecutive durations in a state
|
|
450
|
-
"""
|
|
147
|
+
class OnOffModel(Submodel):
|
|
148
|
+
"""OnOff model using factory patterns"""
|
|
451
149
|
|
|
452
150
|
def __init__(
|
|
453
151
|
self,
|
|
454
|
-
model:
|
|
152
|
+
model: FlowSystemModel,
|
|
455
153
|
label_of_element: str,
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
label: Optional[str] = None,
|
|
154
|
+
parameters: OnOffParameters,
|
|
155
|
+
on_variable: linopy.Variable,
|
|
156
|
+
previous_states: TemporalData | None,
|
|
157
|
+
label_of_model: str | None = None,
|
|
461
158
|
):
|
|
462
159
|
"""
|
|
463
|
-
|
|
160
|
+
This feature model is used to model the on/off state of flow_rate(s). It does not matter of the flow_rates are
|
|
161
|
+
bounded by a size variable or by a hard bound. THe used bound here is the absolute highest/lowest bound!
|
|
464
162
|
|
|
465
163
|
Args:
|
|
466
|
-
model: The
|
|
164
|
+
model: The optimization model instance
|
|
467
165
|
label_of_element: The label of the parent (Element). Used to construct the full label of the model.
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
label: The label of the model. Used to construct the full label of the model.
|
|
166
|
+
parameters: The parameters of the feature model.
|
|
167
|
+
on_variable: The variable that determines the on state
|
|
168
|
+
previous_states: The previous flow_rates
|
|
169
|
+
label_of_model: The label of the model. This is needed to construct the full label of the model.
|
|
473
170
|
"""
|
|
474
|
-
|
|
475
|
-
self._state_variable = state_variable
|
|
171
|
+
self.on = on_variable
|
|
476
172
|
self._previous_states = previous_states
|
|
477
|
-
self.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if isinstance(self._minimum_duration, TimeSeries):
|
|
481
|
-
self._minimum_duration = self._minimum_duration.active_data
|
|
482
|
-
if isinstance(self._maximum_duration, TimeSeries):
|
|
483
|
-
self._maximum_duration = self._maximum_duration.active_data
|
|
484
|
-
|
|
485
|
-
self.duration = None
|
|
486
|
-
|
|
487
|
-
def do_modeling(self):
|
|
488
|
-
"""Create consecutive duration variables and constraints"""
|
|
489
|
-
# Get the hours per step
|
|
490
|
-
hours_per_step = self._model.hours_per_step
|
|
491
|
-
mega = hours_per_step.sum('time') + self.previous_duration
|
|
492
|
-
|
|
493
|
-
# Create the duration variable
|
|
494
|
-
self.duration = self.add(
|
|
495
|
-
self._model.add_variables(
|
|
496
|
-
lower=0,
|
|
497
|
-
upper=self._maximum_duration if self._maximum_duration is not None else mega,
|
|
498
|
-
coords=self._model.coords,
|
|
499
|
-
name=f'{self.label_full}|hours',
|
|
500
|
-
),
|
|
501
|
-
'hours',
|
|
502
|
-
)
|
|
503
|
-
|
|
504
|
-
# Add constraints
|
|
505
|
-
|
|
506
|
-
# Upper bound constraint
|
|
507
|
-
self.add(
|
|
508
|
-
self._model.add_constraints(self.duration <= self._state_variable * mega, name=f'{self.label_full}|con1'),
|
|
509
|
-
'con1',
|
|
510
|
-
)
|
|
511
|
-
|
|
512
|
-
# Forward constraint
|
|
513
|
-
self.add(
|
|
514
|
-
self._model.add_constraints(
|
|
515
|
-
self.duration.isel(time=slice(1, None))
|
|
516
|
-
<= self.duration.isel(time=slice(None, -1)) + hours_per_step.isel(time=slice(None, -1)),
|
|
517
|
-
name=f'{self.label_full}|con2a',
|
|
518
|
-
),
|
|
519
|
-
'con2a',
|
|
520
|
-
)
|
|
521
|
-
|
|
522
|
-
# Backward constraint
|
|
523
|
-
self.add(
|
|
524
|
-
self._model.add_constraints(
|
|
525
|
-
self.duration.isel(time=slice(1, None))
|
|
526
|
-
>= self.duration.isel(time=slice(None, -1))
|
|
527
|
-
+ hours_per_step.isel(time=slice(None, -1))
|
|
528
|
-
+ (self._state_variable.isel(time=slice(1, None)) - 1) * mega,
|
|
529
|
-
name=f'{self.label_full}|con2b',
|
|
530
|
-
),
|
|
531
|
-
'con2b',
|
|
532
|
-
)
|
|
533
|
-
|
|
534
|
-
# Add minimum duration constraints if specified
|
|
535
|
-
if self._minimum_duration is not None:
|
|
536
|
-
self.add(
|
|
537
|
-
self._model.add_constraints(
|
|
538
|
-
self.duration
|
|
539
|
-
>= (
|
|
540
|
-
self._state_variable.isel(time=slice(None, -1)) - self._state_variable.isel(time=slice(1, None))
|
|
541
|
-
)
|
|
542
|
-
* self._minimum_duration.isel(time=slice(None, -1)),
|
|
543
|
-
name=f'{self.label_full}|minimum',
|
|
544
|
-
),
|
|
545
|
-
'minimum',
|
|
546
|
-
)
|
|
547
|
-
|
|
548
|
-
# Handle initial condition
|
|
549
|
-
if 0 < self.previous_duration < self._minimum_duration.isel(time=0):
|
|
550
|
-
self.add(
|
|
551
|
-
self._model.add_constraints(
|
|
552
|
-
self._state_variable.isel(time=0) == 1, name=f'{self.label_full}|initial_minimum'
|
|
553
|
-
),
|
|
554
|
-
'initial_minimum',
|
|
555
|
-
)
|
|
556
|
-
|
|
557
|
-
# Set initial value
|
|
558
|
-
self.add(
|
|
559
|
-
self._model.add_constraints(
|
|
560
|
-
self.duration.isel(time=0)
|
|
561
|
-
== (hours_per_step.isel(time=0) + self.previous_duration) * self._state_variable.isel(time=0),
|
|
562
|
-
name=f'{self.label_full}|initial',
|
|
563
|
-
),
|
|
564
|
-
'initial',
|
|
565
|
-
)
|
|
566
|
-
|
|
567
|
-
return self
|
|
568
|
-
|
|
569
|
-
@property
|
|
570
|
-
def previous_duration(self) -> Scalar:
|
|
571
|
-
"""Computes the previous duration of the state variable"""
|
|
572
|
-
# TODO: Allow for other/dynamic timestep resolutions
|
|
573
|
-
return ConsecutiveStateModel.compute_consecutive_hours_in_state(
|
|
574
|
-
self._previous_states, self._model.hours_per_step.isel(time=0).item()
|
|
575
|
-
)
|
|
576
|
-
|
|
577
|
-
@staticmethod
|
|
578
|
-
def compute_consecutive_hours_in_state(
|
|
579
|
-
binary_values: NumericData, hours_per_timestep: Union[int, float, np.ndarray]
|
|
580
|
-
) -> Scalar:
|
|
581
|
-
"""
|
|
582
|
-
Computes the final consecutive duration in state 'on' (=1) in hours, from a binary array.
|
|
583
|
-
|
|
584
|
-
Args:
|
|
585
|
-
binary_values: An int or 1D binary array containing only `0`s and `1`s.
|
|
586
|
-
hours_per_timestep: The duration of each timestep in hours.
|
|
587
|
-
If a scalar is provided, it is used for all timesteps.
|
|
588
|
-
If an array is provided, it must be as long as the last consecutive duration in binary_values.
|
|
589
|
-
|
|
590
|
-
Returns:
|
|
591
|
-
The duration of the binary variable in hours.
|
|
592
|
-
|
|
593
|
-
Raises
|
|
594
|
-
------
|
|
595
|
-
TypeError
|
|
596
|
-
If the length of binary_values and dt_in_hours is not equal, but None is a scalar.
|
|
597
|
-
"""
|
|
598
|
-
if np.isscalar(binary_values) and np.isscalar(hours_per_timestep):
|
|
599
|
-
return binary_values * hours_per_timestep
|
|
600
|
-
elif np.isscalar(binary_values) and not np.isscalar(hours_per_timestep):
|
|
601
|
-
return binary_values * hours_per_timestep[-1]
|
|
602
|
-
|
|
603
|
-
if np.isclose(binary_values[-1], 0, atol=CONFIG.modeling.EPSILON):
|
|
604
|
-
return 0
|
|
605
|
-
|
|
606
|
-
if np.isscalar(hours_per_timestep):
|
|
607
|
-
hours_per_timestep = np.ones(len(binary_values)) * hours_per_timestep
|
|
608
|
-
hours_per_timestep: np.ndarray
|
|
609
|
-
|
|
610
|
-
indexes_with_zero_values = np.where(np.isclose(binary_values, 0, atol=CONFIG.modeling.EPSILON))[0]
|
|
611
|
-
if len(indexes_with_zero_values) == 0:
|
|
612
|
-
nr_of_indexes_with_consecutive_ones = len(binary_values)
|
|
613
|
-
else:
|
|
614
|
-
nr_of_indexes_with_consecutive_ones = len(binary_values) - indexes_with_zero_values[-1] - 1
|
|
615
|
-
|
|
616
|
-
if len(hours_per_timestep) < nr_of_indexes_with_consecutive_ones:
|
|
617
|
-
raise ValueError(
|
|
618
|
-
f'When trying to calculate the consecutive duration, the length of the last duration '
|
|
619
|
-
f'({len(nr_of_indexes_with_consecutive_ones)}) is longer than the provided hours_per_timestep ({len(hours_per_timestep)}), '
|
|
620
|
-
f'as {binary_values=}'
|
|
621
|
-
)
|
|
622
|
-
|
|
623
|
-
return np.sum(
|
|
624
|
-
binary_values[-nr_of_indexes_with_consecutive_ones:]
|
|
625
|
-
* hours_per_timestep[-nr_of_indexes_with_consecutive_ones:]
|
|
626
|
-
)
|
|
627
|
-
|
|
173
|
+
self.parameters = parameters
|
|
174
|
+
super().__init__(model, label_of_element, label_of_model=label_of_model)
|
|
628
175
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
Class for modeling the on and off state of a variable
|
|
632
|
-
Uses component models to create a modular implementation
|
|
633
|
-
"""
|
|
176
|
+
def _do_modeling(self):
|
|
177
|
+
super()._do_modeling()
|
|
634
178
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
on_off_parameters: OnOffParameters,
|
|
639
|
-
label_of_element: str,
|
|
640
|
-
defining_variables: List[linopy.Variable],
|
|
641
|
-
defining_bounds: List[Tuple[NumericData, NumericData]],
|
|
642
|
-
previous_values: List[Optional[NumericData]],
|
|
643
|
-
label: Optional[str] = None,
|
|
644
|
-
):
|
|
645
|
-
"""
|
|
646
|
-
Constructor for OnOffModel
|
|
179
|
+
if self.parameters.use_off:
|
|
180
|
+
off = self.add_variables(binary=True, short_name='off', coords=self._model.get_coords())
|
|
181
|
+
self.add_constraints(self.on + off == 1, short_name='complementary')
|
|
647
182
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
self.parameters = on_off_parameters
|
|
659
|
-
self._defining_variables = defining_variables
|
|
660
|
-
self._defining_bounds = defining_bounds
|
|
661
|
-
self._previous_values = previous_values
|
|
662
|
-
|
|
663
|
-
self.state_model = None
|
|
664
|
-
self.switch_state_model = None
|
|
665
|
-
self.consecutive_on_model = None
|
|
666
|
-
self.consecutive_off_model = None
|
|
667
|
-
|
|
668
|
-
def do_modeling(self):
|
|
669
|
-
"""Create all variables and constraints for the OnOffModel"""
|
|
670
|
-
|
|
671
|
-
# Create binary state component
|
|
672
|
-
self.state_model = StateModel(
|
|
673
|
-
model=self._model,
|
|
674
|
-
label_of_element=self.label_of_element,
|
|
675
|
-
defining_variables=self._defining_variables,
|
|
676
|
-
defining_bounds=self._defining_bounds,
|
|
677
|
-
previous_values=self._previous_values,
|
|
678
|
-
use_off=self.parameters.use_off,
|
|
679
|
-
on_hours_total_min=self.parameters.on_hours_total_min,
|
|
680
|
-
on_hours_total_max=self.parameters.on_hours_total_max,
|
|
681
|
-
effects_per_running_hour=self.parameters.effects_per_running_hour,
|
|
183
|
+
# 3. Total duration tracking using existing pattern
|
|
184
|
+
ModelingPrimitives.expression_tracking_variable(
|
|
185
|
+
self,
|
|
186
|
+
tracked_expression=(self.on * self._model.hours_per_step).sum('time'),
|
|
187
|
+
bounds=(
|
|
188
|
+
self.parameters.on_hours_total_min if self.parameters.on_hours_total_min is not None else 0,
|
|
189
|
+
self.parameters.on_hours_total_max if self.parameters.on_hours_total_max is not None else np.inf,
|
|
190
|
+
), # TODO: self._model.hours_per_step.sum('time').item() + self._get_previous_on_duration())
|
|
191
|
+
short_name='on_hours_total',
|
|
192
|
+
coords=['period', 'scenario'],
|
|
682
193
|
)
|
|
683
|
-
self.add(self.state_model)
|
|
684
|
-
self.state_model.do_modeling()
|
|
685
194
|
|
|
686
|
-
#
|
|
195
|
+
# 4. Switch tracking using existing pattern
|
|
687
196
|
if self.parameters.use_switch_on:
|
|
688
|
-
self.
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
197
|
+
self.add_variables(binary=True, short_name='switch|on', coords=self.get_coords())
|
|
198
|
+
self.add_variables(binary=True, short_name='switch|off', coords=self.get_coords())
|
|
199
|
+
|
|
200
|
+
BoundingPatterns.state_transition_bounds(
|
|
201
|
+
self,
|
|
202
|
+
state_variable=self.on,
|
|
203
|
+
switch_on=self.switch_on,
|
|
204
|
+
switch_off=self.switch_off,
|
|
205
|
+
name=f'{self.label_of_model}|switch',
|
|
206
|
+
previous_state=self._previous_states.isel(time=-1) if self._previous_states is not None else 0,
|
|
207
|
+
coord='time',
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if self.parameters.switch_on_total_max is not None:
|
|
211
|
+
count = self.add_variables(
|
|
212
|
+
lower=0,
|
|
213
|
+
upper=self.parameters.switch_on_total_max,
|
|
214
|
+
coords=self._model.get_coords(('period', 'scenario')),
|
|
215
|
+
short_name='switch|count',
|
|
216
|
+
)
|
|
217
|
+
self.add_constraints(count == self.switch_on.sum('time'), short_name='switch|count')
|
|
697
218
|
|
|
698
|
-
#
|
|
219
|
+
# 5. Consecutive on duration using existing pattern
|
|
699
220
|
if self.parameters.use_consecutive_on_hours:
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
221
|
+
ModelingPrimitives.consecutive_duration_tracking(
|
|
222
|
+
self,
|
|
223
|
+
state_variable=self.on,
|
|
224
|
+
short_name='consecutive_on_hours',
|
|
704
225
|
minimum_duration=self.parameters.consecutive_on_hours_min,
|
|
705
226
|
maximum_duration=self.parameters.consecutive_on_hours_max,
|
|
706
|
-
|
|
707
|
-
|
|
227
|
+
duration_per_step=self.hours_per_step,
|
|
228
|
+
duration_dim='time',
|
|
229
|
+
previous_duration=self._get_previous_on_duration(),
|
|
708
230
|
)
|
|
709
|
-
self.add(self.consecutive_on_model)
|
|
710
|
-
self.consecutive_on_model.do_modeling()
|
|
711
231
|
|
|
712
|
-
#
|
|
232
|
+
# 6. Consecutive off duration using existing pattern
|
|
713
233
|
if self.parameters.use_consecutive_off_hours:
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
234
|
+
ModelingPrimitives.consecutive_duration_tracking(
|
|
235
|
+
self,
|
|
236
|
+
state_variable=self.off,
|
|
237
|
+
short_name='consecutive_off_hours',
|
|
718
238
|
minimum_duration=self.parameters.consecutive_off_hours_min,
|
|
719
239
|
maximum_duration=self.parameters.consecutive_off_hours_max,
|
|
720
|
-
|
|
721
|
-
|
|
240
|
+
duration_per_step=self.hours_per_step,
|
|
241
|
+
duration_dim='time',
|
|
242
|
+
previous_duration=self._get_previous_off_duration(),
|
|
722
243
|
)
|
|
723
|
-
|
|
724
|
-
self.consecutive_off_model.do_modeling()
|
|
244
|
+
# TODO:
|
|
725
245
|
|
|
726
|
-
self.
|
|
246
|
+
self._add_effects()
|
|
727
247
|
|
|
728
|
-
def
|
|
248
|
+
def _add_effects(self):
|
|
249
|
+
"""Add operational effects"""
|
|
729
250
|
if self.parameters.effects_per_running_hour:
|
|
730
251
|
self._model.effects.add_share_to_effects(
|
|
731
252
|
name=self.label_of_element,
|
|
732
253
|
expressions={
|
|
733
|
-
effect: self.
|
|
254
|
+
effect: self.on * factor * self._model.hours_per_step
|
|
734
255
|
for effect, factor in self.parameters.effects_per_running_hour.items()
|
|
735
256
|
},
|
|
736
|
-
target='
|
|
257
|
+
target='temporal',
|
|
737
258
|
)
|
|
738
259
|
|
|
739
260
|
if self.parameters.effects_per_switch_on:
|
|
740
261
|
self._model.effects.add_share_to_effects(
|
|
741
262
|
name=self.label_of_element,
|
|
742
263
|
expressions={
|
|
743
|
-
effect: self.
|
|
744
|
-
for effect, factor in self.parameters.effects_per_switch_on.items()
|
|
264
|
+
effect: self.switch_on * factor for effect, factor in self.parameters.effects_per_switch_on.items()
|
|
745
265
|
},
|
|
746
|
-
target='
|
|
266
|
+
target='temporal',
|
|
747
267
|
)
|
|
748
268
|
|
|
269
|
+
# Properties access variables from Submodel's tracking system
|
|
270
|
+
|
|
749
271
|
@property
|
|
750
|
-
def
|
|
751
|
-
|
|
272
|
+
def on_hours_total(self) -> linopy.Variable:
|
|
273
|
+
"""Total on hours variable"""
|
|
274
|
+
return self['on_hours_total']
|
|
752
275
|
|
|
753
276
|
@property
|
|
754
|
-
def off(self):
|
|
755
|
-
|
|
277
|
+
def off(self) -> linopy.Variable | None:
|
|
278
|
+
"""Binary off state variable"""
|
|
279
|
+
return self.get('off')
|
|
756
280
|
|
|
757
281
|
@property
|
|
758
|
-
def switch_on(self):
|
|
759
|
-
|
|
282
|
+
def switch_on(self) -> linopy.Variable | None:
|
|
283
|
+
"""Switch on variable"""
|
|
284
|
+
return self.get('switch|on')
|
|
760
285
|
|
|
761
286
|
@property
|
|
762
|
-
def switch_off(self):
|
|
763
|
-
|
|
287
|
+
def switch_off(self) -> linopy.Variable | None:
|
|
288
|
+
"""Switch off variable"""
|
|
289
|
+
return self.get('switch|off')
|
|
764
290
|
|
|
765
291
|
@property
|
|
766
|
-
def switch_on_nr(self):
|
|
767
|
-
|
|
292
|
+
def switch_on_nr(self) -> linopy.Variable | None:
|
|
293
|
+
"""Number of switch-ons variable"""
|
|
294
|
+
return self.get('switch|count')
|
|
768
295
|
|
|
769
296
|
@property
|
|
770
|
-
def consecutive_on_hours(self):
|
|
771
|
-
|
|
297
|
+
def consecutive_on_hours(self) -> linopy.Variable | None:
|
|
298
|
+
"""Consecutive on hours variable"""
|
|
299
|
+
return self.get('consecutive_on_hours')
|
|
772
300
|
|
|
773
301
|
@property
|
|
774
|
-
def consecutive_off_hours(self):
|
|
775
|
-
|
|
302
|
+
def consecutive_off_hours(self) -> linopy.Variable | None:
|
|
303
|
+
"""Consecutive off hours variable"""
|
|
304
|
+
return self.get('consecutive_off_hours')
|
|
305
|
+
|
|
306
|
+
def _get_previous_on_duration(self):
|
|
307
|
+
"""Get previous on duration. Previously OFF by default, for one timestep"""
|
|
308
|
+
hours_per_step = self._model.hours_per_step.isel(time=0).min().item()
|
|
309
|
+
if self._previous_states is None:
|
|
310
|
+
return 0
|
|
311
|
+
else:
|
|
312
|
+
return ModelingUtilities.compute_consecutive_hours_in_state(self._previous_states, hours_per_step)
|
|
313
|
+
|
|
314
|
+
def _get_previous_off_duration(self):
|
|
315
|
+
"""Get previous off duration. Previously OFF by default, for one timestep"""
|
|
316
|
+
hours_per_step = self._model.hours_per_step.isel(time=0).min().item()
|
|
317
|
+
if self._previous_states is None:
|
|
318
|
+
return hours_per_step
|
|
319
|
+
else:
|
|
320
|
+
return ModelingUtilities.compute_consecutive_hours_in_state(self._previous_states * -1 + 1, hours_per_step)
|
|
776
321
|
|
|
777
322
|
|
|
778
|
-
class PieceModel(
|
|
323
|
+
class PieceModel(Submodel):
|
|
779
324
|
"""Class for modeling a linear piece of one or more variables in parallel"""
|
|
780
325
|
|
|
781
326
|
def __init__(
|
|
782
327
|
self,
|
|
783
|
-
model:
|
|
328
|
+
model: FlowSystemModel,
|
|
784
329
|
label_of_element: str,
|
|
785
|
-
|
|
786
|
-
|
|
330
|
+
label_of_model: str,
|
|
331
|
+
dims: FlowSystemDimensions | None,
|
|
787
332
|
):
|
|
788
|
-
|
|
789
|
-
self.
|
|
790
|
-
self.
|
|
791
|
-
self.
|
|
792
|
-
self._as_time_series = as_time_series
|
|
793
|
-
|
|
794
|
-
def do_modeling(self):
|
|
795
|
-
self.inside_piece = self.add(
|
|
796
|
-
self._model.add_variables(
|
|
797
|
-
binary=True,
|
|
798
|
-
name=f'{self.label_full}|inside_piece',
|
|
799
|
-
coords=self._model.coords if self._as_time_series else None,
|
|
800
|
-
),
|
|
801
|
-
'inside_piece',
|
|
802
|
-
)
|
|
333
|
+
self.inside_piece: linopy.Variable | None = None
|
|
334
|
+
self.lambda0: linopy.Variable | None = None
|
|
335
|
+
self.lambda1: linopy.Variable | None = None
|
|
336
|
+
self.dims = dims
|
|
803
337
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
338
|
+
super().__init__(model, label_of_element, label_of_model)
|
|
339
|
+
|
|
340
|
+
def _do_modeling(self):
|
|
341
|
+
super()._do_modeling()
|
|
342
|
+
self.inside_piece = self.add_variables(
|
|
343
|
+
binary=True,
|
|
344
|
+
short_name='inside_piece',
|
|
345
|
+
coords=self._model.get_coords(dims=self.dims),
|
|
346
|
+
)
|
|
347
|
+
self.lambda0 = self.add_variables(
|
|
348
|
+
lower=0,
|
|
349
|
+
upper=1,
|
|
350
|
+
short_name='lambda0',
|
|
351
|
+
coords=self._model.get_coords(dims=self.dims),
|
|
812
352
|
)
|
|
813
353
|
|
|
814
|
-
self.lambda1 = self.
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
coords=self._model.coords if self._as_time_series else None,
|
|
820
|
-
),
|
|
821
|
-
'lambda1',
|
|
354
|
+
self.lambda1 = self.add_variables(
|
|
355
|
+
lower=0,
|
|
356
|
+
upper=1,
|
|
357
|
+
short_name='lambda1',
|
|
358
|
+
coords=self._model.get_coords(dims=self.dims),
|
|
822
359
|
)
|
|
823
360
|
|
|
824
361
|
# eq: lambda0(t) + lambda1(t) = inside_piece(t)
|
|
825
|
-
self.
|
|
826
|
-
self._model.add_constraints(
|
|
827
|
-
self.inside_piece == self.lambda0 + self.lambda1, name=f'{self.label_full}|inside_piece'
|
|
828
|
-
),
|
|
829
|
-
'inside_piece',
|
|
830
|
-
)
|
|
362
|
+
self.add_constraints(self.inside_piece == self.lambda0 + self.lambda1, short_name='inside_piece')
|
|
831
363
|
|
|
832
364
|
|
|
833
|
-
class PiecewiseModel(
|
|
365
|
+
class PiecewiseModel(Submodel):
|
|
834
366
|
def __init__(
|
|
835
367
|
self,
|
|
836
|
-
model:
|
|
368
|
+
model: FlowSystemModel,
|
|
837
369
|
label_of_element: str,
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
370
|
+
label_of_model: str,
|
|
371
|
+
piecewise_variables: dict[str, Piecewise],
|
|
372
|
+
zero_point: bool | linopy.Variable | None,
|
|
373
|
+
dims: FlowSystemDimensions | None,
|
|
842
374
|
):
|
|
843
375
|
"""
|
|
844
|
-
Modeling a Piecewise relation between
|
|
376
|
+
Modeling a Piecewise relation between miultiple variables.
|
|
845
377
|
The relation is defined by a list of Pieces, which are assigned to the variables.
|
|
846
378
|
Each Piece is a tuple of (start, end).
|
|
847
379
|
|
|
848
380
|
Args:
|
|
849
|
-
model: The
|
|
381
|
+
model: The FlowSystemModel that is used to create the model.
|
|
850
382
|
label_of_element: The label of the parent (Element). Used to construct the full label of the model.
|
|
851
|
-
|
|
383
|
+
label_of_model: The label of the model. Used to construct the full label of the model.
|
|
852
384
|
piecewise_variables: The variables to which the Pieces are assigned.
|
|
853
|
-
zero_point: A variable that can be used to define a zero point for the Piecewise relation.
|
|
854
|
-
|
|
855
|
-
unless its its contained in a Piece.
|
|
856
|
-
as_time_series: Whether the Piecewise relation is defined for a TimeSeries or a single variable.
|
|
385
|
+
zero_point: A variable that can be used to define a zero point for the Piecewise relation. If None or False, no zero point is defined.
|
|
386
|
+
dims: The dimensions used for variable creation. If None, all dimensions are used.
|
|
857
387
|
"""
|
|
858
|
-
super().__init__(model, label_of_element, label)
|
|
859
388
|
self._piecewise_variables = piecewise_variables
|
|
860
389
|
self._zero_point = zero_point
|
|
861
|
-
self.
|
|
390
|
+
self.dims = dims
|
|
391
|
+
|
|
392
|
+
self.pieces: list[PieceModel] = []
|
|
393
|
+
self.zero_point: linopy.Variable | None = None
|
|
394
|
+
super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model)
|
|
862
395
|
|
|
863
|
-
|
|
864
|
-
|
|
396
|
+
def _do_modeling(self):
|
|
397
|
+
super()._do_modeling()
|
|
398
|
+
# Validate all piecewise variables have the same number of segments
|
|
399
|
+
segment_counts = [len(pw) for pw in self._piecewise_variables.values()]
|
|
400
|
+
if not all(count == segment_counts[0] for count in segment_counts):
|
|
401
|
+
raise ValueError(f'All piecewises must have the same number of pieces, got {segment_counts}')
|
|
865
402
|
|
|
866
|
-
def do_modeling(self):
|
|
867
403
|
for i in range(len(list(self._piecewise_variables.values())[0])):
|
|
868
|
-
new_piece = self.
|
|
404
|
+
new_piece = self.add_submodels(
|
|
869
405
|
PieceModel(
|
|
870
406
|
model=self._model,
|
|
871
407
|
label_of_element=self.label_of_element,
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
)
|
|
408
|
+
label_of_model=f'{self.label_of_element}|Piece_{i}',
|
|
409
|
+
dims=self.dims,
|
|
410
|
+
),
|
|
411
|
+
short_name=f'Piece_{i}',
|
|
875
412
|
)
|
|
876
413
|
self.pieces.append(new_piece)
|
|
877
|
-
new_piece.do_modeling()
|
|
878
414
|
|
|
879
415
|
for var_name in self._piecewise_variables:
|
|
880
416
|
variable = self._model.variables[var_name]
|
|
881
|
-
self.
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
]
|
|
891
|
-
),
|
|
892
|
-
name=f'{self.label_full}|{var_name}|lambda',
|
|
417
|
+
self.add_constraints(
|
|
418
|
+
variable
|
|
419
|
+
== sum(
|
|
420
|
+
[
|
|
421
|
+
piece_model.lambda0 * piece_bounds.start + piece_model.lambda1 * piece_bounds.end
|
|
422
|
+
for piece_model, piece_bounds in zip(
|
|
423
|
+
self.pieces, self._piecewise_variables[var_name], strict=False
|
|
424
|
+
)
|
|
425
|
+
]
|
|
893
426
|
),
|
|
894
|
-
f'{var_name}|lambda',
|
|
427
|
+
name=f'{self.label_full}|{var_name}|lambda',
|
|
428
|
+
short_name=f'{var_name}|lambda',
|
|
895
429
|
)
|
|
896
430
|
|
|
897
431
|
# a) eq: Segment1.onSeg(t) + Segment2.onSeg(t) + ... = 1 Aufenthalt nur in Segmenten erlaubt
|
|
898
432
|
# b) eq: -On(t) + Segment1.onSeg(t) + Segment2.onSeg(t) + ... = 0 zusätzlich kann alles auch Null sein
|
|
899
433
|
if isinstance(self._zero_point, linopy.Variable):
|
|
900
434
|
self.zero_point = self._zero_point
|
|
901
|
-
|
|
435
|
+
rhs = self.zero_point
|
|
902
436
|
elif self._zero_point is True:
|
|
903
|
-
self.zero_point = self.
|
|
904
|
-
self._model.
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
'zero_point',
|
|
437
|
+
self.zero_point = self.add_variables(
|
|
438
|
+
coords=self._model.get_coords(self.dims),
|
|
439
|
+
binary=True,
|
|
440
|
+
short_name='zero_point',
|
|
908
441
|
)
|
|
909
|
-
|
|
442
|
+
rhs = self.zero_point
|
|
910
443
|
else:
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
self.add(
|
|
914
|
-
self._model.add_constraints(
|
|
915
|
-
sum([piece.inside_piece for piece in self.pieces]),
|
|
916
|
-
sign,
|
|
917
|
-
rhs,
|
|
918
|
-
name=f'{self.label_full}|{variable.name}|single_segment',
|
|
919
|
-
),
|
|
920
|
-
f'{var_name}|single_segment',
|
|
921
|
-
)
|
|
444
|
+
rhs = 1
|
|
922
445
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
model: SystemModel,
|
|
928
|
-
shares_are_time_series: bool,
|
|
929
|
-
label_of_element: Optional[str] = None,
|
|
930
|
-
label: Optional[str] = None,
|
|
931
|
-
label_full: Optional[str] = None,
|
|
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,
|
|
936
|
-
):
|
|
937
|
-
super().__init__(model, label_of_element=label_of_element, label=label, label_full=label_full)
|
|
938
|
-
if not shares_are_time_series: # If the condition is True
|
|
939
|
-
assert max_per_hour is None and min_per_hour is None, (
|
|
940
|
-
'Both max_per_hour and min_per_hour cannot be used when shares_are_time_series is False'
|
|
941
|
-
)
|
|
942
|
-
self.total_per_timestep: Optional[linopy.Variable] = None
|
|
943
|
-
self.total: Optional[linopy.Variable] = None
|
|
944
|
-
self.shares: Dict[str, linopy.Variable] = {}
|
|
945
|
-
self.share_constraints: Dict[str, linopy.Constraint] = {}
|
|
946
|
-
|
|
947
|
-
self._eq_total_per_timestep: Optional[linopy.Constraint] = None
|
|
948
|
-
self._eq_total: Optional[linopy.Constraint] = None
|
|
949
|
-
|
|
950
|
-
# Parameters
|
|
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
|
|
953
|
-
self._total_min = total_min if total_min is not None else -np.inf
|
|
954
|
-
self._max_per_hour = max_per_hour if max_per_hour is not None else np.inf
|
|
955
|
-
self._min_per_hour = min_per_hour if min_per_hour is not None else -np.inf
|
|
956
|
-
|
|
957
|
-
def do_modeling(self):
|
|
958
|
-
self.total = self.add(
|
|
959
|
-
self._model.add_variables(
|
|
960
|
-
lower=self._total_min, upper=self._total_max, coords=None, name=f'{self.label_full}|total'
|
|
961
|
-
),
|
|
962
|
-
'total',
|
|
963
|
-
)
|
|
964
|
-
# eq: sum = sum(share_i) # skalar
|
|
965
|
-
self._eq_total = self.add(
|
|
966
|
-
self._model.add_constraints(self.total == 0, name=f'{self.label_full}|total'), 'total'
|
|
967
|
-
)
|
|
968
|
-
|
|
969
|
-
if self._shares_are_time_series:
|
|
970
|
-
self.total_per_timestep = self.add(
|
|
971
|
-
self._model.add_variables(
|
|
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,
|
|
979
|
-
name=f'{self.label_full}|total_per_timestep',
|
|
980
|
-
),
|
|
981
|
-
'total_per_timestep',
|
|
982
|
-
)
|
|
983
|
-
|
|
984
|
-
self._eq_total_per_timestep = self.add(
|
|
985
|
-
self._model.add_constraints(self.total_per_timestep == 0, name=f'{self.label_full}|total_per_timestep'),
|
|
986
|
-
'total_per_timestep',
|
|
987
|
-
)
|
|
988
|
-
|
|
989
|
-
# Add it to the total
|
|
990
|
-
self._eq_total.lhs -= self.total_per_timestep.sum()
|
|
991
|
-
|
|
992
|
-
def add_share(
|
|
993
|
-
self,
|
|
994
|
-
name: str,
|
|
995
|
-
expression: linopy.LinearExpression,
|
|
996
|
-
):
|
|
997
|
-
"""
|
|
998
|
-
Add a share to the share allocation model. If the share already exists, the expression is added to the existing share.
|
|
999
|
-
The expression is added to the right hand side (rhs) of the constraint.
|
|
1000
|
-
The variable representing the total share is on the left hand side (lhs) of the constraint.
|
|
1001
|
-
var_total = sum(expressions)
|
|
1002
|
-
|
|
1003
|
-
Args:
|
|
1004
|
-
name: The name of the share.
|
|
1005
|
-
expression: The expression of the share. Added to the right hand side of the constraint.
|
|
1006
|
-
"""
|
|
1007
|
-
if name in self.shares:
|
|
1008
|
-
self.share_constraints[name].lhs -= expression
|
|
1009
|
-
else:
|
|
1010
|
-
self.shares[name] = self.add(
|
|
1011
|
-
self._model.add_variables(
|
|
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,
|
|
1017
|
-
name=f'{name}->{self.label_full}',
|
|
1018
|
-
),
|
|
1019
|
-
name,
|
|
1020
|
-
)
|
|
1021
|
-
self.share_constraints[name] = self.add(
|
|
1022
|
-
self._model.add_constraints(self.shares[name] == expression, name=f'{name}->{self.label_full}'), name
|
|
446
|
+
self.add_constraints(
|
|
447
|
+
sum([piece.inside_piece for piece in self.pieces]) <= rhs,
|
|
448
|
+
name=f'{self.label_full}|{variable.name}|single_segment',
|
|
449
|
+
short_name=f'{var_name}|single_segment',
|
|
1023
450
|
)
|
|
1024
|
-
if self.shares[name].ndim == 0:
|
|
1025
|
-
self._eq_total.lhs -= self.shares[name]
|
|
1026
|
-
else:
|
|
1027
|
-
self._eq_total_per_timestep.lhs -= self.shares[name]
|
|
1028
451
|
|
|
1029
452
|
|
|
1030
|
-
class PiecewiseEffectsModel(
|
|
453
|
+
class PiecewiseEffectsModel(Submodel):
|
|
1031
454
|
def __init__(
|
|
1032
455
|
self,
|
|
1033
|
-
model:
|
|
456
|
+
model: FlowSystemModel,
|
|
1034
457
|
label_of_element: str,
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
458
|
+
label_of_model: str,
|
|
459
|
+
piecewise_origin: tuple[str, Piecewise],
|
|
460
|
+
piecewise_shares: dict[str, Piecewise],
|
|
461
|
+
zero_point: bool | linopy.Variable | None,
|
|
1039
462
|
):
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
463
|
+
origin_count = len(piecewise_origin[1])
|
|
464
|
+
share_counts = [len(pw) for pw in piecewise_shares.values()]
|
|
465
|
+
if not all(count == origin_count for count in share_counts):
|
|
466
|
+
raise ValueError(
|
|
467
|
+
f'Piece count mismatch: piecewise_origin has {origin_count} segments, '
|
|
468
|
+
f'but piecewise_shares have {share_counts}'
|
|
469
|
+
)
|
|
1044
470
|
self._zero_point = zero_point
|
|
1045
471
|
self._piecewise_origin = piecewise_origin
|
|
1046
472
|
self._piecewise_shares = piecewise_shares
|
|
1047
|
-
self.shares:
|
|
473
|
+
self.shares: dict[str, linopy.Variable] = {}
|
|
474
|
+
|
|
475
|
+
self.piecewise_model: PiecewiseModel | None = None
|
|
1048
476
|
|
|
1049
|
-
|
|
477
|
+
super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model)
|
|
1050
478
|
|
|
1051
|
-
def
|
|
479
|
+
def _do_modeling(self):
|
|
1052
480
|
self.shares = {
|
|
1053
|
-
effect: self.
|
|
481
|
+
effect: self.add_variables(coords=self._model.get_coords(['period', 'scenario']), short_name=effect)
|
|
1054
482
|
for effect in self._piecewise_shares
|
|
1055
483
|
}
|
|
1056
484
|
|
|
@@ -1062,124 +490,124 @@ class PiecewiseEffectsModel(Model):
|
|
|
1062
490
|
},
|
|
1063
491
|
}
|
|
1064
492
|
|
|
1065
|
-
self.piecewise_model = self.
|
|
493
|
+
self.piecewise_model = self.add_submodels(
|
|
1066
494
|
PiecewiseModel(
|
|
1067
495
|
model=self._model,
|
|
1068
496
|
label_of_element=self.label_of_element,
|
|
1069
497
|
piecewise_variables=piecewise_variables,
|
|
1070
498
|
zero_point=self._zero_point,
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
)
|
|
499
|
+
dims=('period', 'scenario'),
|
|
500
|
+
label_of_model=f'{self.label_of_element}|PiecewiseEffects',
|
|
501
|
+
),
|
|
502
|
+
short_name='PiecewiseEffects',
|
|
1074
503
|
)
|
|
1075
504
|
|
|
1076
|
-
self.piecewise_model.do_modeling()
|
|
1077
|
-
|
|
1078
505
|
# Shares
|
|
1079
506
|
self._model.effects.add_share_to_effects(
|
|
1080
507
|
name=self.label_of_element,
|
|
1081
508
|
expressions={effect: variable * 1 for effect, variable in self.shares.items()},
|
|
1082
|
-
target='
|
|
509
|
+
target='periodic',
|
|
1083
510
|
)
|
|
1084
511
|
|
|
1085
512
|
|
|
1086
|
-
class
|
|
513
|
+
class ShareAllocationModel(Submodel):
|
|
1087
514
|
def __init__(
|
|
1088
515
|
self,
|
|
1089
|
-
model:
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
516
|
+
model: FlowSystemModel,
|
|
517
|
+
dims: list[FlowSystemDimensions],
|
|
518
|
+
label_of_element: str | None = None,
|
|
519
|
+
label_of_model: str | None = None,
|
|
520
|
+
total_max: Scalar | None = None,
|
|
521
|
+
total_min: Scalar | None = None,
|
|
522
|
+
max_per_hour: TemporalData | None = None,
|
|
523
|
+
min_per_hour: TemporalData | None = None,
|
|
1095
524
|
):
|
|
1096
|
-
|
|
1097
|
-
|
|
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
|
|
525
|
+
if 'time' not in dims and (max_per_hour is not None or min_per_hour is not None):
|
|
526
|
+
raise ValueError('Both max_per_hour and min_per_hour cannot be used when has_time_dim is False')
|
|
1103
527
|
|
|
1104
|
-
self.
|
|
528
|
+
self._dims = dims
|
|
529
|
+
self.total_per_timestep: linopy.Variable | None = None
|
|
530
|
+
self.total: linopy.Variable | None = None
|
|
531
|
+
self.shares: dict[str, linopy.Variable] = {}
|
|
532
|
+
self.share_constraints: dict[str, linopy.Constraint] = {}
|
|
1105
533
|
|
|
1106
|
-
self.
|
|
534
|
+
self._eq_total_per_timestep: linopy.Constraint | None = None
|
|
535
|
+
self._eq_total: linopy.Constraint | None = None
|
|
1107
536
|
|
|
1108
|
-
|
|
1109
|
-
self.
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
for effect in self._piecewise_shares
|
|
1114
|
-
}
|
|
537
|
+
# Parameters
|
|
538
|
+
self._total_max = total_max if total_max is not None else np.inf
|
|
539
|
+
self._total_min = total_min if total_min is not None else -np.inf
|
|
540
|
+
self._max_per_hour = max_per_hour if max_per_hour is not None else np.inf
|
|
541
|
+
self._min_per_hour = min_per_hour if min_per_hour is not None else -np.inf
|
|
1115
542
|
|
|
1116
|
-
|
|
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
|
-
}
|
|
543
|
+
super().__init__(model, label_of_element=label_of_element, label_of_model=label_of_model)
|
|
1123
544
|
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
)
|
|
545
|
+
def _do_modeling(self):
|
|
546
|
+
super()._do_modeling()
|
|
547
|
+
self.total = self.add_variables(
|
|
548
|
+
lower=self._total_min,
|
|
549
|
+
upper=self._total_max,
|
|
550
|
+
coords=self._model.get_coords([dim for dim in self._dims if dim != 'time']),
|
|
551
|
+
name=self.label_full,
|
|
552
|
+
short_name='total',
|
|
1133
553
|
)
|
|
554
|
+
# eq: sum = sum(share_i) # skalar
|
|
555
|
+
self._eq_total = self.add_constraints(self.total == 0, name=self.label_full)
|
|
1134
556
|
|
|
1135
|
-
self.
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
)
|
|
557
|
+
if 'time' in self._dims:
|
|
558
|
+
self.total_per_timestep = self.add_variables(
|
|
559
|
+
lower=-np.inf if (self._min_per_hour is None) else self._min_per_hour * self._model.hours_per_step,
|
|
560
|
+
upper=np.inf if (self._max_per_hour is None) else self._max_per_hour * self._model.hours_per_step,
|
|
561
|
+
coords=self._model.get_coords(self._dims),
|
|
562
|
+
short_name='per_timestep',
|
|
563
|
+
)
|
|
1143
564
|
|
|
565
|
+
self._eq_total_per_timestep = self.add_constraints(self.total_per_timestep == 0, short_name='per_timestep')
|
|
1144
566
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
Prevents multiple Multiple Binary variables from being 1 at the same time
|
|
567
|
+
# Add it to the total
|
|
568
|
+
self._eq_total.lhs -= self.total_per_timestep.sum(dim='time')
|
|
1148
569
|
|
|
1149
|
-
|
|
1150
|
-
|
|
570
|
+
def add_share(
|
|
571
|
+
self,
|
|
572
|
+
name: str,
|
|
573
|
+
expression: linopy.LinearExpression,
|
|
574
|
+
dims: list[FlowSystemDimensions] | None = None,
|
|
575
|
+
):
|
|
576
|
+
"""
|
|
577
|
+
Add a share to the share allocation model. If the share already exists, the expression is added to the existing share.
|
|
578
|
+
The expression is added to the right hand side (rhs) of the constraint.
|
|
579
|
+
The variable representing the total share is on the left hand side (lhs) of the constraint.
|
|
580
|
+
var_total = sum(expressions)
|
|
1151
581
|
|
|
582
|
+
Args:
|
|
583
|
+
name: The name of the share.
|
|
584
|
+
expression: The expression of the share. Added to the right hand side of the constraint.
|
|
585
|
+
dims: The dimensions of the share. Defaults to all dimensions. Dims are ordered automatically
|
|
586
|
+
"""
|
|
587
|
+
if dims is None:
|
|
588
|
+
dims = self._dims
|
|
589
|
+
else:
|
|
590
|
+
if 'time' in dims and 'time' not in self._dims:
|
|
591
|
+
raise ValueError('Cannot add share with time-dim to a model without time-dim')
|
|
592
|
+
if 'period' in dims and 'period' not in self._dims:
|
|
593
|
+
raise ValueError('Cannot add share with period-dim to a model without period-dim')
|
|
594
|
+
if 'scenario' in dims and 'scenario' not in self._dims:
|
|
595
|
+
raise ValueError('Cannot add share with scenario-dim to a model without scenario-dim')
|
|
1152
596
|
|
|
1153
|
-
|
|
1154
|
-
|
|
597
|
+
if name in self.shares:
|
|
598
|
+
self.share_constraints[name].lhs -= expression
|
|
599
|
+
else:
|
|
600
|
+
self.shares[name] = self.add_variables(
|
|
601
|
+
coords=self._model.get_coords(dims),
|
|
602
|
+
name=f'{name}->{self.label_full}',
|
|
603
|
+
short_name=name,
|
|
604
|
+
)
|
|
1155
605
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
# 3) geht nur, wenn alle flow.min >= 0
|
|
1160
|
-
# --> könnte man auch umsetzen (statt force_on_variable() für die Flows, aber sollte aufs selbe wie "new" kommen)
|
|
1161
|
-
"""
|
|
606
|
+
self.share_constraints[name] = self.add_constraints(
|
|
607
|
+
self.shares[name] == expression, name=f'{name}->{self.label_full}'
|
|
608
|
+
)
|
|
1162
609
|
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
label_of_element: str,
|
|
1168
|
-
label: str = 'PreventSimultaneousUsage',
|
|
1169
|
-
):
|
|
1170
|
-
super().__init__(model, label_of_element, label)
|
|
1171
|
-
self._simultanious_use_variables = variables
|
|
1172
|
-
assert len(self._simultanious_use_variables) >= 2, (
|
|
1173
|
-
f'Model {self.__class__.__name__} must get at least two variables'
|
|
1174
|
-
)
|
|
1175
|
-
for variable in self._simultanious_use_variables: # classic
|
|
1176
|
-
assert variable.attrs['binary'], f'Variable {variable} must be binary for use in {self.__class__.__name__}'
|
|
1177
|
-
|
|
1178
|
-
def do_modeling(self):
|
|
1179
|
-
# eq: sum(flow_i.on(t)) <= 1.1 (1 wird etwas größer gewählt wg. Binärvariablengenauigkeit)
|
|
1180
|
-
self.add(
|
|
1181
|
-
self._model.add_constraints(
|
|
1182
|
-
sum(self._simultanious_use_variables) <= 1.1, name=f'{self.label_full}|prevent_simultaneous_use'
|
|
1183
|
-
),
|
|
1184
|
-
'prevent_simultaneous_use',
|
|
1185
|
-
)
|
|
610
|
+
if 'time' not in dims:
|
|
611
|
+
self._eq_total.lhs -= self.shares[name]
|
|
612
|
+
else:
|
|
613
|
+
self._eq_total_per_timestep.lhs -= self.shares[name]
|