CHOMP logo

CHOMP — A Modern NLP Solver

CHOMP Handles Optimization of Many Problems

A modular nonlinear programming solver that chomps through tough constrained problems using SQP, Interior-Point, and DFO-L1 Exact Penalty, plus an Auto strategy selector — powered by automatic differentiation, sparse linear algebra, and robust regularization.

Scroll

Get CHOMP

Quick Install (Python)

Editable install for development:

git clone https://github.com/lseman/chomp
cd chomp
pip install -e .

Requires: Python ≥ 3.9, NumPy, SciPy. Native extensions build automatically if your toolchain is available.

Build from Source (CMake + nanobind)

Compile C++ kernels and Python bindings:

git clone https://github.com/lseman/chomp
cd chomp
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release \
  -DAD_ENABLE_OPENMP=ON \
  -DAD_ENABLE_QDLDL=ON -DAD_ENABLE_OSQP=ON -DAD_ENABLE_PIQP=ON -DAD_ENABLE_AMDQG=ON -DAD_ENABLE_L1CORE=ON
cmake --build . -j
CMake Options
  • AD_ENABLE_OPENMP (default OFF)
  • AD_ENABLE_QDLDL, AD_ENABLE_OSQP, AD_ENABLE_PIQP, AD_ENABLE_AMDQG, AD_ENABLE_L1CORE (default ON)
Build Deps
  • nanobind ≥ 2.9, Eigen ≥ 3.4, fmt ≥ 11
  • Python dev headers (3.9+)
  • Optional: OpenMP
  • CPM.cmake auto-fetches if not found

Platform: GCC ≥ 11 / Clang ≥ 14 (Linux/macOS), MSVC 2022 (Windows). Use Release builds for speed.

Core Algorithms

1

SQP with Trust Region (Byrd–Omojokun)

At iterate \(x_k\) we solve a quadratic model with linearized constraints and a trust-region:

$$ \begin{aligned} \min_{p}\;& \tfrac12\,p^\top H_k\,p + \nabla f(x_k)^\top p \\ \text{s.t. }& c_E(x_k) + J_E(x_k)\,p = 0,\quad c_I(x_k) + J_I(x_k)\,p \le 0, \\ & \|p\|_{\mathrm{TR}} \le \Delta_k,\quad \ell \le x_k + p \le u. \end{aligned} $$

We use a Byrd–Omojokun composite step \(p = p_n + p_t\): a normal step reduces feasibility in the equality space, then a tangential step optimizes the Lagrangian in the nullspace, both within the trust region (Euclidean or ellipsoidal).

Trust-Region Geometry

  • Ellipsoidal norm (optional): \(\|p\|_{\mathrm{TR}}=\|L^\top p\|_2\) with \(M=L L^\top \approx \operatorname{sym}(H_k)+\tau I\).
  • PCG (Steihaug–Toint): preconditioned CG on the TR subproblem; dense/CSR path with Jacobi.
  • Active-set inequalities: augment equalities when \(J_I p + c_I(x_k)\) violates feasibility.
  • Box-safe scaling: fraction-to-boundary cap \(\alpha_{\max}\) keeps \(x_k+\alpha p \in [\ell,u]\).

Stabilization & Feasibility

  • dOPT stabilization: small residual shifts on \(c_E,c_I\) near active sets.
  • Criticality safeguard: when \(\|P_T\nabla f\|\) is small, shrink \(\Delta_k\) to regain model accuracy.
  • Adaptive split: \(\Delta_n=\zeta\Delta_k\) with \(\zeta\) adapted from feasibility history.
  • SOC (least-squares) correction: solve \(\min_{\Delta x}\|J_E\Delta x + c_E\|^2 + \|J_I\Delta x + (c_I+s)\|^2\), set \(\Delta s = -(c_I + s + J_I\Delta x)\); fraction-to-boundary on \(\Delta s\).
  • Restoration: feasibility-first step if acceptance fails.

Acceptance & Updates

  • ared/pred ratio: feasibility-aware \(\rho_k\) integrates with filter/funnel.
  • Curvature-aware radius: expand/shrink \(\Delta_k\) based on model agreement.
  • Line search bridge: merit/filter line search before SOC, clipped by \(\alpha_{\max}\).
  • Multipliers: recover \((\lambda,\nu)\) from a safeguarded LS system.
What’s inside the subproblem solve?

Normal step: solve \(J_E p_n \approx -c_E(x_k)\), clipped to \(\Delta_n\) (and box).

