flixopt 2.0.0__py3-none-any.whl → 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of flixopt might be problematic. Click here for more details.
- docs/release-notes/v2.0.1.md +12 -0
- docs/release-notes/v2.1.0.md +31 -0
- flixopt/components.py +13 -9
- flixopt/core.py +59 -3
- flixopt/elements.py +76 -31
- flixopt/features.py +407 -331
- flixopt/interface.py +5 -5
- flixopt/plotting.py +4 -1
- flixopt/structure.py +5 -5
- {flixopt-2.0.0.dist-info → flixopt-2.1.0.dist-info}/METADATA +8 -7
- {flixopt-2.0.0.dist-info → flixopt-2.1.0.dist-info}/RECORD +14 -12
- {flixopt-2.0.0.dist-info → flixopt-2.1.0.dist-info}/WHEEL +0 -0
- {flixopt-2.0.0.dist-info → flixopt-2.1.0.dist-info}/licenses/LICENSE +0 -0
- {flixopt-2.0.0.dist-info → flixopt-2.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Release v2.0.1
|
|
2
|
+
|
|
3
|
+
**Release Date:** 2025-04-10
|
|
4
|
+
|
|
5
|
+
## Improvements
|
|
6
|
+
|
|
7
|
+
* Add logger warning if relative_minimum is used without on_off_parameters in Flow, as this prevents the flow_rate from switching "OFF"
|
|
8
|
+
|
|
9
|
+
## Bug Fixes
|
|
10
|
+
|
|
11
|
+
* Replace "|" with "__" in filenames when saving figures, as "|" can lead to issues on windows
|
|
12
|
+
* Fixed a Bug that prevented the load factor from working without InvestmentParameters
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Release v2.1.0
|
|
2
|
+
|
|
3
|
+
**Release Date:** 2025-04-11
|
|
4
|
+
|
|
5
|
+
## Improvements
|
|
6
|
+
|
|
7
|
+
* Add logger warning if relative_minimum is used without on_off_parameters in Flow, as this prevents the flow_rate from switching "OFF"
|
|
8
|
+
* Python 3.13 support added
|
|
9
|
+
* Greatly improved internal testing infrastructure by leveraging linopy's testing framework
|
|
10
|
+
|
|
11
|
+
## Bug Fixes
|
|
12
|
+
|
|
13
|
+
* Bugfixing the lower bound of `flow_rate` when using optional investments without OnOffParameters.
|
|
14
|
+
* Fixes a Bug that prevented divest effects from working.
|
|
15
|
+
* added lower bounds of 0 to two unbounded vars (only numerical better)
|
|
16
|
+
|
|
17
|
+
## Breaking Changes
|
|
18
|
+
|
|
19
|
+
* We restructured the modeling of the On/Off state of FLows or Components. This leads to slightly renaming of variables and constraints.
|
|
20
|
+
|
|
21
|
+
### Variable renaming
|
|
22
|
+
* "...|consecutive_on_hours" is now "...|ConsecutiveOn|hours"
|
|
23
|
+
* "...|consecutive_off_hours" is now "...|ConsecutiveOff|hours"
|
|
24
|
+
|
|
25
|
+
### Constraint renaming
|
|
26
|
+
* "...|consecutive_on_hours_con1" is now "...|ConsecutiveOn|con1"
|
|
27
|
+
* "...|consecutive_on_hours_con2a" is now "...|ConsecutiveOn|con2a"
|
|
28
|
+
* "...|consecutive_on_hours_con2b" is now "...|ConsecutiveOn|con2b"
|
|
29
|
+
* "...|consecutive_on_hours_initial" is now "...|ConsecutiveOn|initial"
|
|
30
|
+
* "...|consecutive_on_hours_minimum_duration" is now "...|ConsecutiveOn|minimum"
|
|
31
|
+
The same goes for "...|consecutive_off..." --> "...|ConsecutiveOff|..."
|
flixopt/components.py
CHANGED
|
@@ -60,6 +60,7 @@ class LinearConverter(Component):
|
|
|
60
60
|
return self.model
|
|
61
61
|
|
|
62
62
|
def _plausibility_checks(self) -> None:
|
|
63
|
+
super()._plausibility_checks()
|
|
63
64
|
if not self.conversion_factors and not self.piecewise_conversion:
|
|
64
65
|
raise PlausibilityError('Either conversion_factors or piecewise_conversion must be defined!')
|
|
65
66
|
if self.conversion_factors and self.piecewise_conversion:
|
|
@@ -213,6 +214,7 @@ class Storage(Component):
|
|
|
213
214
|
"""
|
|
214
215
|
Check for infeasible or uncommon combinations of parameters
|
|
215
216
|
"""
|
|
217
|
+
super()._plausibility_checks()
|
|
216
218
|
if utils.is_number(self.initial_charge_state):
|
|
217
219
|
if isinstance(self.capacity_in_flow_hours, InvestParameters):
|
|
218
220
|
if self.capacity_in_flow_hours.fixed_size is None:
|
|
@@ -301,6 +303,7 @@ class Transmission(Component):
|
|
|
301
303
|
self.absolute_losses = absolute_losses
|
|
302
304
|
|
|
303
305
|
def _plausibility_checks(self):
|
|
306
|
+
super()._plausibility_checks()
|
|
304
307
|
# check buses:
|
|
305
308
|
if self.in2 is not None:
|
|
306
309
|
assert self.in2.bus == self.out1.bus, (
|
|
@@ -396,6 +399,7 @@ class LinearConverterModel(ComponentModel):
|
|
|
396
399
|
super().__init__(model, element)
|
|
397
400
|
self.element: LinearConverter = element
|
|
398
401
|
self.on_off: Optional[OnOffModel] = None
|
|
402
|
+
self.piecewise_conversion: Optional[PiecewiseConversion] = None
|
|
399
403
|
|
|
400
404
|
def do_modeling(self):
|
|
401
405
|
super().do_modeling()
|
|
@@ -426,16 +430,16 @@ class LinearConverterModel(ComponentModel):
|
|
|
426
430
|
for flow, piecewise in self.element.piecewise_conversion.items()
|
|
427
431
|
}
|
|
428
432
|
|
|
429
|
-
piecewise_conversion =
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
433
|
+
self.piecewise_conversion = self.add(
|
|
434
|
+
PiecewiseModel(
|
|
435
|
+
model=self._model,
|
|
436
|
+
label_of_element=self.label_of_element,
|
|
437
|
+
piecewise_variables=piecewise_conversion,
|
|
438
|
+
zero_point=self.on_off.on if self.on_off is not None else False,
|
|
439
|
+
as_time_series=True,
|
|
440
|
+
)
|
|
436
441
|
)
|
|
437
|
-
piecewise_conversion.do_modeling()
|
|
438
|
-
self.sub_models.append(piecewise_conversion)
|
|
442
|
+
self.piecewise_conversion.do_modeling()
|
|
439
443
|
|
|
440
444
|
|
|
441
445
|
class StorageModel(ComponentModel):
|
flixopt/core.py
CHANGED
|
@@ -426,8 +426,64 @@ class TimeSeries:
|
|
|
426
426
|
True if all values in this TimeSeries are greater than other
|
|
427
427
|
"""
|
|
428
428
|
if isinstance(other, TimeSeries):
|
|
429
|
-
return
|
|
430
|
-
return
|
|
429
|
+
return self.active_data > other.active_data
|
|
430
|
+
return self.active_data > other
|
|
431
|
+
|
|
432
|
+
def __ge__(self, other):
|
|
433
|
+
"""
|
|
434
|
+
Compare if this TimeSeries is greater than or equal to another.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
other: Another TimeSeries to compare with
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
True if all values in this TimeSeries are greater than or equal to other
|
|
441
|
+
"""
|
|
442
|
+
if isinstance(other, TimeSeries):
|
|
443
|
+
return self.active_data >= other.active_data
|
|
444
|
+
return self.active_data >= other
|
|
445
|
+
|
|
446
|
+
def __lt__(self, other):
|
|
447
|
+
"""
|
|
448
|
+
Compare if this TimeSeries is less than another.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
other: Another TimeSeries to compare with
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
True if all values in this TimeSeries are less than other
|
|
455
|
+
"""
|
|
456
|
+
if isinstance(other, TimeSeries):
|
|
457
|
+
return self.active_data < other.active_data
|
|
458
|
+
return self.active_data < other
|
|
459
|
+
|
|
460
|
+
def __le__(self, other):
|
|
461
|
+
"""
|
|
462
|
+
Compare if this TimeSeries is less than or equal to another.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
other: Another TimeSeries to compare with
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
True if all values in this TimeSeries are less than or equal to other
|
|
469
|
+
"""
|
|
470
|
+
if isinstance(other, TimeSeries):
|
|
471
|
+
return self.active_data <= other.active_data
|
|
472
|
+
return self.active_data <= other
|
|
473
|
+
|
|
474
|
+
def __eq__(self, other):
|
|
475
|
+
"""
|
|
476
|
+
Compare if this TimeSeries is equal to another.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
other: Another TimeSeries to compare with
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
True if all values in this TimeSeries are equal to other
|
|
483
|
+
"""
|
|
484
|
+
if isinstance(other, TimeSeries):
|
|
485
|
+
return self.active_data == other.active_data
|
|
486
|
+
return self.active_data == other
|
|
431
487
|
|
|
432
488
|
def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
|
|
433
489
|
"""
|
|
@@ -843,7 +899,7 @@ class TimeSeriesCollection:
|
|
|
843
899
|
if isinstance(item, str):
|
|
844
900
|
return item in self.time_series_data
|
|
845
901
|
elif isinstance(item, TimeSeries):
|
|
846
|
-
return item in self.time_series_data.values()
|
|
902
|
+
return any([item is ts for ts in self.time_series_data.values()])
|
|
847
903
|
return False
|
|
848
904
|
|
|
849
905
|
@property
|
flixopt/elements.py
CHANGED
|
@@ -57,6 +57,7 @@ class Component(Element):
|
|
|
57
57
|
super().__init__(label, meta_data=meta_data)
|
|
58
58
|
self.inputs: List['Flow'] = inputs or []
|
|
59
59
|
self.outputs: List['Flow'] = outputs or []
|
|
60
|
+
self._check_unique_flow_labels()
|
|
60
61
|
self.on_off_parameters = on_off_parameters
|
|
61
62
|
self.prevent_simultaneous_flows: List['Flow'] = prevent_simultaneous_flows or []
|
|
62
63
|
|
|
@@ -77,9 +78,15 @@ class Component(Element):
|
|
|
77
78
|
infos['outputs'] = [flow.infos(use_numpy, use_element_label) for flow in self.outputs]
|
|
78
79
|
return infos
|
|
79
80
|
|
|
81
|
+
def _check_unique_flow_labels(self):
|
|
82
|
+
all_flow_labels = [flow.label for flow in self.inputs + self.outputs]
|
|
83
|
+
|
|
84
|
+
if len(set(all_flow_labels)) != len(all_flow_labels):
|
|
85
|
+
duplicates = {label for label in all_flow_labels if all_flow_labels.count(label) > 1}
|
|
86
|
+
raise ValueError(f'Flow names must be unique! "{self.label_full}" got 2 or more of: {duplicates}')
|
|
87
|
+
|
|
80
88
|
def _plausibility_checks(self) -> None:
|
|
81
|
-
|
|
82
|
-
pass
|
|
89
|
+
self._check_unique_flow_labels()
|
|
83
90
|
|
|
84
91
|
|
|
85
92
|
@register_class_for_io
|
|
@@ -115,7 +122,7 @@ class Bus(Element):
|
|
|
115
122
|
)
|
|
116
123
|
|
|
117
124
|
def _plausibility_checks(self) -> None:
|
|
118
|
-
if self.excess_penalty_per_flow_hour == 0:
|
|
125
|
+
if self.excess_penalty_per_flow_hour is not None and (self.excess_penalty_per_flow_hour == 0).all():
|
|
119
126
|
logger.warning(f'In Bus {self.label}, the excess_penalty_per_flow_hour is 0. Use "None" or a value > 0.')
|
|
120
127
|
|
|
121
128
|
@property
|
|
@@ -277,6 +284,13 @@ class Flow(Element):
|
|
|
277
284
|
f'if you want to allow flows to be switched on and off.'
|
|
278
285
|
)
|
|
279
286
|
|
|
287
|
+
if (self.relative_minimum > 0).any() and self.on_off_parameters is None:
|
|
288
|
+
logger.warning(
|
|
289
|
+
f'Flow {self.label} has a relative_minimum of {self.relative_minimum.active_data} and no on_off_parameters. '
|
|
290
|
+
f'This prevents the flow_rate from switching off (flow_rate = 0). '
|
|
291
|
+
f'Consider using on_off_parameters to allow the flow to be switched on and off.'
|
|
292
|
+
)
|
|
293
|
+
|
|
280
294
|
@property
|
|
281
295
|
def label_full(self) -> str:
|
|
282
296
|
return f'{self.component}({self.label})'
|
|
@@ -306,8 +320,8 @@ class FlowModel(ElementModel):
|
|
|
306
320
|
# eq relative_minimum(t) * size <= flow_rate(t) <= relative_maximum(t) * size
|
|
307
321
|
self.flow_rate: linopy.Variable = self.add(
|
|
308
322
|
self._model.add_variables(
|
|
309
|
-
lower=self.
|
|
310
|
-
upper=self.
|
|
323
|
+
lower=self.flow_rate_lower_bound,
|
|
324
|
+
upper=self.flow_rate_upper_bound,
|
|
311
325
|
coords=self._model.coords,
|
|
312
326
|
name=f'{self.label_full}|flow_rate',
|
|
313
327
|
),
|
|
@@ -322,7 +336,7 @@ class FlowModel(ElementModel):
|
|
|
322
336
|
label_of_element=self.label_of_element,
|
|
323
337
|
on_off_parameters=self.element.on_off_parameters,
|
|
324
338
|
defining_variables=[self.flow_rate],
|
|
325
|
-
defining_bounds=[self.
|
|
339
|
+
defining_bounds=[self.flow_rate_bounds_on],
|
|
326
340
|
previous_values=[self.element.previous_flow_rate],
|
|
327
341
|
),
|
|
328
342
|
'on_off',
|
|
@@ -337,7 +351,8 @@ class FlowModel(ElementModel):
|
|
|
337
351
|
label_of_element=self.label_of_element,
|
|
338
352
|
parameters=self.element.size,
|
|
339
353
|
defining_variable=self.flow_rate,
|
|
340
|
-
relative_bounds_of_defining_variable=self.
|
|
354
|
+
relative_bounds_of_defining_variable=(self.flow_rate_lower_bound_relative,
|
|
355
|
+
self.flow_rate_upper_bound_relative),
|
|
341
356
|
on_variable=self.on_off.on if self.on_off is not None else None,
|
|
342
357
|
),
|
|
343
358
|
'investment',
|
|
@@ -346,7 +361,7 @@ class FlowModel(ElementModel):
|
|
|
346
361
|
|
|
347
362
|
self.total_flow_hours = self.add(
|
|
348
363
|
self._model.add_variables(
|
|
349
|
-
lower=self.element.flow_hours_total_min if self.element.flow_hours_total_min is not None else
|
|
364
|
+
lower=self.element.flow_hours_total_min if self.element.flow_hours_total_min is not None else 0,
|
|
350
365
|
upper=self.element.flow_hours_total_max if self.element.flow_hours_total_max is not None else np.inf,
|
|
351
366
|
coords=None,
|
|
352
367
|
name=f'{self.label_full}|total_flow_hours',
|
|
@@ -389,14 +404,13 @@ class FlowModel(ElementModel):
|
|
|
389
404
|
flow_hours_per_size_max = self._model.hours_per_step.sum() * self.element.load_factor_max
|
|
390
405
|
size = self.element.size if self._investment is None else self._investment.size
|
|
391
406
|
|
|
392
|
-
|
|
393
|
-
self.
|
|
394
|
-
self.
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
)
|
|
407
|
+
self.add(
|
|
408
|
+
self._model.add_constraints(
|
|
409
|
+
self.total_flow_hours <= size * flow_hours_per_size_max,
|
|
410
|
+
name=f'{self.label_full}|{name_short}',
|
|
411
|
+
),
|
|
412
|
+
name_short,
|
|
413
|
+
)
|
|
400
414
|
|
|
401
415
|
# eq: size * sum(dt)* load_factor_min <= var_sumFlowHours
|
|
402
416
|
if self.element.load_factor_min is not None:
|
|
@@ -404,19 +418,18 @@ class FlowModel(ElementModel):
|
|
|
404
418
|
flow_hours_per_size_min = self._model.hours_per_step.sum() * self.element.load_factor_min
|
|
405
419
|
size = self.element.size if self._investment is None else self._investment.size
|
|
406
420
|
|
|
407
|
-
|
|
408
|
-
self.
|
|
409
|
-
self.
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
)
|
|
421
|
+
self.add(
|
|
422
|
+
self._model.add_constraints(
|
|
423
|
+
self.total_flow_hours >= size * flow_hours_per_size_min,
|
|
424
|
+
name=f'{self.label_full}|{name_short}',
|
|
425
|
+
),
|
|
426
|
+
name_short,
|
|
427
|
+
)
|
|
415
428
|
|
|
416
429
|
@property
|
|
417
|
-
def
|
|
430
|
+
def flow_rate_bounds_on(self) -> Tuple[NumericData, NumericData]:
|
|
418
431
|
"""Returns absolute flow rate bounds. Important for OnOffModel"""
|
|
419
|
-
relative_minimum, relative_maximum = self.
|
|
432
|
+
relative_minimum, relative_maximum = self.flow_rate_lower_bound_relative, self.flow_rate_upper_bound_relative
|
|
420
433
|
size = self.element.size
|
|
421
434
|
if not isinstance(size, InvestParameters):
|
|
422
435
|
return relative_minimum * size, relative_maximum * size
|
|
@@ -425,12 +438,44 @@ class FlowModel(ElementModel):
|
|
|
425
438
|
return relative_minimum * size.minimum_size, relative_maximum * size.maximum_size
|
|
426
439
|
|
|
427
440
|
@property
|
|
428
|
-
def
|
|
429
|
-
"""Returns relative
|
|
441
|
+
def flow_rate_lower_bound_relative(self) -> NumericData:
|
|
442
|
+
"""Returns the lower bound of the flow_rate relative to its size"""
|
|
443
|
+
fixed_profile = self.element.fixed_relative_profile
|
|
444
|
+
if fixed_profile is None:
|
|
445
|
+
return self.element.relative_minimum.active_data
|
|
446
|
+
return fixed_profile.active_data
|
|
447
|
+
|
|
448
|
+
@property
|
|
449
|
+
def flow_rate_upper_bound_relative(self) -> NumericData:
|
|
450
|
+
""" Returns the upper bound of the flow_rate relative to its size"""
|
|
430
451
|
fixed_profile = self.element.fixed_relative_profile
|
|
431
452
|
if fixed_profile is None:
|
|
432
|
-
return self.element.
|
|
433
|
-
return fixed_profile.active_data
|
|
453
|
+
return self.element.relative_maximum.active_data
|
|
454
|
+
return fixed_profile.active_data
|
|
455
|
+
|
|
456
|
+
@property
|
|
457
|
+
def flow_rate_lower_bound(self) -> NumericData:
|
|
458
|
+
"""
|
|
459
|
+
Returns the minimum bound the flow_rate can reach.
|
|
460
|
+
Further constraining might be done in OnOffModel and InvestmentModel
|
|
461
|
+
"""
|
|
462
|
+
if self.element.on_off_parameters is not None:
|
|
463
|
+
return 0
|
|
464
|
+
if isinstance(self.element.size, InvestParameters):
|
|
465
|
+
if self.element.size.optional:
|
|
466
|
+
return 0
|
|
467
|
+
return self.flow_rate_lower_bound_relative * self.element.size.minimum_size
|
|
468
|
+
return self.flow_rate_lower_bound_relative * self.element.size
|
|
469
|
+
|
|
470
|
+
@property
|
|
471
|
+
def flow_rate_upper_bound(self) -> NumericData:
|
|
472
|
+
"""
|
|
473
|
+
Returns the maximum bound the flow_rate can reach.
|
|
474
|
+
Further constraining might be done in OnOffModel and InvestmentModel
|
|
475
|
+
"""
|
|
476
|
+
if isinstance(self.element.size, InvestParameters):
|
|
477
|
+
return self.flow_rate_upper_bound_relative * self.element.size.maximum_size
|
|
478
|
+
return self.flow_rate_upper_bound_relative * self.element.size
|
|
434
479
|
|
|
435
480
|
|
|
436
481
|
class BusModel(ElementModel):
|
|
@@ -508,7 +553,7 @@ class ComponentModel(ElementModel):
|
|
|
508
553
|
self.element.on_off_parameters,
|
|
509
554
|
self.label_of_element,
|
|
510
555
|
defining_variables=[flow.model.flow_rate for flow in all_flows],
|
|
511
|
-
defining_bounds=[flow.model.
|
|
556
|
+
defining_bounds=[flow.model.flow_rate_bounds_on for flow in all_flows],
|
|
512
557
|
previous_values=[flow.previous_flow_rate for flow in all_flows],
|
|
513
558
|
)
|
|
514
559
|
)
|