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.
Part 1: Logging Solver Progress
We minimize a 10-dimension Rosenbrock function and print a status line at every iteration.
from optyx import Problem, SolverProgress, VectorVariable
n = 10
v = VectorVariable("v" , n, lb=- 5 , ub= 5 )
prob = Problem("rosenbrock" )
# Vectorized Rosenbrock — VectorVariable supports slicing
v_head = v[:- 1 ] # first n-1 elements
v_tail = v[1 :] # last n-1 elements
obj = ((1 - v_head) ** 2 + 100 * (v_tail - v_head ** 2 ) ** 2 ).sum ()
prob.minimize(obj)
def log_progress(p: SolverProgress) -> None :
print (
f" iter { p. iteration:3d} | obj { p. objective_value:12.4f} "
f" | violation { p. constraint_violation:.2e} "
f" | time { p. elapsed_time:.3f} s"
)
sol = prob.solve(method= "SLSQP" , callback= log_progress)
print (f" \n Status: { sol. status. value} | Objective: { sol. objective_value:.6f} " )
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.001s
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.6516 | 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.004s
iter 16 | obj 4.1233 | violation 0.00e+00 | time 0.004s
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.005s
iter 24 | obj 3.2746 | violation 0.00e+00 | time 0.005s
iter 25 | obj 2.7195 | violation 0.00e+00 | time 0.005s
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.006s
iter 31 | obj 1.3750 | violation 0.00e+00 | time 0.006s
iter 32 | obj 1.2634 | violation 0.00e+00 | time 0.006s
iter 33 | obj 0.8583 | violation 0.00e+00 | time 0.006s
iter 34 | obj 0.7933 | violation 0.00e+00 | time 0.006s
iter 35 | obj 0.5745 | violation 0.00e+00 | time 0.007s
iter 36 | obj 0.5503 | violation 0.00e+00 | time 0.007s
iter 37 | obj 0.4323 | violation 0.00e+00 | time 0.007s
iter 38 | obj 0.3462 | violation 0.00e+00 | time 0.007s
iter 39 | obj 0.2483 | violation 0.00e+00 | time 0.007s
iter 40 | obj 0.1748 | violation 0.00e+00 | time 0.007s
iter 41 | obj 0.1081 | violation 0.00e+00 | time 0.007s
iter 42 | obj 0.0998 | violation 0.00e+00 | time 0.007s
iter 43 | obj 0.1668 | violation 0.00e+00 | time 0.008s
iter 44 | obj 0.1172 | violation 0.00e+00 | time 0.008s
iter 45 | obj 0.0215 | violation 0.00e+00 | time 0.008s
iter 46 | obj 0.0132 | violation 0.00e+00 | time 0.008s
iter 47 | obj 0.0035 | violation 0.00e+00 | time 0.008s
iter 48 | obj 0.0020 | violation 0.00e+00 | time 0.008s
iter 49 | obj 0.0007 | violation 0.00e+00 | time 0.008s
iter 50 | obj 0.0002 | violation 0.00e+00 | time 0.008s
iter 51 | obj 0.0000 | violation 0.00e+00 | time 0.009s
iter 52 | obj 0.0000 | violation 0.00e+00 | time 0.009s
iter 53 | obj 0.0000 | violation 0.00e+00 | time 0.009s
iter 54 | obj 0.0000 | violation 0.00e+00 | time 0.009s
Status: optimal | Objective: 0.000000
The SolverProgress object contains five fields:
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
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 iterations
THRESHOLD = 1.0
def stop_when_good_enough(p: SolverProgress) -> bool :
if p.objective_value < THRESHOLD:
print (f" Objective { p. objective_value:.4f} < { THRESHOLD} — stopping early" )
return True
return False
sol = prob.solve(method= "SLSQP" , callback= stop_when_good_enough)
print (f"Status: { sol. status. value} | Objective: { sol. objective_value:.6f} " )
Objective 0.8583 < 1.0 — stopping early
Status: terminated | Objective: 0.858335
Returning None or False lets the solver continue normally.
Part 3: Time Limits
Pass time_limit= (in seconds) to cap the wall-clock duration. This is useful for production systems that must respond within a deadline.
prob.reset()
sol = prob.solve(method= "SLSQP" , time_limit= 0.005 )
print (f"Status: { sol. status. value} " )
print (f"Objective: { sol. objective_value:.6f} " )
print (f"Solve time: { sol. solve_time:.4f} s | Iterations: { sol. iterations} " )
Status: terminated
Objective: 0.550348
Solve time: 0.0051s | Iterations: 36
Part 4: Combining Callback and Time Limit
Both mechanisms work together — whichever fires first terminates the solve.
prob.reset()
history: list [float ] = []
def record_objective(p: SolverProgress) -> None :
history.append(p.objective_value)
sol = prob.solve(method= "SLSQP" , callback= record_objective, time_limit= 0.05 )
print (f"Status: { sol. status. value} | Objective: { sol. objective_value:.6f} " )
print (f"Recorded { len (history)} objective snapshots" )
Status: optimal | Objective: 0.000000
Recorded 54 objective snapshots
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).