Interpreting results
solve returns a frozen Result. This
page explains every field and how to check that a solve actually converged.
res = ipax.solve(problem, x0)
if res.success:
print(res.x, res.objective)
else:
print("did not converge:", res.status, res.message)
Did it converge?
Check res.success first — it is True only for
Status.OPTIMAL and the explicitly enabled
Status.ACCEPTABLE. The status enum then tells you which
stopping condition fired, and message carries a short human-readable note.
from ipax import Status
match res.status:
case Status.OPTIMAL | Status.ACCEPTABLE:
... # res.x is usable
case Status.MAX_ITER | Status.MAX_TIME:
... # ran out of budget; inspect res.kkt_error
case _:
... # infeasible / numerical trouble / stopped
Status
| Status | success |
Meaning |
|---|---|---|
OPTIMAL |
✅ | scaled-KKT optimality conditions met in one iteration |
ACCEPTABLE |
✅ | acceptable conditions held for the configured consecutive iterations |
INFEASIBLE |
❌ | restoration/feasibility detection concluded the problem is infeasible (or bounds with x_L > x_U) |
UNBOUNDED |
❌ | objective appears unbounded below |
MAX_ITER |
❌ | hit Options.max_iter |
MAX_TIME |
❌ | hit Options.max_time |
RESTORATION_FAILED |
❌ | feasibility restoration could not make progress |
NUMERICAL_ERROR |
❌ | unrecoverable numerical failure in a step |
STOPPED |
❌ | a user callback requested a stop |
A non-success status is not always "wrong": MAX_ITER with a small
kkt_error often means the point is fine but the tolerance was strict. Always
look at the residuals before discarding a result.
The solution and multipliers
| Field | Meaning |
|---|---|
x |
the primal solution, in the problem's backend/namespace |
objective |
f(x) at the solution, in the original problem's units |
y_eq |
equality multipliers (free sign), or None |
y_ineq |
inequality multipliers ≥ 0, or None |
z_lower, z_upper |
bound multipliers ≥ 0 (lower/upper), or None |
A multiplier block is None when the corresponding constraints don't exist.
The Lagrangian sign convention matches the standard form c(x)=0, g(x)≤0,
x_L≤x≤x_U:
Inequality and bound multipliers are non-negative; a (near-)zero entry means that constraint is inactive, a strictly positive entry means it is active and gives its shadow price. With scaling enabled the multipliers are still reported in original units.
active = res.y_ineq > 1e-6 # which inequalities bind at the solution
Convergence diagnostics
| Field | Meaning |
|---|---|
kkt_error |
aggregate scaled KKT ∞-norm that drove termination |
dual_infeasibility |
scaled stationarity residual component |
primal_infeasibility |
scaled constraint-violation component |
complementarity |
scaled complementarity component |
constraint_violation |
scaled ‖(c, g+s)‖ feasibility measure |
kkt_error is the maximum of the three component residuals; comparing the three
tells you which condition is limiting. A solve that stalls at a large
dual_infeasibility but tiny primal_infeasibility, for example, is exactly the
case the acceptable termination mode is designed for.
These remain scaled-space quantities when problem scaling is on.
Performance and provenance
| Field | Meaning |
|---|---|
n_iter |
outer interior-point iterations taken |
solve_time |
total wall-clock seconds for the whole solve call |
linear_solver |
the solver route actually used, e.g. "dense", "krylov (cg, pc=jacobi)", "sparse [Feral LDL^T (CPU)]" |
derivative_sources |
how each derivative was obtained (see below) |
derivative_sources is worth a
look on a first run — it confirms whether your analytic derivatives were picked
up or whether the solver fell back to autodiff/finite-difference:
print(res.derivative_sources)
# DerivativeSources(gradient='analytic', eq_jacobian='n/a',
# ineq_jacobian='finite-diff', hessian='lbfgs')
Iteration history
res.history is a tuple of
IterationRecords, one per
iteration, holding the objective, mu, theta (constraint violation), the KKT
error and its components, step lengths, applied regularization, and per-row
timing splits. Use it to plot convergence or diagnose a slow solve:
import matplotlib.pyplot as plt
plt.semilogy([r.kkt_error for r in res.history])
To react during the solve instead of after, pass a callback — see Monitoring & diagnostics.