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/components.py CHANGED
@@ -3,13 +3,14 @@ This module contains the basic components of the flixopt framework.
3
3
  """
4
4
 
5
5
  import logging
6
+ import warnings
6
7
  from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Set, Tuple, Union
7
8
 
8
9
  import linopy
9
10
  import numpy as np
10
11
 
11
12
  from . import utils
12
- from .core import PlausibilityError, Scalar, ScenarioData, TimeSeries, TimestepData
13
+ from .core import NumericData, NumericDataTS, PlausibilityError, Scalar, TimeSeries
13
14
  from .elements import Component, ComponentModel, Flow
14
15
  from .features import InvestmentModel, OnOffModel, PiecewiseModel
15
16
  from .interface import InvestParameters, OnOffParameters, PiecewiseConversion
@@ -23,9 +24,119 @@ logger = logging.getLogger('flixopt')
23
24
 
24
25
  @register_class_for_io
25
26
  class LinearConverter(Component):
26
- """
27
- Converts input-Flows into output-Flows via linear conversion factors
27
+ """Convert input flows into output flows using linear or piecewise linear conversion factors.
28
+
29
+ This component models conversion equipment where input flows are transformed
30
+ into output flows with fixed or variable conversion ratios, such as:
31
+
32
+ - Heat pumps and chillers with variable efficiency
33
+ - Power plants with fuel-to-electricity conversion
34
+ - Chemical processes with multiple inputs/outputs
35
+ - Pumps and compressors
36
+ - Combined heat and power (CHP) plants
37
+
38
+ Args:
39
+ label: Unique identifier for the component in the FlowSystem.
40
+ inputs: List of input Flow objects that feed into the converter.
41
+ outputs: List of output Flow objects produced by the converter.
42
+ on_off_parameters: Controls binary on/off behavior of the converter.
43
+ When specified, the component can be completely turned on or off, affecting
44
+ all connected flows. This creates binary variables in the optimization.
45
+ For better performance, consider using OnOffParameters on individual flows instead.
46
+ conversion_factors: Linear conversion ratios between flows as time series data.
47
+ List of dictionaries mapping flow labels to their conversion factors.
48
+ Mutually exclusive with piecewise_conversion.
49
+ piecewise_conversion: Piecewise linear conversion relationships between flows.
50
+ Enables modeling of variable efficiency or discrete operating modes.
51
+ Mutually exclusive with conversion_factors.
52
+ meta_data: Additional information stored with the component.
53
+ Saved in results but not used internally. Use only Python native types.
54
+
55
+ Warning:
56
+ When using `piecewise_conversion` without `on_off_parameters`, flow rates cannot
57
+ reach zero unless explicitly defined with zero-valued pieces (e.g., `fx.Piece(0, 0)`).
58
+ This prevents unintended zero flows and maintains mathematical consistency.
59
+
60
+ To allow zero flow rates:
61
+
62
+ - Add `on_off_parameters` to enable complete shutdown, or
63
+ - Include explicit zero pieces in your `piecewise_conversion` definition
64
+
65
+ This behavior was clarified in v2.1.7 to prevent optimization edge cases.
66
+
67
+ Examples:
68
+ Simple heat pump with fixed COP:
69
+
70
+ ```python
71
+ heat_pump = fx.LinearConverter(
72
+ label='heat_pump',
73
+ inputs=[electricity_flow],
74
+ outputs=[heat_flow],
75
+ conversion_factors=[
76
+ {
77
+ 'electricity_flow': 1.0, # 1 kW electricity input
78
+ 'heat_flow': 3.5, # 3.5 kW heat output (COP=3.5)
79
+ }
80
+ ],
81
+ )
82
+ ```
83
+
84
+ Variable efficiency heat pump:
85
+
86
+ ```python
87
+ heat_pump = fx.LinearConverter(
88
+ label='variable_heat_pump',
89
+ inputs=[electricity_flow],
90
+ outputs=[heat_flow],
91
+ piecewise_conversion=fx.PiecewiseConversion(
92
+ {
93
+ 'electricity_flow': fx.Piecewise(
94
+ [
95
+ fx.Piece(0, 10), # Allow zero to 10 kW input
96
+ fx.Piece(10, 25), # Higher load operation
97
+ ]
98
+ ),
99
+ 'heat_flow': fx.Piecewise(
100
+ [
101
+ fx.Piece(0, 35), # COP=3.5 at low loads
102
+ fx.Piece(35, 75), # COP=3.0 at high loads
103
+ ]
104
+ ),
105
+ }
106
+ ),
107
+ )
108
+ ```
109
+
110
+ Combined heat and power plant:
111
+
112
+ ```python
113
+ chp_plant = fx.LinearConverter(
114
+ label='chp_plant',
115
+ inputs=[natural_gas_flow],
116
+ outputs=[electricity_flow, heat_flow],
117
+ conversion_factors=[
118
+ {
119
+ 'natural_gas_flow': 1.0, # 1 MW fuel input
120
+ 'electricity_flow': 0.4, # 40% electrical efficiency
121
+ 'heat_flow': 0.45, # 45% thermal efficiency
122
+ }
123
+ ],
124
+ on_off_parameters=fx.OnOffParameters(
125
+ min_on_hours=4, # Minimum 4-hour operation
126
+ min_off_hours=2, # Minimum 2-hour downtime
127
+ ),
128
+ )
129
+ ```
130
+
131
+ Note:
132
+ Either `conversion_factors` or `piecewise_conversion` must be specified, but not both.
133
+ The component automatically handles the mathematical relationships between all
134
+ connected flows according to the specified conversion ratios.
28
135
 
136
+ See Also:
137
+ PiecewiseConversion: For variable efficiency modeling
138
+ OnOffParameters: For binary on/off control
139
+ Flow: Input and output flow definitions
29
140
  """
