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/features.py CHANGED
@@ -9,9 +9,10 @@ from typing import Dict, List, Optional, Tuple, Union
9
9
  import linopy
10
10
  import numpy as np
11
11
 
12
+ from . import utils
12
13
  from .config import CONFIG
13
- from .core import Scalar, ScenarioData, TimeSeries, TimestepData, extract_data
14
- from .interface import InvestParameters, OnOffParameters, Piecewise
14
+ from .core import NumericData, Scalar, TimeSeries
15
+ from .interface import InvestParameters, OnOffParameters, Piece, Piecewise
15
16
  from .structure import Model, SystemModel
16
17
 
17
18
  logger = logging.getLogger('flixopt')
@@ -26,14 +27,13 @@ class InvestmentModel(Model):
26
27
  label_of_element: str,
27
28
  parameters: InvestParameters,
28
29
  defining_variable: [linopy.Variable],
29
- relative_bounds_of_defining_variable: Tuple[TimestepData, TimestepData],
30
+ relative_bounds_of_defining_variable: Tuple[NumericData, NumericData],
30
31
  label: Optional[str] = None,
31
32
  on_variable: Optional[linopy.Variable] = None,
32
33
  ):
33
34
  super().__init__(model, label_of_element, label)
34
35
  self.size: Optional[Union[Scalar, linopy.Variable]] = None
35
36
  self.is_invested: Optional[linopy.Variable] = None
36
- self.scenario_of_investment: Optional[linopy.Variable] = None
37
37
 
38
38
  self.piecewise_effects: Optional[PiecewiseEffectsModel] = None
39
39
 
@@ -43,32 +43,31 @@ class InvestmentModel(Model):
43
43
  self.parameters = parameters
44
44
 
45
45
  def do_modeling(self):
46
- self.size = self.add(
47
- self._model.add_variables(
48
- lower=0 if self.parameters.optional else extract_data(self.parameters.minimum_size),
49
- upper=extract_data(self.parameters.maximum_size),
50
- name=f'{self.label_full}|size',
51
- coords=self._model.get_coords(time_dim=False),
52
- ),
53
- 'size',
54
- )
46
+ if self.parameters.fixed_size and not self.parameters.optional:
47
+ self.size = self.add(
48
+ self._model.add_variables(
49
+ lower=self.parameters.fixed_size, upper=self.parameters.fixed_size, name=f'{self.label_full}|size'
50
+ ),
51
+ 'size',
52
+ )
53
+ else:
54
+ self.size = self.add(
55
+ self._model.add_variables(
56
+ lower=0 if self.parameters.optional else self.parameters.minimum_size,
57
+ upper=self.parameters.maximum_size,
58
+ name=f'{self.label_full}|size',
59
+ ),
60
+ 'size',
61
+ )
55
62
 
56
63
  # Optional
57
64
  if self.parameters.optional:
58
65
  self.is_invested = self.add(
59
- self._model.add_variables(
60
- binary=True,
61
- name=f'{self.label_full}|is_invested',
62
- coords=self._model.get_coords(time_dim=False),
63
- ),
64
- 'is_invested',
66
+ self._model.add_variables(binary=True, name=f'{self.label_full}|is_invested'), 'is_invested'
65
67
  )
66
68
 
67
69
  self._create_bounds_for_optional_investment()
68
70
 
69
- if self._model.time_series_collection.scenarios is not None:
70
- self._create_bounds_for_scenarios()
71
-
72
71
  # Bounds for defining variable
73
72
  self._create_bounds_for_defining_variable()
74
73
 
@@ -91,7 +90,10 @@ class InvestmentModel(Model):
91
90
  # share: divest_effects - isInvested * divest_effects
92
91
  self._model.effects.add_share_to_effects(
93
92
  name=self.label_of_element,
94
- expressions={effect: -self.is_invested * factor + factor for effect, factor in self.parameters.divest_effects.items()},
93
+ expressions={
94
+ effect: -self.is_invested * factor + factor
95
+ for effect, factor in self.parameters.divest_effects.items()
96
+ },
95
97
  target='invest',
96
98
  )
97
99
 
@@ -154,6 +156,11 @@ class InvestmentModel(Model):
154
156
  ),
