Skip to content

Commit 31844f0

Browse files
authored
Feature/402 feature silent framework (#407)
* Supress solver output in SegmentedCalculation in favor of a progress bar, add method to suppress_output() and add tqdm to dependencies * Add logging.info() about solving * Merge main intop feature/402-feature-silent-framework * Merge main intop feature/402-feature-silent-framework * Add extra log_to_console option to solvers.py * Add extra log_to_console option to solvers.py * Add extra log_to_console option config.py * Add to tests * Use default from console to say if logging to console (gurobipy still has some issues...) * Add rounding duration of solve * Use contextmanager to entirely supress output in SegmentedCalculation * Improve suppress_output() * More options in config.py * Update CHANGELOG.md * Use new Config options in examples * Sett plotting backend in CI directly, overwriting all configs * Fixed tqdm progress bar to respect CONFIG.silent() * Replaced print() with framework logger (examples/05_Two-stage-optimization/two_stage_optimization.py * Added comprehensive tests for suppress_output() * Remove unused import * Ensure progress bar cleanup on exceptions. * Add test * Split method in SegmentedCalculation for better distinction if show or not show solver output * USe config show in exmaples * USe config show in results.plot_network() * Improve readabailty of code * Typo
1 parent efd6b31 commit 31844f0

18 files changed

Lines changed: 598 additions & 80 deletions

File tree

.github/workflows/python-app.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ concurrency:
2424

2525
env:
2626
PYTHON_VERSION: "3.11"
27+
MPLBACKEND: Agg # Non-interactive matplotlib backend for CI/testing
28+
PLOTLY_RENDERER: json # Headless plotly renderer for CI/testing
2729

2830
jobs:
2931
lint:

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,23 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp
5151
5252
## [Unreleased] - ????-??-??
5353
54-
**Summary**:
54+
**Summary**: Enhanced solver configuration with new CONFIG.Solving section for centralized solver parameter management.
5555
5656
If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/).
5757
5858
### ✨ Added
5959
60+
**Solver configuration:**
61+
- **New `CONFIG.Solving` configuration section** for centralized solver parameter management:
62+
- `mip_gap`: Default MIP gap tolerance for solver convergence (default: 0.01)
63+
- `time_limit_seconds`: Default time limit in seconds for solver runs (default: 300)
64+
- `log_to_console`: Whether solver should output to console (default: True)
65+
- `log_main_results`: Whether to log main results after solving (default: True)
66+
- Solvers (`HighsSolver`, `GurobiSolver`) now use `CONFIG.Solving` defaults for parameters, allowing global configuration
67+
- Solver parameters can still be explicitly overridden when creating solver instances
68+
6069
### 💥 Breaking Changes
70+
- Individual solver output is now hidden in **SegmentedCalculation**. To return to the prior behaviour, set `show_individual_solves=True` in `do_modeling_and_solve()`.
6171
6272
### ♻️ Changed
6373

examples/00_Minmal/minimal_example.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
import flixopt as fx
1010

1111
if __name__ == '__main__':
12-
fx.CONFIG.Logging.console = True
13-
fx.CONFIG.apply()
12+
fx.CONFIG.silent()
1413
flow_system = fx.FlowSystem(pd.date_range('2020-01-01', periods=3, freq='h'))
1514

1615
flow_system.add_elements(

examples/01_Simple/simple_example.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88
import flixopt as fx
99

1010
if __name__ == '__main__':
11-
# Enable console logging
12-
fx.CONFIG.Logging.console = True
13-
fx.CONFIG.apply()
11+
fx.CONFIG.exploring()
12+
1413
# --- Create Time Series Data ---
1514
# Heat demand profile (e.g., kW) over time and corresponding power prices
1615
heat_demand_per_h = np.array([30, 0, 90, 110, 110, 20, 20, 20, 20])
@@ -101,7 +100,7 @@
101100
flow_system.add_elements(costs, CO2, boiler, storage, chp, heat_sink, gas_source, power_sink)
102101

103102
# Visualize the flow system for validation purposes
104-
flow_system.plot_network(show=True)
103+
flow_system.plot_network()
105104

106105
# --- Define and Run Calculation ---
107106
# Create a calculation object to model the Flow System

examples/02_Complex/complex_example.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@
99
import flixopt as fx
1010

1111
if __name__ == '__main__':
12-
# Enable console logging
13-
fx.CONFIG.Logging.console = True
14-
fx.CONFIG.apply()
12+
fx.CONFIG.exploring()
13+
1514
# --- Experiment Options ---
1615
# Configure options for testing various parameters and behaviors
1716
check_penalty = False

examples/02_Complex/complex_example_results.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55
import flixopt as fx
66

77
if __name__ == '__main__':
8-
# Enable console logging
9-
fx.CONFIG.Logging.console = True
10-
fx.CONFIG.apply()
8+
fx.CONFIG.exploring()
9+
1110
# --- Load Results ---
1211
try:
1312
results = fx.results.CalculationResults.from_file('results', 'complex example')
@@ -19,7 +18,7 @@
1918
) from e
2019

2120
# --- Basic overview ---
22-
results.plot_network(show=True)
21+
results.plot_network()
2322
results['Fernwärme'].plot_node_balance()
2423

2524
# --- Detailed Plots ---

examples/03_Calculation_types/example_calculation_types.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@
1111
import flixopt as fx
1212

1313
if __name__ == '__main__':
14-
# Enable console logging
15-
fx.CONFIG.Logging.console = True
16-
fx.CONFIG.apply()
14+
fx.CONFIG.exploring()
15+
1716
# Calculation Types
1817
full, segmented, aggregated = True, True, True
1918

@@ -165,7 +164,7 @@
165164
a_kwk,
166165
a_speicher,
167166
)
168-
flow_system.plot_network(controls=False, show=True)
167+
flow_system.plot_network()
169168