30
141
 
31
142
  def __init__(
@@ -34,25 +145,10 @@ class LinearConverter(Component):
34
145
  inputs: List[Flow],
35
146
  outputs: List[Flow],
36
147
  on_off_parameters: OnOffParameters = None,
37
- conversion_factors: List[Dict[str, TimestepData]] = None,
148
+ conversion_factors: List[Dict[str, NumericDataTS]] = None,
38
149
  piecewise_conversion: Optional[PiecewiseConversion] = None,
39
150
  meta_data: Optional[Dict] = None,
40
151
  ):
41
- """
42
- Args:
43
- label: The label of the Element. Used to identify it in the FlowSystem
44
- inputs: The input Flows
45
- outputs: The output Flows
46
- on_off_parameters: Information about on and off state of LinearConverter.
47
- Component is On/Off, if all connected Flows are On/Off. This induces an On-Variable (binary) in all Flows!
48
- If possible, use OnOffParameters in a single Flow instead to keep the number of binary variables low.
49
- See class OnOffParameters.
50
- conversion_factors: linear relation between flows.
51
- Either 'conversion_factors' or 'piecewise_conversion' can be used!
52
- piecewise_conversion: Define a piecewise linear relation between flow rates of different flows.
53
- Either 'conversion_factors' or 'piecewise_conversion' can be used!
54
- meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
55
- """
56
152
  super().__init__(label, inputs, outputs, on_off_parameters, meta_data=meta_data)
57
153
  self.conversion_factors = conversion_factors or []
58
154
  self.piecewise_conversion = piecewise_conversion
@@ -86,9 +182,9 @@ class LinearConverter(Component):
86
182
  if self.piecewise_conversion:
87
183
  for flow in self.flows.values():
88
184
  if isinstance(flow.size, InvestParameters) and flow.size.fixed_size is None:
