flixopt 3.3.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 CHANGED
@@ -13,13 +13,14 @@ from __future__ import annotations
13
13
  import logging
14
14
  import math
15
15
  import pathlib
16
+ import sys
16
17
  import timeit
17
18
  import warnings
18
19
  from collections import Counter
19
20
  from typing import TYPE_CHECKING, Annotated, Any
20
21
 
21
22
  import numpy as np
22
- import yaml
23
+ from tqdm import tqdm
23
24
 
24
25
  from . import io as fx_io
25
26
  from .aggregation import Aggregation, AggregationModel, AggregationParameters
@@ -53,6 +54,8 @@ class Calculation:
53
54
  active_timesteps: Deprecated. Use FlowSystem.sel(time=...) or FlowSystem.isel(time=...) instead.
54
55
  """
55
56
 
57
+ model: FlowSystemModel | None
58
+
56
59
  def __init__(
57
60
  self,
58
61
  name: str,
@@ -87,7 +90,7 @@ class Calculation:
87
90
  flow_system._used_in_calculation = True
88
91
 
89
92
  self.flow_system = flow_system
90
- self.model: FlowSystemModel | None = None
93
+ self.model = None
91
94
 
92
95
  self.durations = {'modeling': 0.0, 'solving': 0.0, 'saving': 0.0}
93
96
  self.folder = pathlib.Path.cwd() / 'results' if folder is None else pathlib.Path(folder)
@@ -225,7 +228,7 @@ class FullCalculation(Calculation):
225
228
  return self
226
229
 
227
230
  def solve(
228
- self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool = True
231
+ self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool | None = None
229
232
  ) -> FullCalculation:
230
233
  t_start = timeit.default_timer()
231
234
 
@@ -235,6 +238,8 @@ class FullCalculation(Calculation):
235
238
  **solver.options,
236
239
  )
237
240
  self.durations['solving'] = round(timeit.default_timer() - t_start, 2)
241
+ logger.info(f'Model solved with {solver.name} in {self.durations["solving"]:.2f} seconds.')
242
+ logger.info(f'Model status after solve: {self.model.status}')
238
243
 
239
244
  if self.model.status == 'warning':
240
245
  # Save the model and the flow_system to file in case of infeasibility
@@ -248,15 +253,13 @@ class FullCalculation(Calculation):
248
253
  )
249
254
 
250
255
  # Log the formatted output
251
- if log_main_results:
256
+ should_log = log_main_results if log_main_results is not None else CONFIG.Solving.log_main_results
257
+ if should_log:
252
258
  logger.info(
253
259
  f'{" Main Results ":#^80}\n'
254
- + yaml.dump(
260
+ + fx_io.format_yaml_string(
255
261
  self.main_results,
256
- default_flow_style=False,
257
- sort_keys=False,
258
- allow_unicode=True,
259
- indent=4,
262
+ compact_numeric_lists=True,
260
263
  )
261
264
  )
262
265
 
@@ -366,7 +369,7 @@ class AggregatedCalculation(FullCalculation):
366
369
  )
367
370
 
368
371
  self.aggregation.cluster()
369
- self.aggregation.plot(show=True, save=self.folder / 'aggregation.html')
372
+ self.aggregation.plot(show=CONFIG.Plotting.default_show, save=self.folder / 'aggregation.html')
370
373
  if self.aggregation_parameters.aggregate_data_and_fix_non_binary_vars:
371
374
  ds = self.flow_system.to_dataset()
372
375
  for name, series in self.aggregation.aggregated_data.items():
@@ -567,48 +570,111 @@ class SegmentedCalculation(Calculation):
567
570
  f'({timesteps_of_segment[0]} -> {timesteps_of_segment[-1]}):'
568
571
  )
569
572
 
573
+ def _solve_single_segment(
574
+ self,
575
+ i: int,
576
+ calculation: FullCalculation,
577
+ solver: _Solver,
578
+ log_file: pathlib.Path | None,
579
+ log_main_results: bool,
580
+ suppress_output: bool,
581
+ ) -> None:
582
+ """Solve a single segment calculation."""
583
+ if i > 0 and self.nr_of_previous_values > 0:
584
+ self._transfer_start_values(i)
585
+
586
+ calculation.do_modeling()
587
+
588
+ # Warn about Investments, but only in first run
589
+ if i == 0:
590
+ invest_elements = [
591
+ model.label_full
592
+ for component in calculation.flow_system.components.values()
593
+ for model in component.submodel.all_submodels
594
+ if isinstance(model, InvestmentModel)
595
+ ]
596
+ if invest_elements:
597
+ logger.critical(
598
+ f'Investments are not supported in Segmented Calculation! '
599
+ f'Following InvestmentModels were found: {invest_elements}'
600
+ )
601
+
602
+ log_path = pathlib.Path(log_file) if log_file is not None else self.folder / f'{self.name}.log'
603
+
604
+ if suppress_output:
605
+ with fx_io.suppress_output():
606
+ calculation.solve(solver, log_file=log_path, log_main_results=log_main_results)
607
+ else:
608
+ calculation.solve(solver, log_file=log_path, log_main_results=log_main_results)
609
+
570
610
  def do_modeling_and_solve(
571
- self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool = False
611
+ self,
612
+ solver: _Solver,
613
+ log_file: pathlib.Path | None = None,
614
+ log_main_results: bool = False,
615
+ show_individual_solves: bool = False,
572
616
  ) -> SegmentedCalculation:
617
+ """Model and solve all segments of the segmented calculation.
618
+
619
+ This method creates sub-calculations for each time segment, then iteratively
620
+ models and solves each segment. It supports two output modes: a progress bar
621
+ for compact output, or detailed individual solve information.
622
+
623
+ Args:
624
+ solver: The solver instance to use for optimization (e.g., Gurobi, HiGHS).
625
+ log_file: Optional path to the solver log file. If None, defaults to
626
+ folder/name.log.
627
+ log_main_results: Whether to log main results (objective, effects, etc.)
628
+ after each segment solve. Defaults to False.
629
+ show_individual_solves: If True, shows detailed output for each segment
630
+ solve with logger messages. If False (default), shows a compact progress
631
+ bar with suppressed solver output for cleaner display.
632
+
633
+ Returns:
634
+ Self, for method chaining.
635
+
636
+ Note:
637
+ The method automatically transfers all start values between segments to ensure
638
+ continuity of storage states and flow rates across segment boundaries.
639
+ """
573
640
  logger.info(f'{"":#^80}')
574
641
  logger.info(f'{" Segmented Solving ":#^80}')
575
642
  self._create_sub_calculations()
576
643
 
577
- for i, calculation in enumerate(self.sub_calculations):
578
- logger.info(
579
- f'{self.segment_names[i]} [{i + 1:>2}/{len(self.segment_names):<2}] '
580
- f'({calculation.flow_system.timesteps[0]} -> {calculation.flow_system.timesteps[-1]}):'
644
+ if show_individual_solves:
645
+ # Path 1: Show individual solves with detailed output
646
+ for i, calculation in enumerate(self.sub_calculations):
647
+ logger.info(
648
+ f'Solving segment {i + 1}/{len(self.sub_calculations)}: '
649
+ f'{calculation.flow_system.timesteps[0]} -> {calculation.flow_system.timesteps[-1]}'
650
+ )
651
+ self._solve_single_segment(i, calculation, solver, log_file, log_main_results, suppress_output=False)
652
+ else:
653
+ # Path 2: Show only progress bar with suppressed output
654
+ progress_bar = tqdm(
655
+ enumerate(self.sub_calculations),
656
+ total=len(self.sub_calculations),
657
+ desc='Solving segments',
658
+ unit='segment',
659
+ file=sys.stdout,
660
+ disable=not CONFIG.Solving.log_to_console,
581
661
  )
582
662
 
583
- if i > 0 and self.nr_of_previous_values > 0:
584
- self._transfer_start_values(i)
585
-
586
- calculation.do_modeling()
587
-
588
- # Warn about Investments, but only in fist run
589
- if i == 0:
590
- invest_elements = [
591
- model.label_full
592
- for component in calculation.flow_system.components.values()
593
- for model in component.submodel.all_submodels
594
- if isinstance(model, InvestmentModel)
595
- ]
596
- if invest_elements:
597
- logger.critical(
598
- f'Investments are not supported in Segmented Calculation! '
599
- f'Following InvestmentModels were found: {invest_elements}'
663
+ try:
664
+ for i, calculation in progress_bar:
665
+ progress_bar.set_description(
666
+ f'Solving ({calculation.flow_system.timesteps[0]} -> {calculation.flow_system.timesteps[-1]})'
600
667
  )
601
-
602
- calculation.solve(
603
- solver,
604
- log_file=pathlib.Path(log_file) if log_file is not None else self.folder / f'{self.name}.log',
605
- log_main_results=log_main_results,
606
- )
668
+ self._solve_single_segment(i, calculation, solver, log_file, log_main_results, suppress_output=True)
669
+ finally:
670
+ progress_bar.close()
607
671
 
608
672
  for calc in self.sub_calculations:
609
673
  for key, value in calc.durations.items():
610
674
  self.durations[key] += value
611
675
 
676
+ logger.info(f'Model solved with {solver.name} in {self.durations["solving"]:.2f} seconds.')
677
+
612
678
  self.results = SegmentedCalculationResults.from_calculation(self)
613
679
 
614
680
  return self
flixopt/components.py CHANGED
@@ -161,6 +161,8 @@ class LinearConverter(Component):
161
161
 
162
162
  """
