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.
Files changed (42) hide show
  1. flixopt/__init__.py +57 -49
  2. flixopt/carrier.py +159 -0
  3. flixopt/clustering/__init__.py +51 -0
  4. flixopt/clustering/base.py +1746 -0
  5. flixopt/clustering/intercluster_helpers.py +201 -0
  6. flixopt/color_processing.py +372 -0
  7. flixopt/comparison.py +819 -0
  8. flixopt/components.py +848 -270
  9. flixopt/config.py +853 -496
  10. flixopt/core.py +111 -98
  11. flixopt/effects.py +294 -284
  12. flixopt/elements.py +484 -223
  13. flixopt/features.py +220 -118
  14. flixopt/flow_system.py +2026 -389
  15. flixopt/interface.py +504 -286
  16. flixopt/io.py +1718 -55
  17. flixopt/linear_converters.py +291 -230
  18. flixopt/modeling.py +304 -181
  19. flixopt/network_app.py +2 -1
  20. flixopt/optimization.py +788 -0
  21. flixopt/optimize_accessor.py +373 -0
  22. flixopt/plot_result.py +143 -0
  23. flixopt/plotting.py +1177 -1034
  24. flixopt/results.py +1331 -372
  25. flixopt/solvers.py +12 -4
  26. flixopt/statistics_accessor.py +2412 -0
  27. flixopt/stats_accessor.py +75 -0
  28. flixopt/structure.py +954 -120
  29. flixopt/topology_accessor.py +676 -0
  30. flixopt/transform_accessor.py +2277 -0
  31. flixopt/types.py +120 -0
  32. flixopt-6.0.0rc7.dist-info/METADATA +290 -0
  33. flixopt-6.0.0rc7.dist-info/RECORD +36 -0
  34. {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/WHEEL +1 -1
  35. flixopt/aggregation.py +0 -382
  36. flixopt/calculation.py +0 -672
  37. flixopt/commons.py +0 -51
  38. flixopt/utils.py +0 -86
  39. flixopt-3.0.1.dist-info/METADATA +0 -209
  40. flixopt-3.0.1.dist-info/RECORD +0 -26
  41. {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/licenses/LICENSE +0 -0
  42. {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