89
- logger.warning(
90
- f'Using a FLow with a fixed size ({flow.label_full}) AND a piecewise_conversion '
91
- f'(in {self.label_full}) and variable size is uncommon. Please check if this is intended!'
185
+ raise PlausibilityError(
186
+ f'piecewise_conversion (in {self.label_full}) and variable size '
187
+ f'(in flow {flow.label_full}) do not make sense together!'
92
188
  )
93
189
 
94
190
  def transform_data(self, flow_system: 'FlowSystem'):
@@ -96,7 +192,6 @@ class LinearConverter(Component):
96
192
  if self.conversion_factors:
97
193
  self.conversion_factors = self._transform_conversion_factors(flow_system)
98
194
  if self.piecewise_conversion:
99
- self.piecewise_conversion.has_time_dim = True
100
195
  self.piecewise_conversion.transform_data(flow_system, f'{self.label_full}|PiecewiseConversion')
101
196
 
102
197
  def _transform_conversion_factors(self, flow_system: 'FlowSystem') -> List[Dict[str, TimeSeries]]:
@@ -128,17 +223,16 @@ class Storage(Component):
128
223
  label: str,
129
224
  charging: Flow,
130
225
  discharging: Flow,
131
- capacity_in_flow_hours: Union[ScenarioData, InvestParameters],
132
- relative_minimum_charge_state: TimestepData = 0,
133
- relative_maximum_charge_state: TimestepData = 1,
134
- initial_charge_state: Union[ScenarioData, Literal['lastValueOfSim']] = 0,
135
- minimal_final_charge_state: Optional[ScenarioData] = None,
136
- maximal_final_charge_state: Optional[ScenarioData] = None,
137
- eta_charge: TimestepData = 1,
138
- eta_discharge: TimestepData = 1,
139
- relative_loss_per_hour: TimestepData = 0,
226
+ capacity_in_flow_hours: Union[Scalar, InvestParameters],
227
+ relative_minimum_charge_state: NumericData = 0,
228
+ relative_maximum_charge_state: NumericData = 1,
229
+ initial_charge_state: Union[Scalar, Literal['lastValueOfSim']] = 0,
230
+ minimal_final_charge_state: Optional[Scalar] = None,
231
+ maximal_final_charge_state: Optional[Scalar] = None,
232
+ eta_charge: NumericData = 1,
233
+ eta_discharge: NumericData = 1,
234
+ relative_loss_per_hour: NumericData = 0,
140
235
  prevent_simultaneous_charge_and_discharge: bool = True,
141
- balanced: bool = False,
142
236
  meta_data: Optional[Dict] = None,
143
237
  ):
144
238
  """
@@ -164,7 +258,6 @@ class Storage(Component):
164
258
  relative_loss_per_hour: loss per chargeState-Unit per hour. The default is 0.
165
259
  prevent_simultaneous_charge_and_discharge: If True, loading and unloading at the same time is not possible.
166
260
  Increases the number of binary variables, but is recommended for easier evaluation. The default is True.
167
- balanced: Wether to equate the size of the charging and discharging flow. Only if not fixed.
168
261
  meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
169
262
  """
170
263
  # TODO: fixed_relative_chargeState implementieren
@@ -179,18 +272,17 @@ class Storage(Component):
179
272
  self.charging = charging
180
273
  self.discharging = discharging
181
274
  self.capacity_in_flow_hours = capacity_in_flow_hours
182
- self.relative_minimum_charge_state: TimestepData = relative_minimum_charge_state
183
- self.relative_maximum_charge_state: TimestepData = relative_maximum_charge_state
275
+ self.relative_minimum_charge_state: NumericDataTS = relative_minimum_charge_state
276
+ self.relative_maximum_charge_state: NumericDataTS = relative_maximum_charge_state
184
277
 
185
278
  self.initial_charge_state = initial_charge_state
186
279
  self.minimal_final_charge_state = minimal_final_charge_state
187
280
  self.maximal_final_charge_state = maximal_final_charge_state
188
281
 
189
- self.eta_charge: TimestepData = eta_charge
190
- self.eta_discharge: TimestepData = eta_discharge
191
- self.relative_loss_per_hour: TimestepData = relative_loss_per_hour
282
+ self.eta_charge: NumericDataTS = eta_charge
283
+ self.eta_discharge: NumericDataTS = eta_discharge
284
+ self.relative_loss_per_hour: NumericDataTS = relative_loss_per_hour
192
285
  self.prevent_simultaneous_charge_and_discharge = prevent_simultaneous_charge_and_discharge
193
- self.balanced = balanced
194
286
 
195
287
  def create_model(self, model: SystemModel) -> 'StorageModel':
196
288
  self._plausibility_checks()
@@ -202,83 +294,55 @@ class Storage(Component):
202
294
  self.relative_minimum_charge_state = flow_system.create_time_series(
203
295
  f'{self.label_full}|relative_minimum_charge_state',
204
296
  self.relative_minimum_charge_state,
205
- has_extra_timestep=True,
297
+ needs_extra_timestep=True,
206
298
  )
207
299
  self.relative_maximum_charge_state = flow_system.create_time_series(
208
300
  f'{self.label_full}|relative_maximum_charge_state',
209
301
  self.relative_maximum_charge_state,
210
- has_extra_timestep=True,
302
+ needs_extra_timestep=True,
211
303
  )
212
304
  self.eta_charge = flow_system.create_time_series(f'{self.label_full}|eta_charge', self.eta_charge)
