Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e65da73
Supress solver output in SegmentedCalculation in favor of a progress …
FBumann Oct 15, 2025
05180a8
Add logging.info() about solving
FBumann Oct 15, 2025
aeb5f05
Merge remote-tracking branch 'origin/main' into feature/402-feature-s…
FBumann Oct 30, 2025
5d9c672
Merge main intop feature/402-feature-silent-framework
FBumann Oct 30, 2025
03b1202
Merge main intop feature/402-feature-silent-framework
FBumann Oct 30, 2025
fc42bc2
Add extra log_to_console option to solvers.py
FBumann Oct 30, 2025
d3bcdc2
Add extra log_to_console option to solvers.py
FBumann Oct 30, 2025
7931580
Add extra log_to_console option config.py
FBumann Oct 30, 2025
168ec61
Add to tests
FBumann Oct 30, 2025
20602f9
Use default from console to say if logging to console (gurobipy still…
FBumann Oct 30, 2025
95b9217
Add rounding duration of solve
FBumann Oct 30, 2025
677f534
Use contextmanager to entirely supress output in SegmentedCalculation
FBumann Oct 30, 2025
767d8ec
Improve suppress_output()
FBumann Oct 30, 2025
faf4267
More options in config.py
FBumann Oct 30, 2025
69ffb13
Update CHANGELOG.md
FBumann Oct 30, 2025
2fbbd3a
Use new Config options in examples
FBumann Oct 30, 2025
209cdfd
Sett plotting backend in CI directly, overwriting all configs
FBumann Oct 30, 2025
f529d9b
Fixed tqdm progress bar to respect CONFIG.silent()
FBumann Oct 30, 2025
3ea3881
Replaced print() with framework logger (examples/05_Two-stage-optimiz…
FBumann Oct 30, 2025
284e3a5
Added comprehensive tests for suppress_output()
FBumann Oct 30, 2025
8f613bc
Remove unused import
FBumann Oct 31, 2025
2bd25bc
Ensure progress bar cleanup on exceptions.
FBumann Oct 31, 2025
6d6f15e
Add test
FBumann Oct 31, 2025
3ad25a0
Split method in SegmentedCalculation for better distinction if show o…
FBumann Oct 31, 2025
691d95c
USe config show in exmaples
FBumann Oct 31, 2025
8a504ef
USe config show in results.plot_network()
FBumann Oct 31, 2025
59b125a
Improve readabailty of code
FBumann Oct 31, 2025
f3f54c9
Typo
FBumann Nov 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/python-app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ concurrency:

env:
PYTHON_VERSION: "3.11"
MPLBACKEND: Agg # Non-interactive matplotlib backend for CI/testing
PLOTLY_RENDERER: json # Headless plotly renderer for CI/testing

jobs:
lint:
Expand Down
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,23 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp

## [Unreleased] - ????-??-??

**Summary**:
**Summary**: Enhanced solver configuration with new CONFIG.Solving section for centralized solver parameter management.

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/).

### ✨ Added

**Solver configuration:**
- **New `CONFIG.Solving` configuration section** for centralized solver parameter management:
- `mip_gap`: Default MIP gap tolerance for solver convergence (default: 0.01)
- `time_limit_seconds`: Default time limit in seconds for solver runs (default: 300)
- `log_to_console`: Whether solver should output to console (default: True)
- `log_main_results`: Whether to log main results after solving (default: True)
- Solvers (`HighsSolver`, `GurobiSolver`) now use `CONFIG.Solving` defaults for parameters, allowing global configuration
- Solver parameters can still be explicitly overridden when creating solver instances

### 💥 Breaking Changes
- Individual solver output is now hidden in **SegmentedCalculation**. To return to the prior behaviour, set `show_individual_solves=True` in `do_modeling_and_solve()`.

### ♻️ Changed

Expand Down
3 changes: 1 addition & 2 deletions examples/00_Minmal/minimal_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
import flixopt as fx

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

flow_system.add_elements(
Expand Down
7 changes: 3 additions & 4 deletions examples/01_Simple/simple_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@
import flixopt as fx

if __name__ == '__main__':
# Enable console logging
fx.CONFIG.Logging.console = True
fx.CONFIG.apply()
fx.CONFIG.exploring()

Comment on lines +11 to +12
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard CONFIG.exploring() against missing Plotly

Line 11 now calls fx.CONFIG.exploring(), which immediately runs CONFIG.browser_plotting() (see flixopt/config.py lines 476-491). That helper unconditionally imports plotly.io; if Plotly is not installed—which was previously fine for this quick-start example—the import raises and the script aborts before doing anything. This regressions affects every entrypoint that now uses CONFIG.exploring(). Please make the helper tolerant of missing Plotly (e.g., catch the ImportError and fall back to default_show=False) before switching the examples to it.

Apply this change in flixopt/config.py:

 @classmethod
 def browser_plotting(cls) -> type[CONFIG]:
-    cls.Plotting.default_show = True
-    cls.apply()
-
-    import plotly.io as pio
-
-    pio.renderers.default = 'browser'
+    try:
+        import plotly.io as pio
+    except ImportError:
+        cls.Plotting.default_show = False
+        cls.apply()
+        return cls
+
+    cls.Plotting.default_show = True
+    cls.apply()
+    pio.renderers.default = 'browser'

This keeps the new configuration API but avoids breaking environments without Plotly. Based on relevant snippet.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fx.CONFIG.exploring()
@classmethod
def browser_plotting(cls) -> type[CONFIG]:
try:
import plotly.io as pio
except ImportError:
cls.Plotting.default_show = False
cls.apply()
return cls
cls.Plotting.default_show = True
cls.apply()
pio.renderers.default = 'browser'
🤖 Prompt for AI Agents
In flixopt/config.py around the browser_plotting() helper (referenced by
examples/01_Simple/simple_example.py lines 11-12), the function unconditionally
imports plotly.io which raises ImportError when Plotly is not installed and
aborts the script; modify browser_plotting() to catch ImportError (or
ModuleNotFoundError), and when caught set default_show=False (and optionally
emit a debug/warn message) so CONFIG.exploring() becomes tolerant of missing
Plotly and won’t break quick-start examples.

# --- Create Time Series Data ---
# Heat demand profile (e.g., kW) over time and corresponding power prices
heat_demand_per_h = np.array([30, 0, 90, 110, 110, 20, 20, 20, 20])
Expand Down Expand Up @@ -101,7 +100,7 @@
flow_system.add_elements(costs, CO2, boiler, storage, chp, heat_sink, gas_source, power_sink)

# Visualize the flow system for validation purposes
flow_system.plot_network(show=True)
flow_system.plot_network()

# --- Define and Run Calculation ---
# Create a calculation object to model the Flow System
Expand Down
5 changes: 2 additions & 3 deletions examples/02_Complex/complex_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@
import flixopt as fx

if __name__ == '__main__':
# Enable console logging
fx.CONFIG.Logging.console = True
fx.CONFIG.apply()
fx.CONFIG.exploring()

# --- Experiment Options ---
# Configure options for testing various parameters and behaviors
check_penalty = False
Expand Down
7 changes: 3 additions & 4 deletions examples/02_Complex/complex_example_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
import flixopt as fx

if __name__ == '__main__':
# Enable console logging
fx.CONFIG.Logging.console = True
fx.CONFIG.apply()
fx.CONFIG.exploring()

# --- Load Results ---
try:
results = fx.results.CalculationResults.from_file('results', 'complex example')
Expand All @@ -19,7 +18,7 @@
) from e

# --- Basic overview ---
results.plot_network(show=True)
results.plot_network()
results['Fernwärme'].plot_node_balance()

# --- Detailed Plots ---
Expand Down
7 changes: 3 additions & 4 deletions examples/03_Calculation_types/example_calculation_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@
import flixopt as fx

if __name__ == '__main__':
# Enable console logging
fx.CONFIG.Logging.console = True
fx.CONFIG.apply()
fx.CONFIG.exploring()

# Calculation Types
full, segmented, aggregated = True, True, True

Expand Down Expand Up @@ -165,7 +164,7 @@
a_kwk,
a_speicher,
)
flow_system.plot_network(controls=False, show=True)
flow_system.plot_network()

