flixopt 2.1.1__py3-none-any.whl → 2.2.0b0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of flixopt might be problematic. Click here for more details.
- docs/release-notes/v2.2.0.md +55 -0
- docs/user-guide/Mathematical Notation/Investment.md +115 -0
- flixopt/calculation.py +65 -37
- flixopt/components.py +115 -73
- flixopt/core.py +966 -451
- flixopt/effects.py +269 -65
- flixopt/elements.py +79 -49
- flixopt/features.py +134 -85
- flixopt/flow_system.py +99 -16
- flixopt/interface.py +142 -51
- flixopt/io.py +56 -27
- flixopt/linear_converters.py +3 -3
- flixopt/plotting.py +34 -16
- flixopt/results.py +806 -108
- flixopt/structure.py +64 -10
- flixopt/utils.py +6 -9
- {flixopt-2.1.1.dist-info → flixopt-2.2.0b0.dist-info}/METADATA +1 -1
- {flixopt-2.1.1.dist-info → flixopt-2.2.0b0.dist-info}/RECORD +21 -21
- {flixopt-2.1.1.dist-info → flixopt-2.2.0b0.dist-info}/WHEEL +1 -1
- {flixopt-2.1.1.dist-info → flixopt-2.2.0b0.dist-info}/top_level.txt +0 -1
- docs/release-notes/v2.1.1.md +0 -11
- site/release-notes/_template.txt +0 -32
- {flixopt-2.1.1.dist-info → flixopt-2.2.0b0.dist-info}/licenses/LICENSE +0 -0
flixopt/components.py
CHANGED
|
@@ -9,7 +9,7 @@ import linopy
|
|
|
9
9
|
import numpy as np
|
|
10
10
|
|
|
11
11
|
from . import utils
|
|
12
|
-
from .core import
|
|
12
|
+
from .core import PlausibilityError, Scalar, ScenarioData, TimeSeries, TimestepData
|
|
13
13
|
from .elements import Component, ComponentModel, Flow
|
|
14
14
|
from .features import InvestmentModel, OnOffModel, PiecewiseModel
|
|
15
15
|
from .interface import InvestParameters, OnOffParameters, PiecewiseConversion
|
|
@@ -34,7 +34,7 @@ class LinearConverter(Component):
|
|
|
34
34
|
inputs: List[Flow],
|
|
35
35
|
outputs: List[Flow],
|
|
36
36
|
on_off_parameters: OnOffParameters = None,
|
|
37
|
-
conversion_factors: List[Dict[str,
|
|
37
|
+
conversion_factors: List[Dict[str, TimestepData]] = None,
|
|
38
38
|
piecewise_conversion: Optional[PiecewiseConversion] = None,
|
|
39
39
|
meta_data: Optional[Dict] = None,
|
|
40
40
|
):
|
|
@@ -86,9 +86,9 @@ class LinearConverter(Component):
|
|
|
86
86
|
if self.piecewise_conversion:
|
|
87
87
|
for flow in self.flows.values():
|
|
88
88
|
if isinstance(flow.size, InvestParameters) and flow.size.fixed_size is None:
|
|
89
|
-
|
|
90
|
-
f'
|
|
91
|
-
f'(in
|
|
89
|
+
logger.warning(
|
|
90
|
+
f'Using a FLow with a fixed size ({flow.label_full}) AND a piecewise_conversion '
|
|
91
|
+
f'(in {self.label_full}) and variable size is uncommon. Please check if this is intended!'
|
|
92
92
|
)
|
|
93
93
|
|
|
94
94
|
def transform_data(self, flow_system: 'FlowSystem'):
|
|
@@ -96,6 +96,7 @@ class LinearConverter(Component):
|
|
|
96
96
|
if self.conversion_factors:
|
|
97
97
|
self.conversion_factors = self._transform_conversion_factors(flow_system)
|
|
98
98
|
if self.piecewise_conversion:
|
|
99
|
+
self.piecewise_conversion.has_time_dim = True
|
|
99
100
|
self.piecewise_conversion.transform_data(flow_system, f'{self.label_full}|PiecewiseConversion')
|
|
100
101
|
|
|
101
102
|
def _transform_conversion_factors(self, flow_system: 'FlowSystem') -> List[Dict[str, TimeSeries]]:
|
|
@@ -127,16 +128,17 @@ class Storage(Component):
|
|
|
127
128
|
label: str,
|
|
128
129
|
charging: Flow,
|
|
129
130
|
discharging: Flow,
|
|
130
|
-
capacity_in_flow_hours: Union[
|
|
131
|
-
relative_minimum_charge_state:
|
|
132
|
-
relative_maximum_charge_state:
|
|
133
|
-
initial_charge_state: Union[
|
|
134
|
-
minimal_final_charge_state: Optional[
|
|
135
|
-
maximal_final_charge_state: Optional[
|
|
136
|
-
eta_charge:
|
|
137
|
-
eta_discharge:
|
|
138
|
-
relative_loss_per_hour:
|
|
131
|
+
capacity_in_flow_hours: Union[ScenarioData, InvestParameters],
|
|
132
|
+
relative_minimum_charge_state: TimestepData = 0,
|
|
133
|
+
relative_maximum_charge_state: TimestepData = 1,
|
|
134
|
+
initial_charge_state: Union[ScenarioData, Literal['lastValueOfSim']] = 0,
|
|
135
|
+
minimal_final_charge_state: Optional[ScenarioData] = None,
|
|
136
|
+
maximal_final_charge_state: Optional[ScenarioData] = None,
|
|
137
|
+
eta_charge: TimestepData = 1,
|
|
138
|
+
eta_discharge: TimestepData = 1,
|
|
139
|
+
relative_loss_per_hour: TimestepData = 0,
|
|
139
140
|
prevent_simultaneous_charge_and_discharge: bool = True,
|
|
141
|
+
balanced: bool = False,
|
|
140
142
|
meta_data: Optional[Dict] = None,
|
|
141
143
|
):
|
|
142
144
|
"""
|
|
@@ -162,6 +164,7 @@ class Storage(Component):
|
|
|
162
164
|
relative_loss_per_hour: loss per chargeState-Unit per hour. The default is 0.
|
|
163
165
|
prevent_simultaneous_charge_and_discharge: If True, loading and unloading at the same time is not possible.
|
|
164
166
|
Increases the number of binary variables, but is recommended for easier evaluation. The default is True.
|
|
167
|
+
balanced: Wether to equate the size of the charging and discharging flow. Only if not fixed.
|
|
165
168
|
meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
|
|
166
169
|
"""
|
|
167
170
|
# TODO: fixed_relative_chargeState implementieren
|
|
@@ -176,17 +179,18 @@ class Storage(Component):
|
|
|
176
179
|
self.charging = charging
|
|
177
180
|
self.discharging = discharging
|
|
178
181
|
self.capacity_in_flow_hours = capacity_in_flow_hours
|
|
179
|
-
self.relative_minimum_charge_state:
|
|
180
|
-
self.relative_maximum_charge_state:
|
|
182
|
+
self.relative_minimum_charge_state: TimestepData = relative_minimum_charge_state
|
|
183
|
+
self.relative_maximum_charge_state: TimestepData = relative_maximum_charge_state
|
|
181
184
|
|
|
182
185
|
self.initial_charge_state = initial_charge_state
|
|
183
186
|
self.minimal_final_charge_state = minimal_final_charge_state
|
|
184
187
|
self.maximal_final_charge_state = maximal_final_charge_state
|
|
185
188
|
|
|
186
|
-
self.eta_charge:
|
|
187
|
-
self.eta_discharge:
|
|
188
|
-
self.relative_loss_per_hour:
|
|
189
|
+
self.eta_charge: TimestepData = eta_charge
|
|
190
|
+
self.eta_discharge: TimestepData = eta_discharge
|
|
191
|
+
self.relative_loss_per_hour: TimestepData = relative_loss_per_hour
|
|
189
192
|
self.prevent_simultaneous_charge_and_discharge = prevent_simultaneous_charge_and_discharge
|
|
193
|
+
self.balanced = balanced
|
|
190
194
|
|
|
191
195
|
def create_model(self, model: SystemModel) -> 'StorageModel':
|
|
192
196
|
self._plausibility_checks()
|
|
@@ -198,55 +202,83 @@ class Storage(Component):
|
|
|
198
202
|
self.relative_minimum_charge_state = flow_system.create_time_series(
|
|
199
203
|
f'{self.label_full}|relative_minimum_charge_state',
|
|
200
204
|
self.relative_minimum_charge_state,
|
|
201
|
-
|
|
205
|
+
has_extra_timestep=True,
|
|
202
206
|
)
|
|
203
207
|
self.relative_maximum_charge_state = flow_system.create_time_series(
|
|
204
208
|
f'{self.label_full}|relative_maximum_charge_state',
|
|
205
209
|
self.relative_maximum_charge_state,
|
|
206
|
-
|
|
210
|
+
has_extra_timestep=True,
|
|
207
211
|
)
|
|
208
212
|
self.eta_charge = flow_system.create_time_series(f'{self.label_full}|eta_charge', self.eta_charge)
|
|
209
213
|
self.eta_discharge = flow_system.create_time_series(f'{self.label_full}|eta_discharge', self.eta_discharge)
|
|
210
214
|
self.relative_loss_per_hour = flow_system.create_time_series(
|
|
211
215
|
f'{self.label_full}|relative_loss_per_hour', self.relative_loss_per_hour
|
|
212
216
|
)
|
|
217
|
+
if not isinstance(self.initial_charge_state, str):
|
|
218
|
+
self.initial_charge_state = flow_system.create_time_series(
|
|
219
|
+
f'{self.label_full}|initial_charge_state', self.initial_charge_state, has_time_dim=False
|
|
220
|
+
)
|
|
221
|
+
self.minimal_final_charge_state = flow_system.create_time_series(
|
|
222
|
+
f'{self.label_full}|minimal_final_charge_state', self.minimal_final_charge_state, has_time_dim=False
|
|
223
|
+
)
|
|
224
|
+
self.maximal_final_charge_state = flow_system.create_time_series(
|
|
225
|
+
f'{self.label_full}|maximal_final_charge_state', self.maximal_final_charge_state, has_time_dim=False
|
|
226
|
+
)
|
|
213
227
|
if isinstance(self.capacity_in_flow_hours, InvestParameters):
|
|
214
|
-
self.capacity_in_flow_hours.transform_data(flow_system)
|
|
228
|
+
self.capacity_in_flow_hours.transform_data(flow_system, f'{self.label_full}|InvestParameters')
|
|
229
|
+
else:
|
|
230
|
+
self.capacity_in_flow_hours = flow_system.create_time_series(
|
|
231
|
+
f'{self.label_full}|capacity_in_flow_hours', self.capacity_in_flow_hours, has_time_dim=False
|
|
232
|
+
)
|
|
215
233
|
|
|
216
234
|
def _plausibility_checks(self) -> None:
|
|
217
235
|
"""
|
|
218
236
|
Check for infeasible or uncommon combinations of parameters
|
|
219
237
|
"""
|
|
220
238
|
super()._plausibility_checks()
|
|
221
|
-
if
|
|
222
|
-
if
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
239
|
+
if isinstance(self.initial_charge_state, str):
|
|
240
|
+
if self.initial_charge_state != 'lastValueOfSim':
|
|
241
|
+
raise PlausibilityError(f'initial_charge_state has undefined value: {self.initial_charge_state}')
|
|
242
|
+
return
|
|
243
|
+
if isinstance(self.capacity_in_flow_hours, InvestParameters):
|
|
244
|
+
if self.capacity_in_flow_hours.fixed_size is None:
|
|
245
|
+
maximum_capacity = self.capacity_in_flow_hours.maximum_size
|
|
246
|
+
minimum_capacity = self.capacity_in_flow_hours.minimum_size
|
|
229
247
|
else:
|
|
230
|
-
maximum_capacity = self.capacity_in_flow_hours
|
|
231
|
-
minimum_capacity = self.capacity_in_flow_hours
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
248
|
+
maximum_capacity = self.capacity_in_flow_hours.fixed_size
|
|
249
|
+
minimum_capacity = self.capacity_in_flow_hours.fixed_size
|
|
250
|
+
else:
|
|
251
|
+
maximum_capacity = self.capacity_in_flow_hours
|
|
252
|
+
minimum_capacity = self.capacity_in_flow_hours
|
|
253
|
+
|
|
254
|
+
# initial capacity >= allowed min for maximum_size:
|
|
255
|
+
minimum_inital_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=0)
|
|
256
|
+
# initial capacity <= allowed max for minimum_size:
|
|
257
|
+
maximum_inital_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=0)
|
|
258
|
+
# TODO: index=1 ??? I think index 0
|
|
259
|
+
|
|
260
|
+
if (self.initial_charge_state > maximum_inital_capacity).any():
|
|
261
|
+
raise ValueError(
|
|
262
|
+
f'{self.label_full}: {self.initial_charge_state=} '
|
|
263
|
+
f'is above allowed maximum charge_state {maximum_inital_capacity}'
|
|
264
|
+
)
|
|
265
|
+
if (self.initial_charge_state < minimum_inital_capacity).any():
|
|
266
|
+
raise ValueError(
|
|
267
|
+
f'{self.label_full}: {self.initial_charge_state=} '
|
|
268
|
+
f'is below allowed minimum charge_state {minimum_inital_capacity}'
|
|
269
|
+
)
|
|
237
270
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
f'
|
|
242
|
-
|
|
243
|
-
if self.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
f'
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
raise ValueError(f'{self.label_full}: {self.initial_charge_state=} has an invalid value')
|
|
271
|
+
if self.balanced:
|
|
272
|
+
if not isinstance(self.charging.size, InvestParameters) or not isinstance(self.discharging.size, InvestParameters):
|
|
273
|
+
raise PlausibilityError(
|
|
274
|
+
f'Balancing charging and discharging Flows in {self.label_full} '
|
|
275
|
+
f'is only possible with Investments.')
|
|
276
|
+
if (self.charging.size.minimum_size > self.discharging.size.maximum_size or
|
|
277
|
+
self.charging.size.maximum_size < self.discharging.size.minimum_size):
|
|
278
|
+
raise PlausibilityError(
|
|
279
|
+
f'Balancing charging and discharging Flows in {self.label_full} need compatible minimum and maximum sizes.'
|
|
280
|
+
f'Got: {self.charging.size.minimum_size=}, {self.charging.size.maximum_size=} and '
|
|
281
|
+
f'{self.charging.size.minimum_size=}, {self.charging.size.maximum_size=}.')
|
|
250
282
|
|
|
251
283
|
|
|
252
284
|
@register_class_for_io
|
|
@@ -264,8 +296,8 @@ class Transmission(Component):
|
|
|
264
296
|
out1: Flow,
|
|
265
297
|
in2: Optional[Flow] = None,
|
|
266
298
|
out2: Optional[Flow] = None,
|
|
267
|
-
relative_losses: Optional[
|
|
268
|
-
absolute_losses: Optional[
|
|
299
|
+
relative_losses: Optional[TimestepData] = None,
|
|
300
|
+
absolute_losses: Optional[TimestepData] = None,
|
|
269
301
|
on_off_parameters: OnOffParameters = None,
|
|
270
302
|
prevent_simultaneous_flows_in_both_directions: bool = True,
|
|
271
303
|
meta_data: Optional[Dict] = None,
|
|
@@ -348,7 +380,7 @@ class TransmissionModel(ComponentModel):
|
|
|
348
380
|
def do_modeling(self):
|
|
349
381
|
"""Initiates all FlowModels"""
|
|
350
382
|
# Force On Variable if absolute losses are present
|
|
351
|
-
if (self.element.absolute_losses is not None) and np.any(self.element.absolute_losses.
|
|
383
|
+
if (self.element.absolute_losses is not None) and np.any(self.element.absolute_losses.selected_data != 0):
|
|
352
384
|
for flow in self.element.inputs + self.element.outputs:
|
|
353
385
|
if flow.on_off_parameters is None:
|
|
354
386
|
flow.on_off_parameters = OnOffParameters()
|
|
@@ -385,14 +417,14 @@ class TransmissionModel(ComponentModel):
|
|
|
385
417
|
# eq: out(t) + on(t)*loss_abs(t) = in(t)*(1 - loss_rel(t))
|
|
386
418
|
con_transmission = self.add(
|
|
387
419
|
self._model.add_constraints(
|
|
388
|
-
out_flow.model.flow_rate == -in_flow.model.flow_rate * (self.element.relative_losses.
|
|
420
|
+
out_flow.model.flow_rate == -in_flow.model.flow_rate * (self.element.relative_losses.selected_data - 1),
|
|
389
421
|
name=f'{self.label_full}|{name}',
|
|
390
422
|
),
|
|
391
423
|
name,
|
|
392
424
|
)
|
|
393
425
|
|
|
394
426
|
if self.element.absolute_losses is not None:
|
|
395
|
-
con_transmission.lhs += in_flow.model.on_off.on * self.element.absolute_losses.
|
|
427
|
+
con_transmission.lhs += in_flow.model.on_off.on * self.element.absolute_losses.selected_data
|
|
396
428
|
|
|
397
429
|
return con_transmission
|
|
398
430
|
|
|
@@ -420,8 +452,10 @@ class LinearConverterModel(ComponentModel):
|
|
|
420
452
|
|
|
421
453
|
self.add(
|
|
422
454
|
self._model.add_constraints(
|
|
423
|
-
sum([flow.model.flow_rate * conv_factors[flow.label].
|
|
424
|
-
== sum(
|
|
455
|
+
sum([flow.model.flow_rate * conv_factors[flow.label].selected_data for flow in used_inputs])
|
|
456
|
+
== sum(
|
|
457
|
+
[flow.model.flow_rate * conv_factors[flow.label].selected_data for flow in used_outputs]
|
|
458
|
+
),
|
|
425
459
|
name=f'{self.label_full}|conversion_{i}',
|
|
426
460
|
)
|
|
427
461
|
)
|
|
@@ -461,12 +495,15 @@ class StorageModel(ComponentModel):
|
|
|
461
495
|
lb, ub = self.absolute_charge_state_bounds
|
|
462
496
|
self.charge_state = self.add(
|
|
463
497
|
self._model.add_variables(
|
|
464
|
-
lower=lb,
|
|
498
|
+
lower=lb,
|
|
499
|
+
upper=ub,
|
|
500
|
+
coords=self._model.get_coords(extra_timestep=True),
|
|
501
|
+
name=f'{self.label_full}|charge_state',
|
|
465
502
|
),
|
|
466
503
|
'charge_state',
|
|
467
504
|
)
|
|
468
505
|
self.netto_discharge = self.add(
|
|
469
|
-
self._model.add_variables(coords=self._model.
|
|
506
|
+
self._model.add_variables(coords=self._model.get_coords(), name=f'{self.label_full}|netto_discharge'),
|
|
470
507
|
'netto_discharge',
|
|
471
508
|
)
|
|
472
509
|
# netto_discharge:
|
|
@@ -481,12 +518,12 @@ class StorageModel(ComponentModel):
|
|
|
481
518
|
)
|
|
482
519
|
|
|
483
520
|
charge_state = self.charge_state
|
|
484
|
-
rel_loss = self.element.relative_loss_per_hour.
|
|
521
|
+
rel_loss = self.element.relative_loss_per_hour.selected_data
|
|
485
522
|
hours_per_step = self._model.hours_per_step
|
|
486
523
|
charge_rate = self.element.charging.model.flow_rate
|
|
487
524
|
discharge_rate = self.element.discharging.model.flow_rate
|
|
488
|
-
eff_charge = self.element.eta_charge.
|
|
489
|
-
eff_discharge = self.element.eta_discharge.
|
|
525
|
+
eff_charge = self.element.eta_charge.selected_data
|
|
526
|
+
eff_discharge = self.element.eta_discharge.selected_data
|
|
490
527
|
|
|
491
528
|
self.add(
|
|
492
529
|
self._model.add_constraints(
|
|
@@ -513,29 +550,34 @@ class StorageModel(ComponentModel):
|
|
|
513
550
|
# Initial charge state
|
|
514
551
|
self._initial_and_final_charge_state()
|
|
515
552
|
|
|
553
|
+
if self.element.balanced:
|
|
554
|
+
self.add(
|
|
555
|
+
self._model.add_constraints(
|
|
556
|
+
self.element.charging.model._investment.size * 1 == self.element.discharging.model._investment.size * 1,
|
|
557
|
+
name=f'{self.label_full}|balanced_sizes',
|
|
558
|
+
),
|
|
559
|
+
'balanced_sizes'
|
|
560
|
+
)
|
|
561
|
+
|
|
516
562
|
def _initial_and_final_charge_state(self):
|
|
517
563
|
if self.element.initial_charge_state is not None:
|
|
518
564
|
name_short = 'initial_charge_state'
|
|
519
565
|
name = f'{self.label_full}|{name_short}'
|
|
520
566
|
|
|
521
|
-
if
|
|
567
|
+
if isinstance(self.element.initial_charge_state, str):
|
|
522
568
|
self.add(
|
|
523
569
|
self._model.add_constraints(
|
|
524
|
-
self.charge_state.isel(time=0) == self.
|
|
570
|
+
self.charge_state.isel(time=0) == self.charge_state.isel(time=-1), name=name
|
|
525
571
|
),
|
|
526
572
|
name_short,
|
|
527
573
|
)
|
|
528
|
-
|
|
574
|
+
else:
|
|
529
575
|
self.add(
|
|
530
576
|
self._model.add_constraints(
|
|
531
|
-
self.charge_state.isel(time=0) == self.
|
|
577
|
+
self.charge_state.isel(time=0) == self.element.initial_charge_state, name=name
|
|
532
578
|
),
|
|
533
579
|
name_short,
|
|
534
580
|
)
|
|
535
|
-
else: # TODO: Validation in Storage Class, not in Model
|
|
536
|
-
raise PlausibilityError(
|
|
537
|
-
f'initial_charge_state has undefined value: {self.element.initial_charge_state}'
|
|
538
|
-
)
|
|
539
581
|
|
|
540
582
|
if self.element.maximal_final_charge_state is not None:
|
|
541
583
|
self.add(
|
|
@@ -556,7 +598,7 @@ class StorageModel(ComponentModel):
|
|
|
556
598
|
)
|
|
557
599
|
|
|
558
600
|
@property
|
|
559
|
-
def absolute_charge_state_bounds(self) -> Tuple[
|
|
601
|
+
def absolute_charge_state_bounds(self) -> Tuple[TimestepData, TimestepData]:
|
|
560
602
|
relative_lower_bound, relative_upper_bound = self.relative_charge_state_bounds
|
|
561
603
|
if not isinstance(self.element.capacity_in_flow_hours, InvestParameters):
|
|
562
604
|
return (
|
|
@@ -570,10 +612,10 @@ class StorageModel(ComponentModel):
|
|
|
570
612
|
)
|
|
571
613
|
|
|
572
614
|
@property
|
|
573
|
-
def relative_charge_state_bounds(self) -> Tuple[
|
|
615
|
+
def relative_charge_state_bounds(self) -> Tuple[TimestepData, TimestepData]:
|
|
574
616
|
return (
|
|
575
|
-
self.element.relative_minimum_charge_state
|
|
576
|
-
self.element.relative_maximum_charge_state
|
|
617
|
+
self.element.relative_minimum_charge_state,
|
|
618
|
+
self.element.relative_maximum_charge_state,
|
|
577
619
|
)
|
|
578
620
|
|
|
579
621
|
|