155
157
  f'fix_{variable.name}',
156
158
  )
159
+ if self._on_variable is not None:
160
+ raise ValueError(
161
+ f'Flow {self.label_full} has a fixed relative flow rate and an on_variable.'
162
+ f'This combination is currently not supported.'
163
+ )
157
164
  return
158
165
 
159
166
  # eq: defining_variable(t) <= size * upper_bound(t)
@@ -178,7 +185,7 @@ class InvestmentModel(Model):
178
185
  # ... mit mega = relative_maximum * maximum_size
179
186
  # äquivalent zu:.
180
187
  # eq: - defining_variable(t) + mega * On(t) + size * relative_minimum(t) <= + mega
181
- mega = self.parameters.maximum_size * lb_relative
188
+ mega = lb_relative * self.parameters.maximum_size
182
189
  on = self._on_variable
183
190
  self.add(
184
191
  self._model.add_constraints(
@@ -188,50 +195,6 @@ class InvestmentModel(Model):
188
195
  )
189
196
  # anmerkung: Glg bei Spezialfall relative_minimum = 0 redundant zu OnOff ??
190
197
 
191
- def _create_bounds_for_scenarios(self):
192
- if isinstance(self.parameters.investment_scenarios, str):
193
- if self.parameters.investment_scenarios == 'individual':
194
- return
195
- raise ValueError(f'Invalid value for investment_scenarios: {self.parameters.investment_scenarios}')
196
-
197
- if self.parameters.investment_scenarios is None:
198
- self.add(
199
- self._model.add_constraints(
200
- self.size.isel(scenario=slice(None, -1)) == self.size.isel(scenario=slice(1, None)),
201
- name=f'{self.label_full}|equalize_size_per_scenario',
202
- ),
203
- 'equalize_size_per_scenario',
204
- )
205
- return
206
- if not isinstance(self.parameters.investment_scenarios, list):
207
- raise ValueError(f'Invalid value for investment_scenarios: {self.parameters.investment_scenarios}')
208
- if not all(scenario in self._model.time_series_collection.scenarios for scenario in self.parameters.investment_scenarios):
209
- raise ValueError(f'Some scenarios in investment_scenarios are not present in the time_series_collection: '
210
- f'{self.parameters.investment_scenarios}. This might be due to selecting a subset of '
211
- f'all scenarios, which is not yet supported.')
212
-
213
- investment_scenarios = self._model.time_series_collection.scenarios.intersection(self.parameters.investment_scenarios)
214
- no_investment_scenarios = self._model.time_series_collection.scenarios.difference(self.parameters.investment_scenarios)
215
-
216
- # eq: size(s) = size(s') for s, s' in investment_scenarios
217
- if len(investment_scenarios) > 1:
218
- self.add(
219
- self._model.add_constraints(
220
- self.size.sel(scenario=investment_scenarios[:-1]) == self.size.sel(scenario=investment_scenarios[1:]),
221
- name=f'{self.label_full}|investment_scenarios',
222
- ),
223
- 'investment_scenarios',
224
- )
225
-
226
- if len(no_investment_scenarios) >= 1:
227
- self.add(
228
- self._model.add_constraints(
229
- self.size.sel(scenario=no_investment_scenarios) == 0,
230
- name=f'{self.label_full}|no_investment_scenarios',
231
- ),
232
- 'no_investment_scenarios',
233
- )
234
-
235
198
 
236
199
  class StateModel(Model):
237
200
  """
@@ -243,12 +206,12 @@ class StateModel(Model):
243
206
  model: SystemModel,
244
207
  label_of_element: str,
245
208
  defining_variables: List[linopy.Variable],
246
- defining_bounds: List[Tuple[TimestepData, TimestepData]],
247
- previous_values: List[Optional[TimestepData]] = None,
209
+ defining_bounds: List[Tuple[NumericData, NumericData]],
210
+ previous_values: List[Optional[NumericData]] = None,
248
211
  use_off: bool = True,
249
- on_hours_total_min: Optional[ScenarioData] = 0,
250
- on_hours_total_max: Optional[ScenarioData] = None,
251
- effects_per_running_hour: Dict[str, TimestepData] = None,
212
+ on_hours_total_min: Optional[NumericData] = 0,
213
+ on_hours_total_max: Optional[NumericData] = None,
214
+ effects_per_running_hour: Dict[str, NumericData] = None,
252
215
  label: Optional[str] = None,
253
216
  ):
254
217
  """
@@ -285,16 +248,16 @@ class StateModel(Model):
285
248
  self._model.add_variables(
286
249
  name=f'{self.label_full}|on',
287
250
  binary=True,
288
- coords=self._model.get_coords(),
251
+ coords=self._model.coords,
289
252
  ),
290
253
  'on',
291
254
  )
