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.

@@ -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 = PiecewiseModel(
430
- model=self._model,
431
- label_of_element=self.label_of_element,
432
- label=self.label_full,
433
- piecewise_variables=piecewise_conversion,
434
- zero_point=self.on_off.on if self.on_off is not None else False,
435
- as_time_series=True,
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 (self.active_data > other.active_data).all().item()
430
- return NotImplemented
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
- # TODO: Check for plausibility
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.absolute_flow_rate_bounds[0] if self.element.on_off_parameters is None else 0,
310
- upper=self.absolute_flow_rate_bounds[1],
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.absolute_flow_rate_bounds],
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.relative_flow_rate_bounds,
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 -np.inf,
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
- if self._investment is not None:
393
- self.add(
394
- self._model.add_constraints(
395
- self.total_flow_hours <= size * flow_hours_per_size_max,
396
- name=f'{self.label_full}|{name_short}',
397
- ),
398
- name_short,
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
- if self._investment is not None:
408
- self.add(
409
- self._model.add_constraints(
410
- self.total_flow_hours >= size * flow_hours_per_size_min,
411
- name=f'{self.label_full}|{name_short}',
412
- ),
413
- name_short,
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 absolute_flow_rate_bounds(self) -> Tuple[NumericData, NumericData]:
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.relative_flow_rate_bounds
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 relative_flow_rate_bounds(self) -> Tuple[NumericData, NumericData]:
429
- """Returns relative flow rate bounds."""
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.relative_minimum.active_data, self.element.relative_maximum.active_data
433
- return fixed_profile.active_data, 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.absolute_flow_rate_bounds for flow in all_flows],
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
  )