flixopt 2.2.0b0__py3-none-any.whl → 3.0.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.

Files changed (63) hide show
  1. flixopt/__init__.py +35 -1
  2. flixopt/aggregation.py +60 -81
  3. flixopt/calculation.py +381 -196
  4. flixopt/components.py +1022 -359
  5. flixopt/config.py +553 -191
  6. flixopt/core.py +475 -1315
  7. flixopt/effects.py +477 -214
  8. flixopt/elements.py +591 -344
  9. flixopt/features.py +403 -957
  10. flixopt/flow_system.py +781 -293
  11. flixopt/interface.py +1159 -189
  12. flixopt/io.py +50 -55
  13. flixopt/linear_converters.py +384 -92
  14. flixopt/modeling.py +759 -0
  15. flixopt/network_app.py +789 -0
  16. flixopt/plotting.py +273 -135
  17. flixopt/results.py +639 -383
  18. flixopt/solvers.py +25 -21
  19. flixopt/structure.py +928 -442
  20. flixopt/utils.py +34 -5
  21. flixopt-3.0.0.dist-info/METADATA +209 -0
  22. flixopt-3.0.0.dist-info/RECORD +26 -0
  23. {flixopt-2.2.0b0.dist-info → flixopt-3.0.0.dist-info}/WHEEL +1 -1
  24. flixopt-3.0.0.dist-info/top_level.txt +1 -0
  25. docs/examples/00-Minimal Example.md +0 -5
  26. docs/examples/01-Basic Example.md +0 -5
  27. docs/examples/02-Complex Example.md +0 -10
  28. docs/examples/03-Calculation Modes.md +0 -5
  29. docs/examples/index.md +0 -5
  30. docs/faq/contribute.md +0 -49
  31. docs/faq/index.md +0 -3
  32. docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
  33. docs/images/architecture_flixOpt.png +0 -0
  34. docs/images/flixopt-icon.svg +0 -1
  35. docs/javascripts/mathjax.js +0 -18
  36. docs/release-notes/_template.txt +0 -32
  37. docs/release-notes/index.md +0 -7
  38. docs/release-notes/v2.0.0.md +0 -93
  39. docs/release-notes/v2.0.1.md +0 -12
  40. docs/release-notes/v2.1.0.md +0 -31
  41. docs/release-notes/v2.2.0.md +0 -55
  42. docs/user-guide/Mathematical Notation/Bus.md +0 -33
  43. docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +0 -132
  44. docs/user-guide/Mathematical Notation/Flow.md +0 -26
  45. docs/user-guide/Mathematical Notation/Investment.md +0 -115
  46. docs/user-guide/Mathematical Notation/LinearConverter.md +0 -21
  47. docs/user-guide/Mathematical Notation/Piecewise.md +0 -49
  48. docs/user-guide/Mathematical Notation/Storage.md +0 -44
  49. docs/user-guide/Mathematical Notation/index.md +0 -22
  50. docs/user-guide/Mathematical Notation/others.md +0 -3
  51. docs/user-guide/index.md +0 -124
  52. flixopt/config.yaml +0 -10
  53. flixopt-2.2.0b0.dist-info/METADATA +0 -146
  54. flixopt-2.2.0b0.dist-info/RECORD +0 -59
  55. flixopt-2.2.0b0.dist-info/top_level.txt +0 -5
  56. pics/architecture_flixOpt-pre2.0.0.png +0 -0
  57. pics/architecture_flixOpt.png +0 -0
  58. pics/flixOpt_plotting.jpg +0 -0
  59. pics/flixopt-icon.svg +0 -1
  60. pics/pics.pptx +0 -0
  61. scripts/gen_ref_pages.py +0 -54
  62. tests/ressources/Zeitreihen2020.csv +0 -35137
  63. {flixopt-2.2.0b0.dist-info → flixopt-3.0.0.dist-info}/licenses/LICENSE +0 -0
flixopt/components.py CHANGED
@@ -2,20 +2,25 @@
2
2
  This module contains the basic components of the flixopt framework.
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+
5
7
  import logging
6
- from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Set, Tuple, Union
8
+ import warnings
9
+ from typing import TYPE_CHECKING, Literal
7
10
 
8
- import linopy
9
11
  import numpy as np
12
+ import xarray as xr
10
13
 
11
- from . import utils
12
- from .core import PlausibilityError, Scalar, ScenarioData, TimeSeries, TimestepData
14
+ from .core import PeriodicDataUser, PlausibilityError, TemporalData, TemporalDataUser
13
15
  from .elements import Component, ComponentModel, Flow
14
- from .features import InvestmentModel, OnOffModel, PiecewiseModel
16
+ from .features import InvestmentModel, PiecewiseModel
15
17
  from .interface import InvestParameters, OnOffParameters, PiecewiseConversion
16
- from .structure import SystemModel, register_class_for_io
18
+ from .modeling import BoundingPatterns
19
+ from .structure import FlowSystemModel, register_class_for_io
17
20
 
18
21
  if TYPE_CHECKING:
22
+ import linopy
23
+
19
24
  from .flow_system import FlowSystem
20
25
 
21
26
  logger = logging.getLogger('flixopt')
@@ -24,43 +29,155 @@ logger = logging.getLogger('flixopt')
24
29
  @register_class_for_io
25
30
  class LinearConverter(Component):
26
31
  """
27
- Converts input-Flows into output-Flows via linear conversion factors
32
+ Converts input-Flows into output-Flows via linear conversion factors.
33
+
34
+ LinearConverter models equipment that transforms one or more input flows into one or
35
+ more output flows through linear relationships. This includes heat exchangers,
36
+ electrical converters, chemical reactors, and other equipment where the
37
+ relationship between inputs and outputs can be expressed as linear equations.
38
+
39
+ The component supports two modeling approaches: simple conversion factors for
40
+ straightforward linear relationships, or piecewise conversion for complex non-linear
41
+ behavior approximated through piecewise linear segments.
42
+
43
+ Mathematical Formulation:
44
+ See the complete mathematical model in the documentation:
45
+ [LinearConverter](../user-guide/mathematical-notation/elements/LinearConverter.md)
46
+
47
+ Args:
48
+ label: The label of the Element. Used to identify it in the FlowSystem.
49
+ inputs: list of input Flows that feed into the converter.
50
+ outputs: list of output Flows that are produced by the converter.
51
+ on_off_parameters: Information about on and off state of LinearConverter.
52
+ Component is On/Off if all connected Flows are On/Off. This induces an
53
+ On-Variable (binary) in all Flows! If possible, use OnOffParameters in a
54
+ single Flow instead to keep the number of binary variables low.
55
+ conversion_factors: Linear relationships between flows expressed as a list of
56
+ dictionaries. Each dictionary maps flow labels to their coefficients in one
57
+ linear equation. The number of conversion factors must be less than the total
58
+ number of flows to ensure degrees of freedom > 0. Either 'conversion_factors'
59
+ OR 'piecewise_conversion' can be used, but not both.
60
+ For examples also look into the linear_converters.py file.
61
+ piecewise_conversion: Define piecewise linear relationships between flow rates
62
+ of different flows. Enables modeling of non-linear conversion behavior through
63
+ linear approximation. Either 'conversion_factors' or 'piecewise_conversion'
64
+ can be used, but not both.
65
+ meta_data: Used to store additional information about the Element. Not used
66
+ internally, but saved in results. Only use Python native types.
67
+
68
+ Examples:
69
+ Simple 1:1 heat exchanger with 95% efficiency:
70
+
71
+ ```python
72
+ heat_exchanger = LinearConverter(
73
+ label='primary_hx',
74
+ inputs=[hot_water_in],
75
+ outputs=[hot_water_out],
76
+ conversion_factors=[{'hot_water_in': 0.95, 'hot_water_out': 1}],
77
+ )
78
+ ```
79
+
80
+ Multi-input heat pump with COP=3:
81
+
82
+ ```python
83
+ heat_pump = LinearConverter(
84
+ label='air_source_hp',
85
+ inputs=[electricity_in],
86
+ outputs=[heat_output],
87
+ conversion_factors=[{'electricity_in': 3, 'heat_output': 1}],
88
+ )
89
+ ```
90
+
91
+ Combined heat and power (CHP) unit with multiple outputs:
92
+
93
+ ```python
94
+ chp_unit = LinearConverter(
95
+ label='gas_chp',
96
+ inputs=[natural_gas],
97
+ outputs=[electricity_out, heat_out],
98
+ conversion_factors=[
99
+ {'natural_gas': 0.35, 'electricity_out': 1},
100
+ {'natural_gas': 0.45, 'heat_out': 1},
101
+ ],
102
+ )
103
+ ```
104
+
105
+ Electrolyzer with multiple conversion relationships:
106
+
107
+ ```python
108
+ electrolyzer = LinearConverter(
109
+ label='pem_electrolyzer',
110
+ inputs=[electricity_in, water_in],
111
+ outputs=[hydrogen_out, oxygen_out],
112
+ conversion_factors=[
113
+ {'electricity_in': 1, 'hydrogen_out': 50}, # 50 kWh/kg H2
114
+ {'water_in': 1, 'hydrogen_out': 9}, # 9 kg H2O/kg H2
115
+ {'hydrogen_out': 8, 'oxygen_out': 1}, # Mass balance
116
+ ],
117
+ )
118
+ ```
119
+
120
+ Complex converter with piecewise efficiency:
121
+
122
+ ```python
123
+ variable_efficiency_converter = LinearConverter(
124
+ label='variable_converter',
125
+ inputs=[fuel_in],
126
+ outputs=[power_out],
127
+ piecewise_conversion=PiecewiseConversion(
128
+ {
129
+ 'fuel_in': Piecewise(
130
+ [
131
+ Piece(0, 10), # Low load operation
132
+ Piece(10, 25), # High load operation
133
+ ]
134
+ ),
135
+ 'power_out': Piecewise(
136
+ [
137
+ Piece(0, 3.5), # Lower efficiency at part load
138
+ Piece(3.5, 10), # Higher efficiency at full load
139
+ ]
140
+ ),
141
+ }
142
+ ),
143
+ )
144
+ ```
145
+
146
+ Note:
147
+ Conversion factors define linear relationships where the sum of (coefficient × flow_rate)
148
+ equals zero for each equation: factor1×flow1 + factor2×flow2 + ... = 0
149
+ Conversion factors define linear relationships:
150
+ `{flow1: a1, flow2: a2, ...}` yields `a1×flow_rate1 + a2×flow_rate2 + ... = 0`.
151
+ Note: The input format may be unintuitive. For example,
152
+ `{"electricity": 1, "H2": 50}` implies `1×electricity = 50×H2`,
153
+ i.e., 50 units of electricity produce 1 unit of H2.
154
+
155
+ The system must have fewer conversion factors than total flows (degrees of freedom > 0)
156
+ to avoid over-constraining the problem. For n total flows, use at most n-1 conversion factors.
157
+
158
+ When using piecewise_conversion, the converter operates on one piece at a time,
159
+ with binary variables determining which piece is active.
28
160
 
29
161
  """
