flixopt 2.2.0b0__py3-none-any.whl → 2.2.0rc2__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 (48) hide show
  1. docs/examples/00-Minimal Example.md +1 -1
  2. docs/examples/01-Basic Example.md +1 -1
  3. docs/examples/02-Complex Example.md +1 -1
  4. docs/examples/index.md +1 -1
  5. docs/faq/contribute.md +26 -14
  6. docs/faq/index.md +1 -1
  7. docs/javascripts/mathjax.js +1 -1
  8. docs/user-guide/Mathematical Notation/Bus.md +1 -1
  9. docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +13 -13
  10. docs/user-guide/Mathematical Notation/Flow.md +1 -1
  11. docs/user-guide/Mathematical Notation/LinearConverter.md +2 -2
  12. docs/user-guide/Mathematical Notation/Piecewise.md +1 -1
  13. docs/user-guide/Mathematical Notation/Storage.md +1 -1
  14. docs/user-guide/Mathematical Notation/index.md +1 -1
  15. docs/user-guide/Mathematical Notation/others.md +1 -1
  16. docs/user-guide/index.md +2 -2
  17. flixopt/__init__.py +5 -0
  18. flixopt/aggregation.py +0 -1
  19. flixopt/calculation.py +40 -72
  20. flixopt/commons.py +10 -1
  21. flixopt/components.py +326 -154
  22. flixopt/core.py +459 -966
  23. flixopt/effects.py +67 -270
  24. flixopt/elements.py +76 -84
  25. flixopt/features.py +172 -154
  26. flixopt/flow_system.py +70 -99
  27. flixopt/interface.py +315 -147
  28. flixopt/io.py +27 -56
  29. flixopt/linear_converters.py +3 -3
  30. flixopt/network_app.py +755 -0
  31. flixopt/plotting.py +16 -34
  32. flixopt/results.py +108 -806
  33. flixopt/structure.py +11 -67
  34. flixopt/utils.py +9 -6
  35. {flixopt-2.2.0b0.dist-info → flixopt-2.2.0rc2.dist-info}/METADATA +63 -42
  36. flixopt-2.2.0rc2.dist-info/RECORD +54 -0
  37. {flixopt-2.2.0b0.dist-info → flixopt-2.2.0rc2.dist-info}/WHEEL +1 -1
  38. scripts/extract_release_notes.py +45 -0
  39. docs/release-notes/_template.txt +0 -32
  40. docs/release-notes/index.md +0 -7
  41. docs/release-notes/v2.0.0.md +0 -93
  42. docs/release-notes/v2.0.1.md +0 -12
  43. docs/release-notes/v2.1.0.md +0 -31
  44. docs/release-notes/v2.2.0.md +0 -55
  45. docs/user-guide/Mathematical Notation/Investment.md +0 -115
  46. flixopt-2.2.0b0.dist-info/RECORD +0 -59
  47. {flixopt-2.2.0b0.dist-info → flixopt-2.2.0rc2.dist-info}/licenses/LICENSE +0 -0
  48. {flixopt-2.2.0b0.dist-info → flixopt-2.2.0rc2.dist-info}/top_level.txt +0 -0
flixopt/elements.py CHANGED
@@ -10,10 +10,10 @@ import linopy
10
10
  import numpy as np
11
11
 
12
12
  from .config import CONFIG
13
- from .core import PlausibilityError, Scalar, ScenarioData, TimestepData, extract_data
14
- from .effects import EffectValuesUserTimestep
15
- from .features import InvestmentModel, OnOffModel, PreventSimultaneousUsageModel
16
- from .interface import InvestParameters, OnOffParameters
13
+ from .core import NumericData, NumericDataTS, PlausibilityError, Scalar, TimeSeriesCollection
14
+ from .effects import EffectValuesUser
15
+ from .features import InvestmentModel, OnOffModel, PiecewiseEffectsPerFlowHourModel, PreventSimultaneousUsageModel
16
+ from .interface import InvestParameters, OnOffParameters, PiecewiseEffectsPerFlowHour
17
17
  from .structure import Element, ElementModel, SystemModel, register_class_for_io
18
18
 
19
19
  if TYPE_CHECKING:
@@ -96,7 +96,7 @@ class Bus(Element):
96
96
  """
97
97
 
98
98
  def __init__(
99
- self, label: str, excess_penalty_per_flow_hour: Optional[TimestepData] = 1e5, meta_data: Optional[Dict] = None
99
+ self, label: str, excess_penalty_per_flow_hour: Optional[NumericDataTS] = 1e5, meta_data: Optional[Dict] = None
100
100
  ):
101
101
  """
