flixopt 2.0.1__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.

@@ -1,6 +1,6 @@
1
1
  # Release v2.0.1
2
2
 
3
- **Release Date:** 2025-04-20
3
+ **Release Date:** 2025-04-10
4
4
 
5
5
  ## Improvements
6
6
 
@@ -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/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
@@ -313,8 +320,8 @@ class FlowModel(ElementModel):
313
320
  # eq relative_minimum(t) * size <= flow_rate(t) <= relative_maximum(t) * size
314
321
  self.flow_rate: linopy.Variable = self.add(
315
322
  self._model.add_variables(
316
- lower=self.absolute_flow_rate_bounds[0] if self.element.on_off_parameters is None else 0,
317
- upper=self.absolute_flow_rate_bounds[1],
323
+ lower=self.flow_rate_lower_bound,
324
+ upper=self.flow_rate_upper_bound,
318
325
  coords=self._model.coords,
319
326
  name=f'{self.label_full}|flow_rate',
320
327
  ),
@@ -329,7 +336,7 @@ class FlowModel(ElementModel):
329
336
  label_of_element=self.label_of_element,
330
337
  on_off_parameters=self.element.on_off_parameters,
331
338
  defining_variables=[self.flow_rate],
332
- defining_bounds=[self.absolute_flow_rate_bounds],
339
+ defining_bounds=[self.flow_rate_bounds_on],
333
340
  previous_values=[self.element.previous_flow_rate],
334
341
  ),
335
342
  'on_off',
@@ -344,7 +351,8 @@ class FlowModel(ElementModel):
344
351
  label_of_element=self.label_of_element,
345
352
  parameters=self.element.size,
346
353
  defining_variable=self.flow_rate,
347
- 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),
348
356
  on_variable=self.on_off.on if self.on_off is not None else None,
349
357
  ),
350
358
  'investment',
@@ -353,7 +361,7 @@ class FlowModel(ElementModel):
353
361
 
354
362
  self.total_flow_hours = self.add(
355
363
  self._model.add_variables(
356
- 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,
357
365
  upper=self.element.flow_hours_total_max if self.element.flow_hours_total_max is not None else np.inf,
358
366
  coords=None,
359
367
  name=f'{self.label_full}|total_flow_hours',
@@ -419,9 +427,9 @@ class FlowModel(ElementModel):
419
427
  )
420
428
 
421
429
  @property
422
- def absolute_flow_rate_bounds(self) -> Tuple[NumericData, NumericData]:
430
+ def flow_rate_bounds_on(self) -> Tuple[NumericData, NumericData]:
423
431
  """Returns absolute flow rate bounds. Important for OnOffModel"""
424
- 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
425
433
  size = self.element.size
426
434
  if not isinstance(size, InvestParameters):
427
435
  return relative_minimum * size, relative_maximum * size
@@ -430,12 +438,44 @@ class FlowModel(ElementModel):
430
438
  return relative_minimum * size.minimum_size, relative_maximum * size.maximum_size
431
439
 
432
440
  @property
433
- def relative_flow_rate_bounds(self) -> Tuple[NumericData, NumericData]:
434
- """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"""
435
443
  fixed_profile = self.element.fixed_relative_profile
436
444
  if fixed_profile is None:
437
- return self.element.relative_minimum.active_data, self.element.relative_maximum.active_data
438
- return fixed_profile.active_data, fixed_profile.active_data
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"""
451
+ fixed_profile = self.element.fixed_relative_profile
452
+ if fixed_profile is None:
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
439
479
 
440
480
 
441
481
  class BusModel(ElementModel):
@@ -513,7 +553,7 @@ class ComponentModel(ElementModel):
513
553
  self.element.on_off_parameters,
514
554
  self.label_of_element,
515
555
  defining_variables=[flow.model.flow_rate for flow in all_flows],
516
- 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],
517
557
  previous_values=[flow.previous_flow_rate for flow in all_flows],
518
558
  )
519
559
  )