Tangential step: minimize the reduced quadratic in the nullspace \( \mathcal N(J_E)\) with Steihaug–Toint PCG; ellipsoidal metric handled by change of variables.

Active-set loop: add violated \(J_I\) rows as temporary equalities until feasible.

2

Interior-Point (Slack + Shifted Log-Barrier)

We solve a slack-augmented, optionally shifted barrier problem with variable bounds:

$$ \begin{aligned} \min_{x,s}&\;\; f(x)\;-\;\mu\!\sum_{j}\log\big(s_j+\tau\big) \\ \text{s.t. }&\; c_E(x)=0,\;\; c_I(x)+s=0, \\ &\; \ell \le x \le u \quad (\text{optional}). \end{aligned} $$

Bounds use shifted slacks \(s_L=x-\ell,\; s_U=u-x\) with duals \(z_L,z_U\) (and optional bound shift \(\beta\)). The perturbed complementarity conditions are:

$$ (s+\tau)\odot\lambda=\mu,\qquad (s_L+\beta)\odot z_L=\mu,\qquad (s_U+\beta)\odot z_U=\mu. $$

We form a regularized augmented system (HYKKT by default; LDLᵀ fallback) with diagonal barriers and solve predictor/corrector steps efficiently.

$$ W \;=\; \nabla_{xx}^2 \mathcal{L}(x,\lambda,\nu)\;+\;\Sigma_x\;+\;J_I^\top \Sigma_s J_I, \quad \begin{bmatrix} W & J_E^\top \\ J_E & 0 \end{bmatrix} \begin{bmatrix} \Delta x \\ \Delta \nu \end{bmatrix} = \begin{bmatrix} -r_d \\ -r_{p,E} \end{bmatrix}, \ \ \Delta s = -\big(c_I + J_I\,\Delta x\big). $$

Algorithmic Highlights

  • Mehrotra Predictor–Corrector: affine predictor, adaptive centering, corrector.
  • Fraction-to-Boundary: separate caps for \(s,\lambda,z_L,z_U\) to keep positivity.
  • Shifted Barrier: automatic \(\tau,\beta\) adaptation to avoid tiny slacks.
  • Gondzio Multi-Corrector (opt): extra RHS sweeps when affine step is short.
  • Higher-Order Correction (opt): capped quadratic correction on \(J_I\).
  • Tiny TR on \(\Delta x\): prevent aggressive steps with marginal conditioning.
  • KKT Solves: HYKKT with reuse & CG refinement; LDLᵀ fallback; Ruiz equilibration.
Stopping & μ-update

Stop when \(\|r_d\|_\infty,\|c_E\|_\infty,\|c_I\|_\infty\) and complementarity residuals are below tolerance and \(\mu\) is small.

\(\mu\) decreases via a safeguarded schedule based on predictor quality, feasibility, complementarity, and Hessian conditioning.

3

AUTO Mode

Chooses SQP vs IP vs DFO based on structure (active inequalities, conditioning, progress). Warm-starts duals/slacks, reuses KKT factors when possible, and flips modes if a funnel/filter rejects steps repeatedly.

  • Switch-to-IP when infeasibility dominates or bound activity explodes.
  • Switch-to-SQP when curvature is well-behaved and feasibility is near.
  • Switch-to-DFO when derivatives are unreliable or noisy (small-n, black-box regimes).

Derivative-Free L1 Exact Penalty (Trust-Region)

Problem & Idea

We solve constrained problems without derivatives:

$$ \min_{x\in\mathbb{R}^n} f(x) \quad \text{s.t.}\quad c_i(x)\le 0,\; i=1,\dots,m. $$

Use an exact penalty with parameter $\mu\!>\!0$:

$$ p(x) \;=\; f(x) \;+\; \mu \sum_{i=1}^m \max\{0,\,c_i(x)\}. $$

Under suitable conditions and sufficiently large $\mu$, minimizing $p(x)$ yields the constrained optimum directly. CHOMP builds local polynomial models of $f$ and $c_i$ in a trust-region, handles non-smoothness near active constraints, and accepts infeasible iterates when beneficial.

Nearly-active vs. clearly violated constraints. Constraints with $|c_i(x)|\le \varepsilon$ are treated specially to avoid zig-zagging near the boundary; others contribute smoothly via model $p_1$.

