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.
- docs/examples/00-Minimal Example.md +5 -0
- docs/examples/01-Basic Example.md +5 -0
- docs/examples/02-Complex Example.md +10 -0
- docs/examples/03-Calculation Modes.md +5 -0
- docs/examples/index.md +5 -0
- docs/faq/contribute.md +49 -0
- docs/faq/index.md +3 -0
- docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
- docs/images/architecture_flixOpt.png +0 -0
- docs/images/flixopt-icon.svg +1 -0
- docs/javascripts/mathjax.js +18 -0
- docs/release-notes/_template.txt +32 -0
- docs/release-notes/index.md +7 -0
- docs/release-notes/v2.0.0.md +93 -0
- docs/release-notes/v2.0.1.md +12 -0
- docs/user-guide/Mathematical Notation/Bus.md +33 -0
- docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +132 -0
- docs/user-guide/Mathematical Notation/Flow.md +26 -0
- docs/user-guide/Mathematical Notation/LinearConverter.md +21 -0
- docs/user-guide/Mathematical Notation/Piecewise.md +49 -0
- docs/user-guide/Mathematical Notation/Storage.md +44 -0
- docs/user-guide/Mathematical Notation/index.md +22 -0
- docs/user-guide/Mathematical Notation/others.md +3 -0
- docs/user-guide/index.md +124 -0
- {flixOpt → flixopt}/__init__.py +5 -2
- {flixOpt → flixopt}/aggregation.py +113 -140
- flixopt/calculation.py +455 -0
- {flixOpt → flixopt}/commons.py +7 -4
- flixopt/components.py +630 -0
- {flixOpt → flixopt}/config.py +9 -8
- {flixOpt → flixopt}/config.yaml +3 -3
- flixopt/core.py +970 -0
- flixopt/effects.py +386 -0
- flixopt/elements.py +534 -0
- flixopt/features.py +1042 -0
- flixopt/flow_system.py +409 -0
- flixopt/interface.py +265 -0
- flixopt/io.py +308 -0
- flixopt/linear_converters.py +331 -0
- flixopt/plotting.py +1340 -0
- flixopt/results.py +898 -0
- flixopt/solvers.py +77 -0
- flixopt/structure.py +630 -0
- flixopt/utils.py +62 -0
- flixopt-2.0.1.dist-info/METADATA +145 -0
- flixopt-2.0.1.dist-info/RECORD +57 -0
- {flixopt-1.0.12.dist-info → flixopt-2.0.1.dist-info}/WHEEL +1 -1
- flixopt-2.0.1.dist-info/top_level.txt +6 -0
- pics/architecture_flixOpt-pre2.0.0.png +0 -0
- pics/architecture_flixOpt.png +0 -0
- pics/flixopt-icon.svg +1 -0
- pics/pics.pptx +0 -0
- scripts/gen_ref_pages.py +54 -0
- site/release-notes/_template.txt +32 -0
- flixOpt/calculation.py +0 -629
- flixOpt/components.py +0 -614
- flixOpt/core.py +0 -182
- flixOpt/effects.py +0 -410
- flixOpt/elements.py +0 -489
- flixOpt/features.py +0 -942
- flixOpt/flow_system.py +0 -351
- flixOpt/interface.py +0 -203
- flixOpt/linear_converters.py +0 -325
- flixOpt/math_modeling.py +0 -1145
- flixOpt/plotting.py +0 -712
- flixOpt/results.py +0 -563
- flixOpt/solvers.py +0 -21
- flixOpt/structure.py +0 -733
- flixOpt/utils.py +0 -134
- flixopt-1.0.12.dist-info/METADATA +0 -174
- flixopt-1.0.12.dist-info/RECORD +0 -29
- flixopt-1.0.12.dist-info/top_level.txt +0 -3
- {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
|
{flixOpt → flixopt}/config.py
RENAMED
|
@@ -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('
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
:
|
|
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] = '
|
|
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('
|
|
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('
|
|
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:
|
{flixOpt → flixopt}/config.yaml
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Default configuration of
|
|
2
|
-
config_name:
|
|
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:
|
|
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
|