flixopt 1.0.12__py3-none-any.whl → 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of flixopt might be problematic. Click here for more details.
- docs/examples/00-Minimal Example.md +5 -0
- docs/examples/01-Basic Example.md +5 -0
- docs/examples/02-Complex Example.md +10 -0
- docs/examples/03-Calculation Modes.md +5 -0
- docs/examples/index.md +5 -0
- docs/faq/contribute.md +49 -0
- docs/faq/index.md +3 -0
- docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
- docs/images/architecture_flixOpt.png +0 -0
- docs/images/flixopt-icon.svg +1 -0
- docs/javascripts/mathjax.js +18 -0
- docs/release-notes/_template.txt +32 -0
- docs/release-notes/index.md +7 -0
- docs/release-notes/v2.0.0.md +93 -0
- docs/release-notes/v2.0.1.md +12 -0
- docs/user-guide/Mathematical Notation/Bus.md +33 -0
- docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +132 -0
- docs/user-guide/Mathematical Notation/Flow.md +26 -0
- docs/user-guide/Mathematical Notation/LinearConverter.md +21 -0
- docs/user-guide/Mathematical Notation/Piecewise.md +49 -0
- docs/user-guide/Mathematical Notation/Storage.md +44 -0
- docs/user-guide/Mathematical Notation/index.md +22 -0
- docs/user-guide/Mathematical Notation/others.md +3 -0
- docs/user-guide/index.md +124 -0
- {flixOpt → flixopt}/__init__.py +5 -2
- {flixOpt → flixopt}/aggregation.py +113 -140
- flixopt/calculation.py +455 -0
- {flixOpt → flixopt}/commons.py +7 -4
- flixopt/components.py +630 -0
- {flixOpt → flixopt}/config.py +9 -8
- {flixOpt → flixopt}/config.yaml +3 -3
- flixopt/core.py +970 -0
- flixopt/effects.py +386 -0
- flixopt/elements.py +534 -0
- flixopt/features.py +1042 -0
- flixopt/flow_system.py +409 -0
- flixopt/interface.py +265 -0
- flixopt/io.py +308 -0
- flixopt/linear_converters.py +331 -0
- flixopt/plotting.py +1340 -0
- flixopt/results.py +898 -0
- flixopt/solvers.py +77 -0
- flixopt/structure.py +630 -0
- flixopt/utils.py +62 -0
- flixopt-2.0.1.dist-info/METADATA +145 -0
- flixopt-2.0.1.dist-info/RECORD +57 -0
- {flixopt-1.0.12.dist-info → flixopt-2.0.1.dist-info}/WHEEL +1 -1
- flixopt-2.0.1.dist-info/top_level.txt +6 -0
- pics/architecture_flixOpt-pre2.0.0.png +0 -0
- pics/architecture_flixOpt.png +0 -0
- pics/flixopt-icon.svg +1 -0
- pics/pics.pptx +0 -0
- scripts/gen_ref_pages.py +54 -0
- site/release-notes/_template.txt +32 -0
- flixOpt/calculation.py +0 -629
- flixOpt/components.py +0 -614
- flixOpt/core.py +0 -182
- flixOpt/effects.py +0 -410
- flixOpt/elements.py +0 -489
- flixOpt/features.py +0 -942
- flixOpt/flow_system.py +0 -351
- flixOpt/interface.py +0 -203
- flixOpt/linear_converters.py +0 -325
- flixOpt/math_modeling.py +0 -1145
- flixOpt/plotting.py +0 -712
- flixOpt/results.py +0 -563
- flixOpt/solvers.py +0 -21
- flixOpt/structure.py +0 -733
- flixOpt/utils.py +0 -134
- flixopt-1.0.12.dist-info/METADATA +0 -174
- flixopt-1.0.12.dist-info/RECORD +0 -29
- flixopt-1.0.12.dist-info/top_level.txt +0 -3
- {flixopt-1.0.12.dist-info → flixopt-2.0.1.dist-info/licenses}/LICENSE +0 -0
flixOpt/features.py
DELETED
|
@@ -1,942 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
This module contains the features of the flixOpt framework.
|
|
3
|
-
Features extend the functionality of Elements.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import logging
|
|
7
|
-
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
|
|
8
|
-
|
|
9
|
-
import numpy as np
|
|
10
|
-
|
|
11
|
-
from .config import CONFIG
|
|
12
|
-
from .core import Numeric, Skalar, TimeSeries
|
|
13
|
-
from .interface import InvestParameters, OnOffParameters
|
|
14
|
-
from .math_modeling import Equation, Variable, VariableTS
|
|
15
|
-
from .structure import (
|
|
16
|
-
Element,
|
|
17
|
-
ElementModel,
|
|
18
|
-
SystemModel,
|
|
19
|
-
create_equation,
|
|
20
|
-
create_variable,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
if TYPE_CHECKING: # for type checking and preventing circular imports
|
|
24
|
-
from .components import Storage
|
|
25
|
-
from .effects import Effect
|
|
26
|
-
from .elements import Flow
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
logger = logging.getLogger('flixOpt')
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class InvestmentModel(ElementModel):
|
|
33
|
-
"""Class for modeling an investment"""
|
|
34
|
-
|
|
35
|
-
def __init__(
|
|
36
|
-
self,
|
|
37
|
-
element: Union['Flow', 'Storage'],
|
|
38
|
-
invest_parameters: InvestParameters,
|
|
39
|
-
defining_variable: [VariableTS],
|
|
40
|
-
relative_bounds_of_defining_variable: Tuple[Numeric, Numeric],
|
|
41
|
-
fixed_relative_profile: Optional[Numeric] = None,
|
|
42
|
-
label: str = 'Investment',
|
|
43
|
-
on_variable: Optional[VariableTS] = None,
|
|
44
|
-
):
|
|
45
|
-
"""
|
|
46
|
-
If fixed relative profile is used, the relative bounds are ignored
|
|
47
|
-
"""
|
|
48
|
-
super().__init__(element, label)
|
|
49
|
-
self.element: Union['Flow', 'Storage'] = element
|
|
50
|
-
self.size: Optional[Union[Skalar, Variable]] = None
|
|
51
|
-
self.is_invested: Optional[Variable] = None
|
|
52
|
-
|
|
53
|
-
self._segments: Optional[SegmentedSharesModel] = None
|
|
54
|
-
|
|
55
|
-
self._on_variable = on_variable
|
|
56
|
-
self._defining_variable = defining_variable
|
|
57
|
-
self._relative_bounds_of_defining_variable = relative_bounds_of_defining_variable
|
|
58
|
-
self._fixed_relative_profile = fixed_relative_profile
|
|
59
|
-
self._invest_parameters = invest_parameters
|
|
60
|
-
|
|
61
|
-
def do_modeling(self, system_model: SystemModel):
|
|
62
|
-
invest_parameters = self._invest_parameters
|
|
63
|
-
if invest_parameters.fixed_size and not invest_parameters.optional:
|
|
64
|
-
self.size = create_variable('size', self, 1, fixed_value=invest_parameters.fixed_size)
|
|
65
|
-
else:
|
|
66
|
-
lower_bound = 0 if invest_parameters.optional else invest_parameters.minimum_size
|
|
67
|
-
self.size = create_variable(
|
|
68
|
-
'size', self, 1, lower_bound=lower_bound, upper_bound=invest_parameters.maximum_size
|
|
69
|
-
)
|
|
70
|
-
# Optional
|
|
71
|
-
if invest_parameters.optional:
|
|
72
|
-
self.is_invested = create_variable('isInvested', self, 1, is_binary=True)
|
|
73
|
-
self._create_bounds_for_optional_investment(system_model)
|
|
74
|
-
|
|
75
|
-
# Bounds for defining variable
|
|
76
|
-
self._create_bounds_for_defining_variable(system_model)
|
|
77
|
-
|
|
78
|
-
self._create_shares(system_model)
|
|
79
|
-
|
|
80
|
-
def _create_shares(self, system_model: SystemModel):
|
|
81
|
-
effect_collection = system_model.effect_collection_model
|
|
82
|
-
invest_parameters = self._invest_parameters
|
|
83
|
-
|
|
84
|
-
# fix_effects:
|
|
85
|
-
fix_effects = invest_parameters.fix_effects
|
|
86
|
-
if fix_effects != {}:
|
|
87
|
-
if invest_parameters.optional: # share: + isInvested * fix_effects
|
|
88
|
-
variable_is_invested = self.is_invested
|
|
89
|
-
else:
|
|
90
|
-
variable_is_invested = None
|
|
91
|
-
effect_collection.add_share_to_invest('fix_effects', self.element, fix_effects, 1, variable_is_invested)
|
|
92
|
-
|
|
93
|
-
# divest_effects:
|
|
94
|
-
divest_effects = invest_parameters.divest_effects
|
|
95
|
-
if divest_effects != {}:
|
|
96
|
-
if invest_parameters.optional: # share: [divest_effects - isInvested * divest_effects]
|
|
97
|
-
# 1. part of share [+ divest_effects]:
|
|
98
|
-
effect_collection.add_share_to_invest('divest_effects', self.element, divest_effects, 1, None)
|
|
99
|
-
# 2. part of share [- isInvested * divest_effects]:
|
|
100
|
-
effect_collection.add_share_to_invest(
|
|
101
|
-
'divest_cancellation_effects', self.element, divest_effects, -1, self.is_invested
|
|
102
|
-
)
|
|
103
|
-
# TODO : these 2 parts should be one share! -> SingleShareModel...?
|
|
104
|
-
|
|
105
|
-
# # specific_effects:
|
|
106
|
-
specific_effects = invest_parameters.specific_effects
|
|
107
|
-
if specific_effects != {}:
|
|
108
|
-
# share: + investment_size (=var) * specific_effects
|
|
109
|
-
effect_collection.add_share_to_invest('specific_effects', self.element, specific_effects, 1, self.size)
|
|
110
|
-
# segmented Effects
|
|
111
|
-
invest_segments = invest_parameters.effects_in_segments
|
|
112
|
-
if invest_segments:
|
|
113
|
-
self._segments = SegmentedSharesModel(
|
|
114
|
-
self.element, (self.size, invest_segments[0]), invest_segments[1], self.is_invested
|
|
115
|
-
)
|
|
116
|
-
self.sub_models.append(self._segments)
|
|
117
|
-
self._segments.do_modeling(system_model)
|
|
118
|
-
|
|
119
|
-
def _create_bounds_for_optional_investment(self, system_model: SystemModel):
|
|
120
|
-
if self._invest_parameters.fixed_size:
|
|
121
|
-
# eq: investment_size = isInvested * fixed_size
|
|
122
|
-
eq_is_invested = create_equation('is_invested', self, 'eq')
|
|
123
|
-
eq_is_invested.add_summand(self.size, -1)
|
|
124
|
-
eq_is_invested.add_summand(self.is_invested, self._invest_parameters.fixed_size)
|
|
125
|
-
else:
|
|
126
|
-
# eq1: P_invest <= isInvested * investSize_max
|
|
127
|
-
eq_is_invested_ub = create_equation('is_invested_ub', self, 'ineq')
|
|
128
|
-
eq_is_invested_ub.add_summand(self.size, 1)
|
|
129
|
-
eq_is_invested_ub.add_summand(self.is_invested, np.multiply(-1, self._invest_parameters.maximum_size))
|
|
130
|
-
|
|
131
|
-
# eq2: P_invest >= isInvested * max(epsilon, investSize_min)
|
|
132
|
-
eq_is_invested_lb = create_equation('is_invested_lb', self, 'ineq')
|
|
133
|
-
eq_is_invested_lb.add_summand(self.size, -1)
|
|
134
|
-
eq_is_invested_lb.add_summand(
|
|
135
|
-
self.is_invested, np.maximum(CONFIG.modeling.EPSILON, self._invest_parameters.minimum_size)
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
def _create_bounds_for_defining_variable(self, system_model: SystemModel):
|
|
139
|
-
label = self._defining_variable.label
|
|
140
|
-
# fixed relative value
|
|
141
|
-
if self._fixed_relative_profile is not None:
|
|
142
|
-
# TODO: Allow Off? Currently not...
|
|
143
|
-
eq_fixed = create_equation(f'fixed_{label}', self)
|
|
144
|
-
eq_fixed.add_summand(self._defining_variable, 1)
|
|
145
|
-
eq_fixed.add_summand(self.size, np.multiply(-1, self._fixed_relative_profile))
|
|
146
|
-
else:
|
|
147
|
-
relative_minimum, relative_maximum = self._relative_bounds_of_defining_variable
|
|
148
|
-
eq_upper = create_equation(f'ub_{label}', self, 'ineq')
|
|
149
|
-
# eq: defining_variable(t) <= size * upper_bound(t)
|
|
150
|
-
eq_upper.add_summand(self._defining_variable, 1)
|
|
151
|
-
eq_upper.add_summand(self.size, np.multiply(-1, relative_maximum))
|
|
152
|
-
|
|
153
|
-
## 2. Gleichung: Minimum durch Investmentgröße ##
|
|
154
|
-
eq_lower = create_equation(f'lb_{label}', self, 'ineq')
|
|
155
|
-
if self._on_variable is None:
|
|
156
|
-
# eq: defining_variable(t) >= investment_size * relative_minimum(t)
|
|
157
|
-
eq_lower.add_summand(self._defining_variable, -1)
|
|
158
|
-
eq_lower.add_summand(self.size, relative_minimum)
|
|
159
|
-
else:
|
|
160
|
-
## 2. Gleichung: Minimum durch Investmentgröße und On
|
|
161
|
-
# eq: defining_variable(t) >= mega * (On(t)-1) + size * relative_minimum(t)
|
|
162
|
-
# ... mit mega = relative_maximum * maximum_size
|
|
163
|
-
# äquivalent zu:.
|
|
164
|
-
# eq: - defining_variable(t) + mega * On(t) + size * relative_minimum(t) <= + mega
|
|
165
|
-
mega = relative_maximum * self._invest_parameters.maximum_size
|
|
166
|
-
eq_lower.add_summand(self._defining_variable, -1)
|
|
167
|
-
eq_lower.add_summand(self._on_variable, mega)
|
|
168
|
-
eq_lower.add_summand(self.size, relative_minimum)
|
|
169
|
-
eq_lower.add_constant(mega)
|
|
170
|
-
# Anmerkung: Glg bei Spezialfall relative_minimum = 0 redundant zu OnOff ??
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
class OnOffModel(ElementModel):
|
|
174
|
-
"""
|
|
175
|
-
Class for modeling the on and off state of a variable
|
|
176
|
-
If defining_bounds are given, creates sufficient lower bounds
|
|
177
|
-
"""
|
|
178
|
-
|
|
179
|
-
def __init__(
|
|
180
|
-
self,
|
|
181
|
-
element: Element,
|
|
182
|
-
on_off_parameters: OnOffParameters,
|
|
183
|
-
defining_variables: List[VariableTS],
|
|
184
|
-
defining_bounds: List[Tuple[Numeric, Numeric]],
|
|
185
|
-
label: str = 'OnOff',
|
|
186
|
-
):
|
|
187
|
-
"""
|
|
188
|
-
defining_bounds: a list of Numeric, that can be used to create the bound for On/Off more efficiently
|
|
189
|
-
"""
|
|
190
|
-
super().__init__(element, label)
|
|
191
|
-
self.element = element
|
|
192
|
-
self.on: Optional[VariableTS] = None
|
|
193
|
-
self.total_on_hours: Optional[Variable] = None
|
|
194
|
-
|
|
195
|
-
self.consecutive_on_hours: Optional[VariableTS] = None
|
|
196
|
-
self.consecutive_off_hours: Optional[VariableTS] = None
|
|
197
|
-
|
|
198
|
-
self.off: Optional[VariableTS] = None
|
|
199
|
-
|
|
200
|
-
self.switch_on: Optional[VariableTS] = None
|
|
201
|
-
self.switch_off: Optional[VariableTS] = None
|
|
202
|
-
self.nr_switch_on: Optional[VariableTS] = None
|
|
203
|
-
|
|
204
|
-
self._on_off_parameters = on_off_parameters
|
|
205
|
-
self._defining_variables = defining_variables
|
|
206
|
-
# Ensure that no lower bound is below a certain threshold
|
|
207
|
-
self._defining_bounds = [(np.maximum(lb, CONFIG.modeling.EPSILON), ub) for lb, ub in defining_bounds]
|
|
208
|
-
assert len(defining_variables) == len(defining_bounds), 'Every defining Variable needs bounds to Model OnOff'
|
|
209
|
-
|
|
210
|
-
def do_modeling(self, system_model: SystemModel):
|
|
211
|
-
self.on = create_variable(
|
|
212
|
-
'on',
|
|
213
|
-
self,
|
|
214
|
-
system_model.nr_of_time_steps,
|
|
215
|
-
is_binary=True,
|
|
216
|
-
previous_values=self._previous_on_values(CONFIG.modeling.EPSILON),
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
self.total_on_hours = create_variable(
|
|
220
|
-
'totalOnHours',
|
|
221
|
-
self,
|
|
222
|
-
1,
|
|
223
|
-
lower_bound=self._on_off_parameters.on_hours_total_min,
|
|
224
|
-
upper_bound=self._on_off_parameters.on_hours_total_max,
|
|
225
|
-
)
|
|
226
|
-
eq_total_on = create_equation('totalOnHours', self)
|
|
227
|
-
eq_total_on.add_summand(self.on, system_model.dt_in_hours, as_sum=True)
|
|
228
|
-
eq_total_on.add_summand(self.total_on_hours, -1)
|
|
229
|
-
|
|
230
|
-
self._add_on_constraints(system_model, system_model.indices)
|
|
231
|
-
|
|
232
|
-
if self._on_off_parameters.use_off:
|
|
233
|
-
self.off = create_variable(
|
|
234
|
-
'off',
|
|
235
|
-
self,
|
|
236
|
-
system_model.nr_of_time_steps,
|
|
237
|
-
is_binary=True,
|
|
238
|
-
previous_values=1 - self._previous_on_values(CONFIG.modeling.EPSILON),
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
self._add_off_constraints(system_model, system_model.indices)
|
|
242
|
-
|
|
243
|
-
if self._on_off_parameters.use_consecutive_on_hours:
|
|
244
|
-
self.consecutive_on_hours = self._get_duration_in_hours(
|
|
245
|
-
'consecutiveOnHours',
|
|
246
|
-
self.on,
|
|
247
|
-
self._on_off_parameters.consecutive_on_hours_min,
|
|
248
|
-
self._on_off_parameters.consecutive_on_hours_max,
|
|
249
|
-
system_model,
|
|
250
|
-
system_model.indices,
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
if self._on_off_parameters.use_consecutive_off_hours:
|
|
254
|
-
self.consecutive_off_hours = self._get_duration_in_hours(
|
|
255
|
-
'consecutiveOffHours',
|
|
256
|
-
self.off,
|
|
257
|
-
self._on_off_parameters.consecutive_off_hours_min,
|
|
258
|
-
self._on_off_parameters.consecutive_off_hours_max,
|
|
259
|
-
system_model,
|
|
260
|
-
system_model.indices,
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
if self._on_off_parameters.use_switch_on:
|
|
264
|
-
self.switch_on = create_variable('switchOn', self, system_model.nr_of_time_steps, is_binary=True)
|
|
265
|
-
self.switch_off = create_variable('switchOff', self, system_model.nr_of_time_steps, is_binary=True)
|
|
266
|
-
self.nr_switch_on = create_variable(
|
|
267
|
-
'nrSwitchOn', self, 1, upper_bound=self._on_off_parameters.switch_on_total_max
|
|
268
|
-
)
|
|
269
|
-
self._add_switch_constraints(system_model)
|
|
270
|
-
|
|
271
|
-
self._create_shares(system_model)
|
|
272
|
-
|
|
273
|
-
def _add_on_constraints(self, system_model: SystemModel, time_indices: Union[list[int], range]):
|
|
274
|
-
assert self.on is not None, f'On variable of {self.element} must be defined to add constraints'
|
|
275
|
-
# % Bedingungen 1) und 2) müssen erfüllt sein:
|
|
276
|
-
|
|
277
|
-
# % Anmerkung: Falls "abschnittsweise linear" gewählt, dann ist eigentlich nur Bedingung 1) noch notwendig
|
|
278
|
-
# % (und dann auch nur wenn erstes Segment bei Q_th=0 beginnt. Dann soll bei Q_th=0 (d.h. die Maschine ist Aus) On = 0 und segment1.onSeg = 0):)
|
|
279
|
-
# % Fazit: Wenn kein Performance-Verlust durch mehr Gleichungen, dann egal!
|
|
280
|
-
|
|
281
|
-
nr_of_defining_variables = len(self._defining_variables)
|
|
282
|
-
assert nr_of_defining_variables > 0, 'Achtung: mindestens 1 Flow notwendig'
|
|
283
|
-
|
|
284
|
-
eq_on_1 = create_equation('On_Constraint_1', self, eq_type='ineq')
|
|
285
|
-
eq_on_2 = create_equation('On_Constraint_2', self, eq_type='ineq')
|
|
286
|
-
if nr_of_defining_variables == 1:
|
|
287
|
-
variable = self._defining_variables[0]
|
|
288
|
-
lower_bound, upper_bound = self._defining_bounds[0]
|
|
289
|
-
#### Bedingung 1) ####
|
|
290
|
-
# eq: On(t) * max(epsilon, lower_bound) <= Q_th(t)
|
|
291
|
-
eq_on_1.add_summand(variable, -1, time_indices)
|
|
292
|
-
eq_on_1.add_summand(self.on, np.maximum(CONFIG.modeling.EPSILON, lower_bound), time_indices)
|
|
293
|
-
|
|
294
|
-
#### Bedingung 2) ####
|
|
295
|
-
# eq: Q_th(t) <= Q_th_max * On(t)
|
|
296
|
-
eq_on_2.add_summand(variable, 1, time_indices)
|
|
297
|
-
eq_on_2.add_summand(self.on, -1 * upper_bound, time_indices)
|
|
298
|
-
|
|
299
|
-
else: # Bei mehreren Leistungsvariablen:
|
|
300
|
-
#### Bedingung 1) ####
|
|
301
|
-
# When all defining variables are 0, On is 0
|
|
302
|
-
# eq: - sum(alle Leistungen(t)) + Epsilon * On(t) <= 0
|
|
303
|
-
for variable in self._defining_variables:
|
|
304
|
-
eq_on_1.add_summand(variable, -1, time_indices)
|
|
305
|
-
eq_on_1.add_summand(self.on, CONFIG.modeling.EPSILON, time_indices)
|
|
306
|
-
|
|
307
|
-
#### Bedingung 2) ####
|
|
308
|
-
## sum(alle Leistung) >0 -> On = 1 | On=0 -> sum(Leistung)=0
|
|
309
|
-
# eq: sum( Leistung(t,i)) - sum(Leistung_max(i)) * On(t) <= 0
|
|
310
|
-
# --> damit Gleichungswerte nicht zu groß werden, noch durch nr_of_flows geteilt:
|
|
311
|
-
# eq: sum( Leistung(t,i) / nr_of_flows ) - sum(Leistung_max(i)) / nr_of_flows * On(t) <= 0
|
|
312
|
-
absolute_maximum: Numeric = 0.0
|
|
313
|
-
for variable, bounds in zip(self._defining_variables, self._defining_bounds, strict=False):
|
|
314
|
-
eq_on_2.add_summand(variable, 1 / nr_of_defining_variables, time_indices)
|
|
315
|
-
absolute_maximum += bounds[
|
|
316
|
-
1
|
|
317
|
-
] # der maximale Nennwert reicht als Obergrenze hier aus. (immer noch math. günster als BigM)
|
|
318
|
-
|
|
319
|
-
upper_bound = absolute_maximum / nr_of_defining_variables
|
|
320
|
-
eq_on_2.add_summand(self.on, -1 * upper_bound, time_indices)
|
|
321
|
-
|
|
322
|
-
if np.max(upper_bound) > CONFIG.modeling.BIG_BINARY_BOUND:
|
|
323
|
-
logger.warning(
|
|
324
|
-
f'In "{self.element.label_full}", a binary definition was created with a big upper bound '
|
|
325
|
-
f'({np.max(upper_bound)}). This can lead to wrong results regarding the on and off variables. '
|
|
326
|
-
f'Avoid this warning by reducing the size of {self.element.label_full} '
|
|
327
|
-
f'(or the maximum_size of the corresponding InvestParameters). '
|
|
328
|
-
f'If its a Component, you might need to adjust the sizes of all of its flows.'
|
|
329
|
-
)
|
|
330
|
-
|
|
331
|
-
def _add_off_constraints(self, system_model: SystemModel, time_indices: Union[list[int], range]):
|
|
332
|
-
assert self.off is not None, f'Off variable of {self.element} must be defined to add constraints'
|
|
333
|
-
# Definition var_off:
|
|
334
|
-
# eq: var_on(t) + var_off(t) = 1
|
|
335
|
-
eq_off = create_equation('var_off', self, eq_type='eq')
|
|
336
|
-
eq_off.add_summand(self.off, 1, time_indices)
|
|
337
|
-
eq_off.add_summand(self.on, 1, time_indices)
|
|
338
|
-
eq_off.add_constant(1)
|
|
339
|
-
|
|
340
|
-
def _get_duration_in_hours(
|
|
341
|
-
self,
|
|
342
|
-
variable_label: str,
|
|
343
|
-
binary_variable: VariableTS,
|
|
344
|
-
minimum_duration: Optional[TimeSeries],
|
|
345
|
-
maximum_duration: Optional[TimeSeries],
|
|
346
|
-
system_model: SystemModel,
|
|
347
|
-
time_indices: Union[list[int], range],
|
|
348
|
-
) -> VariableTS:
|
|
349
|
-
"""
|
|
350
|
-
creates duration variable and adds constraints to a time-series variable to enforce duration limits based on
|
|
351
|
-
binary activity.
|
|
352
|
-
The minimum duration in the last time step is not restricted.
|
|
353
|
-
Previous values before t=0 are not recognised!
|
|
354
|
-
|
|
355
|
-
Parameters:
|
|
356
|
-
variable_label (str):
|
|
357
|
-
Label for the duration variable to be created.
|
|
358
|
-
binary_variable (VariableTS):
|
|
359
|
-
Time-series binary variable (e.g., [0, 0, 1, 1, 1, 0, ...]) representing activity states.
|
|
360
|
-
minimum_duration (Optional[TimeSeries]):
|
|
361
|
-
Minimum duration the activity must remain active once started.
|
|
362
|
-
If None, no minimum duration constraint is applied.
|
|
363
|
-
maximum_duration (Optional[TimeSeries]):
|
|
364
|
-
Maximum duration the activity can remain active.
|
|
365
|
-
If None, the maximum duration is set to the total available time.
|
|
366
|
-
system_model (SystemModel):
|
|
367
|
-
The system model containing time step information.
|
|
368
|
-
time_indices (Union[list[int], range]):
|
|
369
|
-
List or range of indices to which to apply the constraints.
|
|
370
|
-
|
|
371
|
-
Returns:
|
|
372
|
-
VariableTS: The created duration variable representing consecutive active durations.
|
|
373
|
-
|
|
374
|
-
Example:
|
|
375
|
-
binary_variable: [0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, ...]
|
|
376
|
-
duration_in_hours: [0, 0, 1, 2, 3, 4, 0, 1, 2, 3, 0, ...] (only if dt_in_hours=1)
|
|
377
|
-
|
|
378
|
-
Here, duration_in_hours increments while binary_variable is 1. Minimum and maximum durations
|
|
379
|
-
can be enforced to constrain how long the activity remains active.
|
|
380
|
-
|
|
381
|
-
Notes:
|
|
382
|
-
- To count consecutive zeros instead of ones, use a transformed binary variable
|
|
383
|
-
(e.g., `1 - binary_variable`).
|
|
384
|
-
- Constraints ensure the duration variable properly resets or increments based on activity.
|
|
385
|
-
|
|
386
|
-
Raises:
|
|
387
|
-
AssertionError: If the binary_variable is None, indicating the duration constraints cannot be applied.
|
|
388
|
-
|
|
389
|
-
"""
|
|
390
|
-
try:
|
|
391
|
-
previous_duration: Skalar = self.get_consecutive_duration(
|
|
392
|
-
binary_variable.previous_values, system_model.previous_dt_in_hours
|
|
393
|
-
)
|
|
394
|
-
except TypeError as e:
|
|
395
|
-
raise TypeError(f'The consecutive_duration of "{variable_label}" could not be calculated. {e}') from e
|
|
396
|
-
mega = system_model.dt_in_hours_total + previous_duration
|
|
397
|
-
|
|
398
|
-
if maximum_duration is not None:
|
|
399
|
-
first_step_max: Skalar = (
|
|
400
|
-
maximum_duration.active_data[0] if maximum_duration.is_array else maximum_duration.active_data
|
|
401
|
-
)
|
|
402
|
-
if previous_duration + system_model.dt_in_hours[0] > first_step_max:
|
|
403
|
-
logger.warning(
|
|
404
|
-
f'The maximum duration of "{variable_label}" is set to {maximum_duration.active_data}h, '
|
|
405
|
-
f'but the consecutive_duration previous to this model is {previous_duration}h. '
|
|
406
|
-
f'This forces "{binary_variable.label} = 0" in the first time step '
|
|
407
|
-
f'(dt={system_model.dt_in_hours[0]}h)!'
|
|
408
|
-
)
|
|
409
|
-
|
|
410
|
-
duration_in_hours = create_variable(
|
|
411
|
-
variable_label,
|
|
412
|
-
self,
|
|
413
|
-
system_model.nr_of_time_steps,
|
|
414
|
-
lower_bound=0,
|
|
415
|
-
upper_bound=maximum_duration.active_data if maximum_duration is not None else mega,
|
|
416
|
-
previous_values=previous_duration,
|
|
417
|
-
)
|
|
418
|
-
label_prefix = duration_in_hours.label
|
|
419
|
-
|
|
420
|
-
assert binary_variable is not None, f'Duration Variable of {self.element} must be defined to add constraints'
|
|
421
|
-
# TODO: Einfachere Variante von Peter umsetzen!
|
|
422
|
-
|
|
423
|
-
# 1) eq: duration(t) - On(t) * BIG <= 0
|
|
424
|
-
constraint_1 = create_equation(f'{label_prefix}_constraint_1', self, eq_type='ineq')
|
|
425
|
-
constraint_1.add_summand(duration_in_hours, 1)
|
|
426
|
-
constraint_1.add_summand(binary_variable, -1 * mega)
|
|
427
|
-
|
|
428
|
-
# 2a) eq: duration(t) - duration(t-1) <= dt(t)
|
|
429
|
-
# on(t)=1 -> duration(t) - duration(t-1) <= dt(t)
|
|
430
|
-
# on(t)=0 -> duration(t-1) >= negat. value
|
|
431
|
-
constraint_2a = create_equation(f'{label_prefix}_constraint_2a', self, eq_type='ineq')
|
|
432
|
-
constraint_2a.add_summand(duration_in_hours, 1, time_indices[1:]) # duration(t)
|
|
433
|
-
constraint_2a.add_summand(duration_in_hours, -1, time_indices[0:-1]) # duration(t-1)
|
|
434
|
-
constraint_2a.add_constant(system_model.dt_in_hours[1:]) # dt(t)
|
|
435
|
-
|
|
436
|
-
# 2b) eq: dt(t) - BIG * ( 1-On(t) ) <= duration(t) - duration(t-1)
|
|
437
|
-
# eq: -duration(t) + duration(t-1) + On(t) * BIG <= -dt(t) + BIG
|
|
438
|
-
# with BIG = dt_in_hours_total.
|
|
439
|
-
# on(t)=1 -> duration(t)- duration(t-1) >= dt(t)
|
|
440
|
-
# on(t)=0 -> duration(t)- duration(t-1) >= negat. value
|
|
441
|
-
|
|
442
|
-
constraint_2b = create_equation(f'{label_prefix}_constraint_2b', self, eq_type='ineq')
|
|
443
|
-
constraint_2b.add_summand(duration_in_hours, -1, time_indices[1:]) # duration(t)
|
|
444
|
-
constraint_2b.add_summand(duration_in_hours, 1, time_indices[0:-1]) # duration(t-1)
|
|
445
|
-
constraint_2b.add_summand(binary_variable, mega, time_indices[1:]) # on(t)
|
|
446
|
-
constraint_2b.add_constant(-1 * system_model.dt_in_hours[1:] + mega) # dt(t)
|
|
447
|
-
|
|
448
|
-
# 3) check minimum_duration before switchOff-step
|
|
449
|
-
|
|
450
|
-
if minimum_duration is not None:
|
|
451
|
-
# Note: switchOff-step is when: On(t) - On(t+1) == 1
|
|
452
|
-
# Note: (last on-time period (with last timestep of period t=n) is not checked and can be shorter)
|
|
453
|
-
# Note: (previous values before t=1 are not recognised!)
|
|
454
|
-
# eq: duration(t) >= minimum_duration(t) * [On(t) - On(t+1)] for t=1..(n-1)
|
|
455
|
-
# eq: -duration(t) + minimum_duration(t) * On(t) - minimum_duration(t) * On(t+1) <= 0
|
|
456
|
-
if minimum_duration.is_scalar:
|
|
457
|
-
minimum_duration_used = minimum_duration.active_data
|
|
458
|
-
else:
|
|
459
|
-
minimum_duration_used = minimum_duration.active_data[0:-1] # only checked for t=1...(n-1)
|
|
460
|
-
eq_min_duration = create_equation(f'{label_prefix}_minimum_duration', self, eq_type='ineq')
|
|
461
|
-
eq_min_duration.add_summand(duration_in_hours, -1, time_indices[0:-1]) # -duration(t)
|
|
462
|
-
eq_min_duration.add_summand(
|
|
463
|
-
binary_variable, -1 * minimum_duration_used, time_indices[1:]
|
|
464
|
-
) # - minimum_duration (t) * On(t+1)
|
|
465
|
-
eq_min_duration.add_summand(
|
|
466
|
-
binary_variable, minimum_duration_used, time_indices[0:-1]
|
|
467
|
-
) # minimum_duration * On(t)
|
|
468
|
-
|
|
469
|
-
first_step_min: Skalar = (
|
|
470
|
-
minimum_duration.active_data[0] if minimum_duration.is_array else minimum_duration.active_data
|
|
471
|
-
)
|
|
472
|
-
if 0 < duration_in_hours.previous_values < first_step_min:
|
|
473
|
-
# Force the first step to be = 1, if the minimum_duration is not reached in previous_values
|
|
474
|
-
# Note: Only if the previous consecutive_duration is smaller than the minimum duration,
|
|
475
|
-
# and the previous_values is greater 0!
|
|
476
|
-
# eq: duration(t=0) = duration(t=-1) + dt(0)
|
|
477
|
-
eq_min_duration_inital = create_equation(f'{label_prefix}_minimum_duration_inital', self, eq_type='eq')
|
|
478
|
-
eq_min_duration_inital.add_summand(binary_variable, 1, time_indices[0])
|
|
479
|
-
eq_min_duration_inital.add_constant(1)
|
|
480
|
-
|
|
481
|
-
# 4) first index:
|
|
482
|
-
# eq: duration(t=0)= dt(0) * On(0)
|
|
483
|
-
first_index = time_indices[0] # only first element
|
|
484
|
-
eq_first = create_equation(f'{label_prefix}_initial', self)
|
|
485
|
-
eq_first.add_summand(duration_in_hours, 1, first_index)
|
|
486
|
-
eq_first.add_summand(
|
|
487
|
-
binary_variable,
|
|
488
|
-
-1 * (system_model.dt_in_hours[first_index] + duration_in_hours.previous_values),
|
|
489
|
-
first_index,
|
|
490
|
-
)
|
|
491
|
-
|
|
492
|
-
return duration_in_hours
|
|
493
|
-
|
|
494
|
-
def _add_switch_constraints(self, system_model: SystemModel):
|
|
495
|
-
assert self.switch_on is not None, f'Switch On Variable of {self.element} must be defined to add constraints'
|
|
496
|
-
assert self.switch_off is not None, f'Switch Off Variable of {self.element} must be defined to add constraints'
|
|
497
|
-
assert self.nr_switch_on is not None, (
|
|
498
|
-
f'Nr of Switch On Variable of {self.element} must be defined to add constraints'
|
|
499
|
-
)
|
|
500
|
-
assert self.on is not None, f'On Variable of {self.element} must be defined to add constraints'
|
|
501
|
-
# % Schaltänderung aus On-Variable
|
|
502
|
-
# % SwitchOn(t)-SwitchOff(t) = On(t)-On(t-1)
|
|
503
|
-
eq_switch = create_equation('Switch', self)
|
|
504
|
-
eq_switch.add_summand(self.switch_on, 1, system_model.indices[1:]) # SwitchOn(t)
|
|
505
|
-
eq_switch.add_summand(self.switch_off, -1, system_model.indices[1:]) # SwitchOff(t)
|
|
506
|
-
eq_switch.add_summand(self.on, -1, system_model.indices[1:]) # On(t)
|
|
507
|
-
eq_switch.add_summand(self.on, +1, system_model.indices[0:-1]) # On(t-1)
|
|
508
|
-
|
|
509
|
-
# Initital switch on
|
|
510
|
-
# eq: SwitchOn(t=0)-SwitchOff(t=0) = On(t=0) - On(t=-1)
|
|
511
|
-
eq_initial_switch = create_equation('Initial_Switch', self)
|
|
512
|
-
eq_initial_switch.add_summand(self.switch_on, 1, indices_of_variable=0) # SwitchOn(t=0)
|
|
513
|
-
eq_initial_switch.add_summand(self.switch_off, -1, indices_of_variable=0) # SwitchOff(t=0)
|
|
514
|
-
eq_initial_switch.add_summand(self.on, -1, indices_of_variable=0) # On(t=0)
|
|
515
|
-
eq_initial_switch.add_constant(-1 * self.on.previous_values[-1]) # On(t-1)
|
|
516
|
-
|
|
517
|
-
## Entweder SwitchOff oder SwitchOn
|
|
518
|
-
# eq: SwitchOn(t) + SwitchOff(t) <= 1.1
|
|
519
|
-
eq_switch_on_or_off = create_equation('Switch_On_or_Off', self, eq_type='ineq')
|
|
520
|
-
eq_switch_on_or_off.add_summand(self.switch_on, 1)
|
|
521
|
-
eq_switch_on_or_off.add_summand(self.switch_off, 1)
|
|
522
|
-
eq_switch_on_or_off.add_constant(1.1)
|
|
523
|
-
|
|
524
|
-
## Anzahl Starts:
|
|
525
|
-
# eq: nrSwitchOn = sum(SwitchOn(t))
|
|
526
|
-
eq_nr_switch_on = create_equation('NrSwitchOn', self)
|
|
527
|
-
eq_nr_switch_on.add_summand(self.nr_switch_on, 1)
|
|
528
|
-
eq_nr_switch_on.add_summand(self.switch_on, -1, as_sum=True)
|
|
529
|
-
|
|
530
|
-
def _create_shares(self, system_model: SystemModel):
|
|
531
|
-
# Anfahrkosten:
|
|
532
|
-
effect_collection = system_model.effect_collection_model
|
|
533
|
-
effects_per_switch_on = self._on_off_parameters.effects_per_switch_on
|
|
534
|
-
if effects_per_switch_on != {}:
|
|
535
|
-
effect_collection.add_share_to_operation(
|
|
536
|
-
'switch_on_effects', self.element, effects_per_switch_on, 1, self.switch_on
|
|
537
|
-
)
|
|
538
|
-
|
|
539
|
-
# Betriebskosten:
|
|
540
|
-
effects_per_running_hour = self._on_off_parameters.effects_per_running_hour
|
|
541
|
-
if effects_per_running_hour != {}:
|
|
542
|
-
effect_collection.add_share_to_operation(
|
|
543
|
-
'running_hour_effects', self.element, effects_per_running_hour, system_model.dt_in_hours, self.on
|
|
544
|
-
)
|
|
545
|
-
|
|
546
|
-
def _previous_on_values(self, epsilon: float = 1e-5) -> np.ndarray:
|
|
547
|
-
"""
|
|
548
|
-
Returns the previous 'on' states of defining variables as a binary array.
|
|
549
|
-
|
|
550
|
-
Parameters:
|
|
551
|
-
----------
|
|
552
|
-
epsilon : float, optional
|
|
553
|
-
Tolerance for equality to determine "off" state, default is 1e-5.
|
|
554
|
-
|
|
555
|
-
Returns:
|
|
556
|
-
-------
|
|
557
|
-
np.ndarray
|
|
558
|
-
A binary array (0 and 1) indicating the previous on/off states of the variables.
|
|
559
|
-
Returns `array([0])` if no previous values are available.
|
|
560
|
-
"""
|
|
561
|
-
previous_values = [var.previous_values for var in self._defining_variables if var.previous_values is not None]
|
|
562
|
-
|
|
563
|
-
if not previous_values:
|
|
564
|
-
return np.array([0])
|
|
565
|
-
else: # Convert to 2D-array and compute binary on/off states
|
|
566
|
-
previous_values = np.array(previous_values)
|
|
567
|
-
if previous_values.ndim > 1:
|
|
568
|
-
return np.any(~np.isclose(previous_values, 0, atol=epsilon), axis=0).astype(int)
|
|
569
|
-
else:
|
|
570
|
-
return (~np.isclose(previous_values, 0, atol=epsilon)).astype(int)
|
|
571
|
-
|
|
572
|
-
@classmethod
|
|
573
|
-
def get_consecutive_duration(
|
|
574
|
-
cls, binary_values: Union[int, np.ndarray], dt_in_hours: Union[int, float, np.ndarray]
|
|
575
|
-
) -> Skalar:
|
|
576
|
-
"""
|
|
577
|
-
Returns the current consecutive duration in hours, computed from binary values.
|
|
578
|
-
If only one binary value is availlable, the last dt_in_hours is used.
|
|
579
|
-
Of both binary_values and dt_in_hours are arrays, checks that the length of dt_in_hours has at least as
|
|
580
|
-
many elements as the last consecutive duration in binary_values.
|
|
581
|
-
|
|
582
|
-
Parameters
|
|
583
|
-
----------
|
|
584
|
-
binary_values : int, np.ndarray
|
|
585
|
-
An int or 1D binary array containing only `0`s and `1`s.
|
|
586
|
-
dt_in_hours : int, float, np.ndarray
|
|
587
|
-
The duration of each time step in hours.
|
|
588
|
-
|
|
589
|
-
Returns
|
|
590
|
-
-------
|
|
591
|
-
np.ndarray
|
|
592
|
-
The duration of the binary variable in hours.
|
|
593
|
-
|
|
594
|
-
Raises
|
|
595
|
-
------
|
|
596
|
-
TypeError
|
|
597
|
-
If the length of binary_values and dt_in_hours is not equal, but None is a scalar.
|
|
598
|
-
"""
|
|
599
|
-
if np.isscalar(binary_values) and np.isscalar(dt_in_hours):
|
|
600
|
-
return binary_values * dt_in_hours
|
|
601
|
-
elif np.isscalar(binary_values) and not np.isscalar(dt_in_hours):
|
|
602
|
-
return binary_values * dt_in_hours[-1]
|
|
603
|
-
|
|
604
|
-
# Find the indexes where value=`0` in a 1D-array
|
|
605
|
-
zero_indices = np.where(np.isclose(binary_values, 0, atol=CONFIG.modeling.EPSILON))[0]
|
|
606
|
-
length_of_last_duration = zero_indices[-1] + 1 if zero_indices.size > 0 else len(binary_values)
|
|
607
|
-
|
|
608
|
-
if not np.isscalar(binary_values) and np.isscalar(dt_in_hours):
|
|
609
|
-
return np.sum(binary_values[-length_of_last_duration:] * dt_in_hours)
|
|
610
|
-
|
|
611
|
-
elif not np.isscalar(binary_values) and not np.isscalar(dt_in_hours):
|
|
612
|
-
if length_of_last_duration > len(dt_in_hours): # check that lengths are compatible
|
|
613
|
-
raise TypeError(
|
|
614
|
-
f'When trying to calculate the consecutive duration, the length of the last duration '
|
|
615
|
-
f'({len(length_of_last_duration)}) is longer than the dt_in_hours ({len(dt_in_hours)}), '
|
|
616
|
-
f'as {binary_values=}'
|
|
617
|
-
)
|
|
618
|
-
return np.sum(binary_values[-length_of_last_duration:] * dt_in_hours[-length_of_last_duration:])
|
|
619
|
-
|
|
620
|
-
else:
|
|
621
|
-
raise Exception(
|
|
622
|
-
f'Unexpected state reached in function get_consecutive_duration(). binary_values={binary_values}; '
|
|
623
|
-
f'dt_in_hours={dt_in_hours}'
|
|
624
|
-
)
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
class SegmentModel(ElementModel):
|
|
628
|
-
"""Class for modeling a linear segment of one or more variables in parallel"""
|
|
629
|
-
|
|
630
|
-
def __init__(
|
|
631
|
-
self,
|
|
632
|
-
element: Element,
|
|
633
|
-
segment_index: Union[int, str],
|
|
634
|
-
sample_points: Dict[Variable, Tuple[Union[Numeric, TimeSeries], Union[Numeric, TimeSeries]]],
|
|
635
|
-
as_time_series: bool = True,
|
|
636
|
-
):
|
|
637
|
-
super().__init__(element, f'Segment_{segment_index}')
|
|
638
|
-
self.element = element
|
|
639
|
-
self.in_segment: Optional[VariableTS] = None
|
|
640
|
-
self.lambda0: Optional[VariableTS] = None
|
|
641
|
-
self.lambda1: Optional[VariableTS] = None
|
|
642
|
-
|
|
643
|
-
self._segment_index = segment_index
|
|
644
|
-
self._as_time_series = as_time_series
|
|
645
|
-
self.sample_points = sample_points
|
|
646
|
-
|
|
647
|
-
def do_modeling(self, system_model: SystemModel):
|
|
648
|
-
length = system_model.nr_of_time_steps if self._as_time_series else 1
|
|
649
|
-
self.in_segment = create_variable('inSegment', self, length, is_binary=True)
|
|
650
|
-
self.lambda0 = create_variable('lambda0', self, length, lower_bound=0, upper_bound=1) # Wertebereich 0..1
|
|
651
|
-
self.lambda1 = create_variable('lambda1', self, length, lower_bound=0, upper_bound=1) # Wertebereich 0..1
|
|
652
|
-
|
|
653
|
-
# eq: -aSegment.onSeg(t) + aSegment.lambda1(t) + aSegment.lambda2(t) = 0
|
|
654
|
-
equation = create_equation('inSegment', self)
|
|
655
|
-
|
|
656
|
-
equation.add_summand(self.in_segment, -1)
|
|
657
|
-
equation.add_summand(self.lambda0, 1)
|
|
658
|
-
equation.add_summand(self.lambda1, 1)
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
class MultipleSegmentsModel(ElementModel):
|
|
662
|
-
# TODO: Length...
|
|
663
|
-
def __init__(
|
|
664
|
-
self,
|
|
665
|
-
element: Element,
|
|
666
|
-
sample_points: Dict[Variable, List[Tuple[Numeric, Numeric]]],
|
|
667
|
-
can_be_outside_segments: Optional[Union[bool, Variable]],
|
|
668
|
-
as_time_series: bool = True,
|
|
669
|
-
label: str = 'MultipleSegments',
|
|
670
|
-
):
|
|
671
|
-
"""
|
|
672
|
-
can_be_outside_segments: True -> Variable gets created;
|
|
673
|
-
False or None -> No Variable gets_created;
|
|
674
|
-
Variable -> the Variable gets used
|
|
675
|
-
"""
|
|
676
|
-
super().__init__(element, label)
|
|
677
|
-
self.element = element
|
|
678
|
-
|
|
679
|
-
self.outside_segments: Optional[VariableTS] = None
|
|
680
|
-
|
|
681
|
-
self._as_time_series = as_time_series
|
|
682
|
-
self._can_be_outside_segments = can_be_outside_segments
|
|
683
|
-
self._sample_points = sample_points
|
|
684
|
-
self._segment_models: List[SegmentModel] = []
|
|
685
|
-
|
|
686
|
-
def do_modeling(self, system_model: SystemModel):
|
|
687
|
-
restructured_variables_with_segments: List[Dict[Variable, Tuple[Numeric, Numeric]]] = [
|
|
688
|
-
{key: values[i] for key, values in self._sample_points.items()} for i in range(self._nr_of_segments)
|
|
689
|
-
]
|
|
690
|
-
|
|
691
|
-
self._segment_models = [
|
|
692
|
-
SegmentModel(self.element, i, sample_points, self._as_time_series)
|
|
693
|
-
for i, sample_points in enumerate(restructured_variables_with_segments)
|
|
694
|
-
]
|
|
695
|
-
|
|
696
|
-
self.sub_models.extend(self._segment_models)
|
|
697
|
-
|
|
698
|
-
for segment_model in self._segment_models:
|
|
699
|
-
segment_model.do_modeling(system_model)
|
|
700
|
-
|
|
701
|
-
# eq: - v(t) + (v_0_0 * lambda_0_0 + v_0_1 * lambda_0_1) + (v_1_0 * lambda_1_0 + v_1_1 * lambda_1_1) ... = 0
|
|
702
|
-
# -> v_0_0, v_0_1 = Stützstellen des Segments 0
|
|
703
|
-
for variable in self._sample_points.keys():
|
|
704
|
-
lambda_eq = create_equation(f'lambda_{variable.label}', self)
|
|
705
|
-
lambda_eq.add_summand(variable, -1)
|
|
706
|
-
for segment_model in self._segment_models:
|
|
707
|
-
lambda_eq.add_summand(segment_model.lambda0, segment_model.sample_points[variable][0])
|
|
708
|
-
lambda_eq.add_summand(segment_model.lambda1, segment_model.sample_points[variable][1])
|
|
709
|
-
|
|
710
|
-
# a) eq: Segment1.onSeg(t) + Segment2.onSeg(t) + ... = 1 Aufenthalt nur in Segmenten erlaubt
|
|
711
|
-
# b) eq: -On(t) + Segment1.onSeg(t) + Segment2.onSeg(t) + ... = 0 zusätzlich kann alles auch Null sein
|
|
712
|
-
in_single_segment = create_equation('in_single_Segment', self)
|
|
713
|
-
for segment_model in self._segment_models:
|
|
714
|
-
in_single_segment.add_summand(segment_model.in_segment, 1)
|
|
715
|
-
|
|
716
|
-
# a) or b) ?
|
|
717
|
-
if isinstance(self._can_be_outside_segments, Variable): # Use existing Variable
|
|
718
|
-
self.outside_segments = self._can_be_outside_segments
|
|
719
|
-
in_single_segment.add_summand(self.outside_segments, -1)
|
|
720
|
-
elif self._can_be_outside_segments is True: # Create Variable
|
|
721
|
-
length = system_model.nr_of_time_steps if self._as_time_series else 1
|
|
722
|
-
self.outside_segments = create_variable('outside_segments', self, length, is_binary=True)
|
|
723
|
-
in_single_segment.add_summand(self.outside_segments, -1)
|
|
724
|
-
else: # Dont allow outside Segments
|
|
725
|
-
in_single_segment.add_constant(1)
|
|
726
|
-
|
|
727
|
-
@property
|
|
728
|
-
def _nr_of_segments(self):
|
|
729
|
-
return len(next(iter(self._sample_points.values())))
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
class ShareAllocationModel(ElementModel):
|
|
733
|
-
def __init__(
|
|
734
|
-
self,
|
|
735
|
-
element: Element,
|
|
736
|
-
label: str,
|
|
737
|
-
shares_are_time_series: bool,
|
|
738
|
-
total_max: Optional[Skalar] = None,
|
|
739
|
-
total_min: Optional[Skalar] = None,
|
|
740
|
-
max_per_hour: Optional[Numeric] = None,
|
|
741
|
-
min_per_hour: Optional[Numeric] = None,
|
|
742
|
-
):
|
|
743
|
-
super().__init__(element, label)
|
|
744
|
-
if not shares_are_time_series: # If the condition is True
|
|
745
|
-
assert max_per_hour is None and min_per_hour is None, (
|
|
746
|
-
'Both max_per_hour and min_per_hour cannot be used when shares_are_time_series is False'
|
|
747
|
-
)
|
|
748
|
-
self.element = element
|
|
749
|
-
self.sum_TS: Optional[VariableTS] = None
|
|
750
|
-
self.sum: Optional[Variable] = None
|
|
751
|
-
self.shares: Dict[str, Variable] = {}
|
|
752
|
-
|
|
753
|
-
self._eq_time_series: Optional[Equation] = None
|
|
754
|
-
self._eq_sum: Optional[Equation] = None
|
|
755
|
-
|
|
756
|
-
# Parameters
|
|
757
|
-
self._shares_are_time_series = shares_are_time_series
|
|
758
|
-
self._total_max = total_max
|
|
759
|
-
self._total_min = total_min
|
|
760
|
-
self._max_per_hour = max_per_hour
|
|
761
|
-
self._min_per_hour = min_per_hour
|
|
762
|
-
|
|
763
|
-
def do_modeling(self, system_model: SystemModel):
|
|
764
|
-
self.sum = create_variable(
|
|
765
|
-
f'{self.label}_sum', self, 1, lower_bound=self._total_min, upper_bound=self._total_max
|
|
766
|
-
)
|
|
767
|
-
# eq: sum = sum(share_i) # skalar
|
|
768
|
-
self._eq_sum = create_equation(f'{self.label}_sum', self)
|
|
769
|
-
self._eq_sum.add_summand(self.sum, -1)
|
|
770
|
-
|
|
771
|
-
if self._shares_are_time_series:
|
|
772
|
-
lb_ts = None if (self._min_per_hour is None) else np.multiply(self._min_per_hour, system_model.dt_in_hours)
|
|
773
|
-
ub_ts = None if (self._max_per_hour is None) else np.multiply(self._max_per_hour, system_model.dt_in_hours)
|
|
774
|
-
self.sum_TS = create_variable(
|
|
775
|
-
f'{self.label}_sum_TS', self, system_model.nr_of_time_steps, lower_bound=lb_ts, upper_bound=ub_ts
|
|
776
|
-
)
|
|
777
|
-
|
|
778
|
-
# eq: sum_TS = sum(share_TS_i) # TS
|
|
779
|
-
self._eq_time_series = create_equation(f'{self.label}_time_series', self)
|
|
780
|
-
self._eq_time_series.add_summand(self.sum_TS, -1)
|
|
781
|
-
|
|
782
|
-
# eq: sum = sum(sum_TS(t)) # additionaly to self.sum
|
|
783
|
-
self._eq_sum.add_summand(self.sum_TS, 1, as_sum=True)
|
|
784
|
-
|
|
785
|
-
def add_share(
|
|
786
|
-
self,
|
|
787
|
-
system_model: SystemModel,
|
|
788
|
-
name_of_share: str,
|
|
789
|
-
variable: Optional[Variable],
|
|
790
|
-
factor: Numeric,
|
|
791
|
-
share_as_sum: bool = False,
|
|
792
|
-
):
|
|
793
|
-
"""
|
|
794
|
-
Adding a Share to a Share Allocation Model.
|
|
795
|
-
"""
|
|
796
|
-
# TODO: accept only one factor or accept unlimited factors -> *factors
|
|
797
|
-
|
|
798
|
-
# Check to which equation the share should be added
|
|
799
|
-
if share_as_sum or not self._shares_are_time_series:
|
|
800
|
-
target_eq = self._eq_sum
|
|
801
|
-
else:
|
|
802
|
-
target_eq = self._eq_time_series
|
|
803
|
-
|
|
804
|
-
new_share = SingleShareModel(self.element, name_of_share, variable, factor, share_as_sum)
|
|
805
|
-
target_eq.add_summand(new_share.single_share, 1)
|
|
806
|
-
|
|
807
|
-
self.sub_models.append(new_share)
|
|
808
|
-
assert new_share.label not in self.shares, (
|
|
809
|
-
f'A Share with the label {new_share.label} was already present in {self.label}'
|
|
810
|
-
)
|
|
811
|
-
self.shares[new_share.label] = new_share.single_share
|
|
812
|
-
|
|
813
|
-
def results(self):
|
|
814
|
-
return {
|
|
815
|
-
**{variable.label_short: variable.result for variable in self.variables.values()},
|
|
816
|
-
**{'Shares': {variable.label_short: variable.result for variable in self.shares.values()}},
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
class SingleShareModel(ElementModel):
|
|
821
|
-
"""Holds a Variable and an Equation. Summands can be added to the Equation. Used to publish Shares"""
|
|
822
|
-
|
|
823
|
-
def __init__(self, element: Element, name: str, variable: Optional[Variable], factor: Numeric, share_as_sum: bool):
|
|
824
|
-
super().__init__(element, name)
|
|
825
|
-
if variable is not None:
|
|
826
|
-
assert not (variable.length == 1 and share_as_sum), 'A Variable with the length 1 cannot be summed up!'
|
|
827
|
-
|
|
828
|
-
if (
|
|
829
|
-
share_as_sum
|
|
830
|
-
or (variable is not None and variable.length == 1)
|
|
831
|
-
or (variable is None and np.isscalar(factor))
|
|
832
|
-
):
|
|
833
|
-
self.single_share = Variable(self.label_full, 1, self.label)
|
|
834
|
-
elif variable is not None:
|
|
835
|
-
self.single_share = VariableTS(self.label_full, variable.length, self.label)
|
|
836
|
-
else:
|
|
837
|
-
raise Exception('This case is not yet covered for a SingleShareModel')
|
|
838
|
-
|
|
839
|
-
self.add_variables(self.single_share)
|
|
840
|
-
self.single_equation = create_equation(self.label_full, self)
|
|
841
|
-
self.single_equation.add_summand(self.single_share, -1)
|
|
842
|
-
|
|
843
|
-
if variable is None:
|
|
844
|
-
self.single_equation.add_constant(-1 * np.sum(factor) if share_as_sum else -1 * factor)
|
|
845
|
-
else:
|
|
846
|
-
self.single_equation.add_summand(variable, factor, as_sum=share_as_sum)
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
class SegmentedSharesModel(ElementModel):
|
|
850
|
-
# TODO: Length...
|
|
851
|
-
def __init__(
|
|
852
|
-
self,
|
|
853
|
-
element: Element,
|
|
854
|
-
variable_segments: Tuple[Variable, List[Tuple[Skalar, Skalar]]],
|
|
855
|
-
share_segments: Dict['Effect', List[Tuple[Skalar, Skalar]]],
|
|
856
|
-
can_be_outside_segments: Optional[Union[bool, Variable]],
|
|
857
|
-
label: str = 'SegmentedShares',
|
|
858
|
-
):
|
|
859
|
-
super().__init__(element, label)
|
|
860
|
-
assert len(variable_segments[1]) == len(list(share_segments.values())[0]), (
|
|
861
|
-
'Segment length of variable_segments and share_segments must be equal'
|
|
862
|
-
)
|
|
863
|
-
self.element: Element
|
|
864
|
-
self._can_be_outside_segments = can_be_outside_segments
|
|
865
|
-
self._variable_segments = variable_segments
|
|
866
|
-
self._share_segments = share_segments
|
|
867
|
-
self._shares: Optional[Dict['Effect', SingleShareModel]] = None
|
|
868
|
-
self._segments_model: Optional[MultipleSegmentsModel] = None
|
|
869
|
-
self._as_tme_series: bool = isinstance(self._variable_segments[0], VariableTS)
|
|
870
|
-
|
|
871
|
-
def do_modeling(self, system_model: SystemModel):
|
|
872
|
-
length = system_model.nr_of_time_steps if self._as_tme_series else 1
|
|
873
|
-
self._shares = {
|
|
874
|
-
effect: create_variable(f'{effect.label}_segmented', self, length) for effect in self._share_segments
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
segments: Dict[Variable, List[Tuple[Skalar, Skalar]]] = {
|
|
878
|
-
**{self._shares[effect]: segment for effect, segment in self._share_segments.items()},
|
|
879
|
-
**{self._variable_segments[0]: self._variable_segments[1]},
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
self._segments_model = MultipleSegmentsModel(
|
|
883
|
-
self.element,
|
|
884
|
-
segments,
|
|
885
|
-
can_be_outside_segments=self._can_be_outside_segments,
|
|
886
|
-
as_time_series=self._as_tme_series,
|
|
887
|
-
)
|
|
888
|
-
self._segments_model.do_modeling(system_model)
|
|
889
|
-
self.sub_models.append(self._segments_model)
|
|
890
|
-
|
|
891
|
-
# Shares
|
|
892
|
-
effect_collection = system_model.effect_collection_model
|
|
893
|
-
for effect, variable in self._shares.items():
|
|
894
|
-
if self._as_tme_series:
|
|
895
|
-
effect_collection.add_share_to_operation(
|
|
896
|
-
name='segmented_effects',
|
|
897
|
-
element=self.element,
|
|
898
|
-
effect_values={effect: 1},
|
|
899
|
-
factor=1,
|
|
900
|
-
variable=variable,
|
|
901
|
-
)
|
|
902
|
-
else:
|
|
903
|
-
effect_collection.add_share_to_invest(
|
|
904
|
-
name='segmented_effects',
|
|
905
|
-
element=self.element,
|
|
906
|
-
effect_values={effect: 1},
|
|
907
|
-
factor=1,
|
|
908
|
-
variable=variable,
|
|
909
|
-
)
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
class PreventSimultaneousUsageModel(ElementModel):
|
|
913
|
-
"""
|
|
914
|
-
Prevents multiple Multiple Binary variables from being 1 at the same time
|
|
915
|
-
|
|
916
|
-
Only 'classic type is modeled for now (# "classic" -> alle Flows brauchen Binärvariable:)
|
|
917
|
-
In 'new', the binary Variables need to be forced beforehand, which is not that straight forward... --> TODO maybe
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
# "new":
|
|
921
|
-
# eq: flow_1.on(t) + flow_2.on(t) + .. + flow_i.val(t)/flow_i.max <= 1 (1 Flow ohne Binärvariable!)
|
|
922
|
-
|
|
923
|
-
# Anmerkung: Patrick Schönfeld (oemof, custom/link.py) macht bei 2 Flows ohne Binärvariable dies:
|
|
924
|
-
# 1) bin + flow1/flow1_max <= 1
|
|
925
|
-
# 2) bin - flow2/flow2_max >= 0
|
|
926
|
-
# 3) geht nur, wenn alle flow.min >= 0
|
|
927
|
-
# --> könnte man auch umsetzen (statt force_on_variable() für die Flows, aber sollte aufs selbe wie "new" kommen)
|
|
928
|
-
"""
|
|
929
|
-
|
|
930
|
-
def __init__(self, element: Element, variables: List[VariableTS], label: str = 'PreventSimultaneousUsage'):
|
|
931
|
-
super().__init__(element, label)
|
|
932
|
-
self._variables = variables
|
|
933
|
-
assert len(self._variables) >= 2, f'Model {self.__class__.__name__} must get at least two variables'
|
|
934
|
-
for variable in self._variables: # classic
|
|
935
|
-
assert variable.is_binary, f'Variable {variable} must be binary for use in {self.__class__.__name__}'
|
|
936
|
-
|
|
937
|
-
def do_modeling(self, system_model: SystemModel):
|
|
938
|
-
# eq: sum(flow_i.on(t)) <= 1.1 (1 wird etwas größer gewählt wg. Binärvariablengenauigkeit)
|
|
939
|
-
eq = create_equation('prevent_simultaneous_use', self, eq_type='ineq')
|
|
940
|
-
for variable in self._variables:
|
|
941
|
-
eq.add_summand(variable, 1)
|
|
942
|
-
eq.add_constant(1.1)
|