pyqpmad 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyqpmad-1.4.0/.github/workflows/wheels.yml +60 -0
- pyqpmad-1.4.0/.gitignore +19 -0
- pyqpmad-1.4.0/CMakeLists.txt +63 -0
- pyqpmad-1.4.0/PKG-INFO +112 -0
- pyqpmad-1.4.0/README.md +101 -0
- pyqpmad-1.4.0/pyproject.toml +26 -0
- pyqpmad-1.4.0/qpmad_pywrap.cpp +65 -0
- pyqpmad-1.4.0/test_qpmad.py +101 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
name: Wheel and sdist build
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
tags:
|
|
8
|
+
- 'v*'
|
|
9
|
+
pull_request:
|
|
10
|
+
branches:
|
|
11
|
+
- main
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
build_wheels:
|
|
15
|
+
name: Build wheels on ${{ matrix.os }}
|
|
16
|
+
runs-on: ${{ matrix.os }}
|
|
17
|
+
strategy:
|
|
18
|
+
fail-fast: false
|
|
19
|
+
matrix:
|
|
20
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
24
|
+
|
|
25
|
+
- name: Build wheels
|
|
26
|
+
uses: pypa/cibuildwheel@ee02a1537ce3071a004a6b08c41e72f0fdc42d9a # v3.4.0
|
|
27
|
+
|
|
28
|
+
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
|
29
|
+
with:
|
|
30
|
+
name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
|
|
31
|
+
path: ./wheelhouse/*.whl
|
|
32
|
+
|
|
33
|
+
build_sdist:
|
|
34
|
+
name: Build source distribution
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
38
|
+
|
|
39
|
+
- name: Build sdist
|
|
40
|
+
run: pipx run build --sdist
|
|
41
|
+
|
|
42
|
+
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
|
43
|
+
with:
|
|
44
|
+
name: cibw-sdist
|
|
45
|
+
path: dist/*.tar.gz
|
|
46
|
+
|
|
47
|
+
upload_pypi:
|
|
48
|
+
needs: [build_wheels, build_sdist]
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
|
51
|
+
permissions:
|
|
52
|
+
id-token: write
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
|
55
|
+
with:
|
|
56
|
+
pattern: cibw-*
|
|
57
|
+
path: dist
|
|
58
|
+
merge-multiple: true
|
|
59
|
+
|
|
60
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
pyqpmad-1.4.0/.gitignore
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.22)
|
|
2
|
+
project(pyqpmad)
|
|
3
|
+
|
|
4
|
+
find_package(Python 3.10 REQUIRED COMPONENTS Interpreter Development.Module)
|
|
5
|
+
|
|
6
|
+
include(FetchContent)
|
|
7
|
+
FetchContent_Declare(
|
|
8
|
+
nanobind
|
|
9
|
+
GIT_REPOSITORY https://github.com/wjakob/nanobind.git
|
|
10
|
+
GIT_TAG v2.12.0
|
|
11
|
+
)
|
|
12
|
+
FetchContent_MakeAvailable(nanobind)
|
|
13
|
+
|
|
14
|
+
FetchContent_Declare(
|
|
15
|
+
eigen
|
|
16
|
+
GIT_REPOSITORY https://github.com/eigen-mirror/eigen.git
|
|
17
|
+
GIT_TAG 5.0.1
|
|
18
|
+
)
|
|
19
|
+
FetchContent_MakeAvailable(eigen)
|
|
20
|
+
|
|
21
|
+
FetchContent_Declare(
|
|
22
|
+
qpmad
|
|
23
|
+
GIT_REPOSITORY https://github.com/asherikov/qpmad.git
|
|
24
|
+
GIT_TAG 1.4.0
|
|
25
|
+
SOURCE_SUBDIR
|
|
26
|
+
download_only
|
|
27
|
+
)
|
|
28
|
+
FetchContent_MakeAvailable(qpmad)
|
|
29
|
+
file(
|
|
30
|
+
GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated/include/qpmad/config.h
|
|
31
|
+
CONTENT
|
|
32
|
+
"#pragma once
|
|
33
|
+
// #define QPMAD_ENABLE_TRACING
|
|
34
|
+
// #define QPMAD_USE_HOUSEHOLDER
|
|
35
|
+
// #define QPMAD_PEDANTIC_LICENSE
|
|
36
|
+
"
|
|
37
|
+
)
|
|
38
|
+
add_library(qpmad INTERFACE IMPORTED)
|
|
39
|
+
target_include_directories(
|
|
40
|
+
qpmad
|
|
41
|
+
INTERFACE
|
|
42
|
+
${qpmad_SOURCE_DIR}/include
|
|
43
|
+
${CMAKE_CURRENT_BINARY_DIR}/generated/include
|
|
44
|
+
${CMAKE_CURRENT_BINARY_DIR}/generated/include/qpmad
|
|
45
|
+
)
|
|
46
|
+
target_link_libraries(qpmad INTERFACE Eigen3::Eigen)
|
|
47
|
+
|
|
48
|
+
nanobind_add_module(pyqpmad
|
|
49
|
+
NB_STATIC STABLE_ABI LTO
|
|
50
|
+
qpmad_pywrap.cpp
|
|
51
|
+
)
|
|
52
|
+
nanobind_add_stub(
|
|
53
|
+
pyqpmad_stub
|
|
54
|
+
MODULE pyqpmad
|
|
55
|
+
OUTPUT pyqpmad.pyi
|
|
56
|
+
PYTHON_PATH $<TARGET_FILE_DIR:pyqpmad>
|
|
57
|
+
DEPENDS pyqpmad
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
target_link_libraries(pyqpmad PRIVATE qpmad)
|
|
61
|
+
|
|
62
|
+
install(TARGETS pyqpmad DESTINATION .)
|
|
63
|
+
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/pyqpmad.pyi DESTINATION .)
|
pyqpmad-1.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: pyqpmad
|
|
3
|
+
Version: 1.4.0
|
|
4
|
+
Summary: Python wrapper for qpmad
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Provides-Extra: test
|
|
7
|
+
Requires-Dist: pytest; extra == "test"
|
|
8
|
+
Requires-Dist: pytest-benchmark; extra == "test"
|
|
9
|
+
Requires-Dist: numpy; extra == "test"
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# pyqpmad
|
|
13
|
+
|
|
14
|
+
A fast Python wrapper for [qpmad](https://github.com/asherikov/qpmad), a C++ Quadratic Programming (QP) solver based on Goldfarb-Idnani's active-set method.
|
|
15
|
+
`pyqpmad` is built using [nanobind](https://github.com/wjakob/nanobind) for high-performance Python bindings and [Eigen](https://eigen.tuxfamily.org/) for efficient linear algebra.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- Solves Unconstrained, Constrained, and Bounded Quadratic Programs.
|
|
20
|
+
- Lightweight and fast wrapper utilizing `nanobind`.
|
|
21
|
+
- Automatic compilation of dependencies (`qpmad`, `eigen`, `nanobind`) from source via CMake.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
### Using `uv` (Recommended)
|
|
26
|
+
|
|
27
|
+
You can easily build and install this package using [`uv`](https://github.com/astral-sh/uv), an extremely fast Python package installer and resolver.
|
|
28
|
+
|
|
29
|
+
To install it directly:
|
|
30
|
+
```bash
|
|
31
|
+
uv pip install .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
To run the tests:
|
|
35
|
+
```bash
|
|
36
|
+
uv run --with pytest test_qpmad.py
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Using `pip`
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install .
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
To run tests with pip:
|
|
46
|
+
```bash
|
|
47
|
+
pip install pytest
|
|
48
|
+
pytest test_qpmad.py
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Build Explanation
|
|
52
|
+
|
|
53
|
+
The build process is managed entirely by CMake. When you install the package (via `pip` or `uv`), CMake's `FetchContent` module is triggered to automatically download the required dependencies:
|
|
54
|
+
- **`nanobind`** (v2.12.0)
|
|
55
|
+
- **`eigen`** (v5.0.1 mirror)
|
|
56
|
+
- **`qpmad`** (v1.4.0)
|
|
57
|
+
|
|
58
|
+
CMake then configures `qpmad` as an interface library linked with Eigen, and compiles the Python wrapper (`qpmad_pywrap.cpp`) using `nanobind`. Finally, Python type stubs (`.pyi`) are generated for IDE type hinting.
|
|
59
|
+
|
|
60
|
+
## Example Usage
|
|
61
|
+
|
|
62
|
+
Here is a quick example demonstrating how to solve a constrained QP problem.
|
|
63
|
+
|
|
64
|
+
**Problem:** Minimize $x^2 + y^2$ subject to $x + y = 1$
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
import pyqpmad
|
|
68
|
+
import numpy as np
|
|
69
|
+
|
|
70
|
+
solver = pyqpmad.Solver()
|
|
71
|
+
|
|
72
|
+
# Define the Hessian matrix (H) and linear term (h)
|
|
73
|
+
H = np.array([[2.0, 0.0],
|
|
74
|
+
[0.0, 2.0]], order='F')
|
|
75
|
+
h = np.array([0.0, 0.0])
|
|
76
|
+
|
|
77
|
+
# Define the linear constraint: 1.0 <= x + y <= 1.0
|
|
78
|
+
A = np.array([[1.0, 1.0]], order='F')
|
|
79
|
+
Alb = np.array([1.0]) # Lower bound
|
|
80
|
+
Aub = np.array([1.0]) # Upper bound
|
|
81
|
+
|
|
82
|
+
# Pre-allocate output array
|
|
83
|
+
primal = np.zeros(2)
|
|
84
|
+
|
|
85
|
+
# Solve
|
|
86
|
+
status = solver.solve(primal, H, h, A=A, Alb=Alb, Aub=Aub)
|
|
87
|
+
|
|
88
|
+
if status == pyqpmad.ReturnStatus.OK:
|
|
89
|
+
print(f"Optimal solution found: {primal}") # Output: [0.5 0.5]
|
|
90
|
+
else:
|
|
91
|
+
print(f"Failed to solve. Status: {status}")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
For more examples—such as solving unconstrained problems or adding lower/upper bounds—please check `test_qpmad.py`.
|
|
95
|
+
|
|
96
|
+
## Release Pipeline
|
|
97
|
+
|
|
98
|
+
This project uses `cibuildwheel` integrated via GitHub Actions to automatically build wheels for Linux, macOS, and Windows.
|
|
99
|
+
|
|
100
|
+
### Triggering a Release
|
|
101
|
+
|
|
102
|
+
To publish a new release to PyPI, tag the main branch with a version number starting with `v` (e.g., `v1.0.0`) and push the tag to GitHub:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
git tag v1.0.0
|
|
106
|
+
git push origin v1.0.0
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The GitHub Actions workflow will:
|
|
110
|
+
1. Build `sdist` and wheels for all major platforms.
|
|
111
|
+
2. Run tests on the generated wheels.
|
|
112
|
+
3. Automatically publish the artifacts to PyPI.
|
pyqpmad-1.4.0/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# pyqpmad
|
|
2
|
+
|
|
3
|
+
A fast Python wrapper for [qpmad](https://github.com/asherikov/qpmad), a C++ Quadratic Programming (QP) solver based on Goldfarb-Idnani's active-set method.
|
|
4
|
+
`pyqpmad` is built using [nanobind](https://github.com/wjakob/nanobind) for high-performance Python bindings and [Eigen](https://eigen.tuxfamily.org/) for efficient linear algebra.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- Solves Unconstrained, Constrained, and Bounded Quadratic Programs.
|
|
9
|
+
- Lightweight and fast wrapper utilizing `nanobind`.
|
|
10
|
+
- Automatic compilation of dependencies (`qpmad`, `eigen`, `nanobind`) from source via CMake.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
### Using `uv` (Recommended)
|
|
15
|
+
|
|
16
|
+
You can easily build and install this package using [`uv`](https://github.com/astral-sh/uv), an extremely fast Python package installer and resolver.
|
|
17
|
+
|
|
18
|
+
To install it directly:
|
|
19
|
+
```bash
|
|
20
|
+
uv pip install .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
To run the tests:
|
|
24
|
+
```bash
|
|
25
|
+
uv run --with pytest test_qpmad.py
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Using `pip`
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
To run tests with pip:
|
|
35
|
+
```bash
|
|
36
|
+
pip install pytest
|
|
37
|
+
pytest test_qpmad.py
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Build Explanation
|
|
41
|
+
|
|
42
|
+
The build process is managed entirely by CMake. When you install the package (via `pip` or `uv`), CMake's `FetchContent` module is triggered to automatically download the required dependencies:
|
|
43
|
+
- **`nanobind`** (v2.12.0)
|
|
44
|
+
- **`eigen`** (v5.0.1 mirror)
|
|
45
|
+
- **`qpmad`** (v1.4.0)
|
|
46
|
+
|
|
47
|
+
CMake then configures `qpmad` as an interface library linked with Eigen, and compiles the Python wrapper (`qpmad_pywrap.cpp`) using `nanobind`. Finally, Python type stubs (`.pyi`) are generated for IDE type hinting.
|
|
48
|
+
|
|
49
|
+
## Example Usage
|
|
50
|
+
|
|
51
|
+
Here is a quick example demonstrating how to solve a constrained QP problem.
|
|
52
|
+
|
|
53
|
+
**Problem:** Minimize $x^2 + y^2$ subject to $x + y = 1$
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import pyqpmad
|
|
57
|
+
import numpy as np
|
|
58
|
+
|
|
59
|
+
solver = pyqpmad.Solver()
|
|
60
|
+
|
|
61
|
+
# Define the Hessian matrix (H) and linear term (h)
|
|
62
|
+
H = np.array([[2.0, 0.0],
|
|
63
|
+
[0.0, 2.0]], order='F')
|
|
64
|
+
h = np.array([0.0, 0.0])
|
|
65
|
+
|
|
66
|
+
# Define the linear constraint: 1.0 <= x + y <= 1.0
|
|
67
|
+
A = np.array([[1.0, 1.0]], order='F')
|
|
68
|
+
Alb = np.array([1.0]) # Lower bound
|
|
69
|
+
Aub = np.array([1.0]) # Upper bound
|
|
70
|
+
|
|
71
|
+
# Pre-allocate output array
|
|
72
|
+
primal = np.zeros(2)
|
|
73
|
+
|
|
74
|
+
# Solve
|
|
75
|
+
status = solver.solve(primal, H, h, A=A, Alb=Alb, Aub=Aub)
|
|
76
|
+
|
|
77
|
+
if status == pyqpmad.ReturnStatus.OK:
|
|
78
|
+
print(f"Optimal solution found: {primal}") # Output: [0.5 0.5]
|
|
79
|
+
else:
|
|
80
|
+
print(f"Failed to solve. Status: {status}")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
For more examples—such as solving unconstrained problems or adding lower/upper bounds—please check `test_qpmad.py`.
|
|
84
|
+
|
|
85
|
+
## Release Pipeline
|
|
86
|
+
|
|
87
|
+
This project uses `cibuildwheel` integrated via GitHub Actions to automatically build wheels for Linux, macOS, and Windows.
|
|
88
|
+
|
|
89
|
+
### Triggering a Release
|
|
90
|
+
|
|
91
|
+
To publish a new release to PyPI, tag the main branch with a version number starting with `v` (e.g., `v1.0.0`) and push the tag to GitHub:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
git tag v1.0.0
|
|
95
|
+
git push origin v1.0.0
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The GitHub Actions workflow will:
|
|
99
|
+
1. Build `sdist` and wheels for all major platforms.
|
|
100
|
+
2. Run tests on the generated wheels.
|
|
101
|
+
3. Automatically publish the artifacts to PyPI.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pyqpmad"
|
|
3
|
+
version = "1.4.0"
|
|
4
|
+
description = "Python wrapper for qpmad"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = []
|
|
8
|
+
|
|
9
|
+
[project.optional-dependencies]
|
|
10
|
+
test = ["pytest", "pytest-benchmark", "numpy"]
|
|
11
|
+
|
|
12
|
+
[build-system]
|
|
13
|
+
requires = ["scikit-build-core>=0.8.0"]
|
|
14
|
+
build-backend = "scikit_build_core.build"
|
|
15
|
+
|
|
16
|
+
[tool.scikit-build]
|
|
17
|
+
cmake.version = ">=3.22"
|
|
18
|
+
ninja.version = ">=1.5"
|
|
19
|
+
|
|
20
|
+
[tool.cibuildwheel]
|
|
21
|
+
test-command = "pytest {project}/test_qpmad.py"
|
|
22
|
+
test-requires = ["pytest", "pytest-benchmark", "numpy"]
|
|
23
|
+
|
|
24
|
+
# Build for standard CPython only, skipping 32-bit builds
|
|
25
|
+
build = "cp3*"
|
|
26
|
+
skip = "*-win32 *_i686"
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#include <nanobind/eigen/dense.h>
|
|
2
|
+
#include <nanobind/nanobind.h>
|
|
3
|
+
#include <nanobind/stl/optional.h>
|
|
4
|
+
#include <qpmad/solver.h>
|
|
5
|
+
|
|
6
|
+
namespace nb = nanobind;
|
|
7
|
+
|
|
8
|
+
NB_MODULE(pyqpmad, m) {
|
|
9
|
+
nb::enum_<qpmad::SolverParameters::HessianType>(m, "HessianType")
|
|
10
|
+
.value("UNDEFINED", qpmad::SolverParameters::UNDEFINED)
|
|
11
|
+
.value("HESSIAN_LOWER_TRIANGULAR",
|
|
12
|
+
qpmad::SolverParameters::HESSIAN_LOWER_TRIANGULAR)
|
|
13
|
+
.value("HESSIAN_CHOLESKY_FACTOR",
|
|
14
|
+
qpmad::SolverParameters::HESSIAN_CHOLESKY_FACTOR)
|
|
15
|
+
.value("HESSIAN_INVERTED_CHOLESKY_FACTOR",
|
|
16
|
+
qpmad::SolverParameters::HESSIAN_INVERTED_CHOLESKY_FACTOR)
|
|
17
|
+
.export_values();
|
|
18
|
+
|
|
19
|
+
nb::class_<qpmad::SolverParameters>(m, "SolverParameters")
|
|
20
|
+
.def(nb::init<>())
|
|
21
|
+
.def_rw("hessian_type", &qpmad::SolverParameters::hessian_type_)
|
|
22
|
+
.def_rw("tolerance", &qpmad::SolverParameters::tolerance_)
|
|
23
|
+
.def_rw("max_iter", &qpmad::SolverParameters::max_iter_)
|
|
24
|
+
.def_rw("return_inverted_cholesky_factor",
|
|
25
|
+
&qpmad::SolverParameters::return_inverted_cholesky_factor_);
|
|
26
|
+
|
|
27
|
+
nb::enum_<qpmad::Solver::ReturnStatus>(m, "ReturnStatus")
|
|
28
|
+
.value("OK", qpmad::Solver::OK)
|
|
29
|
+
.value("MAXIMAL_NUMBER_OF_ITERATIONS",
|
|
30
|
+
qpmad::Solver::MAXIMAL_NUMBER_OF_ITERATIONS)
|
|
31
|
+
.value("UNDEFINED", qpmad::Solver::UNDEFINED)
|
|
32
|
+
.export_values();
|
|
33
|
+
|
|
34
|
+
nb::class_<qpmad::Solver>(m, "Solver")
|
|
35
|
+
.def(nb::init<>())
|
|
36
|
+
.def("reserve", &qpmad::Solver::reserve, nb::arg("primal_size"),
|
|
37
|
+
nb::arg("num_simple_bounds"), nb::arg("num_general_constraints"))
|
|
38
|
+
.def(
|
|
39
|
+
"solve",
|
|
40
|
+
[](qpmad::Solver &s, Eigen::Ref<Eigen::VectorXd> primal,
|
|
41
|
+
Eigen::MatrixXd H, const Eigen::Ref<const Eigen::VectorXd> &h,
|
|
42
|
+
const std::optional<Eigen::Ref<const Eigen::VectorXd>> &lb,
|
|
43
|
+
const std::optional<Eigen::Ref<const Eigen::VectorXd>> &ub,
|
|
44
|
+
const std::optional<Eigen::Ref<const Eigen::MatrixXd>> &A,
|
|
45
|
+
const std::optional<Eigen::Ref<const Eigen::VectorXd>> &Alb,
|
|
46
|
+
const std::optional<Eigen::Ref<const Eigen::VectorXd>> &Aub,
|
|
47
|
+
const std::optional<qpmad::SolverParameters> ¶ms) {
|
|
48
|
+
Eigen::VectorXd ev;
|
|
49
|
+
Eigen::MatrixXd em;
|
|
50
|
+
qpmad::SolverParameters default_params;
|
|
51
|
+
const qpmad::SolverParameters ¶ms_ref =
|
|
52
|
+
params ? *params : default_params;
|
|
53
|
+
|
|
54
|
+
return s.solve(
|
|
55
|
+
primal, H, h, lb ? *lb : (Eigen::Ref<const Eigen::VectorXd>)ev,
|
|
56
|
+
ub ? *ub : (Eigen::Ref<const Eigen::VectorXd>)ev,
|
|
57
|
+
A ? *A : (Eigen::Ref<const Eigen::MatrixXd>)em,
|
|
58
|
+
Alb ? *Alb : (Eigen::Ref<const Eigen::VectorXd>)ev,
|
|
59
|
+
Aub ? *Aub : (Eigen::Ref<const Eigen::VectorXd>)ev, params_ref);
|
|
60
|
+
},
|
|
61
|
+
nb::arg("primal").noconvert(), nb::arg("H"), nb::arg("h"),
|
|
62
|
+
nb::arg("lb") = nb::none(), nb::arg("ub") = nb::none(),
|
|
63
|
+
nb::arg("A") = nb::none(), nb::arg("Alb") = nb::none(),
|
|
64
|
+
nb::arg("Aub") = nb::none(), nb::arg("params") = nb::none());
|
|
65
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import pyqpmad
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
def test_unconstrained():
|
|
6
|
+
solver = pyqpmad.Solver()
|
|
7
|
+
H = np.array([[2.0, 0.0], [0.0, 2.0]], order='F')
|
|
8
|
+
h = np.array([-2.0, -4.0])
|
|
9
|
+
primal = np.zeros(2)
|
|
10
|
+
|
|
11
|
+
# Minimize 1/2 x'Hx + h'x => x'x + [-2, -4]'x
|
|
12
|
+
# Gradient: 2x + [-2, -4]' = 0 => x = [1, 2]
|
|
13
|
+
|
|
14
|
+
status = solver.solve(primal, H, h)
|
|
15
|
+
|
|
16
|
+
assert status == pyqpmad.ReturnStatus.OK
|
|
17
|
+
np.testing.assert_allclose(primal, [1.0, 2.0])
|
|
18
|
+
|
|
19
|
+
def test_constrained():
|
|
20
|
+
solver = pyqpmad.Solver()
|
|
21
|
+
# Minimize x^2 + y^2 subject to x + y = 1
|
|
22
|
+
H = np.array([[2.0, 0.0], [0.0, 2.0]], order='F')
|
|
23
|
+
h = np.array([0.0, 0.0])
|
|
24
|
+
A = np.array([[1.0, 1.0]], order='F')
|
|
25
|
+
Alb = np.array([1.0])
|
|
26
|
+
Aub = np.array([1.0])
|
|
27
|
+
primal = np.zeros(2)
|
|
28
|
+
|
|
29
|
+
status = solver.solve(primal, H, h, A=A, Alb=Alb, Aub=Aub)
|
|
30
|
+
|
|
31
|
+
assert status == pyqpmad.ReturnStatus.OK
|
|
32
|
+
# Optimum is at x=0.5, y=0.5
|
|
33
|
+
np.testing.assert_allclose(primal, [0.5, 0.5])
|
|
34
|
+
|
|
35
|
+
def test_bounds():
|
|
36
|
+
solver = pyqpmad.Solver()
|
|
37
|
+
# Minimize x^2 + y^2 subject to x >= 1, y >= 1
|
|
38
|
+
H = np.array([[2.0, 0.0], [0.0, 2.0]], order='F')
|
|
39
|
+
h = np.array([0.0, 0.0])
|
|
40
|
+
lb = np.array([1.0, 1.0])
|
|
41
|
+
ub = np.array([10.0, 10.0])
|
|
42
|
+
primal = np.zeros(2)
|
|
43
|
+
|
|
44
|
+
status = solver.solve(primal, H, h, lb=lb, ub=ub)
|
|
45
|
+
|
|
46
|
+
assert status == pyqpmad.ReturnStatus.OK
|
|
47
|
+
np.testing.assert_allclose(primal, [1.0, 1.0])
|
|
48
|
+
|
|
49
|
+
@pytest.mark.parametrize("size", [4, 10, 50, 100, 500])
|
|
50
|
+
def test_random_problems(size):
|
|
51
|
+
np.random.seed(42)
|
|
52
|
+
solver = pyqpmad.Solver()
|
|
53
|
+
|
|
54
|
+
# Generate random positive definite Hessian
|
|
55
|
+
Q, _ = np.linalg.qr(np.random.randn(size, size))
|
|
56
|
+
D = np.diag(np.random.rand(size) + 0.1)
|
|
57
|
+
H = (Q @ D @ Q.T)
|
|
58
|
+
|
|
59
|
+
# Random linear part
|
|
60
|
+
h = np.random.randn(size)
|
|
61
|
+
|
|
62
|
+
# 1. Unconstrained
|
|
63
|
+
primal = np.zeros(size)
|
|
64
|
+
status = solver.solve(primal, H, h)
|
|
65
|
+
assert status == pyqpmad.ReturnStatus.OK
|
|
66
|
+
# Reference solution: x = -H^-1 h
|
|
67
|
+
ref_sol = np.linalg.solve(H, -h)
|
|
68
|
+
np.testing.assert_allclose(primal, ref_sol, atol=1e-8)
|
|
69
|
+
|
|
70
|
+
# 2. Bounded
|
|
71
|
+
# Add tight bounds around a random point
|
|
72
|
+
target = np.random.randn(size)
|
|
73
|
+
lb = target - 0.1
|
|
74
|
+
ub = target + 0.1
|
|
75
|
+
primal = np.zeros(size)
|
|
76
|
+
status = solver.solve(primal, H, h, lb=lb, ub=ub)
|
|
77
|
+
assert status == pyqpmad.ReturnStatus.OK
|
|
78
|
+
# Check if bounds are satisfied
|
|
79
|
+
assert np.all(primal >= lb - 1e-10)
|
|
80
|
+
assert np.all(primal <= ub + 1e-10)
|
|
81
|
+
|
|
82
|
+
# 3. Constrained
|
|
83
|
+
# Add random linear constraints
|
|
84
|
+
n_cons = size // 2 if size > 2 else 1
|
|
85
|
+
A = np.random.randn(n_cons, size)
|
|
86
|
+
# Make constraints feasible by picking a point and calculating bounds
|
|
87
|
+
x_feat = np.random.randn(size)
|
|
88
|
+
Ax_feat = A @ x_feat
|
|
89
|
+
Alb = Ax_feat - 0.1
|
|
90
|
+
Aub = Ax_feat + 0.1
|
|
91
|
+
|
|
92
|
+
primal = np.zeros(size)
|
|
93
|
+
status = solver.solve(primal, H, h, A=A, Alb=Alb, Aub=Aub)
|
|
94
|
+
assert status == pyqpmad.ReturnStatus.OK
|
|
95
|
+
# Check if constraints are satisfied
|
|
96
|
+
Ax = A @ primal
|
|
97
|
+
assert np.all(Ax >= Alb - 1e-10)
|
|
98
|
+
assert np.all(Ax <= Aub + 1e-10)
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
pytest.main([__file__])
|