102
102
  Args:
@@ -154,17 +154,18 @@ class Flow(Element):
154
154
  self,
155
155
  label: str,
156
156
  bus: str,
157
- size: Union[ScenarioData, InvestParameters] = None,
158
- fixed_relative_profile: Optional[TimestepData] = None,
159
- relative_minimum: TimestepData = 0,
160
- relative_maximum: TimestepData = 1,
161
- effects_per_flow_hour: Optional[EffectValuesUserTimestep] = None,
157
+ size: Union[Scalar, InvestParameters] = None,
158
+ fixed_relative_profile: Optional[NumericDataTS] = None,
159
+ relative_minimum: NumericDataTS = 0,
160
+ relative_maximum: NumericDataTS = 1,
161
+ effects_per_flow_hour: Optional[EffectValuesUser] = None,
162
+ piecewise_effects_per_flow_hour: Optional[PiecewiseEffectsPerFlowHour] = None,
162
163
  on_off_parameters: Optional[OnOffParameters] = None,
163
- flow_hours_total_max: Optional[ScenarioData] = None,
164
- flow_hours_total_min: Optional[ScenarioData] = None,
165
- load_factor_min: Optional[ScenarioData] = None,
166
- load_factor_max: Optional[ScenarioData] = None,
167
- previous_flow_rate: Optional[ScenarioData] = None,
164
+ flow_hours_total_max: Optional[Scalar] = None,
165
+ flow_hours_total_min: Optional[Scalar] = None,
166
+ load_factor_min: Optional[Scalar] = None,
167
+ load_factor_max: Optional[Scalar] = None,
168
+ previous_flow_rate: Optional[NumericData] = None,
168
169
  meta_data: Optional[Dict] = None,
169
170
  ):
