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.

Files changed (63) hide show
  1. flixopt/__init__.py +35 -1
  2. flixopt/aggregation.py +60 -81
  3. flixopt/calculation.py +381 -196
  4. flixopt/components.py +1022 -359
  5. flixopt/config.py +553 -191
  6. flixopt/core.py +475 -1315
  7. flixopt/effects.py +477 -214
  8. flixopt/elements.py +591 -344
  9. flixopt/features.py +403 -957
  10. flixopt/flow_system.py +781 -293
  11. flixopt/interface.py +1159 -189
  12. flixopt/io.py +50 -55
  13. flixopt/linear_converters.py +384 -92
  14. flixopt/modeling.py +759 -0
  15. flixopt/network_app.py +789 -0
  16. flixopt/plotting.py +273 -135
  17. flixopt/results.py +639 -383
  18. flixopt/solvers.py +25 -21
  19. flixopt/structure.py +928 -442
  20. flixopt/utils.py +34 -5
  21. flixopt-3.0.0.dist-info/METADATA +209 -0
  22. flixopt-3.0.0.dist-info/RECORD +26 -0
  23. {flixopt-2.2.0b0.dist-info → flixopt-3.0.0.dist-info}/WHEEL +1 -1
  24. flixopt-3.0.0.dist-info/top_level.txt +1 -0
  25. docs/examples/00-Minimal Example.md +0 -5
  26. docs/examples/01-Basic Example.md +0 -5
  27. docs/examples/02-Complex Example.md +0 -10
  28. docs/examples/03-Calculation Modes.md +0 -5
  29. docs/examples/index.md +0 -5
  30. docs/faq/contribute.md +0 -49
  31. docs/faq/index.md +0 -3
  32. docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
  33. docs/images/architecture_flixOpt.png +0 -0
  34. docs/images/flixopt-icon.svg +0 -1
  35. docs/javascripts/mathjax.js +0 -18
  36. docs/release-notes/_template.txt +0 -32
  37. docs/release-notes/index.md +0 -7
  38. docs/release-notes/v2.0.0.md +0 -93
  39. docs/release-notes/v2.0.1.md +0 -12
  40. docs/release-notes/v2.1.0.md +0 -31
  41. docs/release-notes/v2.2.0.md +0 -55
  42. docs/user-guide/Mathematical Notation/Bus.md +0 -33
  43. docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +0 -132
  44. docs/user-guide/Mathematical Notation/Flow.md +0 -26
  45. docs/user-guide/Mathematical Notation/Investment.md +0 -115
  46. docs/user-guide/Mathematical Notation/LinearConverter.md +0 -21
  47. docs/user-guide/Mathematical Notation/Piecewise.md +0 -49
  48. docs/user-guide/Mathematical Notation/Storage.md +0 -44
  49. docs/user-guide/Mathematical Notation/index.md +0 -22
  50. docs/user-guide/Mathematical Notation/others.md +0 -3
  51. docs/user-guide/index.md +0 -124
  52. flixopt/config.yaml +0 -10
  53. flixopt-2.2.0b0.dist-info/METADATA +0 -146
  54. flixopt-2.2.0b0.dist-info/RECORD +0 -59
  55. flixopt-2.2.0b0.dist-info/top_level.txt +0 -5
  56. pics/architecture_flixOpt-pre2.0.0.png +0 -0
  57. pics/architecture_flixOpt.png +0 -0
  58. pics/flixOpt_plotting.jpg +0 -0
  59. pics/flixopt-icon.svg +0 -1
  60. pics/pics.pptx +0 -0
  61. scripts/gen_ref_pages.py +0 -54
  62. tests/ressources/Zeitreihen2020.csv +0 -35137
  63. {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 SystemModel for a given FlowSystem through a solver.
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 SystemModel for the full FlowSystem
6
- 2. AggregatedCalculation: Calculates the SystemModel for the full FlowSystem, but aggregates the TimeSeriesData.
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 SystemModel for each individual Segment of the FlowSystem.
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 typing import Any, Dict, List, Optional, Union
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
- from .solvers import _Solver
33
- from .structure import SystemModel, copy_and_convert_datatypes, get_compact_representation
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
- selected_timesteps: Optional[pd.DatetimeIndex] = None,
48
- selected_scenarios: Optional[pd.Index] = None,
49
- folder: Optional[pathlib.Path] = None,
50
- active_timesteps: Optional[pd.DatetimeIndex] = None,
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
- Args:
54
- name: name of calculation
55
- flow_system: flow_system which should be calculated
56
- selected_timesteps: timesteps which should be used for calculation. If None, then all timesteps are used.
57
- selected_scenarios: scenarios which should be used for calculation. If None, then all scenarios are used.
58
- folder: folder where results should be saved. If None, then the current working directory is used.
59
- active_timesteps: Deprecated. Use selected_timesteps instead.
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. Use selected_timesteps instead.',
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
- selected_timesteps = active_timesteps
68
- self.name = name
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: Optional[SystemModel] = None
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: Optional[CalculationResults] = None
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
- if not self.folder.exists():
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) -> Dict[str, Union[Scalar, Dict]]:
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
- 'operation': effect.model.operation.total.solution.values,
96
- 'invest': effect.model.invest.total.solution.values,
97
- 'total': effect.model.total.solution.values,
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.model.all_sub_models
106
- if isinstance(model, InvestmentModel) and model.size.solution.max() >= CONFIG.modeling.EPSILON
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.model.all_sub_models
112
- if isinstance(model, InvestmentModel) and model.size.solution.max() < CONFIG.modeling.EPSILON
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.model.excess_input.solution.sum('time'),
119
- 'output': bus.model.excess_output.solution.sum('time'),
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.model.excess_input.solution.sum() > 1e-3
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.round_floats(main_results)
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.time_series_collection.timesteps),
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 selected_timesteps instead.',
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.selected_timesteps
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
- class for defined way of solving a flow_system optimization
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) -> SystemModel:
191
+ def do_modeling(self) -> FullCalculation:
162
192
  t_start = timeit.default_timer()