30
162
 
31
163
  def __init__(
32
164
  self,
33
165
  label: str,
34
- inputs: List[Flow],
35
- outputs: List[Flow],
36
- on_off_parameters: OnOffParameters = None,
37
- conversion_factors: List[Dict[str, TimestepData]] = None,
38
- piecewise_conversion: Optional[PiecewiseConversion] = None,
39
- meta_data: Optional[Dict] = None,
166
+ inputs: list[Flow],
167
+ outputs: list[Flow],
168
+ on_off_parameters: OnOffParameters | None = None,
169
+ conversion_factors: list[dict[str, TemporalDataUser]] | None = None,
170
+ piecewise_conversion: PiecewiseConversion | None = None,
171
+ meta_data: dict | None = None,
40
172
  ):
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
173
  super().__init__(label, inputs, outputs, on_off_parameters, meta_data=meta_data)
57
174
  self.conversion_factors = conversion_factors or []
58
175
  self.piecewise_conversion = piecewise_conversion
59
176
 
60
- def create_model(self, model: SystemModel) -> 'LinearConverterModel':
177
+ def create_model(self, model: FlowSystemModel) -> LinearConverterModel:
61
178
  self._plausibility_checks()
62
- self.model = LinearConverterModel(model, self)
63
- return self.model
179
+ self.submodel = LinearConverterModel(model, self)
180
+ return self.submodel
64
181
 
65
182
  def _plausibility_checks(self) -> None:
66
183
  super()._plausibility_checks()
@@ -87,28 +204,31 @@ class LinearConverter(Component):
87
204
  for flow in self.flows.values():
88
205
  if isinstance(flow.size, InvestParameters) and flow.size.fixed_size is None:
89
206
  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!'
207
+ f'Using a Flow with variable size (InvestParameters without fixed_size) '
208
+ f'and a piecewise_conversion in {self.label_full} is uncommon. Please verify intent '
209
+ f'({flow.label_full}).'
92
210
  )
93
211
 
94
- def transform_data(self, flow_system: 'FlowSystem'):
95
- super().transform_data(flow_system)
212
+ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:
213
+ prefix = '|'.join(filter(None, [name_prefix, self.label_full]))
214
+ super().transform_data(flow_system, prefix)
96
215
  if self.conversion_factors:
97
216
  self.conversion_factors = self._transform_conversion_factors(flow_system)
98
217
  if self.piecewise_conversion:
99
218
  self.piecewise_conversion.has_time_dim = True
100
- self.piecewise_conversion.transform_data(flow_system, f'{self.label_full}|PiecewiseConversion')
219
+ self.piecewise_conversion.transform_data(flow_system, f'{prefix}|PiecewiseConversion')
101
220
 
102
- def _transform_conversion_factors(self, flow_system: 'FlowSystem') -> List[Dict[str, TimeSeries]]:
103
- """macht alle Faktoren, die nicht TimeSeries sind, zu TimeSeries"""
221
+ def _transform_conversion_factors(self, flow_system: FlowSystem) -> list[dict[str, xr.DataArray]]:
222
+ """Converts all conversion factors to internal datatypes"""
104
223
  list_of_conversion_factors = []
105
224
  for idx, conversion_factor in enumerate(self.conversion_factors):
106
225
  transformed_dict = {}
107
226
  for flow, values in conversion_factor.items():
108
227
  # TODO: Might be better to use the label of the component instead of the flow
109
- transformed_dict[flow] = flow_system.create_time_series(
110
- f'{self.flows[flow].label_full}|conversion_factor{idx}', values
111
- )
228
+ ts = flow_system.fit_to_model_coords(f'{self.flows[flow].label_full}|conversion_factor{idx}', values)
229
+ if ts is None:
230
+ raise PlausibilityError(f'{self.label_full}: conversion factor for flow "{flow}" must not be None')
231
+ transformed_dict[flow] = ts
112
232
  list_of_conversion_factors.append(transformed_dict)
113
233
  return list_of_conversion_factors
114
234
 
@@ -120,7 +240,140 @@ class LinearConverter(Component):
120
240
  @register_class_for_io
121
241
  class Storage(Component):
122
242
  """
123
- Used to model the storage of energy or material.
243
+ A Storage models the temporary storage and release of energy or material.
244
+
245
+ Storages have one incoming and one outgoing Flow, each with configurable efficiency
246
+ factors. They maintain a charge state variable that represents the stored amount,
247
+ bounded by capacity limits and evolving over time based on charging, discharging,
248
+ and self-discharge losses.
249
+
250
+ The storage model handles complex temporal dynamics including initial conditions,
251
+ final state constraints, and time-varying parameters. It supports both fixed-size
252
+ and investment-optimized storage systems with comprehensive techno-economic modeling.
253
+
254
+ Mathematical Formulation:
255
+ See the complete mathematical model in the documentation:
256
+ [Storage](../user-guide/mathematical-notation/elements/Storage.md)
257
+
258
+ - Equation (1): Charge state bounds
259
+ - Equation (3): Storage balance (charge state evolution)
260
+
261
+ Variable Mapping:
262
+ - ``capacity_in_flow_hours`` → C (storage capacity)
263
+ - ``charge_state`` → c(t_i) (state of charge at time t_i)
264
+ - ``relative_loss_per_hour`` → ċ_rel,loss (self-discharge rate)
265
+ - ``eta_charge`` → η_in (charging efficiency)
266
+ - ``eta_discharge`` → η_out (discharging efficiency)
267
+
268
+ Args:
269
+ label: Element identifier used in the FlowSystem.
270
+ charging: Incoming flow for loading the storage.
271
+ discharging: Outgoing flow for unloading the storage.
272
+ capacity_in_flow_hours: Storage capacity in flow-hours (kWh, m³, kg).
273
+ Scalar for fixed size or InvestParameters for optimization.
274
+ relative_minimum_charge_state: Minimum charge state (0-1). Default: 0.
275
+ relative_maximum_charge_state: Maximum charge state (0-1). Default: 1.
276
+ initial_charge_state: Charge at start. Numeric or 'lastValueOfSim'. Default: 0.
277
+ minimal_final_charge_state: Minimum absolute charge required at end (optional).
278
+ maximal_final_charge_state: Maximum absolute charge allowed at end (optional).
279
+ relative_minimum_final_charge_state: Minimum relative charge at end.
280
+ Defaults to last value of relative_minimum_charge_state.
281
+ relative_maximum_final_charge_state: Maximum relative charge at end.
282
+ Defaults to last value of relative_maximum_charge_state.
283
+ eta_charge: Charging efficiency (0-1). Default: 1.
284
+ eta_discharge: Discharging efficiency (0-1). Default: 1.
285
+ relative_loss_per_hour: Self-discharge per hour (0-0.1). Default: 0.
286
+ prevent_simultaneous_charge_and_discharge: Prevent charging and discharging
287
+ simultaneously. Adds binary variables. Default: True.
288
+ meta_data: Additional information stored in results. Python native types only.
289
+
290
+ Examples:
291
+ Battery energy storage system:
292
+
293
+ ```python
294
+ battery = Storage(
295
+ label='lithium_battery',
296
+ charging=battery_charge_flow,
297
+ discharging=battery_discharge_flow,
298
+ capacity_in_flow_hours=100, # 100 kWh capacity
299
+ eta_charge=0.95, # 95% charging efficiency
300
+ eta_discharge=0.95, # 95% discharging efficiency
301
+ relative_loss_per_hour=0.001, # 0.1% loss per hour
302
+ relative_minimum_charge_state=0.1, # Never below 10% SOC
303
+ relative_maximum_charge_state=0.9, # Never above 90% SOC
304
+ )
305
+ ```
306
+
307
+ Thermal storage with cycling constraints:
308
+
309
+ ```python
310
+ thermal_storage = Storage(
311
+ label='hot_water_tank',
312
+ charging=heat_input,
313
+ discharging=heat_output,
314
+ capacity_in_flow_hours=500, # 500 kWh thermal capacity
315
+ initial_charge_state=250, # Start half full
316
+ # Impact of temperature on energy capacity
317
+ relative_maximum_charge_state=water_temperature_spread / rated_temeprature_spread,
318
+ eta_charge=0.90, # Heat exchanger losses
319
+ eta_discharge=0.85, # Distribution losses
320
+ relative_loss_per_hour=0.02, # 2% thermal loss per hour
321
+ prevent_simultaneous_charge_and_discharge=True,
322
+ )
323
+ ```
324
+
325
+ Pumped hydro storage with investment optimization:
326
+
327
+ ```python
328
+ pumped_hydro = Storage(
329
+ label='pumped_hydro',
330
+ charging=pump_flow,
331
+ discharging=turbine_flow,
332
+ capacity_in_flow_hours=InvestParameters(
333
+ minimum_size=1000, # Minimum economic scale
334
+ maximum_size=10000, # Site constraints
335
+ specific_effects={'cost': 150}, # €150/MWh capacity
336
+ fix_effects={'cost': 50_000_000}, # €50M fixed costs
337
+ ),
338
+ eta_charge=0.85, # Pumping efficiency
339
+ eta_discharge=0.90, # Turbine efficiency
340
+ initial_charge_state='lastValueOfSim', # Ensuring no deficit compared to start
341
+ relative_loss_per_hour=0.0001, # Minimal evaporation
342
+ )
343
+ ```
344
+
345
+ Material storage with inventory management:
346
+
347
+ ```python
348
+ fuel_storage = Storage(
349
+ label='natural_gas_storage',
350
+ charging=gas_injection,
351
+ discharging=gas_withdrawal,
352
+ capacity_in_flow_hours=10000, # 10,000 m³ storage volume
353
+ initial_charge_state=3000, # Start with 3,000 m³
354
+ minimal_final_charge_state=1000, # Strategic reserve
355
+ maximal_final_charge_state=9000, # Prevent overflow
356
+ eta_charge=0.98, # Compression losses
357
+ eta_discharge=0.95, # Pressure reduction losses
358
+ relative_loss_per_hour=0.0005, # 0.05% leakage per hour
359
+ prevent_simultaneous_charge_and_discharge=False, # Allow flow-through
360
+ )
361
+ ```
362
+
363
+ Note:
364
+ **Mathematical formulation**: See [Storage](../user-guide/mathematical-notation/elements/Storage.md)
365
+ for charge state evolution equations and balance constraints.
366
+
367
+ **Efficiency parameters** (eta_charge, eta_discharge) are dimensionless (0-1 range).
368
+ The relative_loss_per_hour represents exponential decay per hour.
369
+
370
+ **Binary variables**: When prevent_simultaneous_charge_and_discharge is True, binary
371
+ variables enforce mutual exclusivity, increasing solution time but preventing unrealistic
372
+ simultaneous charging and discharging.
373
+
374
+ **Units**: Flow rates and charge states are related by the concept of 'flow hours' (=flow_rate * time).
375
+ With flow rates in kW, the charge state is therefore (usually) kWh.
376
+ With flow rates in m3/h, the charge state is therefore in m3.
124
377
  """
