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.

Files changed (73) hide show
  1. docs/examples/00-Minimal Example.md +5 -0
  2. docs/examples/01-Basic Example.md +5 -0
  3. docs/examples/02-Complex Example.md +10 -0
  4. docs/examples/03-Calculation Modes.md +5 -0
  5. docs/examples/index.md +5 -0
  6. docs/faq/contribute.md +49 -0
  7. docs/faq/index.md +3 -0
  8. docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
  9. docs/images/architecture_flixOpt.png +0 -0
  10. docs/images/flixopt-icon.svg +1 -0
  11. docs/javascripts/mathjax.js +18 -0
  12. docs/release-notes/_template.txt +32 -0
  13. docs/release-notes/index.md +7 -0
  14. docs/release-notes/v2.0.0.md +93 -0
  15. docs/release-notes/v2.0.1.md +12 -0
  16. docs/user-guide/Mathematical Notation/Bus.md +33 -0
  17. docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +132 -0
  18. docs/user-guide/Mathematical Notation/Flow.md +26 -0
  19. docs/user-guide/Mathematical Notation/LinearConverter.md +21 -0
  20. docs/user-guide/Mathematical Notation/Piecewise.md +49 -0
  21. docs/user-guide/Mathematical Notation/Storage.md +44 -0
  22. docs/user-guide/Mathematical Notation/index.md +22 -0
  23. docs/user-guide/Mathematical Notation/others.md +3 -0
  24. docs/user-guide/index.md +124 -0
  25. {flixOpt → flixopt}/__init__.py +5 -2
  26. {flixOpt → flixopt}/aggregation.py +113 -140
  27. flixopt/calculation.py +455 -0
  28. {flixOpt → flixopt}/commons.py +7 -4
  29. flixopt/components.py +630 -0
  30. {flixOpt → flixopt}/config.py +9 -8
  31. {flixOpt → flixopt}/config.yaml +3 -3
  32. flixopt/core.py +970 -0
  33. flixopt/effects.py +386 -0
  34. flixopt/elements.py +534 -0
  35. flixopt/features.py +1042 -0
  36. flixopt/flow_system.py +409 -0
  37. flixopt/interface.py +265 -0
  38. flixopt/io.py +308 -0
  39. flixopt/linear_converters.py +331 -0
  40. flixopt/plotting.py +1340 -0
  41. flixopt/results.py +898 -0
  42. flixopt/solvers.py +77 -0
  43. flixopt/structure.py +630 -0
  44. flixopt/utils.py +62 -0
  45. flixopt-2.0.1.dist-info/METADATA +145 -0
  46. flixopt-2.0.1.dist-info/RECORD +57 -0
  47. {flixopt-1.0.12.dist-info → flixopt-2.0.1.dist-info}/WHEEL +1 -1
  48. flixopt-2.0.1.dist-info/top_level.txt +6 -0
  49. pics/architecture_flixOpt-pre2.0.0.png +0 -0
  50. pics/architecture_flixOpt.png +0 -0
  51. pics/flixopt-icon.svg +1 -0
  52. pics/pics.pptx +0 -0
  53. scripts/gen_ref_pages.py +54 -0
  54. site/release-notes/_template.txt +32 -0
  55. flixOpt/calculation.py +0 -629
  56. flixOpt/components.py +0 -614
  57. flixOpt/core.py +0 -182
  58. flixOpt/effects.py +0 -410
  59. flixOpt/elements.py +0 -489
  60. flixOpt/features.py +0 -942
  61. flixOpt/flow_system.py +0 -351
  62. flixOpt/interface.py +0 -203
  63. flixOpt/linear_converters.py +0 -325
  64. flixOpt/math_modeling.py +0 -1145
  65. flixOpt/plotting.py +0 -712
  66. flixOpt/results.py +0 -563
  67. flixOpt/solvers.py +0 -21
  68. flixOpt/structure.py +0 -733
  69. flixOpt/utils.py +0 -134
  70. flixopt-1.0.12.dist-info/METADATA +0 -174
  71. flixopt-1.0.12.dist-info/RECORD +0 -29
  72. flixopt-1.0.12.dist-info/top_level.txt +0 -3
  73. {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)