213
305
  self.eta_discharge = flow_system.create_time_series(f'{self.label_full}|eta_discharge', self.eta_discharge)
214
306
  self.relative_loss_per_hour = flow_system.create_time_series(
215
307
  f'{self.label_full}|relative_loss_per_hour', self.relative_loss_per_hour
216
308
  )
217
- if not isinstance(self.initial_charge_state, str):
218
- self.initial_charge_state = flow_system.create_time_series(
219
- f'{self.label_full}|initial_charge_state', self.initial_charge_state, has_time_dim=False
220
- )
221
- self.minimal_final_charge_state = flow_system.create_time_series(
222
- f'{self.label_full}|minimal_final_charge_state', self.minimal_final_charge_state, has_time_dim=False
223
- )
224
- self.maximal_final_charge_state = flow_system.create_time_series(
225
- f'{self.label_full}|maximal_final_charge_state', self.maximal_final_charge_state, has_time_dim=False
226
- )
227
309
  if isinstance(self.capacity_in_flow_hours, InvestParameters):
228
- self.capacity_in_flow_hours.transform_data(flow_system, f'{self.label_full}|InvestParameters')
229
- else:
230
- self.capacity_in_flow_hours = flow_system.create_time_series(
231
- f'{self.label_full}|capacity_in_flow_hours', self.capacity_in_flow_hours, has_time_dim=False
232
- )
310
+ self.capacity_in_flow_hours.transform_data(flow_system)
233
311
 
234
312
  def _plausibility_checks(self) -> None:
235
313
  """
236
314
  Check for infeasible or uncommon combinations of parameters
237
315
  """
238
316
  super()._plausibility_checks()
239
- if isinstance(self.initial_charge_state, str):
240
- if self.initial_charge_state != 'lastValueOfSim':
241
- raise PlausibilityError(f'initial_charge_state has undefined value: {self.initial_charge_state}')
242
- return
243
- if isinstance(self.capacity_in_flow_hours, InvestParameters):
244
- if self.capacity_in_flow_hours.fixed_size is None:
245
- maximum_capacity = self.capacity_in_flow_hours.maximum_size
246
- minimum_capacity = self.capacity_in_flow_hours.minimum_size
317
+ if utils.is_number(self.initial_charge_state):
318
+ if isinstance(self.capacity_in_flow_hours, InvestParameters):
319
+ if self.capacity_in_flow_hours.fixed_size is None:
320
+ maximum_capacity = self.capacity_in_flow_hours.maximum_size
321
+ minimum_capacity = self.capacity_in_flow_hours.minimum_size
322
+ else:
323
+ maximum_capacity = self.capacity_in_flow_hours.fixed_size
324
+ minimum_capacity = self.capacity_in_flow_hours.fixed_size
247
325
  else:
248
- maximum_capacity = self.capacity_in_flow_hours.fixed_size
249
- minimum_capacity = self.capacity_in_flow_hours.fixed_size
250
- else:
251
- maximum_capacity = self.capacity_in_flow_hours
252
- minimum_capacity = self.capacity_in_flow_hours
253
-
254
- # initial capacity >= allowed min for maximum_size:
255
- minimum_inital_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=0)
256
- # initial capacity <= allowed max for minimum_size:
257
- maximum_inital_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=0)
258
- # TODO: index=1 ??? I think index 0
259
-
260
- if (self.initial_charge_state > maximum_inital_capacity).any():
261
- raise ValueError(
262
- f'{self.label_full}: {self.initial_charge_state=} '
263
- f'is above allowed maximum charge_state {maximum_inital_capacity}'
264
- )
265
- if (self.initial_charge_state < minimum_inital_capacity).any():
266
- raise ValueError(
267
- f'{self.label_full}: {self.initial_charge_state=} '
268
- f'is below allowed minimum charge_state {minimum_inital_capacity}'
269
- )
326
+ maximum_capacity = self.capacity_in_flow_hours
327
+ minimum_capacity = self.capacity_in_flow_hours
270
328
 
