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/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
- self.components = {
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
- self.buses = {label: BusResults(self, **infos) for label, infos in self.solution.attrs['Buses'].items()}
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
- self.effects = {label: EffectResults(self, **infos) for label, infos in self.solution.attrs['Effects'].items()}
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
- self.flows = {}
263
+ flows_dict = {}
264
+ self._has_flow_data = False
256
265
  else:
257
- self.flows = {
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 __getitem__(self, key: str) -> ComponentResults | BusResults | EffectResults:
277
- if key in self.components:
278
- return self.components[key]
279
- if key in self.buses:
280
- return self.buses[key]
281
- if key in self.effects:
282
- return self.effects[key]
283
- if key in self.flows:
284
- return self.flows[key]
285
- raise KeyError(f'No element with label {key} found.')
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 self.flows.values()]),
626
- 'end': ('flow', [flow.end for flow in self.flows.values()]),
627
- 'component': ('flow', [flow.component for flow in self.flows.values()]),
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 = False,
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
  }