flixopt 2.2.0rc2__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 (58) hide show
  1. flixopt/__init__.py +33 -4
  2. flixopt/aggregation.py +60 -80
  3. flixopt/calculation.py +395 -178
  4. flixopt/commons.py +1 -10
  5. flixopt/components.py +939 -448
  6. flixopt/config.py +553 -191
  7. flixopt/core.py +513 -846
  8. flixopt/effects.py +644 -178
  9. flixopt/elements.py +610 -355
  10. flixopt/features.py +394 -966
  11. flixopt/flow_system.py +736 -219
  12. flixopt/interface.py +1104 -302
  13. flixopt/io.py +103 -79
  14. flixopt/linear_converters.py +387 -95
  15. flixopt/modeling.py +759 -0
  16. flixopt/network_app.py +73 -39
  17. flixopt/plotting.py +294 -138
  18. flixopt/results.py +1253 -299
  19. flixopt/solvers.py +25 -21
  20. flixopt/structure.py +938 -396
  21. flixopt/utils.py +38 -12
  22. flixopt-3.0.0.dist-info/METADATA +209 -0
  23. flixopt-3.0.0.dist-info/RECORD +26 -0
  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 -61
  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/user-guide/Mathematical Notation/Bus.md +0 -33
  37. docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +0 -132
  38. docs/user-guide/Mathematical Notation/Flow.md +0 -26
  39. docs/user-guide/Mathematical Notation/LinearConverter.md +0 -21
  40. docs/user-guide/Mathematical Notation/Piecewise.md +0 -49
  41. docs/user-guide/Mathematical Notation/Storage.md +0 -44
  42. docs/user-guide/Mathematical Notation/index.md +0 -22
  43. docs/user-guide/Mathematical Notation/others.md +0 -3
  44. docs/user-guide/index.md +0 -124
  45. flixopt/config.yaml +0 -10
  46. flixopt-2.2.0rc2.dist-info/METADATA +0 -167
  47. flixopt-2.2.0rc2.dist-info/RECORD +0 -54
  48. flixopt-2.2.0rc2.dist-info/top_level.txt +0 -5
  49. pics/architecture_flixOpt-pre2.0.0.png +0 -0
  50. pics/architecture_flixOpt.png +0 -0
  51. pics/flixOpt_plotting.jpg +0 -0
  52. pics/flixopt-icon.svg +0 -1
  53. pics/pics.pptx +0 -0
  54. scripts/extract_release_notes.py +0 -45
  55. scripts/gen_ref_pages.py +0 -54
  56. tests/ressources/Zeitreihen2020.csv +0 -35137
  57. {flixopt-2.2.0rc2.dist-info → flixopt-3.0.0.dist-info}/WHEEL +0 -0
  58. {flixopt-2.2.0rc2.dist-info → flixopt-3.0.0.dist-info}/licenses/LICENSE +0 -0
flixopt/elements.py CHANGED
@@ -2,21 +2,26 @@
2
2
  This module contains the basic elements of the flixopt framework.
3
3
  """
4
4
 
5
+ from __future__ import annotations
6
+
5
7
  import logging
6
8
  import warnings
7
- from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Tuple, Union
9
+ from typing import TYPE_CHECKING
8
10
 
9
- import linopy
10
11
  import numpy as np
12
+ import xarray as xr
11
13
 
12
14
  from .config import CONFIG
13
- from .core import NumericData, NumericDataTS, PlausibilityError, Scalar, TimeSeriesCollection
14
- from .effects import EffectValuesUser
15
- from .features import InvestmentModel, OnOffModel, PiecewiseEffectsPerFlowHourModel, PreventSimultaneousUsageModel
16
- from .interface import InvestParameters, OnOffParameters, PiecewiseEffectsPerFlowHour
17
- from .structure import Element, ElementModel, SystemModel, register_class_for_io
15
+ from .core import PlausibilityError, Scalar, TemporalData, TemporalDataUser
16
+ from .features import InvestmentModel, OnOffModel
17
+ from .interface import InvestParameters, OnOffParameters
18
+ from .modeling import BoundingPatterns, ModelingPrimitives, ModelingUtilitiesAbstract
19
+ from .structure import Element, ElementModel, FlowSystemModel, register_class_for_io
18
20
 
19
21
  if TYPE_CHECKING:
22
+ import linopy
23
+
24
+ from .effects import TemporalEffectsUser
20
25
  from .flow_system import FlowSystem
21
26
 
22
27
  logger = logging.getLogger('flixopt')
@@ -25,58 +30,80 @@ logger = logging.getLogger('flixopt')
25
30
  @register_class_for_io
26
31
  class Component(Element):
27
32
  """
