1818from .io import suppress_output
1919
2020if 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