flixopt 2.2.0rc2__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of flixopt might be problematic. Click here for more details.
- flixopt/__init__.py +33 -4
- flixopt/aggregation.py +60 -80
- flixopt/calculation.py +395 -178
- flixopt/commons.py +1 -10
- flixopt/components.py +939 -448
- flixopt/config.py +553 -191
- flixopt/core.py +513 -846
- flixopt/effects.py +644 -178
- flixopt/elements.py +610 -355
- flixopt/features.py +394 -966
- flixopt/flow_system.py +736 -219
- flixopt/interface.py +1104 -302
- flixopt/io.py +103 -79
- flixopt/linear_converters.py +387 -95
- flixopt/modeling.py +759 -0
- flixopt/network_app.py +73 -39
- flixopt/plotting.py +294 -138
- flixopt/results.py +1253 -299
- flixopt/solvers.py +25 -21
- flixopt/structure.py +938 -396
- flixopt/utils.py +38 -12
- flixopt-3.0.0.dist-info/METADATA +209 -0
- flixopt-3.0.0.dist-info/RECORD +26 -0
- 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 -61
- 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/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/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.0rc2.dist-info/METADATA +0 -167
- flixopt-2.2.0rc2.dist-info/RECORD +0 -54
- flixopt-2.2.0rc2.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/extract_release_notes.py +0 -45
- scripts/gen_ref_pages.py +0 -54
- tests/ressources/Zeitreihen2020.csv +0 -35137
- {flixopt-2.2.0rc2.dist-info → flixopt-3.0.0.dist-info}/WHEEL +0 -0
- {flixopt-2.2.0rc2.dist-info → flixopt-3.0.0.dist-info}/licenses/LICENSE +0 -0
flixopt/calculation.py
CHANGED
|
@@ -1,21 +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
|
|
18
|
+
from collections import Counter
|
|
19
|
+
from typing import TYPE_CHECKING, Annotated, Any
|
|
16
20
|
|
|
17
21
|
import numpy as np
|
|
18
|
-
import pandas as pd
|
|
19
22
|
import yaml
|
|
20
23
|
|
|
21
24
|
from . import io as fx_io
|
|
@@ -23,13 +26,18 @@ from . import utils as utils
|
|
|
23
26
|
from .aggregation import AggregationModel, AggregationParameters
|
|
24
27
|
from .components import Storage
|
|
25
28
|
from .config import CONFIG
|
|
26
|
-
from .core import Scalar
|
|
27
|
-
from .elements import Component
|
|
29
|
+
from .core import DataConverter, Scalar, TimeSeriesData, drop_constant_arrays
|
|
28
30
|
from .features import InvestmentModel
|
|
29
31
|
from .flow_system import FlowSystem
|
|
30
32
|
from .results import CalculationResults, SegmentedCalculationResults
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
33
41
|
|
|
34
42
|
logger = logging.getLogger('flixopt')
|
|
35
43
|
|
|
@@ -37,85 +45,112 @@ logger = logging.getLogger('flixopt')
|
|
|
37
45
|
class Calculation:
|
|
38
46
|
"""
|
|
39
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.
|
|
40
55
|
"""
|
|
41
56
|
|
|
42
57
|
def __init__(
|
|
43
58
|
self,
|
|
44
59
|
name: str,
|
|
45
60
|
flow_system: FlowSystem,
|
|
46
|
-
active_timesteps:
|
|
47
|
-
|
|
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,
|
|
48
67
|
):
|
|
49
|
-
"""
|
|
50
|
-
Args:
|
|
51
|
-
name: name of calculation
|
|
52
|
-
flow_system: flow_system which should be calculated
|
|
53
|
-
active_timesteps: list with indices, which should be used for calculation. If None, then all timesteps are used.
|
|
54
|
-
folder: folder where results should be saved. If None, then the current working directory is used.
|
|
55
|
-
"""
|
|
56
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
|
+
|
|
76
|
+
if active_timesteps is not None:
|
|
77
|
+
warnings.warn(
|
|
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.',
|
|
81
|
+
DeprecationWarning,
|
|
82
|
+
stacklevel=2,
|
|
83
|
+
)
|
|
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
|
+
|
|
57
90
|
self.flow_system = flow_system
|
|
58
|
-
self.model:
|
|
59
|
-
self.active_timesteps = active_timesteps
|
|
91
|
+
self.model: FlowSystemModel | None = None
|
|
60
92
|
|
|
61
93
|
self.durations = {'modeling': 0.0, 'solving': 0.0, 'saving': 0.0}
|
|
62
94
|
self.folder = pathlib.Path.cwd() / 'results' if folder is None else pathlib.Path(folder)
|
|
63
|
-
self.results:
|
|
95
|
+
self.results: CalculationResults | None = None
|
|
64
96
|
|
|
65
97
|
if self.folder.exists() and not self.folder.is_dir():
|
|
66
98
|
raise NotADirectoryError(f'Path {self.folder} exists and is not a directory.')
|
|
67
99
|
self.folder.mkdir(parents=False, exist_ok=True)
|
|
68
100
|
|
|
101
|
+
self._modeled = False
|
|
102
|
+
|
|
69
103
|
@property
|
|
70
|
-
def main_results(self) ->
|
|
104
|
+
def main_results(self) -> dict[str, Scalar | dict]:
|
|
71
105
|
from flixopt.features import InvestmentModel
|
|
72
106
|
|
|
73
|
-
|
|
107
|
+
main_results = {
|
|
74
108
|
'Objective': self.model.objective.value,
|
|
75
|
-
'Penalty':
|
|
109
|
+
'Penalty': self.model.effects.penalty.total.solution.values,
|
|
76
110
|
'Effects': {
|
|
77
111
|
f'{effect.label} [{effect.unit}]': {
|
|
78
|
-
'
|
|
79
|
-
'
|
|
80
|
-
'total':
|
|
112
|
+
'temporal': effect.submodel.temporal.total.solution.values,
|
|
113
|
+
'periodic': effect.submodel.periodic.total.solution.values,
|
|
114
|
+
'total': effect.submodel.total.solution.values,
|
|
81
115
|
}
|
|
82
116
|
for effect in self.flow_system.effects
|
|
83
117
|
},
|
|
84
118
|
'Invest-Decisions': {
|
|
85
119
|
'Invested': {
|
|
86
|
-
model.label_of_element:
|
|
120
|
+
model.label_of_element: model.size.solution
|
|
87
121
|
for component in self.flow_system.components.values()
|
|
88
|
-
for model in component.
|
|
89
|
-
if isinstance(model, InvestmentModel) and
|
|
122
|
+
for model in component.submodel.all_submodels
|
|
123
|
+
if isinstance(model, InvestmentModel) and model.size.solution.max() >= CONFIG.Modeling.epsilon
|
|
90
124
|
},
|
|
91
125
|
'Not invested': {
|
|
92
|
-
model.label_of_element:
|
|
126
|
+
model.label_of_element: model.size.solution
|
|
93
127
|
for component in self.flow_system.components.values()
|
|
94
|
-
for model in component.
|
|
95
|
-
if isinstance(model, InvestmentModel) and
|
|
128
|
+
for model in component.submodel.all_submodels
|
|
129
|
+
if isinstance(model, InvestmentModel) and model.size.solution.max() < CONFIG.Modeling.epsilon
|
|
96
130
|
},
|
|
97
131
|
},
|
|
98
132
|
'Buses with excess': [
|
|
99
133
|
{
|
|
100
134
|
bus.label_full: {
|
|
101
|
-
'input':
|
|
102
|
-
'output':
|
|
135
|
+
'input': bus.submodel.excess_input.solution.sum('time'),
|
|
136
|
+
'output': bus.submodel.excess_output.solution.sum('time'),
|
|
103
137
|
}
|
|
104
138
|
}
|
|
105
139
|
for bus in self.flow_system.buses.values()
|
|
106
140
|
if bus.with_excess
|
|
107
141
|
and (
|
|
108
|
-
|
|
109
|
-
or float(np.sum(bus.model.excess_output.solution.values)) > 1e-3
|
|
142
|
+
bus.submodel.excess_input.solution.sum() > 1e-3 or bus.submodel.excess_output.solution.sum() > 1e-3
|
|
110
143
|
)
|
|
111
144
|
],
|
|
112
145
|
}
|
|
113
146
|
|
|
147
|
+
return utils.round_nested_floats(main_results)
|
|
148
|
+
|
|
114
149
|
@property
|
|
115
150
|
def summary(self):
|
|
116
151
|
return {
|
|
117
152
|
'Name': self.name,
|
|
118
|
-
'Number of timesteps': len(self.flow_system.
|
|
153
|
+
'Number of timesteps': len(self.flow_system.timesteps),
|
|
119
154
|
'Calculation Type': self.__class__.__name__,
|
|
120
155
|
'Constraints': self.model.constraints.ncons,
|
|
121
156
|
'Variables': self.model.variables.nvars,
|
|
@@ -124,23 +159,75 @@ class Calculation:
|
|
|
124
159
|
'Config': CONFIG.to_dict(),
|
|
125
160
|
}
|
|
126
161
|
|
|
162
|
+
@property
|
|
163
|
+
def active_timesteps(self) -> pd.DatetimeIndex:
|
|
164
|
+
warnings.warn(
|
|
165
|
+
'active_timesteps is deprecated. Use flow_system.sel(time=...) or flow_system.isel(time=...) instead.',
|
|
166
|
+
DeprecationWarning,
|
|
167
|
+
stacklevel=2,
|
|
168
|
+
)
|
|
169
|
+
return self._active_timesteps
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def modeled(self) -> bool:
|
|
173
|
+
return True if self.model is not None else False
|
|
174
|
+
|
|
127
175
|
|
|
128
176
|
class FullCalculation(Calculation):
|
|
129
177
|
"""
|
|
130
|
-
|
|
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.
|
|
131
189
|
"""
|
|
132
190
|
|
|
133
|
-
def do_modeling(self) ->
|
|
191
|
+
def do_modeling(self) -> FullCalculation:
|
|
134
192
|
t_start = timeit.default_timer()
|
|
135
|
-
self.
|
|
193
|
+
self.flow_system.connect_and_transform()
|
|
136
194
|
|
|
137
|
-
self.model = self.flow_system.create_model()
|
|
195
|
+
self.model = self.flow_system.create_model(self.normalize_weights)
|
|
138
196
|
self.model.do_modeling()
|
|
139
197
|
|
|
140
198
|
self.durations['modeling'] = round(timeit.default_timer() - t_start, 2)
|
|
141
|
-
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}')
|
|
142
225
|
|
|
143
|
-
|
|
226
|
+
return self
|
|
227
|
+
|
|
228
|
+
def solve(
|
|
229
|
+
self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool = True
|
|
230
|
+
) -> FullCalculation:
|
|
144
231
|
t_start = timeit.default_timer()
|
|
145
232
|
|
|
146
233
|
self.model.solve(
|
|
@@ -163,11 +250,10 @@ class FullCalculation(Calculation):
|
|
|
163
250
|
|
|
164
251
|
# Log the formatted output
|
|
165
252
|
if log_main_results:
|
|
166
|
-
logger.info(f'{" Main Results ":#^80}')
|
|
167
253
|
logger.info(
|
|
168
|
-
'\n'
|
|
254
|
+
f'{" Main Results ":#^80}\n'
|
|
169
255
|
+ yaml.dump(
|
|
170
|
-
utils.
|
|
256
|
+
utils.round_nested_floats(self.main_results),
|
|
171
257
|
default_flow_style=False,
|
|
172
258
|
sort_keys=False,
|
|
173
259
|
allow_unicode=True,
|
|
@@ -177,16 +263,30 @@ class FullCalculation(Calculation):
|
|
|
177
263
|
|
|
178
264
|
self.results = CalculationResults.from_calculation(self)
|
|
179
265
|
|
|
180
|
-
|
|
181
|
-
self.flow_system.transform_data()
|
|
182
|
-
self.flow_system.time_series_collection.activate_timesteps(
|
|
183
|
-
active_timesteps=self.active_timesteps,
|
|
184
|
-
)
|
|
266
|
+
return self
|
|
185
267
|
|
|
186
268
|
|
|
187
269
|
class AggregatedCalculation(FullCalculation):
|
|
188
270
|
"""
|
|
189
|
-
|
|
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
|
|
190
290
|
"""
|
|
191
291
|
|
|
192
292
|
def __init__(
|
|
@@ -194,45 +294,35 @@ class AggregatedCalculation(FullCalculation):
|
|
|
194
294
|
name: str,
|
|
195
295
|
flow_system: FlowSystem,
|
|
196
296
|
aggregation_parameters: AggregationParameters,
|
|
197
|
-
components_to_clusterize:
|
|
198
|
-
active_timesteps:
|
|
199
|
-
|
|
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,
|
|
200
303
|
):
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
1. Aggregating TimeSeriesData via typical periods using tsam.
|
|
204
|
-
2. Equalizing variables of typical periods.
|
|
205
|
-
Args:
|
|
206
|
-
name: name of calculation
|
|
207
|
-
flow_system: flow_system which should be calculated
|
|
208
|
-
aggregation_parameters: Parameters for aggregation. See documentation of AggregationParameters class.
|
|
209
|
-
components_to_clusterize: List of Components to perform aggregation on. If None, then all components are aggregated.
|
|
210
|
-
This means, teh variables in the components are equalized to each other, according to the typical periods
|
|
211
|
-
computed in the DataAggregation
|
|
212
|
-
active_timesteps: pd.DatetimeIndex or None
|
|
213
|
-
list with indices, which should be used for calculation. If None, then all timesteps are used.
|
|
214
|
-
folder: folder where results should be saved. If None, then the current working directory is used.
|
|
215
|
-
"""
|
|
304
|
+
if flow_system.scenarios is not None:
|
|
305
|
+
raise ValueError('Aggregation is not supported for scenarios yet. Please use FullCalculation instead.')
|
|
216
306
|
super().__init__(name, flow_system, active_timesteps, folder=folder)
|
|
217
307
|
self.aggregation_parameters = aggregation_parameters
|
|
218
308
|
self.components_to_clusterize = components_to_clusterize
|
|
219
309
|
self.aggregation = None
|
|
220
310
|
|
|
221
|
-
def do_modeling(self) ->
|
|
311
|
+
def do_modeling(self) -> AggregatedCalculation:
|
|
222
312
|
t_start = timeit.default_timer()
|
|
223
|
-
self.
|
|
313
|
+
self.flow_system.connect_and_transform()
|
|
224
314
|
self._perform_aggregation()
|
|
225
315
|
|
|
226
316
|
# Model the System
|
|
227
|
-
self.model = self.flow_system.create_model()
|
|
317
|
+
self.model = self.flow_system.create_model(self.normalize_weights)
|
|
228
318
|
self.model.do_modeling()
|
|
229
|
-
# Add Aggregation
|
|
319
|
+
# Add Aggregation Submodel after modeling the rest
|
|
230
320
|
self.aggregation = AggregationModel(
|
|
231
321
|
self.model, self.aggregation_parameters, self.flow_system, self.aggregation, self.components_to_clusterize
|
|
232
322
|
)
|
|
233
323
|
self.aggregation.do_modeling()
|
|
234
324
|
self.durations['modeling'] = round(timeit.default_timer() - t_start, 2)
|
|
235
|
-
return self
|
|
325
|
+
return self
|
|
236
326
|
|
|
237
327
|
def _perform_aggregation(self):
|
|
238
328
|
from .aggregation import Aggregation
|
|
@@ -240,41 +330,34 @@ class AggregatedCalculation(FullCalculation):
|
|
|
240
330
|
t_start_agg = timeit.default_timer()
|
|
241
331
|
|
|
242
332
|
# Validation
|
|
243
|
-
dt_min
|
|
244
|
-
|
|
245
|
-
np.max(self.flow_system.time_series_collection.hours_per_timestep),
|
|
246
|
-
)
|
|
333
|
+
dt_min = float(self.flow_system.hours_per_timestep.min().item())
|
|
334
|
+
dt_max = float(self.flow_system.hours_per_timestep.max().item())
|
|
247
335
|
if not dt_min == dt_max:
|
|
248
336
|
raise ValueError(
|
|
249
337
|
f'Aggregation failed due to inconsistent time step sizes:'
|
|
250
338
|
f'delta_t varies from {dt_min} to {dt_max} hours.'
|
|
251
339
|
)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
/ self.flow_system.time_series_collection.hours_per_timestep.max()
|
|
255
|
-
)
|
|
256
|
-
is_integer = (
|
|
257
|
-
self.aggregation_parameters.hours_per_period
|
|
258
|
-
% self.flow_system.time_series_collection.hours_per_timestep.max()
|
|
259
|
-
).item() == 0
|
|
260
|
-
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):
|
|
261
342
|
raise ValueError(
|
|
262
343
|
f'The selected {self.aggregation_parameters.hours_per_period=} does not match the time '
|
|
263
|
-
f'step size of {
|
|
344
|
+
f'step size of {dt_max} hours. It must be an integer multiple of {dt_max} hours.'
|
|
264
345
|
)
|
|
265
346
|
|
|
266
347
|
logger.info(f'{"":#^80}')
|
|
267
348
|
logger.info(f'{" Aggregating TimeSeries Data ":#^80}')
|
|
268
349
|
|
|
350
|
+
ds = self.flow_system.to_dataset()
|
|
351
|
+
|
|
352
|
+
temporaly_changing_ds = drop_constant_arrays(ds, dim='time')
|
|
353
|
+
|
|
269
354
|
# Aggregation - creation of aggregated timeseries:
|
|
270
355
|
self.aggregation = Aggregation(
|
|
271
|
-
original_data=
|
|
272
|
-
include_extra_timestep=False
|
|
273
|
-
), # Exclude last row (NaN)
|
|
356
|
+
original_data=temporaly_changing_ds.to_dataframe(),
|
|
274
357
|
hours_per_time_step=float(dt_min),
|
|
275
358
|
hours_per_period=self.aggregation_parameters.hours_per_period,
|
|
276
359
|
nr_of_periods=self.aggregation_parameters.nr_of_periods,
|
|
277
|
-
weights=self.
|
|
360
|
+
weights=self.calculate_aggregation_weights(temporaly_changing_ds),
|
|
278
361
|
time_series_for_high_peaks=self.aggregation_parameters.labels_for_high_peaks,
|
|
279
362
|
time_series_for_low_peaks=self.aggregation_parameters.labels_for_low_peaks,
|
|
280
363
|
)
|
|
@@ -282,13 +365,155 @@ class AggregatedCalculation(FullCalculation):
|
|
|
282
365
|
self.aggregation.cluster()
|
|
283
366
|
self.aggregation.plot(show=True, save=self.folder / 'aggregation.html')
|
|
284
367
|
if self.aggregation_parameters.aggregate_data_and_fix_non_binary_vars:
|
|
285
|
-
self.flow_system.
|
|
286
|
-
|
|
287
|
-
|
|
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()
|
|
288
382
|
self.durations['aggregation'] = round(timeit.default_timer() - t_start_agg, 2)
|
|
289
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
|
+
|
|
290
407
|
|
|
291
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
|
+
|
|
292
517
|
def __init__(
|
|
293
518
|
self,
|
|
294
519
|
name: str,
|
|
@@ -296,47 +521,25 @@ class SegmentedCalculation(Calculation):
|
|
|
296
521
|
timesteps_per_segment: int,
|
|
297
522
|
overlap_timesteps: int,
|
|
298
523
|
nr_of_previous_values: int = 1,
|
|
299
|
-
folder:
|
|
524
|
+
folder: pathlib.Path | None = None,
|
|
300
525
|
):
|
|
301
|
-
"""
|
|
302
|
-
Dividing and Modeling the problem in (overlapping) segments.
|
|
303
|
-
The final values of each Segment are recognized by the following segment, effectively coupling
|
|
304
|
-
charge_states and flow_rates between segments.
|
|
305
|
-
Because of this intersection, both modeling and solving is done in one step
|
|
306
|
-
|
|
307
|
-
Take care:
|
|
308
|
-
Parameters like InvestParameters, sum_of_flow_hours and other restrictions over the total time_series
|
|
309
|
-
don't really work in this Calculation. Lower bounds to such SUMS can lead to weird results.
|
|
310
|
-
This is NOT yet explicitly checked for...
|
|
311
|
-
|
|
312
|
-
Args:
|
|
313
|
-
name: name of calculation
|
|
314
|
-
flow_system: flow_system which should be calculated
|
|
315
|
-
timesteps_per_segment: The number of time_steps per individual segment (without the overlap)
|
|
316
|
-
overlap_timesteps: The number of time_steps that are added to each individual model. Used for better
|
|
317
|
-
results of storages)
|
|
318
|
-
folder: folder where results should be saved. If None, then the current working directory is used.
|
|
319
|
-
"""
|
|
320
526
|
super().__init__(name, flow_system, folder=folder)
|
|
321
527
|
self.timesteps_per_segment = timesteps_per_segment
|
|
322
528
|
self.overlap_timesteps = overlap_timesteps
|
|
323
529
|
self.nr_of_previous_values = nr_of_previous_values
|
|
324
|
-
self.sub_calculations:
|
|
325
|
-
|
|
326
|
-
self.all_timesteps = self.flow_system.time_series_collection.all_timesteps
|
|
327
|
-
self.all_timesteps_extra = self.flow_system.time_series_collection.all_timesteps_extra
|
|
530
|
+
self.sub_calculations: list[FullCalculation] = []
|
|
328
531
|
|
|
329
532
|
self.segment_names = [
|
|
330
533
|
f'Segment_{i + 1}' for i in range(math.ceil(len(self.all_timesteps) / self.timesteps_per_segment))
|
|
331
534
|
]
|
|
332
|
-
self.
|
|
535
|
+
self._timesteps_per_segment = self._calculate_timesteps_per_segment()
|
|
333
536
|
|
|
334
537
|
assert timesteps_per_segment > 2, 'The Segment length must be greater 2, due to unwanted internal side effects'
|
|
335
538
|
assert self.timesteps_per_segment_with_overlap <= len(self.all_timesteps), (
|
|
336
539
|
f'{self.timesteps_per_segment_with_overlap=} cant be greater than the total length {len(self.all_timesteps)}'
|
|
337
540
|
)
|
|
338
541
|
|
|
339
|
-
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
|
|
340
543
|
# Storing all original start values
|
|
341
544
|
self._original_start_values = {
|
|
342
545
|
**{flow.label_full: flow.previous_flow_rate for flow in self.flow_system.flows.values()},
|
|
@@ -346,106 +549,120 @@ class SegmentedCalculation(Calculation):
|
|
|
346
549
|
if isinstance(comp, Storage)
|
|
347
550
|
},
|
|
348
551
|
}
|
|
349
|
-
self._transfered_start_values:
|
|
350
|
-
|
|
351
|
-
def do_modeling_and_solve(
|
|
352
|
-
self, solver: _Solver, log_file: Optional[pathlib.Path] = None, log_main_results: bool = False
|
|
353
|
-
):
|
|
354
|
-
logger.info(f'{"":#^80}')
|
|
355
|
-
logger.info(f'{" Segmented Solving ":#^80}')
|
|
552
|
+
self._transfered_start_values: list[dict[str, Any]] = []
|
|
356
553
|
|
|
554
|
+
def _create_sub_calculations(self):
|
|
357
555
|
for i, (segment_name, timesteps_of_segment) in enumerate(
|
|
358
|
-
zip(self.segment_names, self.
|
|
556
|
+
zip(self.segment_names, self._timesteps_per_segment, strict=True)
|
|
359
557
|
):
|
|
360
|
-
|
|
361
|
-
|
|
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!
|
|
362
560
|
|
|
561
|
+
self.sub_calculations.append(calc)
|
|
363
562
|
logger.info(
|
|
364
563
|
f'{segment_name} [{i + 1:>2}/{len(self.segment_names):<2}] '
|
|
365
564
|
f'({timesteps_of_segment[0]} -> {timesteps_of_segment[-1]}):'
|
|
366
565
|
)
|
|
367
566
|
|
|
368
|
-
|
|
369
|
-
|
|
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]}):'
|
|
370
578
|
)
|
|
371
|
-
|
|
579
|
+
|
|
580
|
+
if i > 0 and self.nr_of_previous_values > 0:
|
|
581
|
+
self._transfer_start_values(i)
|
|
582
|
+
|
|
372
583
|
calculation.do_modeling()
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
+
|
|
384
599
|
calculation.solve(
|
|
385
600
|
solver,
|
|
386
601
|
log_file=pathlib.Path(log_file) if log_file is not None else self.folder / f'{self.name}.log',
|
|
387
602
|
log_main_results=log_main_results,
|
|
388
603
|
)
|
|
389
604
|
|
|
390
|
-
self._reset_start_values()
|
|
391
|
-
|
|
392
605
|
for calc in self.sub_calculations:
|
|
393
606
|
for key, value in calc.durations.items():
|
|
394
607
|
self.durations[key] += value
|
|
395
608
|
|
|
396
609
|
self.results = SegmentedCalculationResults.from_calculation(self)
|
|
397
610
|
|
|
398
|
-
|
|
611
|
+
return self
|
|
612
|
+
|
|
613
|
+
def _transfer_start_values(self, i: int):
|
|
399
614
|
"""
|
|
400
615
|
This function gets the last values of the previous solved segment and
|
|
401
616
|
inserts them as start values for the next segment
|
|
402
617
|
"""
|
|
403
|
-
timesteps_of_prior_segment = self.
|
|
618
|
+
timesteps_of_prior_segment = self.sub_calculations[i - 1].flow_system.timesteps_extra
|
|
404
619
|
|
|
405
|
-
start = self.
|
|
620
|
+
start = self.sub_calculations[i].flow_system.timesteps[0]
|
|
406
621
|
start_previous_values = timesteps_of_prior_segment[self.timesteps_per_segment - self.nr_of_previous_values]
|
|
407
622
|
end_previous_values = timesteps_of_prior_segment[self.timesteps_per_segment - 1]
|
|
408
623
|
|
|
409
624
|
logger.debug(
|
|
410
|
-
f'
|
|
625
|
+
f'Start of next segment: {start}. Indices of previous values: {start_previous_values} -> {end_previous_values}'
|
|
411
626
|
)
|
|
627
|
+
current_flow_system = self.sub_calculations[i - 1].flow_system
|
|
628
|
+
next_flow_system = self.sub_calculations[i].flow_system
|
|
629
|
+
|
|
412
630
|
start_values_of_this_segment = {}
|
|
413
|
-
|
|
414
|
-
|
|
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(
|
|
415
635
|
time=slice(start_previous_values, end_previous_values)
|
|
416
636
|
).values
|
|
417
|
-
start_values_of_this_segment[
|
|
418
|
-
for comp in self.flow_system.components.values():
|
|
419
|
-
if isinstance(comp, Storage):
|
|
420
|
-
comp.initial_charge_state = comp.model.charge_state.solution.sel(time=start).item()
|
|
421
|
-
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
|
|
422
638
|
|
|
423
|
-
|
|
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
|
|
424
644
|
|
|
425
|
-
|
|
426
|
-
"""This resets the start values of all Elements to its original state"""
|
|
427
|
-
for flow in self.flow_system.flows.values():
|
|
428
|
-
flow.previous_flow_rate = self._original_start_values[flow.label_full]
|
|
429
|
-
for comp in self.flow_system.components.values():
|
|
430
|
-
if isinstance(comp, Storage):
|
|
431
|
-
comp.initial_charge_state = self._original_start_values[comp.label_full]
|
|
645
|
+
self._transfered_start_values.append(start_values_of_this_segment)
|
|
432
646
|
|
|
433
|
-
def
|
|
434
|
-
|
|
647
|
+
def _calculate_timesteps_per_segment(self) -> list[pd.DatetimeIndex]:
|
|
648
|
+
timesteps_per_segment = []
|
|
435
649
|
for i, _ in enumerate(self.segment_names):
|
|
436
650
|
start = self.timesteps_per_segment * i
|
|
437
651
|
end = min(start + self.timesteps_per_segment_with_overlap, len(self.all_timesteps))
|
|
438
|
-
|
|
439
|
-
return
|
|
652
|
+
timesteps_per_segment.append(self.all_timesteps[start:end])
|
|
653
|
+
return timesteps_per_segment
|
|
440
654
|
|
|
441
655
|
@property
|
|
442
656
|
def timesteps_per_segment_with_overlap(self):
|
|
443
657
|
return self.timesteps_per_segment + self.overlap_timesteps
|
|
444
658
|
|
|
445
659
|
@property
|
|
446
|
-
def start_values_of_segments(self) ->
|
|
660
|
+
def start_values_of_segments(self) -> list[dict[str, Any]]:
|
|
447
661
|
"""Gives an overview of the start values of all Segments"""
|
|
448
|
-
return {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|