28
- A Component contains incoming and outgoing [`Flows`][flixopt.elements.Flow]. It defines how these Flows interact with each other.
29
- The On or Off state of the Component is defined by all its Flows. Its on, if any of its FLows is On.
30
- It's mathematically advisable to define the On/Off state in a FLow rather than a Component if possible,
31
- as this introduces less binary variables to the Model
32
- Constraints to the On/Off state are defined by the [`on_off_parameters`][flixopt.interface.OnOffParameters].
33
+ Base class for all system components that transform, convert, or process flows.
34
+
35
+ Components are the active elements in energy systems that define how input and output
36
+ Flows interact with each other. They represent equipment, processes, or logical
37
+ operations that transform energy or materials between different states, carriers,
38
+ or locations.
39
+
40
+ Components serve as connection points between Buses through their associated Flows,
41
+ enabling the modeling of complex energy system topologies and operational constraints.
42
+
43
+ Args:
44
+ label: The label of the Element. Used to identify it in the FlowSystem.
45
+ inputs: list of input Flows feeding into the component. These represent
46
+ energy/material consumption by the component.
47
+ outputs: list of output Flows leaving the component. These represent
48
+ energy/material production by the component.
49
+ on_off_parameters: Defines binary operation constraints and costs when the
50
+ component has discrete on/off states. Creates binary variables for all
51
+ connected Flows. For better performance, prefer defining OnOffParameters
52
+ on individual Flows when possible.
53
+ prevent_simultaneous_flows: list of Flows that cannot be active simultaneously.
54
+ Creates binary variables to enforce mutual exclusivity. Use sparingly as
55
+ it increases computational complexity.
56
+ meta_data: Used to store additional information. Not used internally but saved
57
+ in results. Only use Python native types.
58
+
59
+ Note:
60
+ Component operational state is determined by its connected Flows:
61
+ - Component is "on" if ANY of its Flows is active (flow_rate > 0)
62
+ - Component is "off" only when ALL Flows are inactive (flow_rate = 0)
63
+
64
+ Binary variables and constraints:
65
+ - on_off_parameters creates binary variables for ALL connected Flows
66
+ - prevent_simultaneous_flows creates binary variables for specified Flows
67
+ - For better computational performance, prefer Flow-level OnOffParameters
68
+
69
+ Component is an abstract base class. In practice, use specialized subclasses:
70
+ - LinearConverter: Linear input/output relationships
71
+ - Storage: Temporal energy/material storage
72
+ - Transmission: Transport between locations
73
+ - Source/Sink: System boundaries
74
+
33
75
  """
34
76
 
35
77
  def __init__(
36
78
  self,
37
79
  label: str,
38
- inputs: Optional[List['Flow']] = None,
39
- outputs: Optional[List['Flow']] = None,
40
- on_off_parameters: Optional[OnOffParameters] = None,
41
- prevent_simultaneous_flows: Optional[List['Flow']] = None,
42
- meta_data: Optional[Dict] = None,
80
+ inputs: list[Flow] | None = None,
81
+ outputs: list[Flow] | None = None,
82
+ on_off_parameters: OnOffParameters | None = None,
83
+ prevent_simultaneous_flows: list[Flow] | None = None,
84
+ meta_data: dict | None = None,
43
85
  ):
44
- """
45
- Args:
46
- label: The label of the Element. Used to identify it in the FlowSystem
47
- inputs: input flows.
48
- outputs: output flows.
49
- on_off_parameters: Information about on and off state of Component.
50
- Component is On/Off, if all connected Flows are On/Off. This induces an On-Variable (binary) in all Flows!
51
- If possible, use OnOffParameters in a single Flow instead to keep the number of binary variables low.
52
- See class OnOffParameters.
53
- prevent_simultaneous_flows: Define a Group of Flows. Only one them can be on at a time.
54
- Induces On-Variable in all Flows! If possible, use OnOffParameters in a single Flow instead.
55
- meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
56
- """
57
86
  super().__init__(label, meta_data=meta_data)
58
- self.inputs: List['Flow'] = inputs or []
59
- self.outputs: List['Flow'] = outputs or []
87
+ self.inputs: list[Flow] = inputs or []
88
+ self.outputs: list[Flow] = outputs or []
60
89
  self._check_unique_flow_labels()
61
90
  self.on_off_parameters = on_off_parameters
62
- self.prevent_simultaneous_flows: List['Flow'] = prevent_simultaneous_flows or []
91
+ self.prevent_simultaneous_flows: list[Flow] = prevent_simultaneous_flows or []
63
92
 
64
- self.flows: Dict[str, Flow] = {flow.label: flow for flow in self.inputs + self.outputs}
93
+ self.flows: dict[str, Flow] = {flow.label: flow for flow in self.inputs + self.outputs}
65
94
 
66
- def create_model(self, model: SystemModel) -> 'ComponentModel':
95
+ def create_model(self, model: FlowSystemModel) -> ComponentModel:
67
96
  self._plausibility_checks()
68
- self.model = ComponentModel(model, self)
69
- return self.model
97
+ self.submodel = ComponentModel(model, self)
98
+ return self.submodel
70
99
 
71
- def transform_data(self, flow_system: 'FlowSystem') -> None:
100
+ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:
101
+ prefix = '|'.join(filter(None, [name_prefix, self.label_full]))
72
102
  if self.on_off_parameters is not None:
73
- self.on_off_parameters.transform_data(flow_system, self.label_full)
103
+ self.on_off_parameters.transform_data(flow_system, prefix)
74
104
 
75
- def infos(self, use_numpy=True, use_element_label: bool = False) -> Dict:
76
- infos = super().infos(use_numpy, use_element_label)
77
- infos['inputs'] = [flow.infos(use_numpy, use_element_label) for flow in self.inputs]
78
- infos['outputs'] = [flow.infos(use_numpy, use_element_label) for flow in self.outputs]
79
- return infos
105
+ for flow in self.inputs + self.outputs:
106
+ flow.transform_data(flow_system) # Flow doesnt need the name_prefix
80
107
 
81
108
  def _check_unique_flow_labels(self):
82
109
  all_flow_labels = [flow.label for flow in self.inputs + self.outputs]
@@ -92,38 +119,94 @@ class Component(Element):
92
119
  @register_class_for_io
93
120
  class Bus(Element):
94
121
  """