292
255
 
293
256
  self.total_on_hours = self.add(
294
257
  self._model.add_variables(
295
- lower=extract_data(self._on_hours_total_min),
296
- upper=extract_data(self._on_hours_total_max),
297
- coords=self._model.get_coords(time_dim=False),
258
+ lower=self._on_hours_total_min,
259
+ upper=self._on_hours_total_max,
260
+ coords=None,
298
261
  name=f'{self.label_full}|on_hours_total',
299
262
  ),
300
263
  'on_hours_total',
@@ -302,7 +265,7 @@ class StateModel(Model):
302
265
 
303
266
  self.add(
304
267
  self._model.add_constraints(
305
- self.total_on_hours == (self.on * self._model.hours_per_step).sum('time'),
268
+ self.total_on_hours == (self.on * self._model.hours_per_step).sum(),
306
269
  name=f'{self.label_full}|on_hours_total',
307
270
  ),
308
271
  'on_hours_total',
@@ -316,7 +279,7 @@ class StateModel(Model):
316
279
  self._model.add_variables(
317
280
  name=f'{self.label_full}|off',
318
281
  binary=True,
319
- coords=self._model.get_coords(),
282
+ coords=self._model.coords,
320
283
  ),
321
284
  'off',
322
285
  )
@@ -344,13 +307,11 @@ class StateModel(Model):
344
307
  )
345
308
 
346
309
  # Constraint: on * upper_bound >= def_var
347
- self.add(
348
- self._model.add_constraints(self.on * ub >= def_var, name=f'{self.label_full}|on_con2'), 'on_con2'
349
- )
310
+ self.add(self._model.add_constraints(self.on * ub >= def_var, name=f'{self.label_full}|on_con2'), 'on_con2')
350
311
  else:
351
312
  # Case for multiple defining variables
352
313
  ub = sum(bound[1] for bound in self._defining_bounds) / nr_of_def_vars
353
- lb = CONFIG.modeling.EPSILON #TODO: Can this be a bigger value? (maybe the smallest bound?)
314
+ lb = CONFIG.modeling.EPSILON # TODO: Can this be a bigger value? (maybe the smallest bound?)
354
315
 
355
316
  # Constraint: on * epsilon <= sum(all_defining_variables)
