Monitor solver progress and enforce time limits during optimization
Published
August 24, 2026
1 Overview
Long-running optimizations benefit from real-time visibility into solver progress. Optyx exposes a simple callback interface that delivers a SolverProgress snapshot at every iteration. You can use it to:
Log objective value, constraint violation, and elapsed time
Terminate early when a solution is “good enough”
Enforce a time budget so the solver never runs longer than allowed
Combine a callback with a time limit
All three NLP methods (SLSQP, trust-constr, L-BFGS-B) support callbacks and time limits.
2 Part 1: Logging Solver Progress
We minimize a 10-dimension Rosenbrock function and print a status line at every iteration.
iter 1 | obj 360144.0000 | violation 0.00e+00 | time 0.001s
iter 2 | obj 65177.2777 | violation 0.00e+00 | time 0.001s
iter 3 | obj 195283.1899 | violation 0.00e+00 | time 0.002s
iter 4 | obj 297635.2816 | violation 0.00e+00 | time 0.002s
iter 5 | obj 311586.0357 | violation 0.00e+00 | time 0.002s
iter 6 | obj 353866.5635 | violation 0.00e+00 | time 0.002s
iter 7 | obj 257528.5310 | violation 0.00e+00 | time 0.002s
iter 8 | obj 206097.6517 | violation 0.00e+00 | time 0.002s
iter 9 | obj 239567.5517 | violation 0.00e+00 | time 0.003s
iter 10 | obj 64728.9717 | violation 0.00e+00 | time 0.003s
iter 11 | obj 114520.5722 | violation 0.00e+00 | time 0.003s
iter 12 | obj 4.5025 | violation 0.00e+00 | time 0.003s
iter 13 | obj 4.2049 | violation 0.00e+00 | time 0.003s
iter 14 | obj 4.1354 | violation 0.00e+00 | time 0.003s
iter 15 | obj 4.1244 | violation 0.00e+00 | time 0.003s
iter 16 | obj 4.1233 | violation 0.00e+00 | time 0.003s
iter 17 | obj 4.1206 | violation 0.00e+00 | time 0.004s
iter 18 | obj 4.1114 | violation 0.00e+00 | time 0.004s
iter 19 | obj 4.0894 | violation 0.00e+00 | time 0.004s
iter 20 | obj 4.0296 | violation 0.00e+00 | time 0.004s
iter 21 | obj 3.8590 | violation 0.00e+00 | time 0.004s
iter 22 | obj 4.8255 | violation 0.00e+00 | time 0.004s
iter 23 | obj 195.1493 | violation 0.00e+00 | time 0.004s
iter 24 | obj 3.2746 | violation 0.00e+00 | time 0.004s
iter 25 | obj 2.7195 | violation 0.00e+00 | time 0.004s
iter 26 | obj 2.5076 | violation 0.00e+00 | time 0.005s
iter 27 | obj 2.3862 | violation 0.00e+00 | time 0.005s
iter 28 | obj 2.2280 | violation 0.00e+00 | time 0.005s
iter 29 | obj 3.1347 | violation 0.00e+00 | time 0.005s
iter 30 | obj 3.1953 | violation 0.00e+00 | time 0.005s
iter 31 | obj 1.3750 | violation 0.00e+00 | time 0.005s
iter 32 | obj 1.2634 | violation 0.00e+00 | time 0.005s
iter 33 | obj 0.8583 | violation 0.00e+00 | time 0.005s
iter 34 | obj 0.7933 | violation 0.00e+00 | time 0.005s
iter 35 | obj 0.5745 | violation 0.00e+00 | time 0.006s
iter 36 | obj 0.5503 | violation 0.00e+00 | time 0.006s
iter 37 | obj 0.4323 | violation 0.00e+00 | time 0.006s
iter 38 | obj 0.3462 | violation 0.00e+00 | time 0.006s
iter 39 | obj 0.2483 | violation 0.00e+00 | time 0.006s
iter 40 | obj 0.1748 | violation 0.00e+00 | time 0.006s
iter 41 | obj 0.1081 | violation 0.00e+00 | time 0.006s
iter 42 | obj 0.0998 | violation 0.00e+00 | time 0.006s
iter 43 | obj 0.1667 | violation 0.00e+00 | time 0.007s
iter 44 | obj 0.1171 | violation 0.00e+00 | time 0.007s
iter 45 | obj 0.0214 | violation 0.00e+00 | time 0.007s
iter 46 | obj 0.0132 | violation 0.00e+00 | time 0.007s
iter 47 | obj 0.0035 | violation 0.00e+00 | time 0.007s
iter 48 | obj 0.0020 | violation 0.00e+00 | time 0.007s
iter 49 | obj 0.0007 | violation 0.00e+00 | time 0.007s
iter 50 | obj 0.0002 | violation 0.00e+00 | time 0.007s
iter 51 | obj 0.0000 | violation 0.00e+00 | time 0.008s
iter 52 | obj 0.0000 | violation 0.00e+00 | time 0.008s
iter 53 | obj 0.0000 | violation 0.00e+00 | time 0.008s
iter 54 | obj 0.0000 | violation 0.00e+00 | time 0.008s
iter 1 | obj 0.0000 | violation 0.00e+00 | time 0.003s
iter 2 | obj 0.0001 | violation 0.00e+00 | time 0.005s
iter 3 | obj 0.0003 | violation 0.00e+00 | time 0.007s
iter 4 | obj 0.0003 | violation 0.00e+00 | time 0.009s
iter 5 | obj 0.0003 | violation 0.00e+00 | time 0.011s
iter 6 | obj 0.0000 | violation 0.00e+00 | time 0.013s
iter 7 | obj 0.0000 | violation 0.00e+00 | time 0.015s
iter 8 | obj 0.0000 | violation 0.00e+00 | time 0.016s
iter 9 | obj 0.0000 | violation 0.00e+00 | time 0.018s
iter 10 | obj 0.0000 | violation 0.00e+00 | time 0.020s
iter 11 | obj 0.0000 | violation 0.00e+00 | time 0.022s
iter 12 | obj 0.0000 | violation 0.00e+00 | time 0.023s
iter 13 | obj 0.0000 | violation 0.00e+00 | time 0.024s
iter 14 | obj 0.0000 | violation 0.00e+00 | time 0.027s
iter 15 | obj 0.0000 | violation 0.00e+00 | time 0.028s
iter 16 | obj 0.0000 | violation 0.00e+00 | time 0.029s
iter 17 | obj 0.0000 | violation 0.00e+00 | time 0.031s
iter 18 | obj 0.0000 | violation 0.00e+00 | time 0.033s
iter 19 | obj 0.0000 | violation 0.00e+00 | time 0.035s
iter 20 | obj 0.0000 | violation 0.00e+00 | time 0.036s
iter 21 | obj 0.0000 | violation 0.00e+00 | time 0.037s
iter 22 | obj 0.0000 | violation 0.00e+00 | time 0.037s
iter 23 | obj 0.0000 | violation 0.00e+00 | time 0.038s
iter 24 | obj 0.0000 | violation 0.00e+00 | time 0.038s
iter 25 | obj 0.0000 | violation 0.00e+00 | time 0.039s
iter 26 | obj 0.0000 | violation 0.00e+00 | time 0.040s
iter 27 | obj 0.0000 | violation 0.00e+00 | time 0.040s
iter 28 | obj 0.0000 | violation 0.00e+00 | time 0.041s
iter 29 | obj 0.0000 | violation 0.00e+00 | time 0.041s
iter 30 | obj 0.0000 | violation 0.00e+00 | time 0.042s
iter 31 | obj 0.0000 | violation 0.00e+00 | time 0.042s
iter 32 | obj 0.0000 | violation 0.00e+00 | time 0.043s
iter 33 | obj 0.0000 | violation 0.00e+00 | time 0.044s
iter 34 | obj 0.0000 | violation 0.00e+00 | time 0.044s
iter 35 | obj 0.0000 | violation 0.00e+00 | time 0.045s
iter 36 | obj 0.0000 | violation 0.00e+00 | time 0.045s
iter 37 | obj 0.0000 | violation 0.00e+00 | time 0.047s
iter 38 | obj 0.0000 | violation 0.00e+00 | time 0.049s
Status: optimal | Objective: 0.000000
/tmp/ipykernel_2701/1938637375.py:22: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 8.60e-03). Retrying with trust-constr method for more robust optimization.
sol = prob.solve(method="SLSQP", callback=log_progress)
The SolverProgress object contains five fields:
Field
Type
Description
iteration
int
Current iteration number (1-based)
objective_value
float
Objective in the original sense
constraint_violation
float
Max constraint violation (0 if feasible)
elapsed_time
float
Wall-clock seconds since solve started
x
np.ndarray
Current variable values
3 Part 2: Early Termination
Return True from the callback to stop the solver. The solution will have status SolverStatus.TERMINATED.
prob.reset() # clear warm start so we see more iterationsTHRESHOLD =1.0def stop_when_good_enough(p: SolverProgress) ->bool:if p.objective_value < THRESHOLD:print(f" Objective {p.objective_value:.4f} < {THRESHOLD} — stopping early")returnTruereturnFalsesol = prob.solve(method="SLSQP", callback=stop_when_good_enough)print(f"Status: {sol.status.value} | Objective: {sol.objective_value:.6f}")
Status: optimal | Objective: 0.000000
Recorded 92 objective snapshots
/tmp/ipykernel_2701/4199719111.py:8: UserWarning: SLSQP returned a feasible but non-stationary solution (stationarity residual: 8.60e-03). Retrying with trust-constr method for more robust optimization.
sol = prob.solve(method="SLSQP", callback=record_objective, time_limit=0.05)
6 Key Points
callback receives a SolverProgress each iteration; return True to stop.
time_limit is a wall-clock budget in seconds.
Early-terminated solutions still contain variable values, objective, and solve time — they just carry SolverStatus.TERMINATED instead of OPTIMAL.
Both parameters are supported by all NLP methods (SLSQP, trust-constr, L-BFGS-B).