163
163
 
164
+ submodel: LinearConverterModel | None
165
+
164
166
  def __init__(
165
167
  self,
166
168
  label: str,
@@ -377,6 +379,8 @@ class Storage(Component):
377
379
  With flow rates in m3/h, the charge state is therefore in m3.
378
380
  """
379
381
 
382
+ submodel: StorageModel | None
383
+
380
384
  def __init__(
381
385
  self,
382
386
  label: str,
@@ -650,6 +654,8 @@ class Transmission(Component):
650
654
 
651
655
  """
652
656
 
657
+ submodel: TransmissionModel | None
658
+
653
659
  def __init__(
654
660
  self,
655
661
  label: str,
flixopt/config.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
+ import os
4
5
  import sys
5
6
  import warnings
6
7
  from logging.handlers import RotatingFileHandler
@@ -63,6 +64,14 @@ _DEFAULTS = MappingProxyType(
63
64
  'default_qualitative_colorscale': 'plotly',
64
65
  }
65
66
  ),
67
+ 'solving': MappingProxyType(
68
+ {
69
+ 'mip_gap': 0.01,
70
+ 'time_limit_seconds': 300,
71
+ 'log_to_console': True,
72
+ 'log_main_results': True,
73
+ }
74
+ ),
66
75
  }
67
76
  )
