Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Build artifacts
build/
dist/
*.egg-info/

# Python caches
__pycache__/
*.py[cod]

# Editors / OS
.vscode/
.idea/
.DS_Store
111 changes: 111 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# AGENTS.md

Repository conventions for the `marinholab-solvers-qpoases` project — a Python
(C++ via pybind11) wrapper around [qpOASES](https://github.com/coin-or/qpOASES),
an online active-set solver for quadratic programs.

## Project layout

```
marinholab/solvers/qpoases/
__init__.py Public API re-exports (Solver, Configuration, enums)
solver.py numpy-friendly Solver wrapper
example.py runnable example (console script: qpoases_example)
example_kinematics.py OPTIONAL example (needs dqrobotics + dqrobotics-pyplot)
_core.pyi type stub for the compiled _core extension (ships in the wheel)
py.typed PEP 561 marker so stubs are picked up by type checkers
include/qpOASES_solver.h C++ header: qpOASES_Solver + Configuration (doxygen-documented)
src/core.cpp pybind11 module (_core): binds qpOASES_Solver + Configuration + enums
src/core_function.cpp C++ implementation (wraps qpOASES' QPSolver)
qpOASES/ qpOASES (git submodule)
pybind11/ pybind11 (git submodule)
setup.py PEP 517 build (CMake + pybind11)
```

## Build & install

Requires: a C++23 compiler (e.g. `g++`), CMake, Ninja, `eigen3` (dev headers),
and Python >= 3.9 with `numpy` + `setuptools`/`wheel`. On Ubuntu:
`sudo apt-get install cmake libeigen3-dev`.

```console
git submodule update --init --recursive # if cloning without submodules
pip install -e . # editable install (builds _core in build_ext)
# or produce a wheel:
python setup.py bdist_wheel
pip install dist/marinholab_solvers_qpoases-*.whl
```

The extension name is `marinholab.solvers.qpoases._core`. Builds are slow on
first run; CMake reuses the `build/` cache across builds.

## Run the example (smoke test)

```console
qpoases_example
```

(or `python -m marinholab.solvers.qpoases.example`). Exits 0 and prints one
line of optimal `x` per sub-example (positive-definite, semi-definite,
`None`-constraints, active set).

`example_kinematics.py` additionally needs the *optional* dependencies
`dqrobotics` and `dqrobotics-pyplot` (`pip install --pre dqrobotics
dqrobotics-pyplot`); it is not required for the core package to work.

## Type checking (Pyright)

```console
pyright
```

Configuration lives in `pyrightconfig.json` (`pythonVersion` 3.9,
`typeCheckingMode` basic). It must pass with **0 errors / 0 warnings**.

- The compiled extension `_core` is typed through the `_core.pyi` stub.
Keep the stub in sync with the pybind11 surface in `src/core.cpp`.
- `example_kinematics.py` depends on the untyped third-party `dqrobotics`
package; its `import` lines and matplotlib 3-D axis calls carry targeted
`# type: ignore[reportAttributeAccessIssue]` comments. Do not remove them
(they are the documented reason those lines are ignored).
- The package ships `py.typed` + `_core.pyi` in the wheel (`package_data` in
`setup.py`) so downstream projects can be checked against it.

## Conventions

- **Preserve prior solver behaviour.** The wrapper's historical defaults are
the behavioural contract. In particular:
- `Configuration.enableRegularisation` defaults to `BT_FALSE`
(qpOASES' own default is `BT_TRUE`) — do not "fix" it to `BT_TRUE` without
re-validating the semi-definite / indefinite examples.
- `Configuration.print_level` defaults to `PL_LOW` (qpOASES default is
`PL_MEDIUM`) so the solver is quiet by default.
- `Configuration.enableNZCTests` and `enableFlippingBounds` default to
`BT_FALSE` (qpOASES default is `BT_TRUE`) — the "fast"/MPC preset.
- `Solver` accepts `None` for `A`/`b`/`Aeq`/`beq` and substitutes a single
trivially-satisfied zero row; `Solver.get_active_set()` returns one entry
per combined constraint row (-1 lower / 0 inactive / +1 upper).
- **Style.** Match the existing style: no trailing-whitespace obsession,
docstrings on public API, snake_case for new `Configuration` fields that
map to qpOASES `Options` fields (keep qpOASES-native names where they are
already the established wrapper spelling: `enableFlippingBounds`,
`enableRegularisation`, `enableNZCTests`).
- **Doxygen.** C++ types and members are documented with Doxygen
(`/** ... @brief ... @see ... */` blocks). Keep that when adding fields.
- **Annotations.** All public Python API is fully type-annotated and must
remain Pyright-clean. `requires-python` is `>= 3.9`; use
`from __future__ import annotations` where PEP 604 `X | Y` is used so the
module still parses on 3.9.

## CI

`.github/workflows/python-publish.yml` builds the wheel on `ubuntu-latest` and
`ubuntu-24.04-arm` for Python 3.12 and 3.13 and publishes to PyPI. Local
builds here mirror that (aarch64, Python 3.13).

## Version

The version is `0.0.1` in `pyproject.toml`; `setup.py` computes a
date+commit-derived fallback when the git tag isn't available, which is why
locally-built wheels may show a different distribution version — this is
pre-existing and unrelated to code changes.
219 changes: 219 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,224 @@
# solver-qpoases

A [qpOASES](https://github.com/coin-or/qpOASES) wrapper for Python that ships
prebuilt binaries. It exposes qpOASES' online active-set solver through a thin,
numpy-friendly, MATLAB-`quadprog`-like interface.

```console
pip install marinholab-solvers-qpoases
```

## Overview

Given a symmetric matrix `H`, a vector `f`, and (optionally) inequality and
equality constraint matrices, the solver solves the quadratic program

```
min_x 0.5 * x' H x + f' x
s.t. A x <= b
Aeq x = beq
```

Once a problem has been solved once, subsequent solves on the same `Solver`
instance are *warm-started* by default (`Configuration.use_hotstart = True`),
which is the main performance benefit of qpOASES for repeated, related QPs.

## Quickstart

```python
import numpy as np
from marinholab.solvers import qpoases

solver = qpoases.Solver()

H = np.eye(2) # positive definite Hessian
f = np.array([-1.0, -1.0]) # linear term
A = np.array([[1.0, 0.0]]) # x[0] <= 0.2
b = np.array([0.2])

x = solver.solve_quadratic_program(H, f, A, b,
Aeq=np.zeros((1, 2)),
beq=np.zeros((1,)))
# x ≈ [0.2, 1.0]
```

### Omitting constraints

Any of the four constraint arguments (`A`, `b`, `Aeq`, `beq`) can be `None`,
meaning "no such constraint". The matrix and its right-hand side must be
omitted (or provided) together:

```python
# Unconstrained
x = solver.solve_quadratic_program(H, f, None, None, None, None)

# Equality constraints only
x = solver.solve_quadratic_program(H, f, None, None, Aeq, beq)
```

### Active set

`solver.get_active_set()` reports, for each row of the combined constraint
matrix (rows of `A` followed by rows of `Aeq`), whether it is:

* `-1` active at its **lower** bound,
* `0` inactive,
* `+1` active at its **upper** bound (equality constraints are always `+1`,
since their bounds coincide).

```python
solver.solve_quadratic_program(H, f, A, b, Aeq, beq)
solver.get_active_set() # e.g. [ 1.0, 0.0, 0.0]
```

## Configuration

All of qpOASES' `Options` fields are exposed, plus a few wrapper-specific
settings. Create a `Configuration`, tweak the fields you need, and pass it to
the solver:

```python
config = qpoases.Configuration()
config.hessian_type = qpoases.HessianType.HST_SEMIDEF # H is rank-deficient
config.enableRegularisation = qpoases.BooleanType.BT_FALSE
config.termination_tolerance = 1.0e-9 # tighter convergence
solver = qpoases.Solver(config)
```

The enum types are re-exported for convenience: `qpoases.BooleanType`,
`qpoases.HessianType`, `qpoases.PrintLevel`, and `qpoases.SubjectToStatus`.

### Wrapper-specific options

| Option | Default | Type | Description |
|---|---|---|---|
| `maximum_working_set_recalculations` | `150` | `int` | Max working-set recalculations during the initial homotopy (`nWSR` passed to `init`/`hotstart`). Increase if solves hit the maximum. |
| `use_hotstart` | `True` | `bool` | Warm-start subsequent solves with `hotstart()` instead of re-initialising with `init()`. |
| `hessian_type` | `HST_POSDEF` | `HessianType` | Definiteness assumed for `H`; given to the underlying `SQProblem`. |

### Hessian definiteness (`HessianType`)

| Value | Meaning |
|---|---|
| `HST_ZERO` | Hessian is the zero matrix (LP formulation) |
| `HST_IDENTITY` | Hessian is the identity matrix |
| `HST_POSDEF` | Hessian is (strictly) positive definite |
| `HST_POSDEF_NULLSPACE` | Positive definite on the null space of active bounds/constraints |
| `HST_SEMIDEF` | Positive semi-definite |
| `HST_INDEF` | Indefinite |
| `HST_UNKNOWN` | Unknown |

### qpOASES options

These map 1:1 onto qpOASES' `Options` fields. Defaults match qpOASES' own
defaults for a **double-precision** build (see `Options::setToDefault()`), with
two deliberate exceptions noted inline: `enableNZCTests` and
`enableFlippingBounds` default to `BT_FALSE` here (qpOASES' default is
`BT_TRUE`) because those are the recommended "fast"/MPC settings for repeated,
online solves. See the [qpOASES manual](https://www.coin-or.org/qpOASES/doc/3.0/manual.pdf)
for a full description of each option.

**Booleans** (`BooleanType`: `BT_FALSE` / `BT_TRUE`)

| Option | Default | Description |
|---|---|---|
| `enable_ramping` | `BT_TRUE` | Enables the ramping strategy. |
| `enable_far_bounds` | `BT_TRUE` | Enables the far bounds strategy. |
| `enableFlippingBounds` | `BT_FALSE` | Allows flipping active bounds between lower and upper values. *(differs from qpOASES default)* |
| `enableRegularisation` | `BT_FALSE` | Regularises `H` when (semi-)definiteness is detected. |
| `enable_full_li_tests` | `BT_FALSE` | Uses the condition-hardened linear-independence (LI) test. |
| `enableNZCTests` | `BT_FALSE` | Enables the nonzero-curvature test. *(differs from qpOASES default)* |
| `enable_equalities` | `BT_FALSE` | Treats equality constraints as always active. |
| `enable_inertia_correction` | `BT_TRUE` | Repairs the working set when negative curvature is found during a hotstart. |
| `enable_drop_infeasibles` | `BT_FALSE` | Whether infeasible constraints may be dropped. |

**Integers** (`int_t`)

| Option | Default | Description |
|---|---|---|
| `enable_drift_correction` | `1` | Frequency of drift corrections (`0` = off). |
| `enable_cholesky_refactorisation` | `0` | Frequency of full Cholesky refactorisation of the projected Hessian (`0` = rank updates only). |
| `num_regularisation_steps` | `0` | Max successive regularisation steps. |
| `num_refinement_steps` | `1` | Max iterative-refinement steps. |
| `drop_bound_priority` | `1` | Priority used when dropping bounds. |
| `drop_eq_con_priority` | `1` | Priority used when dropping equality constraints. |
| `drop_ineq_con_priority` | `1` | Priority used when dropping inequality constraints. |

**Reals** (`real_t`, `double`)

| Option | Default | Description |
|---|---|---|
| `termination_tolerance` | `5.0e6 * EPS` (~`1.1e-9`) | Relative tolerance that stops the homotopy. Smaller = more accurate, more work. |
| `bound_tolerance` | `1.0e6 * EPS` | Bound tolerance; a constraint whose bounds differ by less is treated as an equality. |
| `bound_relaxation` | `1.0e4` | Offset for relaxing bounds at the start of the initial homotopy (also the initial far-bound value). |
| `eps_num` | `-1.0e3 * EPS` | Numerator tolerance for the ratio test. |
| `eps_den` | `1.0e3 * EPS` | Denominator tolerance for the ratio test. |
| `max_primal_jump` | `1.0e8` | Max allowed primal jump in nonzero-curvature tests. |
| `max_dual_jump` | `1.0e8` | Max allowed dual jump in LI tests. |
| `initial_ramping` | `0.5` | Start value of the ramping strategy. |
| `final_ramping` | `1.0` | Final value of the ramping strategy. |
| `initial_far_bounds` | `1.0e6` | Initial size of the far bounds. |
| `grow_far_bounds` | `1.0e3` | Growth factor applied to the far bounds. |
| `eps_flipping` | `1.0e3 * EPS` | Tolerance of the squared Cholesky diagonal factor that triggers flipping a bound. |
| `eps_regularisation` | `1.0e3 * EPS` | Scaling factor of the identity matrix used for Hessian regularisation. |
| `eps_iter_ref` | `1.0e2 * EPS` | Early-termination tolerance for iterative refinement. |
| `eps_li_tests` | `1.0e5 * EPS` | Tolerance for the linear-independence tests. |
| `eps_nzc_tests` | `3.0e3 * EPS` | Tolerance for the nonzero-curvature tests. |
| `rcond_s_min` | `1.0e-14` | Min reciprocal condition number of the Schur complement before a refactorisation is triggered. |

**Status / print enums**

| Option | Default | Type | Description |
|---|---|---|---|
| `print_level` | `PL_LOW` | `PrintLevel` | Verbosity of qpOASES output (`PL_NONE`, `PL_LOW`, `PL_MEDIUM`, `PL_HIGH`, `PL_TABULAR`, `PL_DEBUG_ITER`). Defaults to `PL_LOW` so the solver stays quiet (qpOASES' own default is `PL_MEDIUM`). |
| `initial_status_bounds` | `ST_LOWER` | `SubjectToStatus` | Status assumed for all bounds at the first iteration. |

### Print levels (`PrintLevel`)

`PL_DEBUG_ITER`, `PL_TABULAR`, `PL_NONE`, `PL_LOW`, `PL_MEDIUM`, `PL_HIGH`.

### Bound/constraint statuses (`SubjectToStatus`)

`ST_LOWER`, `ST_INACTIVE`, `ST_UPPER`, `ST_INFEASIBLE_LOWER`,
`ST_INFEASIBLE_UPPER`, `ST_UNDEFINED`.

## Examples

* `marinholab/solvers/qpoases/example.py` — positive-definite and
semi-definite solves, the `None`-constraint path, and the active set. Run it
with `qpoases_example` (installed as a console script).
* `marinholab/solvers/qpoases/example_kinematics.py` — an *optional* example
showing the solver used in a hierarchical (task-priority) controller for a
kinematically redundant robot, built on [`dqrobotics`](https://pypi.org/project/dqrobotics/).
It requires the optional dependencies `dqrobotics` and `dqrobotics-pyplot`
(`pip install --pre dqrobotics dqrobotics-pyplot`).

## Building from source

The package builds a C++ extension (via CMake + pybind11) and vendors
[qpOASES](https://github.com/coin-or/qpOASES) and [pybind11](https://github.com/pybind/pybind11)
as git submodules.

```console
git clone --recurse-submodules <repo>
pip install .
```

Prerequisites: a C++23 compiler, CMake, an Eigen3 installation, and Python.
On Ubuntu: `sudo apt-get install cmake libeigen3-dev`.

## Type checking

The package ships a type stub (`marinholab/solvers/qpoases/_core.pyi`) and a
`py.typed` marker, so downstream projects can be checked with
[Pyright](https://github.com/microsoft/pyright) (or Pylance) without extra
configuration. Run the project's own check with:

```console
pyright
```

## License

The qpOASES library is LGPLv2.1; the wrapper is under the terms of the
included `LICENSE` file.
Loading
Loading