125
378
 
126
379
  def __init__(
@@ -128,45 +381,21 @@ class Storage(Component):
128
381
  label: str,
129
382
  charging: Flow,
130
383
  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,
384
+ capacity_in_flow_hours: PeriodicDataUser | InvestParameters,
385
+ relative_minimum_charge_state: TemporalDataUser = 0,
386
+ relative_maximum_charge_state: TemporalDataUser = 1,
387
+ initial_charge_state: PeriodicDataUser | Literal['lastValueOfSim'] = 0,
388
+ minimal_final_charge_state: PeriodicDataUser | None = None,
389
+ maximal_final_charge_state: PeriodicDataUser | None = None,
390
+ relative_minimum_final_charge_state: PeriodicDataUser | None = None,
391
+ relative_maximum_final_charge_state: PeriodicDataUser | None = None,
392
+ eta_charge: TemporalDataUser = 1,
393
+ eta_discharge: TemporalDataUser = 1,
394
+ relative_loss_per_hour: TemporalDataUser = 0,
140
395
  prevent_simultaneous_charge_and_discharge: bool = True,
141
396
  balanced: bool = False,
142
- meta_data: Optional[Dict] = None,
397
+ meta_data: dict | None = None,
143
398
  ):
144
- """
145
- Storages have one incoming and one outgoing Flow each with an efficiency.
146
- Further, storages have a `size` and a `charge_state`.
147
- Similarly to the flow-rate of a Flow, the `size` combined with a relative upper and lower bound
148
- limits the `charge_state` of the storage.
149
-
150
- For mathematical details take a look at our online documentation
151
-
152
- Args:
153
- label: The label of the Element. Used to identify it in the FlowSystem
154
- charging: ingoing flow.
155
- discharging: outgoing flow.
156
- capacity_in_flow_hours: nominal capacity/size of the storage
157
- relative_minimum_charge_state: minimum relative charge state. The default is 0.
158
- relative_maximum_charge_state: maximum relative charge state. The default is 1.
159
- initial_charge_state: storage charge_state at the beginning. The default is 0.
160
- minimal_final_charge_state: minimal value of chargeState at the end of timeseries.
161
- maximal_final_charge_state: maximal value of chargeState at the end of timeseries.
162
- eta_charge: efficiency factor of charging/loading. The default is 1.
163
- eta_discharge: efficiency factor of uncharging/unloading. The default is 1.
164
- relative_loss_per_hour: loss per chargeState-Unit per hour. The default is 0.
165
- prevent_simultaneous_charge_and_discharge: If True, loading and unloading at the same time is not possible.
166
- 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
- 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
- """
170
399
  # TODO: fixed_relative_chargeState implementieren
