flixopt 1.0.12__py3-none-any.whl → 2.0.1__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 (73) hide show
  1. docs/examples/00-Minimal Example.md +5 -0
  2. docs/examples/01-Basic Example.md +5 -0
  3. docs/examples/02-Complex Example.md +10 -0
  4. docs/examples/03-Calculation Modes.md +5 -0
  5. docs/examples/index.md +5 -0
  6. docs/faq/contribute.md +49 -0
  7. docs/faq/index.md +3 -0
  8. docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
  9. docs/images/architecture_flixOpt.png +0 -0
  10. docs/images/flixopt-icon.svg +1 -0
  11. docs/javascripts/mathjax.js +18 -0
  12. docs/release-notes/_template.txt +32 -0
  13. docs/release-notes/index.md +7 -0
  14. docs/release-notes/v2.0.0.md +93 -0
  15. docs/release-notes/v2.0.1.md +12 -0
  16. docs/user-guide/Mathematical Notation/Bus.md +33 -0
  17. docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +132 -0
  18. docs/user-guide/Mathematical Notation/Flow.md +26 -0
  19. docs/user-guide/Mathematical Notation/LinearConverter.md +21 -0
  20. docs/user-guide/Mathematical Notation/Piecewise.md +49 -0
  21. docs/user-guide/Mathematical Notation/Storage.md +44 -0
  22. docs/user-guide/Mathematical Notation/index.md +22 -0
  23. docs/user-guide/Mathematical Notation/others.md +3 -0
  24. docs/user-guide/index.md +124 -0
  25. {flixOpt → flixopt}/__init__.py +5 -2
  26. {flixOpt → flixopt}/aggregation.py +113 -140
  27. flixopt/calculation.py +455 -0
  28. {flixOpt → flixopt}/commons.py +7 -4
  29. flixopt/components.py +630 -0
  30. {flixOpt → flixopt}/config.py +9 -8
  31. {flixOpt → flixopt}/config.yaml +3 -3
  32. flixopt/core.py +970 -0
  33. flixopt/effects.py +386 -0
  34. flixopt/elements.py +534 -0
  35. flixopt/features.py +1042 -0
  36. flixopt/flow_system.py +409 -0
  37. flixopt/interface.py +265 -0
  38. flixopt/io.py +308 -0
  39. flixopt/linear_converters.py +331 -0
  40. flixopt/plotting.py +1340 -0
  41. flixopt/results.py +898 -0
  42. flixopt/solvers.py +77 -0
  43. flixopt/structure.py +630 -0
  44. flixopt/utils.py +62 -0
  45. flixopt-2.0.1.dist-info/METADATA +145 -0
  46. flixopt-2.0.1.dist-info/RECORD +57 -0
  47. {flixopt-1.0.12.dist-info → flixopt-2.0.1.dist-info}/WHEEL +1 -1
  48. flixopt-2.0.1.dist-info/top_level.txt +6 -0
  49. pics/architecture_flixOpt-pre2.0.0.png +0 -0
  50. pics/architecture_flixOpt.png +0 -0
  51. pics/flixopt-icon.svg +1 -0
  52. pics/pics.pptx +0 -0
  53. scripts/gen_ref_pages.py +54 -0
  54. site/release-notes/_template.txt +32 -0
  55. flixOpt/calculation.py +0 -629
  56. flixOpt/components.py +0 -614
  57. flixOpt/core.py +0 -182
  58. flixOpt/effects.py +0 -410
  59. flixOpt/elements.py +0 -489
  60. flixOpt/features.py +0 -942
  61. flixOpt/flow_system.py +0 -351
  62. flixOpt/interface.py +0 -203
  63. flixOpt/linear_converters.py +0 -325
  64. flixOpt/math_modeling.py +0 -1145
  65. flixOpt/plotting.py +0 -712
  66. flixOpt/results.py +0 -563
  67. flixOpt/solvers.py +0 -21
  68. flixOpt/structure.py +0 -733
  69. flixOpt/utils.py +0 -134
  70. flixopt-1.0.12.dist-info/METADATA +0 -174
  71. flixopt-1.0.12.dist-info/RECORD +0 -29
  72. flixopt-1.0.12.dist-info/top_level.txt +0 -3
  73. {flixopt-1.0.12.dist-info → flixopt-2.0.1.dist-info/licenses}/LICENSE +0 -0