271
- if self.balanced:
272
- if not isinstance(self.charging.size, InvestParameters) or not isinstance(self.discharging.size, InvestParameters):
273
- raise PlausibilityError(
274
- f'Balancing charging and discharging Flows in {self.label_full} '
275
- f'is only possible with Investments.')
276
- if (self.charging.size.minimum_size > self.discharging.size.maximum_size or
277
- self.charging.size.maximum_size < self.discharging.size.minimum_size):
278
- raise PlausibilityError(
279
- f'Balancing charging and discharging Flows in {self.label_full} need compatible minimum and maximum sizes.'
280
- f'Got: {self.charging.size.minimum_size=}, {self.charging.size.maximum_size=} and '
281
- f'{self.charging.size.minimum_size=}, {self.charging.size.maximum_size=}.')
329
+ # initial capacity >= allowed min for maximum_size:
330
+ minimum_inital_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=1)
331
+ # initial capacity <= allowed max for minimum_size:
332
+ maximum_inital_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=1)
333
+
334
+ if self.initial_charge_state > maximum_inital_capacity:
335
+ raise ValueError(
336
+ f'{self.label_full}: {self.initial_charge_state=} '
337
+ f'is above allowed maximum charge_state {maximum_inital_capacity}'
338
+ )
339
+ if self.initial_charge_state < minimum_inital_capacity:
340
+ raise ValueError(
341
+ f'{self.label_full}: {self.initial_charge_state=} '
342
+ f'is below allowed minimum charge_state {minimum_inital_capacity}'
343
+ )
344
+ elif self.initial_charge_state != 'lastValueOfSim':
345
+ raise ValueError(f'{self.label_full}: {self.initial_charge_state=} has an invalid value')
282
346
 
283
347
 
284
348
  @register_class_for_io
@@ -296,8 +360,8 @@ class Transmission(Component):
296
360
  out1: Flow,
297
361
  in2: Optional[Flow] = None,
298
362
  out2: Optional[Flow] = None,
299
- relative_losses: Optional[TimestepData] = None,
300
- absolute_losses: Optional[TimestepData] = None,
363
+ relative_losses: Optional[NumericDataTS] = None,
364
+ absolute_losses: Optional[NumericDataTS] = None,
301
365
  on_off_parameters: OnOffParameters = None,
302
366
  prevent_simultaneous_flows_in_both_directions: bool = True,
303
367
  meta_data: Optional[Dict] = None,
@@ -380,7 +444,7 @@ class TransmissionModel(ComponentModel):
380
444
  def do_modeling(self):
381
445
  """Initiates all FlowModels"""
382
446
  # Force On Variable if absolute losses are present
383
- if (self.element.absolute_losses is not None) and np.any(self.element.absolute_losses.selected_data != 0):
447
+ if (self.element.absolute_losses is not None) and np.any(self.element.absolute_losses.active_data != 0):
384
448
  for flow in self.element.inputs + self.element.outputs:
385
449
  if flow.on_off_parameters is None:
386
450
  flow.on_off_parameters = OnOffParameters()
@@ -417,14 +481,14 @@ class TransmissionModel(ComponentModel):
417
481
  # eq: out(t) + on(t)*loss_abs(t) = in(t)*(1 - loss_rel(t))
418
482
  con_transmission = self.add(
419
483
  self._model.add_constraints(
420
- out_flow.model.flow_rate == -in_flow.model.flow_rate * (self.element.relative_losses.selected_data - 1),
484
+ out_flow.model.flow_rate == -in_flow.model.flow_rate * (self.element.relative_losses.active_data - 1),
421
485
  name=f'{self.label_full}|{name}',
422
486
  ),
423
487
  name,
424
488
  )
425
489
 
426
490
  if self.element.absolute_losses is not None:
427
- con_transmission.lhs += in_flow.model.on_off.on * self.element.absolute_losses.selected_data
491
+ con_transmission.lhs += in_flow.model.on_off.on * self.element.absolute_losses.active_data
428
492
 
429
493
  return con_transmission
430
494
 
@@ -452,10 +516,8 @@ class LinearConverterModel(ComponentModel):
452
516
 
453
517
  self.add(
454
518
  self._model.add_constraints(
455
- sum([flow.model.flow_rate * conv_factors[flow.label].selected_data for flow in used_inputs])
456
- == sum(
457
- [flow.model.flow_rate * conv_factors[flow.label].selected_data for flow in used_outputs]
458
- ),
519
+ sum([flow.model.flow_rate * conv_factors[flow.label].active_data for flow in used_inputs])
520
+ == sum([flow.model.flow_rate * conv_factors[flow.label].active_data for flow in used_outputs]),
459
521
  name=f'{self.label_full}|conversion_{i}',
460
522
  )
461
523
  )