171
400
  super().__init__(
172
401
  label,
@@ -179,56 +408,68 @@ class Storage(Component):
179
408
  self.charging = charging
180
409
  self.discharging = discharging
181
410
  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
411
+ self.relative_minimum_charge_state: TemporalDataUser = relative_minimum_charge_state
412
+ self.relative_maximum_charge_state: TemporalDataUser = relative_maximum_charge_state
413
+
414
+ self.relative_minimum_final_charge_state = relative_minimum_final_charge_state
415
+ self.relative_maximum_final_charge_state = relative_maximum_final_charge_state
184
416
 
185
417
  self.initial_charge_state = initial_charge_state
186
418
  self.minimal_final_charge_state = minimal_final_charge_state
187
419
  self.maximal_final_charge_state = maximal_final_charge_state
188
420
 
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
421
+ self.eta_charge: TemporalDataUser = eta_charge
422
+ self.eta_discharge: TemporalDataUser = eta_discharge
423
+ self.relative_loss_per_hour: TemporalDataUser = relative_loss_per_hour
192
424
  self.prevent_simultaneous_charge_and_discharge = prevent_simultaneous_charge_and_discharge
193
425
  self.balanced = balanced
194
426
 
195
- def create_model(self, model: SystemModel) -> 'StorageModel':
427
+ def create_model(self, model: FlowSystemModel) -> StorageModel:
196
428
  self._plausibility_checks()
197
- self.model = StorageModel(model, self)
198
- return self.model
199
-
200
- def transform_data(self, flow_system: 'FlowSystem') -> None:
201
- super().transform_data(flow_system)
202
- self.relative_minimum_charge_state = flow_system.create_time_series(
203
- f'{self.label_full}|relative_minimum_charge_state',
429
+ self.submodel = StorageModel(model, self)
430
+ return self.submodel
431
+
432
+ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:
433
+ prefix = '|'.join(filter(None, [name_prefix, self.label_full]))
434
+ super().transform_data(flow_system, prefix)
435
+ self.relative_minimum_charge_state = flow_system.fit_to_model_coords(
436
+ f'{prefix}|relative_minimum_charge_state',
204
437
  self.relative_minimum_charge_state,
205
- has_extra_timestep=True,
206
438
  )
207
- self.relative_maximum_charge_state = flow_system.create_time_series(
208
- f'{self.label_full}|relative_maximum_charge_state',
439
+ self.relative_maximum_charge_state = flow_system.fit_to_model_coords(
440
+ f'{prefix}|relative_maximum_charge_state',
209
441
  self.relative_maximum_charge_state,
210
- has_extra_timestep=True,
211
442
  )
212
- self.eta_charge = flow_system.create_time_series(f'{self.label_full}|eta_charge', self.eta_charge)
213
- self.eta_discharge = flow_system.create_time_series(f'{self.label_full}|eta_discharge', self.eta_discharge)
214
- self.relative_loss_per_hour = flow_system.create_time_series(
215
- f'{self.label_full}|relative_loss_per_hour', self.relative_loss_per_hour
443
+ self.eta_charge = flow_system.fit_to_model_coords(f'{prefix}|eta_charge', self.eta_charge)
444
+ self.eta_discharge = flow_system.fit_to_model_coords(f'{prefix}|eta_discharge', self.eta_discharge)
445
+ self.relative_loss_per_hour = flow_system.fit_to_model_coords(
446
+ f'{prefix}|relative_loss_per_hour', self.relative_loss_per_hour
216
447
  )
217
448
  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
449
+ self.initial_charge_state = flow_system.fit_to_model_coords(
450
+ f'{prefix}|initial_charge_state', self.initial_charge_state, dims=['period', 'scenario']
220
451
  )
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
452
+ self.minimal_final_charge_state = flow_system.fit_to_model_coords(
453
+ f'{prefix}|minimal_final_charge_state', self.minimal_final_charge_state, dims=['period', 'scenario']
223
454
  )
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
455
+ self.maximal_final_charge_state = flow_system.fit_to_model_coords(
456
+ f'{prefix}|maximal_final_charge_state', self.maximal_final_charge_state, dims=['period', 'scenario']
457
+ )
458
+ self.relative_minimum_final_charge_state = flow_system.fit_to_model_coords(
459
+ f'{prefix}|relative_minimum_final_charge_state',
460
+ self.relative_minimum_final_charge_state,
461
+ dims=['period', 'scenario'],
462
+ )
463
+ self.relative_maximum_final_charge_state = flow_system.fit_to_model_coords(
464
+ f'{prefix}|relative_maximum_final_charge_state',
465
+ self.relative_maximum_final_charge_state,
466
+ dims=['period', 'scenario'],
226
467
  )
227
468
  if isinstance(self.capacity_in_flow_hours, InvestParameters):
228
- self.capacity_in_flow_hours.transform_data(flow_system, f'{self.label_full}|InvestParameters')
469
+ self.capacity_in_flow_hours.transform_data(flow_system, f'{prefix}|InvestParameters')
229
470
  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
471
+ self.capacity_in_flow_hours = flow_system.fit_to_model_coords(
472
+ f'{prefix}|capacity_in_flow_hours', self.capacity_in_flow_hours, dims=['period', 'scenario']
232
473
  )
233
474
 
234
475
  def _plausibility_checks(self) -> None:
@@ -236,89 +477,183 @@ class Storage(Component):
236
477
  Check for infeasible or uncommon combinations of parameters
237
478
  """
238
479
  super()._plausibility_checks()
480
+
481
+ # Validate string values and set flag
482
+ initial_is_last = False
239
483
  if isinstance(self.initial_charge_state, str):
240
- if self.initial_charge_state != 'lastValueOfSim':
484
+ if self.initial_charge_state == 'lastValueOfSim':
485
+ initial_is_last = True
486
+ else:
241
487
  raise PlausibilityError(f'initial_charge_state has undefined value: {self.initial_charge_state}')
242
- return
488
+
489
+ # Use new InvestParameters methods to get capacity bounds
243
490
  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
247
- else:
248
- maximum_capacity = self.capacity_in_flow_hours.fixed_size
249
- minimum_capacity = self.capacity_in_flow_hours.fixed_size
491
+ minimum_capacity = self.capacity_in_flow_hours.minimum_or_fixed_size
492
+ maximum_capacity = self.capacity_in_flow_hours.maximum_or_fixed_size
250
493
  else:
251
494
  maximum_capacity = self.capacity_in_flow_hours
252
495
  minimum_capacity = self.capacity_in_flow_hours
253
496
 
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
497
+ # Initial capacity should not constraint investment decision
498
+ minimum_initial_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=0)
499
+ maximum_initial_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=0)
259
500
 
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
- )
501
+ # Only perform numeric comparisons if not using 'lastValueOfSim'
502
+ if not initial_is_last:
503
+ if (self.initial_charge_state > maximum_initial_capacity).any():
504
+ raise PlausibilityError(
505
+ f'{self.label_full}: {self.initial_charge_state=} '
506
+ f'is constraining the investment decision. Chosse a value above {maximum_initial_capacity}'
507
+ )
508
+ if (self.initial_charge_state < minimum_initial_capacity).any():
509
+ raise PlausibilityError(
510
+ f'{self.label_full}: {self.initial_charge_state=} '
511
+ f'is constraining the investment decision. Chosse a value below {minimum_initial_capacity}'
512
+ )
270
513
 
271
514
  if self.balanced:
272
- if not isinstance(self.charging.size, InvestParameters) or not isinstance(self.discharging.size, InvestParameters):
515
+ if not isinstance(self.charging.size, InvestParameters) or not isinstance(
516
+ self.discharging.size, InvestParameters
517
+ ):
273
518
  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):
519
+ f'Balancing charging and discharging Flows in {self.label_full} is only possible with Investments.'
520
+ )
521
+
522
+ if (self.charging.size.minimum_size > self.discharging.size.maximum_size).any() or (
523
+ self.charging.size.maximum_size < self.discharging.size.minimum_size
524
+ ).any():
278
525
  raise PlausibilityError(
279
526
  f'Balancing charging and discharging Flows in {self.label_full} need compatible minimum and maximum sizes.'
280
527
  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=}.')
528
+ f'{self.discharging.size.minimum_size=}, {self.discharging.size.maximum_size=}.'
529
+ )
282
530
 
283
531
 
284
532
  @register_class_for_io
285
533
  class Transmission(Component):
286
- # TODO: automatic on-Value in Flows if loss_abs
287
- # TODO: loss_abs must be: investment_size * loss_abs_rel!!!
288
- # TODO: investmentsize only on 1 flow
289
- # TODO: automatic investArgs for both in-flows (or alternatively both out-flows!)
290
- # TODO: optional: capacities should be recognised for losses
534
+ """
535
+ Models transmission infrastructure that transports flows between two locations with losses.
536
+
537
+ Transmission components represent physical infrastructure like pipes, cables,
538
+ transmission lines, or conveyor systems that transport energy or materials between
539
+ two points. They can model both unidirectional and bidirectional flow with
540
+ configurable loss mechanisms and operational constraints.
541
+
542
+ The component supports complex transmission scenarios including relative losses
543
+ (proportional to flow), absolute losses (fixed when active), and bidirectional
544
+ operation with flow direction constraints.
545
+
546
+ Args:
547
+ label: The label of the Element. Used to identify it in the FlowSystem.
548
+ in1: The primary inflow (side A). Pass InvestParameters here for capacity optimization.
549
+ out1: The primary outflow (side B).
550
+ in2: Optional secondary inflow (side B) for bidirectional operation.
551
+ If in1 has InvestParameters, in2 will automatically have matching capacity.
552
+ out2: Optional secondary outflow (side A) for bidirectional operation.
553
+ relative_losses: Proportional losses as fraction of throughput (e.g., 0.02 for 2% loss).
554
+ Applied as: output = input × (1 - relative_losses)
555
+ absolute_losses: Fixed losses that occur when transmission is active.
556
+ Automatically creates binary variables for on/off states.
557
+ on_off_parameters: Parameters defining binary operation constraints and costs.
558
+ prevent_simultaneous_flows_in_both_directions: If True, prevents simultaneous
559
+ flow in both directions. Increases binary variables but reflects physical
560
+ reality for most transmission systems. Default is True.
561
+ balanced: Whether to equate the size of the in1 and in2 Flow. Needs InvestParameters in both Flows.
562
+ meta_data: Used to store additional information. Not used internally but saved
563
+ in results. Only use Python native types.
564
+
565
+ Examples:
566
+ Simple electrical transmission line:
567
+
568
+ ```python
569
+ power_line = Transmission(
570
+ label='110kv_line',
571
+ in1=substation_a_out,
572
+ out1=substation_b_in,
573
+ relative_losses=0.03, # 3% line losses
574
+ )
575
+ ```
576
+
577
+ Bidirectional natural gas pipeline:
578
+
579
+ ```python
580
+ gas_pipeline = Transmission(
581
+ label='interstate_pipeline',
582
+ in1=compressor_station_a,
583
+ out1=distribution_hub_b,
584
+ in2=compressor_station_b,
585
+ out2=distribution_hub_a,
586
+ relative_losses=0.005, # 0.5% friction losses
587
+ absolute_losses=50, # 50 kW compressor power when active
588
+ prevent_simultaneous_flows_in_both_directions=True,
589
+ )
590
+ ```
591
+
592
+ District heating network with investment optimization:
593
+
594
+ ```python
595
+ heating_network = Transmission(
596
+ label='dh_main_line',
597
+ in1=Flow(
598
+ label='heat_supply',
599
+ bus=central_plant_bus,
600
+ size=InvestParameters(
601
+ minimum_size=1000, # Minimum 1 MW capacity
602
+ maximum_size=10000, # Maximum 10 MW capacity
603
+ specific_effects={'cost': 200}, # €200/kW capacity
604
+ fix_effects={'cost': 500000}, # €500k fixed installation
605
+ ),
606
+ ),
607
+ out1=district_heat_demand,
608
+ relative_losses=0.15, # 15% thermal losses in distribution
609
+ )
610
+ ```
611
+
612
+ Material conveyor with on/off operation:
613
+
614
+ ```python
615
+ conveyor_belt = Transmission(
616
+ label='material_transport',
617
+ in1=loading_station,
618
+ out1=unloading_station,
619
+ absolute_losses=25, # 25 kW motor power when running
620
+ on_off_parameters=OnOffParameters(
621
+ effects_per_switch_on={'maintenance': 0.1},
622
+ consecutive_on_hours_min=2, # Minimum 2-hour operation
623
+ switch_on_total_max=10, # Maximum 10 starts per day
624
+ ),
625
+ )
626
+ ```
627
+
628
+ Note:
629
+ The transmission equation balances flows with losses:
630
+ output_flow = input_flow × (1 - relative_losses) - absolute_losses
631
+
632
+ For bidirectional transmission, each direction has independent loss calculations.
633
+
634
+ When using InvestParameters on in1, the capacity automatically applies to in2
635
+ to maintain consistent bidirectional capacity without additional investment variables.
636
+
637
+ Absolute losses force the creation of binary on/off variables, which increases
638
+ computational complexity but enables realistic modeling of equipment with
639
+ standby power consumption.
640
+
641
+ """
291
642
 
292
643
  def __init__(
293
644
  self,
294
645
  label: str,
295
646
  in1: Flow,
296
647
  out1: Flow,
297
- in2: Optional[Flow] = None,
298
- out2: Optional[Flow] = None,
299
- relative_losses: Optional[TimestepData] = None,
300
- absolute_losses: Optional[TimestepData] = None,
648
+ in2: Flow | None = None,
649
+ out2: Flow | None = None,
650
+ relative_losses: TemporalDataUser | None = None,
651
+ absolute_losses: TemporalDataUser | None = None,
301
652
  on_off_parameters: OnOffParameters = None,
302
653
  prevent_simultaneous_flows_in_both_directions: bool = True,
303
- meta_data: Optional[Dict] = None,
654
+ balanced: bool = False,
655
+ meta_data: dict | None = None,
304
656
  ):
305
- """
306
- Initializes a Transmission component (Pipe, cable, ...) that models the flows between two sides
307
- with potential losses.
308
-
309
- Args:
310
- label: The label of the Element. Used to identify it in the FlowSystem
311
- in1: The inflow at side A. Pass InvestmentParameters here.
312
- out1: The outflow at side B.
313
- in2: The optional inflow at side B.
314
- If in1 got InvestParameters, the size of this Flow will be equal to in1 (with no extra effects!)
315
- out2: The optional outflow at side A.
316
- relative_losses: The relative loss between inflow and outflow, e.g., 0.02 for 2% loss.
317
- absolute_losses: The absolute loss, occur only when the Flow is on. Induces the creation of the ON-Variable
318
- on_off_parameters: Parameters defining the on/off behavior of the component.
319
- prevent_simultaneous_flows_in_both_directions: If True, inflow and outflow are not allowed to be both non-zero at same timestep.
320
- meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
321
- """
322
657
  super().__init__(
323
658
  label,
324
659
  inputs=[flow for flow in (in1, in2) if flow is not None],
@@ -336,6 +671,7 @@ class Transmission(Component):
336
671
 
337
672
  self.relative_losses = relative_losses
338
673
  self.absolute_losses = absolute_losses
674
+ self.balanced = balanced
339
675
 
340
676
  def _plausibility_checks(self):
341
677
  super()._plausibility_checks()
@@ -348,51 +684,47 @@ class Transmission(Component):
348
684
  assert self.out2.bus == self.in1.bus, (
349
685
  f'Input 1 and Output 2 do not start/end at the same Bus: {self.in1.bus=}, {self.out2.bus=}'
350
686
  )
351
- # Check Investments
352
- for flow in [self.out1, self.in2, self.out2]:
353
- if flow is not None and isinstance(flow.size, InvestParameters):
687
+
688
+ if self.balanced:
689
+ if self.in2 is None:
690
+ raise ValueError('Balanced Transmission needs InvestParameters in both in-Flows')
691
+ if not isinstance(self.in1.size, InvestParameters) or not isinstance(self.in2.size, InvestParameters):
692
+ raise ValueError('Balanced Transmission needs InvestParameters in both in-Flows')
693
+ if (self.in1.size.minimum_or_fixed_size > self.in2.size.maximum_or_fixed_size).any() or (
694
+ self.in1.size.maximum_or_fixed_size < self.in2.size.minimum_or_fixed_size
695
+ ).any():
354
696
  raise ValueError(
355
- 'Transmission currently does not support separate InvestParameters for Flows. '
356
- 'Please use Flow in1. The size of in2 is equal to in1. THis is handled internally'
697
+ f'Balanced Transmission needs compatible minimum and maximum sizes.'
698
+ f'Got: {self.in1.size.minimum_size=}, {self.in1.size.maximum_size=}, {self.in1.size.fixed_size=} and '
699
+ f'{self.in2.size.minimum_size=}, {self.in2.size.maximum_size=}, {self.in2.size.fixed_size=}.'
357
700
  )
358
701
 
359
- def create_model(self, model) -> 'TransmissionModel':
702
+ def create_model(self, model) -> TransmissionModel:
360
703
  self._plausibility_checks()
361
- self.model = TransmissionModel(model, self)
362
- return self.model
704
+ self.submodel = TransmissionModel(model, self)
705
+ return self.submodel
363
706
 
364
- def transform_data(self, flow_system: 'FlowSystem') -> None:
365
- super().transform_data(flow_system)
366
- self.relative_losses = flow_system.create_time_series(
367
- f'{self.label_full}|relative_losses', self.relative_losses
368
- )
369
- self.absolute_losses = flow_system.create_time_series(
370
- f'{self.label_full}|absolute_losses', self.absolute_losses
371
- )
707
+ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:
708
+ prefix = '|'.join(filter(None, [name_prefix, self.label_full]))
709
+ super().transform_data(flow_system, prefix)
710
+ self.relative_losses = flow_system.fit_to_model_coords(f'{prefix}|relative_losses', self.relative_losses)
711
+ self.absolute_losses = flow_system.fit_to_model_coords(f'{prefix}|absolute_losses', self.absolute_losses)
372
712
 
373
713
 
374
714
  class TransmissionModel(ComponentModel):
375
- def __init__(self, model: SystemModel, element: Transmission):
376
- super().__init__(model, element)
377
- self.element: Transmission = element
378
- self.on_off: Optional[OnOffModel] = None
715
+ element: Transmission
379
716
 
380
- def do_modeling(self):
381
- """Initiates all FlowModels"""
382
- # 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):
384
- for flow in self.element.inputs + self.element.outputs:
717
+ def __init__(self, model: FlowSystemModel, element: Transmission):
718
+ if (element.absolute_losses is not None) and np.any(element.absolute_losses != 0):
719
+ for flow in element.inputs + element.outputs:
385
720
  if flow.on_off_parameters is None:
386
721
  flow.on_off_parameters = OnOffParameters()
387
722
 
388
- # Make sure either None or both in Flows have InvestParameters
389
- if self.element.in2 is not None:
390
- if isinstance(self.element.in1.size, InvestParameters) and not isinstance(
391
- self.element.in2.size, InvestParameters
392
- ):
393
- self.element.in2.size = InvestParameters(maximum_size=self.element.in1.size.maximum_size)
723
+ super().__init__(model, element)
394
724
 
395
- super().do_modeling()
725
+ def _do_modeling(self):
726
+ """Initiates all FlowModels"""
727
+ super()._do_modeling()
396
728
 
397
729
  # first direction
398
730
  self.create_transmission_equation('dir1', self.element.in1, self.element.out1)
@@ -402,43 +734,37 @@ class TransmissionModel(ComponentModel):
402
734
  self.create_transmission_equation('dir2', self.element.in2, self.element.out2)
403
735
 
404
736
  # equate size of both directions
405
- if isinstance(self.element.in1.size, InvestParameters) and self.element.in2 is not None:
737
+ if self.element.balanced:
406
738
  # eq: in1.size = in2.size
407
- self.add(
408
- self._model.add_constraints(
409
- self.element.in1.model._investment.size == self.element.in2.model._investment.size,
410
- name=f'{self.label_full}|same_size',
411
- ),
412
- 'same_size',
739
+ self.add_constraints(
740
+ self.element.in1.submodel._investment.size == self.element.in2.submodel._investment.size,
741
+ short_name='same_size',
413
742
  )
414
743
 
415
744
  def create_transmission_equation(self, name: str, in_flow: Flow, out_flow: Flow) -> linopy.Constraint:
416
745
  """Creates an Equation for the Transmission efficiency and adds it to the model"""
417
746
  # eq: out(t) + on(t)*loss_abs(t) = in(t)*(1 - loss_rel(t))
418
- con_transmission = self.add(
419
- self._model.add_constraints(
420
- out_flow.model.flow_rate == -in_flow.model.flow_rate * (self.element.relative_losses.selected_data - 1),
421
- name=f'{self.label_full}|{name}',
422
- ),
423
- name,
747
+ rel_losses = 0 if self.element.relative_losses is None else self.element.relative_losses
748
+ con_transmission = self.add_constraints(
749
+ out_flow.submodel.flow_rate == in_flow.submodel.flow_rate * (1 - rel_losses),
750
+ short_name=name,
424
751
  )
425
752
 
426
753
  if self.element.absolute_losses is not None:
427
- con_transmission.lhs += in_flow.model.on_off.on * self.element.absolute_losses.selected_data
754
+ con_transmission.lhs += in_flow.submodel.on_off.on * self.element.absolute_losses
428
755
 
429
756
  return con_transmission
430
757
 
431
758
 
432
759
  class LinearConverterModel(ComponentModel):
433
- def __init__(self, model: SystemModel, element: LinearConverter):
434
- super().__init__(model, element)
435
- self.element: LinearConverter = element
436
- self.on_off: Optional[OnOffModel] = None
437
- self.piecewise_conversion: Optional[PiecewiseConversion] = None
760
+ element: LinearConverter
438
761
 
439
- def do_modeling(self):
440
- super().do_modeling()
762
+ def __init__(self, model: FlowSystemModel, element: LinearConverter):
763
+ self.piecewise_conversion: PiecewiseConversion | None = None
764
+ super().__init__(model, element)
441
765
 
766
+ def _do_modeling(self):
767
+ super()._do_modeling()
442
768
  # conversion_factors:
443
769
  if self.element.conversion_factors:
444
770
  all_input_flows = set(self.element.inputs)
@@ -447,159 +773,135 @@ class LinearConverterModel(ComponentModel):
447
773
  # für alle linearen Gleichungen:
448
774
  for i, conv_factors in enumerate(self.element.conversion_factors):
449
775
  used_flows = set([self.element.flows[flow_label] for flow_label in conv_factors])
450
- used_inputs: Set = all_input_flows & used_flows
451
- used_outputs: Set = all_output_flows & used_flows
452
-
453
- self.add(
454
- 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
- ),
459
- name=f'{self.label_full}|conversion_{i}',
460
- )
776
+ used_inputs: set[Flow] = all_input_flows & used_flows
777
+ used_outputs: set[Flow] = all_output_flows & used_flows
778
+
779
+ self.add_constraints(
780
+ sum([flow.submodel.flow_rate * conv_factors[flow.label] for flow in used_inputs])
781
+ == sum([flow.submodel.flow_rate * conv_factors[flow.label] for flow in used_outputs]),
782
+ short_name=f'conversion_{i}',
461
783
  )
462
784
 
463
785
  else:
464
786
  # TODO: Improve Inclusion of OnOffParameters. Instead of creating a Binary in every flow, the binary could only be part of the Piece itself
465
787
  piecewise_conversion = {
466
- self.element.flows[flow].model.flow_rate.name: piecewise
788
+ self.element.flows[flow].submodel.flow_rate.name: piecewise
467
789
  for flow, piecewise in self.element.piecewise_conversion.items()
468
790
  }
469
791
 
470
- self.piecewise_conversion = self.add(
792
+ self.piecewise_conversion = self.add_submodels(
471
793
  PiecewiseModel(
472
794
  model=self._model,
473
795
  label_of_element=self.label_of_element,
796
+ label_of_model=f'{self.label_of_element}',
474
797
  piecewise_variables=piecewise_conversion,
475
798
  zero_point=self.on_off.on if self.on_off is not None else False,
476
- as_time_series=True,
477
- )
799
+ dims=('time', 'period', 'scenario'),
800
+ ),
801
+ short_name='PiecewiseConversion',
478
802
  )
479
- self.piecewise_conversion.do_modeling()
480
803
 
481
804
 
482
805
  class StorageModel(ComponentModel):
483
- """Model of Storage"""
806
+ """Submodel of Storage"""
484
807
 
485
- def __init__(self, model: SystemModel, element: Storage):
808
+ element: Storage
809
+
810
+ def __init__(self, model: FlowSystemModel, element: Storage):
486
811
  super().__init__(model, element)
487
- self.element: Storage = element
488
- self.charge_state: Optional[linopy.Variable] = None
489
- self.netto_discharge: Optional[linopy.Variable] = None
490
- self._investment: Optional[InvestmentModel] = None
491
-
492
- def do_modeling(self):
493
- super().do_modeling()
494
-
495
- lb, ub = self.absolute_charge_state_bounds
496
- self.charge_state = self.add(
497
- 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',
502
- ),
503
- 'charge_state',
504
- )
505
- self.netto_discharge = self.add(
506
- self._model.add_variables(coords=self._model.get_coords(), name=f'{self.label_full}|netto_discharge'),
507
- 'netto_discharge',
812
+
813
+ def _do_modeling(self):
814
+ super()._do_modeling()
815
+
816
+ lb, ub = self._absolute_charge_state_bounds
817
+ self.add_variables(
818
+ lower=lb,
819
+ upper=ub,
820
+ coords=self._model.get_coords(extra_timestep=True),
821
+ short_name='charge_state',
508
822
  )
823
+
824
+ self.add_variables(coords=self._model.get_coords(), short_name='netto_discharge')
825
+
509
826
  # netto_discharge:
510
827
  # eq: nettoFlow(t) - discharging(t) + charging(t) = 0
511
- self.add(
512
- self._model.add_constraints(
513
- self.netto_discharge
514
- == self.element.discharging.model.flow_rate - self.element.charging.model.flow_rate,
515
- name=f'{self.label_full}|netto_discharge',
516
- ),
517
- 'netto_discharge',
828
+ self.add_constraints(
829
+ self.netto_discharge
830
+ == self.element.discharging.submodel.flow_rate - self.element.charging.submodel.flow_rate,
831
+ short_name='netto_discharge',
518
832
  )
519
833
 
520
834
  charge_state = self.charge_state
521
- rel_loss = self.element.relative_loss_per_hour.selected_data
835
+ rel_loss = self.element.relative_loss_per_hour
522
836
  hours_per_step = self._model.hours_per_step
523
- charge_rate = self.element.charging.model.flow_rate
524
- 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
527
-
528
- self.add(
529
- self._model.add_constraints(
530
- charge_state.isel(time=slice(1, None))
531
- == charge_state.isel(time=slice(None, -1)) * (1 - rel_loss * hours_per_step)
532
- + charge_rate * eff_charge * hours_per_step
533
- - discharge_rate * eff_discharge * hours_per_step,
534
- name=f'{self.label_full}|charge_state',
535
- ),
536
- 'charge_state',
837
+ charge_rate = self.element.charging.submodel.flow_rate
838
+ discharge_rate = self.element.discharging.submodel.flow_rate
839
+ eff_charge = self.element.eta_charge
840
+ eff_discharge = self.element.eta_discharge
841
+
842
+ self.add_constraints(
843
+ charge_state.isel(time=slice(1, None))
844
+ == charge_state.isel(time=slice(None, -1)) * ((1 - rel_loss) ** hours_per_step)
845
+ + charge_rate * eff_charge * hours_per_step
846
+ - discharge_rate * hours_per_step / eff_discharge,
847
+ short_name='charge_state',
537
848
  )
538
849
 
539
850
  if isinstance(self.element.capacity_in_flow_hours, InvestParameters):
540
- self._investment = InvestmentModel(
541
- model=self._model,
542
- label_of_element=self.label_of_element,
543
- parameters=self.element.capacity_in_flow_hours,
544
- defining_variable=self.charge_state,
545
- relative_bounds_of_defining_variable=self.relative_charge_state_bounds,
851
+ self.add_submodels(
852
+ InvestmentModel(
853
+ model=self._model,
854
+ label_of_element=self.label_of_element,
855
+ label_of_model=self.label_of_element,
856
+ parameters=self.element.capacity_in_flow_hours,
857
+ ),
858
+ short_name='investment',
859
+ )
860
+
861
+ BoundingPatterns.scaled_bounds(
862
+ self,
863
+ variable=self.charge_state,
864
+ scaling_variable=self.investment.size,
865
+ relative_bounds=self._relative_charge_state_bounds,
546
866
  )
547
- self.sub_models.append(self._investment)
548
- self._investment.do_modeling()
549
867
 
550
868
  # Initial charge state
551
869
  self._initial_and_final_charge_state()
552
870
 
553
871
  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'
872
+ self.add_constraints(
873
+ self.element.charging.submodel._investment.size * 1
874
+ == self.element.discharging.submodel._investment.size * 1,
875
+ short_name='balanced_sizes',
560
876
  )
561
877
 
562
878
  def _initial_and_final_charge_state(self):
563
879
  if self.element.initial_charge_state is not None:
564
- name_short = 'initial_charge_state'
565
- name = f'{self.label_full}|{name_short}'
566
-
567
880
  if isinstance(self.element.initial_charge_state, str):
568
- self.add(
569
- self._model.add_constraints(
570
- self.charge_state.isel(time=0) == self.charge_state.isel(time=-1), name=name
571
- ),
572
- name_short,
881
+ self.add_constraints(
882
+ self.charge_state.isel(time=0) == self.charge_state.isel(time=-1), short_name='initial_charge_state'
573
883
  )
574
884
  else:
575
- self.add(
576
- self._model.add_constraints(
577
- self.charge_state.isel(time=0) == self.element.initial_charge_state, name=name
578
- ),
579
- name_short,
885
+ self.add_constraints(
886
+ self.charge_state.isel(time=0) == self.element.initial_charge_state,
887
+ short_name='initial_charge_state',
580
888
  )
581
889
 
582
890
  if self.element.maximal_final_charge_state is not None:
583
- self.add(
584
- self._model.add_constraints(
585
- self.charge_state.isel(time=-1) <= self.element.maximal_final_charge_state,
586
- name=f'{self.label_full}|final_charge_max',
587
- ),
588
- 'final_charge_max',
891
+ self.add_constraints(
892
+ self.charge_state.isel(time=-1) <= self.element.maximal_final_charge_state,
893
+ short_name='final_charge_max',
589
894
  )
590
895
 
591
896
  if self.element.minimal_final_charge_state is not None:
592
- self.add(
593
- self._model.add_constraints(
594
- self.charge_state.isel(time=-1) >= self.element.minimal_final_charge_state,
595
- name=f'{self.label_full}|final_charge_min',
596
- ),
597
- 'final_charge_min',
897
+ self.add_constraints(
898
+ self.charge_state.isel(time=-1) >= self.element.minimal_final_charge_state,
899
+ short_name='final_charge_min',
598
900
  )
599
901
 
600
902
  @property
601
- def absolute_charge_state_bounds(self) -> Tuple[TimestepData, TimestepData]:
602
- relative_lower_bound, relative_upper_bound = self.relative_charge_state_bounds
903
+ def _absolute_charge_state_bounds(self) -> tuple[TemporalData, TemporalData]:
904
+ relative_lower_bound, relative_upper_bound = self._relative_charge_state_bounds
603
905
  if not isinstance(self.element.capacity_in_flow_hours, InvestParameters):
604
906
  return (
605
907
  relative_lower_bound * self.element.capacity_in_flow_hours,
@@ -612,68 +914,429 @@ class StorageModel(ComponentModel):
612
914
  )
613
915
 
614
916
  @property
615
- def relative_charge_state_bounds(self) -> Tuple[TimestepData, TimestepData]:
616
- return (
617
- self.element.relative_minimum_charge_state,
618
- self.element.relative_maximum_charge_state,
619
- )
917
+ def _relative_charge_state_bounds(self) -> tuple[xr.DataArray, xr.DataArray]:
918
+ """
919
+ Get relative charge state bounds with final timestep values.
920
+
921
+ Returns:
922
+ Tuple of (minimum_bounds, maximum_bounds) DataArrays extending to final timestep
923
+ """
924
+ final_coords = {'time': [self._model.flow_system.timesteps_extra[-1]]}
925
+
926
+ # Get final minimum charge state
927
+ if self.element.relative_minimum_final_charge_state is None:
928
+ min_final = self.element.relative_minimum_charge_state.isel(time=-1, drop=True)
929
+ else:
930
+ min_final = self.element.relative_minimum_final_charge_state
931
+ min_final = min_final.expand_dims('time').assign_coords(time=final_coords['time'])
932
+
933
+ # Get final maximum charge state
934
+ if self.element.relative_maximum_final_charge_state is None:
935
+ max_final = self.element.relative_maximum_charge_state.isel(time=-1, drop=True)
936
+ else:
937
+ max_final = self.element.relative_maximum_final_charge_state
938
+ max_final = max_final.expand_dims('time').assign_coords(time=final_coords['time'])
939
+ # Concatenate with original bounds
940
+ min_bounds = xr.concat([self.element.relative_minimum_charge_state, min_final], dim='time')
941
+ max_bounds = xr.concat([self.element.relative_maximum_charge_state, max_final], dim='time')
942
+
943
+ return min_bounds, max_bounds
944
+
945
+ @property
946
+ def _investment(self) -> InvestmentModel | None:
947
+ """Deprecated alias for investment"""
948
+ return self.investment
949
+
950
+ @property
951
+ def investment(self) -> InvestmentModel | None:
952
+ """OnOff feature"""
953
+ if 'investment' not in self.submodels:
954
+ return None
955
+ return self.submodels['investment']
956
+
957
+ @property
958
+ def charge_state(self) -> linopy.Variable:
959
+ """Charge state variable"""
960
+ return self['charge_state']
961
+
962
+ @property
963
+ def netto_discharge(self) -> linopy.Variable:
964
+ """Netto discharge variable"""
965
+ return self['netto_discharge']
620
966
 
621
967
 
622
968
  @register_class_for_io
623
969
  class SourceAndSink(Component):
624
970
  """
625
- class for source (output-flow) and sink (input-flow) in one commponent
971
+ A SourceAndSink combines both supply and demand capabilities in a single component.
972
+
973
+ SourceAndSink components can both consume AND provide energy or material flows
974
+ from and to the system, making them ideal for modeling markets, (simple) storage facilities,
975
+ or bidirectional grid connections where buying and selling occur at the same location.
976
+
977
+ Args:
978
+ label: The label of the Element. Used to identify it in the FlowSystem.
979
+ inputs: Input-flows into the SourceAndSink representing consumption/demand side.
980
+ outputs: Output-flows from the SourceAndSink representing supply/generation side.
981
+ prevent_simultaneous_flow_rates: If True, prevents simultaneous input and output
982
+ flows. This enforces that the component operates either as a source OR sink
983
+ at any given time, but not both simultaneously. Default is True.
984
+ meta_data: Used to store additional information about the Element. Not used
985
+ internally but saved in results. Only use Python native types.
986
+
987
+ Examples:
988
+ Electricity market connection (buy/sell to grid):
989
+
990
+ ```python
991
+ electricity_market = SourceAndSink(
992
+ label='grid_connection',
993
+ inputs=[electricity_purchase], # Buy from grid
994
+ outputs=[electricity_sale], # Sell to grid
995
+ prevent_simultaneous_flow_rates=True, # Can't buy and sell simultaneously
996
+ )
997
+ ```
998
+
999
+ Natural gas storage facility:
1000
+
1001
+ ```python
1002
+ gas_storage_facility = SourceAndSink(
1003
+ label='underground_gas_storage',
1004
+ inputs=[gas_injection_flow], # Inject gas into storage
1005
+ outputs=[gas_withdrawal_flow], # Withdraw gas from storage
1006
+ prevent_simultaneous_flow_rates=True, # Injection or withdrawal, not both
1007
+ )
1008
+ ```
1009
+
1010
+ District heating network connection:
1011
+
1012
+ ```python
1013
+ dh_connection = SourceAndSink(
1014
+ label='district_heating_tie',
1015
+ inputs=[heat_purchase_flow], # Purchase heat from network
1016
+ outputs=[heat_sale_flow], # Sell excess heat to network
1017
+ prevent_simultaneous_flow_rates=False, # May allow simultaneous flows
1018
+ )
1019
+ ```
1020
+
1021
+ Industrial waste heat exchange:
1022
+
1023
+ ```python
1024
+ waste_heat_exchange = SourceAndSink(
1025
+ label='industrial_heat_hub',
1026
+ inputs=[
1027
+ waste_heat_input_a, # Receive waste heat from process A
1028
+ waste_heat_input_b, # Receive waste heat from process B
1029
+ ],
1030
+ outputs=[
1031
+ useful_heat_supply_c, # Supply heat to process C
1032
+ useful_heat_supply_d, # Supply heat to process D
1033
+ ],
1034
+ prevent_simultaneous_flow_rates=False, # Multiple simultaneous flows allowed
1035
+ )
1036
+ ```
1037
+
1038
+ Note:
1039
+ When prevent_simultaneous_flow_rates is True, binary variables are created to
1040
+ ensure mutually exclusive operation between input and output flows, which
1041
+ increases computational complexity but reflects realistic market or storage
1042
+ operation constraints.
1043
+
1044
+ SourceAndSink is particularly useful for modeling:
1045
+ - Energy markets with bidirectional trading
1046
+ - Storage facilities with injection/withdrawal operations
1047
+ - Grid tie points with import/export capabilities
1048
+ - Waste exchange networks with multiple participants
1049
+
1050
+ Deprecated:
1051
+ The deprecated `sink` and `source` kwargs are accepted for compatibility but will be removed in future releases.
626
1052
  """
627
1053
 
628
1054
  def __init__(
629
1055
  self,
630
1056
  label: str,
631
- source: Flow,
632
- sink: Flow,
633
- prevent_simultaneous_sink_and_source: bool = True,
634
- meta_data: Optional[Dict] = None,
1057
+ inputs: list[Flow] | None = None,
1058
+ outputs: list[Flow] | None = None,
1059
+ prevent_simultaneous_flow_rates: bool = True,
1060
+ meta_data: dict | None = None,
1061
+ **kwargs,
635
1062
  ):
636
- """
637
- Args:
638
- 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.
642
- 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
- """
1063
+ # Handle deprecated parameters using centralized helper
1064
+ outputs = self._handle_deprecated_kwarg(kwargs, 'source', 'outputs', outputs, transform=lambda x: [x])
1065
+ inputs = self._handle_deprecated_kwarg(kwargs, 'sink', 'inputs', inputs, transform=lambda x: [x])
1066
+ prevent_simultaneous_flow_rates = self._handle_deprecated_kwarg(
1067
+ kwargs,
1068
+ 'prevent_simultaneous_sink_and_source',
1069
+ 'prevent_simultaneous_flow_rates',
1070
+ prevent_simultaneous_flow_rates,
1071
+ check_conflict=False,
1072
+ )
1073
+
1074
+ # Validate any remaining unexpected kwargs
1075
+ self._validate_kwargs(kwargs)
1076
+
644
1077
  super().__init__(
645
1078
  label,
646
- inputs=[sink],
647
- outputs=[source],
648
- prevent_simultaneous_flows=[sink, source] if prevent_simultaneous_sink_and_source is True else None,
1079
+ inputs=inputs,
1080
+ outputs=outputs,
1081
+ prevent_simultaneous_flows=(inputs or []) + (outputs or []) if prevent_simultaneous_flow_rates else None,
649
1082
  meta_data=meta_data,
650
1083
  )
651
- self.source = source
652
- self.sink = sink
653
- self.prevent_simultaneous_sink_and_source = prevent_simultaneous_sink_and_source
1084
+ self.prevent_simultaneous_flow_rates = prevent_simultaneous_flow_rates
1085
+
1086
+ @property
1087
+ def source(self) -> Flow:
1088
+ warnings.warn(
1089
+ 'The source property is deprecated. Use the outputs property instead.',
1090
+ DeprecationWarning,
1091
+ stacklevel=2,
1092
+ )
1093
+ return self.outputs[0]
1094
+
1095
+ @property
1096
+ def sink(self) -> Flow:
1097
+ warnings.warn(
1098
+ 'The sink property is deprecated. Use the inputs property instead.',
1099
+ DeprecationWarning,
1100
+ stacklevel=2,
1101
+ )
1102
+ return self.inputs[0]
1103
+
1104
+ @property
1105
+ def prevent_simultaneous_sink_and_source(self) -> bool:
1106
+ warnings.warn(
1107
+ 'The prevent_simultaneous_sink_and_source property is deprecated. Use the prevent_simultaneous_flow_rates property instead.',
1108
+ DeprecationWarning,
1109
+ stacklevel=2,
1110
+ )
1111
+ return self.prevent_simultaneous_flow_rates
654
1112
 
655
1113
 
656
1114
  @register_class_for_io
657
1115
  class Source(Component):
658
- def __init__(self, label: str, source: Flow, meta_data: Optional[Dict] = None):
659
- """
660
- Args:
661
- label: The label of the Element. Used to identify it in the FlowSystem
662
- source: output-flow of source
663
- 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
- """
665
- super().__init__(label, outputs=[source], meta_data=meta_data)
666
- self.source = source
1116
+ """
1117
+ A Source generates or provides energy or material flows into the system.
1118
+
1119
+ Sources represent supply points like power plants, fuel suppliers, renewable
1120
+ energy sources, or any system boundary where flows originate. They provide
1121
+ unlimited supply capability subject to flow constraints, demand patterns and effects.
1122
+
1123
+ Args:
1124
+ label: The label of the Element. Used to identify it in the FlowSystem.
1125
+ outputs: Output-flows from the source. Can be single flow or list of flows
1126
+ for sources providing multiple commodities or services.
1127
+ meta_data: Used to store additional information about the Element. Not used
1128
+ internally but saved in results. Only use Python native types.
1129
+ prevent_simultaneous_flow_rates: If True, only one output flow can be active
1130
+ at a time. Useful for modeling mutually exclusive supply options. Default is False.
1131
+
1132
+ Examples:
1133
+ Simple electricity grid connection:
1134
+
1135
+ ```python
1136
+ grid_source = Source(label='electrical_grid', outputs=[grid_electricity_flow])
1137
+ ```
1138
+
1139
+ Natural gas supply with cost and capacity constraints:
1140
+
1141
+ ```python
1142
+ gas_supply = Source(
1143
+ label='gas_network',
1144
+ outputs=[
1145
+ Flow(
1146
+ label='natural_gas_flow',
1147
+ bus=gas_bus,
1148
+ size=1000, # Maximum 1000 kW supply capacity
1149
+ effects_per_flow_hour={'cost': 0.04}, # €0.04/kWh gas cost
1150
+ )
1151
+ ],
1152
+ )
1153
+ ```
1154
+
1155
+ Multi-fuel power plant with switching constraints:
1156
+
1157
+ ```python
1158
+ multi_fuel_plant = Source(
1159
+ label='flexible_generator',
1160
+ outputs=[coal_electricity, gas_electricity, biomass_electricity],
1161
+ prevent_simultaneous_flow_rates=True, # Can only use one fuel at a time
1162
+ )
1163
+ ```
1164
+
1165
+ Renewable energy source with investment optimization:
1166
+
1167
+ ```python
1168
+ solar_farm = Source(
1169
+ label='solar_pv',
1170
+ outputs=[
1171
+ Flow(
1172
+ label='solar_power',
1173
+ bus=electricity_bus,
1174
+ size=InvestParameters(
1175
+ minimum_size=0,
1176
+ maximum_size=50000, # Up to 50 MW
1177
+ specific_effects={'cost': 800}, # €800/kW installed
1178
+ fix_effects={'cost': 100000}, # €100k development costs
1179
+ ),
1180
+ fixed_relative_profile=solar_profile, # Hourly generation profile
1181
+ )
1182
+ ],
1183
+ )
1184
+ ```
1185
+
1186
+ Deprecated:
1187
+ The deprecated `source` kwarg is accepted for compatibility but will be removed in future releases.
1188
+ """
1189
+
1190
+ def __init__(
1191
+ self,
1192
+ label: str,
1193
+ outputs: list[Flow] | None = None,
1194
+ meta_data: dict | None = None,
1195
+ prevent_simultaneous_flow_rates: bool = False,
1196
+ **kwargs,
1197
+ ):
1198
+ # Handle deprecated parameter using centralized helper
1199
+ outputs = self._handle_deprecated_kwarg(kwargs, 'source', 'outputs', outputs, transform=lambda x: [x])
1200
+
1201
+ # Validate any remaining unexpected kwargs
1202
+ self._validate_kwargs(kwargs)
1203
+
1204
+ self.prevent_simultaneous_flow_rates = prevent_simultaneous_flow_rates
1205
+ super().__init__(
1206
+ label,
1207
+ outputs=outputs,
1208
+ meta_data=meta_data,
1209
+ prevent_simultaneous_flows=outputs if prevent_simultaneous_flow_rates else None,
1210
+ )
1211
+
1212
+ @property
1213
+ def source(self) -> Flow:
1214
+ warnings.warn(
1215
+ 'The source property is deprecated. Use the outputs property instead.',
1216
+ DeprecationWarning,
1217
+ stacklevel=2,
1218
+ )
1219
+ return self.outputs[0]
667
1220
 
668
1221
 
669
1222
  @register_class_for_io
670
1223
  class Sink(Component):
671
- def __init__(self, label: str, sink: Flow, meta_data: Optional[Dict] = None):
1224
+ """
1225
+ A Sink consumes energy or material flows from the system.
1226
+
1227
+ Sinks represent demand points like electrical loads, heat demands, material
1228
+ consumption, or any system boundary where flows terminate. They provide
1229
+ unlimited consumption capability subject to flow constraints, demand patterns and effects.
1230
+
1231
+ Args:
1232
+ label: The label of the Element. Used to identify it in the FlowSystem.
1233
+ inputs: Input-flows into the sink. Can be single flow or list of flows
1234
+ for sinks consuming multiple commodities or services.
1235
+ meta_data: Used to store additional information about the Element. Not used
1236
+ internally but saved in results. Only use Python native types.
1237
+ prevent_simultaneous_flow_rates: If True, only one input flow can be active
1238
+ at a time. Useful for modeling mutually exclusive consumption options. Default is False.
1239
+
1240
+ Examples:
1241
+ Simple electrical demand:
1242
+
1243
+ ```python
1244
+ electrical_load = Sink(label='building_load', inputs=[electricity_demand_flow])
1245
+ ```
1246
+
1247
+ Heat demand with time-varying profile:
1248
+
1249
+ ```python
1250
+ heat_demand = Sink(
1251
+ label='district_heating_load',
1252
+ inputs=[
1253
+ Flow(
1254
+ label='heat_consumption',
1255
+ bus=heat_bus,
1256
+ fixed_relative_profile=hourly_heat_profile, # Demand profile
1257
+ size=2000, # Peak demand of 2000 kW
1258
+ )
1259
+ ],
1260
+ )
1261
+ ```
1262
+
1263
+ Multi-energy building with switching capabilities:
1264
+
1265
+ ```python
1266
+ flexible_building = Sink(
1267
+ label='smart_building',
1268
+ inputs=[electricity_heating, gas_heating, heat_pump_heating],
1269
+ prevent_simultaneous_flow_rates=True, # Can only use one heating mode
1270
+ )
1271
+ ```
1272
+
1273
+ Industrial process with variable demand:
1274
+
1275
+ ```python
1276
+ factory_load = Sink(
1277
+ label='manufacturing_plant',
1278
+ inputs=[
1279
+ Flow(
1280
+ label='electricity_process',
1281
+ bus=electricity_bus,
1282
+ size=5000, # Base electrical load
1283
+ effects_per_flow_hour={'cost': -0.1}, # Value of service (negative cost)
1284
+ ),
1285
+ Flow(
1286
+ label='steam_process',
1287
+ bus=steam_bus,
1288
+ size=3000, # Process steam demand
1289
+ fixed_relative_profile=production_schedule,
1290
+ ),
1291
+ ],
1292
+ )
1293
+ ```
1294
+
1295
+ Deprecated:
1296
+ The deprecated `sink` kwarg is accepted for compatibility but will be removed in future releases.
1297
+ """
1298
+
1299
+ def __init__(
1300
+ self,
1301
+ label: str,
1302
+ inputs: list[Flow] | None = None,
1303
+ meta_data: dict | None = None,
1304
+ prevent_simultaneous_flow_rates: bool = False,
1305
+ **kwargs,
1306
+ ):
672
1307
  """
673
- Args:
674
- 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
1308
+ Initialize a Sink (consumes flow from the system).
1309
+
1310
+ Supports legacy `sink=` keyword for backward compatibility (deprecated): if `sink` is provided it is used as the single input flow and a DeprecationWarning is issued; specifying both `inputs` and `sink` raises ValueError.
1311
+
1312
+ Parameters:
1313
+ label (str): Unique element label.
1314
+ inputs (list[Flow], optional): Input flows for the sink.
1315
+ meta_data (dict, optional): Arbitrary metadata attached to the element.
1316
+ prevent_simultaneous_flow_rates (bool, optional): If True, prevents simultaneous nonzero flow rates across the element's inputs by wiring that restriction into the base Component setup.
1317
+
1318
+ Note:
1319
+ The deprecated `sink` kwarg is accepted for compatibility but will be removed in future releases.
677
1320
  """
678
- super().__init__(label, inputs=[sink], meta_data=meta_data)
679
- self.sink = sink
1321
+ # Handle deprecated parameter using centralized helper
1322
+ inputs = self._handle_deprecated_kwarg(kwargs, 'sink', 'inputs', inputs, transform=lambda x: [x])
1323
+
1324
+ # Validate any remaining unexpected kwargs
1325
+ self._validate_kwargs(kwargs)
1326
+
1327
+ self.prevent_simultaneous_flow_rates = prevent_simultaneous_flow_rates
1328
+ super().__init__(
1329
+ label,
1330
+ inputs=inputs,
1331
+ meta_data=meta_data,
1332
+ prevent_simultaneous_flows=inputs if prevent_simultaneous_flow_rates else None,
1333
+ )
1334
+
1335
+ @property
1336
+ def sink(self) -> Flow:
1337
+ warnings.warn(
1338
+ 'The sink property is deprecated. Use the inputs property instead.',
1339
+ DeprecationWarning,
1340
+ stacklevel=2,
1341
+ )
1342
+ return self.inputs[0]