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