95
- A Bus represents a nodal balance between the flow rates of its incoming and outgoing Flows.
122
+ Buses represent nodal balances between flow rates, serving as connection points.
123
+
124
+ A Bus enforces energy or material balance constraints where the sum of all incoming
125
+ flows must equal the sum of all outgoing flows at each time step. Buses represent
126
+ physical or logical connection points for energy carriers (electricity, heat, gas)
127
+ or material flows between different Components.
128
+
129
+ Mathematical Formulation:
130
+ See the complete mathematical model in the documentation:
131
+ [Bus](../user-guide/mathematical-notation/elements/Bus.md)
132
+
133
+ Args:
134
+ label: The label of the Element. Used to identify it in the FlowSystem.
135
+ excess_penalty_per_flow_hour: Penalty costs for bus balance violations.
136
+ When None, no excess/deficit is allowed (hard constraint). When set to a
137
+ value > 0, allows bus imbalances at penalty cost. Default is 1e5 (high penalty).
138
+ meta_data: Used to store additional information. Not used internally but saved
139
+ in results. Only use Python native types.
140
+
141
+ Examples:
142
+ Electrical bus with strict balance:
143
+
144
+ ```python
145
+ electricity_bus = Bus(
146
+ label='main_electrical_bus',
147
+ excess_penalty_per_flow_hour=None, # No imbalance allowed
148
+ )
149
+ ```
150
+
151
+ Heat network with penalty for imbalances:
152
+
153
+ ```python
154
+ heat_network = Bus(
155
+ label='district_heating_network',
156
+ excess_penalty_per_flow_hour=1000, # €1000/MWh penalty for imbalance
157
+ )
158
+ ```
159
+
160
+ Material flow with time-varying penalties:
161
+
162
+ ```python
163
+ material_hub = Bus(
164
+ label='material_processing_hub',
165
+ excess_penalty_per_flow_hour=waste_disposal_costs, # Time series
166
+ )
167
+ ```
168
+
169
+ Note:
170
+ The bus balance equation enforced is: Σ(inflows) = Σ(outflows) + excess - deficit
171
+
172
+ When excess_penalty_per_flow_hour is None, excess and deficit are forced to zero.
173
+ When a penalty cost is specified, the optimization can choose to violate the
174
+ balance if economically beneficial, paying the penalty.
175
+ The penalty is added to the objective directly.
176
+
177
+ Empty `inputs` and `outputs` lists are initialized and populated automatically
178
+ by the FlowSystem during system setup.
96
179
  """
97
180
 
98
181
  def __init__(
99
- self, label: str, excess_penalty_per_flow_hour: Optional[NumericDataTS] = 1e5, meta_data: Optional[Dict] = None
182
+ self,
183
+ label: str,
184
+ excess_penalty_per_flow_hour: TemporalDataUser | None = 1e5,
185
+ meta_data: dict | None = None,
100
186
  ):
101
- """
102
- Args:
103
- label: The label of the Element. Used to identify it in the FlowSystem
104
- excess_penalty_per_flow_hour: excess costs / penalty costs (bus balance compensation)
105
- (none/ 0 -> no penalty). The default is 1e5.
106
- (Take care: if you use a timeseries (no scalar), timeseries is aggregated if calculation_type = aggregated!)
107
- meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
108
- """
109
187
  super().__init__(label, meta_data=meta_data)
110
188
  self.excess_penalty_per_flow_hour = excess_penalty_per_flow_hour
111
- self.inputs: List[Flow] = []
112
- self.outputs: List[Flow] = []
189
+ self.inputs: list[Flow] = []
190
+ self.outputs: list[Flow] = []
113
191
 
114
- def create_model(self, model: SystemModel) -> 'BusModel':
192
+ def create_model(self, model: FlowSystemModel) -> BusModel:
115
193
  self._plausibility_checks()
116
- self.model = BusModel(model, self)
117
- return self.model
194
+ self.submodel = BusModel(model, self)
195
+ return self.submodel
118
196
 
119
- def transform_data(self, flow_system: 'FlowSystem'):
120
- self.excess_penalty_per_flow_hour = flow_system.create_time_series(
121
- f'{self.label_full}|excess_penalty_per_flow_hour', self.excess_penalty_per_flow_hour
197
+ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:
198
+ prefix = '|'.join(filter(None, [name_prefix, self.label_full]))
199
+ self.excess_penalty_per_flow_hour = flow_system.fit_to_model_coords(
200
+ f'{prefix}|excess_penalty_per_flow_hour', self.excess_penalty_per_flow_hour
122
201
  )
123
202
 
124
203
  def _plausibility_checks(self) -> None:
125
- if self.excess_penalty_per_flow_hour is not None and (self.excess_penalty_per_flow_hour == 0).all():
126
- logger.warning(f'In Bus {self.label}, the excess_penalty_per_flow_hour is 0. Use "None" or a value > 0.')
204
+ if self.excess_penalty_per_flow_hour is not None:
205
+ zero_penalty = np.all(np.equal(self.excess_penalty_per_flow_hour, 0))
206
+ if zero_penalty:
207
+ logger.warning(
208
+ f'In Bus {self.label_full}, the excess_penalty_per_flow_hour is 0. Use "None" or a value > 0.'
209
+ )
127
210
 
128
211
  @property
129
212
  def with_excess(self) -> bool:
@@ -145,62 +228,155 @@ class Connection:
145
228
 
146
229
  @register_class_for_io
147
230
  class Flow(Element):
148
- r"""
149
- A **Flow** moves energy (or material) between a [Bus][flixopt.elements.Bus] and a [Component][flixopt.elements.Component] in a predefined direction.
150
- The flow-rate is the main optimization variable of the **Flow**.
231
+ """Define a directed flow of energy or material between bus and component.
232
+
233
+ A Flow represents the transfer of energy (electricity, heat, fuel) or material
234
+ between a Bus and a Component in a specific direction. The flow rate is the
235
+ primary optimization variable, with constraints and costs defined through
236
+ various parameters. Flows can have fixed or variable sizes, operational
237
+ constraints, and complex on/off behavior.
238
+
239
+ Key Concepts:
240
+ **Flow Rate**: The instantaneous rate of energy/material transfer (optimization variable) [kW, m³/h, kg/h]
241
+ **Flow Hours**: Amount of energy/material transferred per timestep. [kWh, m³, kg]
242
+ **Flow Size**: The maximum capacity or nominal rating of the flow [kW, m³/h, kg/h]
243
+ **Relative Bounds**: Flow rate limits expressed as fractions of flow size
244
+
245
+ Integration with Parameter Classes:
246
+ - **InvestParameters**: Used for `size` when flow Size is an investment decision
247
+ - **OnOffParameters**: Used for `on_off_parameters` when flow has discrete states
248
+
249
+ Mathematical Formulation:
250
+ See the complete mathematical model in the documentation:
251
+ [Flow](../user-guide/mathematical-notation/elements/Flow.md)
252
+
253
+ Args:
254
+ label: Unique flow identifier within its component.
255
+ bus: Bus label this flow connects to.
256
+ size: Flow capacity. Scalar, InvestParameters, or None (uses CONFIG.Modeling.big).
257
+ relative_minimum: Minimum flow rate as fraction of size (0-1). Default: 0.
258
+ relative_maximum: Maximum flow rate as fraction of size. Default: 1.
259
+ load_factor_min: Minimum average utilization (0-1). Default: 0.
260
+ load_factor_max: Maximum average utilization (0-1). Default: 1.
261
+ effects_per_flow_hour: Operational costs/impacts per flow-hour.
262
+ Dict mapping effect names to values (e.g., {'cost': 45, 'CO2': 0.8}).
263
+ on_off_parameters: Binary operation constraints (OnOffParameters). Default: None.
264
+ flow_hours_total_max: Maximum cumulative flow-hours. Alternative to load_factor_max.
265
+ flow_hours_total_min: Minimum cumulative flow-hours. Alternative to load_factor_min.
266
+ fixed_relative_profile: Predetermined pattern as fraction of size.
267
+ Flow rate = size × fixed_relative_profile(t).
268
+ previous_flow_rate: Initial flow state for on/off dynamics. Default: None (off).
269
+ meta_data: Additional info stored in results. Python native types only.
270
+
271
+ Examples:
272
+ Basic power flow with fixed capacity:
273
+
274
+ ```python
275
+ generator_output = Flow(
276
+ label='electricity_out',
277
+ bus='electricity_grid',
278
+ size=100, # 100 MW capacity
279
+ relative_minimum=0.4, # Cannot operate below 40 MW
280
+ effects_per_flow_hour={'fuel_cost': 45, 'co2_emissions': 0.8},
281
+ )
282
+ ```
283
+
284
+ Investment decision for battery capacity:
285
+
286
+ ```python
287
+ battery_flow = Flow(
288
+ label='electricity_storage',
289
+ bus='electricity_grid',
290
+ size=InvestParameters(
291
+ minimum_size=10, # Minimum 10 MWh
292
+ maximum_size=100, # Maximum 100 MWh
293
+ specific_effects={'cost': 150_000}, # €150k/MWh annualized
294
+ ),
295
+ )
296
+ ```
297
+
298
+ Heat pump with startup costs and minimum run times:
299
+
300
+ ```python
301
+ heat_pump = Flow(
302
+ label='heat_output',
303
+ bus='heating_network',
304
+ size=50, # 50 kW thermal
305
+ relative_minimum=0.3, # Minimum 15 kW output when on
306
+ effects_per_flow_hour={'electricity_cost': 25, 'maintenance': 2},
307
+ on_off_parameters=OnOffParameters(
308
+ effects_per_switch_on={'startup_cost': 100, 'wear': 0.1},
309
+ consecutive_on_hours_min=2, # Must run at least 2 hours
310
+ consecutive_off_hours_min=1, # Must stay off at least 1 hour
311
+ switch_on_total_max=200, # Maximum 200 starts per period
312
+ ),
313
+ )
314
+ ```
315
+
316
+ Fixed renewable generation profile:
317
+
318
+ ```python
319
+ solar_generation = Flow(
320
+ label='solar_power',
321
+ bus='electricity_grid',
322
+ size=25, # 25 MW installed capacity
323
+ fixed_relative_profile=np.array([0, 0.1, 0.4, 0.8, 0.9, 0.7, 0.3, 0.1, 0]),
324
+ effects_per_flow_hour={'maintenance_costs': 5}, # €5/MWh maintenance
325
+ )
326
+ ```
327
+
328
+ Industrial process with annual utilization limits:
329
+
330
+ ```python
331
+ production_line = Flow(
332
+ label='product_output',
333
+ bus='product_market',
334
+ size=1000, # 1000 units/hour capacity
335
+ load_factor_min=0.6, # Must achieve 60% annual utilization
336
+ load_factor_max=0.85, # Cannot exceed 85% for maintenance
337
+ effects_per_flow_hour={'variable_cost': 12, 'quality_control': 0.5},
338
+ )
339
+ ```
340
+
341
+ Design Considerations:
342
+ **Size vs Load Factors**: Use `flow_hours_total_min/max` for absolute limits,
343
+ `load_factor_min/max` for utilization-based constraints.
344
+
345
+ **Relative Bounds**: Set `relative_minimum > 0` only when equipment cannot
346
+ operate below that level. Use `on_off_parameters` for discrete on/off behavior.
347
+
348
+ **Fixed Profiles**: Use `fixed_relative_profile` for known exact patterns,
349
+ `relative_maximum` for upper bounds on optimization variables.
350
+
351
+ Notes:
352
+ - Default size (CONFIG.Modeling.big) is used when size=None
353
+ - list inputs for previous_flow_rate are converted to NumPy arrays
354
+ - Flow direction is determined by component input/output designation
355
+
356
+ Deprecated:
357
+ Passing Bus objects to `bus` parameter. Use bus label strings instead.
358
+
151
359
  """
