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/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 PlausibilityError, Scalar, ScenarioData, TimestepData, extract_data
14
- from .effects import EffectValuesUserTimestep
15
- from .features import InvestmentModel, OnOffModel, PreventSimultaneousUsageModel
15
+ from .core import PlausibilityError, Scalar, TemporalData, TemporalDataUser
16
+ from .features import InvestmentModel, OnOffModel
16
17
  from .interface import InvestParameters, OnOffParameters
17
- from .structure import Element, ElementModel, SystemModel, register_class_for_io
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[TimestepData] = 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,60 +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[ScenarioData, InvestParameters] = None,
158
- fixed_relative_profile: Optional[TimestepData] = None,
159
- relative_minimum: TimestepData = 0,
160
- relative_maximum: TimestepData = 1,
161
- effects_per_flow_hour: Optional[EffectValuesUserTimestep] = None,
162
- on_off_parameters: Optional[OnOffParameters] = None,
163
- flow_hours_total_max: Optional[ScenarioData] = None,
164
- flow_hours_total_min: Optional[ScenarioData] = None,
165
- load_factor_min: Optional[ScenarioData] = None,
166
- load_factor_max: Optional[ScenarioData] = None,
167
- previous_flow_rate: Optional[ScenarioData] = None,
168
- 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,
169
377
  ):
170
- r"""
171
- Args:
172
- 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.
173
- bus: blabel of the bus the flow is connected to.
174
- size: size of the flow. If InvestmentParameters is used, size is optimized.
175
- If size is None, a default value is used.
176
- relative_minimum: min value is relative_minimum multiplied by size
177
- relative_maximum: max value is relative_maximum multiplied by size. If size = max then relative_maximum=1
178
- load_factor_min: minimal load factor general: avg Flow per nominalVal/investSize
179
- (e.g. boiler, kW/kWh=h; solarthermal: kW/m²;
180
- def: :math:`load\_factor:= sumFlowHours/ (nominal\_val \cdot \Delta t_{tot})`
181
- load_factor_max: maximal load factor (see minimal load factor)
182
- effects_per_flow_hour: operational costs, costs per flow-"work"
183
- on_off_parameters: If present, flow can be "off", i.e. be zero (only relevant if relative_minimum > 0)
184
- Therefore a binary var "on" is used. Further, several other restrictions and effects can be modeled
185
- through this On/Off State (See OnOffParameters)
186
- flow_hours_total_max: maximum flow-hours ("flow-work")
187
- (if size is not const, maybe load_factor_max is the better choice!)
188
- flow_hours_total_min: minimum flow-hours ("flow-work")
189
- (if size is not predefined, maybe load_factor_min is the better choice!)
190
- fixed_relative_profile: fixed relative values for flow (if given).
191
- flow_rate(t) := fixed_relative_profile(t) * size(t)
192
- With this value, the flow_rate is no optimization-variable anymore.
193
- (relative_minimum and relative_maximum are ignored)
194
- used for fixed load or supply profiles, i.g. heat demand, wind-power, solarthermal
195
- If the load-profile is just an upper limit, use relative_maximum instead.
196
- previous_flow_rate: previous flow rate of the flow. Used to determine if and how long the
197
- flow is already on / off. If None, the flow is considered to be off for one timestep.
198
- meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
199
- """
200
378
  super().__init__(label, meta_data=meta_data)
201
- self.size = size or CONFIG.modeling.BIG # Default size
379
+ self.size = CONFIG.Modeling.big if size is None else size
202
380
  self.relative_minimum = relative_minimum
203
381
  self.relative_maximum = relative_maximum
204
382
  self.fixed_relative_profile = fixed_relative_profile
@@ -211,12 +389,10 @@ class Flow(Element):
211
389
  self.flow_hours_total_min = flow_hours_total_min
212
390
  self.on_off_parameters = on_off_parameters
213
391
 
214
- self.previous_flow_rate = (
215
- previous_flow_rate if not isinstance(previous_flow_rate, list) else np.array(previous_flow_rate)
216
- )
392
+ self.previous_flow_rate = previous_flow_rate
217
393
 
218
394
  self.component: str = 'UnknownComponent'
219
- self.is_input_in_component: Optional[bool] = None
395
+ self.is_input_in_component: bool | None = None
220
396
  if isinstance(bus, Bus):
221
397
  self.bus = bus.label_full