356
317
  self.add(
@@ -384,7 +345,7 @@ class StateModel(Model):
384
345
  return 1 - self.previous_states
385
346
 
386
347
  @staticmethod
387
- def compute_previous_states(previous_values: List[TimestepData], epsilon: float = 1e-5) -> np.ndarray:
348
+ def compute_previous_states(previous_values: List[NumericData], epsilon: float = 1e-5) -> np.ndarray:
388
349
  """Computes the previous states {0, 1} of defining variables as a binary array from their previous values."""
389
350
  if not previous_values or all([val is None for val in previous_values]):
390
351
  return np.array([0])
@@ -425,19 +386,19 @@ class SwitchStateModel(Model):
425
386
 
426
387
  # Create switch variables
427
388
  self.switch_on = self.add(
428
- self._model.add_variables(binary=True, name=f'{self.label_full}|switch_on', coords=self._model.get_coords()),
389
+ self._model.add_variables(binary=True, name=f'{self.label_full}|switch_on', coords=self._model.coords),
429
390
  'switch_on',
430
391
  )
431
392
 
432
393
  self.switch_off = self.add(
433
- self._model.add_variables(binary=True, name=f'{self.label_full}|switch_off', coords=self._model.get_coords()),
394
+ self._model.add_variables(binary=True, name=f'{self.label_full}|switch_off', coords=self._model.coords),
434
395
  'switch_off',
435
396
  )
436
397
 
437
398
  # Create count variable for number of switches
438
399
  self.switch_on_nr = self.add(
439
400
  self._model.add_variables(
440
- upper=extract_data(self._switch_on_max),
401
+ upper=self._switch_on_max,
441
402
  lower=0,
442
403
  name=f'{self.label_full}|switch_on_nr',
443
404
  ),
@@ -466,7 +427,9 @@ class SwitchStateModel(Model):
466
427
 
467
428
  # Mutual exclusivity constraint
468
429
  self.add(
469
- self._model.add_constraints(self.switch_on + self.switch_off <= 1.1, name=f'{self.label_full}|switch_on_or_off'),
430
+ self._model.add_constraints(
431
+ self.switch_on + self.switch_off <= 1.1, name=f'{self.label_full}|switch_on_or_off'
432
+ ),
470
433
  'switch_on_or_off',
471
434
  )
472
435
 
@@ -491,9 +454,9 @@ class ConsecutiveStateModel(Model):
491
454
  model: SystemModel,
492
455
  label_of_element: str,
493
456
  state_variable: linopy.Variable,
494
- minimum_duration: Optional[TimestepData] = None,
495
- maximum_duration: Optional[TimestepData] = None,
496
- previous_states: Optional[TimestepData] = None,
457
+ minimum_duration: Optional[NumericData] = None,
458
+ maximum_duration: Optional[NumericData] = None,
459
+ previous_states: Optional[NumericData] = None,
497
460
  label: Optional[str] = None,
498
461
  ):
499
462
  """
@@ -515,9 +478,9 @@ class ConsecutiveStateModel(Model):
515
478
  self._maximum_duration = maximum_duration
516
479
 
517
480
  if isinstance(self._minimum_duration, TimeSeries):
518
- self._minimum_duration = self._minimum_duration.selected_data
481
+ self._minimum_duration = self._minimum_duration.active_data
519
482
  if isinstance(self._maximum_duration, TimeSeries):
520
- self._maximum_duration = self._maximum_duration.selected_data
483
+ self._maximum_duration = self._maximum_duration.active_data
521
484
 
522
485
  self.duration = None
523
486
 
@@ -531,8 +494,8 @@ class ConsecutiveStateModel(Model):
531
494
  self.duration = self.add(
532
495
  self._model.add_variables(
533
496
  lower=0,
534
- upper=extract_data(self._maximum_duration, mega),
535
- coords=self._model.get_coords(),
497
+ upper=self._maximum_duration if self._maximum_duration is not None else mega,
498
+ coords=self._model.coords,
536
499
  name=f'{self.label_full}|hours',
537
500
  ),
538
501
  'hours',
@@ -542,9 +505,7 @@ class ConsecutiveStateModel(Model):
542
505
 
543
506
  # Upper bound constraint
544
507
  self.add(
545
- self._model.add_constraints(
546
- self.duration <= self._state_variable * mega, name=f'{self.label_full}|con1'
547
- ),
508
+ self._model.add_constraints(self.duration <= self._state_variable * mega, name=f'{self.label_full}|con1'),
548
509
  'con1',
549
510
  )
550
511
 
@@ -585,7 +546,7 @@ class ConsecutiveStateModel(Model):
585
546
  )
586
547
 
587
548
  # Handle initial condition
588
- if 0 < self.previous_duration < self._minimum_duration.isel(time=0).max():
549
+ if 0 < self.previous_duration < self._minimum_duration.isel(time=0):
589
550
  self.add(
590
551
  self._model.add_constraints(
591
552
  self._state_variable.isel(time=0) == 1, name=f'{self.label_full}|initial_minimum'
@@ -596,8 +557,8 @@ class ConsecutiveStateModel(Model):
596
557
  # Set initial value
597
558
  self.add(
598
559
  self._model.add_constraints(
599
- self.duration.isel(time=0) ==
600
- (hours_per_step.isel(time=0) + self.previous_duration) * self._state_variable.isel(time=0),
560
+ self.duration.isel(time=0)
561
+ == (hours_per_step.isel(time=0) + self.previous_duration) * self._state_variable.isel(time=0),
601
562
  name=f'{self.label_full}|initial',
602
563
  ),
603
564
  'initial',
@@ -608,14 +569,14 @@ class ConsecutiveStateModel(Model):
608
569
  @property
609
570
  def previous_duration(self) -> Scalar:
610
571
  """Computes the previous duration of the state variable"""
611
- #TODO: Allow for other/dynamic timestep resolutions
572
+ # TODO: Allow for other/dynamic timestep resolutions
612
573
  return ConsecutiveStateModel.compute_consecutive_hours_in_state(
613
- self._previous_states, self._model.hours_per_step.isel(time=0).values.flatten()[0]
574
+ self._previous_states, self._model.hours_per_step.isel(time=0).item()
614
575
  )
615
576
 
616
577
  @staticmethod
617
578
  def compute_consecutive_hours_in_state(
618
- binary_values: TimestepData, hours_per_timestep: Union[int, float, np.ndarray]
579
+ binary_values: NumericData, hours_per_timestep: Union[int, float, np.ndarray]
619
580
  ) -> Scalar:
620
581
  """
621
582
  Computes the final consecutive duration in state 'on' (=1) in hours, from a binary array.
@@ -659,7 +620,10 @@ class ConsecutiveStateModel(Model):
659
620
  f'as {binary_values=}'
660
621
  )