170
171
  r"""
@@ -180,6 +181,7 @@ class Flow(Element):
180
181
  def: :math:`load\_factor:= sumFlowHours/ (nominal\_val \cdot \Delta t_{tot})`
181
182
  load_factor_max: maximal load factor (see minimal load factor)
182
183
  effects_per_flow_hour: operational costs, costs per flow-"work"
184
+ piecewise_effects_per_flow_hour: piecewise relation between flow hours and effects
183
185
  on_off_parameters: If present, flow can be "off", i.e. be zero (only relevant if relative_minimum > 0)
184
186
  Therefore a binary var "on" is used. Further, several other restrictions and effects can be modeled
185
187
  through this On/Off State (See OnOffParameters)
@@ -207,6 +209,7 @@ class Flow(Element):
207
209
  self.load_factor_max = load_factor_max
208
210
  # self.positive_gradient = TimeSeries('positive_gradient', positive_gradient, self)
209
211
  self.effects_per_flow_hour = effects_per_flow_hour if effects_per_flow_hour is not None else {}
212
+ self.piecewise_effects_per_flow_hour = piecewise_effects_per_flow_hour
210
213
  self.flow_hours_total_max = flow_hours_total_max
211
214
  self.flow_hours_total_min = flow_hours_total_min
212
215
  self.on_off_parameters = on_off_parameters
@@ -248,25 +251,12 @@ class Flow(Element):
248
251
  self.effects_per_flow_hour = flow_system.create_effect_time_series(
249
252
  self.label_full, self.effects_per_flow_hour, 'per_flow_hour'
250
253
  )
251
- self.flow_hours_total_max = flow_system.create_time_series(
252
- f'{self.label_full}|flow_hours_total_max', self.flow_hours_total_max, has_time_dim=False
253
- )
254
- self.flow_hours_total_min = flow_system.create_time_series(
255
- f'{self.label_full}|flow_hours_total_min', self.flow_hours_total_min, has_time_dim=False
256
- )
257
- self.load_factor_max = flow_system.create_time_series(
258
- f'{self.label_full}|load_factor_max', self.load_factor_max, has_time_dim=False
259
- )
260
- self.load_factor_min = flow_system.create_time_series(
261
- f'{self.label_full}|load_factor_min', self.load_factor_min, has_time_dim=False
262
- )
263
-
254
+ if self.piecewise_effects_per_flow_hour is not None:
255
+ self.piecewise_effects_per_flow_hour.transform_data(flow_system, self.label_full)
264
256
  if self.on_off_parameters is not None:
265
257
  self.on_off_parameters.transform_data(flow_system, self.label_full)
266
258
  if isinstance(self.size, InvestParameters):
267
- self.size.transform_data(flow_system, self.label_full)
268
- else:
269
- self.size = flow_system.create_time_series(f'{self.label_full}|size', self.size, has_time_dim=False)
259
+ self.size.transform_data(flow_system)
270
260
 
271
261
  def infos(self, use_numpy: bool = True, use_element_label: bool = False) -> Dict:
272
262
  infos = super().infos(use_numpy, use_element_label)
@@ -284,8 +274,8 @@ class Flow(Element):
284
274
  if np.any(self.relative_minimum > self.relative_maximum):
285
275
  raise PlausibilityError(self.label_full + ': Take care, that relative_minimum <= relative_maximum!')
286
276
 
287
- if not isinstance(self.size, InvestParameters) and (
288
- np.any(self.size == CONFIG.modeling.BIG) and self.fixed_relative_profile is not None
277
+ if (
278
+ self.size == CONFIG.modeling.BIG and self.fixed_relative_profile is not None
289
279
  ): # Default Size --> Most likely by accident
290
280
  logger.warning(
291
281
  f'Flow "{self.label}" has no size assigned, but a "fixed_relative_profile". '
@@ -294,14 +284,15 @@ class Flow(Element):
294
284
  )
295
285
 
296
286
  if self.fixed_relative_profile is not None and self.on_off_parameters is not None:
297
- logger.warning(
298
- f'Flow {self.label} has both a fixed_relative_profile and an on_off_parameters.'
299
- f'This will allow the flow to be switched on and off, effectively differing from the fixed_flow_rate.'
287
+ raise ValueError(
288
+ f'Flow {self.label} has both a fixed_relative_profile and an on_off_parameters. This is not supported. '
289
+ f'Use relative_minimum and relative_maximum instead, '
290
+ f'if you want to allow flows to be switched on and off.'
300
291
  )
301
292
 
302
293
  if (self.relative_minimum > 0).any() and self.on_off_parameters is None:
303
294
  logger.warning(
304
- f'Flow {self.label} has a relative_minimum of {self.relative_minimum.selected_data} and no on_off_parameters. '
295
+ f'Flow {self.label} has a relative_minimum of {self.relative_minimum.active_data} and no on_off_parameters. '
305
296
  f'This prevents the flow_rate from switching off (flow_rate = 0). '
306
297
  f'Consider using on_off_parameters to allow the flow to be switched on and off.'
307
298
  )
@@ -337,7 +328,7 @@ class FlowModel(ElementModel):
337
328
  self._model.add_variables(
338
329
  lower=self.flow_rate_lower_bound,
339
330
  upper=self.flow_rate_upper_bound,
340
- coords=self._model.get_coords(),
331
+ coords=self._model.coords,
341
332
  name=f'{self.label_full}|flow_rate',
342
333
  ),
343
334
  'flow_rate',
@@ -366,8 +357,10 @@ class FlowModel(ElementModel):
366
357
  label_of_element=self.label_of_element,
367
358
  parameters=self.element.size,
368
359
  defining_variable=self.flow_rate,
369
- relative_bounds_of_defining_variable=(self.flow_rate_lower_bound_relative,
370
- self.flow_rate_upper_bound_relative),
360
+ relative_bounds_of_defining_variable=(
361
+ self.flow_rate_lower_bound_relative,
362
+ self.flow_rate_upper_bound_relative,
363
+ ),
371
364
  on_variable=self.on_off.on if self.on_off is not None else None,
372
365
  ),
373
366
  'investment',
@@ -376,9 +369,9 @@ class FlowModel(ElementModel):
376
369
 
377
370
  self.total_flow_hours = self.add(
378
371
  self._model.add_variables(
379
- lower=extract_data(self.element.flow_hours_total_min, 0),
380
- upper=extract_data(self.element.flow_hours_total_max, np.inf),
381
- coords=self._model.get_coords(time_dim=False),
372
+ lower=self.element.flow_hours_total_min if self.element.flow_hours_total_min is not None else 0,
373
+ upper=self.element.flow_hours_total_max if self.element.flow_hours_total_max is not None else np.inf,
374
+ coords=None,
382
375
  name=f'{self.label_full}|total_flow_hours',
383
376
  ),
384
377
  'total_flow_hours',
@@ -386,7 +379,7 @@ class FlowModel(ElementModel):
386
379
 
387
380
  self.add(
388
381
  self._model.add_constraints(
389
- self.total_flow_hours == (self.flow_rate * self._model.hours_per_step).sum('time'),
382
+ self.total_flow_hours == (self.flow_rate * self._model.hours_per_step).sum(),
390
383
  name=f'{self.label_full}|total_flow_hours',
391
384
  ),
392
385
  'total_flow_hours',
@@ -398,33 +391,40 @@ class FlowModel(ElementModel):
398
391
  # Shares
399
392
  self._create_shares()
400
393
 
401
- def results_structure(self):
402
- return {
403
- **super().results_structure(),
404
- 'start': self.element.bus if self.element.is_input_in_component else self.element.component,
405
- 'end': self.element.component if self.element.is_input_in_component else self.element.bus,
406
- 'component': self.element.component,
407
- }
408
-
409
394
  def _create_shares(self):
410
395
  # Arbeitskosten:
411
396
  if self.element.effects_per_flow_hour != {}:
412
397
  self._model.effects.add_share_to_effects(
413
398
  name=self.label_full, # Use the full label of the element
414
399
  expressions={
415
- effect: self.flow_rate * self._model.hours_per_step * factor.selected_data
400
+ effect: self.flow_rate * self._model.hours_per_step * factor.active_data
416
401
  for effect, factor in self.element.effects_per_flow_hour.items()
417
402
  },
418
403
  target='operation',
419
404
  )
420
405
 
406
+ if self.element.piecewise_effects_per_flow_hour is not None:
407
+ self.piecewise_effects = self.add(
408
+ PiecewiseEffectsPerFlowHourModel(
409
+ model=self._model,
410
+ label_of_element=self.label_of_element,
411
+ piecewise_origin=(
412
+ self.flow_rate.name,
413
+ self.element.piecewise_effects_per_flow_hour.piecewise_flow_rate,
414
+ ),
415
+ piecewise_shares=self.element.piecewise_effects_per_flow_hour.piecewise_shares,
416
+ zero_point=self.on_off.on if self.on_off is not None else False,
417
+ ),
418
+ )
419
+ self.piecewise_effects.do_modeling()
420
+
421
421
  def _create_bounds_for_load_factor(self):
422
422
  # TODO: Add Variable load_factor for better evaluation?
423
423
 
424
424
  # eq: var_sumFlowHours <= size * dt_tot * load_factor_max
425
425
  if self.element.load_factor_max is not None:
426
426
  name_short = 'load_factor_max'
427
- flow_hours_per_size_max = self._model.hours_per_step.sum('time') * self.element.load_factor_max
427
+ flow_hours_per_size_max = self._model.hours_per_step.sum() * self.element.load_factor_max
428
428
  size = self.element.size if self._investment is None else self._investment.size
429
429
 
430
430
  self.add(
@@ -438,7 +438,7 @@ class FlowModel(ElementModel):
438
438
  # eq: size * sum(dt)* load_factor_min <= var_sumFlowHours
439
439
  if self.element.load_factor_min is not None:
440
440
  name_short = 'load_factor_min'
441
- flow_hours_per_size_min = self._model.hours_per_step.sum('time') * self.element.load_factor_min
441
+ flow_hours_per_size_min = self._model.hours_per_step.sum() * self.element.load_factor_min
442
442
  size = self.element.size if self._investment is None else self._investment.size
443
443
 
444
444
  self.add(
@@ -450,36 +450,34 @@ class FlowModel(ElementModel):
450
450
  )
451
451
 
452
452
  @property
453
- def flow_rate_bounds_on(self) -> Tuple[TimestepData, TimestepData]:
453
+ def flow_rate_bounds_on(self) -> Tuple[NumericData, NumericData]:
454
454
  """Returns absolute flow rate bounds. Important for OnOffModel"""
455
455
  relative_minimum, relative_maximum = self.flow_rate_lower_bound_relative, self.flow_rate_upper_bound_relative
456
456
  size = self.element.size
457
457
  if not isinstance(size, InvestParameters):
458
- return relative_minimum * extract_data(size), relative_maximum * extract_data(size)
458
+ return relative_minimum * size, relative_maximum * size
459
459
  if size.fixed_size is not None:
460
- return (relative_minimum * extract_data(size.fixed_size),
461
- relative_maximum * extract_data(size.fixed_size))
462
- return (relative_minimum * extract_data(size.minimum_size),
463
- relative_maximum * extract_data(size.maximum_size))
460
+ return relative_minimum * size.fixed_size, relative_maximum * size.fixed_size
461
+ return relative_minimum * size.minimum_size, relative_maximum * size.maximum_size
464
462
 
465
463
  @property
466
- def flow_rate_lower_bound_relative(self) -> TimestepData:
464
+ def flow_rate_lower_bound_relative(self) -> NumericData:
467
465
  """Returns the lower bound of the flow_rate relative to its size"""
468
466
  fixed_profile = self.element.fixed_relative_profile
469
467
  if fixed_profile is None:
470
- return extract_data(self.element.relative_minimum)
471
- return extract_data(fixed_profile)
468
+ return self.element.relative_minimum.active_data
469
+ return fixed_profile.active_data
472
470
 
473
471
  @property
474
- def flow_rate_upper_bound_relative(self) -> TimestepData:
475
- """ Returns the upper bound of the flow_rate relative to its size"""
472
+ def flow_rate_upper_bound_relative(self) -> NumericData:
473
+ """Returns the upper bound of the flow_rate relative to its size"""
476
474
  fixed_profile = self.element.fixed_relative_profile
477
475
  if fixed_profile is None:
478
- return extract_data(self.element.relative_maximum)
479
- return extract_data(fixed_profile)
476
+ return self.element.relative_maximum.active_data
477
+ return fixed_profile.active_data
480
478
 
481
479
  @property
482
- def flow_rate_lower_bound(self) -> TimestepData:
480
+ def flow_rate_lower_bound(self) -> NumericData:
483
481
  """
