SQP internals¶
The DenseSQPFunction and SparseSQPFunction classes wrap a Problem/SparseProblem and SQPSettings. They generate:
- All auxiliary NumericalFunctions (primal/dual updates, constraint violations, etc.)
- piqp QP solver integration code
- A unified Ws workspace struct containing all intermediate buffers
Sparse vs Dense¶
DenseProblem/DenseSQPFunction: Dense Jacobians/Hessians, usespiqp_data_denseSparseProblem/SparseSQPFunction: CSC-format sparse derivatives viaspjacobian/sphessian, usespiqp_data_sparse
The sparse Lagrangian Hessian stores only upper-triangular values (PIQP requirement).
SQP algorithm¶
All problem functions take a shared runtime parameter vector \(p \in \mathbb{R}^{n_p}\) as trailing input. All derivatives are taken with respect to \(x\) only; \(p\) is fixed data.
The SQP algorithm is a primal-dual algorithm that sets up: - dual variables \(\lambda_\mathrm{eq}\in\mathbb{R}^q, \lambda_\mathrm{ineq}\in\mathbb{R}^m, \lambda_\mathrm{box}\in\mathbb{R}^n\) - the Lagrangian \(\mathcal{L}(x, \lambda_\mathrm{eq}, \lambda_\mathrm{ineq}, \lambda_\mathrm{box}; p) = f(x, p) + \lambda_\mathrm{eq}^\top g_\mathrm{eq}(x, p) + \lambda_\mathrm{ineq}^\top g_\mathrm{ineq}(x, p) + \lambda_\mathrm{box}^\top (x - x_\mathrm{lb})\) - the gradient of the Lagrangian \(\nabla_x \mathcal{L} = \nabla_x f(x, p) + D_x g_\mathrm{eq}(x, p)^\top \lambda_\mathrm{eq} + D_x g_\mathrm{ineq}(x, p)^\top \lambda_\mathrm{ineq} + \lambda_\mathrm{box}\) - the Hessian of the Lagrangian \(\nabla^2_{x,x} \mathcal{L} = \nabla^2_{x,x} f(x, p) + \sum_{i=1}^q \nabla^2_{x,x} g_{\mathrm{eq},i}(x, p) \lambda_{\mathrm{eq}, i} + \sum_{i=1}^m \nabla^2_{x,x} g_{\mathrm{ineq},i}(x, p) \lambda_{\mathrm{ineq}, i}\)
Note: For box constraints and general inequality constraints, we use the same dual variable for both lower and upper bounds and rely on the sign of the multiplier to indicate which bound is active.
The SQP loop: 1. Linearize around the current iterate: compute \(f(x_k), \nabla f(x_k), g_\mathrm{eq}(x_k), Dg_\mathrm{eq}(x_k), g_\mathrm{ineq}(x_k), Dg_\mathrm{ineq}(x_k)\) 2. Check termination conditions: - stationarity: \(\|\nabla_x \mathcal{L}(x_k)\|_\infty < \epsilon_\mathrm{stat}\) - constraint violation: \(\max\{0, g_\mathrm{lb} - g_\mathrm{ineq}(x_k), g_\mathrm{ineq}(x_k) - g_\mathrm{ub}, |g_\mathrm{eq}(x_k)|, x_\mathrm{lb} - x_k, x_k - x_\mathrm{ub}\} < \epsilon_\mathrm{cons}\) - complementarity: bounded complementarity slackness violation 3. Compute the Lagrangian Hessian 4. Solve the QP subproblem to obtain primal step \(d_k\) and dual solutions 5. Line search for step size \(\alpha_k\) 6. Update iterates: \(x_{k+1} = x_k + \alpha_k d_k\), similar for duals
Current limitations: - Line search: full-step only (\(\alpha_k = 1\)) - No Hessian regularization - No elastic mode
JIT compilation¶
DenseSQPFunction.__call__ and SparseSQPFunction.__call__ dispatch to a JIT-compiled native SQP loop. The full SQP iteration (evaluation, violation checks, Hessian computation, QP solve via PIQP, primal/dual updates) runs in C++ with zero Python overhead per iteration.
The SQP shared library links against libpiqpc with -Wl,-rpath for runtime discovery. Settings are passed as flat scalars (not a struct) to avoid ctypes layout fragility. Results are extracted via accessor functions (get_x, get_lam_bounds, etc.) that return pointers into the workspace struct, which are then copied to numpy arrays.
SparseSQPFunction inherits all JIT machinery from DenseSQPFunction — the templates handle fn.is_sparse conditionally.
See CPU JIT compilation for the full JIT architecture.