Array-API-conformant primal–dual interior-point solver for large-scale nonlinear constrained optimization.
Project description
ipax
Interior-Point on the Array API — a pure-Python, Python Array API–conformant primal–dual interior-point solver for large-scale nonlinear constrained optimization, tuned for the scale and structure of radiotherapy (RT) treatment planning.
ipax solves
min f(x)
s.t. c(x) = 0 (equalities)
g(x) ≤ 0 (inequalities; two-sided b_l ≤ ĝ(x) ≤ b_u supported)
x_L ≤ x ≤ x_U (bounds, possibly ±∞)
with a log-barrier interior-point method, an L-BFGS (Powell-damped) Lagrangian Hessian by default, and an IPOPT-style filter line search. It runs unchanged on NumPy, PyTorch, JAX, and CuPy (CPU/GPU) because the core never imports a concrete array library — it infers the namespace from the arrays a problem returns.
Design draws on Wächter & Biegler (2006) (IPOPT) for globalization and on Breedveld et al. (2017) for the RT-tuned primal–dual formulation.
The solver surface is complete: equality/inequality/bound constraints, the
dense / matrix-free / sparse-direct solve routes, filter line search with
restoration, and multi-backend support. ipax is beta — the
algorithm is feature-complete and tested, and the road to a stable 1.0.0 is
about hardening the public API, docs, and verification rather than new features
(see Status and roadmap). We are looking for adopters
to try it on real workloads.
For more information check the Documentation.
Install
# core + a backend (pick what you have)
pip install -e ".[numpy]"
pip install -e ".[torch]"
# full dev environment (NumPy + Torch + strict + sparse + tooling)
pip install -e ".[dev]"
CUDA is user-managed so that the CuPy wheel matches the installed toolkit.
Install the appropriate CuPy package and cuDSS runtime yourself; the
sparse-cuda extra installs only the nvmath bindings. Without cuDSS, the CuPy
adapter falls back to cupyx.scipy.sparse.linalg.spsolve and cannot report
inertia.
Quick start
import numpy as np
import ipax
Q = np.asarray([[4.0, 1.0], [1.0, 3.0]], dtype=np.float64)
c = np.asarray([-1.0, -2.0], dtype=np.float64)
problem = ipax.QuadraticProblem(Q, c)
res = ipax.solve(
problem,
x0=np.asarray([0.0, 0.0], dtype=np.float64),
options=ipax.Options(hessian="exact", linsolve="dense"),
)
print(res.status, res.x, res.objective)
Enable gradient-based problem scaling with a short Options value:
res = ipax.solve(problem, x0, options=ipax.Options(scaling="gradient-based"))
For a custom threshold, use
ipax.Options(scaling=ipax.ScalingOptions(method="gradient-based", max_gradient=50.0)).
Termination comes in two families that share the same conditions: the objective
magnitude f_tol (|f| ≤ f_tol), the relative objective change
f_rel_change_tol, and the scaled KKT components dual_inf_tol,
constr_viol_tol, compl_inf_tol (None disables one). Optimality is
single-iteration and reports Status.OPTIMAL — the defaults are the classic
scaled-KKT test (each component ≤ 1e-8):
options = ipax.Options(
optimality=ipax.OptimalityConditionOptions(
dual_inf_tol=1e-4, compl_inf_tol=1e-4, # loose optimality, but…
constr_viol_tol=1e-8, # …tight feasibility, in one step
)
)
Acceptable is multi-iteration and reports Status.ACCEPTABLE (also a
success): the conditions must hold for n_iter consecutive iterations. It is
off by default and useful when a dual-infeasibility-dominated residual plateaus
but the objective and primal feasibility have settled:
options = ipax.Options(
acceptable=ipax.AcceptableStoppingOptions(
dual_inf_tol=1.0, # tolerate the stuck dual infeasibility
constr_viol_tol=1e-6,
f_tol=1e-7, # relative objective change
n_iter=5,
)
)
The top-level max_iter and max_time (seconds) limits report Status.MAX_ITER
and Status.MAX_TIME.
The backend is whatever x0 is: pass a torch.Tensor and the whole solve runs
in PyTorch. Derivatives you don't supply are filled by autodiff (if the backend
supports it) or finite differences; the Hessian defaults to L-BFGS.
See examples/ for short, runnable examples.
Architecture at a glance
ipax/
problem/ Problem ABC + derivative resolution (analytic → autodiff → finite-diff → L-BFGS)
backend/ namespace resolution, LinearOperator hierarchy, per-backend sparse adapters
linalg/ LinearSolver protocol: dense / matrix-free Krylov / sparse-direct
ipm/ barrier, KKT assembly, filter line search, restoration, L-BFGS, driver
testing/ analytic oracle problems shared by tests and benchmarks
tests/ contract batteries + unit/property/integration/backends/regression
examples/ minimal runnable examples for the current implementation surface
benchmarks/ RT-like generators, corpus, asv harness (separate from tests)
docs/ MkDocs site
The two protocols LinearOperator and LinearSolver are the architectural
core: every Jacobian/Hessian is an operator and every KKT solve goes through a
pluggable solver, so the same IPM loop runs dense, matrix-free, or sparse.
Non-negotiable invariants
- No concrete array library in the core — only in
backend/sparse/andproblem/autodiff/adapters. - Stay inside the Array API standard (main namespace + optional
linalg). - Linear algebra is injected, never hard-wired into the IPM loop.
- Sparsity is an adapter concern — the core emits index/value vectors only.
- No global mutable state.
These are enforced in CI (scripts/check_purity.py + array-api-strict).
Status and roadmap
ipax is beta. The solver surface is complete and tested across
NumPy and PyTorch, but the public API has not yet been hardened against real-world
use — expect refinements across the 0.x series before it is frozen at 1.0.0.
We are looking for adopters. If you have a large-scale constrained NLP —
radiotherapy planning or otherwise — and would like to try ipax, please
open an issue. Early feedback on the
Problem / solve API directly shapes the road to 1.0.0.
Road to 1.0.0
1.0.0 is the point at which the public API carries a semver stability promise.
The day-to-day work stays maintenance and measurement (below), and the release
is gated on these exit criteria:
- API stability. The public surface —
solve,Problem,Options,Result, and theLinearOperator/LinearSolverprotocols — holds stable across the0.xseries, with any deprecations carried for at least one minor release. - Published documentation. The MkDocs site (API reference + guides) is built and published.
- GPU & sparse-direct verification. The CuPy/cuDSS and Torch/JAX-CUDA sparse routes and the MUMPS inertia path are exercised on real hardware in CI, or documented as provisional until GPU CI exists.
- Adopter validation. At least one external workload runs on
ipax, with the feedback folded back into the API. - Tracked baselines.
asvperformance baselines and the QC accuracy sweep are tracked across releases, with cross-checks vs IPOPT / SciPy / OSQP green.
Near-term work is maintenance and measurement rather than new features:
- Bug-fixing — correctness on hard nonconvex / ill-conditioned problems; every fix ships with a regression test.
- Performance — drive the
benchmarks/scaling and micro suites (asv); reduce per-iteration cost and memory across the dense / matrix-free / sparse routes. - Accuracy benchmarking — grow the QC sweep and reference cross-checks (vs IPOPT / SciPy / OSQP); track scaled-KKT accuracy and robustness over time.
New algorithmic features beyond the current scope are out until discussed. See
CONTRIBUTING.md and AGENTS.md.
Development
pip install -e ".[dev]"
python scripts/check.py # one command: format, lint, types, purity, multi-backend tests
python scripts/check.py --fast # skip the slow test gate
python scripts/check.py --list # show the gates and the raw commands they wrap
scripts/check.py is the single verification entrypoint and runs exactly what CI
runs. The individual tools (pytest, ruff, mypy, scripts/check_purity.py,
asv) can still be invoked directly — --list prints them.
Agent-assisted development
This repo is set up for agentic coding. The import-purity boundary
(scripts/check_purity.py) is enforced both by a pre-commit hook and, for
Claude Code, by a PostToolUse hook that fires the
moment an edit touches ipax/. Checked-in Claude Code config in .claude/ adds an
invariant-auditor review subagent (diffs vs. the five invariants, math
conventions, and GPU/device-efficiency rules) plus /verify and /tdd commands.
See CONTRIBUTING.md for the short contributor guide and
AGENTS.md for the canonical, detailed version.
References
- Wächter & Biegler (2006), On the implementation of an interior-point filter line-search algorithm for large-scale nonlinear programming, Math. Prog. 106(1):25–57. (IPOPT)
- Breedveld, van den Berg, Heijmen (2017), An interior-point implementation developed and tuned for radiation therapy treatment planning, COAP 68:209–242.
- Friedlander & Orban (2012), A primal–dual regularized interior-point method for convex quadratic programs, Math. Prog. Comp. 4:71–107
License
Apache-2.0 — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ipax-0.2.0.tar.gz.
File metadata
- Download URL: ipax-0.2.0.tar.gz
- Upload date:
- Size: 200.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6fe402fe3496b56b883da0f59cdc21f7b44a3734289fb32d220531ac290edd62
|
|
| MD5 |
a0f9d56774b18e8be218b7d5260b6e9d
|
|
| BLAKE2b-256 |
06b291214b5ad6efab0aa18c030aace6c46a41f9771d99a1751e55db727f25ca
|
Provenance
The following attestation bundles were made for ipax-0.2.0.tar.gz:
Publisher:
release.yml on wahln/ipax
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ipax-0.2.0.tar.gz -
Subject digest:
6fe402fe3496b56b883da0f59cdc21f7b44a3734289fb32d220531ac290edd62 - Sigstore transparency entry: 1897885586
- Sigstore integration time:
-
Permalink:
wahln/ipax@d22637f819499461a8a0fcd33612d5067bffe084 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/wahln
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d22637f819499461a8a0fcd33612d5067bffe084 -
Trigger Event:
push
-
Statement type:
File details
Details for the file ipax-0.2.0-py3-none-any.whl.
File metadata
- Download URL: ipax-0.2.0-py3-none-any.whl
- Upload date:
- Size: 138.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
32c26b9c07f4aae0f06fb56297895e3abd74840ec43bff99d66bb61763e39207
|
|
| MD5 |
ef61f21394f2a0c44bb3dc4f59c1cdc2
|
|
| BLAKE2b-256 |
7bd20a2d9974f68616df65708326700df2fbc45ff5cc849612ee71e9c6ec9b62
|
Provenance
The following attestation bundles were made for ipax-0.2.0-py3-none-any.whl:
Publisher:
release.yml on wahln/ipax
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ipax-0.2.0-py3-none-any.whl -
Subject digest:
32c26b9c07f4aae0f06fb56297895e3abd74840ec43bff99d66bb61763e39207 - Sigstore transparency entry: 1897885691
- Sigstore integration time:
-
Permalink:
wahln/ipax@d22637f819499461a8a0fcd33612d5067bffe084 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/wahln
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d22637f819499461a8a0fcd33612d5067bffe084 -
Trigger Event:
push
-
Statement type: