flixopt 3.2.1__py3-none-any.whl → 3.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of flixopt might be problematic. Click here for more details.
- flixopt/calculation.py +105 -39
- flixopt/components.py +16 -0
- flixopt/config.py +120 -0
- flixopt/effects.py +28 -28
- flixopt/elements.py +58 -1
- flixopt/flow_system.py +141 -84
- flixopt/interface.py +23 -2
- flixopt/io.py +506 -4
- flixopt/results.py +52 -24
- flixopt/solvers.py +12 -4
- flixopt/structure.py +369 -49
- {flixopt-3.2.1.dist-info → flixopt-3.4.0.dist-info}/METADATA +3 -2
- flixopt-3.4.0.dist-info/RECORD +26 -0
- flixopt-3.2.1.dist-info/RECORD +0 -26
- {flixopt-3.2.1.dist-info → flixopt-3.4.0.dist-info}/WHEEL +0 -0
- {flixopt-3.2.1.dist-info → flixopt-3.4.0.dist-info}/licenses/LICENSE +0 -0
- {flixopt-3.2.1.dist-info → flixopt-3.4.0.dist-info}/top_level.txt +0 -0
flixopt/results.py
CHANGED
|
@@ -17,6 +17,7 @@ from . import plotting
|
|
|
17
17
|
from .color_processing import process_colors
|
|
18
18
|
from .config import CONFIG
|
|
19
19
|
from .flow_system import FlowSystem
|
|
20
|
+
from .structure import CompositeContainerMixin, ElementContainer, ResultsContainer
|
|
20
21
|
|
|
21
22
|
if TYPE_CHECKING:
|
|
22
23
|
import matplotlib.pyplot as plt
|
|
@@ -53,7 +54,7 @@ class _FlowSystemRestorationError(Exception):
|
|
|
53
54
|
pass
|
|
54
55
|
|
|
55
56
|
|
|
56
|
-
class CalculationResults:
|
|
57
|
+
class CalculationResults(CompositeContainerMixin['ComponentResults | BusResults | EffectResults | FlowResults']):
|
|
57
58
|
"""Comprehensive container for optimization calculation results and analysis tools.
|
|
58
59
|
|
|
59
60
|
This class provides unified access to all optimization results including flow rates,
|
|
@@ -147,6 +148,8 @@ class CalculationResults:
|
|
|
147
148
|
|
|
148
149
|
"""
|
|
149
150
|
|
|
151
|
+
model: linopy.Model | None
|
|
152
|
+
|
|
150
153
|
@classmethod
|
|
151
154
|
def from_file(cls, folder: str | pathlib.Path, name: str) -> CalculationResults:
|
|
152
155
|
"""Load CalculationResults from saved files.
|
|
@@ -238,13 +241,18 @@ class CalculationResults:
|
|
|
238
241
|
self.name = name
|
|
239
242
|
self.model = model
|
|
240
243
|
self.folder = pathlib.Path(folder) if folder is not None else pathlib.Path.cwd() / 'results'
|
|
241
|
-
|
|
244
|
+
|
|
245
|
+
# Create ResultsContainers for better access patterns
|
|
246
|
+
components_dict = {
|
|
242
247
|
label: ComponentResults(self, **infos) for label, infos in self.solution.attrs['Components'].items()
|
|
243
248
|
}
|
|
249
|
+
self.components = ResultsContainer(elements=components_dict, element_type_name='component results')
|
|
244
250
|
|
|
245
|
-
|
|
251
|
+
buses_dict = {label: BusResults(self, **infos) for label, infos in self.solution.attrs['Buses'].items()}
|
|
252
|
+
self.buses = ResultsContainer(elements=buses_dict, element_type_name='bus results')
|
|
246
253
|
|
|
247
|
-
|
|
254
|
+
effects_dict = {label: EffectResults(self, **infos) for label, infos in self.solution.attrs['Effects'].items()}
|
|
255
|
+
self.effects = ResultsContainer(elements=effects_dict, element_type_name='effect results')
|
|
248
256
|
|
|
249
257
|
if 'Flows' not in self.solution.attrs:
|
|
250
258
|
warnings.warn(
|
|
@@ -252,11 +260,14 @@ class CalculationResults:
|
|
|
252
260
|
'is not availlable. We recommend to evaluate your results with a version <2.2.0.',
|
|
253
261
|
stacklevel=2,
|
|
254
262
|
)
|
|
255
|
-
|
|
263
|
+
flows_dict = {}
|
|
264
|
+
self._has_flow_data = False
|
|
256
265
|
else:
|
|
257
|
-
|
|
266
|
+
flows_dict = {
|
|
258
267
|
label: FlowResults(self, **infos) for label, infos in self.solution.attrs.get('Flows', {}).items()
|
|
259
268
|
}
|
|
269
|
+
self._has_flow_data = True
|
|
270
|
+
self.flows = ResultsContainer(elements=flows_dict, element_type_name='flow results')
|
|
260
271
|
|
|
261
272
|
self.timesteps_extra = self.solution.indexes['time']
|
|
262
273
|
self.hours_per_timestep = FlowSystem.calculate_hours_per_timestep(self.timesteps_extra)
|
|
@@ -273,16 +284,22 @@ class CalculationResults:
|
|
|
273
284
|
|
|
274
285
|
self.colors: dict[str, str] = {}
|
|
275
286
|
|
|
276
|
-
def
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
287
|
+
def _get_container_groups(self) -> dict[str, ResultsContainer]:
|
|
288
|
+
"""Return ordered container groups for CompositeContainerMixin."""
|
|
289
|
+
return {
|
|
290
|
+
'Components': self.components,
|
|
291
|
+
'Buses': self.buses,
|
|
292
|
+
'Effects': self.effects,
|
|
293
|
+
'Flows': self.flows,
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
def __repr__(self) -> str:
|
|
297
|
+
"""Return grouped representation of all results."""
|
|
298
|
+
r = fx_io.format_title_with_underline(self.__class__.__name__, '=')
|
|
299
|
+
r += f'Name: "{self.name}"\nFolder: {self.folder}\n'
|
|
300
|
+
# Add grouped container view
|
|
301
|
+
r += '\n' + self._format_grouped_containers()
|
|
302
|
+
return r
|
|
286
303
|
|
|
287
304
|
@property
|
|
288
305
|
def storages(self) -> list[ComponentResults]:
|
|
@@ -547,6 +564,8 @@ class CalculationResults:
|
|
|
547
564
|
To recombine filtered dataarrays, use `xr.concat` with dim 'flow':
|
|
548
565
|
>>>xr.concat([results.flow_rates(start='Fernwärme'), results.flow_rates(end='Fernwärme')], dim='flow')
|
|
549
566
|
"""
|
|
567
|
+
if not self._has_flow_data:
|
|
568
|
+
raise ValueError('Flow data is not available in this results object (pre-v2.2.0).')
|
|
550
569
|
if self._flow_rates is None:
|
|
551
570
|
self._flow_rates = self._assign_flow_coords(
|
|
552
571
|
xr.concat(
|
|
@@ -608,6 +627,8 @@ class CalculationResults:
|
|
|
608
627
|
>>>xr.concat([results.sizes(start='Fernwärme'), results.sizes(end='Fernwärme')], dim='flow')
|
|
609
628
|
|
|
610
629
|
"""
|
|
630
|
+
if not self._has_flow_data:
|
|
631
|
+
raise ValueError('Flow data is not available in this results object (pre-v2.2.0).')
|
|
611
632
|
if self._sizes is None:
|
|
612
633
|
self._sizes = self._assign_flow_coords(
|
|
613
634
|
xr.concat(
|
|
@@ -620,11 +641,12 @@ class CalculationResults:
|
|
|
620
641
|
|
|
621
642
|
def _assign_flow_coords(self, da: xr.DataArray):
|
|
622
643
|
# Add start and end coordinates
|
|
644
|
+
flows_list = list(self.flows.values())
|
|
623
645
|
da = da.assign_coords(
|
|
624
646
|
{
|
|
625
|
-
'start': ('flow', [flow.start for flow in
|
|
626
|
-
'end': ('flow', [flow.end for flow in
|
|
627
|
-
'component': ('flow', [flow.component for flow in
|
|
647
|
+
'start': ('flow', [flow.start for flow in flows_list]),
|
|
648
|
+
'end': ('flow', [flow.end for flow in flows_list]),
|
|
649
|
+
'component': ('flow', [flow.component for flow in flows_list]),
|
|
628
650
|
}
|
|
629
651
|
)
|
|
630
652
|
|
|
@@ -743,8 +765,6 @@ class CalculationResults:
|
|
|
743
765
|
temporal = temporal.sum('time')
|
|
744
766
|
if periodic.isnull().all():
|
|
745
767
|
return temporal.rename(f'{element}->{effect}')
|
|
746
|
-
if 'time' in temporal.indexes:
|
|
747
|
-
temporal = temporal.sum('time')
|
|
748
768
|
return periodic + temporal
|
|
749
769
|
|
|
750
770
|
total = xr.DataArray(0)
|
|
@@ -1011,14 +1031,14 @@ class CalculationResults:
|
|
|
1011
1031
|
]
|
|
1012
1032
|
) = True,
|
|
1013
1033
|
path: pathlib.Path | None = None,
|
|
1014
|
-
show: bool =
|
|
1034
|
+
show: bool | None = None,
|
|
1015
1035
|
) -> pyvis.network.Network | None:
|
|
1016
1036
|
"""Plot interactive network visualization of the system.
|
|
1017
1037
|
|
|
1018
1038
|
Args:
|
|
1019
1039
|
controls: Enable/disable interactive controls.
|
|
1020
1040
|
path: Save path for network HTML.
|
|
1021
|
-
show: Whether to display the plot.
|
|
1041
|
+
show: Whether to display the plot. If None, uses CONFIG.Plotting.default_show.
|
|
1022
1042
|
"""
|
|
1023
1043
|
if path is None:
|
|
1024
1044
|
path = self.folder / f'{self.name}--network.html'
|
|
@@ -1056,7 +1076,7 @@ class CalculationResults:
|
|
|
1056
1076
|
fx_io.save_dataset_to_netcdf(self.solution, paths.solution, compression=compression)
|
|
1057
1077
|
fx_io.save_dataset_to_netcdf(self.flow_system_data, paths.flow_system, compression=compression)
|
|
1058
1078
|
|
|
1059
|
-
fx_io.save_yaml(self.summary, paths.summary)
|
|
1079
|
+
fx_io.save_yaml(self.summary, paths.summary, compact_numeric_lists=True)
|
|
1060
1080
|
|
|
1061
1081
|
if save_linopy_model:
|
|
1062
1082
|
if self.model is None:
|
|
@@ -1106,6 +1126,14 @@ class _ElementResults:
|
|
|
1106
1126
|
raise ValueError('The linopy model is not available.')
|
|
1107
1127
|
return self._calculation_results.model.constraints[self._constraint_names]
|
|
1108
1128
|
|
|
1129
|
+
def __repr__(self) -> str:
|
|
1130
|
+
"""Return string representation with element info and dataset preview."""
|
|
1131
|
+
class_name = self.__class__.__name__
|
|
1132
|
+
header = f'{class_name}: "{self.label}"'
|
|
1133
|
+
sol = self.solution.copy(deep=False)
|
|
1134
|
+
sol.attrs = {}
|
|
1135
|
+
return f'{header}\n{"-" * len(header)}\n{repr(sol)}'
|
|
1136
|
+
|
|
1109
1137
|
def filter_solution(
|
|
1110
1138
|
self,
|
|
1111
1139
|
variable_dims: Literal['scalar', 'time', 'scenario', 'timeonly', 'scenarioonly'] | None = None,
|
flixopt/solvers.py
CHANGED
|
@@ -8,6 +8,8 @@ import logging
|
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
9
|
from typing import Any, ClassVar
|
|
10
10
|
|
|
11
|
+
from flixopt.config import CONFIG
|
|
12
|
+
|
|
11
13
|
logger = logging.getLogger('flixopt')
|
|
12
14
|
|
|
13
15
|
|
|
@@ -17,14 +19,16 @@ class _Solver:
|
|
|
17
19
|
Abstract base class for solvers.
|
|
18
20
|
|
|
19
21
|
Args:
|
|
20
|
-
mip_gap: Acceptable relative optimality gap in [0.0, 1.0].
|
|
21
|
-
time_limit_seconds: Time limit in seconds.
|
|
22
|
+
mip_gap: Acceptable relative optimality gap in [0.0, 1.0]. Defaults to CONFIG.Solving.mip_gap.
|
|
23
|
+
time_limit_seconds: Time limit in seconds. Defaults to CONFIG.Solving.time_limit_seconds.
|
|
24
|
+
log_to_console: If False, no output to console. Defaults to CONFIG.Solving.log_to_console.
|
|
22
25
|
extra_options: Additional solver options merged into `options`.
|
|
23
26
|
"""
|
|
24
27
|
|
|
25
28
|
name: ClassVar[str]
|
|
26
|
-
mip_gap: float
|
|
27
|
-
time_limit_seconds: int
|
|
29
|
+
mip_gap: float = field(default_factory=lambda: CONFIG.Solving.mip_gap)
|
|
30
|
+
time_limit_seconds: int = field(default_factory=lambda: CONFIG.Solving.time_limit_seconds)
|
|
31
|
+
log_to_console: bool = field(default_factory=lambda: CONFIG.Solving.log_to_console)
|
|
28
32
|
extra_options: dict[str, Any] = field(default_factory=dict)
|
|
29
33
|
|
|
30
34
|
@property
|
|
@@ -45,6 +49,7 @@ class GurobiSolver(_Solver):
|
|
|
45
49
|
Args:
|
|
46
50
|
mip_gap: Acceptable relative optimality gap in [0.0, 1.0]; mapped to Gurobi `MIPGap`.
|
|
47
51
|
time_limit_seconds: Time limit in seconds; mapped to Gurobi `TimeLimit`.
|
|
52
|
+
log_to_console: If False, no output to console.
|
|
48
53
|
extra_options: Additional solver options merged into `options`.
|
|
49
54
|
"""
|
|
50
55
|
|
|
@@ -55,6 +60,7 @@ class GurobiSolver(_Solver):
|
|
|
55
60
|
return {
|
|
56
61
|
'MIPGap': self.mip_gap,
|
|
57
62
|
'TimeLimit': self.time_limit_seconds,
|
|
63
|
+
'LogToConsole': 1 if self.log_to_console else 0,
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
|
|
@@ -65,6 +71,7 @@ class HighsSolver(_Solver):
|
|
|
65
71
|
Attributes:
|
|
66
72
|
mip_gap: Acceptable relative optimality gap in [0.0, 1.0]; mapped to HiGHS `mip_rel_gap`.
|
|
67
73
|
time_limit_seconds: Time limit in seconds; mapped to HiGHS `time_limit`.
|
|
74
|
+
log_to_console: If False, no output to console.
|
|
68
75
|
extra_options: Additional solver options merged into `options`.
|
|
69
76
|
threads (int | None): Number of threads to use. If None, HiGHS chooses.
|
|
70
77
|
"""
|
|
@@ -78,4 +85,5 @@ class HighsSolver(_Solver):
|
|
|
78
85
|
'mip_rel_gap': self.mip_gap,
|
|
79
86
|
'time_limit': self.time_limit_seconds,
|
|
80
87
|
'threads': self.threads,
|
|
88
|
+
'log_to_console': self.log_to_console,
|
|
81
89
|
}
|