herculesabqp 0.1.2

A convex box-constrained quadratic programming solver with warm starts and active-set polishing.
Documentation
# HerculesABQP

HerculesABQP is a solver for convex box-constrained quadratic programs of the form

```text
minimize    1/2 x^T Q x + c^T x
subject to  l <= x <= u
```

It is especially useful when the same quadratic model is solved many times with changing bounds and warm starts.

## How It Works

The solver does two things:

1. It takes accelerated projected gradient steps on the box-constrained QP, with adaptive restart and optional diagonal scaling.
2. Once the first-order phase is close, (optionally) it applies a small number of active-set polishing steps in the style of ProxQP to clean up the final solution. This is based on the main iteration idea from proxqp.

## Features

- accelerated projected gradient descent with adaptive gradient restart
- Gershgorin or auto Lipschitz estimation
- optional Hessian-diagonal scaling
- active-set polishing with reduced solves
- dense and sparse matrix support
- warm starts and a `PreparedSolver` path for repeated solves
- explicit and implicit operator interfaces
- benchmarks and validation tests

## Quick Start

Run the Criterion benchmarks:

```bash
cargo bench --bench solver_bench
```

Build the Python extension in a virtual environment:

```bash
maturin develop --manifest-path python/Cargo.toml
```

Install Python test dependencies and run the binding tests:

```bash
python -m pip install "./python[test]"
python -m pytest tests/test_python_bindings.py -q
```

## Example Use

Solve one box-constrained QP directly:

```rust
use herculesabqp::matrix::QuadraticMatrix;
use herculesabqp::solver::{solve_box_qp, SolverOptions};
use ndarray::arr2;

fn main() {
    let q = QuadraticMatrix::dense(arr2(&[
        [4.0, 1.0],
        [1.0, 2.0],
    ]));
    let c = vec![-1.0, -0.5];
    let lb = vec![0.0, 0.0];
    let ub = vec![1.0, 1.0];
    let options = SolverOptions::default();

    let result = solve_box_qp(&q, &c, &lb, &ub, &options)
        .expect("failed to solve box-constrained QP");
    println!("x = {:?}", result.x);
    println!("objective = {}", result.objective);
}
```

Prepare a solver once and reuse it for repeated solves with changing bounds:

```rust
use herculesabqp::matrix::QuadraticMatrix;
use herculesabqp::solver::{PreparedSolver, SolverOptions};
use ndarray::arr2;

fn main() {
    let q = QuadraticMatrix::dense(arr2(&[
        [4.0, 1.0],
        [1.0, 2.0],
    ]));
    let c = vec![-1.0, -0.5];
    let options = SolverOptions::default();

    let prepared = PreparedSolver::new(&q, &c, &options)
        .expect("failed to prepare reusable solver state");

    let root = prepared
        .solve_subproblem(&[0.0, 0.0], &[1.0, 1.0], None)
        .expect("failed to solve root QP");

    let child = prepared
        .solve_subproblem(&[1.0, 0.0], &[1.0, 1.0], Some(&root.x))
        .expect("failed to solve child QP");

    println!("root x = {:?}", root.x);
    println!("child x = {:?}", child.x);
}
```

`PreparedSolver` is the right entry point when `Q` and `c` stay fixed across many solves. It prepares the matrix-side work once, then lets later solves vary only in the bounds and optional warm start.

## Python Interface

The optional Python bindings support:

- dense `numpy.ndarray` inputs for `Q`
- sparse SciPy matrices or arrays for `Q` through their `tocsr()` interface
- dense NumPy vectors for `c`, `lb`, `ub`, and optional `x0`
- Python-defined implicit operators with `n` and `matvec(x)`

The main Python entry points are:

- `herculesabqp.solve_box_qp(...)` for one-off solves
- `herculesabqp.PreparedSolver(...)` for repeated solves with shared `Q` and `c`
- `herculesabqp.solve_box_qp_implicit(...)` for Python-defined implicit operators
- `herculesabqp.PreparedImplicitSolver(...)` for repeated implicit solves

The Python extension lives in the thin wrapper crate under [python/](python/), so
normal Rust dependencies only build the Rust library API and do not trigger a
`cdylib` link step.

## Notes

- The solver assumes a convex QP.
- By default the solver symmetrizes the supplied matrix. Set `options.assume_symmetric = true` if `Q` is already symmetric and you want to skip that preprocessing step.
- For repeated solves, prefer the prepared solver APIs.
- For implicit operators, diagonal scaling is only available when the operator exposes `diagonal()`. If `scaling="hessian_diag"` is requested without a diagonal, the implicit path falls back to an unscaled solve.
- The implicit path is first-order only and currently skips the final polishing phase.
- Additional examples live in [examples/]examples/.
- Core references are collected in [references.bib]references.bib.