@@ -495,15 +557,12 @@ class StorageModel(ComponentModel):
495
557
  lb, ub = self.absolute_charge_state_bounds
496
558
  self.charge_state = self.add(
497
559
  self._model.add_variables(
498
- lower=lb,
499
- upper=ub,
500
- coords=self._model.get_coords(extra_timestep=True),
501
- name=f'{self.label_full}|charge_state',
560
+ lower=lb, upper=ub, coords=self._model.coords_extra, name=f'{self.label_full}|charge_state'
502
561
  ),
503
562
  'charge_state',
504
563
  )
505
564
  self.netto_discharge = self.add(
506
- self._model.add_variables(coords=self._model.get_coords(), name=f'{self.label_full}|netto_discharge'),
565
+ self._model.add_variables(coords=self._model.coords, name=f'{self.label_full}|netto_discharge'),
507
566
  'netto_discharge',
508
567
  )
509
568
  # netto_discharge:
@@ -518,17 +577,17 @@ class StorageModel(ComponentModel):
518
577
  )
519
578
 
520
579
  charge_state = self.charge_state
521
- rel_loss = self.element.relative_loss_per_hour.selected_data
580
+ rel_loss = self.element.relative_loss_per_hour.active_data
522
581
  hours_per_step = self._model.hours_per_step
523
582
  charge_rate = self.element.charging.model.flow_rate
524
583
  discharge_rate = self.element.discharging.model.flow_rate
525
- eff_charge = self.element.eta_charge.selected_data
526
- eff_discharge = self.element.eta_discharge.selected_data
584
+ eff_charge = self.element.eta_charge.active_data
585
+ eff_discharge = self.element.eta_discharge.active_data
527
586
 
