flixopt 3.0.1__py3-none-any.whl → 6.0.0rc7__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.
- flixopt/__init__.py +57 -49
- flixopt/carrier.py +159 -0
- flixopt/clustering/__init__.py +51 -0
- flixopt/clustering/base.py +1746 -0
- flixopt/clustering/intercluster_helpers.py +201 -0
- flixopt/color_processing.py +372 -0
- flixopt/comparison.py +819 -0
- flixopt/components.py +848 -270
- flixopt/config.py +853 -496
- flixopt/core.py +111 -98
- flixopt/effects.py +294 -284
- flixopt/elements.py +484 -223
- flixopt/features.py +220 -118
- flixopt/flow_system.py +2026 -389
- flixopt/interface.py +504 -286
- flixopt/io.py +1718 -55
- flixopt/linear_converters.py +291 -230
- flixopt/modeling.py +304 -181
- flixopt/network_app.py +2 -1
- flixopt/optimization.py +788 -0
- flixopt/optimize_accessor.py +373 -0
- flixopt/plot_result.py +143 -0
- flixopt/plotting.py +1177 -1034
- flixopt/results.py +1331 -372
- flixopt/solvers.py +12 -4
- flixopt/statistics_accessor.py +2412 -0
- flixopt/stats_accessor.py +75 -0
- flixopt/structure.py +954 -120
- flixopt/topology_accessor.py +676 -0
- flixopt/transform_accessor.py +2277 -0
- flixopt/types.py +120 -0
- flixopt-6.0.0rc7.dist-info/METADATA +290 -0
- flixopt-6.0.0rc7.dist-info/RECORD +36 -0
- {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/WHEEL +1 -1
- flixopt/aggregation.py +0 -382
- flixopt/calculation.py +0 -672
- flixopt/commons.py +0 -51
- flixopt/utils.py +0 -86
- flixopt-3.0.1.dist-info/METADATA +0 -209
- flixopt-3.0.1.dist-info/RECORD +0 -26
- {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/licenses/LICENSE +0 -0
- {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/top_level.txt +0 -0
flixopt/calculation.py
DELETED
|
@@ -1,672 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
This module contains the Calculation functionality for the flixopt framework.
|
|
3
|
-
It is used to calculate a FlowSystemModel for a given FlowSystem through a solver.
|
|
4
|
-
There are three different Calculation types:
|
|
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
|
-
This simplifies the mathematical model and usually speeds up the solving process.
|
|
8
|
-
3. SegmentedCalculation: Solves a FlowSystemModel for each individual Segment of the FlowSystem.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
import logging
|
|
14
|
-
import math
|
|
15
|
-
import pathlib
|
|
16
|
-
import timeit
|
|
17
|
-
import warnings
|
|
18
|
-
from collections import Counter
|
|
19
|
-
from typing import TYPE_CHECKING, Annotated, Any
|
|
20
|
-
|
|
21
|
-
import numpy as np
|
|
22
|
-
import yaml
|
|
23
|
-
|
|
24
|
-
from . import io as fx_io
|
|
25
|
-
from . import utils as utils
|
|
26
|
-
from .aggregation import Aggregation, AggregationModel, AggregationParameters
|
|
27
|
-
from .components import Storage
|
|
28
|
-
from .config import CONFIG
|
|
29
|
-
from .core import DataConverter, Scalar, TimeSeriesData, drop_constant_arrays
|
|
30
|
-
from .features import InvestmentModel
|
|
31
|
-
from .flow_system import FlowSystem
|
|
32
|
-
from .results import CalculationResults, SegmentedCalculationResults
|
|
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
|
|
41
|
-
|
|
42
|
-
logger = logging.getLogger('flixopt')
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class Calculation:
|
|
46
|
-
"""
|
|
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.
|
|
55
|
-
"""
|
|
56
|
-
|
|
57
|
-
def __init__(
|
|
58
|
-
self,
|
|
59
|
-
name: str,
|
|
60
|
-
flow_system: FlowSystem,
|
|
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,
|
|
67
|
-
):
|
|
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
|
-
|
|
90
|
-
self.flow_system = flow_system
|
|
91
|
-
self.model: FlowSystemModel | None = None
|
|
92
|
-
|
|
93
|
-
self.durations = {'modeling': 0.0, 'solving': 0.0, 'saving': 0.0}
|
|
94
|
-
self.folder = pathlib.Path.cwd() / 'results' if folder is None else pathlib.Path(folder)
|
|
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)
|
|
100
|
-
|
|
101
|
-
self._modeled = False
|
|
102
|
-
|
|
103
|
-
@property
|
|
104
|
-
def main_results(self) -> dict[str, Scalar | dict]:
|
|
105
|
-
from flixopt.features import InvestmentModel
|
|
106
|
-
|
|
107
|
-
main_results = {
|
|
108
|
-
'Objective': self.model.objective.value,
|
|
109
|
-
'Penalty': self.model.effects.penalty.total.solution.values,
|
|
110
|
-
'Effects': {
|
|
111
|
-
f'{effect.label} [{effect.unit}]': {
|
|
112
|
-
'temporal': effect.submodel.temporal.total.solution.values,
|
|
113
|
-
'periodic': effect.submodel.periodic.total.solution.values,
|
|
114
|
-
'total': effect.submodel.total.solution.values,
|
|
115
|
-
}
|
|
116
|
-
for effect in self.flow_system.effects
|
|
117
|
-
},
|
|
118
|
-
'Invest-Decisions': {
|
|
119
|
-
'Invested': {
|
|
120
|
-
model.label_of_element: model.size.solution
|
|
121
|
-
for component in self.flow_system.components.values()
|
|
122
|
-
for model in component.submodel.all_submodels
|
|
123
|
-
if isinstance(model, InvestmentModel) and model.size.solution.max() >= CONFIG.Modeling.epsilon
|
|
124
|
-
},
|
|
125
|
-
'Not invested': {
|
|
126
|
-
model.label_of_element: model.size.solution
|
|
127
|
-
for component in self.flow_system.components.values()
|
|
128
|
-
for model in component.submodel.all_submodels
|
|
129
|
-
if isinstance(model, InvestmentModel) and model.size.solution.max() < CONFIG.Modeling.epsilon
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
'Buses with excess': [
|
|
133
|
-
{
|
|
134
|
-
bus.label_full: {
|
|
135
|
-
'input': bus.submodel.excess_input.solution.sum('time'),
|
|
136
|
-
'output': bus.submodel.excess_output.solution.sum('time'),
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
for bus in self.flow_system.buses.values()
|
|
140
|
-
if bus.with_excess
|
|
141
|
-
and (
|
|
142
|
-
bus.submodel.excess_input.solution.sum() > 1e-3 or bus.submodel.excess_output.solution.sum() > 1e-3
|
|
143
|
-
)
|
|
144
|
-
],
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return utils.round_nested_floats(main_results)
|
|
148
|
-
|
|
149
|
-
@property
|
|
150
|
-
def summary(self):
|
|
151
|
-
return {
|
|
152
|
-
'Name': self.name,
|
|
153
|
-
'Number of timesteps': len(self.flow_system.timesteps),
|
|
154
|
-
'Calculation Type': self.__class__.__name__,
|
|
155
|
-
'Constraints': self.model.constraints.ncons,
|
|
156
|
-
'Variables': self.model.variables.nvars,
|
|
157
|
-
'Main Results': self.main_results,
|
|
158
|
-
'Durations': self.durations,
|
|
159
|
-
'Config': CONFIG.to_dict(),
|
|
160
|
-
}
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
class FullCalculation(Calculation):
|
|
177
|
-
"""
|
|
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.
|
|
189
|
-
"""
|
|
190
|
-
|
|
191
|
-
def do_modeling(self) -> FullCalculation:
|
|
192
|
-
t_start = timeit.default_timer()
|
|
193
|
-
self.flow_system.connect_and_transform()
|
|
194
|
-
|
|
195
|
-
self.model = self.flow_system.create_model(self.normalize_weights)
|
|
196
|
-
self.model.do_modeling()
|
|
197
|
-
|
|
198
|
-
self.durations['modeling'] = round(timeit.default_timer() - t_start, 2)
|
|
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
|
|
227
|
-
|
|
228
|
-
def solve(
|
|
229
|
-
self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool = True
|
|
230
|
-
) -> FullCalculation:
|
|
231
|
-
t_start = timeit.default_timer()
|
|
232
|
-
|
|
233
|
-
self.model.solve(
|
|
234
|
-
log_fn=pathlib.Path(log_file) if log_file is not None else self.folder / f'{self.name}.log',
|
|
235
|
-
solver_name=solver.name,
|
|
236
|
-
**solver.options,
|
|
237
|
-
)
|
|
238
|
-
self.durations['solving'] = round(timeit.default_timer() - t_start, 2)
|
|
239
|
-
|
|
240
|
-
if self.model.status == 'warning':
|
|
241
|
-
# Save the model and the flow_system to file in case of infeasibility
|
|
242
|
-
paths = fx_io.CalculationResultsPaths(self.folder, self.name)
|
|
243
|
-
from .io import document_linopy_model
|
|
244
|
-
|
|
245
|
-
document_linopy_model(self.model, paths.model_documentation)
|
|
246
|
-
self.flow_system.to_netcdf(paths.flow_system)
|
|
247
|
-
raise RuntimeError(
|
|
248
|
-
f'Model was infeasible. Please check {paths.model_documentation=} and {paths.flow_system=} for more information.'
|
|
249
|
-
)
|
|
250
|
-
|
|
251
|
-
# Log the formatted output
|
|
252
|
-
if log_main_results:
|
|
253
|
-
logger.info(
|
|
254
|
-
f'{" Main Results ":#^80}\n'
|
|
255
|
-
+ yaml.dump(
|
|
256
|
-
utils.round_nested_floats(self.main_results),
|
|
257
|
-
default_flow_style=False,
|
|
258
|
-
sort_keys=False,
|
|
259
|
-
allow_unicode=True,
|
|
260
|
-
indent=4,
|
|
261
|
-
)
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
self.results = CalculationResults.from_calculation(self)
|
|
265
|
-
|
|
266
|
-
return self
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
class AggregatedCalculation(FullCalculation):
|
|
270
|
-
"""
|
|
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
|
-
|
|
290
|
-
Attributes:
|
|
291
|
-
aggregation (Aggregation | None): Contains the clustered time series data
|
|
292
|
-
aggregation_model (AggregationModel | None): Contains Variables and Constraints that equalize clusters of the time series data
|
|
293
|
-
"""
|
|
294
|
-
|
|
295
|
-
def __init__(
|
|
296
|
-
self,
|
|
297
|
-
name: str,
|
|
298
|
-
flow_system: FlowSystem,
|
|
299
|
-
aggregation_parameters: AggregationParameters,
|
|
300
|
-
components_to_clusterize: list[Component] | None = None,
|
|
301
|
-
active_timesteps: Annotated[
|
|
302
|
-
pd.DatetimeIndex | None,
|
|
303
|
-
'DEPRECATED: Use flow_system.sel(time=...) or flow_system.isel(time=...) instead',
|
|
304
|
-
] = None,
|
|
305
|
-
folder: pathlib.Path | None = None,
|
|
306
|
-
):
|
|
307
|
-
if flow_system.scenarios is not None:
|
|
308
|
-
raise ValueError('Aggregation is not supported for scenarios yet. Please use FullCalculation instead.')
|
|
309
|
-
super().__init__(name, flow_system, active_timesteps, folder=folder)
|
|
310
|
-
self.aggregation_parameters = aggregation_parameters
|
|
311
|
-
self.components_to_clusterize = components_to_clusterize
|
|
312
|
-
self.aggregation: Aggregation | None = None
|
|
313
|
-
self.aggregation_model: AggregationModel | None = None
|
|
314
|
-
|
|
315
|
-
def do_modeling(self) -> AggregatedCalculation:
|
|
316
|
-
t_start = timeit.default_timer()
|
|
317
|
-
self.flow_system.connect_and_transform()
|
|
318
|
-
self._perform_aggregation()
|
|
319
|
-
|
|
320
|
-
# Model the System
|
|
321
|
-
self.model = self.flow_system.create_model(self.normalize_weights)
|
|
322
|
-
self.model.do_modeling()
|
|
323
|
-
# Add Aggregation Submodel after modeling the rest
|
|
324
|
-
self.aggregation_model = AggregationModel(
|
|
325
|
-
self.model, self.aggregation_parameters, self.flow_system, self.aggregation, self.components_to_clusterize
|
|
326
|
-
)
|
|
327
|
-
self.aggregation_model.do_modeling()
|
|
328
|
-
self.durations['modeling'] = round(timeit.default_timer() - t_start, 2)
|
|
329
|
-
return self
|
|
330
|
-
|
|
331
|
-
def _perform_aggregation(self):
|
|
332
|
-
from .aggregation import Aggregation
|
|
333
|
-
|
|
334
|
-
t_start_agg = timeit.default_timer()
|
|
335
|
-
|
|
336
|
-
# Validation
|
|
337
|
-
dt_min = float(self.flow_system.hours_per_timestep.min().item())
|
|
338
|
-
dt_max = float(self.flow_system.hours_per_timestep.max().item())
|
|
339
|
-
if not dt_min == dt_max:
|
|
340
|
-
raise ValueError(
|
|
341
|
-
f'Aggregation failed due to inconsistent time step sizes:'
|
|
342
|
-
f'delta_t varies from {dt_min} to {dt_max} hours.'
|
|
343
|
-
)
|
|
344
|
-
ratio = self.aggregation_parameters.hours_per_period / dt_max
|
|
345
|
-
if not np.isclose(ratio, round(ratio), atol=1e-9):
|
|
346
|
-
raise ValueError(
|
|
347
|
-
f'The selected {self.aggregation_parameters.hours_per_period=} does not match the time '
|
|
348
|
-
f'step size of {dt_max} hours. It must be an integer multiple of {dt_max} hours.'
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
logger.info(f'{"":#^80}')
|
|
352
|
-
logger.info(f'{" Aggregating TimeSeries Data ":#^80}')
|
|
353
|
-
|
|
354
|
-
ds = self.flow_system.to_dataset()
|
|
355
|
-
|
|
356
|
-
temporaly_changing_ds = drop_constant_arrays(ds, dim='time')
|
|
357
|
-
|
|
358
|
-
# Aggregation - creation of aggregated timeseries:
|
|
359
|
-
self.aggregation = Aggregation(
|
|
360
|
-
original_data=temporaly_changing_ds.to_dataframe(),
|
|
361
|
-
hours_per_time_step=float(dt_min),
|
|
362
|
-
hours_per_period=self.aggregation_parameters.hours_per_period,
|
|
363
|
-
nr_of_periods=self.aggregation_parameters.nr_of_periods,
|
|
364
|
-
weights=self.calculate_aggregation_weights(temporaly_changing_ds),
|
|
365
|
-
time_series_for_high_peaks=self.aggregation_parameters.labels_for_high_peaks,
|
|
366
|
-
time_series_for_low_peaks=self.aggregation_parameters.labels_for_low_peaks,
|
|
367
|
-
)
|
|
368
|
-
|
|
369
|
-
self.aggregation.cluster()
|
|
370
|
-
self.aggregation.plot(show=True, save=self.folder / 'aggregation.html')
|
|
371
|
-
if self.aggregation_parameters.aggregate_data_and_fix_non_binary_vars:
|
|
372
|
-
ds = self.flow_system.to_dataset()
|
|
373
|
-
for name, series in self.aggregation.aggregated_data.items():
|
|
374
|
-
da = (
|
|
375
|
-
DataConverter.to_dataarray(series, self.flow_system.coords)
|
|
376
|
-
.rename(name)
|
|
377
|
-
.assign_attrs(ds[name].attrs)
|
|
378
|
-
)
|
|
379
|
-
if TimeSeriesData.is_timeseries_data(da):
|
|
380
|
-
da = TimeSeriesData.from_dataarray(da)
|
|
381
|
-
|
|
382
|
-
ds[name] = da
|
|
383
|
-
|
|
384
|
-
self.flow_system = FlowSystem.from_dataset(ds)
|
|
385
|
-
self.flow_system.connect_and_transform()
|
|
386
|
-
self.durations['aggregation'] = round(timeit.default_timer() - t_start_agg, 2)
|
|
387
|
-
|
|
388
|
-
@classmethod
|
|
389
|
-
def calculate_aggregation_weights(cls, ds: xr.Dataset) -> dict[str, float]:
|
|
390
|
-
"""Calculate weights for all datavars in the dataset. Weights are pulled from the attrs of the datavars."""
|
|
391
|
-
|
|
392
|
-
groups = [da.attrs['aggregation_group'] for da in ds.data_vars.values() if 'aggregation_group' in da.attrs]
|
|
393
|
-
group_counts = Counter(groups)
|
|
394
|
-
|
|
395
|
-
# Calculate weight for each group (1/count)
|
|
396
|
-
group_weights = {group: 1 / count for group, count in group_counts.items()}
|
|
397
|
-
|
|
398
|
-
weights = {}
|
|
399
|
-
for name, da in ds.data_vars.items():
|
|
400
|
-
group_weight = group_weights.get(da.attrs.get('aggregation_group'))
|
|
401
|
-
if group_weight is not None:
|
|
402
|
-
weights[name] = group_weight
|
|
403
|
-
else:
|
|
404
|
-
weights[name] = da.attrs.get('aggregation_weight', 1)
|
|
405
|
-
|
|
406
|
-
if np.all(np.isclose(list(weights.values()), 1, atol=1e-6)):
|
|
407
|
-
logger.info('All Aggregation weights were set to 1')
|
|
408
|
-
|
|
409
|
-
return weights
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
class SegmentedCalculation(Calculation):
|
|
413
|
-
"""Solve large optimization problems by dividing time horizon into (overlapping) segments.
|
|
414
|
-
|
|
415
|
-
This class addresses memory and computational limitations of large-scale optimization
|
|
416
|
-
problems by decomposing the time horizon into smaller overlapping segments that are
|
|
417
|
-
solved sequentially. Each segment uses final values from the previous segment as
|
|
418
|
-
initial conditions, ensuring dynamic continuity across the solution.
|
|
419
|
-
|
|
420
|
-
Key Concepts:
|
|
421
|
-
**Temporal Decomposition**: Divides long time horizons into manageable segments
|
|
422
|
-
**Overlapping Windows**: Segments share timesteps to improve storage dynamics
|
|
423
|
-
**Value Transfer**: Final states of one segment become initial states of the next
|
|
424
|
-
**Sequential Solving**: Each segment solved independently but with coupling
|
|
425
|
-
|
|
426
|
-
Limitations and Constraints:
|
|
427
|
-
**Investment Parameters**: InvestParameters are not supported in segmented calculations
|
|
428
|
-
as investment decisions must be made for the entire time horizon, not per segment.
|
|
429
|
-
|
|
430
|
-
**Global Constraints**: Time-horizon-wide constraints (flow_hours_total_min/max,
|
|
431
|
-
load_factor_min/max) may produce suboptimal results as they cannot be enforced
|
|
432
|
-
globally across segments.
|
|
433
|
-
|
|
434
|
-
**Storage Dynamics**: While overlap helps, storage optimization may be suboptimal
|
|
435
|
-
compared to full-horizon solutions due to limited foresight in each segment.
|
|
436
|
-
|
|
437
|
-
Args:
|
|
438
|
-
name: Unique identifier for the calculation, used in result files and logging.
|
|
439
|
-
flow_system: The FlowSystem to optimize, containing all components, flows, and buses.
|
|
440
|
-
timesteps_per_segment: Number of timesteps in each segment (excluding overlap).
|
|
441
|
-
Must be > 2 to avoid internal side effects. Larger values provide better
|
|
442
|
-
optimization at the cost of memory and computation time.
|
|
443
|
-
overlap_timesteps: Number of additional timesteps added to each segment.
|
|
444
|
-
Improves storage optimization by providing lookahead. Higher values
|
|
445
|
-
improve solution quality but increase computational cost.
|
|
446
|
-
nr_of_previous_values: Number of previous timestep values to transfer between
|
|
447
|
-
segments for initialization. Typically 1 is sufficient.
|
|
448
|
-
folder: Directory for saving results. Defaults to current working directory + 'results'.
|
|
449
|
-
|
|
450
|
-
Examples:
|
|
451
|
-
Annual optimization with monthly segments:
|
|
452
|
-
|
|
453
|
-
```python
|
|
454
|
-
# 8760 hours annual data with monthly segments (730 hours) and 48-hour overlap
|
|
455
|
-
segmented_calc = SegmentedCalculation(
|
|
456
|
-
name='annual_energy_system',
|
|
457
|
-
flow_system=energy_system,
|
|
458
|
-
timesteps_per_segment=730, # ~1 month
|
|
459
|
-
overlap_timesteps=48, # 2 days overlap
|
|
460
|
-
folder=Path('results/segmented'),
|
|
461
|
-
)
|
|
462
|
-
segmented_calc.do_modeling_and_solve(solver='gurobi')
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
Weekly optimization with daily overlap:
|
|
466
|
-
|
|
467
|
-
```python
|
|
468
|
-
# Weekly segments for detailed operational planning
|
|
469
|
-
weekly_calc = SegmentedCalculation(
|
|
470
|
-
name='weekly_operations',
|
|
471
|
-
flow_system=industrial_system,
|
|
472
|
-
timesteps_per_segment=168, # 1 week (hourly data)
|
|
473
|
-
overlap_timesteps=24, # 1 day overlap
|
|
474
|
-
nr_of_previous_values=1,
|
|
475
|
-
)
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
Large-scale system with minimal overlap:
|
|
479
|
-
|
|
480
|
-
```python
|
|
481
|
-
# Large system with minimal overlap for computational efficiency
|
|
482
|
-
large_calc = SegmentedCalculation(
|
|
483
|
-
name='large_scale_grid',
|
|
484
|
-
flow_system=grid_system,
|
|
485
|
-
timesteps_per_segment=100, # Shorter segments
|
|
486
|
-
overlap_timesteps=5, # Minimal overlap
|
|
487
|
-
)
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
Design Considerations:
|
|
491
|
-
**Segment Size**: Balance between solution quality and computational efficiency.
|
|
492
|
-
Larger segments provide better optimization but require more memory and time.
|
|
493
|
-
|
|
494
|
-
**Overlap Duration**: More overlap improves storage dynamics and reduces
|
|
495
|
-
end-effects but increases computational cost. Typically 5-10% of segment length.
|
|
496
|
-
|
|
497
|
-
**Storage Systems**: Systems with large storage components benefit from longer
|
|
498
|
-
overlaps to capture charge/discharge cycles effectively.
|
|
499
|
-
|
|
500
|
-
**Investment Decisions**: Use FullCalculation for problems requiring investment
|
|
501
|
-
optimization, as SegmentedCalculation cannot handle investment parameters.
|
|
502
|
-
|
|
503
|
-
Common Use Cases:
|
|
504
|
-
- **Annual Planning**: Long-term planning with seasonal variations
|
|
505
|
-
- **Large Networks**: Spatially or temporally large energy systems
|
|
506
|
-
- **Memory-Limited Systems**: When full optimization exceeds available memory
|
|
507
|
-
- **Operational Planning**: Detailed short-term optimization with limited foresight
|
|
508
|
-
- **Sensitivity Analysis**: Quick approximate solutions for parameter studies
|
|
509
|
-
|
|
510
|
-
Performance Tips:
|
|
511
|
-
- Start with FullCalculation and use this class if memory issues occur
|
|
512
|
-
- Use longer overlaps for systems with significant storage
|
|
513
|
-
- Monitor solution quality at segment boundaries for discontinuities
|
|
514
|
-
|
|
515
|
-
Warning:
|
|
516
|
-
The evaluation of the solution is a bit more complex than FullCalculation or AggregatedCalculation
|
|
517
|
-
due to the overlapping individual solutions.
|
|
518
|
-
|
|
519
|
-
"""
|
|
520
|
-
|
|
521
|
-
def __init__(
|
|
522
|
-
self,
|
|
523
|
-
name: str,
|
|
524
|
-
flow_system: FlowSystem,
|
|
525
|
-
timesteps_per_segment: int,
|
|
526
|
-
overlap_timesteps: int,
|
|
527
|
-
nr_of_previous_values: int = 1,
|
|
528
|
-
folder: pathlib.Path | None = None,
|
|
529
|
-
):
|
|
530
|
-
super().__init__(name, flow_system, folder=folder)
|
|
531
|
-
self.timesteps_per_segment = timesteps_per_segment
|
|
532
|
-
self.overlap_timesteps = overlap_timesteps
|
|
533
|
-
self.nr_of_previous_values = nr_of_previous_values
|
|
534
|
-
self.sub_calculations: list[FullCalculation] = []
|
|
535
|
-
|
|
536
|
-
self.segment_names = [
|
|
537
|
-
f'Segment_{i + 1}' for i in range(math.ceil(len(self.all_timesteps) / self.timesteps_per_segment))
|
|
538
|
-
]
|
|
539
|
-
self._timesteps_per_segment = self._calculate_timesteps_per_segment()
|
|
540
|
-
|
|
541
|
-
assert timesteps_per_segment > 2, 'The Segment length must be greater 2, due to unwanted internal side effects'
|
|
542
|
-
assert self.timesteps_per_segment_with_overlap <= len(self.all_timesteps), (
|
|
543
|
-
f'{self.timesteps_per_segment_with_overlap=} cant be greater than the total length {len(self.all_timesteps)}'
|
|
544
|
-
)
|
|
545
|
-
|
|
546
|
-
self.flow_system._connect_network() # Connect network to ensure that all Flows know their Component
|
|
547
|
-
# Storing all original start values
|
|
548
|
-
self._original_start_values = {
|
|
549
|
-
**{flow.label_full: flow.previous_flow_rate for flow in self.flow_system.flows.values()},
|
|
550
|
-
**{
|
|
551
|
-
comp.label_full: comp.initial_charge_state
|
|
552
|
-
for comp in self.flow_system.components.values()
|
|
553
|
-
if isinstance(comp, Storage)
|
|
554
|
-
},
|
|
555
|
-
}
|
|
556
|
-
self._transfered_start_values: list[dict[str, Any]] = []
|
|
557
|
-
|
|
558
|
-
def _create_sub_calculations(self):
|
|
559
|
-
for i, (segment_name, timesteps_of_segment) in enumerate(
|
|
560
|
-
zip(self.segment_names, self._timesteps_per_segment, strict=True)
|
|
561
|
-
):
|
|
562
|
-
calc = FullCalculation(f'{self.name}-{segment_name}', self.flow_system.sel(time=timesteps_of_segment))
|
|
563
|
-
calc.flow_system._connect_network() # Connect to have Correct names of Flows!
|
|
564
|
-
|
|
565
|
-
self.sub_calculations.append(calc)
|
|
566
|
-
logger.info(
|
|
567
|
-
f'{segment_name} [{i + 1:>2}/{len(self.segment_names):<2}] '
|
|
568
|
-
f'({timesteps_of_segment[0]} -> {timesteps_of_segment[-1]}):'
|
|
569
|
-
)
|
|
570
|
-
|
|
571
|
-
def do_modeling_and_solve(
|
|
572
|
-
self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool = False
|
|
573
|
-
) -> SegmentedCalculation:
|
|
574
|
-
logger.info(f'{"":#^80}')
|
|
575
|
-
logger.info(f'{" Segmented Solving ":#^80}')
|
|
576
|
-
self._create_sub_calculations()
|
|
577
|
-
|
|
578
|
-
for i, calculation in enumerate(self.sub_calculations):
|
|
579
|
-
logger.info(
|
|
580
|
-
f'{self.segment_names[i]} [{i + 1:>2}/{len(self.segment_names):<2}] '
|
|
581
|
-
f'({calculation.flow_system.timesteps[0]} -> {calculation.flow_system.timesteps[-1]}):'
|
|
582
|
-
)
|
|
583
|
-
|
|
584
|
-
if i > 0 and self.nr_of_previous_values > 0:
|
|
585
|
-
self._transfer_start_values(i)
|
|
586
|
-
|
|
587
|
-
calculation.do_modeling()
|
|
588
|
-
|
|
589
|
-
# Warn about Investments, but only in fist run
|
|
590
|
-
if i == 0:
|
|
591
|
-
invest_elements = [
|
|
592
|
-
model.label_full
|
|
593
|
-
for component in calculation.flow_system.components.values()
|
|
594
|
-
for model in component.submodel.all_submodels
|
|
595
|
-
if isinstance(model, InvestmentModel)
|
|
596
|
-
]
|
|
597
|
-
if invest_elements:
|
|
598
|
-
logger.critical(
|
|
599
|
-
f'Investments are not supported in Segmented Calculation! '
|
|
600
|
-
f'Following InvestmentModels were found: {invest_elements}'
|
|
601
|
-
)
|
|
602
|
-
|
|
603
|
-
calculation.solve(
|
|
604
|
-
solver,
|
|
605
|
-
log_file=pathlib.Path(log_file) if log_file is not None else self.folder / f'{self.name}.log',
|
|
606
|
-
log_main_results=log_main_results,
|
|
607
|
-
)
|
|
608
|
-
|
|
609
|
-
for calc in self.sub_calculations:
|
|
610
|
-
for key, value in calc.durations.items():
|
|
611
|
-
self.durations[key] += value
|
|
612
|
-
|
|
613
|
-
self.results = SegmentedCalculationResults.from_calculation(self)
|
|
614
|
-
|
|
615
|
-
return self
|
|
616
|
-
|
|
617
|
-
def _transfer_start_values(self, i: int):
|
|
618
|
-
"""
|
|
619
|
-
This function gets the last values of the previous solved segment and
|
|
620
|
-
inserts them as start values for the next segment
|
|
621
|
-
"""
|
|
622
|
-
timesteps_of_prior_segment = self.sub_calculations[i - 1].flow_system.timesteps_extra
|
|
623
|
-
|
|
624
|
-
start = self.sub_calculations[i].flow_system.timesteps[0]
|
|
625
|
-
start_previous_values = timesteps_of_prior_segment[self.timesteps_per_segment - self.nr_of_previous_values]
|
|
626
|
-
end_previous_values = timesteps_of_prior_segment[self.timesteps_per_segment - 1]
|
|
627
|
-
|
|
628
|
-
logger.debug(
|
|
629
|
-
f'Start of next segment: {start}. Indices of previous values: {start_previous_values} -> {end_previous_values}'
|
|
630
|
-
)
|
|
631
|
-
current_flow_system = self.sub_calculations[i - 1].flow_system
|
|
632
|
-
next_flow_system = self.sub_calculations[i].flow_system
|
|
633
|
-
|
|
634
|
-
start_values_of_this_segment = {}
|
|
635
|
-
|
|
636
|
-
for current_flow in current_flow_system.flows.values():
|
|
637
|
-
next_flow = next_flow_system.flows[current_flow.label_full]
|
|
638
|
-
next_flow.previous_flow_rate = current_flow.submodel.flow_rate.solution.sel(
|
|
639
|
-
time=slice(start_previous_values, end_previous_values)
|
|
640
|
-
).values
|
|
641
|
-
start_values_of_this_segment[current_flow.label_full] = next_flow.previous_flow_rate
|
|
642
|
-
|
|
643
|
-
for current_comp in current_flow_system.components.values():
|
|
644
|
-
next_comp = next_flow_system.components[current_comp.label_full]
|
|
645
|
-
if isinstance(next_comp, Storage):
|
|
646
|
-
next_comp.initial_charge_state = current_comp.submodel.charge_state.solution.sel(time=start).item()
|
|
647
|
-
start_values_of_this_segment[current_comp.label_full] = next_comp.initial_charge_state
|
|
648
|
-
|
|
649
|
-
self._transfered_start_values.append(start_values_of_this_segment)
|
|
650
|
-
|
|
651
|
-
def _calculate_timesteps_per_segment(self) -> list[pd.DatetimeIndex]:
|
|
652
|
-
timesteps_per_segment = []
|
|
653
|
-
for i, _ in enumerate(self.segment_names):
|
|
654
|
-
start = self.timesteps_per_segment * i
|
|
655
|
-
end = min(start + self.timesteps_per_segment_with_overlap, len(self.all_timesteps))
|
|
656
|
-
timesteps_per_segment.append(self.all_timesteps[start:end])
|
|
657
|
-
return timesteps_per_segment
|
|
658
|
-
|
|
659
|
-
@property
|
|
660
|
-
def timesteps_per_segment_with_overlap(self):
|
|
661
|
-
return self.timesteps_per_segment + self.overlap_timesteps
|
|
662
|
-
|
|
663
|
-
@property
|
|
664
|
-
def start_values_of_segments(self) -> list[dict[str, Any]]:
|
|
665
|
-
"""Gives an overview of the start values of all Segments"""
|
|
666
|
-
return [{name: value for name, value in self._original_start_values.items()}] + [
|
|
667
|
-
start_values for start_values in self._transfered_start_values
|
|
668
|
-
]
|
|
669
|
-
|
|
670
|
-
@property
|
|
671
|
-
def all_timesteps(self) -> pd.DatetimeIndex:
|
|
672
|
-
return self.flow_system.timesteps
|