flixopt/components.py ADDED
@@ -0,0 +1,630 @@
1
+ """
2
+ This module contains the basic components of the flixopt framework.
3
+ """
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Set, Tuple, Union
7
+
8
+ import linopy
9
+ import numpy as np
10
+
11
+ from . import utils
12
+ from .core import NumericData, NumericDataTS, PlausibilityError, Scalar, TimeSeries
13
+ from .elements import Component, ComponentModel, Flow
14
+ from .features import InvestmentModel, OnOffModel, PiecewiseModel
15
+ from .interface import InvestParameters, OnOffParameters, PiecewiseConversion
16
+ from .structure import SystemModel, register_class_for_io
17
+
18
+ if TYPE_CHECKING:
19
+ from .flow_system import FlowSystem
20
+
21
+ logger = logging.getLogger('flixopt')
22
+
23
+
24
+ @register_class_for_io
25
+ class LinearConverter(Component):
26
+ """
27
+ Converts input-Flows into output-Flows via linear conversion factors
28
+
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ label: str,
34
+ inputs: List[Flow],
35
+ outputs: List[Flow],
36
+ on_off_parameters: OnOffParameters = None,
37
+ conversion_factors: List[Dict[str, NumericDataTS]] = None,
38
+ piecewise_conversion: Optional[PiecewiseConversion] = None,
39
+ meta_data: Optional[Dict] = None,
40
+ ):
41
+ """
42
+ Args:
43
+ label: The label of the Element. Used to identify it in the FlowSystem
44
+ inputs: The input Flows
45
+ outputs: The output Flows
46
+ on_off_parameters: Information about on and off states. See class OnOffParameters.
47
+ conversion_factors: linear relation between flows.
48
+ Either 'conversion_factors' or 'piecewise_conversion' can be used!
49
+ piecewise_conversion: Define a piecewise linear relation between flow rates of different flows.
50
+ Either 'conversion_factors' or 'piecewise_conversion' can be used!
51
+ meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
52
+ """
53
+ super().__init__(label, inputs, outputs, on_off_parameters, meta_data=meta_data)
54
+ self.conversion_factors = conversion_factors or []
55
+ self.piecewise_conversion = piecewise_conversion
56
+
57
+ def create_model(self, model: SystemModel) -> 'LinearConverterModel':
58
+ self._plausibility_checks()
59
+ self.model = LinearConverterModel(model, self)
60
+ return self.model
61
+
62
+ def _plausibility_checks(self) -> None:
63
+ if not self.conversion_factors and not self.piecewise_conversion:
64
+ raise PlausibilityError('Either conversion_factors or piecewise_conversion must be defined!')
65
+ if self.conversion_factors and self.piecewise_conversion:
66
+ raise PlausibilityError('Only one of conversion_factors or piecewise_conversion can be defined, not both!')
67
+
68
+ if self.conversion_factors:
69
+ if self.degrees_of_freedom <= 0:
70
+ raise PlausibilityError(
71
+ f'Too Many conversion_factors_specified. Care that you use less conversion_factors '
72
+ f'then inputs + outputs!! With {len(self.inputs + self.outputs)} inputs and outputs, '
73
+ f'use not more than {len(self.inputs + self.outputs) - 1} conversion_factors!'
74
+ )
75
+
76
+ for conversion_factor in self.conversion_factors:
77
+ for flow in conversion_factor:
78
+ if flow not in self.flows:
79
+ raise PlausibilityError(
80
+ f'{self.label}: Flow {flow} in conversion_factors is not in inputs/outputs'
81
+ )
82
+ if self.piecewise_conversion:
83
+ for flow in self.flows.values():
84
+ if isinstance(flow.size, InvestParameters) and flow.size.fixed_size is None:
85
+ raise PlausibilityError(
86
+ f'piecewise_conversion (in {self.label_full}) and variable size '
87
+ f'(in flow {flow.label_full}) do not make sense together!'
88
+ )
89
+
90
+ def transform_data(self, flow_system: 'FlowSystem'):
91
+ super().transform_data(flow_system)
92
+ if self.conversion_factors:
93
+ self.conversion_factors = self._transform_conversion_factors(flow_system)
94
+ if self.piecewise_conversion:
95
+ self.piecewise_conversion.transform_data(flow_system, f'{self.label_full}|PiecewiseConversion')
96
+
97
+ def _transform_conversion_factors(self, flow_system: 'FlowSystem') -> List[Dict[str, TimeSeries]]:
98
+ """macht alle Faktoren, die nicht TimeSeries sind, zu TimeSeries"""
99
+ list_of_conversion_factors = []
100
+ for idx, conversion_factor in enumerate(self.conversion_factors):
101
+ transformed_dict = {}
102
+ for flow, values in conversion_factor.items():
103
+ # TODO: Might be better to use the label of the component instead of the flow
104
+ transformed_dict[flow] = flow_system.create_time_series(
105
+ f'{self.flows[flow].label_full}|conversion_factor{idx}', values
106
+ )
107
+ list_of_conversion_factors.append(transformed_dict)
108
+ return list_of_conversion_factors
109
+
110
+ @property
111
+ def degrees_of_freedom(self):
112
+ return len(self.inputs + self.outputs) - len(self.conversion_factors)
113
+
114
+
115
+ @register_class_for_io
116
+ class Storage(Component):
117
+ """
118
+ Used to model the storage of energy or material.
119
+ """
120
+
121
+ def __init__(
122
+ self,
123
+ label: str,
124
+ charging: Flow,
125
+ discharging: Flow,
126
+ capacity_in_flow_hours: Union[Scalar, InvestParameters],
127
+ relative_minimum_charge_state: NumericData = 0,
128
+ relative_maximum_charge_state: NumericData = 1,
129
+ initial_charge_state: Union[Scalar, Literal['lastValueOfSim']] = 0,
130
+ minimal_final_charge_state: Optional[Scalar] = None,
131
+ maximal_final_charge_state: Optional[Scalar] = None,
132
+ eta_charge: NumericData = 1,
133
+ eta_discharge: NumericData = 1,
134
+ relative_loss_per_hour: NumericData = 0,
135
+ prevent_simultaneous_charge_and_discharge: bool = True,
136
+ meta_data: Optional[Dict] = None,
137
+ ):
138
+ """
139
+ Storages have one incoming and one outgoing Flow each with an efficiency.
140
+ Further, storages have a `size` and a `charge_state`.
141
+ Similarly to the flow-rate of a Flow, the `size` combined with a relative upper and lower bound
142
+ limits the `charge_state` of the storage.
143
+
144
+ For mathematical details take a look at our online documentation
145
+
146
+ Args:
147
+ label: The label of the Element. Used to identify it in the FlowSystem
148
+ charging: ingoing flow.
149
+ discharging: outgoing flow.
150
+ capacity_in_flow_hours: nominal capacity/size of the storage
151
+ relative_minimum_charge_state: minimum relative charge state. The default is 0.
152
+ relative_maximum_charge_state: maximum relative charge state. The default is 1.
153
+ initial_charge_state: storage charge_state at the beginning. The default is 0.
154
+ minimal_final_charge_state: minimal value of chargeState at the end of timeseries.
155
+ maximal_final_charge_state: maximal value of chargeState at the end of timeseries.
156
+ eta_charge: efficiency factor of charging/loading. The default is 1.
157
+ eta_discharge: efficiency factor of uncharging/unloading. The default is 1.
158
+ relative_loss_per_hour: loss per chargeState-Unit per hour. The default is 0.
159
+ prevent_simultaneous_charge_and_discharge: If True, loading and unloading at the same time is not possible.
160
+ Increases the number of binary variables, but is recommended for easier evaluation. The default is True.
161
+ meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
162
+ """
163
+ # TODO: fixed_relative_chargeState implementieren
164
+ super().__init__(
165
+ label,
166
+ inputs=[charging],
167
+ outputs=[discharging],
168
+ prevent_simultaneous_flows=[charging, discharging] if prevent_simultaneous_charge_and_discharge else None,
169
+ meta_data=meta_data,
170
+ )
171
+
172
+ self.charging = charging
173
+ self.discharging = discharging
174
+ self.capacity_in_flow_hours = capacity_in_flow_hours
175
+ self.relative_minimum_charge_state: NumericDataTS = relative_minimum_charge_state
176
+ self.relative_maximum_charge_state: NumericDataTS = relative_maximum_charge_state
177
+
178
+ self.initial_charge_state = initial_charge_state
179
+ self.minimal_final_charge_state = minimal_final_charge_state
180
+ self.maximal_final_charge_state = maximal_final_charge_state
181
+
182
+ self.eta_charge: NumericDataTS = eta_charge
183
+ self.eta_discharge: NumericDataTS = eta_discharge
184
+ self.relative_loss_per_hour: NumericDataTS = relative_loss_per_hour
185
+ self.prevent_simultaneous_charge_and_discharge = prevent_simultaneous_charge_and_discharge
186
+
187
+ def create_model(self, model: SystemModel) -> 'StorageModel':
188
+ self._plausibility_checks()
189
+ self.model = StorageModel(model, self)
190
+ return self.model
191
+
192
+ def transform_data(self, flow_system: 'FlowSystem') -> None:
193
+ super().transform_data(flow_system)
194
+ self.relative_minimum_charge_state = flow_system.create_time_series(
195
+ f'{self.label_full}|relative_minimum_charge_state',
196
+ self.relative_minimum_charge_state,
197
+ needs_extra_timestep=True,
198
+ )
199
+ self.relative_maximum_charge_state = flow_system.create_time_series(
200
+ f'{self.label_full}|relative_maximum_charge_state',
201
+ self.relative_maximum_charge_state,
202
+ needs_extra_timestep=True,
203
+ )
204
+ self.eta_charge = flow_system.create_time_series(f'{self.label_full}|eta_charge', self.eta_charge)
205
+ self.eta_discharge = flow_system.create_time_series(f'{self.label_full}|eta_discharge', self.eta_discharge)
206
+ self.relative_loss_per_hour = flow_system.create_time_series(
207
+ f'{self.label_full}|relative_loss_per_hour', self.relative_loss_per_hour
208
+ )
209
+ if isinstance(self.capacity_in_flow_hours, InvestParameters):
210
+ self.capacity_in_flow_hours.transform_data(flow_system)
211
+
212
+ def _plausibility_checks(self) -> None:
213
+ """
214
+ Check for infeasible or uncommon combinations of parameters
215
+ """
216
+ if utils.is_number(self.initial_charge_state):
217
+ if isinstance(self.capacity_in_flow_hours, InvestParameters):
218
+ if self.capacity_in_flow_hours.fixed_size is None:
219
+ maximum_capacity = self.capacity_in_flow_hours.maximum_size
220
+ minimum_capacity = self.capacity_in_flow_hours.minimum_size
221
+ else:
222
+ maximum_capacity = self.capacity_in_flow_hours.fixed_size
223
+ minimum_capacity = self.capacity_in_flow_hours.fixed_size
224
+ else:
225
+ maximum_capacity = self.capacity_in_flow_hours
226
+ minimum_capacity = self.capacity_in_flow_hours
227
+
228
+ # initial capacity >= allowed min for maximum_size:
229
+ minimum_inital_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=1)
230
+ # initial capacity <= allowed max for minimum_size:
231
+ maximum_inital_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=1)
232
+
233
+ if self.initial_charge_state > maximum_inital_capacity:
234
+ raise ValueError(
235
+ f'{self.label_full}: {self.initial_charge_state=} '
236
+ f'is above allowed maximum charge_state {maximum_inital_capacity}'
237
+ )
238
+ if self.initial_charge_state < minimum_inital_capacity:
239
+ raise ValueError(
240
+ f'{self.label_full}: {self.initial_charge_state=} '
241
+ f'is below allowed minimum charge_state {minimum_inital_capacity}'
242
+ )
243
+ elif self.initial_charge_state != 'lastValueOfSim':
244
+ raise ValueError(f'{self.label_full}: {self.initial_charge_state=} has an invalid value')
245
+
246
+
247
+ @register_class_for_io
248
+ class Transmission(Component):
249
+ # TODO: automatic on-Value in Flows if loss_abs
250
+ # TODO: loss_abs must be: investment_size * loss_abs_rel!!!
251
+ # TODO: investmentsize only on 1 flow
252
+ # TODO: automatic investArgs for both in-flows (or alternatively both out-flows!)
253
+ # TODO: optional: capacities should be recognised for losses
254
+
255
+ def __init__(
256
+ self,
257
+ label: str,
258
+ in1: Flow,
259
+ out1: Flow,
260
+ in2: Optional[Flow] = None,
261
+ out2: Optional[Flow] = None,
262
+ relative_losses: Optional[NumericDataTS] = None,
263
+ absolute_losses: Optional[NumericDataTS] = None,
264
+ on_off_parameters: OnOffParameters = None,
265
+ prevent_simultaneous_flows_in_both_directions: bool = True,
266
+ meta_data: Optional[Dict] = None,
267
+ ):
268
+ """
269
+ Initializes a Transmission component (Pipe, cable, ...) that models the flows between two sides
270
+ with potential losses.
271
+
272
+ Args:
273
+ label: The label of the Element. Used to identify it in the FlowSystem
274
+ in1: The inflow at side A. Pass InvestmentParameters here.
275
+ out1: The outflow at side B.
276
+ in2: The optional inflow at side B.
277
+ If in1 got InvestParameters, the size of this Flow will be equal to in1 (with no extra effects!)
278
+ out2: The optional outflow at side A.
279
+ relative_losses: The relative loss between inflow and outflow, e.g., 0.02 for 2% loss.
280
+ absolute_losses: The absolute loss, occur only when the Flow is on. Induces the creation of the ON-Variable
281
+ on_off_parameters: Parameters defining the on/off behavior of the component.
282
+ prevent_simultaneous_flows_in_both_directions: If True, inflow and outflow are not allowed to be both non-zero at same timestep.
283
+ meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
284
+ """
285
+ super().__init__(
286
+ label,
287
+ inputs=[flow for flow in (in1, in2) if flow is not None],
288
+ outputs=[flow for flow in (out1, out2) if flow is not None],
289
+ on_off_parameters=on_off_parameters,
290
+ prevent_simultaneous_flows=None
291
+ if in2 is None or prevent_simultaneous_flows_in_both_directions is False
292
+ else [in1, in2],
293
+ meta_data=meta_data,
294
+ )
295
+ self.in1 = in1
296
+ self.out1 = out1
297
+ self.in2 = in2
298
+ self.out2 = out2
299
+
300
+ self.relative_losses = relative_losses
301
+ self.absolute_losses = absolute_losses
302
+
303
+ def _plausibility_checks(self):
304
+ # check buses:
305
+ if self.in2 is not None:
306
+ assert self.in2.bus == self.out1.bus, (
307
+ f'Output 1 and Input 2 do not start/end at the same Bus: {self.out1.bus=}, {self.in2.bus=}'
308
+ )
309
+ if self.out2 is not None:
310
+ assert self.out2.bus == self.in1.bus, (
311
+ f'Input 1 and Output 2 do not start/end at the same Bus: {self.in1.bus=}, {self.out2.bus=}'
312
+ )
313
+ # Check Investments
314
+ for flow in [self.out1, self.in2, self.out2]:
315
+ if flow is not None and isinstance(flow.size, InvestParameters):
316
+ raise ValueError(
317
+ 'Transmission currently does not support separate InvestParameters for Flows. '
318
+ 'Please use Flow in1. The size of in2 is equal to in1. THis is handled internally'
319
+ )
320
+
321
+ def create_model(self, model) -> 'TransmissionModel':
322
+ self._plausibility_checks()
323
+ self.model = TransmissionModel(model, self)
324
+ return self.model
325
+
326
+ def transform_data(self, flow_system: 'FlowSystem') -> None:
327
+ super().transform_data(flow_system)
328
+ self.relative_losses = flow_system.create_time_series(
329
+ f'{self.label_full}|relative_losses', self.relative_losses
330
+ )
331
+ self.absolute_losses = flow_system.create_time_series(
332
+ f'{self.label_full}|absolute_losses', self.absolute_losses
333
+ )
334
+
335
+
336
+ class TransmissionModel(ComponentModel):
337
+ def __init__(self, model: SystemModel, element: Transmission):
338
+ super().__init__(model, element)
339
+ self.element: Transmission = element
340
+ self.on_off: Optional[OnOffModel] = None
341
+
342
+ def do_modeling(self):
343
+ """Initiates all FlowModels"""
344
+ # Force On Variable if absolute losses are present
345
+ if (self.element.absolute_losses is not None) and np.any(self.element.absolute_losses.active_data != 0):
346
+ for flow in self.element.inputs + self.element.outputs:
347
+ if flow.on_off_parameters is None:
348
+ flow.on_off_parameters = OnOffParameters()
349
+
350
+ # Make sure either None or both in Flows have InvestParameters
351
+ if self.element.in2 is not None:
352
+ if isinstance(self.element.in1.size, InvestParameters) and not isinstance(
353
+ self.element.in2.size, InvestParameters
354
+ ):
355
+ self.element.in2.size = InvestParameters(maximum_size=self.element.in1.size.maximum_size)
356
+
357
+ super().do_modeling()
358
+
359
+ # first direction
360
+ self.create_transmission_equation('dir1', self.element.in1, self.element.out1)
361
+
362
+ # second direction:
363
+ if self.element.in2 is not None:
364
+ self.create_transmission_equation('dir2', self.element.in2, self.element.out2)
365
+
366
+ # equate size of both directions
367
+ if isinstance(self.element.in1.size, InvestParameters) and self.element.in2 is not None:
368
+ # eq: in1.size = in2.size
369
+ self.add(
370
+ self._model.add_constraints(
371
+ self.element.in1.model._investment.size == self.element.in2.model._investment.size,
372
+ name=f'{self.label_full}|same_size',
373
+ ),
374
+ 'same_size',
375
+ )
376
+
377
+ def create_transmission_equation(self, name: str, in_flow: Flow, out_flow: Flow) -> linopy.Constraint:
378
+ """Creates an Equation for the Transmission efficiency and adds it to the model"""
379
+ # eq: out(t) + on(t)*loss_abs(t) = in(t)*(1 - loss_rel(t))
380
+ con_transmission = self.add(
381
+ self._model.add_constraints(
382
+ out_flow.model.flow_rate == -in_flow.model.flow_rate * (self.element.relative_losses.active_data - 1),
383
+ name=f'{self.label_full}|{name}',
384
+ ),
385
+ name,
386
+ )
387
+
388
+ if self.element.absolute_losses is not None:
389
+ con_transmission.lhs += in_flow.model.on_off.on * self.element.absolute_losses.active_data
390
+
391
+ return con_transmission
392
+
393
+
394
+ class LinearConverterModel(ComponentModel):
395
+ def __init__(self, model: SystemModel, element: LinearConverter):
396
+ super().__init__(model, element)
397
+ self.element: LinearConverter = element
398
+ self.on_off: Optional[OnOffModel] = None
399
+
400
+ def do_modeling(self):
401
+ super().do_modeling()
402
+
403
+ # conversion_factors:
404
+ if self.element.conversion_factors:
405
+ all_input_flows = set(self.element.inputs)
406
+ all_output_flows = set(self.element.outputs)
407
+
408
+ # für alle linearen Gleichungen:
409
+ for i, conv_factors in enumerate(self.element.conversion_factors):
410
+ used_flows = set([self.element.flows[flow_label] for flow_label in conv_factors])
411
+ used_inputs: Set = all_input_flows & used_flows
412
+ used_outputs: Set = all_output_flows & used_flows
413
+
414
+ self.add(
415
+ self._model.add_constraints(
416
+ sum([flow.model.flow_rate * conv_factors[flow.label].active_data for flow in used_inputs])
417
+ == sum([flow.model.flow_rate * conv_factors[flow.label].active_data for flow in used_outputs]),
418
+ name=f'{self.label_full}|conversion_{i}',
419
+ )
420
+ )
421
+
422
+ else:
423
+ # TODO: Improve Inclusion of OnOffParameters. Instead of creating a Binary in every flow, the binary could only be part of the Piece itself
424
+ piecewise_conversion = {
425
+ self.element.flows[flow].model.flow_rate.name: piecewise
426
+ for flow, piecewise in self.element.piecewise_conversion.items()
427
+ }
428
+
429
+ piecewise_conversion = PiecewiseModel(
430
+ model=self._model,
431
+ label_of_element=self.label_of_element,
432
+ label=self.label_full,
433
+ piecewise_variables=piecewise_conversion,
434
+ zero_point=self.on_off.on if self.on_off is not None else False,
435
+ as_time_series=True,
436
+ )
437
+ piecewise_conversion.do_modeling()
438
+ self.sub_models.append(piecewise_conversion)
439
+
440
+
441
+ class StorageModel(ComponentModel):
442
+ """Model of Storage"""
443
+
444
+ def __init__(self, model: SystemModel, element: Storage):
445
+ super().__init__(model, element)
446
+ self.element: Storage = element
447
+ self.charge_state: Optional[linopy.Variable] = None
448
+ self.netto_discharge: Optional[linopy.Variable] = None
449
+ self._investment: Optional[InvestmentModel] = None
450
+
451
+ def do_modeling(self):
452
+ super().do_modeling()
453
+
454
+ lb, ub = self.absolute_charge_state_bounds
455
+ self.charge_state = self.add(
456
+ self._model.add_variables(
457
+ lower=lb, upper=ub, coords=self._model.coords_extra, name=f'{self.label_full}|charge_state'
458
+ ),
459
+ 'charge_state',
460
+ )
461
+ self.netto_discharge = self.add(
462
+ self._model.add_variables(coords=self._model.coords, name=f'{self.label_full}|netto_discharge'),
463
+ 'netto_discharge',
464
+ )
465
+ # netto_discharge:
466
+ # eq: nettoFlow(t) - discharging(t) + charging(t) = 0
467
+ self.add(
468
+ self._model.add_constraints(
469
+ self.netto_discharge
470
+ == self.element.discharging.model.flow_rate - self.element.charging.model.flow_rate,
471
+ name=f'{self.label_full}|netto_discharge',
472
+ ),
473
+ 'netto_discharge',
474
+ )
475
+
476
+ charge_state = self.charge_state
477
+ rel_loss = self.element.relative_loss_per_hour.active_data
478
+ hours_per_step = self._model.hours_per_step
479
+ charge_rate = self.element.charging.model.flow_rate
480
+ discharge_rate = self.element.discharging.model.flow_rate
481
+ eff_charge = self.element.eta_charge.active_data
482
+ eff_discharge = self.element.eta_discharge.active_data
483
+
484
+ self.add(
485
+ self._model.add_constraints(
486
+ charge_state.isel(time=slice(1, None))
487
+ == charge_state.isel(time=slice(None, -1)) * (1 - rel_loss * hours_per_step)
488
+ + charge_rate * eff_charge * hours_per_step
489
+ - discharge_rate * eff_discharge * hours_per_step,
490
+ name=f'{self.label_full}|charge_state',
491
+ ),
492
+ 'charge_state',
493
+ )
494
+
495
+ if isinstance(self.element.capacity_in_flow_hours, InvestParameters):
496
+ self._investment = InvestmentModel(
497
+ model=self._model,
498
+ label_of_element=self.label_of_element,
499
+ parameters=self.element.capacity_in_flow_hours,
500
+ defining_variable=self.charge_state,
501
+ relative_bounds_of_defining_variable=self.relative_charge_state_bounds,
502
+ )
503
+ self.sub_models.append(self._investment)
504
+ self._investment.do_modeling()
505
+
506
+ # Initial charge state
507
+ self._initial_and_final_charge_state()
508
+
509
+ def _initial_and_final_charge_state(self):
510
+ if self.element.initial_charge_state is not None:
511
+ name_short = 'initial_charge_state'
512
+ name = f'{self.label_full}|{name_short}'
513
+
514
+ if utils.is_number(self.element.initial_charge_state):
515
+ self.add(
516
+ self._model.add_constraints(
517
+ self.charge_state.isel(time=0) == self.element.initial_charge_state, name=name
518
+ ),
519
+ name_short,
520
+ )
521
+ elif self.element.initial_charge_state == 'lastValueOfSim':
522
+ self.add(
523
+ self._model.add_constraints(
524
+ self.charge_state.isel(time=0) == self.charge_state.isel(time=-1), name=name
525
+ ),
526
+ name_short,
527
+ )
528
+ else: # TODO: Validation in Storage Class, not in Model
529
+ raise PlausibilityError(
530
+ f'initial_charge_state has undefined value: {self.element.initial_charge_state}'
531
+ )
532
+
533
+ if self.element.maximal_final_charge_state is not None:
534
+ self.add(
535
+ self._model.add_constraints(
536
+ self.charge_state.isel(time=-1) <= self.element.maximal_final_charge_state,
537
+ name=f'{self.label_full}|final_charge_max',
538
+ ),
539
+ 'final_charge_max',
540
+ )
541
+
542
+ if self.element.minimal_final_charge_state is not None:
543
+ self.add(
544
+ self._model.add_constraints(
545
+ self.charge_state.isel(time=-1) >= self.element.minimal_final_charge_state,
546
+ name=f'{self.label_full}|final_charge_min',
547
+ ),
548
+ 'final_charge_min',
549
+ )
550
+
551
+ @property
552
+ def absolute_charge_state_bounds(self) -> Tuple[NumericData, NumericData]:
553
+ relative_lower_bound, relative_upper_bound = self.relative_charge_state_bounds
554
+ if not isinstance(self.element.capacity_in_flow_hours, InvestParameters):
555
+ return (
556
+ relative_lower_bound * self.element.capacity_in_flow_hours,
557
+ relative_upper_bound * self.element.capacity_in_flow_hours,
558
+ )
559
+ else:
560
+ return (
561
+ relative_lower_bound * self.element.capacity_in_flow_hours.minimum_size,
562
+ relative_upper_bound * self.element.capacity_in_flow_hours.maximum_size,
563
+ )
564
+
565
+ @property
566
+ def relative_charge_state_bounds(self) -> Tuple[NumericData, NumericData]:
567
+ return (
568
+ self.element.relative_minimum_charge_state.active_data,
569
+ self.element.relative_maximum_charge_state.active_data,
570
+ )
571
+
572
+
573
+ @register_class_for_io
574
+ class SourceAndSink(Component):
575
+ """
576
+ class for source (output-flow) and sink (input-flow) in one commponent
577
+ """
578
+
579
+ def __init__(
580
+ self,
581
+ label: str,
582
+ source: Flow,
583
+ sink: Flow,
584
+ prevent_simultaneous_sink_and_source: bool = True,
585
+ meta_data: Optional[Dict] = None,
586
+ ):
587
+ """
588
+ Args:
589
+ label: The label of the Element. Used to identify it in the FlowSystem
590
+ source: output-flow of this component
591
+ sink: input-flow of this component
592
+ prevent_simultaneous_sink_and_source: If True, inflow and outflow can not be active simultaniously.
593
+ meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
594
+ """
595
+ super().__init__(
596
+ label,
597
+ inputs=[sink],
598
+ outputs=[source],
599
+ prevent_simultaneous_flows=[sink, source] if prevent_simultaneous_sink_and_source is True else None,
600
+ meta_data=meta_data,
601
+ )
602
+ self.source = source
603
+ self.sink = sink
604
+ self.prevent_simultaneous_sink_and_source = prevent_simultaneous_sink_and_source
605
+
606
+
607
+ @register_class_for_io
608
+ class Source(Component):
609
+ def __init__(self, label: str, source: Flow, meta_data: Optional[Dict] = None):
610
+ """
611
+ Args:
612
+ label: The label of the Element. Used to identify it in the FlowSystem
613
+ source: output-flow of source
614
+ meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
615
+ """
616
+ super().__init__(label, outputs=[source], meta_data=meta_data)
617
+ self.source = source
618
+
619
+
620
+ @register_class_for_io
621
+ class Sink(Component):
622
+ def __init__(self, label: str, sink: Flow, meta_data: Optional[Dict] = None):
623
+ """
624
+ Args:
625
+ label: The label of the Element. Used to identify it in the FlowSystem
626
+ meta_data: used to store more information about the element. Is not used internally, but saved in the results
627
+ sink: input-flow of sink
628
+ """
629
+ super().__init__(label, inputs=[sink], meta_data=meta_data)
630
+ self.sink = sink
@@ -8,16 +8,17 @@ import yaml
8
8
  from rich.console import Console
9
9
  from rich.logging import RichHandler
10
10
 
11
- logger = logging.getLogger('flixOpt')
11
+ logger = logging.getLogger('flixopt')
12
12
 
13
13
 
14
14
  def merge_configs(defaults: dict, overrides: dict) -> dict:
15
15
  """