528
587
  self.add(
529
588
  self._model.add_constraints(
530
589
  charge_state.isel(time=slice(1, None))
531
- == charge_state.isel(time=slice(None, -1)) * (1 - rel_loss * hours_per_step)
590
+ == charge_state.isel(time=slice(None, -1)) * ((1 - rel_loss) ** hours_per_step)
532
591
  + charge_rate * eff_charge * hours_per_step
533
592
  - discharge_rate * eff_discharge * hours_per_step,
534
593
  name=f'{self.label_full}|charge_state',
@@ -550,34 +609,29 @@ class StorageModel(ComponentModel):
550
609
  # Initial charge state
551
610
  self._initial_and_final_charge_state()
552
611
 
553
- if self.element.balanced:
554
- self.add(
555
- self._model.add_constraints(
556
- self.element.charging.model._investment.size * 1 == self.element.discharging.model._investment.size * 1,
557
- name=f'{self.label_full}|balanced_sizes',
558
- ),
559
- 'balanced_sizes'
560
- )
561
-
562
612
  def _initial_and_final_charge_state(self):
563
613
  if self.element.initial_charge_state is not None:
564
614
  name_short = 'initial_charge_state'
565
615
  name = f'{self.label_full}|{name_short}'
566
616
 
567
- if isinstance(self.element.initial_charge_state, str):
617
+ if utils.is_number(self.element.initial_charge_state):
568
618
  self.add(
569
619
  self._model.add_constraints(
570
- self.charge_state.isel(time=0) == self.charge_state.isel(time=-1), name=name
620
+ self.charge_state.isel(time=0) == self.element.initial_charge_state, name=name
571
621
  ),
572
622
  name_short,
573
623
  )
574
- else:
624
+ elif self.element.initial_charge_state == 'lastValueOfSim':
575
625
  self.add(
576
626
  self._model.add_constraints(
577
- self.charge_state.isel(time=0) == self.element.initial_charge_state, name=name
627
+ self.charge_state.isel(time=0) == self.charge_state.isel(time=-1), name=name
578
628
  ),
579
629
  name_short,
580
630
  )
631
+ else: # TODO: Validation in Storage Class, not in Model
632
+ raise PlausibilityError(
633
+ f'initial_charge_state has undefined value: {self.element.initial_charge_state}'
634
+ )
581
635
 
582
636
  if self.element.maximal_final_charge_state is not None:
583
637
  self.add(
@@ -598,7 +652,7 @@ class StorageModel(ComponentModel):
598
652
  )
599
653
 
600
654
  @property
601
- def absolute_charge_state_bounds(self) -> Tuple[TimestepData, TimestepData]:
655
+ def absolute_charge_state_bounds(self) -> Tuple[NumericData, NumericData]:
602
656
  relative_lower_bound, relative_upper_bound = self.relative_charge_state_bounds
603
657
  if not isinstance(self.element.capacity_in_flow_hours, InvestParameters):
604
658
  return (
@@ -612,10 +666,10 @@ class StorageModel(ComponentModel):
612
666
  )
613
667
 
614
668
  @property
615
- def relative_charge_state_bounds(self) -> Tuple[TimestepData, TimestepData]:
669
+ def relative_charge_state_bounds(self) -> Tuple[NumericData, NumericData]:
616
670
  return (
617
- self.element.relative_minimum_charge_state,
618
- self.element.relative_maximum_charge_state,
671
+ self.element.relative_minimum_charge_state.active_data,
672
+ self.element.relative_maximum_charge_state.active_data,
619
673
  )
620
674
 
621
675
 
@@ -628,52 +682,170 @@ class SourceAndSink(Component):
628
682
  def __init__(
629
683
  self,
630
684
  label: str,
631
- source: Flow,
632
- sink: Flow,
633
- prevent_simultaneous_sink_and_source: bool = True,
685
+ inputs: List[Flow] = None,
686
+ outputs: List[Flow] = None,
687
+ prevent_simultaneous_flow_rates: bool = True,
634
688
  meta_data: Optional[Dict] = None,
689
+ **kwargs,
635
690
  ):
636
691
  """
637
692
  Args:
638
693
  label: The label of the Element. Used to identify it in the FlowSystem
639
- source: output-flow of this component
640
- sink: input-flow of this component
641
- prevent_simultaneous_sink_and_source: If True, inflow and outflow can not be active simultaniously.
694
+ outputs: output-flows of this component
695
+ inputs: input-flows of this component
696
+ prevent_simultaneous_flow_rates: If True, inflow and outflow can not be active simultaniously.
642
697
  meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
643
698
  """
699
+ source = kwargs.pop('source', None)
700
+ sink = kwargs.pop('sink', None)
701
+ prevent_simultaneous_sink_and_source = kwargs.pop('prevent_simultaneous_sink_and_source', None)
702
+ if source is not None:
703
+ warnings.deprecated(
704
+ 'The use of the source argument is deprecated. Use the outputs argument instead.',
705
+ stacklevel=2,
706
+ )
707
+ if outputs is not None:
708
+ raise ValueError('Either source or outputs can be specified, but not both.')
709
+ outputs = [source]
710
+
711
+ if sink is not None:
712
+ warnings.deprecated(
713
+ 'The use of the sink argument is deprecated. Use the outputs argument instead.',
714
+ stacklevel=2,
715
+ )
716
+ if inputs is not None:
717
+ raise ValueError('Either sink or outputs can be specified, but not both.')
718
+ inputs = [sink]
719
+
720
+ if prevent_simultaneous_sink_and_source is not None:
721
+ warnings.deprecated(
722
+ 'The use of the prevent_simultaneous_sink_and_source argument is deprecated. Use the prevent_simultaneous_flow_rates argument instead.',
723
+ stacklevel=2,
724
+ )
725
+ prevent_simultaneous_flow_rates = prevent_simultaneous_sink_and_source
726
+
644
727
  super().__init__(
645
728
  label,
646
- inputs=[sink],
647
- outputs=[source],
648
- prevent_simultaneous_flows=[sink, source] if prevent_simultaneous_sink_and_source is True else None,
729
+ inputs=inputs,
730
+ outputs=outputs,
731
+ prevent_simultaneous_flows=inputs + outputs if prevent_simultaneous_flow_rates is True else None,
649
732
  meta_data=meta_data,
650
733
  )
651
- self.source = source
652
- self.sink = sink
653
- self.prevent_simultaneous_sink_and_source = prevent_simultaneous_sink_and_source
734
+ self.prevent_simultaneous_flow_rates = prevent_simultaneous_flow_rates
735
+
736
+ @property
737
+ def source(self) -> Flow:
738
+ warnings.warn(
739
+ 'The source property is deprecated. Use the outputs property instead.',
740
+ DeprecationWarning,
741
+ stacklevel=2,
742
+ )
743
+ return self.outputs[0]
744
+
745
+ @property
746
+ def sink(self) -> Flow:
747
+ warnings.warn(
748
+ 'The sink property is deprecated. Use the outputs property instead.',
749
+ DeprecationWarning,
750
+ stacklevel=2,
751
+ )
752
+ return self.inputs[0]
753
+
754
+ @property
755
+ def prevent_simultaneous_sink_and_source(self) -> bool:
756
+ warnings.warn(
757
+ 'The prevent_simultaneous_sink_and_source property is deprecated. Use the prevent_simultaneous_flow_rates property instead.',
758
+ DeprecationWarning,
759
+ stacklevel=2,
760
+ )
761
+ return self.prevent_simultaneous_flow_rates
654
762
 
655
763
 
656
764
  @register_class_for_io
657
765
  class Source(Component):
658
- def __init__(self, label: str, source: Flow, meta_data: Optional[Dict] = None):
766
+ def __init__(
767
+ self,
768
+ label: str,
769
+ outputs: List[Flow] = None,
770
+ meta_data: Optional[Dict] = None,
771
+ prevent_simultaneous_flow_rates: bool = False,
772
+ **kwargs,
773
+ ):
659
774
  """
660
775
  Args:
661
776
  label: The label of the Element. Used to identify it in the FlowSystem
662
- source: output-flow of source
777
+ outputs: output-flows of source
663
778
  meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
664
779
  """
665
- super().__init__(label, outputs=[source], meta_data=meta_data)
666
- self.source = source
780
+ source = kwargs.pop('source', None)
781
+ if source is not None:
782
+ warnings.warn(
783
+ 'The use of the source argument is deprecated. Use the outputs argument instead.',
784
+ DeprecationWarning,
785
+ stacklevel=2,
786
+ )
787
+ if outputs is not None:
788
+ raise ValueError('Either source or outputs can be specified, but not both.')
789
+ outputs = [source]
790
+
791
+ self.prevent_simultaneous_flow_rates = prevent_simultaneous_flow_rates
792
+ super().__init__(
793
+ label,
794
+ outputs=outputs,
795
+ meta_data=meta_data,
796
+ prevent_simultaneous_flows=outputs if prevent_simultaneous_flow_rates else None,
797
+ )
798
+
799
+ @property
800
+ def source(self) -> Flow:
801
+ warnings.warn(
802
+ 'The source property is deprecated. Use the outputs property instead.',
803
+ DeprecationWarning,
804
+ stacklevel=2,
805
+ )
806
+ return self.outputs[0]
667
807
 
668
808
 
669
809
  @register_class_for_io
670
810
  class Sink(Component):
671
- def __init__(self, label: str, sink: Flow, meta_data: Optional[Dict] = None):
811
+ def __init__(
812
+ self,
813
+ label: str,
814
+ inputs: List[Flow] = None,
815
+ meta_data: Optional[Dict] = None,
816
+ prevent_simultaneous_flow_rates: bool = False,
817
+ **kwargs,
818
+ ):
672
819
  """
673
820
  Args:
674
821
  label: The label of the Element. Used to identify it in the FlowSystem
675
- meta_data: used to store more information about the element. Is not used internally, but saved in the results
676
- sink: input-flow of sink
822
+ inputs: output-flows of source
823
+ meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
677
824
  """
678
- super().__init__(label, inputs=[sink], meta_data=meta_data)
679
- self.sink = sink
825
+ sink = kwargs.pop('sink', None)
826
+ if sink is not None:
827
+ warnings.warn(
828
+ 'The use of the sink argument is deprecated. Use the outputs argument instead.',
829
+ DeprecationWarning,
830
+ stacklevel=2,
831
+ )
832
+ if inputs is not None:
833
+ raise ValueError('Either sink or outputs can be specified, but not both.')
834
+ inputs = [sink]
835
+
836
+ self.prevent_simultaneous_flow_rates = prevent_simultaneous_flow_rates
837
+ super().__init__(
838
+ label,
839
+ inputs=inputs,
840
+ meta_data=meta_data,
841
+ prevent_simultaneous_flows=inputs if prevent_simultaneous_flow_rates else None,
842
+ )
843
+
844
+ @property
845
+ def sink(self) -> Flow:
846
+ warnings.warn(
847
+ 'The sink property is deprecated. Use the outputs property instead.',
848
+ DeprecationWarning,
849
+ stacklevel=2,
850
+ )
851
+ return self.inputs[0]