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.
- flixopt/__init__.py +35 -1
- flixopt/aggregation.py +60 -81
- flixopt/calculation.py +381 -196
- flixopt/components.py +1022 -359
- flixopt/config.py +553 -191
- flixopt/core.py +475 -1315
- flixopt/effects.py +477 -214
- flixopt/elements.py +591 -344
- flixopt/features.py +403 -957
- flixopt/flow_system.py +781 -293
- flixopt/interface.py +1159 -189
- flixopt/io.py +50 -55
- flixopt/linear_converters.py +384 -92
- flixopt/modeling.py +759 -0
- flixopt/network_app.py +789 -0
- flixopt/plotting.py +273 -135
- flixopt/results.py +639 -383
- flixopt/solvers.py +25 -21
- flixopt/structure.py +928 -442
- flixopt/utils.py +34 -5
- flixopt-3.0.0.dist-info/METADATA +209 -0
- flixopt-3.0.0.dist-info/RECORD +26 -0
- {flixopt-2.2.0b0.dist-info → flixopt-3.0.0.dist-info}/WHEEL +1 -1
- flixopt-3.0.0.dist-info/top_level.txt +1 -0
- docs/examples/00-Minimal Example.md +0 -5
- docs/examples/01-Basic Example.md +0 -5
- docs/examples/02-Complex Example.md +0 -10
- docs/examples/03-Calculation Modes.md +0 -5
- docs/examples/index.md +0 -5
- docs/faq/contribute.md +0 -49
- docs/faq/index.md +0 -3
- docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
- docs/images/architecture_flixOpt.png +0 -0
- docs/images/flixopt-icon.svg +0 -1
- docs/javascripts/mathjax.js +0 -18
- docs/release-notes/_template.txt +0 -32
- docs/release-notes/index.md +0 -7
- docs/release-notes/v2.0.0.md +0 -93
- docs/release-notes/v2.0.1.md +0 -12
- docs/release-notes/v2.1.0.md +0 -31
- docs/release-notes/v2.2.0.md +0 -55
- docs/user-guide/Mathematical Notation/Bus.md +0 -33
- docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +0 -132
- docs/user-guide/Mathematical Notation/Flow.md +0 -26
- docs/user-guide/Mathematical Notation/Investment.md +0 -115
- docs/user-guide/Mathematical Notation/LinearConverter.md +0 -21
- docs/user-guide/Mathematical Notation/Piecewise.md +0 -49
- docs/user-guide/Mathematical Notation/Storage.md +0 -44
- docs/user-guide/Mathematical Notation/index.md +0 -22
- docs/user-guide/Mathematical Notation/others.md +0 -3
- docs/user-guide/index.md +0 -124
- flixopt/config.yaml +0 -10
- flixopt-2.2.0b0.dist-info/METADATA +0 -146
- flixopt-2.2.0b0.dist-info/RECORD +0 -59
- flixopt-2.2.0b0.dist-info/top_level.txt +0 -5
- pics/architecture_flixOpt-pre2.0.0.png +0 -0
- pics/architecture_flixOpt.png +0 -0
- pics/flixOpt_plotting.jpg +0 -0
- pics/flixopt-icon.svg +0 -1
- pics/pics.pptx +0 -0
- scripts/gen_ref_pages.py +0 -54
- tests/ressources/Zeitreihen2020.csv +0 -35137
- {flixopt-2.2.0b0.dist-info → flixopt-3.0.0.dist-info}/licenses/LICENSE +0 -0
flixopt/calculation.py
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
"""
|
|
2
2
|
This module contains the Calculation functionality for the flixopt framework.
|
|
3
|
-
It is used to calculate a
|
|
3
|
+
It is used to calculate a FlowSystemModel for a given FlowSystem through a solver.
|
|
4
4
|
There are three different Calculation types:
|
|
5
|
-
1. FullCalculation: Calculates the
|
|
6
|
-
2. AggregatedCalculation: Calculates the
|
|
5
|
+
1. FullCalculation: Calculates the FlowSystemModel for the full FlowSystem
|
|
6
|
+
2. AggregatedCalculation: Calculates the FlowSystemModel for the full FlowSystem, but aggregates the TimeSeriesData.
|
|
7
7
|
This simplifies the mathematical model and usually speeds up the solving process.
|
|
8
|
-
3. SegmentedCalculation: Solves a
|
|
8
|
+
3. SegmentedCalculation: Solves a FlowSystemModel for each individual Segment of the FlowSystem.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
11
13
|
import logging
|
|
12
14
|
import math
|
|
13
15
|
import pathlib
|
|
14
16
|
import timeit
|
|
15
17
|
import warnings
|
|
16
|
-
from
|
|
18
|
+
from collections import Counter
|
|
19
|
+
from typing import TYPE_CHECKING, Annotated, Any
|
|
17
20
|
|
|
18
21
|
import numpy as np
|
|
19
|
-
import pandas as pd
|
|
20
22
|
import yaml
|
|
21
23
|
|
|
22
24
|
from . import io as fx_io
|
|
@@ -24,13 +26,18 @@ from . import utils as utils
|
|
|
24
26
|
from .aggregation import AggregationModel, AggregationParameters
|
|
25
27
|
from .components import Storage
|
|
26
28
|
from .config import CONFIG
|
|
27
|
-
from .core import Scalar
|
|
28
|
-
from .elements import Component
|
|
29
|
+
from .core import DataConverter, Scalar, TimeSeriesData, drop_constant_arrays
|
|
29
30
|
from .features import InvestmentModel
|
|
30
31
|
from .flow_system import FlowSystem
|
|
31
32
|
from .results import CalculationResults, SegmentedCalculationResults
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
import pandas as pd
|
|
36
|
+
import xarray as xr
|
|
37
|
+
|
|
38
|
+
from .elements import Component
|
|
39
|
+
from .solvers import _Solver
|
|
40
|
+
from .structure import FlowSystemModel
|
|
34
41
|
|
|
35
42
|
logger = logging.getLogger('flixopt')
|
|
36
43
|
|
|
@@ -38,53 +45,63 @@ logger = logging.getLogger('flixopt')
|
|
|
38
45
|
class Calculation:
|
|
39
46
|
"""
|
|
40
47
|
class for defined way of solving a flow_system optimization
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
name: name of calculation
|
|
51
|
+
flow_system: flow_system which should be calculated
|
|
52
|
+
folder: folder where results should be saved. If None, then the current working directory is used.
|
|
53
|
+
normalize_weights: Whether to automatically normalize the weights (periods and scenarios) to sum up to 1 when solving.
|
|
54
|
+
active_timesteps: Deprecated. Use FlowSystem.sel(time=...) or FlowSystem.isel(time=...) instead.
|
|
41
55
|
"""
|
|
42
56
|
|
|
43
57
|
def __init__(
|
|
44
58
|
self,
|
|
45
59
|
name: str,
|
|
46
60
|
flow_system: FlowSystem,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
61
|
+
active_timesteps: Annotated[
|
|
62
|
+
pd.DatetimeIndex | None,
|
|
63
|
+
'DEPRECATED: Use flow_system.sel(time=...) or flow_system.isel(time=...) instead',
|
|
64
|
+
] = None,
|
|
65
|
+
folder: pathlib.Path | None = None,
|
|
66
|
+
normalize_weights: bool = True,
|
|
51
67
|
):
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"""
|
|
68
|
+
self.name = name
|
|
69
|
+
if flow_system.used_in_calculation:
|
|
70
|
+
logger.warning(
|
|
71
|
+
f'This FlowSystem is already used in a calculation:\n{flow_system}\n'
|
|
72
|
+
f'Creating a copy of the FlowSystem for Calculation "{self.name}".'
|
|
73
|
+
)
|
|
74
|
+
flow_system = flow_system.copy()
|
|
75
|
+
|
|
61
76
|
if active_timesteps is not None:
|
|
62
77
|
warnings.warn(
|
|
63
|
-
'active_timesteps is deprecated
|
|
78
|
+
"The 'active_timesteps' parameter is deprecated and will be removed in a future version. "
|
|
79
|
+
'Use flow_system.sel(time=timesteps) or flow_system.isel(time=indices) before passing '
|
|
80
|
+
'the FlowSystem to the Calculation instead.',
|
|
64
81
|
DeprecationWarning,
|
|
65
82
|
stacklevel=2,
|
|
66
83
|
)
|
|
67
|
-
|
|
68
|
-
self.
|
|
84
|
+
flow_system = flow_system.sel(time=active_timesteps)
|
|
85
|
+
self._active_timesteps = active_timesteps # deprecated
|
|
86
|
+
self.normalize_weights = normalize_weights
|
|
87
|
+
|
|
88
|
+
flow_system._used_in_calculation = True
|
|
89
|
+
|
|
69
90
|
self.flow_system = flow_system
|
|
70
|
-
self.model:
|
|
71
|
-
self.selected_timesteps = selected_timesteps
|
|
72
|
-
self.selected_scenarios = selected_scenarios
|
|
91
|
+
self.model: FlowSystemModel | None = None
|
|
73
92
|
|
|
74
93
|
self.durations = {'modeling': 0.0, 'solving': 0.0, 'saving': 0.0}
|
|
75
94
|
self.folder = pathlib.Path.cwd() / 'results' if folder is None else pathlib.Path(folder)
|
|
76
|
-
self.results:
|
|
95
|
+
self.results: CalculationResults | None = None
|
|
96
|
+
|
|
97
|
+
if self.folder.exists() and not self.folder.is_dir():
|
|
98
|
+
raise NotADirectoryError(f'Path {self.folder} exists and is not a directory.')
|
|
99
|
+
self.folder.mkdir(parents=False, exist_ok=True)
|
|
77
100
|
|
|
78
|
-
|
|
79
|
-
try:
|
|
80
|
-
self.folder.mkdir(parents=False)
|
|
81
|
-
except FileNotFoundError as e:
|
|
82
|
-
raise FileNotFoundError(
|
|
83
|
-
f'Folder {self.folder} and its parent do not exist. Please create them first.'
|
|
84
|
-
) from e
|
|
101
|
+
self._modeled = False
|
|
85
102
|
|
|
86
103
|
@property
|
|
87
|
-
def main_results(self) ->
|
|
104
|
+
def main_results(self) -> dict[str, Scalar | dict]:
|
|
88
105
|
from flixopt.features import InvestmentModel
|
|
89
106
|
|
|
90
107
|
main_results = {
|
|
@@ -92,9 +109,9 @@ class Calculation:
|
|
|
92
109
|
'Penalty': self.model.effects.penalty.total.solution.values,
|
|
93
110
|
'Effects': {
|
|
94
111
|
f'{effect.label} [{effect.unit}]': {
|
|
95
|
-
'
|
|
96
|
-
'
|
|
97
|
-
'total': effect.
|
|
112
|
+
'temporal': effect.submodel.temporal.total.solution.values,
|
|
113
|
+
'periodic': effect.submodel.periodic.total.solution.values,
|
|
114
|
+
'total': effect.submodel.total.solution.values,
|
|
98
115
|
}
|
|
99
116
|
for effect in self.flow_system.effects
|
|
100
117
|
},
|
|
@@ -102,39 +119,38 @@ class Calculation:
|
|
|
102
119
|
'Invested': {
|
|
103
120
|
model.label_of_element: model.size.solution
|
|
104
121
|
for component in self.flow_system.components.values()
|
|
105
|
-
for model in component.
|
|
106
|
-
if isinstance(model, InvestmentModel) and model.size.solution.max() >= CONFIG.
|
|
122
|
+
for model in component.submodel.all_submodels
|
|
123
|
+
if isinstance(model, InvestmentModel) and model.size.solution.max() >= CONFIG.Modeling.epsilon
|
|
107
124
|
},
|
|
108
125
|
'Not invested': {
|
|
109
126
|
model.label_of_element: model.size.solution
|
|
110
127
|
for component in self.flow_system.components.values()
|
|
111
|
-
for model in component.
|
|
112
|
-
if isinstance(model, InvestmentModel) and model.size.solution.max() < CONFIG.
|
|
128
|
+
for model in component.submodel.all_submodels
|
|
129
|
+
if isinstance(model, InvestmentModel) and model.size.solution.max() < CONFIG.Modeling.epsilon
|
|
113
130
|
},
|
|
114
131
|
},
|
|
115
132
|
'Buses with excess': [
|
|
116
133
|
{
|
|
117
134
|
bus.label_full: {
|
|
118
|
-
'input': bus.
|
|
119
|
-
'output': bus.
|
|
135
|
+
'input': bus.submodel.excess_input.solution.sum('time'),
|
|
136
|
+
'output': bus.submodel.excess_output.solution.sum('time'),
|
|
120
137
|
}
|
|
121
138
|
}
|
|
122
139
|
for bus in self.flow_system.buses.values()
|
|
123
140
|
if bus.with_excess
|
|
124
141
|
and (
|
|
125
|
-
bus.
|
|
126
|
-
or bus.model.excess_output.solution.sum() > 1e-3
|
|
142
|
+
bus.submodel.excess_input.solution.sum() > 1e-3 or bus.submodel.excess_output.solution.sum() > 1e-3
|
|
127
143
|
)
|
|
128
144
|
],
|
|
129
145
|
}
|
|
130
146
|
|
|
131
|
-
return utils.
|
|
147
|
+
return utils.round_nested_floats(main_results)
|
|
132
148
|
|
|
133
149
|
@property
|
|
134
150
|
def summary(self):
|
|
135
151
|
return {
|
|
136
152
|
'Name': self.name,
|
|
137
|
-
'Number of timesteps': len(self.flow_system.
|
|
153
|
+
'Number of timesteps': len(self.flow_system.timesteps),
|
|
138
154
|
'Calculation Type': self.__class__.__name__,
|
|
139
155
|
'Constraints': self.model.constraints.ncons,
|
|
140
156
|
'Variables': self.model.variables.nvars,
|
|
@@ -146,29 +162,72 @@ class Calculation:
|
|
|
146
162
|
@property
|
|
147
163
|
def active_timesteps(self) -> pd.DatetimeIndex:
|
|
148
164
|
warnings.warn(
|
|
149
|
-
'active_timesteps is deprecated. Use
|
|
165
|
+
'active_timesteps is deprecated. Use flow_system.sel(time=...) or flow_system.isel(time=...) instead.',
|
|
150
166
|
DeprecationWarning,
|
|
151
167
|
stacklevel=2,
|
|
152
168
|
)
|
|
153
|
-
return self.
|
|
169
|
+
return self._active_timesteps
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def modeled(self) -> bool:
|
|
173
|
+
return True if self.model is not None else False
|
|
154
174
|
|
|
155
175
|
|
|
156
176
|
class FullCalculation(Calculation):
|
|
157
177
|
"""
|
|
158
|
-
|
|
178
|
+
FullCalculation solves the complete optimization problem using all time steps.
|
|
179
|
+
|
|
180
|
+
This is the most comprehensive calculation type that considers every time step
|
|
181
|
+
in the optimization, providing the most accurate but computationally intensive solution.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
name: name of calculation
|
|
185
|
+
flow_system: flow_system which should be calculated
|
|
186
|
+
folder: folder where results should be saved. If None, then the current working directory is used.
|
|
187
|
+
normalize_weights: Whether to automatically normalize the weights (periods and scenarios) to sum up to 1 when solving.
|
|
188
|
+
active_timesteps: Deprecated. Use FlowSystem.sel(time=...) or FlowSystem.isel(time=...) instead.
|
|
159
189
|
"""
|
|
160
190
|
|
|
161
|
-
def do_modeling(self) ->
|
|
191
|
+
def do_modeling(self) -> FullCalculation:
|
|
162
192
|
t_start = timeit.default_timer()
|
|
163
|
-
self.
|
|
193
|
+
self.flow_system.connect_and_transform()
|
|
164
194
|
|
|
165
|
-
self.model = self.flow_system.create_model()
|
|
195
|
+
self.model = self.flow_system.create_model(self.normalize_weights)
|
|
166
196
|
self.model.do_modeling()
|
|
167
197
|
|
|
168
198
|
self.durations['modeling'] = round(timeit.default_timer() - t_start, 2)
|
|
169
|
-
return self
|
|
199
|
+
return self
|
|
200
|
+
|
|
201
|
+
def fix_sizes(self, ds: xr.Dataset, decimal_rounding: int | None = 5) -> FullCalculation:
|
|
202
|
+
"""Fix the sizes of the calculations to specified values.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
ds: The dataset that contains the variable names mapped to their sizes. If None, the dataset is loaded from the results.
|
|
206
|
+
decimal_rounding: The number of decimal places to round the sizes to. If no rounding is applied, numerical errors might lead to infeasibility.
|
|
207
|
+
"""
|
|
208
|
+
if not self.modeled:
|
|
209
|
+
raise RuntimeError('Model was not created. Call do_modeling() first.')
|
|
210
|
+
if decimal_rounding is not None:
|
|
211
|
+
ds = ds.round(decimal_rounding)
|
|
212
|
+
|
|
213
|
+
for name, da in ds.data_vars.items():
|
|
214
|
+
if '|size' not in name:
|
|
215
|
+
continue
|
|
216
|
+
if name not in self.model.variables:
|
|
217
|
+
logger.debug(f'Variable {name} not found in calculation model. Skipping.')
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
con = self.model.add_constraints(
|
|
221
|
+
self.model[name] == da,
|
|
222
|
+
name=f'{name}-fixed',
|
|
223
|
+
)
|
|
224
|
+
logger.debug(f'Fixed "{name}":\n{con}')
|
|
225
|
+
|
|
226
|
+
return self
|
|
170
227
|
|
|
171
|
-
def solve(
|
|
228
|
+
def solve(
|
|
229
|
+
self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool = True
|
|
230
|
+
) -> FullCalculation:
|
|
172
231
|
t_start = timeit.default_timer()
|
|
173
232
|
|
|
174
233
|
self.model.solve(
|
|
@@ -191,11 +250,10 @@ class FullCalculation(Calculation):
|
|
|
191
250
|
|
|
192
251
|
# Log the formatted output
|
|
193
252
|
if log_main_results:
|
|
194
|
-
logger.info(f'{" Main Results ":#^80}')
|
|
195
253
|
logger.info(
|
|
196
|
-
'\n'
|
|
254
|
+
f'{" Main Results ":#^80}\n'
|
|
197
255
|
+ yaml.dump(
|
|
198
|
-
utils.
|
|
256
|
+
utils.round_nested_floats(self.main_results),
|
|
199
257
|
default_flow_style=False,
|
|
200
258
|
sort_keys=False,
|
|
201
259
|
allow_unicode=True,
|
|
@@ -205,16 +263,30 @@ class FullCalculation(Calculation):
|
|
|
205
263
|
|
|
206
264
|
self.results = CalculationResults.from_calculation(self)
|
|
207
265
|
|
|
208
|
-
|
|
209
|
-
self.flow_system.transform_data()
|
|
210
|
-
self.flow_system.time_series_collection.set_selection(
|
|
211
|
-
timesteps=self.selected_timesteps, scenarios=self.selected_scenarios
|
|
212
|
-
)
|
|
266
|
+
return self
|
|
213
267
|
|
|
214
268
|
|
|
215
269
|
class AggregatedCalculation(FullCalculation):
|
|
216
270
|
"""
|
|
217
|
-
|
|
271
|
+
AggregatedCalculation reduces computational complexity by clustering time series into typical periods.
|
|
272
|
+
|
|
273
|
+
This calculation approach aggregates time series data using clustering techniques (tsam) to identify
|
|
274
|
+
representative time periods, significantly reducing computation time while maintaining solution accuracy.
|
|
275
|
+
|
|
276
|
+
Note:
|
|
277
|
+
The quality of the solution depends on the choice of aggregation parameters.
|
|
278
|
+
The optimal parameters depend on the specific problem and the characteristics of the time series data.
|
|
279
|
+
For more information, refer to the [tsam documentation](https://tsam.readthedocs.io/en/latest/).
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
name: Name of the calculation
|
|
283
|
+
flow_system: FlowSystem to be optimized
|
|
284
|
+
aggregation_parameters: Parameters for aggregation. See AggregationParameters class documentation
|
|
285
|
+
components_to_clusterize: list of Components to perform aggregation on. If None, all components are aggregated.
|
|
286
|
+
This equalizes variables in the components according to the typical periods computed in the aggregation
|
|
287
|
+
active_timesteps: DatetimeIndex of timesteps to use for calculation. If None, all timesteps are used
|
|
288
|
+
folder: Folder where results should be saved. If None, current working directory is used
|
|
289
|
+
aggregation: contains the aggregation model
|
|
218
290
|
"""
|
|
219
291
|
|
|
220
292
|
def __init__(
|
|
@@ -222,47 +294,35 @@ class AggregatedCalculation(FullCalculation):
|
|
|
222
294
|
name: str,
|
|
223
295
|
flow_system: FlowSystem,
|
|
224
296
|
aggregation_parameters: AggregationParameters,
|
|
225
|
-
components_to_clusterize:
|
|
226
|
-
|
|
227
|
-
|
|
297
|
+
components_to_clusterize: list[Component] | None = None,
|
|
298
|
+
active_timesteps: Annotated[
|
|
299
|
+
pd.DatetimeIndex | None,
|
|
300
|
+
'DEPRECATED: Use flow_system.sel(time=...) or flow_system.isel(time=...) instead',
|
|
301
|
+
] = None,
|
|
302
|
+
folder: pathlib.Path | None = None,
|
|
228
303
|
):
|
|
229
|
-
|
|
230
|
-
Class for Optimizing the `FlowSystem` including:
|
|
231
|
-
1. Aggregating TimeSeriesData via typical periods using tsam.
|
|
232
|
-
2. Equalizing variables of typical periods.
|
|
233
|
-
Args:
|
|
234
|
-
name: name of calculation
|
|
235
|
-
flow_system: flow_system which should be calculated
|
|
236
|
-
aggregation_parameters: Parameters for aggregation. See documentation of AggregationParameters class.
|
|
237
|
-
components_to_clusterize: List of Components to perform aggregation on. If None, then all components are aggregated.
|
|
238
|
-
This means, teh variables in the components are equalized to each other, according to the typical periods
|
|
239
|
-
computed in the DataAggregation
|
|
240
|
-
selected_timesteps: pd.DatetimeIndex or None
|
|
241
|
-
list with indices, which should be used for calculation. If None, then all timesteps are used.
|
|
242
|
-
folder: folder where results should be saved. If None, then the current working directory is used.
|
|
243
|
-
"""
|
|
244
|
-
if flow_system.time_series_collection.scenarios is not None:
|
|
304
|
+
if flow_system.scenarios is not None:
|
|
245
305
|
raise ValueError('Aggregation is not supported for scenarios yet. Please use FullCalculation instead.')
|
|
246
|
-
super().__init__(name, flow_system,
|
|
306
|
+
super().__init__(name, flow_system, active_timesteps, folder=folder)
|
|
247
307
|
self.aggregation_parameters = aggregation_parameters
|
|
248
308
|
self.components_to_clusterize = components_to_clusterize
|
|
249
309
|
self.aggregation = None
|
|
250
310
|
|
|
251
|
-
def do_modeling(self) ->
|
|
311
|
+
def do_modeling(self) -> AggregatedCalculation:
|
|
252
312
|
t_start = timeit.default_timer()
|
|
253
|
-
self.
|
|
313
|
+
self.flow_system.connect_and_transform()
|
|
254
314
|
self._perform_aggregation()
|
|
255
315
|
|
|
256
316
|
# Model the System
|
|
257
|
-
self.model = self.flow_system.create_model()
|
|
317
|
+
self.model = self.flow_system.create_model(self.normalize_weights)
|
|
258
318
|
self.model.do_modeling()
|
|
259
|
-
# Add Aggregation
|
|
319
|
+
# Add Aggregation Submodel after modeling the rest
|
|
260
320
|
self.aggregation = AggregationModel(
|
|
261
321
|
self.model, self.aggregation_parameters, self.flow_system, self.aggregation, self.components_to_clusterize
|
|
262
322
|
)
|
|
263
323
|
self.aggregation.do_modeling()
|
|
264
324
|
self.durations['modeling'] = round(timeit.default_timer() - t_start, 2)
|
|
265
|
-
return self
|
|
325
|
+
return self
|
|
266
326
|
|
|
267
327
|
def _perform_aggregation(self):
|
|
268
328
|
from .aggregation import Aggregation
|
|
@@ -270,41 +330,34 @@ class AggregatedCalculation(FullCalculation):
|
|
|
270
330
|
t_start_agg = timeit.default_timer()
|
|
271
331
|
|
|
272
332
|
# Validation
|
|
273
|
-
dt_min
|
|
274
|
-
|
|
275
|
-
np.max(self.flow_system.time_series_collection.hours_per_timestep),
|
|
276
|
-
)
|
|
333
|
+
dt_min = float(self.flow_system.hours_per_timestep.min().item())
|
|
334
|
+
dt_max = float(self.flow_system.hours_per_timestep.max().item())
|
|
277
335
|
if not dt_min == dt_max:
|
|
278
336
|
raise ValueError(
|
|
279
337
|
f'Aggregation failed due to inconsistent time step sizes:'
|
|
280
338
|
f'delta_t varies from {dt_min} to {dt_max} hours.'
|
|
281
339
|
)
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
/ self.flow_system.time_series_collection.hours_per_timestep.max()
|
|
285
|
-
)
|
|
286
|
-
is_integer = (
|
|
287
|
-
self.aggregation_parameters.hours_per_period
|
|
288
|
-
% self.flow_system.time_series_collection.hours_per_timestep.max()
|
|
289
|
-
).item() == 0
|
|
290
|
-
if not (steps_per_period.size == 1 and is_integer):
|
|
340
|
+
ratio = self.aggregation_parameters.hours_per_period / dt_max
|
|
341
|
+
if not np.isclose(ratio, round(ratio), atol=1e-9):
|
|
291
342
|
raise ValueError(
|
|
292
343
|
f'The selected {self.aggregation_parameters.hours_per_period=} does not match the time '
|
|
293
|
-
f'step size of {
|
|
344
|
+
f'step size of {dt_max} hours. It must be an integer multiple of {dt_max} hours.'
|
|
294
345
|
)
|
|
295
346
|
|
|
296
347
|
logger.info(f'{"":#^80}')
|
|
297
348
|
logger.info(f'{" Aggregating TimeSeries Data ":#^80}')
|
|
298
349
|
|
|
350
|
+
ds = self.flow_system.to_dataset()
|
|
351
|
+
|
|
352
|
+
temporaly_changing_ds = drop_constant_arrays(ds, dim='time')
|
|
353
|
+
|
|
299
354
|
# Aggregation - creation of aggregated timeseries:
|
|
300
355
|
self.aggregation = Aggregation(
|
|
301
|
-
original_data=
|
|
302
|
-
with_extra_timestep=False, with_constants=False
|
|
303
|
-
).to_dataframe(),
|
|
356
|
+
original_data=temporaly_changing_ds.to_dataframe(),
|
|
304
357
|
hours_per_time_step=float(dt_min),
|
|
305
358
|
hours_per_period=self.aggregation_parameters.hours_per_period,
|
|
306
359
|
nr_of_periods=self.aggregation_parameters.nr_of_periods,
|
|
307
|
-
weights=self.
|
|
360
|
+
weights=self.calculate_aggregation_weights(temporaly_changing_ds),
|
|
308
361
|
time_series_for_high_peaks=self.aggregation_parameters.labels_for_high_peaks,
|
|
309
362
|
time_series_for_low_peaks=self.aggregation_parameters.labels_for_low_peaks,
|
|
310
363
|
)
|
|
@@ -312,15 +365,155 @@ class AggregatedCalculation(FullCalculation):
|
|
|
312
365
|
self.aggregation.cluster()
|
|
313
366
|
self.aggregation.plot(show=True, save=self.folder / 'aggregation.html')
|
|
314
367
|
if self.aggregation_parameters.aggregate_data_and_fix_non_binary_vars:
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
368
|
+
ds = self.flow_system.to_dataset()
|
|
369
|
+
for name, series in self.aggregation.aggregated_data.items():
|
|
370
|
+
da = (
|
|
371
|
+
DataConverter.to_dataarray(series, self.flow_system.coords)
|
|
372
|
+
.rename(name)
|
|
373
|
+
.assign_attrs(ds[name].attrs)
|
|
374
|
+
)
|
|
375
|
+
if TimeSeriesData.is_timeseries_data(da):
|
|
376
|
+
da = TimeSeriesData.from_dataarray(da)
|
|
377
|
+
|
|
378
|
+
ds[name] = da
|
|
379
|
+
|
|
380
|
+
self.flow_system = FlowSystem.from_dataset(ds)
|
|
381
|
+
self.flow_system.connect_and_transform()
|
|
320
382
|
self.durations['aggregation'] = round(timeit.default_timer() - t_start_agg, 2)
|
|
321
383
|
|
|
384
|
+
@classmethod
|
|
385
|
+
def calculate_aggregation_weights(cls, ds: xr.Dataset) -> dict[str, float]:
|
|
386
|
+
"""Calculate weights for all datavars in the dataset. Weights are pulled from the attrs of the datavars."""
|
|
387
|
+
|
|
388
|
+
groups = [da.attrs['aggregation_group'] for da in ds.data_vars.values() if 'aggregation_group' in da.attrs]
|
|
389
|
+
group_counts = Counter(groups)
|
|
390
|
+
|
|
391
|
+
# Calculate weight for each group (1/count)
|
|
392
|
+
group_weights = {group: 1 / count for group, count in group_counts.items()}
|
|
393
|
+
|
|
394
|
+
weights = {}
|
|
395
|
+
for name, da in ds.data_vars.items():
|
|
396
|
+
group_weight = group_weights.get(da.attrs.get('aggregation_group'))
|
|
397
|
+
if group_weight is not None:
|
|
398
|
+
weights[name] = group_weight
|
|
399
|
+
else:
|
|
400
|
+
weights[name] = da.attrs.get('aggregation_weight', 1)
|
|
401
|
+
|
|
402
|
+
if np.all(np.isclose(list(weights.values()), 1, atol=1e-6)):
|
|
403
|
+
logger.info('All Aggregation weights were set to 1')
|
|
404
|
+
|
|
405
|
+
return weights
|
|
406
|
+
|
|
322
407
|
|
|
323
408
|
class SegmentedCalculation(Calculation):
|
|
409
|
+
"""Solve large optimization problems by dividing time horizon into (overlapping) segments.
|
|
410
|
+
|
|
411
|
+
This class addresses memory and computational limitations of large-scale optimization
|
|
412
|
+
problems by decomposing the time horizon into smaller overlapping segments that are
|
|
413
|
+
solved sequentially. Each segment uses final values from the previous segment as
|
|
414
|
+
initial conditions, ensuring dynamic continuity across the solution.
|
|
415
|
+
|
|
416
|
+
Key Concepts:
|
|
417
|
+
**Temporal Decomposition**: Divides long time horizons into manageable segments
|
|
418
|
+
**Overlapping Windows**: Segments share timesteps to improve storage dynamics
|
|
419
|
+
**Value Transfer**: Final states of one segment become initial states of the next
|
|
420
|
+
**Sequential Solving**: Each segment solved independently but with coupling
|
|
421
|
+
|
|
422
|
+
Limitations and Constraints:
|
|
423
|
+
**Investment Parameters**: InvestParameters are not supported in segmented calculations
|
|
424
|
+
as investment decisions must be made for the entire time horizon, not per segment.
|
|
425
|
+
|
|
426
|
+
**Global Constraints**: Time-horizon-wide constraints (flow_hours_total_min/max,
|
|
427
|
+
load_factor_min/max) may produce suboptimal results as they cannot be enforced
|
|
428
|
+
globally across segments.
|
|
429
|
+
|
|
430
|
+
**Storage Dynamics**: While overlap helps, storage optimization may be suboptimal
|
|
431
|
+
compared to full-horizon solutions due to limited foresight in each segment.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
name: Unique identifier for the calculation, used in result files and logging.
|
|
435
|
+
flow_system: The FlowSystem to optimize, containing all components, flows, and buses.
|
|
436
|
+
timesteps_per_segment: Number of timesteps in each segment (excluding overlap).
|
|
437
|
+
Must be > 2 to avoid internal side effects. Larger values provide better
|
|
438
|
+
optimization at the cost of memory and computation time.
|
|
439
|
+
overlap_timesteps: Number of additional timesteps added to each segment.
|
|
440
|
+
Improves storage optimization by providing lookahead. Higher values
|
|
441
|
+
improve solution quality but increase computational cost.
|
|
442
|
+
nr_of_previous_values: Number of previous timestep values to transfer between
|
|
443
|
+
segments for initialization. Typically 1 is sufficient.
|
|
444
|
+
folder: Directory for saving results. Defaults to current working directory + 'results'.
|
|
445
|
+
|
|
446
|
+
Examples:
|
|
447
|
+
Annual optimization with monthly segments:
|
|
448
|
+
|
|
449
|
+
```python
|
|
450
|
+
# 8760 hours annual data with monthly segments (730 hours) and 48-hour overlap
|
|
451
|
+
segmented_calc = SegmentedCalculation(
|
|
452
|
+
name='annual_energy_system',
|
|
453
|
+
flow_system=energy_system,
|
|
454
|
+
timesteps_per_segment=730, # ~1 month
|
|
455
|
+
overlap_timesteps=48, # 2 days overlap
|
|
456
|
+
folder=Path('results/segmented'),
|
|
457
|
+
)
|
|
458
|
+
segmented_calc.do_modeling_and_solve(solver='gurobi')
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Weekly optimization with daily overlap:
|
|
462
|
+
|
|
463
|
+
```python
|
|
464
|
+
# Weekly segments for detailed operational planning
|
|
465
|
+
weekly_calc = SegmentedCalculation(
|
|
466
|
+
name='weekly_operations',
|
|
467
|
+
flow_system=industrial_system,
|
|
468
|
+
timesteps_per_segment=168, # 1 week (hourly data)
|
|
469
|
+
overlap_timesteps=24, # 1 day overlap
|
|
470
|
+
nr_of_previous_values=1,
|
|
471
|
+
)
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Large-scale system with minimal overlap:
|
|
475
|
+
|
|
476
|
+
```python
|
|
477
|
+
# Large system with minimal overlap for computational efficiency
|
|
478
|
+
large_calc = SegmentedCalculation(
|
|
479
|
+
name='large_scale_grid',
|
|
480
|
+
flow_system=grid_system,
|
|
481
|
+
timesteps_per_segment=100, # Shorter segments
|
|
482
|
+
overlap_timesteps=5, # Minimal overlap
|
|
483
|
+
)
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Design Considerations:
|
|
487
|
+
**Segment Size**: Balance between solution quality and computational efficiency.
|
|
488
|
+
Larger segments provide better optimization but require more memory and time.
|
|
489
|
+
|
|
490
|
+
**Overlap Duration**: More overlap improves storage dynamics and reduces
|
|
491
|
+
end-effects but increases computational cost. Typically 5-10% of segment length.
|
|
492
|
+
|
|
493
|
+
**Storage Systems**: Systems with large storage components benefit from longer
|
|
494
|
+
overlaps to capture charge/discharge cycles effectively.
|
|
495
|
+
|
|
496
|
+
**Investment Decisions**: Use FullCalculation for problems requiring investment
|
|
497
|
+
optimization, as SegmentedCalculation cannot handle investment parameters.
|
|
498
|
+
|
|
499
|
+
Common Use Cases:
|
|
500
|
+
- **Annual Planning**: Long-term planning with seasonal variations
|
|
501
|
+
- **Large Networks**: Spatially or temporally large energy systems
|
|
502
|
+
- **Memory-Limited Systems**: When full optimization exceeds available memory
|
|
503
|
+
- **Operational Planning**: Detailed short-term optimization with limited foresight
|
|
504
|
+
- **Sensitivity Analysis**: Quick approximate solutions for parameter studies
|
|
505
|
+
|
|
506
|
+
Performance Tips:
|
|
507
|
+
- Start with FullCalculation and use this class if memory issues occur
|
|
508
|
+
- Use longer overlaps for systems with significant storage
|
|
509
|
+
- Monitor solution quality at segment boundaries for discontinuities
|
|
510
|
+
|
|
511
|
+
Warning:
|
|
512
|
+
The evaluation of the solution is a bit more complex than FullCalculation or AggregatedCalculation
|
|
513
|
+
due to the overlapping individual solutions.
|
|
514
|
+
|
|
515
|
+
"""
|
|
516
|
+
|
|
324
517
|
def __init__(
|
|
325
518
|
self,
|
|
326
519
|
name: str,
|
|
@@ -328,47 +521,25 @@ class SegmentedCalculation(Calculation):
|
|
|
328
521
|
timesteps_per_segment: int,
|
|
329
522
|
overlap_timesteps: int,
|
|
330
523
|
nr_of_previous_values: int = 1,
|
|
331
|
-
folder:
|
|
524
|
+
folder: pathlib.Path | None = None,
|
|
332
525
|
):
|
|
333
|
-
"""
|
|
334
|
-
Dividing and Modeling the problem in (overlapping) segments.
|
|
335
|
-
The final values of each Segment are recognized by the following segment, effectively coupling
|
|
336
|
-
charge_states and flow_rates between segments.
|
|
337
|
-
Because of this intersection, both modeling and solving is done in one step
|
|
338
|
-
|
|
339
|
-
Take care:
|
|
340
|
-
Parameters like InvestParameters, sum_of_flow_hours and other restrictions over the total time_series
|
|
341
|
-
don't really work in this Calculation. Lower bounds to such SUMS can lead to weird results.
|
|
342
|
-
This is NOT yet explicitly checked for...
|
|
343
|
-
|
|
344
|
-
Args:
|
|
345
|
-
name: name of calculation
|
|
346
|
-
flow_system: flow_system which should be calculated
|
|
347
|
-
timesteps_per_segment: The number of time_steps per individual segment (without the overlap)
|
|
348
|
-
overlap_timesteps: The number of time_steps that are added to each individual model. Used for better
|
|
349
|
-
results of storages)
|
|
350
|
-
folder: folder where results should be saved. If None, then the current working directory is used.
|
|
351
|
-
"""
|
|
352
526
|
super().__init__(name, flow_system, folder=folder)
|
|
353
527
|
self.timesteps_per_segment = timesteps_per_segment
|
|
354
528
|
self.overlap_timesteps = overlap_timesteps
|
|
355
529
|
self.nr_of_previous_values = nr_of_previous_values
|
|
356
|
-
self.sub_calculations:
|
|
357
|
-
|
|
358
|
-
self.all_timesteps = self.flow_system.time_series_collection._full_timesteps
|
|
359
|
-
self.all_timesteps_extra = self.flow_system.time_series_collection._full_timesteps_extra
|
|
530
|
+
self.sub_calculations: list[FullCalculation] = []
|
|
360
531
|
|
|
361
532
|
self.segment_names = [
|
|
362
533
|
f'Segment_{i + 1}' for i in range(math.ceil(len(self.all_timesteps) / self.timesteps_per_segment))
|
|
363
534
|
]
|
|
364
|
-
self.
|
|
535
|
+
self._timesteps_per_segment = self._calculate_timesteps_per_segment()
|
|
365
536
|
|
|
366
537
|
assert timesteps_per_segment > 2, 'The Segment length must be greater 2, due to unwanted internal side effects'
|
|
367
538
|
assert self.timesteps_per_segment_with_overlap <= len(self.all_timesteps), (
|
|
368
539
|
f'{self.timesteps_per_segment_with_overlap=} cant be greater than the total length {len(self.all_timesteps)}'
|
|
369
540
|
)
|
|
370
541
|
|
|
371
|
-
self.flow_system._connect_network() # Connect network to ensure that all
|
|
542
|
+
self.flow_system._connect_network() # Connect network to ensure that all Flows know their Component
|
|
372
543
|
# Storing all original start values
|
|
373
544
|
self._original_start_values = {
|
|
374
545
|
**{flow.label_full: flow.previous_flow_rate for flow in self.flow_system.flows.values()},
|
|
@@ -378,106 +549,120 @@ class SegmentedCalculation(Calculation):
|
|
|
378
549
|
if isinstance(comp, Storage)
|
|
379
550
|
},
|
|
380
551
|
}
|
|
381
|
-
self._transfered_start_values:
|
|
382
|
-
|
|
383
|
-
def do_modeling_and_solve(
|
|
384
|
-
self, solver: _Solver, log_file: Optional[pathlib.Path] = None, log_main_results: bool = False
|
|
385
|
-
):
|
|
386
|
-
logger.info(f'{"":#^80}')
|
|
387
|
-
logger.info(f'{" Segmented Solving ":#^80}')
|
|
552
|
+
self._transfered_start_values: list[dict[str, Any]] = []
|
|
388
553
|
|
|
554
|
+
def _create_sub_calculations(self):
|
|
389
555
|
for i, (segment_name, timesteps_of_segment) in enumerate(
|
|
390
|
-
zip(self.segment_names, self.
|
|
556
|
+
zip(self.segment_names, self._timesteps_per_segment, strict=True)
|
|
391
557
|
):
|
|
392
|
-
|
|
393
|
-
|
|
558
|
+
calc = FullCalculation(f'{self.name}-{segment_name}', self.flow_system.sel(time=timesteps_of_segment))
|
|
559
|
+
calc.flow_system._connect_network() # Connect to have Correct names of Flows!
|
|
394
560
|
|
|
561
|
+
self.sub_calculations.append(calc)
|
|
395
562
|
logger.info(
|
|
396
563
|
f'{segment_name} [{i + 1:>2}/{len(self.segment_names):<2}] '
|
|
397
564
|
f'({timesteps_of_segment[0]} -> {timesteps_of_segment[-1]}):'
|
|
398
565
|
)
|
|
399
566
|
|
|
400
|
-
|
|
401
|
-
|
|
567
|
+
def do_modeling_and_solve(
|
|
568
|
+
self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool = False
|
|
569
|
+
) -> SegmentedCalculation:
|
|
570
|
+
logger.info(f'{"":#^80}')
|
|
571
|
+
logger.info(f'{" Segmented Solving ":#^80}')
|
|
572
|
+
self._create_sub_calculations()
|
|
573
|
+
|
|
574
|
+
for i, calculation in enumerate(self.sub_calculations):
|
|
575
|
+
logger.info(
|
|
576
|
+
f'{self.segment_names[i]} [{i + 1:>2}/{len(self.segment_names):<2}] '
|
|
577
|
+
f'({calculation.flow_system.timesteps[0]} -> {calculation.flow_system.timesteps[-1]}):'
|
|
402
578
|
)
|
|
403
|
-
|
|
579
|
+
|
|
580
|
+
if i > 0 and self.nr_of_previous_values > 0:
|
|
581
|
+
self._transfer_start_values(i)
|
|
582
|
+
|
|
404
583
|
calculation.do_modeling()
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
584
|
+
|
|
585
|
+
# Warn about Investments, but only in fist run
|
|
586
|
+
if i == 0:
|
|
587
|
+
invest_elements = [
|
|
588
|
+
model.label_full
|
|
589
|
+
for component in calculation.flow_system.components.values()
|
|
590
|
+
for model in component.submodel.all_submodels
|
|
591
|
+
if isinstance(model, InvestmentModel)
|
|
592
|
+
]
|
|
593
|
+
if invest_elements:
|
|
594
|
+
logger.critical(
|
|
595
|
+
f'Investments are not supported in Segmented Calculation! '
|
|
596
|
+
f'Following InvestmentModels were found: {invest_elements}'
|
|
597
|
+
)
|
|
598
|
+
|
|
416
599
|
calculation.solve(
|
|
417
600
|
solver,
|
|
418
601
|
log_file=pathlib.Path(log_file) if log_file is not None else self.folder / f'{self.name}.log',
|
|
419
602
|
log_main_results=log_main_results,
|
|
420
603
|
)
|
|
421
604
|
|
|
422
|
-
self._reset_start_values()
|
|
423
|
-
|
|
424
605
|
for calc in self.sub_calculations:
|
|
425
606
|
for key, value in calc.durations.items():
|
|
426
607
|
self.durations[key] += value
|
|
427
608
|
|
|
428
609
|
self.results = SegmentedCalculationResults.from_calculation(self)
|
|
429
610
|
|
|
430
|
-
|
|
611
|
+
return self
|
|
612
|
+
|
|
613
|
+
def _transfer_start_values(self, i: int):
|
|
431
614
|
"""
|
|
432
615
|
This function gets the last values of the previous solved segment and
|
|
433
616
|
inserts them as start values for the next segment
|
|
434
617
|
"""
|
|
435
|
-
timesteps_of_prior_segment = self.
|
|
618
|
+
timesteps_of_prior_segment = self.sub_calculations[i - 1].flow_system.timesteps_extra
|
|
436
619
|
|
|
437
|
-
start = self.
|
|
620
|
+
start = self.sub_calculations[i].flow_system.timesteps[0]
|
|
438
621
|
start_previous_values = timesteps_of_prior_segment[self.timesteps_per_segment - self.nr_of_previous_values]
|
|
439
622
|
end_previous_values = timesteps_of_prior_segment[self.timesteps_per_segment - 1]
|
|
440
623
|
|
|
441
624
|
logger.debug(
|
|
442
|
-
f'
|
|
625
|
+
f'Start of next segment: {start}. Indices of previous values: {start_previous_values} -> {end_previous_values}'
|
|
443
626
|
)
|
|
627
|
+
current_flow_system = self.sub_calculations[i - 1].flow_system
|
|
628
|
+
next_flow_system = self.sub_calculations[i].flow_system
|
|
629
|
+
|
|
444
630
|
start_values_of_this_segment = {}
|
|
445
|
-
|
|
446
|
-
|
|
631
|
+
|
|
632
|
+
for current_flow in current_flow_system.flows.values():
|
|
633
|
+
next_flow = next_flow_system.flows[current_flow.label_full]
|
|
634
|
+
next_flow.previous_flow_rate = current_flow.submodel.flow_rate.solution.sel(
|
|
447
635
|
time=slice(start_previous_values, end_previous_values)
|
|
448
636
|
).values
|
|
449
|
-
start_values_of_this_segment[
|
|
450
|
-
for comp in self.flow_system.components.values():
|
|
451
|
-
if isinstance(comp, Storage):
|
|
452
|
-
comp.initial_charge_state = comp.model.charge_state.solution.sel(time=start).item()
|
|
453
|
-
start_values_of_this_segment[comp.label_full] = comp.initial_charge_state
|
|
637
|
+
start_values_of_this_segment[current_flow.label_full] = next_flow.previous_flow_rate
|
|
454
638
|
|
|
455
|
-
|
|
639
|
+
for current_comp in current_flow_system.components.values():
|
|
640
|
+
next_comp = next_flow_system.components[current_comp.label_full]
|
|
641
|
+
if isinstance(next_comp, Storage):
|
|
642
|
+
next_comp.initial_charge_state = current_comp.submodel.charge_state.solution.sel(time=start).item()
|
|
643
|
+
start_values_of_this_segment[current_comp.label_full] = next_comp.initial_charge_state
|
|
456
644
|
|
|
457
|
-
|
|
458
|
-
"""This resets the start values of all Elements to its original state"""
|
|
459
|
-
for flow in self.flow_system.flows.values():
|
|
460
|
-
flow.previous_flow_rate = self._original_start_values[flow.label_full]
|
|
461
|
-
for comp in self.flow_system.components.values():
|
|
462
|
-
if isinstance(comp, Storage):
|
|
463
|
-
comp.initial_charge_state = self._original_start_values[comp.label_full]
|
|
645
|
+
self._transfered_start_values.append(start_values_of_this_segment)
|
|
464
646
|
|
|
465
|
-
def
|
|
466
|
-
|
|
647
|
+
def _calculate_timesteps_per_segment(self) -> list[pd.DatetimeIndex]:
|
|
648
|
+
timesteps_per_segment = []
|
|
467
649
|
for i, _ in enumerate(self.segment_names):
|
|
468
650
|
start = self.timesteps_per_segment * i
|
|
469
651
|
end = min(start + self.timesteps_per_segment_with_overlap, len(self.all_timesteps))
|
|
470
|
-
|
|
471
|
-
return
|
|
652
|
+
timesteps_per_segment.append(self.all_timesteps[start:end])
|
|
653
|
+
return timesteps_per_segment
|
|
472
654
|
|
|
473
655
|
@property
|
|
474
656
|
def timesteps_per_segment_with_overlap(self):
|
|
475
657
|
return self.timesteps_per_segment + self.overlap_timesteps
|
|
476
658
|
|
|
477
659
|
@property
|
|
478
|
-
def start_values_of_segments(self) ->
|
|
660
|
+
def start_values_of_segments(self) -> list[dict[str, Any]]:
|
|
479
661
|
"""Gives an overview of the start values of all Segments"""
|
|
480
|
-
return {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
662
|
+
return [{name: value for name, value in self._original_start_values.items()}] + [
|
|
663
|
+
start_values for start_values in self._transfered_start_values
|
|
664
|
+
]
|
|
665
|
+
|
|
666
|
+
@property
|
|
667
|
+
def all_timesteps(self) -> pd.DatetimeIndex:
|
|
668
|
+
return self.flow_system.timesteps
|