16
16
  Merge the default configuration with user-provided overrides.
17
-
18
- :param defaults: Default configuration dictionary.
19
- :param overrides: User configuration dictionary.
20
- :return: Merged configuration dictionary.
17
+ Args:
18
+ defaults: Default configuration dictionary.
19
+ overrides: User configuration dictionary.
20
+ Returns:
21
+ Merged configuration dictionary.
21
22
  """
22
23
  for key, value in overrides.items():
23
24
  if isinstance(value, dict) and key in defaults and isinstance(defaults[key], dict):
@@ -224,11 +225,11 @@ def _get_logging_handler(log_file: Optional[str] = None, use_rich_handler: bool
224
225
 
225
226
  def setup_logging(
226
227
  default_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = 'INFO',
227
- log_file: Optional[str] = 'flixOpt.log',
228
+ log_file: Optional[str] = 'flixopt.log',
228
229
  use_rich_handler: bool = False,
229
230
  ):
230
231
  """Setup logging configuration"""
231
- logger = logging.getLogger('flixOpt') # Use a specific logger name for your package
232
+ logger = logging.getLogger('flixopt') # Use a specific logger name for your package
232
233
  logger.setLevel(get_logging_level_by_name(default_level))
233
234
  # Clear existing handlers
234
235
  if logger.hasHandlers():
@@ -251,7 +252,7 @@ def get_logging_level_by_name(level_name: Literal['DEBUG', 'INFO', 'WARNING', 'E
251
252
 
252
253
 
253
254
  def change_logging_level(level_name: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']):
254
- logger = logging.getLogger('flixOpt')
255
+ logger = logging.getLogger('flixopt')
255
256
  logging_level = get_logging_level_by_name(level_name)
256
257
  logger.setLevel(logging_level)
257
258
  for handler in logger.handlers:
@@ -1,8 +1,8 @@
1
- # Default configuration of flixOpt
2
- config_name: flixOpt # Name of the config file. This has no effect on the configuration itself.
1
+ # Default configuration of flixopt
2
+ config_name: flixopt # Name of the config file. This has no effect on the configuration itself.
3
3
  logging:
4
4
  level: INFO
5
- file: flixOpt.log
5
+ file: flixopt.log
6
6
  rich: false # logging output is formatted using rich. This is only advisable when using a proper terminal
7
7
  modeling:
8
8
  BIG: 10000000 # 1e notation not possible in yaml