# Calculations
calculations: list[fx.FullCalculation | fx.AggregatedCalculation | fx.SegmentedCalculation] = []
Expand Down
5 changes: 3 additions & 2 deletions examples/04_Scenarios/scenario_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import flixopt as fx

if __name__ == '__main__':
fx.CONFIG.exploring()

# Create datetime array starting from '2020-01-01' for one week
timesteps = pd.date_range('2020-01-01', periods=24 * 7, freq='h')
scenarios = pd.Index(['Base Case', 'High Demand'])
Expand Down Expand Up @@ -186,7 +188,7 @@
flow_system.add_elements(costs, CO2, boiler, storage, chp, heat_sink, gas_source, power_sink)

# Visualize the flow system for validation purposes
flow_system.plot_network(show=True)
flow_system.plot_network()

# --- Define and Run Calculation ---
# Create a calculation object to model the Flow System
Expand Down Expand Up @@ -215,7 +217,6 @@

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

# Save results to file for later usage
calculation.results.to_file()
2 changes: 2 additions & 0 deletions examples/05_Two-stage-optimization/two_stage_optimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
logger = logging.getLogger('flixopt')

if __name__ == '__main__':
fx.CONFIG.exploring()

# Data Import
data_import = pd.read_csv(
pathlib.Path(__file__).parent.parent / 'resources' / 'Zeitreihen2020.csv', index_col=0
Expand Down
130 changes: 99 additions & 31 deletions flixopt/calculation.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
import logging
import math
import pathlib
import sys
import timeit
import warnings
from collections import Counter
from typing import TYPE_CHECKING, Annotated, Any

import numpy as np
import yaml
from tqdm import tqdm

from . import io as fx_io
from .aggregation import Aggregation, AggregationModel, AggregationParameters
Expand Down Expand Up @@ -225,7 +227,7 @@ def fix_sizes(self, ds: xr.Dataset, decimal_rounding: int | None = 5) -> FullCal
return self

def solve(
self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool = True
self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool | None = None
) -> FullCalculation:
t_start = timeit.default_timer()

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

if self.model.status == 'warning':
# Save the model and the flow_system to file in case of infeasibility
Expand All @@ -248,7 +252,8 @@ def solve(
)

# Log the formatted output
if log_main_results:
should_log = log_main_results if log_main_results is not None else CONFIG.Solving.log_main_results
if should_log:
logger.info(
f'{" Main Results ":#^80}\n'
+ yaml.dump(
Expand Down Expand Up @@ -366,7 +371,7 @@ def _perform_aggregation(self):
)

self.aggregation.cluster()
self.aggregation.plot(show=True, save=self.folder / 'aggregation.html')
self.aggregation.plot(show=CONFIG.Plotting.default_show, save=self.folder / 'aggregation.html')
if self.aggregation_parameters.aggregate_data_and_fix_non_binary_vars:
ds = self.flow_system.to_dataset()
for name, series in self.aggregation.aggregated_data.items():
Expand Down Expand Up @@ -567,48 +572,111 @@ def _create_sub_calculations(self):
f'({timesteps_of_segment[0]} -> {timesteps_of_segment[-1]}):'
)

def _solve_single_segment(
self,
i: int,
calculation: FullCalculation,
solver: _Solver,
log_file: pathlib.Path | None,
log_main_results: bool,
suppress_output: bool,
) -> None:
"""Solve a single segment calculation."""
if i > 0 and self.nr_of_previous_values > 0:
self._transfer_start_values(i)

calculation.do_modeling()

# Warn about Investments, but only in first run
if i == 0:
invest_elements = [
model.label_full
for component in calculation.flow_system.components.values()
for model in component.submodel.all_submodels
if isinstance(model, InvestmentModel)
]
if invest_elements:
logger.critical(
f'Investments are not supported in Segmented Calculation! '
f'Following InvestmentModels were found: {invest_elements}'
)

log_path = pathlib.Path(log_file) if log_file is not None else self.folder / f'{self.name}.log'

if suppress_output:
with fx_io.suppress_output():
calculation.solve(solver, log_file=log_path, log_main_results=log_main_results)
else:
calculation.solve(solver, log_file=log_path, log_main_results=log_main_results)

def do_modeling_and_solve(
self, solver: _Solver, log_file: pathlib.Path | None = None, log_main_results: bool = False
self,
solver: _Solver,
log_file: pathlib.Path | None = None,
log_main_results: bool = False,
show_individual_solves: bool = False,
) -> SegmentedCalculation:
"""Model and solve all segments of the segmented calculation.

This method creates sub-calculations for each time segment, then iteratively
models and solves each segment. It supports two output modes: a progress bar
for compact output, or detailed individual solve information.

Args:
solver: The solver instance to use for optimization (e.g., Gurobi, HiGHS).
log_file: Optional path to the solver log file. If None, defaults to
folder/name.log.
log_main_results: Whether to log main results (objective, effects, etc.)
after each segment solve. Defaults to False.
show_individual_solves: If True, shows detailed output for each segment
solve with logger messages. If False (default), shows a compact progress
bar with suppressed solver output for cleaner display.

Returns:
Self, for method chaining.

Note:
The method automatically transfers all start values between segments to ensure
continuity of storage states and flow rates across segment boundaries.
"""
logger.info(f'{"":#^80}')
logger.info(f'{" Segmented Solving ":#^80}')
self._create_sub_calculations()

for i, calculation in enumerate(self.sub_calculations):
logger.info(
f'{self.segment_names[i]} [{i + 1:>2}/{len(self.segment_names):<2}] '
f'({calculation.flow_system.timesteps[0]} -> {calculation.flow_system.timesteps[-1]}):'
if show_individual_solves:
# Path 1: Show individual solves with detailed output
for i, calculation in enumerate(self.sub_calculations):
logger.info(
f'Solving segment {i + 1}/{len(self.sub_calculations)}: '
f'{calculation.flow_system.timesteps[0]} -> {calculation.flow_system.timesteps[-1]}'
)
self._solve_single_segment(i, calculation, solver, log_file, log_main_results, suppress_output=False)
else:
# Path 2: Show only progress bar with suppressed output
progress_bar = tqdm(
enumerate(self.sub_calculations),
total=len(self.sub_calculations),
desc='Solving segments',
unit='segment',
file=sys.stdout,
disable=not CONFIG.Solving.log_to_console,
)

if i > 0 and self.nr_of_previous_values > 0:
self._transfer_start_values(i)

calculation.do_modeling()

# Warn about Investments, but only in fist run
if i == 0:
invest_elements = [
model.label_full
for component in calculation.flow_system.components.values()
for model in component.submodel.all_submodels
if isinstance(model, InvestmentModel)
]
if invest_elements:
logger.critical(
f'Investments are not supported in Segmented Calculation! '
f'Following InvestmentModels were found: {invest_elements}'
try:
for i, calculation in progress_bar:
progress_bar.set_description(
f'Solving ({calculation.flow_system.timesteps[0]} -> {calculation.flow_system.timesteps[-1]})'
)

calculation.solve(
solver,
log_file=pathlib.Path(log_file) if log_file is not None else self.folder / f'{self.name}.log',
log_main_results=log_main_results,
)
self._solve_single_segment(i, calculation, solver, log_file, log_main_results, suppress_output=True)
finally:
progress_bar.close()

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

logger.info(f'Model solved with {solver.name} in {self.durations["solving"]:.2f} seconds.')

self.results = SegmentedCalculationResults.from_calculation(self)

return self
Expand Down
Loading