Step Strategy (Sketch)

  1. Direction: Solve a small LP to get a “steepest” descent direction for the model of $p$ that respects nearly-active gradients.
  2. 1-D Search with Breakpoints: Line search along the direction across piecewise-quadratic segments.
  3. Project Nearly-Active to Zero: Small LS correction that drives nearly-active constraints closer to $0$.
  4. Trust-Region & Acceptance: Compare predicted vs. actual reduction; adjust radius and refine models.
When to use: Black-box objectives/constraints, limited $n$ (e.g., $n\le 50$), noisy derivatives, or expensive simulations.

Quick Start

Classic API

import numpy as np
from chomp import NLPSolver, SQPConfig

# Rosenbrock + circle inequality
f = lambda x: (1-x[0])**2 + 100*(x[1]-x[0]**2)**2
c_ineq = [lambda x: x[0]**2 + x[1]**2 - 25]

x0 = np.array([3.0, 2.0])
solver = NLPSolver(f, c_ineq=c_ineq, x0=x0, config=SQPConfig())
x_opt, lam, info = solver.solve(mode="auto", max_iter=200, tol=1e-8, verbose=True)
print(x_opt, info.status)

Wrapper API (Symbolic)

Build with operator-overloaded expressions; AD-aware trigs and reductions.

import math
import numpy as np
from nlp.wrapper import Model, cos   # AD-aware functions
from chomp import NLPSolver, SQPConfig

# Branin with circular inequality
m = Model("branin")
x = m.add_var("x", shape=2)          # decision vars
x1, x2 = x[0], x[1]

a, b, c, r, s, t = 1.0, 5.1/(4*math.pi**2), 5.0/math.pi, 6.0, 10.0, 1/(8*math.pi)
f = a*(x2 - b*(x1**2) + c*x1 - r)**2 + s*(1 - t)*cos(x1) + s
m.minimize(f)
m.add_constr(x1**2 + x2**2 <= 60.0)

# Compile to callables and solve with SQP
f_fun, cI, cE, x0 = m.build()
solver = NLPSolver(f_fun, c_ineq=cI, c_eq=cE, x0=np.array([-3.0, 12.0]),
                   config=SQPConfig())
x_star, info = solver.solve(mode="auto", max_iter=150, tol=1e-8, verbose=True)
print("x* =", x_star, info.status)
STEP 1

Choose mode

mode="sqp", "ip", "dfo", or "auto".

STEP 2

Configure

Trust-region / filter options, barrier parameters, tolerances.

STEP 3

Inspect

Use info/history for residuals, step types, and timings.

API Cheatsheet

Key Classes

  • NLPSolver(f, c_ineq, c_eq, x0, config)
  • SQPStepper, InteriorPointStepper, DFOStepper
  • Regularizer (inertia-aware spectral fixes)
  • KKT (assembly/factorization, Schur)

Configs

  • SQPConfig(tr_radius, filter, ls_armijo, dogleg, ...)
  • IPConfig(mu0, sigma, ftb, corrector, ...)
  • DFOConfig(tr_radius, mu, eps_active, ...)

Wrapper (Symbolic)

  • Model.add_var(name, shape, lb, ub, x0)
  • Model.add_constr(lhs <= rhs), lhs == rhs
  • Model.minimize(expr) / maximize(expr)
  • Model.build() → (f, c_ineq, c_eq, x0)

Worked Examples

Quadratic with Linear Constraints (SQP)

Filter globalization + trust region with active-set changes.

import numpy as np
from chomp import NLPSolver
from nlp.blocks.aux import SQPConfig

Q = np.diag([1.0, 10.0]); q = np.array([-1.0, -2.0])
A = np.array([[1.0, 1.0]]); b = np.array([1.0])  # equality: x1+x2=1

f = lambda x: 0.5*x@Q@x + q@x
c_eq = [lambda x: A@x - b]

solver = NLPSolver(f, c_eq=c_eq, x0=np.zeros(2), config=SQPConfig())
x, lam, info = solver.solve(mode="sqp", verbose=True)
print(x, info.status)

Inequalities via Slacks (IP)

Log-barrier with Mehrotra predictor–corrector and fraction-to-boundary.

from chomp import NLPSolver, IPConfig

f = lambda x: (x[0]-1)**2 + (x[1]+2)**2
c_ineq = [lambda x: x[0] + x[1] - 1]  # <= 0

solver = NLPSolver(f, c_ineq=c_ineq, x0=[3.0, -1.0], config=IPConfig())
x, lam, info = solver.solve(mode="ip", verbose=True)
print(x, info.status)