484
482
  Returns the minimum bound the flow_rate can reach.
485
483
  Further constraining might be done in OnOffModel and InvestmentModel
@@ -489,18 +487,18 @@ class FlowModel(ElementModel):
489
487
  if isinstance(self.element.size, InvestParameters):
490
488
  if self.element.size.optional:
491
489
  return 0
492
- return self.flow_rate_lower_bound_relative * extract_data(self.element.size.minimum_size)
493
- return self.flow_rate_lower_bound_relative * extract_data(self.element.size)
490
+ return self.flow_rate_lower_bound_relative * self.element.size.minimum_size
491
+ return self.flow_rate_lower_bound_relative * self.element.size
494
492
 
495
493
  @property
496
- def flow_rate_upper_bound(self) -> TimestepData:
494
+ def flow_rate_upper_bound(self) -> NumericData:
497
495
  """
498
496
  Returns the maximum bound the flow_rate can reach.
499
497
  Further constraining might be done in OnOffModel and InvestmentModel
500
498
  """
501
499
  if isinstance(self.element.size, InvestParameters):
502
- return self.flow_rate_upper_bound_relative * extract_data(self.element.size.maximum_size)
503
- return self.flow_rate_upper_bound_relative * extract_data(self.element.size)
500
+ return self.flow_rate_upper_bound_relative * self.element.size.maximum_size
501
+ return self.flow_rate_upper_bound_relative * self.element.size
504
502
 
