Skip to content

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, uses piqp_data_dense
  • SparseProblem/SparseSQPFunction: CSC-format sparse derivatives via spjacobian/sphessian, uses piqp_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.