661
622
 
662
- return np.sum(binary_values[-nr_of_indexes_with_consecutive_ones:] * hours_per_timestep[-nr_of_indexes_with_consecutive_ones:])
623
+ return np.sum(
624
+ binary_values[-nr_of_indexes_with_consecutive_ones:]
625
+ * hours_per_timestep[-nr_of_indexes_with_consecutive_ones:]
626
+ )
663
627
 
664
628
 
665
629
  class OnOffModel(Model):
@@ -674,8 +638,8 @@ class OnOffModel(Model):
674
638
  on_off_parameters: OnOffParameters,
675
639
  label_of_element: str,
676
640
  defining_variables: List[linopy.Variable],
677
- defining_bounds: List[Tuple[TimestepData, TimestepData]],
678
- previous_values: List[Optional[TimestepData]],
641
+ defining_bounds: List[Tuple[NumericData, NumericData]],
642
+ previous_values: List[Optional[NumericData]],
679
643
  label: Optional[str] = None,
680
644
  ):
681
645
  """
@@ -712,8 +676,8 @@ class OnOffModel(Model):
712
676
  defining_bounds=self._defining_bounds,
713
677
  previous_values=self._previous_values,
714
678
  use_off=self.parameters.use_off,
715
- on_hours_total_min=extract_data(self.parameters.on_hours_total_min),
716
- on_hours_total_max=extract_data(self.parameters.on_hours_total_max),
679
+ on_hours_total_min=self.parameters.on_hours_total_min,
680
+ on_hours_total_max=self.parameters.on_hours_total_max,
717
681
  effects_per_running_hour=self.parameters.effects_per_running_hour,
718
682
  )
719
683
  self.add(self.state_model)
@@ -832,7 +796,7 @@ class PieceModel(Model):
832
796
  self._model.add_variables(
833
797
  binary=True,
834
798
  name=f'{self.label_full}|inside_piece',
835
- coords=self._model.get_coords(time_dim=self._as_time_series),
799
+ coords=self._model.coords if self._as_time_series else None,
836
800
  ),
837
801
  'inside_piece',
838
802
  )
@@ -842,7 +806,7 @@ class PieceModel(Model):
842
806
  lower=0,
843
807
  upper=1,
844
808
  name=f'{self.label_full}|lambda0',
845
- coords=self._model.get_coords(time_dim=self._as_time_series),
809
+ coords=self._model.coords if self._as_time_series else None,
846
810
  ),
847
811
  'lambda0',
848
812
  )
@@ -852,7 +816,7 @@ class PieceModel(Model):
852
816
  lower=0,
853
817
  upper=1,
854
818
  name=f'{self.label_full}|lambda1',
855
- coords=self._model.get_coords(time_dim=self._as_time_series),
819
+ coords=self._model.coords if self._as_time_series else None,
856
820
  ),
857
821
  'lambda1',
858
822
  )
@@ -877,7 +841,7 @@ class PiecewiseModel(Model):
877
841
  label: str = '',
878
842
  ):
879
843
  """