152
360
 
153
361
  def __init__(
154
362
  self,
155
363
  label: str,
156
364
  bus: str,
157
- size: Union[Scalar, InvestParameters] = None,
158
- fixed_relative_profile: Optional[NumericDataTS] = None,
159
- relative_minimum: NumericDataTS = 0,
160
- relative_maximum: NumericDataTS = 1,
161
- effects_per_flow_hour: Optional[EffectValuesUser] = None,
162
- piecewise_effects_per_flow_hour: Optional[PiecewiseEffectsPerFlowHour] = None,
163
- on_off_parameters: Optional[OnOffParameters] = None,
164
- flow_hours_total_max: Optional[Scalar] = None,
165
- flow_hours_total_min: Optional[Scalar] = None,
166
- load_factor_min: Optional[Scalar] = None,
167
- load_factor_max: Optional[Scalar] = None,
168
- previous_flow_rate: Optional[NumericData] = None,
169
- meta_data: Optional[Dict] = None,
365
+ size: Scalar | InvestParameters = None,
366
+ fixed_relative_profile: TemporalDataUser | None = None,
367
+ relative_minimum: TemporalDataUser = 0,
368
+ relative_maximum: TemporalDataUser = 1,
369
+ effects_per_flow_hour: TemporalEffectsUser | None = None,
370
+ on_off_parameters: OnOffParameters | None = None,
371
+ flow_hours_total_max: Scalar | None = None,
372
+ flow_hours_total_min: Scalar | None = None,
373
+ load_factor_min: Scalar | None = None,
374
+ load_factor_max: Scalar | None = None,
375
+ previous_flow_rate: Scalar | list[Scalar] | None = None,
376
+ meta_data: dict | None = None,
170
377
  ):
171
- r"""
172
- Args:
173
- label: The label of the FLow. Used to identify it in the FlowSystem. Its `full_label` consists of the label of the Component and the label of the Flow.
174
- bus: blabel of the bus the flow is connected to.
175
- size: size of the flow. If InvestmentParameters is used, size is optimized.
176
- If size is None, a default value is used.
177
- relative_minimum: min value is relative_minimum multiplied by size
178
- relative_maximum: max value is relative_maximum multiplied by size. If size = max then relative_maximum=1
179
- load_factor_min: minimal load factor general: avg Flow per nominalVal/investSize
180
- (e.g. boiler, kW/kWh=h; solarthermal: kW/m²;
181
- def: :math:`load\_factor:= sumFlowHours/ (nominal\_val \cdot \Delta t_{tot})`
182
- load_factor_max: maximal load factor (see minimal load factor)
183
- effects_per_flow_hour: operational costs, costs per flow-"work"
184
- piecewise_effects_per_flow_hour: piecewise relation between flow hours and effects
185
- on_off_parameters: If present, flow can be "off", i.e. be zero (only relevant if relative_minimum > 0)
186
- Therefore a binary var "on" is used. Further, several other restrictions and effects can be modeled
187
- through this On/Off State (See OnOffParameters)
188
- flow_hours_total_max: maximum flow-hours ("flow-work")
189
- (if size is not const, maybe load_factor_max is the better choice!)
190
- flow_hours_total_min: minimum flow-hours ("flow-work")
191
- (if size is not predefined, maybe load_factor_min is the better choice!)
192
- fixed_relative_profile: fixed relative values for flow (if given).
193
- flow_rate(t) := fixed_relative_profile(t) * size(t)
194
- With this value, the flow_rate is no optimization-variable anymore.
195
- (relative_minimum and relative_maximum are ignored)
196
- used for fixed load or supply profiles, i.g. heat demand, wind-power, solarthermal
197
- If the load-profile is just an upper limit, use relative_maximum instead.
198
- previous_flow_rate: previous flow rate of the flow. Used to determine if and how long the
199
- flow is already on / off. If None, the flow is considered to be off for one timestep.
200
- meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
201
- """
202
378
  super().__init__(label, meta_data=meta_data)
203
- self.size = size or CONFIG.modeling.BIG # Default size
379
+ self.size = CONFIG.Modeling.big if size is None else size
204
380
  self.relative_minimum = relative_minimum
205
381
  self.relative_maximum = relative_maximum
206
382
  self.fixed_relative_profile = fixed_relative_profile
@@ -209,17 +385,14 @@ class Flow(Element):
209
385
  self.load_factor_max = load_factor_max
210
386
  # self.positive_gradient = TimeSeries('positive_gradient', positive_gradient, self)
211
387
  self.effects_per_flow_hour = effects_per_flow_hour if effects_per_flow_hour is not None else {}
212
- self.piecewise_effects_per_flow_hour = piecewise_effects_per_flow_hour
213
388
  self.flow_hours_total_max = flow_hours_total_max
214
389
  self.flow_hours_total_min = flow_hours_total_min
215
390
  self.on_off_parameters = on_off_parameters
216
391
 
217
- self.previous_flow_rate = (
218
- previous_flow_rate if not isinstance(previous_flow_rate, list) else np.array(previous_flow_rate)
219
- )
392
+ self.previous_flow_rate = previous_flow_rate
220
393
 
221
394
  self.component: str = 'UnknownComponent'
222
- self.is_input_in_component: Optional[bool] = None
395
+ self.is_input_in_component: bool | None = None
223
396
  if isinstance(bus, Bus):
224
397
  self.bus = bus.label_full