222
398
  warnings.warn(
@@ -230,82 +406,80 @@ class Flow(Element):
230
406
  self.bus = bus
231
407
  self._bus_object = None
232
408
 
233
- def create_model(self, model: SystemModel) -> 'FlowModel':
409
+ def create_model(self, model: FlowSystemModel) -> FlowModel:
234
410
  self._plausibility_checks()
235
- self.model = FlowModel(model, self)
236
- return self.model
237
-
238
- def transform_data(self, flow_system: 'FlowSystem'):
239
- self.relative_minimum = flow_system.create_time_series(
240
- f'{self.label_full}|relative_minimum', self.relative_minimum
241
- )
242
- self.relative_maximum = flow_system.create_time_series(
243
- f'{self.label_full}|relative_maximum', self.relative_maximum
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
244
420
  )
245
- self.fixed_relative_profile = flow_system.create_time_series(
246
- f'{self.label_full}|fixed_relative_profile', self.fixed_relative_profile
421
+ self.effects_per_flow_hour = flow_system.fit_effects_to_model_coords(
422
+ prefix, self.effects_per_flow_hour, 'per_flow_hour'
247
423
  )
248
- self.effects_per_flow_hour = flow_system.create_effect_time_series(
249
- self.label_full, self.effects_per_flow_hour, 'per_flow_hour'
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']
250
426
  )
251
- self.flow_hours_total_max = flow_system.create_time_series(
252
- f'{self.label_full}|flow_hours_total_max', self.flow_hours_total_max, has_time_dim=False
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']
253
429
  )
254
- self.flow_hours_total_min = flow_system.create_time_series(
255
- f'{self.label_full}|flow_hours_total_min', self.flow_hours_total_min, has_time_dim=False
430
+ self.load_factor_max = flow_system.fit_to_model_coords(
431
+ f'{prefix}|load_factor_max', self.load_factor_max, dims=['period', 'scenario']
256
432
  )
257
- self.load_factor_max = flow_system.create_time_series(
258
- f'{self.label_full}|load_factor_max', self.load_factor_max, has_time_dim=False
259
- )
260
- self.load_factor_min = flow_system.create_time_series(
261
- f'{self.label_full}|load_factor_min', self.load_factor_min, has_time_dim=False
433
+ self.load_factor_min = flow_system.fit_to_model_coords(
434
+ f'{prefix}|load_factor_min', self.load_factor_min, dims=['period', 'scenario']
262
435
  )
263
436
 
264
437
  if self.on_off_parameters is not None:
265
- self.on_off_parameters.transform_data(flow_system, self.label_full)
438
+ self.on_off_parameters.transform_data(flow_system, prefix)
266
439
  if isinstance(self.size, InvestParameters):
267
- self.size.transform_data(flow_system, self.label_full)
440
+ self.size.transform_data(flow_system, prefix)
268
441
  else:
269
- self.size = flow_system.create_time_series(f'{self.label_full}|size', self.size, has_time_dim=False)
270
-
271
- def infos(self, use_numpy: bool = True, use_element_label: bool = False) -> Dict:
272
- infos = super().infos(use_numpy, use_element_label)
273
- infos['is_input_in_component'] = self.is_input_in_component
274
- return infos
275
-
276
- def to_dict(self) -> Dict:
277
- data = super().to_dict()
278
- if isinstance(data.get('previous_flow_rate'), np.ndarray):
279
- data['previous_flow_rate'] = data['previous_flow_rate'].tolist()
280
- return data
442
+ self.size = flow_system.fit_to_model_coords(f'{prefix}|size', self.size, dims=['period', 'scenario'])
281
443
 
282
444
  def _plausibility_checks(self) -> None:
283
445
  # TODO: Incorporate into Variable? (Lower_bound can not be greater than upper bound
284
- if np.any(self.relative_minimum > self.relative_maximum):
446
+ if (self.relative_minimum > self.relative_maximum).any():
285
447
  raise PlausibilityError(self.label_full + ': Take care, that relative_minimum <= relative_maximum!')
286
448
 
287
449
  if not isinstance(self.size, InvestParameters) and (
288
- np.any(self.size == CONFIG.modeling.BIG) and self.fixed_relative_profile is not None
450
+ np.any(self.size == CONFIG.Modeling.big) and self.fixed_relative_profile is not None
289
451
  ): # Default Size --> Most likely by accident
290
452
  logger.warning(
291
- f'Flow "{self.label}" has no size assigned, but a "fixed_relative_profile". '
292
- 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", '
293
455
  f'the resulting flow_rate will be very high. To fix this, assign a size to the Flow {self}.'
294
456
  )
295
457
 
296
458
  if self.fixed_relative_profile is not None and self.on_off_parameters is not None:
297
459
  logger.warning(
298
- f'Flow {self.label} has both a fixed_relative_profile and an on_off_parameters.'
460
+ f'Flow {self.label_full} has both a fixed_relative_profile and an on_off_parameters.'
299
461
  f'This will allow the flow to be switched on and off, effectively differing from the fixed_flow_rate.'
300
462
  )
301
463
 
302
- 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:
303
465
  logger.warning(
304
- f'Flow {self.label} has a relative_minimum of {self.relative_minimum.selected_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. '
305
467
  f'This prevents the flow_rate from switching off (flow_rate = 0). '
306
468
  f'Consider using on_off_parameters to allow the flow to be switched on and off.'
307
469
  )
308
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
+
309
483
  @property
310
484
  def label_full(self) -> str:
311
485
  return f'{self.component}({self.label})'
@@ -315,88 +489,129 @@ class Flow(Element):
315
489
  # Wenn kein InvestParameters existiert --> True; Wenn Investparameter, den Wert davon nehmen
316
490
  return False if (isinstance(self.size, InvestParameters) and self.size.fixed_size is None) else True
317
491
 
318
- @property
319
- def invest_is_optional(self) -> bool:
320
- # Wenn kein InvestParameters existiert: # Investment ist nicht optional -> Keine Variable --> False
321
- return False if (isinstance(self.size, InvestParameters) and not self.size.optional) else True
322
-
323
492
 
324
493
  class FlowModel(ElementModel):
325
- def __init__(self, model: SystemModel, element: Flow):
494
+ element: Flow # Type hint
495
+
496
+ def __init__(self, model: FlowSystemModel, element: Flow):
326
497
  super().__init__(model, element)
327
- self.element: Flow = element
328
- self.flow_rate: Optional[linopy.Variable] = None
329
- self.total_flow_hours: Optional[linopy.Variable] = None
330
-
331
- self.on_off: Optional[OnOffModel] = None
332
- self._investment: Optional[InvestmentModel] = None
333
-
334
- def do_modeling(self):
335
- # eq relative_minimum(t) * size <= flow_rate(t) <= relative_maximum(t) * size
336
- self.flow_rate: linopy.Variable = self.add(
337
- self._model.add_variables(
338
- lower=self.flow_rate_lower_bound,
339
- upper=self.flow_rate_upper_bound,
340
- coords=self._model.get_coords(),
341
- 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,
342
519
  ),
343
- 'flow_rate',
520
+ coords=['period', 'scenario'],
521
+ short_name='total_flow_hours',
344
522
  )
345
523
 
346
- # OnOff
347
- if self.element.on_off_parameters is not None:
348
- self.on_off: OnOffModel = self.add(
349
- OnOffModel(
350
- model=self._model,
351
- label_of_element=self.label_of_element,
352
- on_off_parameters=self.element.on_off_parameters,
353
- defining_variables=[self.flow_rate],
354
- defining_bounds=[self.flow_rate_bounds_on],
355
- previous_values=[self.element.previous_flow_rate],
356
- ),
357
- 'on_off',
358
- )
359
- self.on_off.do_modeling()
524
+ # Load factor constraints
525
+ self._create_bounds_for_load_factor()
360
526
 
361
- # Investment
362
- if isinstance(self.element.size, InvestParameters):
363
- self._investment: InvestmentModel = self.add(
364
- InvestmentModel(
365
- model=self._model,
366
- label_of_element=self.label_of_element,
367
- parameters=self.element.size,
368
- defining_variable=self.flow_rate,
369
- relative_bounds_of_defining_variable=(self.flow_rate_lower_bound_relative,
370
- self.flow_rate_upper_bound_relative),
371
- on_variable=self.on_off.on if self.on_off is not None else None,
372
- ),
373
- 'investment',
374
- )
375
- self._investment.do_modeling()
376
-
377
- self.total_flow_hours = self.add(
378
- self._model.add_variables(
379
- lower=extract_data(self.element.flow_hours_total_min, 0),
380
- upper=extract_data(self.element.flow_hours_total_max, np.inf),
381
- coords=self._model.get_coords(time_dim=False),
382
- 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,
383
540
  ),
384
- 'total_flow_hours',
541
+ short_name='on_off',
385
542
  )
386
543
 
387
- self.add(
388
- self._model.add_constraints(
389
- self.total_flow_hours == (self.flow_rate * self._model.hours_per_step).sum('time'),
390
- 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,
391
551
  ),
392
- 'total_flow_hours',
552
+ 'investment',
393
553
  )
394
554
 
395
- # Load factor
396
- 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
+ )
397
570
 
398
- # Shares
399
- 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']
400
615
 
401
616
  def results_structure(self):
402
617
  return {
@@ -407,158 +622,161 @@ class FlowModel(ElementModel):
407
622
  }
408
623
 
409
624
  def _create_shares(self):
410
- # Arbeitskosten:
411
- if self.element.effects_per_flow_hour != {}:
625
+ # Effects per flow hour
626
+ if self.element.effects_per_flow_hour:
412
627
  self._model.effects.add_share_to_effects(
413
- name=self.label_full, # Use the full label of the element
628
+ name=self.label_full,
414
629
  expressions={
415
- effect: self.flow_rate * self._model.hours_per_step * factor.selected_data
630
+ effect: self.flow_rate * self._model.hours_per_step * factor
416
631
  for effect, factor in self.element.effects_per_flow_hour.items()
417
632
  },
418
- target='operation',
633
+ target='temporal',
419
634
  )
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
643
  flow_hours_per_size_max = self._model.hours_per_step.sum('time') * 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,
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
651
  flow_hours_per_size_min = self._model.hours_per_step.sum('time') * 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,
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[TimestepData, TimestepData]:
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 * extract_data(size), relative_maximum * extract_data(size)
459
- if size.fixed_size is not None:
460
- return (relative_minimum * extract_data(size.fixed_size),
461
- relative_maximum * extract_data(size.fixed_size))
462
- return (relative_minimum * extract_data(size.minimum_size),
463
- relative_maximum * extract_data(size.maximum_size))
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
464
662
 
465
663
  @property
466
- def flow_rate_lower_bound_relative(self) -> TimestepData:
467
- """Returns the lower bound of the flow_rate relative to its size"""
468
- fixed_profile = self.element.fixed_relative_profile
469
- if fixed_profile is None:
470
- return extract_data(self.element.relative_minimum)
471
- return extract_data(fixed_profile)
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
472
686
 
473
687
  @property
474
- def flow_rate_upper_bound_relative(self) -> TimestepData:
475
- """ Returns the upper bound of the flow_rate relative to its size"""
476
- fixed_profile = self.element.fixed_relative_profile
477
- if fixed_profile is None:
478
- return extract_data(self.element.relative_maximum)
479
- return extract_data(fixed_profile)
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']
480
693
 
481
694
  @property
482
- def flow_rate_lower_bound(self) -> TimestepData:
483
- """
484
- Returns the minimum bound the flow_rate can reach.
485
- Further constraining might be done in OnOffModel and InvestmentModel
486
- """
487
- if self.element.on_off_parameters is not None:
488
- return 0
489
- if isinstance(self.element.size, InvestParameters):
490
- if self.element.size.optional:
491
- return 0
492
- return self.flow_rate_lower_bound_relative * extract_data(self.element.size.minimum_size)
493
- return self.flow_rate_lower_bound_relative * extract_data(self.element.size)
695
+ def _investment(self) -> InvestmentModel | None:
696
+ """Deprecated alias for investment"""
697
+ return self.investment
494
698
 
495
699
  @property
496
- def flow_rate_upper_bound(self) -> TimestepData:
497
- """
498
- Returns the maximum bound the flow_rate can reach.
499
- Further constraining might be done in OnOffModel and InvestmentModel
500
- """
501
- if isinstance(self.element.size, InvestParameters):
502
- return self.flow_rate_upper_bound_relative * extract_data(self.element.size.maximum_size)
503
- return self.flow_rate_upper_bound_relative * extract_data(self.element.size)
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
+ )
504
721
 
505
722
 
506
723
  class BusModel(ElementModel):
507
- 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
508
729
  super().__init__(model, element)
509
- self.element: Bus = element
510
- self.excess_input: Optional[linopy.Variable] = None
511
- self.excess_output: Optional[linopy.Variable] = None
512
730
 
513
- def do_modeling(self) -> None:
731
+ def _do_modeling(self) -> None:
732
+ super()._do_modeling()
514
733
  # inputs == outputs
515
734
  for flow in self.element.inputs + self.element.outputs:
516
- self.add(flow.model.flow_rate, flow.label_full)
517
- inputs = sum([flow.model.flow_rate for flow in self.element.inputs])
518
- outputs = sum([flow.model.flow_rate for flow in self.element.outputs])
519
- 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')
520
739
 
521
740
  # Fehlerplus/-minus:
522
741
  if self.element.with_excess:
523
- excess_penalty = np.multiply(
524
- self._model.hours_per_step, self.element.excess_penalty_per_flow_hour.selected_data
525
- )
526
- self.excess_input = self.add(
527
- self._model.add_variables(
528
- lower=0, coords=self._model.get_coords(), name=f'{self.label_full}|excess_input'
529
- ),
530
- 'excess_input',
531
- )
532
- self.excess_output = self.add(
533
- self._model.add_variables(
534
- lower=0, coords=self._model.get_coords(), name=f'{self.label_full}|excess_output'
535
- ),
536
- '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'
537
748
  )
749
+
538
750
  eq_bus_balance.lhs -= -self.excess_input + self.excess_output
539
751
 
540
752
  self._model.effects.add_share_to_penalty(self.label_of_element, (self.excess_input * excess_penalty).sum())
541
753
  self._model.effects.add_share_to_penalty(self.label_of_element, (self.excess_output * excess_penalty).sum())
542
754
 
543
755
  def results_structure(self):
544
- inputs = [flow.model.flow_rate.name for flow in self.element.inputs]
545
- 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]
546
758
  if self.excess_input is not None:
547
759
  inputs.append(self.excess_input.name)
548
760
  if self.excess_output is not None:
549
761
  outputs.append(self.excess_output.name)
550
- return {**super().results_structure(), 'inputs': inputs, 'outputs': outputs,
551
- 'flows': [flow.label_full for flow in self.element.inputs + self.element.outputs]}
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
+ }
552
768
 