880
- Modeling a Piecewise relation between miultiple variables.
844
+ Modeling a Piecewise relation between multiple variables.
881
845
  The relation is defined by a list of Pieces, which are assigned to the variables.
882
846
  Each Piece is a tuple of (start, end).
883
847
 
@@ -886,7 +850,9 @@ class PiecewiseModel(Model):
886
850
  label_of_element: The label of the parent (Element). Used to construct the full label of the model.
887
851
  label: The label of the model. Used to construct the full label of the model.
888
852
  piecewise_variables: The variables to which the Pieces are assigned.
889
- zero_point: A variable that can be used to define a zero point for the Piecewise relation. If None or False, no zero point is defined.
853
+ zero_point: A variable that can be used to define a zero point for the Piecewise relation.
854
+ If None or False, no zero point is defined. THis leads to 0 not being possible,
855
+ unless its its contained in a Piece.
890
856
  as_time_series: Whether the Piecewise relation is defined for a TimeSeries or a single variable.
891
857
  """
892
858
  super().__init__(model, label_of_element, label)
@@ -932,21 +898,23 @@ class PiecewiseModel(Model):
932
898
  # b) eq: -On(t) + Segment1.onSeg(t) + Segment2.onSeg(t) + ... = 0 zusätzlich kann alles auch Null sein
933
899
  if isinstance(self._zero_point, linopy.Variable):
934
900
  self.zero_point = self._zero_point
935
- rhs = self.zero_point
901
+ sign, rhs = '<=', self.zero_point
936
902
  elif self._zero_point is True:
937
903
  self.zero_point = self.add(
938
904
  self._model.add_variables(
939
- coords=self._model.get_coords(), binary=True, name=f'{self.label_full}|zero_point'
905
+ coords=self._model.coords, binary=True, name=f'{self.label_full}|zero_point'
940
906
  ),
941
907
  'zero_point',
942
908
  )
943
- rhs = self.zero_point
909
+ sign, rhs = '<=', self.zero_point
944
910
  else:
945
- rhs = 1
911
+ sign, rhs = '=', 1
946
912
 
947
913
  self.add(
948
914
  self._model.add_constraints(
949
- sum([piece.inside_piece for piece in self.pieces]) <= rhs,
915
+ sum([piece.inside_piece for piece in self.pieces]),
916
+ sign,
917
+ rhs,
950
918
  name=f'{self.label_full}|{variable.name}|single_segment',
951
919
  ),
952
920
  f'{var_name}|single_segment',
@@ -957,20 +925,19 @@ class ShareAllocationModel(Model):
957
925
  def __init__(
958
926
  self,
959
927
  model: SystemModel,
960
- has_time_dim: bool,
961
- has_scenario_dim: bool,
928
+ shares_are_time_series: bool,
962
929
  label_of_element: Optional[str] = None,
963
930
  label: Optional[str] = None,
964
931
  label_full: Optional[str] = None,
965
- total_max: Optional[ScenarioData] = None,
966
- total_min: Optional[ScenarioData] = None,
967
- max_per_hour: Optional[TimestepData] = None,
968
- min_per_hour: Optional[TimestepData] = None,
932
+ total_max: Optional[Scalar] = None,
933
+ total_min: Optional[Scalar] = None,
934
+ max_per_hour: Optional[NumericData] = None,
935
+ min_per_hour: Optional[NumericData] = None,
969
936
  ):
970
937
  super().__init__(model, label_of_element=label_of_element, label=label, label_full=label_full)
971
- if not has_time_dim: # If the condition is True
938
+ if not shares_are_time_series: # If the condition is True
972
939
  assert max_per_hour is None and min_per_hour is None, (
973
- 'Both max_per_hour and min_per_hour cannot be used when has_time_dim is False'
940
+ 'Both max_per_hour and min_per_hour cannot be used when shares_are_time_series is False'
974
941
  )
975
942
  self.total_per_timestep: Optional[linopy.Variable] = None
976
943
  self.total: Optional[linopy.Variable] = None
@@ -981,9 +948,8 @@ class ShareAllocationModel(Model):
981
948
  self._eq_total: Optional[linopy.Constraint] = None
982
949
 
983
950
  # Parameters
984
- self._has_time_dim = has_time_dim
985
- self._has_scenario_dim = has_scenario_dim
986
- self._total_max = total_max if total_max is not None else np.inf
951
+ self._shares_are_time_series = shares_are_time_series
952
+ self._total_max = total_max if total_min is not None else np.inf
987
953
  self._total_min = total_min if total_min is not None else -np.inf
988
954
  self._max_per_hour = max_per_hour if max_per_hour is not None else np.inf
989
955
  self._min_per_hour = min_per_hour if min_per_hour is not None else -np.inf
@@ -991,10 +957,7 @@ class ShareAllocationModel(Model):
991
957
  def do_modeling(self):
992
958
  self.total = self.add(
993
959
  self._model.add_variables(
994
- lower=self._total_min,
995
- upper=self._total_max,
996
- coords=self._model.get_coords(time_dim=False, scenario_dim=self._has_scenario_dim),
997
- name=f'{self.label_full}|total',
960
+ lower=self._total_min, upper=self._total_max, coords=None, name=f'{self.label_full}|total'
998
961
  ),
999
962
  'total',
1000
963
  )
@@ -1003,12 +966,16 @@ class ShareAllocationModel(Model):
1003
966
  self._model.add_constraints(self.total == 0, name=f'{self.label_full}|total'), 'total'
1004
967
  )
1005
968
 
1006
- if self._has_time_dim:
969
+ if self._shares_are_time_series:
1007
970
  self.total_per_timestep = self.add(
1008
971
  self._model.add_variables(
1009
- lower=-np.inf if (self._min_per_hour is None) else extract_data(self._min_per_hour) * self._model.hours_per_step,
1010
- upper=np.inf if (self._max_per_hour is None) else extract_data(self._max_per_hour) * self._model.hours_per_step,
1011
- coords=self._model.get_coords(time_dim=True, scenario_dim=self._has_scenario_dim),
972
+ lower=-np.inf
973
+ if (self._min_per_hour is None)
974
+ else np.multiply(self._min_per_hour, self._model.hours_per_step),
975
+ upper=np.inf
976
+ if (self._max_per_hour is None)
977
+ else np.multiply(self._max_per_hour, self._model.hours_per_step),
978
+ coords=self._model.coords,
1012
979
  name=f'{self.label_full}|total_per_timestep',
1013
980
  ),
1014
981
  'total_per_timestep',
@@ -1020,14 +987,12 @@ class ShareAllocationModel(Model):
1020
987
  )
1021
988
 
1022
989
  # Add it to the total
1023
- self._eq_total.lhs -= self.total_per_timestep.sum(dim='time')
990
+ self._eq_total.lhs -= self.total_per_timestep.sum()
1024
991
 
1025
992
  def add_share(
1026
993
  self,
1027
994
  name: str,
1028
995
  expression: linopy.LinearExpression,
1029
- has_time_dim: bool,
1030
- has_scenario_dim: bool,
1031
996
  ):
1032
997
  """