170169
# Calculations
171170
calculations: list[fx.FullCalculation | fx.AggregatedCalculation | fx.SegmentedCalculation] = []

examples/04_Scenarios/scenario_example.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import flixopt as fx
99

1010
if __name__ == '__main__':
11+
fx.CONFIG.exploring()
12+
1113
# Create datetime array starting from '2020-01-01' for one week
1214
timesteps = pd.date_range('2020-01-01', periods=24 * 7, freq='h')
1315
scenarios = pd.Index(['Base Case', 'High Demand'])
@@ -186,7 +188,7 @@
186188
flow_system.add_elements(costs, CO2, boiler, storage, chp, heat_sink, gas_source, power_sink)
187189

188190
# Visualize the flow system for validation purposes
189-
flow_system.plot_network(show=True)
191+
flow_system.plot_network()
190192

191193
# --- Define and Run Calculation ---
192194
# Create a calculation object to model the Flow System
@@ -215,7 +217,6 @@
215217

216218
# Convert the results for the storage component to a dataframe and display
217219
df = calculation.results['Storage'].node_balance_with_charge_state()
218-
print(df)
219220

220221
# Save results to file for later usage
221222
calculation.results.to_file()

examples/05_Two-stage-optimization/two_stage_optimization.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
logger = logging.getLogger('flixopt')
2020