68
77
 
@@ -75,6 +84,8 @@ class CONFIG:
75
84
  Attributes:
76
85
  Logging: Logging configuration.
77
86
  Modeling: Optimization modeling parameters.
87
+ Solving: Solver configuration and default parameters.
88
+ Plotting: Plotting configuration.
78
89
  config_name: Configuration name.
79
90
 
80
91
  Examples:
@@ -91,6 +102,9 @@ class CONFIG:
91
102
  level: DEBUG
92
103
  console: true
93
104
  file: app.log
105
+ solving:
106
+ mip_gap: 0.001
107
+ time_limit_seconds: 600
94
108
  ```
95
109
  """
96
110
 
@@ -194,6 +208,30 @@ class CONFIG:
194
208
  epsilon: float = _DEFAULTS['modeling']['epsilon']
195
209
  big_binary_bound: int = _DEFAULTS['modeling']['big_binary_bound']
196
210
 
211
+ class Solving:
212
+ """Solver configuration and default parameters.
213
+
214
+ Attributes:
215
+ mip_gap: Default MIP gap tolerance for solver convergence.
216
+ time_limit_seconds: Default time limit in seconds for solver runs.
217
+ log_to_console: Whether solver should output to console.
218
+ log_main_results: Whether to log main results after solving.
219
+
220
+ Examples:
221
+ ```python
222
+ # Set tighter convergence and longer timeout
223
+ CONFIG.Solving.mip_gap = 0.001
224
+ CONFIG.Solving.time_limit_seconds = 600
225
+ CONFIG.Solving.log_to_console = False
226
+ CONFIG.apply()
227
+ ```
228
+ """
229
+
230
+ mip_gap: float = _DEFAULTS['solving']['mip_gap']
231
+ time_limit_seconds: int = _DEFAULTS['solving']['time_limit_seconds']
232
+ log_to_console: bool = _DEFAULTS['solving']['log_to_console']
233
+ log_main_results: bool = _DEFAULTS['solving']['log_main_results']
234
+
197
235
  class Plotting:
198
236
  """Plotting configuration.
199
237
 
@@ -246,6 +284,12 @@ class CONFIG:
246
284
  for key, value in _DEFAULTS['modeling'].items():
247
285
  setattr(cls.Modeling, key, value)
248
286
 
287
+ for key, value in _DEFAULTS['solving'].items():
288
+ setattr(cls.Solving, key, value)
289
+
290
+ for key, value in _DEFAULTS['plotting'].items():
291
+ setattr(cls.Plotting, key, value)
292
+
249
293
  cls.config_name = _DEFAULTS['config_name']
250
294
  cls.apply()
251
295
 
@@ -329,6 +373,12 @@ class CONFIG:
329
373
  elif key == 'modeling' and isinstance(value, dict):
330
374
  for nested_key, nested_value in value.items():
331
375
  setattr(cls.Modeling, nested_key, nested_value)
376
+ elif key == 'solving' and isinstance(value, dict):
377
+ for nested_key, nested_value in value.items():
378
+ setattr(cls.Solving, nested_key, nested_value)
379
+ elif key == 'plotting' and isinstance(value, dict):
380
+ for nested_key, nested_value in value.items():
381
+ setattr(cls.Plotting, nested_key, nested_value)
332
382
  elif hasattr(cls, key):
333
383
  setattr(cls, key, value)
334
384
 
@@ -366,6 +416,12 @@ class CONFIG:
366
416
  'epsilon': cls.Modeling.epsilon,
367
417
  'big_binary_bound': cls.Modeling.big_binary_bound,
368
418
  },
419
+ 'solving': {
420
+ 'mip_gap': cls.Solving.mip_gap,
421
+ 'time_limit_seconds': cls.Solving.time_limit_seconds,
422
+ 'log_to_console': cls.Solving.log_to_console,
423
+ 'log_main_results': cls.Solving.log_main_results,
424
+ },
369
425
  'plotting': {
370
426
  'default_show': cls.Plotting.default_show,
371
427
  'default_engine': cls.Plotting.default_engine,
@@ -376,6 +432,70 @@ class CONFIG:
376
432
  },
377
433
  }
378
434
 
435
+ @classmethod
436
+ def silent(cls) -> type[CONFIG]:
437
+ """Configure for silent operation.
438
+
439
+ Disables console logging, solver output, and result logging
440
+ for clean production runs. Does not show plots. Automatically calls apply().
441
+ """
442
+ cls.Logging.console = False
443
+ cls.Plotting.default_show = False
444
+ cls.Logging.file = None
445
+ cls.Solving.log_to_console = False
446
+ cls.Solving.log_main_results = False
447
+ cls.apply()
448
+ return cls
449
+
450
+ @classmethod
451
+ def debug(cls) -> type[CONFIG]:
452
+ """Configure for debug mode with verbose output.
453
+
454
+ Enables console logging at DEBUG level and all solver output for
455
+ troubleshooting. Automatically calls apply().
456
+ """
457
+ cls.Logging.console = True
458
+ cls.Logging.level = 'DEBUG'
459
+ cls.Solving.log_to_console = True
460
+ cls.Solving.log_main_results = True
461
+ cls.apply()
462
+ return cls
463
+
464
+ @classmethod
465
+ def exploring(cls) -> type[CONFIG]:
466
+ """Configure for exploring flixopt
467
+
468
+ Enables console logging at INFO level and all solver output.
469
+ Also enables browser plotting for plotly with showing plots per default
470
+ """
471
+ cls.Logging.console = True
472
+ cls.Logging.level = 'INFO'
473
+ cls.Solving.log_to_console = True
474
+ cls.Solving.log_main_results = True
475
+ cls.browser_plotting()
476
+ cls.apply()
477
+ return cls
478
+
479
+ @classmethod
480
+ def browser_plotting(cls) -> type[CONFIG]:
481
+ """Configure for interactive usage with plotly to open plots in browser.
482
+
483
+ Sets plotly.io.renderers.default = 'browser'. Useful for running examples
484
+ and viewing interactive plots. Does NOT modify CONFIG.Plotting settings.
485
+
486
+ Respects FLIXOPT_CI environment variable if set.
487
+ """
488
+ cls.Plotting.default_show = True
489
+ cls.apply()
490
+
491
+ # Only set to True if environment variable hasn't overridden it
492
+ if 'FLIXOPT_CI' not in os.environ:
493
+ import plotly.io as pio
494
+
495
+ pio.renderers.default = 'browser'
496
+
497
+ return cls
498
+
379
499
 
380
500
  class MultilineFormatter(logging.Formatter):
381
501
  """Formatter that handles multi-line messages with consistent prefixes.
flixopt/effects.py CHANGED
@@ -160,6 +160,8 @@ class Effect(Element):
160
160
 
161
161
  """
162
162
 
163
+ submodel: EffectModel | None
164
+
163
165
  def __init__(
164
166
  self,
165
167
  label: str,
@@ -454,12 +456,14 @@ class EffectCollection(ElementContainer[Effect]):
454
456
  Handling all Effects
455
457
  """
456
458
 
459
+ submodel: EffectCollectionModel | None
460
+
457
461
  def __init__(self, *effects: Effect):
458
462
  super().__init__(element_type_name='effects')
459
463
  self._standard_effect: Effect | None = None
460
464
  self._objective_effect: Effect | None = None
461
465
 
462
- self.submodel: EffectCollectionModel | None = None
466
+ self.submodel = None
463
467
  self.add_effects(*effects)
464
468
 
465
469
  def create_model(self, model: FlowSystemModel) -> EffectCollectionModel:
flixopt/elements.py CHANGED
@@ -223,6 +223,8 @@ class Bus(Element):
223
223
  by the FlowSystem during system setup.
224
224
  """
225
225
 
226
+ submodel: BusModel | None
227
+
226
228
  def __init__(
227
229
  self,
228
230
  label: str,
@@ -411,6 +413,8 @@ class Flow(Element):
411
413
 
412
414
  """
413
415
 
416
+ submodel: FlowModel | None
417
+
414
418
  def __init__(
415
419
  self,
416
420
  label: str,
flixopt/flow_system.py CHANGED
@@ -143,6 +143,8 @@ class FlowSystem(Interface, CompositeContainerMixin[Element]):
143
143
  connected_and_transformed automatically when trying to solve a calculation.
144
144
  """
145
145
 
146
+ model: FlowSystemModel | None
147
+
146
148
  def __init__(
147
149
  self,
148
150
  timesteps: pd.DatetimeIndex,
flixopt/io.py CHANGED
@@ -3,8 +3,11 @@ from __future__ import annotations
3
3
  import inspect
4
4
  import json
5
5
  import logging
6
+ import os
6
7
  import pathlib
7
8
  import re
9
+ import sys
10
+ from contextlib import contextmanager
8
11
  from dataclasses import dataclass
9
12
  from typing import TYPE_CHECKING, Any
10
13
 
@@ -167,6 +170,35 @@ def _load_yaml_unsafe(path: str | pathlib.Path) -> dict | list:
167
170
  return yaml.unsafe_load(f) or {}
168
171
 
169
172
 
173
+ def _create_compact_dumper():
174
+ """
175
+ Create a YAML dumper class with custom representer for compact numeric lists.
176
+
177
+ Returns:
178
+ A yaml.SafeDumper subclass configured to format numeric lists inline.
179
+ """
180
+
181
+ def represent_list(dumper, data):
182
+ """
183
+ Custom representer for lists to format them inline (flow style)
184
+ but only if they contain only numbers or nested numeric lists.
185
+ """
186
+ if data and all(
187
+ isinstance(item, (int, float, np.integer, np.floating))
188
+ or (isinstance(item, list) and all(isinstance(x, (int, float, np.integer, np.floating)) for x in item))
189
+ for item in data
190
+ ):
191
+ return dumper.represent_sequence('tag:yaml.org,2002:seq', data, flow_style=True)
192
+ return dumper.represent_sequence('tag:yaml.org,2002:seq', data, flow_style=False)
193
+
194
+ # Create custom dumper with the representer
195
+ class CompactDumper(yaml.SafeDumper):
196
+ pass
197
+
198
+ CompactDumper.add_representer(list, represent_list)
199
+ return CompactDumper
200
+
201
+
170
202
  def save_yaml(
171
203
  data: dict | list,
172
204
  path: str | pathlib.Path,
@@ -193,31 +225,11 @@ def save_yaml(
193
225
  path = pathlib.Path(path)
194
226
 
195
227
  if compact_numeric_lists:
196
- # Define custom representer for compact numeric lists
197
- def represent_list(dumper, data):
198
- """
199
- Custom representer for lists to format them inline (flow style)
200
- but only if they contain only numbers or nested numeric lists.
201
- """
202
- if data and all(
203
- isinstance(item, (int, float, np.integer, np.floating))
204
- or (isinstance(item, list) and all(isinstance(x, (int, float, np.integer, np.floating)) for x in item))
205
- for item in data
206
- ):
207
- return dumper.represent_sequence('tag:yaml.org,2002:seq', data, flow_style=True)
208
- return dumper.represent_sequence('tag:yaml.org,2002:seq', data, flow_style=False)
209
-
210
- # Create custom dumper with the representer
211
- class CompactDumper(yaml.SafeDumper):
212
- pass
213
-
214
- CompactDumper.add_representer(list, represent_list)
215
-
216
228
  with open(path, 'w', encoding='utf-8') as f:
217
229
  yaml.dump(
218
230
  data,
219
231
  f,
220
- Dumper=CompactDumper,
232
+ Dumper=_create_compact_dumper(),
221
233
  indent=indent,
222
234
  width=width,
223
235
  allow_unicode=allow_unicode,
@@ -239,6 +251,56 @@ def save_yaml(
239
251
  )
240
252
 
241
253
 
254
+ def format_yaml_string(
255
+ data: dict | list,
256
+ indent: int = 4,
257
+ width: int = 1000,
258
+ allow_unicode: bool = True,
259
+ sort_keys: bool = False,
260
+ compact_numeric_lists: bool = False,
261
+ **kwargs,
262
+ ) -> str:
263
+ """
264
+ Format data as a YAML string with consistent formatting.
265
+
266
+ This function provides the same formatting as save_yaml() but returns a string
267
+ instead of writing to a file. Useful for logging or displaying YAML data.
268
+
269
+ Args:
270
+ data: Data to format (dict or list).
271
+ indent: Number of spaces for indentation (default: 4).
272
+ width: Maximum line width (default: 1000).
273
+ allow_unicode: If True, allow Unicode characters (default: True).
274
+ sort_keys: If True, sort dictionary keys (default: False).
275
+ compact_numeric_lists: If True, format numeric lists inline for better readability (default: False).
276
+ **kwargs: Additional arguments to pass to yaml.dump().
277
+
278
+ Returns:
279
+ Formatted YAML string.
280
+ """
281
+ if compact_numeric_lists:
282
+ return yaml.dump(
283
+ data,
284
+ Dumper=_create_compact_dumper(),
285
+ indent=indent,
286
+ width=width,
287
+ allow_unicode=allow_unicode,
288
+ sort_keys=sort_keys,
289
+ default_flow_style=False,
290
+ **kwargs,
291
+ )
292
+ else:
293
+ return yaml.safe_dump(
294
+ data,
295
+ indent=indent,
296
+ width=width,
297
+ allow_unicode=allow_unicode,
298
+ sort_keys=sort_keys,
299
+ default_flow_style=False,
300
+ **kwargs,
301
+ )
302
+
303
+
242
304
  def load_config_file(path: str | pathlib.Path) -> dict:
243
305
  """
244
306
  Load a configuration file, automatically detecting JSON or YAML format.
@@ -931,3 +993,59 @@ def build_metadata_info(parts: list[str], prefix: str = ' | ') -> str:
931
993
  return ''
932
994
  info = ' | '.join(parts)
933
995
  return prefix + info if prefix else info
996
+
997
+
998
+ @contextmanager
999
+ def suppress_output():
1000
+ """
1001
+ Suppress all console output including C-level output from solvers.
1002
+
1003
+ WARNING: Not thread-safe. Modifies global file descriptors.
1004
+ Use only with sequential execution or multiprocessing.
1005
+ """
1006
+ # Save original file descriptors
1007
+ old_stdout_fd = os.dup(1)
1008
+ old_stderr_fd = os.dup(2)
1009
+ devnull_fd = None
1010
+
1011
+ try:
1012
+ # Open devnull
1013
+ devnull_fd = os.open(os.devnull, os.O_WRONLY)
1014
+
1015
+ # Flush Python buffers before redirecting
1016
+ sys.stdout.flush()
1017
+ sys.stderr.flush()
1018
+
1019
+ # Redirect file descriptors to devnull
1020
+ os.dup2(devnull_fd, 1)
1021
+ os.dup2(devnull_fd, 2)
1022
+
1023
+ yield
1024
+
1025
+ finally:
1026
+ # Restore original file descriptors with nested try blocks
1027
+ # to ensure all cleanup happens even if one step fails
1028
+ try:
1029
+ # Flush any buffered output in the redirected streams
1030
+ sys.stdout.flush()
1031
+ sys.stderr.flush()
1032
+ except (OSError, ValueError):
1033
+ pass # Stream might be closed or invalid
1034
+
1035
+ try:
1036
+ os.dup2(old_stdout_fd, 1)
1037
+ except OSError:
1038
+ pass # Failed to restore stdout, continue cleanup
1039
+
1040
+ try:
1041
+ os.dup2(old_stderr_fd, 2)
1042
+ except OSError:
1043
+ pass # Failed to restore stderr, continue cleanup
1044
+
1045
+ # Close all file descriptors
1046
+ for fd in [devnull_fd, old_stdout_fd, old_stderr_fd]:
1047
+ if fd is not None:
1048
+ try:
1049
+ os.close(fd)
1050
+ except OSError:
1051
+ pass # FD already closed or invalid
flixopt/results.py CHANGED
@@ -148,6 +148,8 @@ class CalculationResults(CompositeContainerMixin['ComponentResults | BusResults
148
148
 
149
149
  """
150
150
 
151
+ model: linopy.Model | None
152
+
151
153
  @classmethod
152
154
  def from_file(cls, folder: str | pathlib.Path, name: str) -> CalculationResults:
153
155
  """Load CalculationResults from saved files.
@@ -1029,14 +1031,14 @@ class CalculationResults(CompositeContainerMixin['ComponentResults | BusResults
1029
1031
  ]
1030
1032
  ) = True,
1031
1033
  path: pathlib.Path | None = None,
1032
- show: bool = False,
1034
+ show: bool | None = None,
1033
1035
  ) -> pyvis.network.Network | None:
1034
1036
  """Plot interactive network visualization of the system.
1035
1037
 
1036
1038
  Args:
1037
1039
  controls: Enable/disable interactive controls.
1038
1040
  path: Save path for network HTML.
1039
- show: Whether to display the plot.
1041
+ show: Whether to display the plot. If None, uses CONFIG.Plotting.default_show.
1040
1042
  """
1041
1043
  if path is None:
1042
1044
  path = self.folder / f'{self.name}--network.html'
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
  }
flixopt/structure.py CHANGED
@@ -850,6 +850,8 @@ class Interface:
850
850
  class Element(Interface):
851
851
  """This class is the basic Element of flixopt. Every Element has a label"""
852
852
 
853
+ submodel: ElementModel | None
854
+
853
855
  def __init__(self, label: str, meta_data: dict | None = None):
854
856
  """
855
857
  Args:
@@ -858,7 +860,7 @@ class Element(Interface):
858
860
  """
859
861
  self.label = Element._valid_label(label)
860
862
  self.meta_data = meta_data if meta_data is not None else {}
861
- self.submodel: ElementModel | None = None
863
+ self.submodel = None
862
864
 
863
865
  def _plausibility_checks(self) -> None:
864
866
  """This function is used to do some basic plausibility checks for each Element during initialization.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flixopt
3
- Version: 3.3.1
3
+ Version: 3.4.0
4
4
  Summary: Vector based energy and material flow optimization framework in Python.
5
5
  Author-email: "Chair of Building Energy Systems and Heat Supply, TU Dresden" <peter.stange@tu-dresden.de>, Felix Bumann <felixbumann387@gmail.com>, Felix Panitz <baumbude@googlemail.com>, Peter Stange <peter.stange@tu-dresden.de>
6
6
  Maintainer-email: Felix Bumann <felixbumann387@gmail.com>, Peter Stange <peter.stange@tu-dresden.de>
@@ -27,6 +27,7 @@ Requires-Dist: linopy<0.6,>=0.5.1
27
27
  Requires-Dist: netcdf4<2,>=1.6.1
28
28
  Requires-Dist: pyyaml<7,>=6.0.0
29
29
  Requires-Dist: rich<15,>=13.0.0
30
+ Requires-Dist: tqdm<5,>=4.66.0
30
31
  Requires-Dist: tomli<3,>=2.0.1; python_version < "3.11"
31
32
  Requires-Dist: highspy<2,>=1.5.3
32
33
  Requires-Dist: matplotlib<4,>=3.5.2
@@ -60,7 +61,7 @@ Requires-Dist: pyvis==0.3.2; extra == "dev"
60
61
  Requires-Dist: tsam==2.3.9; extra == "dev"
61
62
  Requires-Dist: scipy==1.15.1; extra == "dev"
62
63
  Requires-Dist: gurobipy==12.0.3; extra == "dev"
63
- Requires-Dist: dash==3.0.0; extra == "dev"
64
+ Requires-Dist: dash==3.2.0; extra == "dev"
64
65
  Requires-Dist: dash-cytoscape==1.0.2; extra == "dev"
65
66
  Requires-Dist: dash-daq==0.6.0; extra == "dev"
66
67
  Requires-Dist: networkx==3.0.0; extra == "dev"
@@ -0,0 +1,26 @@
1
+ flixopt/__init__.py,sha256=_5d7Buc1ugaip5QbDGc9ebMO8LK0WWAjYHQMX2Th8P0,2217
2
+ flixopt/aggregation.py,sha256=ZE0LcUAZ8xNet13YjxvvMw8BAL7Qo4TcJBwBCE2sHqE,16562
3
+ flixopt/calculation.py,sha256=2jKc2Sma7zra1wJruyJmaNfLouquu1fKk0R4NQAqAWw,32681
4
+ flixopt/color_processing.py,sha256=bSq6iAnreiEBFz4Xf0AIUMyENJsWbJ-5xpiqM7_teUc,9027
5
+ flixopt/commons.py,sha256=ZNlUN1z-h9OGHPo-s-n5OLlJaoPZKVGcAdRyGKpMk4M,1256
6
+ flixopt/components.py,sha256=37JR4jJca7aD40ZWPM83Cyr9w7yotXo7gHUx25UUF8Q,58236
7
+ flixopt/config.py,sha256=vl6drczrsMshCA12kd6FXYE0uBBT3HF08GEF02OeU9Y,28958
8
+ flixopt/core.py,sha256=OG789eUaS5Lu0CjJiMIdtaixqnV5ZtMiKfERjCPRTv8,26366
9
+ flixopt/effects.py,sha256=BZE6Dn3krK9JOX2nn0LohA2GhWGiU9HJUwwp0zEGsb0,34355
10
+ flixopt/elements.py,sha256=2jVqtMgQrP6CO08A-S0JMANLKHddxUfA12KY6VfjZu8,38775
11
+ flixopt/features.py,sha256=kd-fMvADv8GXoKkrXObYjRJLN8toBG-5bOHTuh-59kk,25073
12
+ flixopt/flow_system.py,sha256=foZgjRYEY1qcuqs6c98y5T3Bd84KYtp4CNG2ppPJYJw,43792
13
+ flixopt/interface.py,sha256=TEm1tF24cWwCbP_0yBhhH0aVy_j5Fbgl3LI49H5yOIE,58692
14
+ flixopt/io.py,sha256=cxH3KDetLrfp3b9caOvSSv6A-Vis1dr2w8gRoQ0sZnY,36773
15
+ flixopt/linear_converters.py,sha256=tcz5c1SI36hRFbCX-4NXced12ss9VETg5BE7zOdyeo4,22699
16
+ flixopt/modeling.py,sha256=s0zipbblq-LJrSe7angKT3Imxgr3kIbprG98HUvmkzI,31322
17
+ flixopt/network_app.py,sha256=LnVAlAgzL1BgMYLsJ20a62j6nQUmNccF1zo4ACUXzL4,29433
18
+ flixopt/plotting.py,sha256=C_VyBVQIUP1HYt8roXk__Gz9m17cSSPikXZL4jidIpg,65024
19
+ flixopt/results.py,sha256=KboEmzyu7hv42e8lICaTJMQuV6Rjuejuc_ivdTz2WQo,120518
20
+ flixopt/solvers.py,sha256=rTFuL-lBflpbY_NGVGdXeWB2vLw5AdKemTn-Q0KaG7w,3007
21
+ flixopt/structure.py,sha256=7wjpthFkjFJpSo-OmFov4tF5lFoM6SSNhvDF65ecHdE,58155
22
+ flixopt-3.4.0.dist-info/licenses/LICENSE,sha256=HKsZnbrM_3Rvnr_u9cWSG90cBsj5_slaqI_z_qcxnGI,1118
23
+ flixopt-3.4.0.dist-info/METADATA,sha256=p7zVaOUK8x0umDLWYI1G7Qf-ghvO8wtfKpgC36q1PDM,12886
24
+ flixopt-3.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
+ flixopt-3.4.0.dist-info/top_level.txt,sha256=fanTzb9NylIXfv6Ic7spU97fVmRgGDPKvI_91tw4S3E,8
26
+ flixopt-3.4.0.dist-info/RECORD,,
@@ -1,26 +0,0 @@
1
- flixopt/__init__.py,sha256=_5d7Buc1ugaip5QbDGc9ebMO8LK0WWAjYHQMX2Th8P0,2217
2
- flixopt/aggregation.py,sha256=ZE0LcUAZ8xNet13YjxvvMw8BAL7Qo4TcJBwBCE2sHqE,16562
3
- flixopt/calculation.py,sha256=uWgWeU-eg7bbLris85oDtm6fyj7cHFI8CQ-U13vy2u0,29658
4
- flixopt/color_processing.py,sha256=bSq6iAnreiEBFz4Xf0AIUMyENJsWbJ-5xpiqM7_teUc,9027
5
- flixopt/commons.py,sha256=ZNlUN1z-h9OGHPo-s-n5OLlJaoPZKVGcAdRyGKpMk4M,1256
6
- flixopt/components.py,sha256=XYNjQF5IWN1buFfvY4HyWrRD1S3q9cq_ApyaOdT6BE4,58118
7
- flixopt/config.py,sha256=ScqPyYn_URJmOo_aQDViQ-TktF3TZPrcCSoBCQXVpXc,24591
8
- flixopt/core.py,sha256=OG789eUaS5Lu0CjJiMIdtaixqnV5ZtMiKfERjCPRTv8,26366
9
- flixopt/effects.py,sha256=YXx0Ou1Pu3xMugB8DvcqvqC2x8Vx7fDWdHuvn_Eh214,34307
10
- flixopt/elements.py,sha256=N2RG3OMbGRO8qEROYd-_FsH0Vkx4C-ePii3YiHN0mqA,38712
11
- flixopt/features.py,sha256=kd-fMvADv8GXoKkrXObYjRJLN8toBG-5bOHTuh-59kk,25073
12
- flixopt/flow_system.py,sha256=7iSWYCItIQhwUqVZ3VUG_cIUUm0O-OeJbC0tQeyp0OU,43757
13
- flixopt/interface.py,sha256=TEm1tF24cWwCbP_0yBhhH0aVy_j5Fbgl3LI49H5yOIE,58692
14
- flixopt/io.py,sha256=Oh1pRA6H_HOeRzvFZtQ8zXlAjWv-J-lYOWCoKIB-n2M,33409
15
- flixopt/linear_converters.py,sha256=tcz5c1SI36hRFbCX-4NXced12ss9VETg5BE7zOdyeo4,22699
16
- flixopt/modeling.py,sha256=s0zipbblq-LJrSe7angKT3Imxgr3kIbprG98HUvmkzI,31322
17
- flixopt/network_app.py,sha256=LnVAlAgzL1BgMYLsJ20a62j6nQUmNccF1zo4ACUXzL4,29433
18
- flixopt/plotting.py,sha256=C_VyBVQIUP1HYt8roXk__Gz9m17cSSPikXZL4jidIpg,65024
19
- flixopt/results.py,sha256=b35Y8bkduFEbHFn_nHFc2PW7vjQzPWrM4mCC5rz0Njw,120436
20
- flixopt/solvers.py,sha256=m38Smc22MJfHYMiqfNf1MA3OmvbTRm5OWS9nECkDdQk,2355
21
- flixopt/structure.py,sha256=ZB36753ei-VhbaONHLLms9ee_SOs_sEnJLkexRdwoa4,58141
22
- flixopt-3.3.1.dist-info/licenses/LICENSE,sha256=HKsZnbrM_3Rvnr_u9cWSG90cBsj5_slaqI_z_qcxnGI,1118
23
- flixopt-3.3.1.dist-info/METADATA,sha256=4HxgUkfhMLstct176MJPqpIvtx7Q1yIv_LlmvRlUF9o,12855
24
- flixopt-3.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
- flixopt-3.3.1.dist-info/top_level.txt,sha256=fanTzb9NylIXfv6Ic7spU97fVmRgGDPKvI_91tw4S3E,8
26
- flixopt-3.3.1.dist-info/RECORD,,