Skip to content

Commit d83cc2a

Browse files
committed
Added before_solve callback parameter to both optimize() and rolling_horizon():
# Simple usage flow_system.optimize(solver) # With custom constraints def add_constraints(fs): model = fs.model boiler = model.variables['Boiler(Q_th)|flow_rate'] model.add_constraints(boiler >= 10, name='min_boiler') flow_system.optimize(solver, before_solve=add_constraints) # Works with rolling_horizon too flow_system.optimize.rolling_horizon( solver, horizon=168, before_solve=add_constraints ) Key design decision: The callback receives FlowSystem (not FlowSystemModel) so users have access to: - fs.model for adding constraints - fs.timesteps for time-based constraint logic - fs.components, fs.buses, etc. for referencing elements
1 parent ab04f19 commit d83cc2a

1 file changed

Lines changed: 46 additions & 11 deletions

File tree

flixopt/optimize_accessor.py

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
from .io import suppress_output
1919

2020
if TYPE_CHECKING:
21+
from collections.abc import Callable
22+
2123
from .flow_system import FlowSystem
2224
from .solvers import _Solver
2325

@@ -53,17 +55,24 @@ def __init__(self, flow_system: FlowSystem) -> None:
5355
"""
5456
self._fs = flow_system
5557

56-
def __call__(self, solver: _Solver, normalize_weights: bool | None = None) -> FlowSystem:
58+
def __call__(
59+
self,
60+
solver: _Solver,
61+
before_solve: Callable[[FlowSystem], None] | None = None,
62+
normalize_weights: bool | None = None,
63+
) -> FlowSystem:
5764
"""
5865
Build and solve the optimization model in one step.
5966
6067
This is a convenience method that combines `build_model()` and `solve()`.
61-
Use this for simple optimization workflows. For more control (e.g., inspecting
62-
the model before solving, or adding custom constraints), use `build_model()`
63-
and `solve()` separately.
68+
Use the optional `before_solve` callback to add custom constraints or
69+
modify the model before solving.
6470
6571
Args:
6672
solver: The solver to use (e.g., HighsSolver, GurobiSolver).
73+
before_solve: Optional callback function that receives the FlowSystem
74+
after building the model and before solving. Use this to add custom
75+
constraints via `flow_system.model.add_constraints()`.
6776
normalize_weights: Deprecated. Scenario weights are now always normalized in FlowSystem.
6877
6978
Returns:
@@ -75,11 +84,25 @@ def __call__(self, solver: _Solver, normalize_weights: bool | None = None) -> Fl
7584
>>> flow_system.optimize(HighsSolver())
7685
>>> print(flow_system.solution['Boiler(Q_th)|flow_rate'])
7786
78-
Access element solutions directly:
87+
With custom constraints:
88+
89+
>>> def add_constraints(fs):
90+
... model = fs.model
91+
... boiler = model.variables['Boiler(Q_th)|flow_rate']
92+
... chp = model.variables['CHP(Q_th)|flow_rate']
93+
... model.add_constraints(boiler >= chp, name='boiler_min_chp')
94+
>>> flow_system.optimize(solver, before_solve=add_constraints)
95+
96+
Using FlowSystem context in constraints:
7997
80-
>>> flow_system.optimize(solver)
81-
>>> boiler = flow_system.components['Boiler']
82-
>>> print(boiler.solution)
98+
>>> def seasonal_constraints(fs):
99+
... summer = fs.timesteps.month.isin([6, 7, 8])
100+
... flow = fs.model.variables['Boiler(Q_th)|flow_rate']
101+
... fs.model.add_constraints(
102+
... flow.sel(time=fs.timesteps[summer]) <= 50,
103+
... name='summer_limit',
104+
... )
105+
>>> flow_system.optimize(solver, before_solve=seasonal_constraints)
83106
84107
Method chaining:
85108
@@ -97,6 +120,8 @@ def __call__(self, solver: _Solver, normalize_weights: bool | None = None) -> Fl
97120
stacklevel=2,
98121
)
99122
self._fs.build_model()
123+
if before_solve is not None:
124+
before_solve(self._fs)
100125
self._fs.solve(solver)
101126
return self._fs
102127

@@ -106,6 +131,7 @@ def rolling_horizon(
106131
horizon: int = 100,
107132
overlap: int = 0,
108133
nr_of_previous_values: int = 1,
134+
before_solve: Callable[[FlowSystem], None] | None = None,
109135
) -> list[FlowSystem]:
110136
"""
111137
Solve the optimization using a rolling horizon approach.
@@ -130,6 +156,9 @@ def rolling_horizon(
130156
improve solution quality but increase computational cost. Default: 0.
131157
nr_of_previous_values: Number of previous timestep values to transfer between
132158
segments for initialization (e.g., for uptime/downtime tracking). Default: 1.
159+
before_solve: Optional callback function that receives each segment's FlowSystem
160+
after building the model and before solving. Use this to add custom
161+
constraints to each segment.
133162
134163
Returns:
135164
List of segment FlowSystems, each with their individual solution.
@@ -150,10 +179,12 @@ def rolling_horizon(
150179
... )
151180
>>> print(flow_system.solution) # Combined result
152181
153-
Inspect individual segments:
182+
With custom constraints per segment:
154183
155-
>>> for i, seg in enumerate(segments):
156-
... print(f'Segment {i}: {seg.solution["costs"].item():.2f}')
184+
>>> def add_constraints(fs):
185+
... flow = fs.model.variables['Boiler(Q_th)|flow_rate']
186+
... fs.model.add_constraints(flow >= 10, name='min_boiler')
187+
>>> segments = flow_system.optimize.rolling_horizon(solver, horizon=168, before_solve=add_constraints)
157188
158189
Note:
159190
- InvestParameters are not supported as investment decisions require
@@ -227,6 +258,8 @@ def rolling_horizon(
227258
segment_fs.build_model()
228259
if i == 0:
229260
self._check_no_investments(segment_fs)
261+
if before_solve is not None:
262+
before_solve(segment_fs)
230263
segment_fs.solve(solver)
231264
finally:
232265
logger.setLevel(original_level)
@@ -242,6 +275,8 @@ def rolling_horizon(
242275
segment_fs.build_model()
243276
if i == 0:
244277
self._check_no_investments(segment_fs)
278+
if before_solve is not None:
279+
before_solve(segment_fs)
245280
segment_fs.solve(solver)
246281

247282
segment_flow_systems.append(segment_fs)

0 commit comments

Comments
 (0)