1033
998
  Add a share to the share allocation model. If the share already exists, the expression is added to the existing share.
@@ -1039,17 +1004,16 @@ class ShareAllocationModel(Model):
1039
1004
  name: The name of the share.
1040
1005
  expression: The expression of the share. Added to the right hand side of the constraint.
1041
1006
  """
1042
- if has_time_dim and not self._has_time_dim:
1043
- raise ValueError('Cannot add share with time_dim=True to a model without time_dim')
1044
- if has_scenario_dim and not self._has_scenario_dim:
1045
- raise ValueError('Cannot add share with scenario_dim=True to a model without scenario_dim')
1046
-
1047
1007
  if name in self.shares:
1048
1008
  self.share_constraints[name].lhs -= expression
1049
1009
  else:
1050
1010
  self.shares[name] = self.add(
1051
1011
  self._model.add_variables(
1052
- coords=self._model.get_coords(time_dim=has_time_dim, scenario_dim=has_scenario_dim),
1012
+ coords=None
1013
+ if isinstance(expression, linopy.LinearExpression)
1014
+ and expression.ndim == 0
1015
+ or not isinstance(expression, linopy.LinearExpression)
1016
+ else self._model.coords,
1053
1017
  name=f'{name}->{self.label_full}',
1054
1018
  ),
1055
1019
  name,
@@ -1057,7 +1021,7 @@ class ShareAllocationModel(Model):
1057
1021
  self.share_constraints[name] = self.add(
1058
1022
  self._model.add_constraints(self.shares[name] == expression, name=f'{name}->{self.label_full}'), name
1059
1023
  )
1060
- if not has_time_dim:
1024
+ if self.shares[name].ndim == 0:
1061
1025
  self._eq_total.lhs -= self.shares[name]
1062
1026
  else:
1063
1027
  self._eq_total_per_timestep.lhs -= self.shares[name]
@@ -1086,12 +1050,7 @@ class PiecewiseEffectsModel(Model):
1086
1050
 
1087
1051
  def do_modeling(self):
1088
1052
  self.shares = {
1089
- effect: self.add(
1090
- self._model.add_variables(
1091
- coords=self._model.get_coords(time_dim=False), name=f'{self.label_full}|{effect}'
1092
- ),
1093
- f'{effect}',
1094
- )
1053
+ effect: self.add(self._model.add_variables(coords=None, name=f'{self.label_full}|{effect}'), f'{effect}')
1095
1054
  for effect in self._piecewise_shares
1096
1055
  }
1097
1056
 
@@ -1124,6 +1083,65 @@ class PiecewiseEffectsModel(Model):
1124
1083
  )
1125
1084
 
1126
1085
 
1086
+ class PiecewiseEffectsPerFlowHourModel(Model):
1087
+ def __init__(
1088
+ self,
1089
+ model: SystemModel,
1090
+ label_of_element: str,
1091
+ piecewise_origin: Tuple[str, Piecewise],
1092
+ piecewise_shares: Dict[str, Piecewise],
1093
+ zero_point: Optional[Union[bool, linopy.Variable]],
1094
+ label: str = 'PiecewiseEffectsPerFlowHour',
1095
+ ):
1096
+ super().__init__(model, label_of_element, label)
1097
+ assert len(piecewise_origin[1]) == len(list(piecewise_shares.values())[0]), (
1098
+ 'Piece length of variable_segments and share_segments must be equal'
1099
+ )
1100
+ self._zero_point = zero_point
1101
+ self._piecewise_origin = piecewise_origin
1102
+ self._piecewise_shares = piecewise_shares
1103
+
1104
+ self.shares: Dict[str, linopy.Variable] = {}
1105
+
1106
+ self.piecewise_model: Optional[PiecewiseModel] = None
1107
+
1108
+ def do_modeling(self):
1109
+ self.shares = {
1110
+ effect: self.add(
1111
+ self._model.add_variables(coords=self._model.coords, name=f'{self.label_full}|{effect}'), f'{effect}'
1112
+ )
1113
+ for effect in self._piecewise_shares
1114
+ }
1115
+
1116
+ piecewise_variables = {
1117
+ self._piecewise_origin[0]: self._piecewise_origin[1],
1118
+ **{
1119
+ self.shares[effect_label].name: self._piecewise_shares[effect_label]
1120
+ for effect_label in self._piecewise_shares
1121
+ },
1122
+ }
1123
+
1124
+ self.piecewise_model = self.add(
1125
+ PiecewiseModel(
1126
+ model=self._model,
1127
+ label_of_element=self.label_of_element,
1128
+ piecewise_variables=piecewise_variables,
1129
+ zero_point=self._zero_point,
1130
+ as_time_series=True,
1131
+ label='PiecewiseEffectsPerFlowHour',
1132
+ )
1133
+ )
1134
+
1135
+ self.piecewise_model.do_modeling()
1136
+
1137
+ # Shares
1138
+ self._model.effects.add_share_to_effects(
1139
+ name=self.label_of_element,
1140
+ expressions={effect: variable * self._model.hours_per_step for effect, variable in self.shares.items()},
1141
+ target='operation',
1142
+ )
1143
+
1144
+
1127
1145
  class PreventSimultaneousUsageModel(Model):
1128
1146
  """
1129
1147
  Prevents multiple Multiple Binary variables from being 1 at the same time