553
769
 
554
770
  class ComponentModel(ElementModel):
555
- 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
556
775
  super().__init__(model, element)
557
- self.element: Component = element
558
- self.on_off: Optional[OnOffModel] = None
559
776
 
560
- def do_modeling(self):
777
+ def _do_modeling(self):
561
778
  """Initiates all FlowModels"""
779
+ super()._do_modeling()
562
780
  all_flows = self.element.inputs + self.element.outputs
563
781
  if self.element.on_off_parameters:
564
782
  for flow in all_flows:
@@ -571,35 +789,64 @@ class ComponentModel(ElementModel):
571
789
  flow.on_off_parameters = OnOffParameters()
572
790
 
573
791
  for flow in all_flows:
574
- self.add(flow.create_model(self._model), flow.label)
575
-
576
- for sub_model in self.sub_models:
577
- sub_model.do_modeling()
792
+ self.add_submodels(flow.create_model(self._model), short_name=flow.label)
578
793
 
579
794
  if self.element.on_off_parameters:
580
- self.on_off = self.add(
581
- OnOffModel(
582
- self._model,
583
- self.element.on_off_parameters,
584
- self.label_of_element,
585
- defining_variables=[flow.model.flow_rate for flow in all_flows],
586
- defining_bounds=[flow.model.flow_rate_bounds_on for flow in all_flows],
587
- 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'
588
804
  )
589
- )
590
805
 
591
- 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
+ )
592
817
 
593
818
  if self.element.prevent_simultaneous_flows:
594
819
  # Simultanious Useage --> Only One FLow is On at a time, but needs a Binary for every flow
595
- on_variables = [flow.model.on_off.on for flow in self.element.prevent_simultaneous_flows]
596
- simultaneous_use = self.add(PreventSimultaneousUsageModel(self._model, on_variables, self.label_full))
597
- 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
+ )
598
825
 
599
826
  def results_structure(self):
600
827
  return {
601
828
  **super().results_structure(),
602
- 'inputs': [flow.model.flow_rate.name for flow in self.element.inputs],
603
- '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],
604
831
  'flows': [flow.label_full for flow in self.element.inputs + self.element.outputs],
605
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)