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.
@@ -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
@@ -0,0 +1,19 @@
1
+ build*/
2
+ install*/
3
+ .pytest_cache/
4
+ .cache/
5
+ .pixi/
6
+ __pycache__/
7
+ Xcode*
8
+ *.pyc
9
+ *~
10
+ *.egg-info
11
+ .ruff_cache/
12
+ .DS_Store
13
+ compile_commands.json
14
+ cmake-profiling.json
15
+ result
16
+ *.conda
17
+ dist/
18
+ .venv/
19
+ wheelhouse/
@@ -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.
@@ -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> &params) {
48
+ Eigen::VectorXd ev;
49
+ Eigen::MatrixXd em;
50
+ qpmad::SolverParameters default_params;
51
+ const qpmad::SolverParameters &params_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__])