163
- self._activate_time_series()
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.model
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(self, solver: _Solver, log_file: Optional[pathlib.Path] = None, log_main_results: bool = True):
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.round_floats(self.main_results),
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
- def _activate_time_series(self):
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
- class for defined way of solving a flow_system optimization
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: Optional[List[Component]] = None,
226
- selected_timesteps: Optional[pd.DatetimeIndex] = None,
227
- folder: Optional[pathlib.Path] = None,
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, selected_timesteps, folder=folder)
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) -> SystemModel:
311
+ def do_modeling(self) -> AggregatedCalculation:
252
312
  t_start = timeit.default_timer()
253
- self._activate_time_series()
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 Model after modeling the rest
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.model
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, dt_max = (
274
- np.min(self.flow_system.time_series_collection.hours_per_timestep),
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
- steps_per_period = (
283
- self.aggregation_parameters.hours_per_period
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 {dt_min} hours). It must be a multiple of {dt_min} hours.'
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=self.flow_system.time_series_collection.as_dataset(
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.flow_system.time_series_collection.calculate_aggregation_weights(),
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
- for col in self.aggregation.aggregated_data.columns:
316
- data = self.aggregation.aggregated_data[col].values
317
- if col in self.flow_system.time_series_collection._has_extra_timestep:
318
- data = np.append(data, data[-1])
319
- self.flow_system.time_series_collection.update_time_series(col, data)
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: Optional[pathlib.Path] = None,
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: List[FullCalculation] = []
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.selected_timesteps_per_segment = self._calculate_timesteps_of_segment()
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 FLows know their Component
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: List[Dict[str, Any]] = []
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.selected_timesteps_per_segment, strict=False)
556
+ zip(self.segment_names, self._timesteps_per_segment, strict=True)
391
557
  ):
392
- if self.sub_calculations:
393
- self._transfer_start_values(i)
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
- calculation = FullCalculation(
401
- f'{self.name}-{segment_name}', self.flow_system, selected_timesteps=timesteps_of_segment
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
- self.sub_calculations.append(calculation)
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
- invest_elements = [
406
- model.label_full
407
- for component in self.flow_system.components.values()
408
- for model in component.model.all_sub_models
409
- if isinstance(model, InvestmentModel)
410
- ]
411
- if invest_elements:
412
- logger.critical(
413
- f'Investments are not supported in Segmented Calculation! '
414
- f'Following InvestmentModels were found: {invest_elements}'
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
- def _transfer_start_values(self, segment_index: int):
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.selected_timesteps_per_segment[segment_index - 1]
618
+ timesteps_of_prior_segment = self.sub_calculations[i - 1].flow_system.timesteps_extra
436
619
 
437
- start = self.selected_timesteps_per_segment[segment_index][0]
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'start of next segment: {start}. indices of previous values: {start_previous_values}:{end_previous_values}'
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
- for flow in self.flow_system.flows.values():
446
- flow.previous_flow_rate = flow.model.flow_rate.solution.sel(
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[flow.label_full] = flow.previous_flow_rate
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
- self._transfered_start_values.append(start_values_of_this_segment)
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
- def _reset_start_values(self):
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 _calculate_timesteps_of_segment(self) -> List[pd.DatetimeIndex]:
466
- selected_timesteps_per_segment = []
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
- selected_timesteps_per_segment.append(self.all_timesteps[start:end])
471
- return selected_timesteps_per_segment
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) -> Dict[int, Dict[str, Any]]:
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
- 0: {element.label_full: value for element, value in self._original_start_values.items()},
482
- **{i: start_values for i, start_values in enumerate(self._transfered_start_values, 1)},
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