225
398
  warnings.warn(
@@ -233,70 +406,80 @@ class Flow(Element):
233
406
  self.bus = bus
234
407
  self._bus_object = None
235
408
 
236
- def create_model(self, model: SystemModel) -> 'FlowModel':
409
+ def create_model(self, model: FlowSystemModel) -> FlowModel:
237
410
  self._plausibility_checks()
238
- self.model = FlowModel(model, self)
239
- return self.model
240
-
241
- def transform_data(self, flow_system: 'FlowSystem'):
242
- self.relative_minimum = flow_system.create_time_series(
243
- f'{self.label_full}|relative_minimum', self.relative_minimum
411
+ self.submodel = FlowModel(model, self)
412
+ return self.submodel
413
+
414
+ def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:
415
+ prefix = '|'.join(filter(None, [name_prefix, self.label_full]))
416
+ self.relative_minimum = flow_system.fit_to_model_coords(f'{prefix}|relative_minimum', self.relative_minimum)
417
+ self.relative_maximum = flow_system.fit_to_model_coords(f'{prefix}|relative_maximum', self.relative_maximum)
418
+ self.fixed_relative_profile = flow_system.fit_to_model_coords(
419
+ f'{prefix}|fixed_relative_profile', self.fixed_relative_profile
420
+ )
421
+ self.effects_per_flow_hour = flow_system.fit_effects_to_model_coords(
422
+ prefix, self.effects_per_flow_hour, 'per_flow_hour'
244
423
  )
245
- self.relative_maximum = flow_system.create_time_series(
246
- f'{self.label_full}|relative_maximum', self.relative_maximum
424
+ self.flow_hours_total_max = flow_system.fit_to_model_coords(
425
+ f'{prefix}|flow_hours_total_max', self.flow_hours_total_max, dims=['period', 'scenario']
247
426
  )
248
- self.fixed_relative_profile = flow_system.create_time_series(
249
- f'{self.label_full}|fixed_relative_profile', self.fixed_relative_profile
427
+ self.flow_hours_total_min = flow_system.fit_to_model_coords(
428
+ f'{prefix}|flow_hours_total_min', self.flow_hours_total_min, dims=['period', 'scenario']
250
429
  )
251
- self.effects_per_flow_hour = flow_system.create_effect_time_series(
252
- self.label_full, self.effects_per_flow_hour, 'per_flow_hour'
430
+ self.load_factor_max = flow_system.fit_to_model_coords(
431
+ f'{prefix}|load_factor_max', self.load_factor_max, dims=['period', 'scenario']
253
432
  )
254
- if self.piecewise_effects_per_flow_hour is not None:
255
- self.piecewise_effects_per_flow_hour.transform_data(flow_system, self.label_full)
433
+ self.load_factor_min = flow_system.fit_to_model_coords(
434
+ f'{prefix}|load_factor_min', self.load_factor_min, dims=['period', 'scenario']
435
+ )
436
+
256
437
  if self.on_off_parameters is not None:
257
- self.on_off_parameters.transform_data(flow_system, self.label_full)
438
+ self.on_off_parameters.transform_data(flow_system, prefix)
258
439
  if isinstance(self.size, InvestParameters):
259
- self.size.transform_data(flow_system)
260
-
261
- def infos(self, use_numpy: bool = True, use_element_label: bool = False) -> Dict:
262
- infos = super().infos(use_numpy, use_element_label)
263
- infos['is_input_in_component'] = self.is_input_in_component
264
- return infos
265
-
266
- def to_dict(self) -> Dict:
267
- data = super().to_dict()
268
- if isinstance(data.get('previous_flow_rate'), np.ndarray):
269
- data['previous_flow_rate'] = data['previous_flow_rate'].tolist()
270
- return data
440
+ self.size.transform_data(flow_system, prefix)
441
+ else:
442
+ self.size = flow_system.fit_to_model_coords(f'{prefix}|size', self.size, dims=['period', 'scenario'])
271
443
 
272
444
  def _plausibility_checks(self) -> None:
273
445
  # TODO: Incorporate into Variable? (Lower_bound can not be greater than upper bound
274
- if np.any(self.relative_minimum > self.relative_maximum):
446
+ if (self.relative_minimum > self.relative_maximum).any():
275
447
  raise PlausibilityError(self.label_full + ': Take care, that relative_minimum <= relative_maximum!')
276
448
 
277
- if (
278
- self.size == CONFIG.modeling.BIG and self.fixed_relative_profile is not None
449
+ if not isinstance(self.size, InvestParameters) and (
450
+ np.any(self.size == CONFIG.Modeling.big) and self.fixed_relative_profile is not None
279
451
  ): # Default Size --> Most likely by accident
280
452
  logger.warning(
281
- f'Flow "{self.label}" has no size assigned, but a "fixed_relative_profile". '
282
- f'The default size is {CONFIG.modeling.BIG}. As "flow_rate = size * fixed_relative_profile", '
453
+ f'Flow "{self.label_full}" has no size assigned, but a "fixed_relative_profile". '
454
+ f'The default size is {CONFIG.Modeling.big}. As "flow_rate = size * fixed_relative_profile", '
283
455
  f'the resulting flow_rate will be very high. To fix this, assign a size to the Flow {self}.'
284
456
  )
285
457
 
286
458
  if self.fixed_relative_profile is not None and self.on_off_parameters is not None:
287
- raise ValueError(
288
- f'Flow {self.label} has both a fixed_relative_profile and an on_off_parameters. This is not supported. '
289
- f'Use relative_minimum and relative_maximum instead, '
290
- f'if you want to allow flows to be switched on and off.'
459
+ logger.warning(
460
+ f'Flow {self.label_full} has both a fixed_relative_profile and an on_off_parameters.'
461
+ f'This will allow the flow to be switched on and off, effectively differing from the fixed_flow_rate.'
291
462
  )
292
463
 
293
- if (self.relative_minimum > 0).any() and self.on_off_parameters is None:
464
+ if np.any(self.relative_minimum > 0) and self.on_off_parameters is None:
294
465
  logger.warning(
295
- f'Flow {self.label} has a relative_minimum of {self.relative_minimum.active_data} and no on_off_parameters. '
466
+ f'Flow {self.label_full} has a relative_minimum of {self.relative_minimum} and no on_off_parameters. '
296
467
  f'This prevents the flow_rate from switching off (flow_rate = 0). '
297
468
  f'Consider using on_off_parameters to allow the flow to be switched on and off.'
298
469
  )
299
470
 
471
+ if self.previous_flow_rate is not None:
472
+ if not any(
473
+ [
474
+ isinstance(self.previous_flow_rate, np.ndarray) and self.previous_flow_rate.ndim == 1,
475
+ isinstance(self.previous_flow_rate, (int, float, list)),
476
+ ]
477
+ ):
478
+ raise TypeError(
479
+ f'previous_flow_rate must be None, a scalar, a list of scalars or a 1D-numpy-array. Got {type(self.previous_flow_rate)}. '
480
+ f'Different values in different periods or scenarios are not yet supported.'
481
+ )
482
+
300
483
  @property
301
484
  def label_full(self) -> str:
302
485
  return f'{self.component}({self.label})'
@@ -306,252 +489,294 @@ class Flow(Element):
306
489
  # Wenn kein InvestParameters existiert --> True; Wenn Investparameter, den Wert davon nehmen
307
490
  return False if (isinstance(self.size, InvestParameters) and self.size.fixed_size is None) else True
308
491
 
309
- @property
310
- def invest_is_optional(self) -> bool:
311
- # Wenn kein InvestParameters existiert: # Investment ist nicht optional -> Keine Variable --> False
312
- return False if (isinstance(self.size, InvestParameters) and not self.size.optional) else True
313
-
314
492
 
315
493
  class FlowModel(ElementModel):
316
- def __init__(self, model: SystemModel, element: Flow):
494
+ element: Flow # Type hint
495
+
496
+ def __init__(self, model: FlowSystemModel, element: Flow):
317
497
  super().__init__(model, element)
318
- self.element: Flow = element
319
- self.flow_rate: Optional[linopy.Variable] = None
320
- self.total_flow_hours: Optional[linopy.Variable] = None
321
-
322
- self.on_off: Optional[OnOffModel] = None
323
- self._investment: Optional[InvestmentModel] = None
324
-
325
- def do_modeling(self):
326
- # eq relative_minimum(t) * size <= flow_rate(t) <= relative_maximum(t) * size
327
- self.flow_rate: linopy.Variable = self.add(
328
- self._model.add_variables(
329
- lower=self.flow_rate_lower_bound,
330
- upper=self.flow_rate_upper_bound,
331
- coords=self._model.coords,
332
- name=f'{self.label_full}|flow_rate',
498
+
499
+ def _do_modeling(self):
500
+ super()._do_modeling()
501
+ # Main flow rate variable
502
+ self.add_variables(
503
+ lower=self.absolute_flow_rate_bounds[0],
504
+ upper=self.absolute_flow_rate_bounds[1],
505
+ coords=self._model.get_coords(),
506
+ short_name='flow_rate',
507
+ )
508
+
509
+ self._constraint_flow_rate()
510
+
511
+ # Total flow hours tracking
512
+ ModelingPrimitives.expression_tracking_variable(
513
+ model=self,
514
+ name=f'{self.label_full}|total_flow_hours',
515
+ tracked_expression=(self.flow_rate * self._model.hours_per_step).sum('time'),
516
+ bounds=(
517
+ self.element.flow_hours_total_min if self.element.flow_hours_total_min is not None else 0,
518
+ self.element.flow_hours_total_max if self.element.flow_hours_total_max is not None else None,
333
519
  ),
334
- 'flow_rate',
520
+ coords=['period', 'scenario'],
521
+ short_name='total_flow_hours',
335
522
  )
336
523
 
337
- # OnOff
338
- if self.element.on_off_parameters is not None:
339
- self.on_off: OnOffModel = self.add(
340
- OnOffModel(
341
- model=self._model,
342
- label_of_element=self.label_of_element,
343
- on_off_parameters=self.element.on_off_parameters,
344
- defining_variables=[self.flow_rate],
345
- defining_bounds=[self.flow_rate_bounds_on],
346
- previous_values=[self.element.previous_flow_rate],
347
- ),
348
- 'on_off',
349
- )
350
- self.on_off.do_modeling()
524
+ # Load factor constraints
525
+ self._create_bounds_for_load_factor()
351
526
 
352
- # Investment
353
- if isinstance(self.element.size, InvestParameters):
354
- self._investment: InvestmentModel = self.add(
355
- InvestmentModel(
356
- model=self._model,
357
- label_of_element=self.label_of_element,
358
- parameters=self.element.size,
359
- defining_variable=self.flow_rate,
360
- relative_bounds_of_defining_variable=(
361
- self.flow_rate_lower_bound_relative,
362
- self.flow_rate_upper_bound_relative,
363
- ),
364
- on_variable=self.on_off.on if self.on_off is not None else None,
365
- ),
366
- 'investment',
367
- )
368
- self._investment.do_modeling()
369
-
370
- self.total_flow_hours = self.add(
371
- self._model.add_variables(
372
- lower=self.element.flow_hours_total_min if self.element.flow_hours_total_min is not None else 0,
373
- upper=self.element.flow_hours_total_max if self.element.flow_hours_total_max is not None else np.inf,
374
- coords=None,
375
- name=f'{self.label_full}|total_flow_hours',
527
+ # Effects
528
+ self._create_shares()
529
+
530
+ def _create_on_off_model(self):
531
+ on = self.add_variables(binary=True, short_name='on', coords=self._model.get_coords())
532
+ self.add_submodels(
533
+ OnOffModel(
534
+ model=self._model,
535
+ label_of_element=self.label_of_element,
536
+ parameters=self.element.on_off_parameters,
537
+ on_variable=on,
538
+ previous_states=self.previous_states,
539
+ label_of_model=self.label_of_element,
376
540
  ),
377
- 'total_flow_hours',
541
+ short_name='on_off',
378
542
  )
379
543
 
380
- self.add(
381
- self._model.add_constraints(
382
- self.total_flow_hours == (self.flow_rate * self._model.hours_per_step).sum(),
383
- name=f'{self.label_full}|total_flow_hours',
544
+ def _create_investment_model(self):
545
+ self.add_submodels(
546
+ InvestmentModel(
547
+ model=self._model,
548
+ label_of_element=self.label_of_element,
549
+ parameters=self.element.size,
550
+ label_of_model=self.label_of_element,
384
551
  ),
385
- 'total_flow_hours',
552
+ 'investment',
386
553
  )
387
554
 
388
- # Load factor
389
- self._create_bounds_for_load_factor()
555
+ def _constraint_flow_rate(self):
556
+ if not self.with_investment and not self.with_on_off:
557
+ # Most basic case. Already covered by direct variable bounds
558
+ pass
559
+
560
+ elif self.with_on_off and not self.with_investment:
561
+ # OnOff, but no Investment
562
+ self._create_on_off_model()
563
+ bounds = self.relative_flow_rate_bounds
564
+ BoundingPatterns.bounds_with_state(
565
+ self,
566
+ variable=self.flow_rate,
567
+ bounds=(bounds[0] * self.element.size, bounds[1] * self.element.size),
568
+ variable_state=self.on_off.on,
569
+ )
390
570
 
391
- # Shares
392
- self._create_shares()
571
+ elif self.with_investment and not self.with_on_off:
572
+ # Investment, but no OnOff
573
+ self._create_investment_model()
574
+ BoundingPatterns.scaled_bounds(
575
+ self,
576
+ variable=self.flow_rate,
577
+ scaling_variable=self.investment.size,
578
+ relative_bounds=self.relative_flow_rate_bounds,
579
+ )
580
+
581
+ elif self.with_investment and self.with_on_off:
582
+ # Investment and OnOff
583
+ self._create_investment_model()
584
+ self._create_on_off_model()
585
+
586
+ BoundingPatterns.scaled_bounds_with_state(
587
+ model=self,
588
+ variable=self.flow_rate,
589
+ scaling_variable=self._investment.size,
590
+ relative_bounds=self.relative_flow_rate_bounds,
591
+ scaling_bounds=(self.element.size.minimum_or_fixed_size, self.element.size.maximum_or_fixed_size),
592
+ variable_state=self.on_off.on,
593
+ )
594
+ else:
595
+ raise Exception('Not valid')
596
+
597
+ @property
598
+ def with_on_off(self) -> bool:
599
+ return self.element.on_off_parameters is not None
600
+
601
+ @property
602
+ def with_investment(self) -> bool:
603
+ return isinstance(self.element.size, InvestParameters)
604
+
605
+ # Properties for clean access to variables
606
+ @property
607
+ def flow_rate(self) -> linopy.Variable:
608
+ """Main flow rate variable"""
609
+ return self['flow_rate']
610
+
611
+ @property
612
+ def total_flow_hours(self) -> linopy.Variable:
613
+ """Total flow hours variable"""
614
+ return self['total_flow_hours']
615
+
616
+ def results_structure(self):
617
+ return {
618
+ **super().results_structure(),
619
+ 'start': self.element.bus if self.element.is_input_in_component else self.element.component,
620
+ 'end': self.element.component if self.element.is_input_in_component else self.element.bus,
621
+ 'component': self.element.component,
622
+ }
393
623
 
394
624
  def _create_shares(self):
395
- # Arbeitskosten:
396
- if self.element.effects_per_flow_hour != {}:
625
+ # Effects per flow hour
626
+ if self.element.effects_per_flow_hour:
397
627
  self._model.effects.add_share_to_effects(
398
- name=self.label_full, # Use the full label of the element
628
+ name=self.label_full,
399
629
  expressions={
400
- effect: self.flow_rate * self._model.hours_per_step * factor.active_data
630
+ effect: self.flow_rate * self._model.hours_per_step * factor
401
631
  for effect, factor in self.element.effects_per_flow_hour.items()
402
632
  },
403
- target='operation',
404
- )
405
-
406
- if self.element.piecewise_effects_per_flow_hour is not None:
407
- self.piecewise_effects = self.add(
408
- PiecewiseEffectsPerFlowHourModel(
409
- model=self._model,
410
- label_of_element=self.label_of_element,
411
- piecewise_origin=(
412
- self.flow_rate.name,
413
- self.element.piecewise_effects_per_flow_hour.piecewise_flow_rate,
414
- ),
415
- piecewise_shares=self.element.piecewise_effects_per_flow_hour.piecewise_shares,
416
- zero_point=self.on_off.on if self.on_off is not None else False,
417
- ),
633
+ target='temporal',
418
634
  )
419
- self.piecewise_effects.do_modeling()
420
635
 
421
636
  def _create_bounds_for_load_factor(self):
422
- # TODO: Add Variable load_factor for better evaluation?
637
+ """Create load factor constraints using current approach"""
638
+ # Get the size (either from element or investment)
639
+ size = self.investment.size if self.with_investment else self.element.size
423
640
 
424
- # eq: var_sumFlowHours <= size * dt_tot * load_factor_max
641
+ # Maximum load factor constraint
425
642
  if self.element.load_factor_max is not None:
426
- name_short = 'load_factor_max'
427
- flow_hours_per_size_max = self._model.hours_per_step.sum() * self.element.load_factor_max
428
- size = self.element.size if self._investment is None else self._investment.size
429
-
430
- self.add(
431
- self._model.add_constraints(
432
- self.total_flow_hours <= size * flow_hours_per_size_max,
433
- name=f'{self.label_full}|{name_short}',
434
- ),
435
- name_short,
643
+ flow_hours_per_size_max = self._model.hours_per_step.sum('time') * self.element.load_factor_max
644
+ self.add_constraints(
645
+ self.total_flow_hours <= size * flow_hours_per_size_max,
646
+ short_name='load_factor_max',
436
647
  )
437
648
 
438
- # eq: size * sum(dt)* load_factor_min <= var_sumFlowHours
649
+ # Minimum load factor constraint
439
650
  if self.element.load_factor_min is not None:
440
- name_short = 'load_factor_min'
441
- flow_hours_per_size_min = self._model.hours_per_step.sum() * self.element.load_factor_min
442
- size = self.element.size if self._investment is None else self._investment.size
443
-
444
- self.add(
445
- self._model.add_constraints(
446
- self.total_flow_hours >= size * flow_hours_per_size_min,
447
- name=f'{self.label_full}|{name_short}',
448
- ),
449
- name_short,
651
+ flow_hours_per_size_min = self._model.hours_per_step.sum('time') * self.element.load_factor_min
652
+ self.add_constraints(
653
+ self.total_flow_hours >= size * flow_hours_per_size_min,
654
+ short_name='load_factor_min',
450
655
  )
451
656
 
452
657
  @property
453
- def flow_rate_bounds_on(self) -> Tuple[NumericData, NumericData]:
454
- """Returns absolute flow rate bounds. Important for OnOffModel"""
455
- relative_minimum, relative_maximum = self.flow_rate_lower_bound_relative, self.flow_rate_upper_bound_relative
456
- size = self.element.size
457
- if not isinstance(size, InvestParameters):
458
- return relative_minimum * size, relative_maximum * size
459
- if size.fixed_size is not None:
460
- return relative_minimum * size.fixed_size, relative_maximum * size.fixed_size
461
- return relative_minimum * size.minimum_size, relative_maximum * size.maximum_size
658
+ def relative_flow_rate_bounds(self) -> tuple[TemporalData, TemporalData]:
659
+ if self.element.fixed_relative_profile is not None:
660
+ return self.element.fixed_relative_profile, self.element.fixed_relative_profile
661
+ return self.element.relative_minimum, self.element.relative_maximum
462
662
 
463
663
  @property
464
- def flow_rate_lower_bound_relative(self) -> NumericData:
465
- """Returns the lower bound of the flow_rate relative to its size"""
466
- fixed_profile = self.element.fixed_relative_profile
467
- if fixed_profile is None:
468
- return self.element.relative_minimum.active_data
469
- return fixed_profile.active_data
664
+ def absolute_flow_rate_bounds(self) -> tuple[TemporalData, TemporalData]:
665
+ """
666
+ Returns the absolute bounds the flow_rate can reach.
667
+ Further constraining might be needed
668
+ """
669
+ lb_relative, ub_relative = self.relative_flow_rate_bounds
670
+
671
+ lb = 0
672
+ if not self.with_on_off:
673
+ if not self.with_investment:
674
+ # Basic case without investment and without OnOff
675
+ lb = lb_relative * self.element.size
676
+ elif self.with_investment and self.element.size.mandatory:
677
+ # With mandatory Investment
678
+ lb = lb_relative * self.element.size.minimum_or_fixed_size
679
+
680
+ if self.with_investment:
681
+ ub = ub_relative * self.element.size.maximum_or_fixed_size
682
+ else:
683
+ ub = ub_relative * self.element.size
684
+
685
+ return lb, ub
470
686
 
471
687
  @property
472
- def flow_rate_upper_bound_relative(self) -> NumericData:
473
- """Returns the upper bound of the flow_rate relative to its size"""
474
- fixed_profile = self.element.fixed_relative_profile
475
- if fixed_profile is None:
476
- return self.element.relative_maximum.active_data
477
- return fixed_profile.active_data
688
+ def on_off(self) -> OnOffModel | None:
689
+ """OnOff feature"""
690
+ if 'on_off' not in self.submodels:
691
+ return None
692
+ return self.submodels['on_off']
478
693
 
479
694
  @property
480
- def flow_rate_lower_bound(self) -> NumericData:
481
- """
482
- Returns the minimum bound the flow_rate can reach.
483
- Further constraining might be done in OnOffModel and InvestmentModel
484
- """
485
- if self.element.on_off_parameters is not None:
486
- return 0
487
- if isinstance(self.element.size, InvestParameters):
488
- if self.element.size.optional:
489
- return 0
490
- return self.flow_rate_lower_bound_relative * self.element.size.minimum_size
491
- return self.flow_rate_lower_bound_relative * self.element.size
695
+ def _investment(self) -> InvestmentModel | None:
696
+ """Deprecated alias for investment"""
697
+ return self.investment
492
698
 
493
699
  @property
494
- def flow_rate_upper_bound(self) -> NumericData:
495
- """
496
- Returns the maximum bound the flow_rate can reach.
497
- Further constraining might be done in OnOffModel and InvestmentModel
498
- """
499
- if isinstance(self.element.size, InvestParameters):
500
- return self.flow_rate_upper_bound_relative * self.element.size.maximum_size
501
- return self.flow_rate_upper_bound_relative * self.element.size
700
+ def investment(self) -> InvestmentModel | None:
701
+ """OnOff feature"""
702
+ if 'investment' not in self.submodels:
703
+ return None
704
+ return self.submodels['investment']
705
+
706
+ @property
707
+ def previous_states(self) -> TemporalData | None:
708
+ """Previous states of the flow rate"""
709
+ # TODO: This would be nicer to handle in the Flow itself, and allow DataArrays as well.
710
+ previous_flow_rate = self.element.previous_flow_rate
711
+ if previous_flow_rate is None:
712
+ return None
713
+
714
+ return ModelingUtilitiesAbstract.to_binary(
715
+ values=xr.DataArray(
716
+ [previous_flow_rate] if np.isscalar(previous_flow_rate) else previous_flow_rate, dims='time'
717
+ ),
718
+ epsilon=CONFIG.Modeling.epsilon,
719
+ dims='time',
720
+ )
502
721
 
503
722
 
504
723
  class BusModel(ElementModel):
505
- def __init__(self, model: SystemModel, element: Bus):
724
+ element: Bus # Type hint
725
+
726
+ def __init__(self, model: FlowSystemModel, element: Bus):
727
+ self.excess_input: linopy.Variable | None = None
728
+ self.excess_output: linopy.Variable | None = None
506
729
  super().__init__(model, element)
507
- self.element: Bus = element
508
- self.excess_input: Optional[linopy.Variable] = None
509
- self.excess_output: Optional[linopy.Variable] = None
510
730
 
511
- def do_modeling(self) -> None:
731
+ def _do_modeling(self) -> None:
732
+ super()._do_modeling()
512
733
  # inputs == outputs
513
734
  for flow in self.element.inputs + self.element.outputs:
514
- self.add(flow.model.flow_rate, flow.label_full)
515
- inputs = sum([flow.model.flow_rate for flow in self.element.inputs])
516
- outputs = sum([flow.model.flow_rate for flow in self.element.outputs])
517
- eq_bus_balance = self.add(self._model.add_constraints(inputs == outputs, name=f'{self.label_full}|balance'))
735
+ self.register_variable(flow.submodel.flow_rate, flow.label_full)
736
+ inputs = sum([flow.submodel.flow_rate for flow in self.element.inputs])
737
+ outputs = sum([flow.submodel.flow_rate for flow in self.element.outputs])
738
+ eq_bus_balance = self.add_constraints(inputs == outputs, short_name='balance')
518
739
 
519
740
  # Fehlerplus/-minus:
520
741
  if self.element.with_excess:
521
- excess_penalty = np.multiply(
522
- self._model.hours_per_step, self.element.excess_penalty_per_flow_hour.active_data
523
- )
524
- self.excess_input = self.add(
525
- self._model.add_variables(lower=0, coords=self._model.coords, name=f'{self.label_full}|excess_input'),
526
- 'excess_input',
527
- )
528
- self.excess_output = self.add(
529
- self._model.add_variables(lower=0, coords=self._model.coords, name=f'{self.label_full}|excess_output'),
530
- 'excess_output',
742
+ excess_penalty = np.multiply(self._model.hours_per_step, self.element.excess_penalty_per_flow_hour)
743
+
744
+ self.excess_input = self.add_variables(lower=0, coords=self._model.get_coords(), short_name='excess_input')
745
+
746
+ self.excess_output = self.add_variables(
747
+ lower=0, coords=self._model.get_coords(), short_name='excess_output'
531
748
  )
749
+
532
750
  eq_bus_balance.lhs -= -self.excess_input + self.excess_output
533
751
 
534
752
  self._model.effects.add_share_to_penalty(self.label_of_element, (self.excess_input * excess_penalty).sum())
535
753
  self._model.effects.add_share_to_penalty(self.label_of_element, (self.excess_output * excess_penalty).sum())
536
754
 
537
755
  def results_structure(self):
538
- inputs = [flow.model.flow_rate.name for flow in self.element.inputs]
539
- outputs = [flow.model.flow_rate.name for flow in self.element.outputs]
756
+ inputs = [flow.submodel.flow_rate.name for flow in self.element.inputs]
757
+ outputs = [flow.submodel.flow_rate.name for flow in self.element.outputs]
540
758
  if self.excess_input is not None:
541
759
  inputs.append(self.excess_input.name)
542
760
  if self.excess_output is not None:
543
761
  outputs.append(self.excess_output.name)
544
- return {**super().results_structure(), 'inputs': inputs, 'outputs': outputs}
762
+ return {
763
+ **super().results_structure(),
764
+ 'inputs': inputs,
765
+ 'outputs': outputs,
766
+ 'flows': [flow.label_full for flow in self.element.inputs + self.element.outputs],
767
+ }
545
768
 
546
769
 
547
770
  class ComponentModel(ElementModel):
548
- def __init__(self, model: SystemModel, element: Component):
771
+ element: Component # Type hint
772
+
773
+ def __init__(self, model: FlowSystemModel, element: Component):
774
+ self.on_off: OnOffModel | None = None
549
775
  super().__init__(model, element)
550
- self.element: Component = element
551
- self.on_off: Optional[OnOffModel] = None
552
776
 
553
- def do_modeling(self):
777
+ def _do_modeling(self):
554
778
  """Initiates all FlowModels"""
779
+ super()._do_modeling()
555
780
  all_flows = self.element.inputs + self.element.outputs
556
781
  if self.element.on_off_parameters:
557
782
  for flow in all_flows:
@@ -564,34 +789,64 @@ class ComponentModel(ElementModel):
564
789
  flow.on_off_parameters = OnOffParameters()
565
790
 
566
791
  for flow in all_flows:
567
- self.add(flow.create_model(self._model), flow.label)
568
-
569
- for sub_model in self.sub_models:
570
- sub_model.do_modeling()
792
+ self.add_submodels(flow.create_model(self._model), short_name=flow.label)
571
793
 
572
794
  if self.element.on_off_parameters:
573
- self.on_off = self.add(
574
- OnOffModel(
575
- self._model,
576
- self.element.on_off_parameters,
577
- self.label_of_element,
578
- defining_variables=[flow.model.flow_rate for flow in all_flows],
579
- defining_bounds=[flow.model.flow_rate_bounds_on for flow in all_flows],
580
- previous_values=[flow.previous_flow_rate for flow in all_flows],
795
+ on = self.add_variables(binary=True, short_name='on', coords=self._model.get_coords())
796
+ if len(all_flows) == 1:
797
+ self.add_constraints(on == all_flows[0].submodel.on_off.on, short_name='on')
798
+ else:
799
+ flow_ons = [flow.submodel.on_off.on for flow in all_flows]
800
+ # TODO: Is the EPSILON even necessary?
801
+ self.add_constraints(on <= sum(flow_ons) + CONFIG.Modeling.epsilon, short_name='on|ub')
802
+ self.add_constraints(
803
+ on >= sum(flow_ons) / (len(flow_ons) + CONFIG.Modeling.epsilon), short_name='on|lb'
581
804
  )
582
- )
583
805
 
584
- self.on_off.do_modeling()
806
+ self.on_off = self.add_submodels(
807
+ OnOffModel(
808
+ model=self._model,
809
+ label_of_element=self.label_of_element,
810
+ parameters=self.element.on_off_parameters,
811
+ on_variable=on,
812
+ label_of_model=self.label_of_element,
813
+ previous_states=self.previous_states,
814
+ ),
815
+ short_name='on_off',
816
+ )
585
817
 
586
818
  if self.element.prevent_simultaneous_flows:
587
819
  # Simultanious Useage --> Only One FLow is On at a time, but needs a Binary for every flow
588
- on_variables = [flow.model.on_off.on for flow in self.element.prevent_simultaneous_flows]
589
- simultaneous_use = self.add(PreventSimultaneousUsageModel(self._model, on_variables, self.label_full))
590
- simultaneous_use.do_modeling()
820
+ ModelingPrimitives.mutual_exclusivity_constraint(
821
+ self,
822
+ binary_variables=[flow.submodel.on_off.on for flow in self.element.prevent_simultaneous_flows],
823
+ short_name='prevent_simultaneous_use',
824
+ )
591
825
 
592
826
  def results_structure(self):
593
827
  return {
594
828
  **super().results_structure(),
595
- 'inputs': [flow.model.flow_rate.name for flow in self.element.inputs],
596
- 'outputs': [flow.model.flow_rate.name for flow in self.element.outputs],
829
+ 'inputs': [flow.submodel.flow_rate.name for flow in self.element.inputs],
830
+ 'outputs': [flow.submodel.flow_rate.name for flow in self.element.outputs],
831
+ 'flows': [flow.label_full for flow in self.element.inputs + self.element.outputs],
597
832
  }
833
+
834
+ @property
835
+ def previous_states(self) -> xr.DataArray | None:
836
+ """Previous state of the component, derived from its flows"""
837
+ if self.element.on_off_parameters is None:
838
+ raise ValueError(f'OnOffModel not present in \n{self}\nCant access previous_states')
839
+
840
+ previous_states = [flow.submodel.on_off._previous_states for flow in self.element.inputs + self.element.outputs]
841
+ previous_states = [da for da in previous_states if da is not None]
842
+
843
+ if not previous_states: # Empty list
844
+ return None
845
+
846
+ max_len = max(da.sizes['time'] for da in previous_states)
847
+
848
+ padded_previous_states = [
849
+ da.assign_coords(time=range(-da.sizes['time'], 0)).reindex(time=range(-max_len, 0), fill_value=0)
850
+ for da in previous_states
851
+ ]
852
+ return xr.concat(padded_previous_states, dim='flow').any(dim='flow').astype(int)