2121
if __name__ == '__main__':
22+
fx.CONFIG.exploring()
23+
2224
# Data Import
2325
data_import = pd.read_csv(
2426
pathlib.Path(__file__).parent.parent / 'resources' / 'Zeitreihen2020.csv', index_col=0

flixopt/calculation.py

Lines changed: 99 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@
1313
import logging
1414
import math
1515
import pathlib
16+
import sys
1617
import timeit
1718
import warnings
1819
from collections import Counter
1920
from typing import TYPE_CHECKING, Annotated, Any
2021

2122
import numpy as np
2223
import yaml
24+
from tqdm import tqdm
2325

2426
from . import io as fx_io
2527
from .aggregation import Aggregation, AggregationModel, AggregationParameters
@@ -227,7 +229,7 @@ def fix_sizes(self, ds: xr.Dataset, decimal_rounding: int | None = 5) -> FullCal
227229
return self
228230

229231
def solve(
230-
self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool = True
232+
self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool | None = None
231233
) -> FullCalculation:
232234
t_start = timeit.default_timer()
233235

@@ -237,6 +239,8 @@ def solve(
237239
**solver.options,
238240
)
239241
self.durations['solving'] = round(timeit.default_timer() - t_start, 2)
242+
logger.info(f'Model solved with {solver.name} in {self.durations["solving"]:.2f} seconds.')
243+
logger.info(f'Model status after solve: {self.model.status}')
240244

241245
if self.model.status == 'warning':
242246
# Save the model and the flow_system to file in case of infeasibility
@@ -250,7 +254,8 @@ def solve(
250254
)
251255

252256
# Log the formatted output
253-
if log_main_results:
257+
should_log = log_main_results if log_main_results is not None else CONFIG.Solving.log_main_results
258+
if should_log:
254259
logger.info(
255260
f'{" Main Results ":#^80}\n'
256261
+ yaml.dump(
@@ -368,7 +373,7 @@ def _perform_aggregation(self):
368373
)
369374

370375
self.aggregation.cluster()
371-
self.aggregation.plot(show=True, save=self.folder / 'aggregation.html')
376+
self.aggregation.plot(show=CONFIG.Plotting.default_show, save=self.folder / 'aggregation.html')
372377
if self.aggregation_parameters.aggregate_data_and_fix_non_binary_vars:
373378
ds = self.flow_system.to_dataset()
374379
for name, series in self.aggregation.aggregated_data.items():
@@ -569,48 +574,111 @@ def _create_sub_calculations(self):
569574
f'({timesteps_of_segment[0]} -> {timesteps_of_segment[-1]}):'
570575
)
571576

577+
def _solve_single_segment(
578+
self,
579+
i: int,
580+
calculation: FullCalculation,
581+
solver: _Solver,
582+
log_file: pathlib.Path | None,
583+
log_main_results: bool,
584+
suppress_output: bool,
585+
) -> None:
586+
"""Solve a single segment calculation."""
587+
if i > 0 and self.nr_of_previous_values > 0:
588+
self._transfer_start_values(i)
589+
590+
calculation.do_modeling()
591+
592+
# Warn about Investments, but only in first run
593+
if i == 0:
594+
invest_elements = [
595+
model.label_full
596+
for component in calculation.flow_system.components.values()
597+
for model in component.submodel.all_submodels
598+
if isinstance(model, InvestmentModel)
599+
]
600+
if invest_elements:
601+
logger.critical(
602+
f'Investments are not supported in Segmented Calculation! '
603+
f'Following InvestmentModels were found: {invest_elements}'
604+
)
605+
606+
log_path = pathlib.Path(log_file) if log_file is not None else self.folder / f'{self.name}.log'
607+
608+
if suppress_output:
609+
with fx_io.suppress_output():
610+
calculation.solve(solver, log_file=log_path, log_main_results=log_main_results)
611+
else:
612+
calculation.solve(solver, log_file=log_path, log_main_results=log_main_results)
613+
572614
def do_modeling_and_solve(
573-
self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool = False
615+
self,
616+
solver: _Solver,
617+
log_file: pathlib.Path | None = None,
618+
log_main_results: bool = False,
619+
show_individual_solves: bool = False,
574620
) -> SegmentedCalculation:
621+
"""Model and solve all segments of the segmented calculation.
622+
623+
This method creates sub-calculations for each time segment, then iteratively
624+
models and solves each segment. It supports two output modes: a progress bar
625+
for compact output, or detailed individual solve information.
626+
627+
Args:
628+
solver: The solver instance to use for optimization (e.g., Gurobi, HiGHS).
629+
log_file: Optional path to the solver log file. If None, defaults to
630+
folder/name.log.
631+
log_main_results: Whether to log main results (objective, effects, etc.)
632+
after each segment solve. Defaults to False.
633+
show_individual_solves: If True, shows detailed output for each segment
634+
solve with logger messages. If False (default), shows a compact progress
635+
bar with suppressed solver output for cleaner display.
636+
637+
Returns:
638+
Self, for method chaining.
639+
640+
Note:
641+
The method automatically transfers all start values between segments to ensure
642+
continuity of storage states and flow rates across segment boundaries.
643+
"""
575644
logger.info(f'{"":#^80}')
576645
logger.info(f'{" Segmented Solving ":#^80}')
577646
self._create_sub_calculations()
578647

579-
for i, calculation in enumerate(self.sub_calculations):
580-
logger.info(
581-
f'{self.segment_names[i]} [{i + 1:>2}/{len(self.segment_names):<2}] '
582-
f'({calculation.flow_system.timesteps[0]} -> {calculation.flow_system.timesteps[-1]}):'
648+
if show_individual_solves:
649+
# Path 1: Show individual solves with detailed output
650+
for i, calculation in enumerate(self.sub_calculations):
651+
logger.info(
652+
f'Solving segment {i + 1}/{len(self.sub_calculations)}: '
653+
f'{calculation.flow_system.timesteps[0]} -> {calculation.flow_system.timesteps[-1]}'
654+
)
655+
self._solve_single_segment(i, calculation, solver, log_file, log_main_results, suppress_output=False)
656+
else:
657+
# Path 2: Show only progress bar with suppressed output
658+
progress_bar = tqdm(
659+
enumerate(self.sub_calculations),
660+
total=len(self.sub_calculations),
661+
desc='Solving segments',
662+
unit='segment',
663+
file=sys.stdout,
664+
disable=not CONFIG.Solving.log_to_console,
583665
)
584666

585-
if i > 0 and self.nr_of_previous_values > 0:
586-
self._transfer_start_values(i)
587-
588-
calculation.do_modeling()
589-
590-
# Warn about Investments, but only in fist run
591-
if i == 0:
592-
invest_elements = [
593-
model.label_full
594-
for component in calculation.flow_system.components.values()
595-
for model in component.submodel.all_submodels
596-
if isinstance(model, InvestmentModel)
597-
]
598-
if invest_elements:
599-
logger.critical(
600-
f'Investments are not supported in Segmented Calculation! '
601-
f'Following InvestmentModels were found: {invest_elements}'
667+
try:
668+
for i, calculation in progress_bar:
669+
progress_bar.set_description(
670+
f'Solving ({calculation.flow_system.timesteps[0]} -> {calculation.flow_system.timesteps[-1]})'
602671
)
603-
604-
calculation.solve(
605-
solver,
606-
log_file=pathlib.Path(log_file) if log_file is not None else self.folder / f'{self.name}.log',
607-
log_main_results=log_main_results,
608-
)
672+
self._solve_single_segment(i, calculation, solver, log_file, log_main_results, suppress_output=True)
673+
finally:
674+
progress_bar.close()
609675

610676
for calc in self.sub_calculations:
611677
for key, value in calc.durations.items():
612678
self.durations[key] += value
613679

680+
logger.info(f'Model solved with {solver.name} in {self.durations["solving"]:.2f} seconds.')
681+
614682
self.results = SegmentedCalculationResults.from_calculation(self)
615683

616684
return self

0 commit comments

Comments
 (0)