505
503
 
506
504
  class BusModel(ElementModel):
@@ -521,18 +519,14 @@ class BusModel(ElementModel):
521
519
  # Fehlerplus/-minus:
522
520
  if self.element.with_excess:
523
521
  excess_penalty = np.multiply(
524
- self._model.hours_per_step, self.element.excess_penalty_per_flow_hour.selected_data
522
+ self._model.hours_per_step, self.element.excess_penalty_per_flow_hour.active_data
525
523
  )
526
524
  self.excess_input = self.add(
527
- self._model.add_variables(
528
- lower=0, coords=self._model.get_coords(), name=f'{self.label_full}|excess_input'
529
- ),
525
+ self._model.add_variables(lower=0, coords=self._model.coords, name=f'{self.label_full}|excess_input'),
530
526
  'excess_input',
531
527
  )
532
528
  self.excess_output = self.add(
533
- self._model.add_variables(
534
- lower=0, coords=self._model.get_coords(), name=f'{self.label_full}|excess_output'
535
- ),
529
+ self._model.add_variables(lower=0, coords=self._model.coords, name=f'{self.label_full}|excess_output'),
536
530
  'excess_output',
537
531
  )
538
532
  eq_bus_balance.lhs -= -self.excess_input + self.excess_output
@@ -547,8 +541,7 @@ class BusModel(ElementModel):
547
541
  inputs.append(self.excess_input.name)
548
542
  if self.excess_output is not None:
549
543
  outputs.append(self.excess_output.name)
550
- return {**super().results_structure(), 'inputs': inputs, 'outputs': outputs,
551
- 'flows': [flow.label_full for flow in self.element.inputs + self.element.outputs]}
544
+ return {**super().results_structure(), 'inputs': inputs, 'outputs': outputs}
552
545
 
553
546
 
554
547
  class ComponentModel(ElementModel):
@@ -601,5 +594,4 @@ class ComponentModel(ElementModel):
601
594
  **super().results_structure(),
602
595
  'inputs': [flow.model.flow_rate.name for flow in self.element.inputs],
603
596
  'outputs': [flow.model.flow_rate.name for flow in self.element.outputs],
604
- 'flows': [flow.label_full for flow in self.element.inputs + self.element.outputs],
605
597
  }