Repository: stephane-caron/qpsolvers Branch: main Commit: a9b509eff8cd Files: 111 Total size: 520.4 KB Directory structure: gitextract_sm8s1023/ ├── .gitattributes ├── .github/ │ └── workflows/ │ ├── changelog.yml │ ├── ci.yml │ ├── docs.yml │ └── pypi.yml ├── .gitignore ├── CHANGELOG.md ├── CITATION.cff ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── doc/ │ ├── conf.py │ ├── developer-notes.rst │ ├── index.rst │ ├── installation.rst │ ├── least-squares.rst │ ├── quadratic-programming.rst │ ├── references.rst │ ├── supported-solvers.rst │ └── unsupported-solvers.rst ├── examples/ │ ├── README.md │ ├── box_inequalities.py │ ├── constrained_linear_regression.py │ ├── dual_multipliers.py │ ├── lasso_regularization.py │ ├── least_squares.py │ ├── model_predictive_control.py │ ├── quadratic_program.py │ ├── sparse_least_squares.py │ ├── test_dense_problem.py │ ├── test_model_predictive_control.py │ ├── test_random_problems.py │ └── test_sparse_problem.py ├── pyproject.toml ├── qpsolvers/ │ ├── __init__.py │ ├── active_set.py │ ├── conversions/ │ │ ├── __init__.py │ │ ├── combine_linear_box_inequalities.py │ │ ├── ensure_sparse_matrices.py │ │ ├── linear_from_box_inequalities.py │ │ ├── socp_from_qp.py │ │ └── split_dual_linear_box.py │ ├── exceptions.py │ ├── problem.py │ ├── problems.py │ ├── py.typed │ ├── solution.py │ ├── solve_ls.py │ ├── solve_problem.py │ ├── solve_qp.py │ ├── solve_unconstrained.py │ ├── solvers/ │ │ ├── __init__.py │ │ ├── clarabel_.py │ │ ├── copt_.py │ │ ├── cvxopt_.py │ │ ├── daqp_.py │ │ ├── ecos_.py │ │ ├── gurobi_.py │ │ ├── highs_.py │ │ ├── hpipm_.py │ │ ├── jaxopt_osqp_.py │ │ ├── kvxopt_.py │ │ ├── mosek_.py │ │ ├── nppro_.py │ │ ├── osqp_.py │ │ ├── pdhcg_.py │ │ ├── piqp_.py │ │ ├── proxqp_.py │ │ ├── pyqpmad_.py │ │ ├── qpalm_.py │ │ ├── qpax_.py │ │ ├── qpoases_.py │ │ ├── qpswift_.py │ │ ├── qtqp_.py │ │ ├── quadprog_.py │ │ ├── scs_.py │ │ └── sip_.py │ ├── utils.py │ └── warnings.py └── tests/ ├── __init__.py ├── problems.py ├── test_clarabel.py ├── test_combine_linear_box_inequalities.py ├── test_conversions.py ├── test_copt.py ├── test_cvxopt.py ├── test_ecos.py ├── test_gurobi.py ├── test_highs.py ├── test_jaxopt_osqp.py ├── test_kvxopt.py ├── test_mosek.py ├── test_nppro.py ├── test_osqp.py ├── test_piqp.py ├── test_problem.py ├── test_proxqp.py ├── test_pyqpmad.py ├── test_qpax.py ├── test_qpoases.py ├── test_qpswift.py ├── test_quadprog.py ├── test_scs.py ├── test_sip.py ├── test_solution.py ├── test_solve_ls.py ├── test_solve_problem.py ├── test_solve_qp.py ├── test_timings.py ├── test_unfeasible_problem.py └── test_utils.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ pixi.lock merge=binary linguist-language=YAML linguist-generated=true -diff ================================================ FILE: .github/workflows/changelog.yml ================================================ name: Changelog on: pull_request: branches: [ main ] jobs: changelog: name: "Check changelog update" runs-on: ubuntu-latest steps: - uses: tarides/changelog-check-action@v2 with: changelog: CHANGELOG.md changelog_success: name: "Changelog success" runs-on: ubuntu-latest needs: [changelog] steps: - run: echo "Changelog workflow completed successfully" ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: [ main ] pull_request: branches: [ main ] workflow_dispatch: jobs: check-secrets: name: "Check availability of GitHub secrets" runs-on: ubuntu-latest outputs: has-secrets: ${{ steps.secret-check.outputs.available }} steps: - name: Check whether GitHub secrets are available id: secret-check shell: bash run: | if [ '${{ secrets.MSK_LICENSE }}' != '' ]; then echo "available=true" >> ${GITHUB_OUTPUT}; else echo "available=false" >> ${GITHUB_OUTPUT}; fi coverage: name: "Coverage" runs-on: ubuntu-latest needs: [check-secrets] if: needs.check-secrets.outputs.has-secrets == 'true' steps: - name: "Checkout sources" uses: actions/checkout@v4 - name: "Setup Pixi" uses: prefix-dev/setup-pixi@v0.8.8 with: pixi-version: v0.59.0 cache: true - name: "Prepare license files" env: MSK_LICENSE: ${{ secrets.MSK_LICENSE }} run: | echo "${MSK_LICENSE}" > ${{ github.workspace }}/mosek.lic - name: "Check code coverage" env: MOSEKLM_LICENSE_FILE: ${{ github.workspace }}/mosek.lic run: | pixi run coverage - name: "Upload coverage results" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | pixi run coveralls licensed: name: "Test licensed solvers on ${{ matrix.os }}" runs-on: ${{ matrix.os }} needs: [check-secrets] if: needs.check-secrets.outputs.has-secrets == 'true' strategy: matrix: os: [ubuntu-latest, macos-latest] steps: - name: "Checkout sources" uses: actions/checkout@v4 - name: "Setup Pixi" uses: prefix-dev/setup-pixi@v0.8.8 with: pixi-version: v0.59.0 environments: licensed cache: true - name: "Prepare license files" env: MSK_LICENSE: ${{ secrets.MSK_LICENSE }} run: | echo "${MSK_LICENSE}" > ${{ github.workspace }}/mosek.lic - name: "Test licensed solvers" env: MOSEKLM_LICENSE_FILE: ${{ github.workspace }}/mosek.lic run: | pixi run -e licensed licensed lint: name: "Code style" runs-on: ubuntu-latest steps: - name: "Checkout sources" uses: actions/checkout@v4 - name: "Setup Pixi" uses: prefix-dev/setup-pixi@v0.8.8 with: pixi-version: v0.59.0 environments: lint cache: true - name: "Lint qpsolvers" run: | pixi run lint test: name: "Test ${{ matrix.os }} with ${{ matrix.pyenv }}" runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] pyenv: [py310, py311, py312, py313] env: PIXI_ENV: test-${{ matrix.pyenv }} steps: - name: "Checkout sources" uses: actions/checkout@v4 - name: "Setup Pixi" uses: prefix-dev/setup-pixi@v0.8.8 with: pixi-version: v0.59.0 environments: ${{ env.PIXI_ENV }} cache: true - name: "Run unit tests" run: | pixi run --environment ${{ env.PIXI_ENV }} test ci_success: name: "CI success" runs-on: ubuntu-latest needs: [coverage, licensed, lint, test] steps: - run: echo "CI workflow completed successfully" ================================================ FILE: .github/workflows/docs.yml ================================================ name: Documentation on: push: branches: [ main ] pull_request: branches: [ main ] jobs: docs: name: "GitHub Pages" runs-on: ubuntu-latest permissions: contents: write steps: - name: "Checkout Git repository" uses: actions/checkout@v4 - name: "Setup Pixi" uses: prefix-dev/setup-pixi@v0.8.8 with: pixi-version: v0.59.0 environments: docs cache: true - name: "Checkout qpSWIFT" uses: actions/checkout@v4 with: repository: qpSWIFT/qpSWIFT path: qpSWIFT - name: "Install qpSWIFT" run: | cd qpSWIFT/python pixi run -e docs python setup.py install - name: "Build documentation" run: | pixi run -e docs docs-build - name: "Deploy to GitHub Pages" uses: peaceiris/actions-gh-pages@v3 if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} with: publish_branch: gh-pages github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: _build/ force_orphan: true ================================================ FILE: .github/workflows/pypi.yml ================================================ name: PyPI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: pypi: name: "Install from PyPI" runs-on: ubuntu-latest steps: - name: "Checkout sources" uses: actions/checkout@v4 - name: "Install dependencies" run: | python -m pip install --upgrade pip - name: "Install package" run: python -m pip install qpsolvers - name: "Install at least one solver" run: python -m pip install quadprog - name: "Test module import" run: python -c "import qpsolvers" testpypi: name: "Install from TestPyPI" runs-on: ubuntu-latest steps: - name: "Checkout sources" uses: actions/checkout@v4 - name: "Install dependencies" run: | python -m pip install --upgrade pip - name: "Install package" run: python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ qpsolvers - name: "Install at least one solver" run: python -m pip install quadprog - name: "Test module import" run: python -c "import qpsolvers" pypi_success: name: "PyPI success" runs-on: ubuntu-latest needs: [pypi, testpypi] steps: - run: echo "PyPI workflow completed successfully" ================================================ FILE: .gitignore ================================================ *.pyc *.pyo .coverage .ropeproject .tox MANIFEST _build/ build/ dist/ examples/qpsolvers gurobi.log htmlcov/ qpsolvers.egg-info ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ### Added - Add build and solve timing info (thanks to @ahoarau) - DAQP: Warm starts and time limit are now enabled in DAQP (thanks to @darnstrom) - New solver: qpmad (thanks to @ahoarau) - Support Python 3.13 ### Fixed - HiGHS: update interface following code changes in HiGHS v1.14.0 ## [4.11.0] - 2026-03-16 ### Added - New solver: PDHCG (thanks to @Lhongpei) ### Changed - Change params in copt test to avoid unexpected timeout fail (thanks to @Salancelot) - Merge unsupported solvers submodule within the main solvers submodule - Remove unintended copied copyright information in copt_.py (thanks to @Salancelot) ### Fixed - CICD: Clean up OS-specific features and reduce environments (thanks to @ahoarau) ## [4.10.0] - 2026-03-10 ### Added - New solver: [COPT](https://guide.coap.online/copt/en-doc/index.html) (thanks to @Salancelot) ### Changed - Gurobi: update to newer Gurobi API ### Fixed - CICD: Add Gurobi to pixi PyPI dependencies ## [4.9.0] - 2026-03-04 ### Added - Add `unpack_as_dense` function to problems - Add type annotations to internal `__solve_sparse_ls` function - New solver: [QTQP](https://github.com/google-deepmind/qtqp) ### Changed - Bump minimum Python version to 3.10 - CICD: Configure Git attributes for the pixi.lock file - CICD: Switch from tox to pixi - KVXOPT: Update type annotations - SIP: Ensure returned primal and dual vectors are NumPy arrays - SIP: Remove unused CVXOPT-related code from solver interface ### Fixed - Correct type annotation of SOCP conversion function - Correct type annotation of solver function prototypes - ECOS: Rename internal intermediate variables to improve type checking - ECOS: Revise type annotations in solver interface - Fix sparse type annotations in `concatenate_bound` - Fix type annotation corner case in `linear_from_box_inequalities` - Fix type annotations in internal `__solve_dense_ls` function - Fix variable name re-use errors raised by newer versions of mypy - SIP: Forward allow-non-PSD argument in `solve_qp` variant of the interface - Unpack problems as NumPy arrays for dense solver APIs ### Removed - Remove support for Python 3.9 - CICD: Remove Python 3.9 ## [4.8.2] - 2025-11-25 ### Added - docs: Document warm-starting for all solver interfaces ### Changed - Prevent deprecation warnings from OSQP related to solver status (thanks to @jkeust) - Prevent warning message from OSQP about conversion to a CSC matrix (thanks to @jkeust) - Slightly more explicit error message when a solver is not found ## [4.8.1] - 2025-08-07 ### Changed - Clarabel: Warn when problem is unconstrained and solved by `lsqr` (thanks to @proyan) ## [4.8.0] - 2025-07-02 ### Added - PIQP: Add support for new API in v0.6.0 (thanks to @RSchwan) - PIQP: Add new solver options to the documentation (thanks to @RSchwan) ### Fixed - CICD: Enable macOS tests for KVXOPT (thanks to @sanurielf) ## [4.7.1] - 2025-06-03 ### Added - `py.typed` file to indicate tools like `mypy` to use type annotations (thanks to @ValerianRey) ## [4.7.0] - 2025-05-13 ### Added - New solver: [SIP](https://github.com/joaospinto/sip_python) (thanks to @joaospinto) - warnings: Add `SparseConversionWarning` to filter the corresponding warning - warnings: Base class `QPWarning` for all qpsolvers-related warnings - warnings: Recall solver name when issuing conversion warnings ### Changed - Add solver name argument to internal `ensure_sparse_matrices` function - CICD: Update Python version to 3.10 in coverage job ### Fixed - docs: Add jaxopt.OSQP to the list of supported solvers ### Removed - OSQP: Remove pre-1.0 version pin - OSQP: Update interface after relase of v1.0.4 - Warning that was issued every time an unsupported solver is available ## [4.6.0] - 2025-04-17 ### Added - New solver: [KVXOPT](https://github.com/sanurielf/kvxopt/) (thanks to @agroudiev) - jaxopt.OSQP: Support JAX array inputs when jaxopt.OSQP is the selected solver ## [4.5.1] - 2025-04-10 ### Changed - CICD: Update checkout action to v4 ### Fixed - OSQP: Temporary fix in returning primal-dual infeasibility certificates ## [4.5.0] - 2025-03-04 ### Added - HPIPM: Document new `tol_dual_gap` parameter - New solver: [jaxopt.OSQP](https://jaxopt.github.io/stable/_autosummary/jaxopt.OSQP.html) - Support Python 3.12 ### Changed - Bump minimum Python version to 3.8 - CICD: Remove Python 3.8 from continuous integration - Fix output datatypes when splitting linear-box dual multipliers - OSQP: version-pin to < 1.0.0 pending an interface update - Warn when solving unconstrained problem by SciPy's LSQR rather than QP solver ### Fixed - Fix mypy error in `Solution.primal_residual` ## [4.4.0] - 2024-09-24 ### Added - HPIPM: Link to reference paper for details on solver modes - New solver: [qpax](https://github.com/kevin-tracy/qpax) (thanks to @lvjonok) ## [4.3.3] - 2024-08-06 ### Changed - CICD: Remove Gurobi from macOS continuous integration - CICD: Remove Python 3.7 from continuous integration - CICD: Update ruff to 0.4.3 ### Fixed - CICD: Fix coverage and licensed-solver workflows - CICD: Install missing dependency in licensed solver test environment - Clarabel: Catch pyO3 panics that can happen when building a problem - Default arguments to active set dataclass to `None` rather than empty list - PIQP: Warning message about CSC matrix conversions (thanks to @itsahmedkhalil) - Update all instances of `np.infty` to `np.inf` ## [4.3.2] - 2024-03-25 ### Added - Optional dependency: `wheels_only` for solvers with pre-compiled binaries ### Changed - Update developer notes in the documentation - Update some solver tolerances in unit tests - Warn rather than raise when there is no solver detected ### Fixed - CICD: Update micromamba setup action ## [4.3.1] - 2024-02-06 ### Fixed - Gurobi: sign of inequality multipliers (thanks to @563925743) ## [4.3.0] - 2024-01-23 ### Added - Extend continuous integration to Python 3.11 - Function to get the CUTE classification string of the problem - Optional dependencies for all solvers in the list available on PyPI ### Changed - **Breaking:** no default QP solver installed along with the library - NPPro: update exit flag value to match new solver API (thanks to @ottapav) ### Fixed - Documentation: Add Clarabel to the list of supported solvers (thanks to @ogencoglu) - Documentation: Correct note in `solve_ls` documentation (thanks to @ogencoglu) - Documentation: Correct output of LS example (thanks to @ogencoglu) ## [4.2.0] - 2023-12-21 ### Added - Example: [lasso regularization](https://scaron.info/blog/lasso-regularization-in-quadratic-programming.html) - `Problem.load` function - `Problem.save` function ## [4.1.1] - 2023-12-05 ### Changed - Mark QPALM as a sparse solver only ## [4.1.0] - 2023-12-04 ### Added - New solver: [QPALM](https://kul-optec.github.io/QPALM/Doxygen/) - Unit test for internal linear-box inequality combination ### Changed - Internal: refactor linear-box inequality combination function - Renamed main branch of the repository from `master` to `main` ### Fixed - Fix combination of box inequalities with empty linear inequalities - Gurobi: Account for a slight regression in QPSUT01 performance ## [4.0.1] - 2023-11-01 ### Added - Allow installation of a subset of QP solvers from PyPI ## [4.0.0] - 2023-08-30 ### Added - New solver: [PIQP](https://github.com/PREDICT-EPFL/piqp) (thanks to @shaoanlu) - Type for active set of equality and inequality constraints ### Changed - **Breaking:** condition number requires an active set (thanks to @aescande) ## [3.5.0] - 2023-08-16 ### Added - New solver: [HPIPM](https://github.com/giaf/hpipm) (thanks to @adamheins) ### Changed - MOSEK: Disable CI test on QPSUT03 due to regression with 10.1.8 - MOSEK: Relax test tolerances as latest version is less accurate with defaults ## [3.4.0] - 2023-04-28 ### Changed - Converted THANKS file to [CFF](https://citation-file-format.github.io/) - ECOS: raise a ProblemError if inequality vectors contain infinite values - ECOS: raise a ProblemError if the cost matrix is not positive definite - MOSEK is now a supported solver (thanks to @uricohen and @aszekMosek) ### Fixed - Residual and duality gap computations when solution is not found - Update OSQP version to 0.6.2.post9 for testing ## [3.3.1] - 2023-04-12 ### Fixed - DAQP: Update to 0.5.1 to fix installation of arm64 wheels ## [3.3.0] - 2023-04-11 ### Added - New sample problems in `qpsolvers.problems` - New solver: [DAQP](https://darnstrom.github.io/daqp/) (thanks to @darnstrom) ### Changed - Dual multipliers are empty arrays rather than None when no constraint - Store solver results even when solution is not found - Switch to `Solution.found` as solver success status (thanks to @rxian) ### Fixed - Unit test on actual solution to QPSUT03 problem ## [3.2.0] - 2023-03-29 ### Added - Sparse strategy to convert LS problems to QP (thanks to @bodono) - Start `problems` submodule to collect sample test problems ### Fixed - Clarabel: upstream handling of infinite values in inequalities - CVXOPT: option passing ## [3.1.0] - 2023-03-07 ### Added - New solver: NPPro ### Changed - Documentation: separate support and unsupported solver lists - Exclude unsupported solvers from code coverage report - Move unsupported solvers to a separate submodule - Remove CVXOPT from dependencies as it doesn't have arm64 wheels - Remove quadprog from dependencies as it doesn't have arm64 wheels ## [3.0.0] - 2023-02-28 ### Added - Exception `ParamError` for incorrect solver parameters - Exception `SolverError` for solver failures ### Changed - All functions throw only qpsolvers-owned exceptions - CVXOPT: rethrow `ValueError` as either `ProblemError` or `SolverError` - Checking `Solution.is_empty` becomes `not Solution.found` - Install open source solvers with wheels by default - Remove `solve_safer_qp` - Remove `sym_proj` parameter ## [2.8.1] - 2023-02-28 ### Changed - Expose `solve_unconstrained` function from main module ### Fixed - Clarabel: handle unconstrained problems - README: correct and improve FAQ on non-convex problems (thanks to @nrontsis) ## [2.8.0] - 2023-02-27 ### Added - New solver: [Clarabel](https://github.com/oxfordcontrol/Clarabel.rs) ### Changed - Move documentation to [GitHub Pages](https://qpsolvers.github.io/qpsolvers/) - Remove Python 2 installation instructions ## [2.7.4] - 2023-01-31 ### Fixed - Check vector shapes in problem constructor ## [2.7.3] - 2023-01-16 ### Added - qpOASES: return number of WSR in solution extra info ### Fixed - CVXOPT: fix domain errors when some bounds are infinite - qpOASES: fix missing lower bound when there is no equality constraint - qpOASES: handle infinite bounds - qpOASES: segmentation fault with conda feedstock ## [2.7.2] - 2023-01-02 ### Added - ECOS: handle two more exit flags - Exception `ProblemError` for problem formulation errors - Exception `QPError` as a base class for exceptions - Property to check if a Problem has sparse matrices - qpOASES: raise a ProblemError when matrices are not dense - qpSWIFT: raise a ProblemError when matrices are not dense - quadprog: raise a ProblemError when matrices are not dense ### Changed - Add `use_sparse` argument to internal linear-from-box conversion - Restrict condition number calculation to dense problems for now ## [2.7.1] - 2022-12-23 ### Added - Document problem conversion functions in developer notes - ECOS: handle more exit flags ### Changed - quadprog: use internal `split_dual_linear_box` conversion function ### Fixed - SCS: require at least version 3.2 - Solution: duality gap computation under infinite box bounds ## [2.7.0] - 2022-12-15 ### Added - Continuous integration for macOS - CVXOPT: return dual multipliers - ECOS: return dual multipliers - Example: dual multipliers - Gurobi: return dual multipliers - HiGHS: return dual multipliers - MOSEK: return dual multipliers - OSQP: return dual multipliers - Problem class with utility metrics on quadratic programs - Problem: condition number - ProxQP: return dual multipliers - qpOASES: return dual multipliers - qpOASES: return objective value - qpSWIFT: return dual multipliers - qpSWIFT: return objective value - quadprog: return dual multipliers - SCS: return dual multipliers ### Changed - Code: move `solve_safer_qp` to a separate source file - Code: refactor location of internal conversions submodule - ProxQP: bump minimum supported version to 0.2.9 ### Fixed - qpOASES: eliminate redundant equality constraints ## [2.6.0] - 2022-11-14 ### Added - Example: constrained linear regression - Example: sparse linear least squares - Gurobi: forward keyword arguments as solver parameters - Handle diagonal matrices when combining linear and box inequalities - qpOASES: pre-defined options parameter - qpOASES: time limit parameter ### Changed - CVXOPT: forward all keyword arguments as solver options - Deprecate `solve_safer_qp` and warn about future removal - Example: disable verbose output in least squares example - HiGHS: forward all keyword arguments as solver options - OSQP: drop support for versions <= 0.5.0 - OSQP: streamline stacking of box inequalities - ProxQP: also consider constraint matrices to select backend - qpOASES: forward all keyword arguments as solver options - qpOASES: forward box inequalities directly - Remove CVXPY which is not a solver - SCS: `SOLVED_INACCURATE` is now considered a failure ### Fixed - Dot product bug in `solve_ls` with sparse matrices - MOSEK: restore CVXOPT options after calling MOSEK - ProxQP: fix box inequality shapes when combining bounds - qpOASES: non-persistent solver options between calls - qpOASES: return failure on `RET_INIT_FAILED*` return codes ## [2.5.0] - 2022-11-04 ### Added - CVXOPT: absolute tolerance parameter - CVXOPT: feasibility tolerance parameter - CVXOPT: limit maximum number of iterations - CVXOPT: refinement parameter - CVXOPT: relative tolerance parameter - Documentation: reference solver papers - ECOS: document additional parameters - Gurobi: time limit parameter - HiGHS: dual feasibility tolerance parameter - HiGHS: primal feasibility tolerance parameter - HiGHS: time limit parameter ### Changed - CVXOPT matrices are not valid types for qpsolvers any more - CVXOPT: improve documentation - CVXOPT: solver is now listed as sparse as well - ECOS: type annotations allow sparse input matrices - OSQP: don't override default solver tolerances - Remove internal CVXOPT-specific type annotation - Restrict matrix types to NumPy arrays and SciPy CSC matrices - SCS: don't override default solver tolerances - Simplify intermediate internal type annotations ### Fixed - CVXOPT: pass warm-start primal properly - ECOS: forward keyword arguments - OSQP: dense arrays for vectors in type annotations - SCS: fix handling of problems with only box inequalities ## [2.4.1] - 2022-10-21 ### Changed - Update ProxQP to version 0.2.2 ## [2.4.0] - 2022-09-29 ### Added - New solver: [HiGHS](https://github.com/ERGO-Code/HiGHS) - Raise error when there is no available solver ### Changed - Make sure plot is shown in MPC example - Print expected solutions in QP, LS and box-inequality examples - Renamed starter solvers optional deps to `open_source_solvers` ### Fixed - Correct documentation of `R` argument to `solve_ls` ## [2.3.0] - 2022-09-06 ### Added - New solver: [ProxQP](https://github.com/Simple-Robotics/proxsuite) ### Changed - Clean up unused dependencies in GitHub workflow - Non-default solver parameters in unit tests to test their precision ### Fixed - Configuration of `tox-gh-actions` for Python 3.7 - Enforce `USING_COVERAGE` in GitHub workflow configuration - Remove redundant solver loop from `test_all_shapes` ## [2.2.0] - 2022-08-15 ### Added - Add `lb` and `ub` arguments to all `_solve_qp` functions - Internal `conversions` submodule ### Changed - Moved `concatenate_bounds` to internal `conversions` submodule - Moved `convert_to_socp` to internal `conversions` submodule - Renamed `concatenate_bounds` to `linear_from_box_inequalities` - Renamed internal `convert_to_socp` function to `socp_from_qp` ## [2.1.0] - 2022-07-25 ### Added - Document how to add a new QP solver to the library - Example with (box) lower and upper bounds - Test case where `lb` XOR `ub` is set ### Changed - SCS: use the box cone API when lower/upper bounds are set ## [2.0.0] - 2022-07-05 ### Added - Exception `NoSolverSelected` raised when the solver kwarg is missing - Starter set of QP solvers as optional dependencies - Test exceptions raised by `solve_ls` and `solve_qp` ### Changed - **Breaking:** `solver` keyword argument is now mandatory for `solve_ls` - **Breaking:** `solver` keyword argument is now mandatory for `solve_qp` - Quadratic programming example now randomly selects an available solver ## [1.10.0] - 2022-06-25 ### Changed - qpSWIFT: forward solver options as keywords arguments as with other solvers ## [1.9.1] - 2022-05-02 ### Fixed - OSQP: pass extra keyword arguments properly (thanks to @urob) ## [1.9.0] - 2022-04-03 ### Added - Benchmark on model predictive control problem - Model predictive control example - qpSWIFT 0.0.2 solver interface ### Changed - Compute colors automatically in benchmark example ### Fixed - Bounds concatenation for CVXOPT sparse matrices ## [1.8.1] - 2022-03-05 ### Added - Setup instructions for Microsoft Visual Studio - Unit tests where the problem is unbounded below ### Changed - Minimum supported Python version is now 3.7 ### Fixed - Clear all Pylint warnings - Disable Pylint false positives that are covered by mypy - ECOS: raise a ValueError when the cost matrix is not positive definite ## [1.8.0] - 2022-01-13 ### Added - Build and test for Python 3.10 in GitHub Actions ### Changed - Moved SCS to sparse solvers - Re-run solver benchmark reported to the README - Removed `requirements2.txt` and update Python 2 installation instructions - Updated SCS to new 3.0 version ### Fixed - Handle sparse matrices in `print_matrix_vector` - Match `__all__` in model and top-level `__init__.py` - Run unit tests in GitHub Actions - Typing error in bound concatenation ## [1.7.2] - 2021-11-24 ### Added - Convenience function to prettyprint a matrix and vector side by side ### Changed - Move old tests from the examples folder to the unit test suite - Removed deprecated `requirements.txt` installation file - Renamed `solvers` optional dependencies to `all_pypi_solvers` ## [1.7.1] - 2021-10-02 ### Fixed - Make CVXOPT optional again (thanks to @adamoppenheimer) ## [1.7.0] - 2021-09-19 ### Added - Example script corresponding exactly to the README - Handle lower and upper bounds with sparse matrices (thanks to @MeindertHH) - SCS 2.0 solver interface - Type annotations to all solve functions - Unit tests: package coverage is now 94% ### Changed - ECOS: simplify sparse matrix conversions - Ignore warnings when running unit tests - Inequality tolerance is now 1e-10 when validating solvers on README example - Refactor QP to SOCP conversion to use more than one SOCP solver - Rename "example problem" for testing to "README problem" (less ambiguous) - Rename `sw` parameter of `solve_safer_qp` to `sr` for "slack repulsion" - Reorganize code with a qpsolvers/solvers submodule - quadprog: warning when `initvals is not None` is now verbose ### Fixed - OSQP: forward keyword arguments to solver properly - quadprog: forward keyword arguments to solver properly ## [1.6.1] - 2021-04-09 ### Fixed - Add quadprog dependency properly in `pyproject.toml` ## [1.6.0] - 2021-04-09 ### Added - Add `__version__` to main module - First unit tests to check all solvers over a pre-defined set of problems - GitHub Actions now make sure the project is built and tested upon updates - Type hints now decorate all function definitions ### Changed - Code formatting now applies [Black](https://github.com/psf/black) - ECOS: refactor SOCP conversion to improve function readability - Gurobi: performance significantly improved by new matrix API (thanks to @DKenefake) ### Fixed - CVXPY: properly return `None` on unfeasible problems - Consistently warn when `initvals` is passed but ignored by solver interface - ECOS: properly return `None` on unfeasible problems - Fix `None` case in `solve_safer_qp` (found by static type checking) - Fix warnings in repository-level `__init__.py` - OSQP: properly return `None` on unfeasible problems - Pass Flake8 validation for overall code style - Reduce complexity of entry `solve_qp` via a module-level solve-function index - Remove Python 2 compatibility line from examples - quadprog: properly return `None` on unfeasible problems (thanks to @DKenefake) ## [1.5.0] - 2020-12-05 ### Added - Upgrade to Python 3 and deprecate Python 2 - Saved Python 2 package versions to `requirements2.txt` ### Fixed - Deprecation warning in CVXPY ## [1.4.1] - 2020-11-29 ### Added - New `solve_ls` function to solve linear Least Squares problems ### Fixed - Call to `print` in PyPI description - Handling of quadprog ValueError exceptions ## [1.4.0] - 2020-07-04 ### Added - Solver settings can now by passed to `solve_qp` as keyword arguments - Started an [API documentation](https://scaron.info/doc/qpsolvers/) ### Changed - Made `verbose` an explicit keyword argument of all internal functions - OSQP settings now match precision of other solvers (thanks to @Neotriple) ## [1.3.1] - 2020-06-13 ### Fixed - Equation of quadratic program on [PyPI page](https://pypi.org/project/qpsolvers/) ## [1.3.0] - 2020-05-16 ### Added - Lower and upper bound keyword arguments `lb` and `ub` ### Fixed - Check that equality/inequality matrices/vectors are provided consistently - Relaxed offset check in [test\_solvers.py](examples/test_solvers.py) ## [1.2.1] - 2020-05-16 ### Added - CVXPY: verbose keyword argument - ECOS: verbose keyword argument - Gurobi: verbose keyword argument - OSQP: verbose keyword argument ### Fixed - Ignore verbosity argument when solver is not available ## [1.2.0] - 2020-05-16 ### Added - cvxopt: verbose keyword argument - mosek: verbose keyword argument - qpoases: verbose keyword argument ## [1.1.2] - 2020-05-15 ### Fixed - osqp: handle both old and more recent versions ## [1.1.1] - 2020-05-15 ### Fixed - Avoid variable name clash in OSQP - Handle quadprog exception to avoid confusion on cost matrix notation ## [1.1.0] - 2020-03-07 ### Added - ECOS solver interface (no need to go through CVXPY any more) - Update ECOS performance in benchmark (much better than before!) ### Fixed - Fix link to ECOS in setup.py - Remove ned for IPython in solver test - Update notes on P matrix ## [1.0.7] - 2019-10-26 ### Changed - Always reshape A or G vectors into one-line matrices ### Fixed - cvxopt: handle case where G and h are None but not A and b - osqp: handle case where G and h are None - osqp: handle case where both G and A are one-line matrices - qpoases: handle case where G and h are None but not A and b ## [1.0.6] - 2019-10-26 Thanks to Brian Delhaisse and Soeren Wolfers who contributed fixes to this release! ### Fixed - quadprog: handle case where G and h are None - quadprog: handle cas where A.ndim == 1 - Make examples compatible with both Python 2 and Python 3 ## [1.0.5] - 2019-04-10 ### Added - Equality constraint shown in the README example - Installation file `requirements.txt` - Installation instructions for qpOASES - OSQP: automatic CSC matrix conversions (with performance warnings) - This change log ### Fixed - CVXOPT: case where A is one-dimensional - qpOASES: case where both G and A are not None - quadprog: wrapper for one-dimensional A matrix (thanks to @nvitucci) ### Changed - CVXOPT version is now 1.1.8 due to [this issue](https://github.com/urinieto/msaf-gpl/issues/2) - Examples now in a [separate folder](examples) ## [1.0.4] - 2018-07-05 ### Added - A changelog :) [unreleased]: https://github.com/qpsolvers/qpsolvers/compare/v4.11.0...HEAD [4.11.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v4.11.0 [4.10.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v4.10.0 [4.9.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v4.9.0 [4.8.2]: https://github.com/qpsolvers/qpsolvers/releases/tag/v4.8.2 [4.8.1]: https://github.com/qpsolvers/qpsolvers/releases/tag/v4.8.1 [4.8.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v4.8.0 [4.7.1]: https://github.com/qpsolvers/qpsolvers/releases/tag/v4.7.1 [4.7.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v4.7.0 [4.6.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v4.6.0 [4.5.1]: https://github.com/qpsolvers/qpsolvers/releases/tag/v4.5.1 [4.5.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v4.5.0 [4.4.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v4.4.0 [4.3.3]: https://github.com/qpsolvers/qpsolvers/releases/tag/v4.3.3 [4.3.2]: https://github.com/qpsolvers/qpsolvers/releases/tag/v4.3.2 [4.3.1]: https://github.com/qpsolvers/qpsolvers/releases/tag/v4.3.1 [4.3.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v4.3.0 [4.2.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v4.2.0 [4.1.1]: https://github.com/qpsolvers/qpsolvers/releases/tag/v4.1.1 [4.1.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v4.1.0 [4.0.1]: https://github.com/qpsolvers/qpsolvers/releases/tag/v4.0.1 [4.0.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v4.0.0 [3.5.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v3.5.0 [3.4.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v3.4.0 [3.3.1]: https://github.com/qpsolvers/qpsolvers/releases/tag/v3.3.1 [3.3.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v3.3.0 [3.2.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v3.2.0 [3.1.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v3.1.0 [3.0.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v3.0.0 [2.8.1]: https://github.com/qpsolvers/qpsolvers/releases/tag/v2.8.1 [2.8.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v2.8.0 [2.7.3]: https://github.com/qpsolvers/qpsolvers/releases/tag/v2.7.3 [2.7.2]: https://github.com/qpsolvers/qpsolvers/releases/tag/v2.7.2 [2.7.1]: https://github.com/qpsolvers/qpsolvers/releases/tag/v2.7.1 [2.7.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v2.7.0 [2.6.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v2.6.0 [2.5.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v2.5.0 [2.4.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v2.4.0 [2.3.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v2.3.0 [2.2.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v2.2.0 [2.1.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v2.1.0 [2.0.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v2.0.0 [1.10.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.10.0 [1.9.1]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.9.1 [1.9.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.9.0 [1.8.1]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.8.1 [1.8.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.8.0 [1.7.2]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.7.2 [1.7.1]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.7.1 [1.7.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.7.0 [1.6.1]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.6.1 [1.6.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.6.0 [1.5.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.5.0 [1.4.1]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.4.1 [1.4.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.4.0 [1.3.1]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.3.1 [1.3.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.3.0 [1.2.1]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.2.1 [1.2.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.2.0 [1.1.2]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.1.2 [1.1.1]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.1.1 [1.1.0]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.1.0 [1.0.7]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.0.7 [1.0.6]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.0.6 [1.0.5]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.0.5 [1.0.4]: https://github.com/qpsolvers/qpsolvers/releases/tag/v1.0.4 ================================================ FILE: CITATION.cff ================================================ cff-version: 1.2.0 message: "If you find this code helpful, please cite it as below." title: "qpsolvers: Quadratic Programming Solvers in Python" version: 4.11.0 date-released: 2026-03-10 url: "https://github.com/qpsolvers/qpsolvers" license: "LGPL-3.0" authors: - family-names: "Caron" given-names: "Stéphane" orcid: "https://orcid.org/0000-0003-2906-692X" - family-names: "Arnström" given-names: "Daniel" - family-names: "Bonagiri" given-names: "Suraj" - family-names: "Dechaume" given-names: "Antoine" - family-names: "Flowers" given-names: "Nikolai" - family-names: "Heins" given-names: "Adam" - family-names: "Ishikawa" given-names: "Takuma" - family-names: "Kenefake" given-names: "Dustin" - family-names: "Mazzamuto" given-names: "Giacomo" - family-names: "Meoli" given-names: "Donato" - family-names: "O'Donoghue" given-names: "Brendan" - family-names: "Oppenheimer" given-names: "Adam A." - family-names: "Otta" given-names: "Pavel" - family-names: "Pandala" given-names: "Abhishek" - family-names: "Quiroz Omaña" given-names: "Juan José" - family-names: "Rontsis" given-names: "Nikitas" - family-names: "Shah" given-names: "Paarth" - family-names: "St-Jean" given-names: "Samuel" - family-names: "Vitucci" given-names: "Nicola" - family-names: "Wolfers" given-names: "Soeren" - family-names: "Yang" given-names: "Fengyu" - family-names: "Delhaisse" given-names: "Brian" - family-names: "MeindertHH" - family-names: "rimaddo" - family-names: "urob" - family-names: "shaoanlu" - family-names: "Sandoval" given-names: "Uriel" - family-names: "Khalil" given-names: "Ahmed" - family-names: "Kozlov" given-names: "Lev" - family-names: "Groudiev" given-names: "Antoine" - family-names: "Sousa Pinto" given-names: "João" orcid: "https://orcid.org/0000-0003-2469-2809" - family-names: "Rey" given-names: "Valérian" - family-names: "Schwan" given-names: "Roland" - family-names: "Budhiraja" given-names: "Rohan" - family-names: "Keustermans" given-names: "Johannes" - family-names: "Wu" given-names: "Yubin" - family-names: "Li" given-names: "Hongpei" orcid: "https://orcid.org/0009-0000-3001-8883" ================================================ FILE: CONTRIBUTING.md ================================================ # 👷 Contributing There are many ways you can contribute to qpsolvers. Here are some ideas: - Add a [new solver](https://scaron.info/doc/qpsolvers/developer-notes.html#adding-a-new-solver) to the library, for instance one from the [wish list](https://github.com/qpsolvers/qpsolvers/issues?q=is%3Aissue+is%3Aopen+label%3A%22new+solver%22) - Describe your use case in [Show and tell](https://github.com/qpsolvers/qpsolvers/discussions/categories/show-and-tell) - Suggest improvements to the library, see for instance [these contributions](https://github.com/qpsolvers/qpsolvers/pulls?q=is%3Apr+-author%3Astephane-caron+is%3Amerged) - Find code that is [not covered](https://coveralls.io/github/qpsolvers/qpsolvers?branch=main) by unit tests, and add a test for it - Address one of the [open issues](https://github.com/qpsolvers/qpsolvers/issues?q=is%3Aissue+is%3Aopen) When you contribute a PR to the library, make sure to add yourself to `CITATION.cff` and to the BibTeX citation in the readme. ================================================ FILE: LICENSE ================================================ GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. ================================================ FILE: README.md ================================================ # Quadratic Programming Solvers in Python [![CI](https://img.shields.io/github/actions/workflow/status/qpsolvers/qpsolvers/ci.yml?branch=main)](https://github.com/qpsolvers/qpsolvers/actions) [![Documentation](https://img.shields.io/github/actions/workflow/status/qpsolvers/qpsolvers/docs.yml?branch=main&label=docs)](https://qpsolvers.github.io/qpsolvers/) [![Coverage](https://coveralls.io/repos/github/qpsolvers/qpsolvers/badge.svg?branch=main)](https://coveralls.io/github/qpsolvers/qpsolvers?branch=main) [![Conda version](https://img.shields.io/conda/vn/conda-forge/qpsolvers.svg?color=blue)](https://anaconda.org/conda-forge/qpsolvers) [![PyPI version](https://img.shields.io/pypi/v/qpsolvers?color=blue)](https://pypi.org/project/qpsolvers/) [![PyPI downloads](https://img.shields.io/pypi/dm/qpsolvers?color=blue)](https://pypistats.org/packages/qpsolvers) This library provides a [`solve_qp`](https://qpsolvers.github.io/qpsolvers/quadratic-programming.html#qpsolvers.solve_qp) function to solve convex quadratic programs: $$ \begin{split} \begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array} \end{split} $$ Vector inequalities apply coordinate by coordinate. The function returns the primal solution $x^\*$ found by the backend QP solver, or ``None`` in case of failure/unfeasible problem. All solvers require the problem to be convex, meaning the matrix $P$ should be [positive semi-definite](https://en.wikipedia.org/wiki/Definite_symmetric_matrix). Some solvers further require the problem to be strictly convex, meaning $P$ should be positive definite. **Dual multipliers:** there is also a [`solve_problem`](https://qpsolvers.github.io/qpsolvers/quadratic-programming.html#qpsolvers.solve_problem) function that returns not only the primal solution, but also its dual multipliers and all other relevant quantities computed by the backend solver. ## Example To solve a quadratic program, build the matrices that define it and call ``solve_qp``, selecting the backend QP solver via the ``solver`` keyword argument: ```python import numpy as np from qpsolvers import solve_qp M = np.array([[1.0, 2.0, 0.0], [-8.0, 3.0, 2.0], [0.0, 1.0, 1.0]]) P = M.T @ M # this is a positive definite matrix q = np.array([3.0, 2.0, 3.0]) @ M G = np.array([[1.0, 2.0, 1.0], [2.0, 0.0, 1.0], [-1.0, 2.0, -1.0]]) h = np.array([3.0, 2.0, -2.0]) A = np.array([1.0, 1.0, 1.0]) b = np.array([1.0]) x = solve_qp(P, q, G, h, A, b, solver="proxqp") print(f"QP solution: {x = }") ``` This example outputs the solution ``[0.30769231, -0.69230769, 1.38461538]``. It is also possible to get dual multipliers at the solution, as shown in [this example](https://qpsolvers.github.io/qpsolvers/quadratic-programming.html#dual-multipliers). ## Installation ### From conda-forge ```console conda install -c conda-forge qpsolvers ``` ### From PyPI To install the library with open source QP solvers: ```console pip install qpsolvers[open_source_solvers] ``` This one-size-fits-all installation may not work immediately on all systems (for instance if [a solver tries to compile from source](https://github.com/quadprog/quadprog/issues/42)). If you run into any issue, check out the following variants: - ``pip install qpsolvers[wheels_only]`` will only install solvers with pre-compiled binaries, - ``pip install qpsolvers[clarabel,daqp,proxqp,scs]`` (for instance) will install the listed set of QP solvers, - ``pip install qpsolvers`` will only install the library itself. When imported, qpsolvers loads all the solvers it can find and lists them in ``qpsolvers.available_solvers``. ## Solvers | Solver | Keyword | Algorithm | API | License | |------------------------------------------------------------------------------|-----------------| --------- | --- | ------- | | [Clarabel](https://github.com/oxfordcontrol/Clarabel.rs) | ``clarabel`` | Interior point | Sparse | Apache-2.0 | | [COPT](https://www.shanshu.ai/copt) | ``copt`` | Interior point | Sparse | Commercial | | [CVXOPT](http://cvxopt.org/) | ``cvxopt`` | Interior point | Dense | GPL-3.0 | | [DAQP](https://github.com/darnstrom/daqp) | ``daqp`` | Active set | Dense | MIT | | [ECOS](https://web.stanford.edu/~boyd/papers/ecos.html) | ``ecos`` | Interior point | Sparse | GPL-3.0 | | [Gurobi](https://www.gurobi.com/) | ``gurobi`` | Interior point | Sparse | Commercial | | [HiGHS](https://highs.dev/) | ``highs`` | Active set | Sparse | MIT | | [HPIPM](https://github.com/giaf/hpipm) | ``hpipm`` | Interior point | Dense | BSD-2-Clause | | [jaxopt.OSQP](https://jaxopt.github.io/stable/_autosummary/jaxopt.OSQP.html) | ``jaxopt_osqp`` | Augmented Lagrangian | Dense | Apache-2.0 | | [KVXOPT](https://github.com/sanurielf/kvxopt) | ``kvxopt`` | Interior point | Dense & Sparse | GPL-3.0 | | [MOSEK](https://mosek.com/) | ``mosek`` | Interior point | Sparse | Commercial | | NPPro | ``nppro`` | Active set | Dense | Commercial | | [OSQP](https://osqp.org/) | ``osqp`` | Augmented Lagrangian | Sparse | Apache-2.0 | | [PDHCG](https://github.com/Lhongpei/PDHCG-II) | ``pdhcg`` | Primal-dual hybrid gradient | Dense & Sparse | Apache-2.0 | | [PIQP](https://github.com/PREDICT-EPFL/piqp) | ``piqp`` | Proximal interior point | Dense & Sparse | BSD-2-Clause | | [ProxQP](https://github.com/Simple-Robotics/proxsuite) | ``proxqp`` | Augmented Lagrangian | Dense & Sparse | BSD-2-Clause | | [QPALM](https://github.com/kul-optec/QPALM) | ``qpalm`` | Augmented Lagrangian | Sparse | LGPL-3.0 | | [qpmad](https://github.com/asherikov/qpmad) | ``qpmad`` | Active set | Dense | Apache-2.0 | | [QTQP](https://github.com/google-deepmind/qtqp) | ``qtqp`` | Interior point | Sparse | Apache-2.0 | | [qpax](https://github.com/kevin-tracy/qpax/) | ``qpax`` | Interior point | Dense | MIT | | [qpOASES](https://github.com/coin-or/qpOASES) | ``qpoases`` | Active set | Dense | LGPL-2.1 | | [qpSWIFT](https://github.com/qpSWIFT/qpSWIFT) | ``qpswift`` | Interior point | Sparse | GPL-3.0 | | [quadprog](https://github.com/quadprog/quadprog) | ``quadprog`` | Active set | Dense | GPL-2.0 | | [SCS](https://www.cvxgrp.org/scs/) | ``scs`` | Augmented Lagrangian | Sparse | MIT | | [SIP](https://github.com/joaospinto/sip_python) | ``sip`` | Barrier Augmented Lagrangian | Sparse | MIT | Matrix arguments are NumPy arrays for dense solvers and SciPy Compressed Sparse Column (CSC) matrices for sparse ones. ## Frequently Asked Questions - [Can I print the list of solvers available on my machine?](https://github.com/qpsolvers/qpsolvers/discussions/37) - [Is it possible to solve a least squares rather than a quadratic program?](https://github.com/qpsolvers/qpsolvers/discussions/223) - [I have a squared norm in my cost function, how can I apply a QP solver to my problem?](https://github.com/qpsolvers/qpsolvers/discussions/224) - [I have a non-convex quadratic program, is there a solver I can use?](https://github.com/qpsolvers/qpsolvers/discussions/240) - [I have quadratic equality constraints, is there a solver I can use?](https://github.com/qpsolvers/qpsolvers/discussions/241) - [Error: Mircrosoft Visual C++ 14.0 or greater is required on Windows](https://github.com/qpsolvers/qpsolvers/discussions/257) - [Can I add penalty terms as in ridge regression or LASSO?](https://github.com/qpsolvers/qpsolvers/discussions/272) ## Benchmark QP solvers come with their strengths and weaknesses depending on the algorithmic choices they make. To help you find the ones most suited to your problems, you can check out the results from [`qpbenchmark`](https://github.com/qpsolvers/qpbenchmark), a benchmark for QP solvers in Python. The benchmark is divided into test sets, each test set representing a different distribution of quadratic programs with specific dimensions and structure (large sparse problems, optimal control problems, ...): - 📈 [Free-for-all test set](https://github.com/qpsolvers/free_for_all_qpbenchmark): open to all problems submitted by the community. - 📈 [Maros-Meszaros test set](https://github.com/qpsolvers/maros_meszaros_qpbenchmark): hard problems curated by the numerical optimization community. - 📈 [MPC test set](https://github.com/qpsolvers/mpc_qpbenchmark): convex model predictive control problems arising in robotics. ## Citing qpsolvers If you find this project useful, please consider giving it a :star: or citing it if your work is scientific: ```bibtex @software{qpsolvers, title = {{qpsolvers: Quadratic Programming Solvers in Python}}, author = {Caron, Stéphane and Arnström, Daniel and Bonagiri, Suraj and Dechaume, Antoine and Flowers, Nikolai and Heins, Adam and Ishikawa, Takuma and Kenefake, Dustin and Mazzamuto, Giacomo and Meoli, Donato and O'Donoghue, Brendan and Oppenheimer, Adam A. and Otta, Pavel and Pandala, Abhishek and Quiroz Omaña, Juan José and Rontsis, Nikitas and Shah, Paarth and St-Jean, Samuel and Vitucci, Nicola and Wolfers, Soeren and Yang, Fengyu and Delhaisse, Brian and MeindertHH and rimaddo and urob and shaoanlu and Sandoval, Uriel and Khalil, Ahmed and Kozlov, Lev and Groudiev, Antoine and Sousa Pinto, João and Schwan, Roland and Budhiraja, Rohan and Keustermans, Johannes and Wu, Yubin and Li, Hongpei}, license = {LGPL-3.0}, url = {https://github.com/qpsolvers/qpsolvers}, version = {4.11.0}, year = {2026} } ``` Don't forget to add yourself to the BibTeX above and to `CITATION.cff` if you contribute to this repository. ## Contributing We welcome contributions! The first step is to install the library and use it. Report any bug in the [issue tracker](https://github.com/qpsolvers/qpsolvers/issues). If you're a developer looking to hack on open source, check out the [contribution guidelines](https://github.com/qpsolvers/qpsolvers/blob/main/CONTRIBUTING.md) for suggestions. ## See also - [qpbenchmark](https://github.com/qpsolvers/qpbenchmark/): Benchmark for quadratic programming solvers available in Python. - [qpsolvers-eigen](https://github.com/ami-iit/qpsolvers-eigen): C++ abstraction layer for quadratic programming solvers using Eigen. ================================================ FILE: doc/conf.py ================================================ # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2024 Stéphane Caron and the qpsolvers contributors import re import sys from os.path import abspath, dirname, join sys.path.insert(0, abspath("..")) # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.coverage", "sphinx-mathjax-offline", "sphinx.ext.napoleon", # before sphinx_autodoc_typehints "sphinx_autodoc_typehints", ] # List of modules to be mocked up autodoc_mock_imports = [ "coptpy", "ecos", "gurobipy", "hpipm_python", "mosek", "nppro", "osqp", "pdhcg", "qpoases", "qpSWIFT", ] # Add any paths that contain templates here, relative to this directory. templates_path = [] # The suffix(es) of source filenames. source_suffix = {".rst": "restructuredtext"} # The master toctree document. master_doc = "index" # General information about the project. project = "qpsolvers" copyright = "2016-2024 Stéphane Caron and the qpsolvers contributors" author = "Stéphane Caron" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # The short X.Y version. version = None # The full version, including alpha/beta/rc tags. release = None # Read version info directly from the module's __init__.py init_path = join(dirname(dirname(str(abspath(__file__)))), "qpsolvers") with open(f"{init_path}/__init__.py", "r") as fh: for line in fh: match = re.match( r'__version__ = "((\d+)\.(\d+)\.\d+)[a-z0-9\-]*".*', line, ) if match is not None: release = f"{match.group(2)}.{match.group(3)}" version = match.group(1) assert len(release.split(".")) == 2, f"{release=}" assert len(version.split(".")) == 3, f"{version=}" break # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ["build", "Thumbs.db", ".DS_Store"] # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "furo" # Override Pygments style. pygments_style = "sphinx" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Output file base name for HTML help builder. htmlhelp_basename = "qpsolversdoc" ================================================ FILE: doc/developer-notes.rst ================================================ *************** Developer notes *************** Adding a new solver =================== Let's imagine we want to add a new solver called *AwesomeQP*. The solver keyword, the string passed via the ``solver`` keyword argument, is the lowercase version of the vernacular name of a QP solver. For our imaginary solver, the keyword is therefore ``"awesomeqp"``. The process to add AwesomeQP to *qpsolvers* goes as follows: 1. Create a new file ``qpsolvers/solvers/awesomeqp_.py`` (named after the solver keyword, with a trailing underscore) 2. Implement in this file a function ``awesomeqp_solve_problem`` that returns a :class:`.Solution` 3. Implement in the same file a function ``awesomeqp_solve_qp`` to connect it to the historical API, typically as follows: .. code:: python def awesomeqp_solve_qp(P, q, G, h, A, b, lb, ub, initvals=None, verbose=False, **kwargs): ) -> Optional[np.ndarray]: r"""Solve a quadratic program using AwesomeQP. [document parameters and return values here] """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = awesomeqp_solve_problem( problem, initvals, verbose, backend, **kwargs ) return solution.x if solution.found else None 4. Define the two function prototypes for ``awesomeqp_solve_problem`` and ``awesomeqp_solve_qp`` in ``qpsolvers/solvers/__init__.py``: .. code:: python # AwesomeQP # ======== awesome_solve_qp: Optional[ Callable[ [ ndarray, ndarray, Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[str], bool, ], Optional[ndarray], ] ] = None .. note:: The prototype needs to match the actual function. You can check its correctness by running ``tox -e py`` in the repository. 5. Below the prototype, import the function into the ``solve_function`` dictionary: .. code:: python try: from .awesomeqp_ import awesomeqp_solve_qp solve_function["awesomeqp"] = awesomeqp_solve_qp available_solvers.append("awesomeqp") # dense_solvers.append("awesomeqp") if applicable # sparse_solvers.append("awesomeqp") if applicable except ImportError: pass 6. Append the solver identifier to ``dense_solvers`` or ``sparse_solvers``, if applicable 7. Import ``awesomeqp_solve_qp`` from ``qpsolvers/__init__.py`` and add it to ``__all__`` 8. Add the solver to ``doc/src/supported-solvers.rst`` 9. Add the solver to the *Solvers* section of the README 10. Assuming AwesomeQP is distributed on `PyPI `__, add it to the ``[testenv]`` and ``[testenv:coverage]`` environments of ``tox.ini`` for unit testing 11. Assuming AwesomeQP is distributed on Conda or PyPI, add it to the list of dependencies in ``doc/environment.yml`` 12. Log the new solver as an addition in the changelog 13. If you are a new contributor, feel free to add your name to ``CITATION.cff``. Problem conversions =================== .. automodule:: qpsolvers.conversions :members: Testing locally =============== To run all CI checks locally, go to the repository folder and run .. code:: bash tox -e py This will run linters and unit tests. ================================================ FILE: doc/index.rst ================================================ .. title:: Table of Contents ######### qpsolvers ######### Unified interface to Quadratic Programming (QP) solvers available in Python. The library provides a one-stop shop :func:`.solve_qp` function with a ``solver`` keyword argument to select the backend solver. It solves :ref:`convex quadratic programs ` in standard form: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} A similar function is provided for :ref:`least squares `. .. toctree:: :maxdepth: 1 installation.rst quadratic-programming.rst least-squares.rst supported-solvers.rst unsupported-solvers.rst developer-notes.rst references.rst ================================================ FILE: doc/installation.rst ================================================ ************ Installation ************ Linux ===== Conda ----- To install the library from `conda-forge `__, simply run: .. code:: bash conda install -c conda-forge qpsolvers PyPI ---- First, install the pip package manager, for example on a recent Debian-based distribution with Python 3: .. code:: bash sudo apt install python3-dev You can then install the library by: .. code:: bash pip install qpsolvers Add the ``--user`` parameter for a user-only installation. Windows ======= Anaconda -------- - First, install the `Visual C++ Build Tools `_ - Install your Python environment, for instance `Anaconda `_ - Install the library from conda-forge, for instance in a terminal opened from the Anaconda Navigator: .. code:: bash conda install -c conda-forge qpsolvers Microsoft Visual Studio ----------------------- - Open Microsoft Visual Studio - Create a new project: - Select a new "Python Application" project template - Click "Next" - Give a name to your project - Click "Create" - Go to Tools → Python → Python Environments: - To the left of the "Python Environments" tab that opens, select a Python version >= 3.8 - Click on "Packages (PyPI)" - In the search box, type "qpsolvers" - Below the search box, click on "Run command: pip install qpsolvers" - A window pops up asking for administrator privileges: grant them - Check the text messages in the "Output" pane at the bottom of the window - Go to the main code tab (it should be your project name followed by the ".py" extension) - Copy the `example code `_ from the README and paste it there - Click on the "Run" icon in the toolbar to execute this program At this point a ``python.exe`` window should open with the following output: .. code:: bash QP solution: x = [0.30769231, -0.69230769, 1.38461538] Press any key to continue . . . Solvers ======= Open source solvers ------------------- To install at once all open source QP solvers available from the `Python Package Index `_, run the ``pip`` command as follows: .. code:: bash pip install "qpsolvers[open_source_solvers]" You can also install a subset of QP solvers of your liking, for instance: .. code:: bash pip install qpsolvers[clarabel,daqp,proxqp,scs] .. _gurobi-install: Gurobi ------ Gurobi comes with a `one-line pip installation `_ where you can fetch the solver directly from the company servers: .. code:: bash python -m pip install -i https://pypi.gurobi.com gurobipy This version comes with limitations. For instance, trying to solve a problem with 200 optimization variables fails with the following warning: .. code:: python Warning: Model too large for size-limited license; visit https://www.gurobi.com/free-trial for a full license .. _copt-install: COPT ------ COPT comes with an installation doc at `COPT installation `_ where you can install by pip: .. code:: bash python -m pip install coptpy This version comes with limitations. For instance, trying to solve a problem with 200 optimization variables fails with the following warning: .. code:: python No license found. Starting COPT with size limitations for non-commercial use Please apply for a license from www.shanshu.ai/copt .. _qpoases-install: HiGHS ----- The simplest way to install HiGHS is: .. code:: bash pip install highspy If this solution doesn't work for you, follow the `Python installation instructions `__ from the README. PDHCG ----- You can install the GPU-accelerated PDHCG solver directly from PyPI: .. code:: bash pip install pdhcg Note that PDHCG requires an NVIDIA GPU and CUDA 12.0+. If your system has multiple CUDA versions or the installation fails to find the compiler, you must explicitly point to your modern CUDA compiler using environment variables before installing: .. code:: bash export CUDACXX=/your/path/to/nvcc export SKBUILD_CMAKE_ARGS="-DCMAKE_CUDA_COMPILER=/your/path/to/nvcc" pip install pdhcg quadprog -------- You can install the quadprog solver from PyPI: .. code:: bash pip install quadprog This package comes with wheels to avoid recompiling the solver from source. qpOASES ------- The simplest way to install qpOASES is via conda-forge: .. code:: bash conda install -c conda-forge qpoases You can also check out the `official qpOASES installation page `_ for the latest release. ================================================ FILE: doc/least-squares.rst ================================================ .. _Least squares: ************* Least squares ************* To solve a linear least-squares problem, simply build the matrices that define it and call the :func:`.solve_ls` function: .. code:: python from numpy import array, dot from qpsolvers import solve_ls R = array([[1., 2., 0.], [-8., 3., 2.], [0., 1., 1.]]) s = array([3., 2., 3.]) G = array([[1., 2., 1.], [2., 0., 1.], [-1., 2., -1.]]) h = array([3., 2., -2.]).reshape((3,)) x_sol = solve_ls(R, s, G, h, solver="osqp") print(f"LS solution: {x_sol = }") The backend QP solver is selected among :ref:`supported solvers ` via the ``solver`` keyword argument. This example outputs the solution ``[0.12997217, -0.06498019, 1.74004125]``. .. autofunction:: qpsolvers.solve_ls See the ``examples/`` folder in the repository for more advanced use cases. For a more general introduction you can also check out this post on `least squares in Python `_. ================================================ FILE: doc/quadratic-programming.rst ================================================ .. _Quadratic programming: ********************* Quadratic programming ********************* Primal problem ============== A quadratic program is defined in standard form as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} The vectors :math:`lb` and :math:`ub` can contain :math:`\pm \infty` values to disable bounds on some coordinates. To solve such a problem, build the matrices that define it and call the :func:`.solve_qp` function: .. code:: python from numpy import array, dot from qpsolvers import solve_qp M = array([[1., 2., 0.], [-8., 3., 2.], [0., 1., 1.]]) P = dot(M.T, M) # quick way to build a symmetric matrix q = dot(array([3., 2., 3.]), M).reshape((3,)) G = array([[1., 2., 1.], [2., 0., 1.], [-1., 2., -1.]]) h = array([3., 2., -2.]).reshape((3,)) A = array([1., 1., 1.]) b = array([1.]) x = solve_qp(P, q, G, h, A, b, solver="osqp") print(f"QP solution: x = {x}") The backend QP solver is selected among :ref:`supported solvers ` via the ``solver`` keyword argument. This example outputs the solution ``[0.30769231, -0.69230769, 1.38461538]``. .. autofunction:: qpsolvers.solve_qp See the ``examples/`` folder in the repository for more use cases. For a more general introduction you can also check out this post on `quadratic programming in Python `_. Problem class ============= Alternatively, we can define the matrices and vectors using the :class:`.Problem` class: .. autoclass:: qpsolvers.problem.Problem :members: The solve function corresponding to :class:`.Problem` is :func:`.solve_problem` rather than :func:`.solve_qp`. Dual multipliers ================ The dual of the quadratic program defined above can be written as: .. math:: \begin{split}\begin{array}{ll} \underset{x, z, y, z_{\mathit{box}}}{\mbox{maximize}} & -\frac{1}{2} x^T P x - h^T z - b^T y - lb^T z_{\mathit{box}}^- - ub^T z_{\mathit{box}}^+ \\ \mbox{subject to} & P x + G^T z + A^T y + z_{\mathit{box}} + q = 0 \\ & z \geq 0 \end{array}\end{split} were :math:`v^- = \min(v, 0)` and :math:`v^+ = \max(v, 0)`. To solve both a problem and its dual, getting a full primal-dual solution :math:`(x^*, z^*, y^*, z_\mathit{box}^*)`, build a :class:`.Problem` and call the :func:`.solve_problem` function: .. code:: python import numpy as np from qpsolvers import Problem, solve_problem M = np.array([[1., 2., 0.], [-8., 3., 2.], [0., 1., 1.]]) P = M.T.dot(M) # quick way to build a symmetric matrix q = np.array([3., 2., 3.]).dot(M).reshape((3,)) G = np.array([[1., 2., 1.], [2., 0., 1.], [-1., 2., -1.]]) h = np.array([3., 2., -2.]).reshape((3,)) A = np.array([1., 1., 1.]) b = np.array([1.]) lb = -0.6 * np.ones(3) ub = +0.7 * np.ones(3) problem = Problem(P, q, G, h, A, b, lb, ub) solution = solve_problem(problem, solver="proxqp") print(f"Primal: x = {solution.x}") print(f"Dual (Gx <= h): z = {solution.z}") print(f"Dual (Ax == b): y = {solution.y}") print(f"Dual (lb <= x <= ub): z_box = {solution.z_box}") The function returns a :class:`.Solution` with both primal and dual vectors. This example outputs the following solution: .. code:: Primal: x = [ 0.63333169 -0.33333307 0.70000137] Dual (Gx <= h): z = [0. 0. 7.66660538] Dual (Ax == b): y = [-16.63326017] Dual (lb <= x <= ub): z_box = [ 0. 0. 26.26649724] .. autofunction:: qpsolvers.solve_problem See the ``examples/`` folder in the repository for more use cases. For an introduction to dual multipliers you can also check out this post on `optimality conditions and numerical tolerances in QP solvers `_. Optimality of a solution ======================== The :class:`.Solution` class describes the solution found by a solver to a given problem. It is linked to the corresponding :class:`.Problem`, which it can use for instance to check residuals. We can for instance check the optimality of the solution returned by a solver with: .. code:: python import numpy as np from qpsolvers import Problem, solve_problem M = np.array([[1., 2., 0.], [-8., 3., 2.], [0., 1., 1.]]) P = M.T.dot(M) # quick way to build a symmetric matrix q = np.array([3., 2., 3.]).dot(M).reshape((3,)) G = np.array([[1., 2., 1.], [2., 0., 1.], [-1., 2., -1.]]) h = np.array([3., 2., -2.]).reshape((3,)) A = np.array([1., 1., 1.]) b = np.array([1.]) lb = -0.6 * np.ones(3) ub = +0.7 * np.ones(3) problem = Problem(P, q, G, h, A, b, lb, ub) solution = solve_problem(problem, solver="qpalm") print(f"- Solution is{'' if solution.is_optimal(1e-8) else ' NOT'} optimal") print(f"- Primal residual: {solution.primal_residual():.1e}") print(f"- Dual residual: {solution.dual_residual():.1e}") print(f"- Duality gap: {solution.duality_gap():.1e}") This example prints: .. code:: - Solution is optimal - Primal residual: 1.1e-16 - Dual residual: 1.4e-14 - Duality gap: 0.0e+00 You can check out [Caron2022]_ for an overview of optimality conditions and why a solution is optimal if and only if these three residuals are zero. .. autoclass:: qpsolvers.solution.Solution :members: ================================================ FILE: doc/references.rst ================================================ ********** References ********** .. [Tracy2024] `On the Differentiability of the Primal-Dual Interior-Point Method `_, K. Tracy and Z. Manchester. ArXiv, 2024. .. [Schwan2023] `PIQP: A Proximal Interior-Point Quadratic Programming Solver `_, R. Schwan, Y. Jiang, D. Kuhn, C.N. Jones. ArXiv, 2023. .. [Arnstrom2022] `A dual active-set solver for embedded quadratic programming using recursive LDL updates `_, D. Arnström, A. Bemporad and D. Axehill. IEEE Transactions on Automatic Control, 2022, 67, no. 8 p. 4362-4369. .. [Bambade2022] `PROX-QP: Yet another Quadratic Programming Solver for Robotics and beyond `__, A. Bambade, S. El-Kazdadi, A. Taylor and J. Carpentier. Robotics: Science and Systems. 2022. .. [Caron2022] `Optimality conditions and numerical tolerances in QP solvers `_, S. Caron, 2022. .. [Hermans2022] `QPALM: A Newton-type Proximal Augmented Lagrangian Method for Quadratic Programs `_, B. Hermans, A. Themelis and P. Patrinos. Mathematical Programming Computation, 2022, vol. 14, no 3, p. 497-541. .. [ODonoghue2021] `Operator splitting for a homogeneous embedding of the linear complementarity problem `_, B. O'Donoghue. SIAM Journal on Optimization, 2021, vol. 31, no 3, p. 1999-2023. .. [Frison2020] `HPIPM: a high-performance quadratic programming framework for model predictive control `__, G. Frison and M. Diehl. IFAC-PapersOnline, 2020, vol. 53, no 2, p. 6563-6569. .. [Stellato2020] `OSQP: An Operator Splitting Solver for Quadratic Programs `__, B. Stellato, G. Banjac, P. Goulart, A. Bemporad, and S. Boyd. Mathematical Programming Computation, 2020, vol. 12, no 4, p. 637-672. .. [Pandala2019] `qpSWIFT: A real-time sparse quadratic program solver for robotic applications `_, A. G. Pandala, Y. Ding and H. W. Park. IEEE Robotics and Automation Letters, 2019, vol. 4, no 4, p. 3355-3362. .. [Huangfu2018] *Parallelizing the dual revised simplex method*. Q. Huangfu and J. Hall. Mathematical Programming Computation, 2018, vol. 10, no 1, p. 119-142. .. [Ferreau2014] `qpOASES: A parametric active-set algorithm for quadratic programming `_, H. J. Ferreau, C. Kirches, A. Potschka, H. G. Bock and M. Diehl. Mathematical Programming Computation, 2014, vol. 6, no 4, p. 327-363. .. [Domahidi2013] `ECOS: An SOCP solver for embedded systems `_, A. Domahidi, E. Chu and S. Boyd. European Control Conference. IEEE, 2013. p. 3071-3076. .. [Vandenberghe2010] `The CVXOPT linear and quadratic cone program solvers `_, L. Vandenberghe. 2010. .. [Goldfarb1983] *A numerically stable dual method for solving strictly convex quadratic programs*. D. Goldfarb and A. Idnani. Mathematical Programming, vol. 27, p. 1-33. ================================================ FILE: doc/supported-solvers.rst ================================================ .. _Supported solvers: ***************** Supported solvers ***************** Solvers that are detected as installed on your machine are listed in: .. autodata:: qpsolvers.available_solvers Clarabel ======== .. automodule:: qpsolvers.solvers.clarabel_ :members: COPT ======== .. automodule:: qpsolvers.solvers.copt_ :members: CVXOPT ====== .. automodule:: qpsolvers.solvers.cvxopt_ :members: DAQP ====== .. automodule:: qpsolvers.solvers.daqp_ :members: ECOS ==== .. automodule:: qpsolvers.solvers.ecos_ :members: Gurobi ====== .. automodule:: qpsolvers.solvers.gurobi_ :members: HiGHS ===== .. automodule:: qpsolvers.solvers.highs_ :members: HPIPM ===== .. automodule:: qpsolvers.solvers.hpipm_ :members: jaxopt.OSQP =========== .. automodule:: qpsolvers.solvers.jaxopt_osqp_ :members: KVXOPT ====== .. automodule:: qpsolvers.solvers.kvxopt_ :members: MOSEK ===== .. automodule:: qpsolvers.solvers.mosek_ :members: OSQP ==== .. automodule:: qpsolvers.solvers.osqp_ :members: PIQP ====== .. automodule:: qpsolvers.solvers.piqp_ :members: ProxQP ====== .. automodule:: qpsolvers.solvers.proxqp_ :members: pyqpmad ======= .. automodule:: qpsolvers.solvers.pyqpmad_ :members: QPALM ===== .. automodule:: qpsolvers.solvers.qpalm_ :members: qpOASES ======= .. automodule:: qpsolvers.solvers.qpoases_ :members: qpSWIFT ======= .. automodule:: qpsolvers.solvers.qpswift_ :members: qpax === .. automodule:: qpsolvers.solvers.qpax_ :members: QTQP ==== .. automodule:: qpsolvers.solvers.qtqp_ :members: quadprog ======== .. automodule:: qpsolvers.solvers.quadprog_ :members: SCS === .. automodule:: qpsolvers.solvers.scs_ :members: SIP === .. automodule:: qpsolvers.solvers.sip_ :members: ================================================ FILE: doc/unsupported-solvers.rst ================================================ ******************* Unsupported solvers ******************* Unsupported solvers will be made available if they are detected on your system, but their performance is not guaranteed as they are not part of `continuous integration `__ (typically because they are not open source). PDHCG ===== .. automodule:: qpsolvers.solvers.pdhcg_ :members: NPPro ===== .. automodule:: qpsolvers.solvers.nppro_ :members: ================================================ FILE: examples/README.md ================================================ # Quadratic programming examples Examples are roughly sorted from simple to complex. The basic ones are: - [Quadratic program](quadratic_program.py) - [Linear least squares](least_squares.py) - [Box inequalities](box_inequalities.py) - [Sparse linear least squares](sparse_least_squares.py) For more advance use cases, check out: - [Dual multipliers](dual_multipliers.py) ## Applications Feel free to share yours in [Show and tell](https://github.com/qpsolvers/qpsolvers/discussions/categories/show-and-tell)! - [Constrained linear regression](constrained_linear_regression.py) - [Model predictive control for humanoid locomotion](model_predictive_control.py) ================================================ FILE: examples/box_inequalities.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Test an available QP solvers on a small problem with box inequalities.""" import random from time import perf_counter import numpy as np from qpsolvers import available_solvers, print_matrix_vector, solve_qp M = np.array([[1.0, 2.0, 0.0], [-8.0, 3.0, 2.0], [0.0, 1.0, 1.0]]) P = np.dot(M.T, M) # this is a positive definite matrix q = np.dot(np.array([3.0, 2.0, 3.0]), M) A = np.array([1.0, 1.0, 1.0]) b = np.array([1.0]) lb = -0.5 * np.ones(3) ub = 1.0 * np.ones(3) x_sol = np.array([0.41463414566726164, -0.41463414566726164, 1.0]) if __name__ == "__main__": start_time = perf_counter() solver = random.choice(available_solvers) x = solve_qp(P, q, A=A, b=b, lb=lb, ub=ub, solver=solver) end_time = perf_counter() print("") print(" min. 1/2 x^T P x + q^T x") print(" s.t. A * x == b") print(" lb <= x <= ub") print("") print_matrix_vector(P, "P", q, "q") print("") print_matrix_vector(A, "A", b, "b") print("") print_matrix_vector(lb.reshape((3, 1)), "lb", ub, "ub") print("") print(f"Solution: x = {x}") print(f"It should be close to x* = {x_sol}") print(f"Found in {1e6 * (end_time - start_time):.0f} [us] with {solver}") ================================================ FILE: examples/constrained_linear_regression.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Test a random QP solver on a constrained linear regression problem. This example originates from: https://stackoverflow.com/a/74422084 See also: https://scaron.info/blog/simple-linear-regression-with-online-updates.html """ import random import numpy as np import qpsolvers from qpsolvers import solve_ls a = np.array([1.2, 2.3, 4.2]) b = np.array([1.0, 5.0, 6.0]) c = np.array([5.4, 6.2, 1.9]) m = np.vstack([a, b, c]) y = np.array([5.3, 0.9, 5.6]) # Objective: || [a b c] x - y ||^2 R = m.T s = y # Constraint: sum(x) = 1 A = np.ones((1, 3)) b = np.array([1.0]) # Constraint: x >= 0 lb = np.zeros(3) if __name__ == "__main__": solver = random.choice(qpsolvers.available_solvers) x = solve_ls(R, s, A=A, b=b, lb=lb, solver=solver) print(f"Found solution {x=}") ================================================ FILE: examples/dual_multipliers.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Get both primal and dual solutions to a quadratic program.""" import random import numpy as np from qpsolvers import ( Problem, available_solvers, print_matrix_vector, solve_problem, ) M = np.array([[1.0, 2.0, 0.0], [-8.0, 3.0, 2.0], [0.0, 1.0, 1.0]]) P = np.dot(M.T, M) # this is a positive definite matrix q = np.dot(np.array([3.0, 2.0, 3.0]), M) G = np.array([[4.0, 2.0, 0.0], [-1.0, 2.0, -1.0]]) h = np.array([1.0, -2.0]) A = np.array([1.0, 1.0, 1.0]).reshape((1, 3)) b = np.array([1.0]) lb = np.array([-0.5, -0.4, -0.5]) ub = np.array([1.0, 1.0, 1.0]) if __name__ == "__main__": solver = random.choice(available_solvers) problem = Problem(P, q, G, h, A, b, lb, ub) solution = solve_problem(problem, solver) print("========================= PRIMAL PROBLEM =========================") print("") print(" min. 1/2 x^T P x + q^T x") print(" s.t. G x <= h") print(" A x == b") print(" lb <= x <= ub") print("") print_matrix_vector(P, "P", q, "q") print("") print_matrix_vector(G, "G", h, "h") print("") print_matrix_vector(A, "A", b, "b") print("") print_matrix_vector(lb.reshape((3, 1)), "lb", ub, "ub") print("") print("============================ SOLUTION ============================") print("") print(f'Found with solver="{solver}"') print("") print_matrix_vector( solution.x.reshape((3, 1)), "Primal x*", solution.z, "Dual (Gx <= h) z*", ) print("") print_matrix_vector( solution.y.reshape((1, 1)), "Dual (Ax == b) y*", solution.z_box.reshape((3, 1)), "Dual (lb <= x <= ub) z_box*", ) print("") print("=== Optimality checks ===") print(f"- Primal residual: {solution.primal_residual():.1e}") print(f"- Dual residual: {solution.dual_residual():.1e}") print(f"- Duality gap: {solution.duality_gap():.1e}") print("") ================================================ FILE: examples/lasso_regularization.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Apply lasso regularization to a quadratic program. Details: https://scaron.info/blog/lasso-regularization-in-quadratic-programming.html """ import numpy as np from qpsolvers import solve_qp # Objective: || R x - s ||^2 n = 6 R = np.diag(range(1, n + 1)) s = np.ones(n) # Convert our least-squares objective to quadratic programming P = np.dot(R.transpose(), R) q = -np.dot(s.transpose(), R) # Linear inequality constraints: G x <= h G = np.array( [ [1.0, 0.0] * (n // 2), [0.0, 1.0] * (n // 2), ] ) h = np.array([10.0, -10.0]) # Lasso parameter t: float = 10.0 # Lasso: inequality constraints G_lasso = np.vstack( [ np.hstack([G, np.zeros((G.shape[0], n))]), np.hstack([+np.eye(n), -np.eye(n)]), np.hstack([-np.eye(n), -np.eye(n)]), np.hstack([np.zeros((1, n)), np.ones((1, n))]), ] ) h_lasso = np.hstack([h, np.zeros(n), np.zeros(n), t]) # Lasso: objective P_lasso = np.vstack( [ np.hstack([P, np.zeros((n, n))]), np.zeros((n, 2 * n)), ] ) q_lasso = np.hstack([q, np.ones(n)]) if __name__ == "__main__": x_unreg = solve_qp(P, q, G, h, solver="proxqp") print(f"Solution without lasso: {x_unreg = }") lasso_res = solve_qp(P_lasso, q_lasso, G_lasso, h_lasso, solver="proxqp") x_lasso = lasso_res[:n] z_lasso = lasso_res[n:] print(f"Solution with lasso ({t=}): {x_lasso = }") print(f"We can check that abs(x_lasso) = {z_lasso = }") ================================================ FILE: examples/least_squares.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Test a random available QP solver on a small least-squares problem.""" import random from time import perf_counter import numpy as np import qpsolvers from qpsolvers import print_matrix_vector, solve_ls R = np.array([[1.0, 2.0, 0.0], [-8.0, 3.0, 2.0], [0.0, 1.0, 1.0]]) s = np.array([3.0, 2.0, 3.0]) G = np.array([[1.0, 2.0, 1.0], [2.0, 0.0, 1.0], [-1.0, 2.0, -1.0]]) h = np.array([3.0, 2.0, -2.0]) x_sol = np.array([0.1299734765610818, -0.0649867382805409, 1.7400530468778364]) if __name__ == "__main__": start_time = perf_counter() solver = random.choice(qpsolvers.available_solvers) x = solve_ls(R, s, G, h, solver=solver, verbose=False) end_time = perf_counter() print("") print(" min. || R * x - s ||^2") print(" s.t. G * x <= h") print("") print_matrix_vector(R, "R", s, "s") print("") print_matrix_vector(G, "G", h, "h") print("") print(f"Solution: x = {x}") print(f"It should be close to x* = {x_sol}") print(f"Found in {1e6 * (end_time - start_time):.0f} [us] with {solver}") ================================================ FILE: examples/model_predictive_control.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Test the "quadprog" QP solver on a model predictive control problem. The problem is to balance a humanoid robot walking on a flat horizontal floor. See the following post for context: https://scaron.info/robot-locomotion/prototyping-a-walking-pattern-generator.html """ import random from dataclasses import dataclass from typing import Optional import numpy as np import matplotlib.pyplot as plt from scipy.sparse import csc_matrix import qpsolvers from qpsolvers import solve_qp gravity = 9.81 # [m] / [s]^2 @dataclass class HumanoidSteppingProblem: com_height: float = 0.8 dsp_duration: float = 0.1 end_pos: float = 0.3 foot_length: float = 0.1 horizon_duration: float = 2.5 nb_timesteps: int = 16 ssp_duration: float = 0.7 start_pos: float = 0.0 class LinearModelPredictiveControl: """ Linear model predictive control for a system with linear dynamics and linear constraints. This class is fully documented at: https://scaron.info/doc/pymanoid/walking-pattern-generation.html#pymanoid.mpc.LinearPredictiveControl """ def __init__( self, A, B, C, D, e, x_init, x_goal, nb_timesteps: int, wxt: Optional[float], wxc: Optional[float], wu: float, ): assert C is not None or D is not None, "use LQR for unconstrained case" assert ( wu > 0.0 ), "non-negative control weight needed for regularization" assert wxt is not None or wxc is not None, "set either wxt or wxc" u_dim = B.shape[1] x_dim = A.shape[1] self.A = A self.B = B self.C = C self.D = D self.G = None self.P = None self.U = None self.U_dim = u_dim * nb_timesteps self.e = e self.h = None self.nb_timesteps = nb_timesteps self.q = None self.u_dim = u_dim self.wu = wu self.wxc = wxc self.wxt = wxt self.x_dim = x_dim self.x_goal = x_goal self.x_init = x_init # self.build() def build(self): phi = np.eye(self.x_dim) psi = np.zeros((self.x_dim, self.U_dim)) G_list, h_list = [], [] phi_list, psi_list = [], [] for k in range(self.nb_timesteps): # Loop invariant: x == psi * U + phi * x_init if self.wxc is not None: phi_list.append(phi) psi_list.append(psi) C = self.C[k] if type(self.C) is list else self.C D = self.D[k] if type(self.D) is list else self.D e = self.e[k] if type(self.e) is list else self.e G = np.zeros((e.shape[0], self.U_dim)) h = e if C is None else e - np.dot(C.dot(phi), self.x_init) if D is not None: # we rely on G == 0 to avoid a slower += G[:, k * self.u_dim : (k + 1) * self.u_dim] = D if C is not None: G += C.dot(psi) if k == 0 and D is None: # corner case, input has no effect assert np.all(h >= 0.0) else: # regular case G_list.append(G) h_list.append(h) phi = self.A.dot(phi) psi = self.A.dot(psi) psi[:, self.u_dim * k : self.u_dim * (k + 1)] = self.B P = self.wu * np.eye(self.U_dim) q = np.zeros(self.U_dim) if self.wxt is not None and self.wxt > 1e-10: c = np.dot(phi, self.x_init) - self.x_goal P += self.wxt * np.dot(psi.T, psi) q += self.wxt * np.dot(c.T, psi) if self.wxc is not None and self.wxc > 1e-10: Phi = np.vstack(phi_list) Psi = np.vstack(psi_list) X_goal = np.hstack([self.x_goal] * self.nb_timesteps) c = np.dot(Phi, self.x_init) - X_goal P += self.wxc * np.dot(Psi.T, Psi) q += self.wxc * np.dot(c.T, Psi) self.P = P self.q = q self.G = np.vstack(G_list) self.h = np.hstack(h_list) self.P_csc = csc_matrix(self.P) self.G_csc = csc_matrix(self.G) def solve(self, solver: str, sparse: bool = False, **kwargs): P = self.P_csc if sparse else self.P G = self.G_csc if sparse else self.G U = solve_qp(P, self.q, G, self.h, solver=solver, **kwargs) self.U = U.reshape((self.nb_timesteps, self.u_dim)) @property def states(self): assert self.U is not None, "you need to solve() the MPC problem first" X = np.zeros((self.nb_timesteps + 1, self.x_dim)) X[0] = self.x_init for k in range(self.nb_timesteps): X[k + 1] = self.A.dot(X[k]) + self.B.dot(self.U[k]) return X class HumanoidModelPredictiveControl(LinearModelPredictiveControl): def __init__(self, problem: HumanoidSteppingProblem): T = problem.horizon_duration / problem.nb_timesteps nb_init_dsp_steps = int(round(problem.dsp_duration / T)) nb_init_ssp_steps = int(round(problem.ssp_duration / T)) nb_dsp_steps = int(round(problem.dsp_duration / T)) state_matrix = np.array( [[1.0, T, T ** 2 / 2.0], [0.0, 1.0, T], [0.0, 0.0, 1.0]] ) control_matrix = np.array([T ** 3 / 6.0, T ** 2 / 2.0, T]) control_matrix = control_matrix.reshape((3, 1)) zmp_from_state = np.array([1.0, 0.0, -problem.com_height / gravity]) ineq_matrix = np.array([+zmp_from_state, -zmp_from_state]) cur_max = problem.start_pos + 0.5 * problem.foot_length cur_min = problem.start_pos - 0.5 * problem.foot_length next_max = problem.end_pos + 0.5 * problem.foot_length next_min = problem.end_pos - 0.5 * problem.foot_length ineq_vector = [ np.array([+1000.0, +1000.0]) if i < nb_init_dsp_steps else np.array([+cur_max, -cur_min]) if i - nb_init_dsp_steps <= nb_init_ssp_steps else np.array([+1000.0, +1000.0]) if i - nb_init_dsp_steps - nb_init_ssp_steps < nb_dsp_steps else np.array([+next_max, -next_min]) for i in range(problem.nb_timesteps) ] super().__init__( state_matrix, control_matrix, ineq_matrix, None, ineq_vector, x_init=np.array([problem.start_pos, 0.0, 0.0]), x_goal=np.array([problem.end_pos, 0.0, 0.0]), nb_timesteps=problem.nb_timesteps, wxt=1.0, wxc=None, wu=1e-3, ) def plot_mpc_solution(problem, mpc): t = np.linspace(0.0, problem.horizon_duration, problem.nb_timesteps + 1) X = mpc.states zmp_from_state = np.array([1.0, 0.0, -problem.com_height / gravity]) zmp = X.dot(zmp_from_state) pos = X[:, 0] zmp_min = [x[0] if abs(x[0]) < 10 else None for x in mpc.e] zmp_max = [-x[1] if abs(x[1]) < 10 else None for x in mpc.e] zmp_min.append(zmp_min[-1]) zmp_max.append(zmp_max[-1]) plt.ion() plt.clf() plt.plot(t, pos) plt.plot(t, zmp, "r-") plt.plot(t, zmp_min, "g:") plt.plot(t, zmp_max, "b:") plt.grid(True) plt.show(block=True) if __name__ == "__main__": problem = HumanoidSteppingProblem() mpc = HumanoidModelPredictiveControl(problem) mpc.solve(solver=random.choice(qpsolvers.available_solvers)) plot_mpc_solution(problem, mpc) ================================================ FILE: examples/quadratic_program.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Test the "quadprog" QP solver on a small dense problem.""" import random from time import perf_counter import numpy as np from qpsolvers import available_solvers, print_matrix_vector, solve_qp M = np.array([[1.0, 2.0, 0.0], [-8.0, 3.0, 2.0], [0.0, 1.0, 1.0]]) P = np.dot(M.T, M) # this is a positive definite matrix q = np.dot(np.array([3.0, 2.0, 3.0]), M) G = np.array([[1.0, 2.0, 1.0], [2.0, 0.0, 1.0], [-1.0, 2.0, -1.0]]) h = np.array([3.0, 2.0, -2.0]) A = np.array([1.0, 1.0, 1.0]) b = np.array([1.0]) x_sol = np.array([0.3076923111580727, -0.6923076888419274, 1.3846153776838548]) if __name__ == "__main__": start_time = perf_counter() solver = random.choice(available_solvers) x = solve_qp(P, q, G, h, A, b, solver=solver) end_time = perf_counter() print("") print(" min. 1/2 x^T P x + q^T x") print(" s.t. G * x <= h") print(" A * x == b") print("") print_matrix_vector(P, "P", q, "q") print("") print_matrix_vector(G, "G", h, "h") print("") print_matrix_vector(A, "A", b, "b") print("") print(f"Solution: x = {x}") print(f"It should be close to x* = {x_sol}") print(f"Found in {1e6 * (end_time - start_time):.0f} [us] with {solver}") ================================================ FILE: examples/sparse_least_squares.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Test a random sparse QP solver on a sparse least-squares problem. See also: https://stackoverflow.com/a/74415546/3721564 """ import random from time import perf_counter import qpsolvers from qpsolvers import solve_ls from qpsolvers.problems import get_sparse_least_squares if __name__ == "__main__": solver = random.choice(qpsolvers.sparse_solvers) R, s, G, h, A, b, lb, ub = get_sparse_least_squares(n=150_000) start_time = perf_counter() x = solve_ls( R, s, G, h, A, b, lb, ub, solver=solver, verbose=False, sparse_conversion=True, ) end_time = perf_counter() duration_ms = 1e3 * (end_time - start_time) tol = 1e-6 # tolerance for checks print("") print(" min. || x - s ||^2") print(" s.t. G * x <= h") print(" sum(x) = 42") print(" 0 <= x") print("") print(f"Found solution in {duration_ms:.0f} milliseconds with {solver}") print("") print(f"- Objective: {0.5 * (x - s).dot(x - s):.1f}") print(f"- G * x <= h: {(G.dot(x) <= h + tol).all()}") print(f"- x >= 0: {(x + tol >= 0.0).all()}") print(f"- sum(x) = {x.sum():.1f}") print("") ================================================ FILE: examples/test_dense_problem.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Test all available QP solvers on a dense quadratic program.""" from os.path import basename from IPython import get_ipython from numpy import array, dot from qpsolvers import dense_solvers, solve_qp, sparse_solvers from scipy.sparse import csc_matrix M = array([[1.0, 2.0, 0.0], [-8.0, 3.0, 2.0], [0.0, 1.0, 1.0]]) P = dot(M.T, M) q = dot(array([3.0, 2.0, 3.0]), M) G = array([[1.0, 2.0, 1.0], [2.0, 0.0, 1.0], [-1.0, 2.0, -1.0]]) h = array([3.0, 2.0, -2.0]) P_csc = csc_matrix(P) G_csc = csc_matrix(G) if __name__ == "__main__": if get_ipython() is None: print( "This example should be run with IPython:\n\n" f"\tipython -i {basename(__file__)}\n" ) exit() dense_instr = { solver: f"u = solve_qp(P, q, G, h, solver='{solver}')" for solver in dense_solvers } sparse_instr = { solver: f"u = solve_qp(P_csc, q, G_csc, h, solver='{solver}')" for solver in sparse_solvers } benchmark = "https://github.com/qpsolvers/qpbenchmark" print("\nTesting all QP solvers on one given dense quadratic program") print(f"For a proper benchmark, check out {benchmark}") sol0 = solve_qp(P, q, G, h, solver=dense_solvers[0]) abstol = 2e-4 # tolerance on absolute solution error for solver in dense_solvers: sol = solve_qp(P, q, G, h, solver=solver) for solver in sparse_solvers: sol = solve_qp(P_csc, q, G_csc, h, solver=solver) print("\nDense solvers\n-------------") for solver, instr in dense_instr.items(): print(f"{solver}: ", end="") get_ipython().run_line_magic("timeit", instr) print("\nSparse solvers\n--------------") for solver, instr in sparse_instr.items(): print(f"{solver}: ", end="") get_ipython().run_line_magic("timeit", instr) ================================================ FILE: examples/test_model_predictive_control.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Test all available QP solvers on a model predictive control problem.""" from os.path import basename from IPython import get_ipython from qpsolvers import dense_solvers, sparse_solvers from model_predictive_control import ( HumanoidModelPredictiveControl, HumanoidSteppingProblem, ) problem = HumanoidSteppingProblem() mpc = HumanoidModelPredictiveControl(problem) if __name__ == "__main__": if get_ipython() is None: print( "This example should be run with IPython:\n\n" f"\tipython -i {basename(__file__)}\n" ) exit() dense_instr = { solver: f"u = mpc.solve(solver='{solver}', sparse=False)" for solver in dense_solvers } sparse_instr = { solver: f"u = mpc.solve(solver='{solver}', sparse=True)" for solver in sparse_solvers } benchmark = "https://github.com/qpsolvers/qpbenchmark" print("\nTesting QP solvers on one given model predictive control problem") print(f"For a proper benchmark, check out {benchmark}") print("\nDense solvers\n-------------") for solver, instr in dense_instr.items(): print(f"{solver}: ", end="") get_ipython().run_line_magic("timeit", instr) print("\nSparse solvers\n--------------") for solver, instr in sparse_instr.items(): print(f"{solver}: ", end="") get_ipython().run_line_magic("timeit", instr) ================================================ FILE: examples/test_random_problems.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Test all available QP solvers on random quadratic programs.""" import sys try: from IPython import get_ipython except ImportError: print("This example requires IPython, try installing ipython3") sys.exit(-1) from os.path import basename from timeit import timeit from numpy import dot, linspace, ones, random from qpsolvers import available_solvers, solve_qp from scipy.linalg import toeplitz nb_iter = 10 sizes = [10, 20, 50, 100, 200, 500, 1000, 2000] def solve_random_qp(n, solver): M, b = random.random((n, n)), random.random(n) P, q = dot(M.T, M), dot(b, M) G = toeplitz( [1.0, 0.0, 0.0] + [0.0] * (n - 3), [1.0, 2.0, 3.0] + [0.0] * (n - 3) ) h = ones(n) return solve_qp(P, q, G, h, solver=solver) def plot_results(perfs): try: from pylab import ( clf, get_cmap, grid, ion, legend, plot, xlabel, xscale, ylabel, yscale, ) except ImportError: print("Cannot plot results, try installing python3-matplotlib") print("Results are stored in the global `perfs` dictionary") return cmap = get_cmap("tab10") colors = cmap(linspace(0, 1, len(available_solvers))) solver_color = { solver: colors[i] for i, solver in enumerate(available_solvers) } ion() clf() for solver in perfs: plot(sizes, perfs[solver], lw=2, color=solver_color[solver]) grid(True) legend(list(perfs.keys()), loc="lower right") xscale("log") yscale("log") xlabel("Problem size $n$") ylabel("Time (s)") for solver in perfs: plot(sizes, perfs[solver], marker="o", color=solver_color[solver]) if __name__ == "__main__": if get_ipython() is None: print( "This example should be run with IPython:\n\n" f"\tipython -i {basename(__file__)}\n" ) exit() perfs = {} benchmark = "https://github.com/qpsolvers/qpbenchmark" print("\nTesting all solvers on a given set of random QPs") print(f"For a proper benchmark, check out {benchmark}") for solver in available_solvers: try: perfs[solver] = [] for size in sizes: print(f"Running {solver} on problem size {size}...") cum_time = timeit( stmt=f"solve_random_qp({size}, '{solver}')", setup="from __main__ import solve_random_qp", number=nb_iter, ) perfs[solver].append(cum_time / nb_iter) except Exception as e: print(f"Warning: {str(e)}") if solver in perfs: del perfs[solver] plot_results(perfs) ================================================ FILE: examples/test_sparse_problem.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Test all available QP solvers on a sparse quadratic program.""" from os.path import basename import numpy as np import scipy.sparse from IPython import get_ipython from numpy.linalg import norm from scipy.sparse import csc_matrix from qpsolvers import dense_solvers, solve_qp, sparse_solvers n = 500 M = scipy.sparse.lil_matrix(scipy.sparse.eye(n)) for i in range(1, n - 1): M[i, i + 1] = -1 M[i, i - 1] = 1 P = csc_matrix(M.dot(M.transpose())) q = -np.ones((n,)) G = csc_matrix(-scipy.sparse.eye(n)) h = -2 * np.ones((n,)) P_array = np.array(P.todense()) G_array = np.array(G.todense()) def check_same_solutions(tol=0.05): sol0 = solve_qp(P, q, G, h, solver=sparse_solvers[0]) for solver in sparse_solvers: sol = solve_qp(P, q, G, h, solver=solver) relvar = norm(sol - sol0) / norm(sol0) assert ( relvar < tol ), f"{solver}'s solution offset by {100.0 * relvar:.1f}%" for solver in dense_solvers: sol = solve_qp(P_array, q, G_array, h, solver=solver) relvar = norm(sol - sol0) / norm(sol0) assert ( relvar < tol ), f"{solver}'s solution offset by {100.0 * relvar:.1f}%" def time_dense_solvers(): instructions = { solver: f"u = solve_qp(P_array, q, G_array, h, solver='{solver}')" for solver in dense_solvers } print("\nDense solvers\n-------------") for solver, instr in instructions.items(): print(f"{solver}: ", end="") get_ipython().run_line_magic("timeit", instr) def time_sparse_solvers(): instructions = { solver: f"u = solve_qp(P, q, G, h, solver='{solver}')" for solver in sparse_solvers } print("\nSparse solvers\n--------------") for solver, instr in instructions.items(): print(f"{solver}: ", end="") get_ipython().run_line_magic("timeit", instr) if __name__ == "__main__": if get_ipython() is None: print( "This example should be run with IPython:\n\n" f"\tipython -i {basename(__file__)}\n" ) exit() benchmark = "https://github.com/qpsolvers/qpbenchmark" print("\nTesting all QP solvers on one given sparse quadratic program") print(f"For a proper benchmark, check out {benchmark}") check_same_solutions() time_dense_solvers() time_sparse_solvers() ================================================ FILE: pyproject.toml ================================================ [build-system] requires = ["flit_core >=2,<4"] build-backend = "flit_core.buildapi" [project] name = "qpsolvers" readme = "README.md" authors = [ {name = "Stéphane Caron", email = "stephane.caron@normalesup.org"}, ] maintainers = [ {name = "Stéphane Caron", email = "stephane.caron@normalesup.org"}, ] dynamic = ['version', 'description'] requires-python = ">=3.10" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", "Operating System :: OS Independent", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: Mathematics", ] dependencies = [ "numpy >=1.15.4", "scipy >=1.2.0", ] keywords = ["quadratic programming", "solver", "numerical optimization"] [project.optional-dependencies] clarabel = ["clarabel >=0.4.1"] copt = ["coptpy>=7.0.0"] cvxopt = ["cvxopt >=1.2.6"] kvxopt = ["kvxopt >=1.3.2"] daqp = ["daqp >=0.8.2"] ecos = ["ecos >=2.0.8"] gurobi = ["gurobipy >=9.5.2"] highs = ["highspy >=1.1.2.dev3,<1.14.0"] jaxopt = ["jaxopt >=0.8.3"] mosek = ["cvxopt >=1.2.6", "mosek >=10.0.40"] osqp = ["osqp >=0.6.2"] piqp = ["piqp >=0.2.2"] proxqp = ["proxsuite >=0.2.9"] qpalm = ["qpalm >=1.2.1"] pyqpmad = ["pyqpmad >=1.4.0.post3"] qpax = ["qpax>=0.0.9"] qtqp = ["qtqp >=0.0.3"] quadprog = ["quadprog >=0.1.11"] scs = ["scs >=3.2.0"] sip = ["sip-python >=0.0.2"] open_source_solvers = ["qpsolvers[clarabel,cvxopt,daqp,ecos,highs,jaxopt,osqp,piqp,proxqp,pyqpmad,qpalm,qtqp,quadprog,scs,sip,qpax]"] # Wheels-only solvers should distribute wheels that work on: # - macOS (aarch64) # - macOS (x86) # - Linux (x86) # - Windows (x86) wheels_only = ["qpsolvers[cvxopt,daqp,ecos,highs,piqp,proxqp,qpalm,sip]"] [project.urls] Homepage = "https://github.com/qpsolvers/qpsolvers" Documentation = "https://qpsolvers.github.io/qpsolvers/" Source = "https://github.com/qpsolvers/qpsolvers" Tracker = "https://github.com/qpsolvers/qpsolvers/issues" Changelog = "https://github.com/qpsolvers/qpsolvers/blob/main/CHANGELOG.md" [tool.coverage] report.include = ["qpsolvers/*"] report.omit = [ "qpsolvers/solvers/pdhcg_.py", "qpsolvers/solvers/nppro_.py", ] [tool.mypy] ignore_missing_imports = true [tool.pixi.workspace] channels = ["conda-forge"] platforms = ["linux-64", "linux-aarch64", "osx-arm64", "win-64"] [tool.pixi.dependencies] python = ">=3.10,<3.14" clarabel = ">=0.4.1" cvxopt = ">=1.2.6" daqp = ">=0.8.2" ecos = ">=2.0.8" highspy = ">=1.5.3,<1.14.0" kvxopt = ">=1.3.2" numpy = ">=1.15.4" osqp = ">=0.6.2" piqp = ">=0.2.2" proxsuite = ">=0.2.9" quadprog = ">=0.1.11" scipy = ">=1.2.0" scs = ">=3.2.0" [tool.pixi.pypi-dependencies] coptpy = ">=7.0.0" gurobipy = ">=9.5.2" jaxopt = ">=0.8.3" qpalm = ">=1.2.1" qpax = ">=0.0.9" qtqp = ">=0.0.3,<0.0.4" sip-python = ">=0.0.2" pyqpmad = ">=1.4.0.post3" [tool.pixi.feature.coverage.dependencies] coverage = ">=5.5" coveralls = "*" [tool.pixi.feature.coverage.tasks] coverage-erase = { cmd = "coverage erase" } coverage-run = { cmd = "coverage run -m unittest discover --failfast", depends-on = ["coverage-erase"] } coverage = { cmd = "coverage report", depends-on = ["coverage-run"] } coveralls = { cmd = "coveralls --service=github" } [tool.pixi.feature.docs.dependencies] sphinx = ">=7.2.2" sphinx-autodoc-typehints = "*" setuptools = ">=60.0" [tool.pixi.feature.docs.pypi-dependencies] furo = ">=2023.8.17" sphinx-mathjax-offline = "*" [tool.pixi.feature.licensed.pypi-dependencies] mosek = ">=10.0.40" [tool.pixi.feature.licensed.tasks] licensed = "python -m unittest discover --failfast" [tool.pixi.feature.lint.dependencies] mypy = ">=0.812" pylint = ">=2.8.2" ruff = ">=0.5.4" scipy-stubs = "*" [tool.pixi.feature.py310.dependencies] python = "3.10.*" [tool.pixi.feature.py311.dependencies] python = "3.11.*" [tool.pixi.feature.py312.dependencies] python = "3.12.*" [tool.pixi.feature.py313.dependencies] python = "3.13.*" [tool.pixi.feature.test.dependencies] [tool.pixi.feature.test.tasks] test = "python -m unittest discover --failfast" [tool.pixi.feature.lint.tasks] mypy = "mypy qpsolvers --config-file=pyproject.toml" pylint = "pylint qpsolvers --exit-zero --rcfile=pyproject.toml" ruff = "ruff check qpsolvers && ruff format --check qpsolvers" lint = { depends-on = ["mypy", "pylint", "ruff"] } [tool.pixi.feature.docs.tasks] docs-build = "sphinx-build doc _build -W" [tool.pixi.environments] coverage = { features = ["py310", "coverage", "licensed"], solve-group = "py310" } docs = { features = ["py310", "docs"], solve-group = "py310" } licensed = { features = ["py310", "licensed"], solve-group = "py310" } lint = { features = ["py312", "lint"], solve-group = "py312" } test-py310 = { features = ["py310", "test"], solve-group = "py310" } test-py311 = { features = ["py311", "test"], solve-group = "py311" } test-py312 = { features = ["py312", "test"], solve-group = "py312" } test-py313 = { features = ["py313", "test"], solve-group = "py313" } [tool.pylint.'MESSAGES CONTROL'] disable = [ "C0103", # Argument name doesn't conform to snake_case (we use uppercase matrices) "E0611", # No name 'solve_qp' in module 'quadprog' (false positive) "E1130", # Bad operand type for unary - (false positive, covered by mypy) "R0801", # Similar lines in many files (functions share the same prototype) "R0902", # Too many instance attributes (our QP class has 8 > 7) "R0913", # Too many arguments (functions have > 5 args) "R0914", # Too many local variables (functions often > 15 locals) "import-error", # Suppress import‑error when optional back‑ends are missing "too-many-branches", # We accept the blame "too-many-positional-arguments", # We accept the blame "too-many-statements", # We accept the blame ] [tool.pylint.'TYPECHECK'] generated-members = [ "clarabel.DefaultSettings", "clarabel.DefaultSolver", "clarabel.NonnegativeConeT", "clarabel.SolverStatus", "clarabel.ZeroConeT", "coptpy.Envr", "coptpy.EnvrConfig", "coptpy.MConstr", "coptpy.Model", "coptpy.NdArray", "daqp.solve", "gurobipy.MConstr", "gurobipy.Model", "piqp.DenseSolver", "piqp.PIQP_SOLVED", "piqp.SparseSolver", "piqp.__version__", "proxsuite.proxqp", "qpSWIFT.run", "qpalm.Data", "qpalm.Settings", "qpalm.Solver", "sip.ModelCallbackInput", "sip.ModelCallbackOutput", "sip.ProblemDimensions", "sip.QDLDLSettings", "sip.Settings", "sip.Solver", "sip.Status", "sip.Variables", ] [tool.ruff] line-length = 79 [tool.ruff.lint] ignore = [ "D401", # good for methods but not for class docstrings "D405", # British-style section names are also "proper"! ] select = [ # pyflakes "F", # pycodestyle "E", "W", # isort "I001", # pydocstyle "D" ] [tool.ruff.lint.pydocstyle] convention = "numpy" ================================================ FILE: qpsolvers/__init__.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Quadratic programming solvers in Python with a unified API.""" from .active_set import ActiveSet from .exceptions import ( NoSolverSelected, ParamError, ProblemError, QPError, SolverError, SolverNotFound, ) from .problem import Problem from .solution import Solution from .solve_ls import solve_ls from .solve_qp import solve_problem, solve_qp from .solve_unconstrained import solve_unconstrained from .solvers import ( available_solvers, cvxopt_solve_qp, daqp_solve_qp, dense_solvers, ecos_solve_qp, gurobi_solve_qp, highs_solve_qp, hpipm_solve_qp, kvxopt_solve_qp, mosek_solve_qp, nppro_solve_qp, osqp_solve_qp, pdhcg_solve_qp, piqp_solve_qp, proxqp_solve_qp, pyqpmad_solve_qp, qpalm_solve_qp, qpoases_solve_qp, qpswift_solve_qp, qtqp_solve_qp, quadprog_solve_qp, scs_solve_qp, sip_solve_qp, sparse_solvers, ) from .utils import print_matrix_vector __version__ = "4.11.0" __all__ = [ "ActiveSet", "NoSolverSelected", "ParamError", "Problem", "ProblemError", "QPError", "Solution", "SolverError", "SolverNotFound", "__version__", "available_solvers", "cvxopt_solve_qp", "daqp_solve_qp", "dense_solvers", "ecos_solve_qp", "gurobi_solve_qp", "highs_solve_qp", "hpipm_solve_qp", "kvxopt_solve_qp", "mosek_solve_qp", "nppro_solve_qp", "osqp_solve_qp", "print_matrix_vector", "pdhcg_solve_qp", "piqp_solve_qp", "proxqp_solve_qp", "pyqpmad_solve_qp", "qpalm_solve_qp", "qpoases_solve_qp", "qpswift_solve_qp", "quadprog_solve_qp", "qtqp_solve_qp", "scs_solve_qp", "sip_solve_qp", "solve_ls", "solve_problem", "solve_qp", "solve_unconstrained", "sparse_solvers", ] ================================================ FILE: qpsolvers/active_set.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2023 Inria """Active set: indices of inequality constraints saturated at the optimum.""" from dataclasses import dataclass from typing import Optional, Sequence @dataclass class ActiveSet: """Indices of active inequality constraints. Attributes ---------- G_indices : Indices of active linear inequality constraints. lb_indices : Indices of active lower-bound inequality constraints. ub_indices : Indices of active upper-bound inequality constraints. """ G_indices: Sequence[int] lb_indices: Sequence[int] ub_indices: Sequence[int] def __init__( self, G_indices: Optional[Sequence[int]] = None, lb_indices: Optional[Sequence[int]] = None, ub_indices: Optional[Sequence[int]] = None, ) -> None: self.G_indices = list(G_indices) if G_indices is not None else [] self.lb_indices = list(lb_indices) if lb_indices is not None else [] self.ub_indices = list(ub_indices) if ub_indices is not None else [] ================================================ FILE: qpsolvers/conversions/__init__.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Convert problems from and to standard QP form.""" from .combine_linear_box_inequalities import combine_linear_box_inequalities from .ensure_sparse_matrices import ensure_sparse_matrices from .linear_from_box_inequalities import linear_from_box_inequalities from .socp_from_qp import socp_from_qp from .split_dual_linear_box import split_dual_linear_box __all__ = [ "combine_linear_box_inequalities", "ensure_sparse_matrices", "linear_from_box_inequalities", "socp_from_qp", "split_dual_linear_box", ] ================================================ FILE: qpsolvers/conversions/combine_linear_box_inequalities.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2023 Stéphane Caron and the qpsolvers contributors """Combine linear and box inequalities into double-sided linear format.""" import numpy as np import scipy.sparse as spa from ..exceptions import ProblemError def combine_linear_box_inequalities(G, h, lb, ub, n: int, use_csc: bool): r"""Combine linear and box inequalities into double-sided linear format. Input format: .. math:: \begin{split}\begin{array}{ll} G x & \leq h \\ lb & \leq x \leq ub \end{array}\end{split} Output format: .. math:: l \leq C \leq u Parameters ---------- G : Linear inequality constraint matrix. Must be two-dimensional. h : Linear inequality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. n : Number of optimization variables. use_csc : If ``True``, use sparse rather than dense matrices. Returns ------- : Linear inequality matrix :math:`C` and vectors :math:`u`, :math:`l`. The two vector will contain :math:`\pm\infty` values on coordinates where there is no corresponding constraint. Raises ------ ProblemError If the inequality matrix and vector are not consistent. """ if lb is None and ub is None: C_out = G u_out = h l_out = np.full(h.shape, -np.inf) if h is not None else None elif G is None: # lb is not None or ub is not None: C_out = spa.eye(n, format="csc") if use_csc else np.eye(n) u_out = ub if ub is not None else np.full(n, +np.inf) l_out = lb if lb is not None else np.full(n, -np.inf) elif h is not None: # G is not None and h is not None and not (lb is None and ub is None) C_out = ( spa.vstack((G, spa.eye(n)), format="csc") if use_csc else np.vstack((G, np.eye(n))) ) ub = ub if ub is not None else np.full(G.shape[1], +np.inf) lb = lb if lb is not None else np.full(G.shape[1], -np.inf) l_out = np.hstack((np.full(h.shape, -np.inf), lb)) u_out = np.hstack((h, ub)) else: # G is not None and h is None raise ProblemError("Inconsistent inequalities: G is set but h is None") return C_out, u_out, l_out ================================================ FILE: qpsolvers/conversions/ensure_sparse_matrices.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Make sure problem matrices are sparse.""" import warnings from typing import Optional, Tuple, Union import numpy as np import scipy.sparse as spa from ..warnings import SparseConversionWarning def __warn_about_sparse_conversion(matrix_name: str, solver_name: str) -> None: """Warn about conversion from dense to sparse matrix. Parameters ---------- matrix_name : Name of matrix being converted from dense to sparse. solver_name : Name of the QP solver matrices will be passed to. """ warnings.warn( f"Converted matrix '{matrix_name}' of your problem to " f"scipy.sparse.csc_matrix to pass it to solver '{solver_name}'; " f"for best performance, build your matrix as a csc_matrix directly.", category=SparseConversionWarning, ) def ensure_sparse_matrices( solver_name: str, P: Union[np.ndarray, spa.csc_matrix], G: Optional[Union[np.ndarray, spa.csc_matrix]], A: Optional[Union[np.ndarray, spa.csc_matrix]], ) -> Tuple[spa.csc_matrix, Optional[spa.csc_matrix], Optional[spa.csc_matrix]]: """ Make sure problem matrices are sparse. Parameters ---------- solver_name : Name of the QP solver matrices will be passed to. P : Cost matrix. G : Inequality constraint matrix, if any. A : Equality constraint matrix, if any. Returns ------- : Tuple of all three matrices as sparse matrices. """ if isinstance(P, np.ndarray): __warn_about_sparse_conversion("P", solver_name) P = spa.csc_matrix(P) if isinstance(G, np.ndarray): __warn_about_sparse_conversion("G", solver_name) G = spa.csc_matrix(G) if isinstance(A, np.ndarray): __warn_about_sparse_conversion("A", solver_name) A = spa.csc_matrix(A) return P, G, A ================================================ FILE: qpsolvers/conversions/linear_from_box_inequalities.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Functions to convert vector bounds into linear inequality constraints.""" from typing import Optional, Tuple, Union import numpy as np import scipy.sparse as spa from ..exceptions import ProblemError def concatenate_bound( G: Optional[Union[np.ndarray, spa.csc_matrix, spa.dia_matrix]], h: Optional[np.ndarray], b: np.ndarray, sign: float, use_sparse: bool, ) -> Tuple[Optional[Union[np.ndarray, spa.csc_matrix]], Optional[np.ndarray]]: """Append bound constraint vectors to inequality constraints. Parameters ---------- G : Linear inequality matrix. h : Linear inequality vector. b : Bound constraint vector. sign : Sign factor: -1.0 for a lower and +1.0 for an upper bound. use_sparse : Use sparse matrices if true, dense matrices otherwise. Returns ------- G : Updated linear inequality matrix. h : Updated linear inequality vector. """ n = len(b) # == number of optimization variables if G is None or h is None: G = sign * (spa.eye(n, format="csc") if use_sparse else np.eye(n)) h = sign * b return (G, h) h = np.concatenate((h, sign * b)) if isinstance(G, np.ndarray): dense_G: np.ndarray = np.concatenate((G, sign * np.eye(n)), 0) return (dense_G, h) if isinstance(G, (spa.csc_matrix, spa.dia_matrix)): sparse_G: spa.csc_matrix = spa.vstack( [G, sign * spa.eye(n)], format="csc" ) return (sparse_G, h) # G is not an instance of a type we know name = type(G).__name__ raise ProblemError(f"invalid type '{name}' for inequality matrix G") def linear_from_box_inequalities( G: Optional[Union[np.ndarray, spa.csc_matrix, spa.dia_matrix]], h: Optional[np.ndarray], lb: Optional[np.ndarray], ub: Optional[np.ndarray], use_sparse: bool, ) -> Tuple[Optional[Union[np.ndarray, spa.csc_matrix]], Optional[np.ndarray]]: """Append lower or upper bound vectors to inequality constraints. Parameters ---------- G : Linear inequality matrix. h : Linear inequality vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. use_sparse : Use sparse matrices if true, dense matrices otherwise. Returns ------- G : Updated linear inequality matrix. h : Updated linear inequality vector. """ if lb is not None: G, h = concatenate_bound(G, h, lb, -1.0, use_sparse) if ub is not None: G, h = concatenate_bound(G, h, ub, +1.0, use_sparse) if isinstance(G, spa.dia_matrix): # corner case with no new box bound return (spa.csc_matrix(G), h) return (G, h) ================================================ FILE: qpsolvers/conversions/socp_from_qp.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Convert quadratic programs to second-order cone programs.""" from typing import Any, Dict, Optional, Tuple, Union import numpy as np from numpy import ndarray, sqrt from numpy.linalg import LinAlgError, cholesky from scipy.sparse import csc_matrix from ..exceptions import ProblemError def socp_from_qp( P: ndarray, q: ndarray, G: Optional[ndarray], h: Optional[ndarray] ) -> Tuple[ndarray, csc_matrix, ndarray, Dict[str, Any]]: r"""Convert a quadratic program to a second-order cone program. The quadratic program is defined by: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \end{array}\end{split} The equivalent second-order cone program is: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & c^T_s y \\ \mbox{subject to} & G_s y \leq_{\cal K} h_s \end{array}\end{split} This function is adapted from ``ecosqp.m`` in the `ecos-matlab `_ repository. See the documentation in that script for details on this reformulation. Parameters ---------- P : Primal quadratic cost matrix. q : Primal quadratic cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. Returns ------- c_socp : array SOCP cost vector. G_socp : array SOCP inequality matrix. h_socp : array SOCP inequality vector. dims : dict Dimension dictionary used by SOCP solvers, where ``dims["l"]`` is the number of inequality constraints. Raises ------ ValueError : If the cost matrix is not positive definite. """ n = P.shape[1] # dimension of QP variable c_socp = np.hstack([np.zeros(n), 1]) # new SOCP variable stacked as [x, t] try: L = cholesky(P) except LinAlgError as e: error = str(e) if "not positive definite" in error: raise ProblemError("matrix P is not positive definite") from e raise e # other linear algebraic error scale = 1.0 / sqrt(2) G_quad = np.vstack( [ scale * np.hstack([q, -1.0]), np.hstack([-L.T, np.zeros((L.shape[0], 1))]), scale * np.hstack([-q, +1.0]), ] ) h_quad = np.hstack([scale, np.zeros(L.shape[0]), scale]) dims: Dict[str, Any] = {"q": [L.shape[0] + 2]} G_socp: Union[ndarray, csc_matrix] if G is not None and h is not None: G_socp = np.vstack([np.hstack([G, np.zeros((G.shape[0], 1))]), G_quad]) h_socp = np.hstack([h, h_quad]) dims["l"] = G.shape[0] else: # no linear inequality constraint G_socp = G_quad h_socp = h_quad dims["l"] = 0 G_socp = csc_matrix(G_socp) return c_socp, G_socp, h_socp, dims ================================================ FILE: qpsolvers/conversions/split_dual_linear_box.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Convert stacked dual multipliers into linear and box multipliers.""" from typing import Optional, Tuple import numpy as np def split_dual_linear_box( z_stacked: np.ndarray, lb: Optional[np.ndarray], ub: Optional[np.ndarray], ) -> Tuple[Optional[np.ndarray], Optional[np.ndarray]]: """Separate linear and box multipliers from a stacked dual vector. This function assumes linear and box inequalities were combined using :func:`qpsolvers.conversions.linear_from_box_inequalities`. Parameters ---------- z_stacked : Stacked vector of dual multipliers. lb : Lower bound constraint vector. ub : Upper bound constraint vector. Returns ------- : Pair :code:`z, z_box` of linear and box multipliers. Both can be empty arrays if there is no corresponding constraint. """ z = np.empty((0,), dtype=z_stacked.dtype) z_box = np.empty((0,), dtype=z_stacked.dtype) if lb is not None and ub is not None: n_lb = lb.shape[0] n_ub = ub.shape[0] n_box = n_lb + n_ub z_box = z_stacked[-n_ub:] - z_stacked[-n_box:-n_ub] z = z_stacked[:-n_box] elif ub is not None: # lb is None n_ub = ub.shape[0] z_box = z_stacked[-n_ub:] z = z_stacked[:-n_ub] elif lb is not None: # ub is None n_lb = lb.shape[0] z_box = -z_stacked[-n_lb:] z = z_stacked[:-n_lb] else: # lb is None and ub is None z = z_stacked return z, z_box ================================================ FILE: qpsolvers/exceptions.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """ Exceptions from qpsolvers. We catch all solver exceptions and re-throw them in a qpsolvers-owned exception to avoid abstraction leakage. See this `design decision `__ for more details on the rationale behind this choice. """ class QPError(Exception): """Base class for qpsolvers exceptions.""" class NoSolverSelected(QPError): """Exception raised when the `solver` keyword argument is not set.""" class ParamError(QPError): """Exception raised when solver parameters are incorrect.""" class ProblemError(QPError): """Exception raised when a quadratic program is malformed.""" class SolverNotFound(QPError): """Exception raised when a requested solver is not found.""" class SolverError(QPError): """Exception raised when a solver failed.""" ================================================ FILE: qpsolvers/problem.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Model for a quadratic program.""" from typing import List, Optional, Tuple, Union import numpy as np import scipy.sparse as spa from .active_set import ActiveSet from .conversions import linear_from_box_inequalities from .exceptions import ParamError, ProblemError class Problem: r"""Data structure describing a quadratic program. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} This class provides sanity checks and metrics such as the condition number of a problem. Attributes ---------- P : Symmetric cost matrix (most solvers require it to be definite as well). q : Cost vector. G : Linear inequality matrix. h : Linear inequality vector. A : Linear equality matrix. b : Linear equality vector. lb : Lower bound constraint vector. Can contain ``-np.inf``. ub : Upper bound constraint vector. Can contain ``+np.inf``. """ P: Union[np.ndarray, spa.csc_matrix] q: np.ndarray G: Optional[Union[np.ndarray, spa.csc_matrix]] = None h: Optional[np.ndarray] = None A: Optional[Union[np.ndarray, spa.csc_matrix]] = None b: Optional[np.ndarray] = None lb: Optional[np.ndarray] = None ub: Optional[np.ndarray] = None @staticmethod def __check_matrix( M: Union[np.ndarray, spa.csc_matrix], ) -> Union[np.ndarray, spa.csc_matrix]: """ Ensure a problem matrix has proper shape. Parameters ---------- M : Problem matrix. name : Matrix name. Returns ------- : Same matrix with proper shape. """ if hasattr(M, "ndim") and M.ndim == 1: # type: ignore M = M.reshape((1, M.shape[0])) # type: ignore return M @staticmethod def __check_vector(v: np.ndarray, name: str) -> np.ndarray: """ Ensure a problem vector has proper shape. Parameters ---------- M : Problem matrix. name : Matrix name. Returns ------- : Same matrix with proper shape. Raises ------ ProblemError If the vector cannot be flattened. """ if v.ndim <= 1: return v if v.shape[0] != 1 and v.shape[1] != 1 or v.ndim > 2: raise ProblemError( f"vector '{name}' should be flat " f"and cannot be flattened as its shape is {v.shape}" ) return v.flatten() def __init__( self, P: Union[np.ndarray, spa.csc_matrix], q: np.ndarray, G: Optional[Union[np.ndarray, spa.csc_matrix]] = None, h: Optional[np.ndarray] = None, A: Optional[Union[np.ndarray, spa.csc_matrix]] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, ) -> None: P = Problem.__check_matrix(P) q = Problem.__check_vector(q, "q") G = Problem.__check_matrix(G) if G is not None else None h = Problem.__check_vector(h, "h") if h is not None else None A = Problem.__check_matrix(A) if A is not None else None b = Problem.__check_vector(b, "b") if b is not None else None lb = Problem.__check_vector(lb, "lb") if lb is not None else None ub = Problem.__check_vector(ub, "ub") if ub is not None else None self.P = P self.q = q self.G = G self.h = h self.A = A self.b = b self.lb = lb self.ub = ub @property def has_sparse(self) -> bool: """Check whether the problem has sparse matrices. Returns ------- : True if at least one of the :math:`P`, :math:`G` or :math:`A` matrices is sparse. """ sparse_types = (spa.csc_matrix, spa.dia_matrix) return ( isinstance(self.P, sparse_types) or isinstance(self.G, sparse_types) or isinstance(self.A, sparse_types) ) @property def is_unconstrained(self) -> bool: """Check whether the problem has any constraint. Returns ------- : True if the problem has at least one constraint. """ return ( self.G is None and self.A is None and self.lb is None and self.ub is None ) def unpack( self, ) -> Tuple[ Union[np.ndarray, spa.csc_matrix], np.ndarray, Optional[Union[np.ndarray, spa.csc_matrix]], Optional[np.ndarray], Optional[Union[np.ndarray, spa.csc_matrix]], Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray], ]: """Get problem matrices as a tuple. Returns ------- : Tuple ``(P, q, G, h, A, b, lb, ub)`` of problem matrices. """ return ( self.P, self.q, self.G, self.h, self.A, self.b, self.lb, self.ub, ) def unpack_as_dense( self, ) -> Tuple[ np.ndarray, np.ndarray, Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray], ]: """Get problem matrices as a tuple of dense matrices and vectors. Returns ------- : Tuple ``(P, q, G, h, A, b, lb, ub)`` of problem matrices. """ return ( self.P.toarray() if isinstance(self.P, spa.csc_matrix) else self.P, self.q, self.G.toarray() if isinstance(self.G, spa.csc_matrix) else self.G, self.h, self.A.toarray() if isinstance(self.A, spa.csc_matrix) else self.A, self.b, self.lb, self.ub, ) def check_constraints(self): """Check that problem constraints are properly specified. Raises ------ ProblemError If the constraints are not properly defined. """ if self.G is None and self.h is not None: raise ProblemError("incomplete inequality constraint (missing h)") if self.G is not None and self.h is None: raise ProblemError("incomplete inequality constraint (missing G)") if self.A is None and self.b is not None: raise ProblemError("incomplete equality constraint (missing b)") if self.A is not None and self.b is None: raise ProblemError("incomplete equality constraint (missing A)") def __get_active_inequalities( self, active_set: ActiveSet ) -> Optional[Union[np.ndarray, spa.csc_matrix]]: r"""Combine active linear and box inequalities into a single matrix. Parameters ---------- active_set : Active set to evaluate the condition number with. It should contain the set of active constraints at the optimum of the problem. Returns ------- : Combined matrix of active inequalities. """ G_full, _ = linear_from_box_inequalities( self.G, self.h, self.lb, self.ub, use_sparse=False ) if G_full is None: return None indices: List[int] = [] offset: int = 0 if self.h is not None: indices.extend(active_set.G_indices) offset += self.h.size if self.lb is not None: indices.extend(offset + i for i in active_set.lb_indices) offset += self.lb.size if self.ub is not None: indices.extend(offset + i for i in active_set.ub_indices) G_active = G_full[indices] return G_active def cond(self, active_set: ActiveSet) -> float: r"""Condition number of the problem matrix. Compute the condition number of the symmetric matrix representing the problem data: .. math:: M = \begin{bmatrix} P & G_{act}^T & A_{act}^T \\ G_{act} & 0 & 0 \\ A_{act} & 0 & 0 \end{bmatrix} where :math:`G_{act}` and :math:`A_{act}` denote the active inequality and equality constraints at the optimum of the problem. Parameters ---------- active_set : Active set to evaluate the condition number with. It should contain the set of active constraints at the optimum of the problem. Returns ------- : Condition number of the problem. Raises ------ ProblemError : If the problem is sparse. Notes ----- Having a low condition number (say, less than 1e10) condition number is strongly tied to the capacity of numerical solvers to solve a problem. This is the motivation for preconditioning, as detailed for instance in Section 5 of [Stellato2020]_. """ if self.has_sparse: raise ProblemError("This function is for dense problems only") if active_set.lb_indices and self.lb is None: raise ProblemError("Lower bound in active set but not in problem") if active_set.ub_indices and self.ub is None: raise ProblemError("Upper bound in active set but not in problem") P: np.ndarray = ( self.P.toarray() if isinstance(self.P, spa.csc_matrix) else self.P ) G_active_full = self.__get_active_inequalities(active_set) G_active: Optional[np.ndarray] = ( G_active_full.toarray() if isinstance(G_active_full, spa.csc_matrix) else G_active_full ) A: Optional[np.ndarray] = ( self.A.toarray() if isinstance(self.A, spa.csc_matrix) else self.A ) n_G = G_active.shape[0] if G_active is not None else 0 n_A = A.shape[0] if A is not None else 0 if G_active is not None and A is not None: M = np.vstack( [ np.hstack([P, G_active.T, A.T]), np.hstack( [ G_active, np.zeros((n_G, n_G)), np.zeros((n_G, n_A)), ] ), np.hstack( [ A, np.zeros((n_A, n_G)), np.zeros((n_A, n_A)), ] ), ] ) elif G_active is not None: M = np.vstack( [ np.hstack([P, G_active.T]), np.hstack([G_active, np.zeros((n_G, n_G))]), ] ) elif A is not None: M = np.vstack( [ np.hstack([P, A.T]), np.hstack([A, np.zeros((n_A, n_A))]), ] ) else: # G_active is None and A is None M = P return np.linalg.cond(M) def save(self, file: str) -> None: """Save problem to a compressed NumPy file. Parameters ---------- file : str or file Either the filename (string) or an open file (file-like object) where the data will be saved. If file is a string or a Path, the ``.npz`` extension will be appended to the filename if it is not already there. """ np.savez( file, P=( self.P.toarray() if isinstance(self.P, spa.csc_matrix) else self.P ), q=self.q, G=np.array(self.G), h=np.array(self.h), A=np.array(self.A), b=np.array(self.b), lb=np.array(self.lb), ub=np.array(self.ub), ) @staticmethod def load(file: str): """Load problem from a NumPy file. Parameters ---------- file : file-like object, string, or pathlib.Path The file to read. File-like objects must support the ``seek()`` and ``read()`` methods and must always be opened in binary mode. Pickled files require that the file-like object support the ``readline()`` method as well. """ problem_data = np.load(file, allow_pickle=False) def load_optional(key): try: return problem_data[key] except ValueError: return None return Problem( P=load_optional("P"), q=load_optional("q"), G=load_optional("G"), h=load_optional("h"), A=load_optional("A"), b=load_optional("b"), lb=load_optional("lb"), ub=load_optional("ub"), ) def get_cute_classification(self, interest: str) -> str: """Get the CUTE classification string of the problem. Parameters ---------- interest: Either 'A', 'M' or 'R': 'A' if the problem is academic, that is, has been constructed specifically by researchers to test one or more algorithms; 'M' if the problem is part of a modelling exercise where the actual value of the solution is not used in a genuine practical application; and 'R' if the problem's solution is (or has been) actually used in a real application for purposes other than testing algorithms. Returns ------- : CUTE classification string of the problem Notes ----- Check out the `CUTE classification scheme `__ for details. """ if interest not in ("A", "M", "R"): raise ParamError(f"interest '{interest}' not in 'A', 'M' or 'R'") nb_var = self.P.shape[0] nb_cons = 0 if self.G is not None: nb_cons += self.G.shape[0] if self.A is not None: nb_cons += self.A.shape[0] # NB: we don't cound bounds as constraints in this classification return f"QLR2-{interest}N-{nb_var}-{nb_cons}" ================================================ FILE: qpsolvers/problems.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2023 Stéphane Caron and the qpsolvers contributors """Collection of sample problems.""" from typing import Tuple import numpy as np import scipy.sparse as spa from .problem import Problem from .solution import Solution def get_sparse_least_squares(n): """ Get a sparse least squares problem. Parameters ---------- n : Problem size. Notes ----- This problem was inspired by `this question on Stack Overflow `__. """ # minimize 1/2 || x - s ||^2 R = spa.eye(n, format="csc") s = np.array(range(n), dtype=float) # such that G * x <= h G = spa.diags( diagonals=[ [1.0 if i % 2 == 0 else 0.0 for i in range(n)], [1.0 if i % 3 == 0 else 0.0 for i in range(n - 1)], [1.0 if i % 5 == 0 else 0.0 for i in range(n - 1)], ], offsets=[0, 1, -1], format="csc", ) h = np.ones(G.shape[0]) # such that sum(x) == 42 A = spa.csc_matrix(np.ones((1, n))) b = np.array([42.0]).reshape((1,)) # such that x >= 0 lb = np.zeros(n) ub = None return R, s, G, h, A, b, lb, ub def get_qpsut01() -> Tuple[Problem, Solution]: """Get QPSUT01 problem and its solution. Returns ------- : Problem-solution pair. """ M = np.array([[1.0, 2.0, 0.0], [-8.0, 3.0, 2.0], [0.0, 1.0, 1.0]]) P = np.dot(M.T, M) # this is a positive definite matrix q = np.dot(np.array([3.0, 2.0, 3.0]), M) G = np.array([[4.0, 2.0, 0.0], [-1.0, 2.0, -1.0]]) h = np.array([1.0, -2.0]) A = np.array([1.0, 1.0, 1.0]).reshape((1, 3)) b = np.array([1.0]) lb = np.array([-0.5, -0.4, -0.5]) ub = np.array([1.0, 1.0, 1.0]) problem = Problem(P, q, G, h, A, b, lb, ub) solution = Solution(problem) solution.found = True solution.x = np.array([0.4, -0.4, 1.0]) solution.z = np.array([0.0, 0.0]) solution.y = np.array([-5.8]) solution.z_box = np.array([0.0, -1.8, 3.0]) return problem, solution def get_qpsut02() -> Tuple[Problem, Solution]: """Get QPSUT02 problem and its solution. Returns ------- : Problem-solution pair. """ M = np.array( [ [1.0, -2.0, 0.0, 8.0], [-6.0, 3.0, 1.0, 4.0], [-2.0, 1.0, 0.0, 1.0], [9.0, 9.0, 5.0, 3.0], ] ) P = np.dot(M.T, M) # this is a positive definite matrix q = np.dot(np.array([-3.0, 2.0, 0.0, 9.0]), M) G = np.array( [ [4.0, 7.0, 0.0, -2.0], ] ) h = np.array([30.0]) A = np.array( [ [1.0, 1.0, 1.0, 1.0], [1.0, -1.0, -1.0, 1.0], ] ) b = np.array([10.0, 0.0]) lb = np.array([-2.0, -1.0, -3.0, 1.0]) ub = np.array([4.0, 2.0, 6.0, 10.0]) problem = Problem(P, q, G, h, A, b, lb, ub) solution = Solution(problem) solution.found = True solution.x = np.array([1.36597938, -1.0, 6.0, 3.63402062]) solution.z = np.array([0.0]) solution.y = np.array([-377.60314303, -62.75251185]) # YMMV solution.z_box = np.array([0.0, -138.9585918, 37.53106937, 0.0]) # YMMV return problem, solution def get_qpsut03() -> Tuple[Problem, Solution]: """Get QPSUT03 problem and its solution. Returns ------- : Problem-solution pair. Notes ----- This problem has partial box bounds, that is, -infinity on some lower bounds and +infinity on some upper bounds. """ M = np.array( [ [1.0, -2.0, 0.0, 8.0], [-6.0, 3.0, 1.0, 4.0], [-2.0, 1.0, 0.0, 1.0], [9.0, 9.0, 5.0, 3.0], ] ) P = np.dot(M.T, M) # this is a positive definite matrix q = np.dot(np.array([-3.0, 2.0, 0.0, 9.0]), M) G = None h = None A = None b = None lb = np.array([-np.inf, -0.4, -np.inf, -1.0]) ub = np.array([np.inf, np.inf, 0.5, 1.0]) problem = Problem(P, q, G, h, A, b, lb, ub) solution = Solution(problem) solution.found = True solution.x = np.array([0.18143455, 0.00843864, -2.35442995, 0.35443034]) solution.z = np.array([]) solution.y = np.array([]) solution.z_box = np.array([0.0, 0.0, 0.0, 0.0]) return problem, solution def get_qpsut04() -> Tuple[Problem, Solution]: """Get QPSUT04 problem and its solution. Returns ------- : Problem-solution pair. """ n = 3 P = np.eye(n) q = 0.01 * np.ones(shape=(n, 1)) # non-flat vector G = np.eye(n) h = np.ones(shape=(n,)) A = np.ones(shape=(n,)) b = np.ones(shape=(1,)) problem = Problem(P, q, G, h, A, b) solution = Solution(problem) solution.found = True solution.x = 1.0 / 3.0 * np.ones(n) solution.y = np.array([1.0 / 3.0 + 0.01]) solution.z = np.zeros(n) return problem, solution def get_qpsut05() -> Tuple[Problem, Solution]: """Get QPSUT05 problem and its solution. Returns ------- : Problem-solution pair. """ P = np.array([2.0]) q = np.array([-2.0]) problem = Problem(P, q) solution = Solution(problem) solution.found = True solution.x = np.array([1.0]) return problem, solution def get_qptest(): """Get QPTEST problem from the Maros-Meszaros test set. Returns ------- : Problem-solution pair. """ P = np.array([[8.0, 2.0], [2.0, 10.0]]) q = np.array([1.5, -2.0]) G = np.array([[-1.0, 2.0], [-2.0, -1.0]]) h = np.array([6.0, -2.0]) lb = np.array([0.0, 0.0]) ub = np.array([20.0, np.inf]) problem = Problem(P, q, G, h, lb=lb, ub=ub) solution = Solution(problem) solution.found = True solution.x = np.array([0.7625, 0.475]) solution.z = np.array([0.0, 4.275]) solution.z_box = np.array([0.0, 0.0]) return problem, solution def get_qpgurdu(): """Get sample random problem with linear inequality constraints. Returns ------- : Problem-solution pair. """ P = np.array( [ [ 3.57211988, 3.04767485, 2.81378189, 3.10290601, 3.70204698, 3.21624815, 3.07738552, 2.97880055, 2.87282375, 3.13101137, ], [ 3.04767485, 3.29764869, 2.96655517, 2.99532101, 3.27631229, 2.95993532, 3.36890754, 3.41940015, 2.71055468, 3.48874903, ], [ 2.81378189, 2.96655517, 4.07209512, 3.15291684, 3.25120445, 3.16570711, 3.29693401, 3.57945021, 2.38634372, 3.56010605, ], [ 3.10290601, 2.99532101, 3.15291684, 4.18950328, 3.80236382, 3.30578443, 3.86461151, 3.73403774, 2.65103423, 3.6915013, ], [ 3.70204698, 3.27631229, 3.25120445, 3.80236382, 4.49927773, 3.71882781, 3.72242148, 3.36633929, 3.07400851, 3.44904275, ], [ 3.21624815, 2.95993532, 3.16570711, 3.30578443, 3.71882781, 3.54009378, 3.3619341, 3.45111777, 2.52760157, 3.47292034, ], [ 3.07738552, 3.36890754, 3.29693401, 3.86461151, 3.72242148, 3.3619341, 4.18766506, 3.9158527, 2.73687599, 3.94376429, ], [ 2.97880055, 3.41940015, 3.57945021, 3.73403774, 3.36633929, 3.45111777, 3.9158527, 4.4180459, 2.50596495, 4.25387869, ], [ 2.87282375, 2.71055468, 2.38634372, 2.65103423, 3.07400851, 2.52760157, 2.73687599, 2.50596495, 2.74656049, 2.54212279, ], [ 3.13101137, 3.48874903, 3.56010605, 3.6915013, 3.44904275, 3.47292034, 3.94376429, 4.25387869, 2.54212279, 4.634129, ], ] ) q = np.array( [ [0.49318579], [0.82113304], [0.67851692], [0.34081485], [0.14826526], [0.81974126], [0.41957706], [0.53118637], [0.59189664], [0.98775649], ] ) G = np.array( [ [ 4.38410058e-01, 4.43204832e-01, 3.01827071e-01, 5.77725615e-02, 8.04962962e-01, 6.13555163e-01, 1.15255766e-01, 7.11331164e-01, 7.71820534e-02, 3.86074035e-01, ], [ 8.47645982e-01, 9.37475356e-01, 3.54726656e-01, 9.64635375e-01, 5.95008737e-01, 4.65424573e-01, 3.60529910e-01, 5.83149169e-01, 5.51353698e-01, 8.45823800e-01, ], [ 2.29674075e-04, 5.54870256e-02, 7.83869376e-01, 9.97727284e-01, 1.49512389e-01, 7.44775614e-01, 8.76446593e-02, 2.57348591e-01, 7.28916655e-01, 5.97511590e-01, ], [ 6.92184129e-01, 9.04600884e-01, 7.57700115e-01, 7.76548565e-01, 5.31039749e-01, 8.32203998e-01, 4.27810742e-01, 1.92236814e-01, 2.91129478e-01, 7.76195308e-01, ], [ 4.73333212e-01, 3.02129792e-02, 6.86517354e-01, 5.08992776e-01, 8.43205462e-01, 6.30402967e-01, 7.92221172e-01, 3.67768984e-01, 1.10864990e-01, 5.44828940e-01, ], [ 9.23060980e-01, 4.55743966e-01, 4.81958856e-02, 5.47614699e-02, 8.23194952e-01, 2.40526659e-01, 9.33519842e-01, 5.40430172e-01, 6.27229337e-01, 4.27829243e-01, ], [ 2.39454128e-01, 1.29688157e-01, 7.64521599e-01, 2.66943061e-01, 4.94990723e-01, 3.87798160e-01, 5.76282838e-01, 8.87340479e-01, 5.49439650e-01, 2.99596520e-01, ], [ 3.73174589e-02, 4.08407618e-01, 1.19009418e-01, 3.02572289e-02, 1.90287316e-01, 2.93975786e-01, 7.65243508e-01, 8.64670246e-02, 3.90593097e-01, 1.33870683e-01, ], [ 9.10093385e-01, 9.63382642e-02, 2.94162739e-01, 9.71178995e-01, 1.81811460e-01, 9.69904715e-02, 4.10693806e-01, 7.56873549e-01, 2.36595007e-01, 3.19756491e-01, ], [ 8.58362518e-02, 7.88161645e-02, 9.67300428e-01, 2.59894669e-01, 1.62774911e-01, 3.33859109e-01, 6.15307748e-01, 1.81164951e-02, 5.99620503e-01, 5.71512979e-01, ], ] ) h = np.array( [ [4.94957567], [7.50577326], [5.40302286], [7.18164978], [5.98834884], [6.07449251], [5.59605532], [3.45542914], [5.27449417], [4.69303926], ] ) problem = Problem(P, q, G, h) solution = Solution(problem) solution.found = True return problem, solution def get_qpgurabs(): """Get sample random problem with box constraints. Returns ------- : Problem-solution pair. """ qpgurdu, _ = get_qpgurdu() box = np.abs(qpgurdu.h) problem = Problem(qpgurdu.P, qpgurdu.q, lb=-box, ub=+box) solution = Solution(problem) solution.found = True return problem, solution def get_qpgureq(): """Get sample random problem with equality constraints. Returns ------- : Problem-solution pair. """ qpgurdu, _ = get_qpgurdu() A = qpgurdu.G b = 0.1 * qpgurdu.h problem = Problem(qpgurdu.P, qpgurdu.q, A=A, b=b) solution = Solution(problem) solution.found = True return problem, solution ================================================ FILE: qpsolvers/py.typed ================================================ ================================================ FILE: qpsolvers/solution.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Output from a QP solver.""" from dataclasses import dataclass, field from typing import Optional import numpy as np from .problem import Problem @dataclass(frozen=False) class Solution: """Solution returned by a QP solver for a given problem. Attributes ---------- extras : Other outputs, specific to each solver. found : True if the solution was found successfully by a solver, False if the solver did not find a solution or detected an unfeasible problem, ``None`` if no solver was run. problem : Quadratic program the solution corresponds to. obj : Value of the primal objective at the solution (``None`` if no solution was found). x : Solution vector for the primal quadratic program (``None`` if no solution was found). y : Dual multipliers for equality constraints (``None`` if no solution was found, or if there is no equality constraint). The dimension of :math:`y` is equal to the number of equality constraints. The values :math:`y_i` can be either positive or negative. z : Dual multipliers for linear inequality constraints (``None`` if no solution was found, or if there is no inequality constraint). The dimension of :math:`z` is equal to the number of inequalities. The value :math:`z_i` for inequality :math:`i` is always positive. - If :math:`z_i > 0`, the inequality is active at the solution: :math:`G_i x = h_i`. - If :math:`z_i = 0`, the inequality is inactive at the solution: :math:`G_i x < h_i`. z_box : Dual multipliers for box inequality constraints (``None`` if no solution was found, or if there is no box inequality). The sign of :math:`z_{box,i}` depends on the active bound: - If :math:`z_{box,i} < 0`, then the lower bound :math:`lb_i = x_i` is active at the solution. - If :math:`z_{box,i} = 0`, then neither the lower nor the upper bound are active and :math:`lb_i < x_i < ub_i`. - If :math:`z_{box,i} > 0`, then the upper bound :math:`x_i = ub_i` is active at the solution. build_time : Time taken, during the call to `solve_problem`, to build problem matrices in the QP solver's expected format. solve_time : Time taken, during the call to `solve_problem`, to call the QP solver itself. """ problem: Problem extras: dict = field(default_factory=dict) found: Optional[bool] = None obj: Optional[float] = None x: Optional[np.ndarray] = None y: Optional[np.ndarray] = None z: Optional[np.ndarray] = None z_box: Optional[np.ndarray] = None build_time: Optional[float] = None solve_time: Optional[float] = None def is_optimal(self, eps_abs: float) -> bool: """Check whether the solution is indeed optimal. Parameters ---------- eps_abs : Absolute tolerance for the primal residual, dual residual and duality gap. Notes ----- See for instance [Caron2022]_ for an overview of optimality conditions in quadratic programming. """ return ( self.primal_residual() < eps_abs and self.dual_residual() < eps_abs and self.duality_gap() < eps_abs ) def primal_residual(self) -> float: r"""Compute the primal residual of the solution. The primal residual is: .. math:: r_p := \max(\| A x - b \|_\infty, [G x - h]^+, [lb - x]^+, [x - ub]^+) were :math:`v^- = \min(v, 0)` and :math:`v^+ = \max(v, 0)`. Returns ------- : Primal residual if it is defined, ``np.inf`` otherwise. Notes ----- See for instance [Caron2022]_ for an overview of optimality conditions and why this residual will be zero at the optimum. """ _, _, G, h, A, b, lb, ub = self.problem.unpack() if not self.found or self.x is None: return np.inf x = self.x return max( [ 0.0, np.max(G.dot(x) - h) if G is not None else 0.0, np.max(np.abs(A.dot(x) - b)) if A is not None else 0.0, np.max(lb - x) if lb is not None else 0.0, np.max(x - ub) if ub is not None else 0.0, ] ) def dual_residual(self) -> float: r"""Compute the dual residual of the solution. The dual residual is: .. math:: r_d := \| P x + q + A^T y + G^T z + z_{box} \|_\infty Returns ------- : Dual residual if it is defined, ``np.inf`` otherwise. Notes ----- See for instance [Caron2022]_ for an overview of optimality conditions and why this residual will be zero at the optimum. """ P, q, G, _, A, _, lb, ub = self.problem.unpack() if not self.found or self.x is None: return np.inf zeros = np.zeros(self.x.shape) Px = P.dot(self.x) ATy = zeros if A is not None: if self.y is None: return np.inf ATy = A.T.dot(self.y) GTz = zeros if G is not None: if self.z is None: return np.inf GTz = G.T.dot(self.z) z_box = zeros if lb is not None or ub is not None: if self.z_box is None: return np.inf z_box = self.z_box p = np.linalg.norm(Px + q + GTz + ATy + z_box, np.inf) return p # type: ignore def duality_gap(self) -> float: r"""Compute the duality gap of the solution. The duality gap is: .. math:: r_g := | x^T P x + q^T x + b^T y + h^T z + lb^T z_{box}^- + ub^T z_{box}^+ | were :math:`v^- = \min(v, 0)` and :math:`v^+ = \max(v, 0)`. Returns ------- : Duality gap if it is defined, ``np.inf`` otherwise. Notes ----- See for instance [Caron2022]_ for an overview of optimality conditions and why this gap will be zero at the optimum. """ P, q, _, h, _, b, lb, ub = self.problem.unpack() if not self.found or self.x is None: return np.inf xPx = self.x.T.dot(P.dot(self.x)) qx = q.dot(self.x) hz = 0.0 if h is not None: if self.z is None: return np.inf hz = h.dot(self.z) by = 0.0 if b is not None: if self.y is None: return np.inf by = b.dot(self.y) lb_z_box = 0.0 ub_z_box = 0.0 if self.z_box is not None: if lb is not None: finite = np.asarray(lb != -np.inf).nonzero() z_box_neg = np.minimum(self.z_box, 0.0) lb_z_box = lb[finite].dot(z_box_neg[finite]) if ub is not None: finite = np.asarray(ub != np.inf).nonzero() z_box_pos = np.maximum(self.z_box, 0.0) ub_z_box = ub[finite].dot(z_box_pos[finite]) return abs(xPx + qx + hz + by + lb_z_box + ub_z_box) ================================================ FILE: qpsolvers/solve_ls.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Solve linear least squares.""" from typing import Optional, Union import numpy as np import scipy.sparse as spa from .problem import Problem from .solve_qp import solve_qp def __solve_dense_ls( R: Union[np.ndarray, spa.csc_matrix], s: np.ndarray, G: Optional[Union[np.ndarray, spa.csc_matrix]] = None, h: Optional[np.ndarray] = None, A: Optional[Union[np.ndarray, spa.csc_matrix]] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, W: Optional[Union[np.ndarray, spa.csc_matrix]] = None, solver: Optional[str] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Optional[np.ndarray]: WR: Union[np.ndarray, spa.csc_matrix] = ( R if W is None else W @ R # type: ignore[assignment] ) P_: Union[np.ndarray, spa.csc_matrix] = ( R.T @ WR # type: ignore[assignment] ) P: Union[np.ndarray, spa.csc_matrix] = ( P_ if isinstance(P_, np.ndarray) else P_.tocsc() ) q = -(s.T @ WR) return solve_qp( P, q, G, h, A, b, lb, ub, solver=solver, initvals=initvals, verbose=verbose, **kwargs, ) def __solve_sparse_ls( R: Union[np.ndarray, spa.csc_matrix], s: np.ndarray, G: Optional[Union[np.ndarray, spa.csc_matrix]] = None, h: Optional[np.ndarray] = None, A: Optional[Union[np.ndarray, spa.csc_matrix]] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, W: Optional[Union[np.ndarray, spa.csc_matrix]] = None, solver: Optional[str] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Optional[np.ndarray]: m, n = R.shape eye_m = spa.eye(m, format="csc") q = np.zeros(n + m) # We know the RHS of this assignment is CSC from the format kwarg P_: spa.csc_matrix = spa.block_diag( # type: ignore[assignment] [spa.csc_matrix((n, n)), eye_m if W is None else W], format="csc", ) P, q, G, h, A, b, lb, ub = Problem(P_, q, G, h, A, b, lb, ub).unpack() if G is not None: G = spa.hstack( # type: ignore[call-overload] [G, spa.csc_matrix((G.shape[0], m))], format="csc", ) if A is not None: A = spa.hstack( # type: ignore[call-overload] [A, spa.csc_matrix((A.shape[0], m))], format="csc", ) Rx_minus_y = spa.hstack( # type: ignore[call-overload] [R, -eye_m], format="csc", ) if A is not None and b is not None: # help mypy A = spa.vstack( # type: ignore[call-overload] [A, Rx_minus_y], format="csc", ) b = np.hstack([b, s]) else: # no input equality constraint A = Rx_minus_y b = s if lb is not None: lb = np.hstack([lb, np.full((m,), -np.inf)]) if ub is not None: ub = np.hstack([ub, np.full((m,), np.inf)]) xy = solve_qp( P, q, G, h, A, b, lb, ub, solver=solver, initvals=initvals, verbose=verbose, **kwargs, ) return xy[:n] if xy is not None else None def solve_ls( R: Union[np.ndarray, spa.csc_matrix], s: np.ndarray, G: Optional[Union[np.ndarray, spa.csc_matrix]] = None, h: Optional[np.ndarray] = None, A: Optional[Union[np.ndarray, spa.csc_matrix]] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, W: Optional[Union[np.ndarray, spa.csc_matrix]] = None, solver: Optional[str] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, sparse_conversion: Optional[bool] = None, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a constrained weighted linear Least Squares problem. The linear least squares is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac12 \| R x - s \|^2_W = \frac12 (R x - s)^T W (R x - s) \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} using the QP solver selected by the ``solver`` keyword argument. Parameters ---------- R : Union[np.ndarray, spa.csc_matrix] factor of the cost function (most solvers require :math:`R^T W R` to be positive definite, which means :math:`R` should have full row rank). s : Vector term of the cost function. G : Linear inequality matrix. h : Linear inequality vector. A : Linear equality matrix. b : Linear equality vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. W : Definite symmetric weight matrix used to define the norm of the cost function. The standard L2 norm (W = Identity) is used by default. solver : Name of the QP solver, to choose in :data:`qpsolvers.available_solvers`. This argument is mandatory. initvals : Vector of initial `x` values used to warm-start the solver. verbose : Set to `True` to print out extra information. sparse_conversion : Set to `True` to use a sparse conversion strategy and to `False` to use a dense strategy. By default, the conversion strategy to follow is determined by the sparsity of :math:`R` (sparse if CSC matrix, dense otherwise). See Notes below. Returns ------- : Optimal solution if found, otherwise ``None``. Note ---- Some solvers (like quadprog) will require a full-rank matrix :math:`R`, while others (like ProxQP or QPALM) can work even when :math:`R` has a non-empty nullspace. Notes ----- This function implements two strategies to convert the least-squares cost :math:`(R, s)` to a quadratic-programming cost :math:`(P, q)`: one that assumes :math:`R` is dense, and one that assumes :math:`R` is sparse. These two strategies are detailed in `this note `__. The sparse strategy introduces extra variables :math;`y = R x` and will likely perform better on sparse problems, although this may not always be the case (for instance, it may perform worse if :math:`R` has many more rows than columns). Extra keyword arguments given to this function are forwarded to the underlying solvers. For example, OSQP has a setting `eps_abs` which we can provide by ``solve_ls(R, s, G, h, solver='osqp', eps_abs=1e-4)``. """ if sparse_conversion is None: sparse_conversion = not isinstance(R, np.ndarray) if sparse_conversion: return __solve_sparse_ls( R, s, G, h, A, b, lb, ub, W, solver, initvals, verbose, **kwargs ) return __solve_dense_ls( R, s, G, h, A, b, lb, ub, W, solver, initvals, verbose, **kwargs ) ================================================ FILE: qpsolvers/solve_problem.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Solve quadratic programs.""" from typing import Optional import numpy as np from .exceptions import SolverNotFound from .problem import Problem from .solution import Solution from .solvers import available_solvers, solve_function def solve_problem( problem: Problem, solver: str, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Solution: r"""Solve a quadratic program using a given solver. Parameters ---------- problem : Quadratic program to solve. solver : Name of the solver, to choose in :data:`qpsolvers.available_solvers`. initvals : Primal candidate vector :math:`x` values used to warm-start the solver. verbose : Set to ``True`` to print out extra information. Note ---- In quadratic programming, the matrix :math:`P` should be symmetric. Many solvers (including CVXOPT, OSQP and quadprog) assume this is the case and may return unintended results when the provided matrix is not. Thus, make sure you matrix is indeed symmetric before calling this function, for instance by projecting it on its symmetric part :math:`S = \frac{1}{2} (P + P^T)`. Returns ------- : Solution found by the solver, if any, along with solver-specific return values. Raises ------ SolverNotFound If the requested solver is not in :data:`qpsolvers.available_solvers`. ValueError If the problem is not correctly defined. For instance, if the solver requires a definite cost matrix but the provided matrix :math:`P` is not. Notes ----- Extra keyword arguments given to this function are forwarded to the underlying solver. For example, we can call OSQP with a custom absolute feasibility tolerance by ``solve_problem(problem, solver='osqp', eps_abs=1e-6)``. See the :ref:`Supported solvers ` page for details on the parameters available to each solver. There is no guarantee that a ``ValueError`` is raised if the provided problem is non-convex, as some solvers don't check for this. Rather, if the problem is non-convex and the solver fails because of that, then a ``ValueError`` will be raised. """ problem.check_constraints() kwargs["initvals"] = initvals kwargs["verbose"] = verbose try: return solve_function[solver](problem, **kwargs) except KeyError as e: raise SolverNotFound( f"'{solver}' does not seem to be installed " f"(found solvers: {available_solvers}); if '{solver}' is " "listed in https://github.com/qpsolvers/qpsolvers#solvers " f"you can install it by `pip install qpsolvers[{solver}]`" ) from e ================================================ FILE: qpsolvers/solve_qp.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Solve quadratic programs.""" from typing import Optional, Union import numpy as np import scipy.sparse as spa from .exceptions import NoSolverSelected from .problem import Problem from .solve_problem import solve_problem from .solvers import available_solvers def solve_qp( P: Union[np.ndarray, spa.csc_matrix], q: np.ndarray, G: Optional[Union[np.ndarray, spa.csc_matrix]] = None, h: Optional[np.ndarray] = None, A: Optional[Union[np.ndarray, spa.csc_matrix]] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, solver: Optional[str] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} using the QP solver selected by the ``solver`` keyword argument. Parameters ---------- P : Symmetric cost matrix (most solvers require it to be definite as well). q : Cost vector. G : Linear inequality matrix. h : Linear inequality vector. A : Linear equality matrix. b : Linear equality vector. lb : Lower bound constraint vector. Can contain ``-np.inf``. ub : Upper bound constraint vector. Can contain ``+np.inf``. solver : Name of the QP solver, to choose in :data:`qpsolvers.available_solvers`. This argument is mandatory. initvals : Primal candidate vector :math:`x` values used to warm-start the solver. verbose : Set to ``True`` to print out extra information. Note ---- In quadratic programming, the matrix :math:`P` should be symmetric. Many solvers (including CVXOPT, OSQP and quadprog) leverage this property and may return unintended results when it is not the case. You can set project :math:`P` on its symmetric part by: .. code:: python P = 0.5 * (P + P.transpose()) Some solvers (like quadprog) will further require that :math:`P` is definite, while other solvers (like ProxQP or QPALM) can work with semi-definite matrices. Returns ------- : Optimal solution if found, otherwise ``None``. Raises ------ NoSolverSelected If the ``solver`` keyword argument is not set. ParamError If any solver parameter is incorrect. ProblemError If the problem is not correctly defined. For instance, if the solver requires a definite cost matrix but the provided matrix :math:`P` is not. SolverError If the solver failed during its execution. SolverNotFound If the requested solver is not in :data:`qpsolvers.available_solvers`. Notes ----- Extra keyword arguments given to this function are forwarded to the underlying solver. For example, we can call OSQP with a custom absolute feasibility tolerance by ``solve_qp(P, q, G, h, solver='osqp', eps_abs=1e-6)``. See the :ref:`Supported solvers ` page for details on the parameters available to each solver. There is no guarantee that a ``ValueError`` is raised if the provided problem is non-convex, as some solvers don't check for this. Rather, if the problem is non-convex and the solver fails because of that, then a ``ValueError`` will be raised. """ if solver is None: raise NoSolverSelected( "Set the `solver` keyword argument to one of the " f"available solvers in {available_solvers}" ) problem = Problem(P, q, G, h, A, b, lb, ub) solution = solve_problem(problem, solver, initvals, verbose, **kwargs) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solve_unconstrained.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2023 Inria """Solve an unconstrained quadratic program.""" import numpy as np from scipy.sparse.linalg import lsqr from .exceptions import ProblemError from .problem import Problem from .solution import Solution def solve_unconstrained(problem: Problem) -> Solution: """Solve an unconstrained quadratic program with SciPy's LSQR. Parameters ---------- problem : Unconstrained quadratic program. Returns ------- : Solution to the unconstrained QP, if it is bounded. Raises ------ ValueError If the quadratic program is not unbounded below. """ P, q, _, _, _, _, _, _ = problem.unpack() solution = Solution(problem) solution.x = lsqr(P, -q)[0] cost_check = np.linalg.norm(P @ solution.x + q) if cost_check > 1e-8: raise ProblemError( f"problem is unbounded below (cost_check={cost_check:.1e}), " "q has component in the nullspace of P" ) solution.found = True return solution ================================================ FILE: qpsolvers/solvers/__init__.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Import available QP solvers.""" import warnings from typing import Any, Callable, Dict, List, Optional, Union from numpy import ndarray from scipy.sparse import csc_matrix from ..problem import Problem from ..solution import Solution available_solvers: List[str] = [] dense_solvers: List[str] = [] solve_function: Dict[str, Any] = {} sparse_solvers: List[str] = [] # Clarabel.rs # =========== clarabel_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], bool, ], Solution, ] ] = None clarabel_solve_qp: Optional[ Callable[ [ Union[ndarray, csc_matrix], ndarray, Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], bool, ], Optional[ndarray], ] ] = None try: from .clarabel_ import clarabel_solve_problem, clarabel_solve_qp solve_function["clarabel"] = clarabel_solve_problem available_solvers.append("clarabel") sparse_solvers.append("clarabel") except ImportError: pass # CVXOPT # ====== cvxopt_solve_problem: Optional[ Callable[ [ Problem, Optional[str], Optional[ndarray], bool, ], Solution, ] ] = None cvxopt_solve_qp: Optional[ Callable[ [ Union[ndarray, csc_matrix], ndarray, Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[str], Optional[ndarray], bool, ], Optional[ndarray], ] ] = None try: from .cvxopt_ import cvxopt_solve_problem, cvxopt_solve_qp solve_function["cvxopt"] = cvxopt_solve_problem available_solvers.append("cvxopt") dense_solvers.append("cvxopt") sparse_solvers.append("cvxopt") except ImportError: pass # DAQP # ======== daqp_solve_qp: Optional[ Callable[ [ ndarray, ndarray, Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], bool, ], Optional[ndarray], ] ] = None daqp_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], bool, ], Solution, ] ] = None try: from .daqp_ import daqp_solve_problem, daqp_solve_qp solve_function["daqp"] = daqp_solve_problem available_solvers.append("daqp") dense_solvers.append("daqp") except ImportError: pass # ECOS # ==== ecos_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], bool, ], Solution, ] ] = None ecos_solve_qp: Optional[ Callable[ [ ndarray, ndarray, Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], bool, ], Optional[ndarray], ] ] = None try: from .ecos_ import ecos_solve_problem, ecos_solve_qp solve_function["ecos"] = ecos_solve_problem available_solvers.append("ecos") dense_solvers.append("ecos") # considered dense as it calls cholesky(P) except ImportError: pass # Gurobi # ====== gurobi_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], bool, ], Solution, ] ] = None gurobi_solve_qp: Optional[ Callable[ [ ndarray, ndarray, Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], bool, ], Optional[ndarray], ] ] = None try: from .gurobi_ import gurobi_solve_problem, gurobi_solve_qp solve_function["gurobi"] = gurobi_solve_problem available_solvers.append("gurobi") sparse_solvers.append("gurobi") except ImportError: pass # COPT # ====== copt_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], bool, ], Solution, ] ] = None copt_solve_qp: Optional[ Callable[ [ ndarray, ndarray, Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], bool, ], Optional[ndarray], ] ] = None try: from .copt_ import copt_solve_problem, copt_solve_qp solve_function["copt"] = copt_solve_problem available_solvers.append("copt") sparse_solvers.append("copt") except ImportError: pass # HiGHS # ===== highs_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], bool, ], Solution, ] ] = None highs_solve_qp: Optional[ Callable[ [ Union[ndarray, csc_matrix], ndarray, Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], bool, ], Optional[ndarray], ] ] = None try: from .highs_ import highs_solve_problem, highs_solve_qp solve_function["highs"] = highs_solve_problem available_solvers.append("highs") sparse_solvers.append("highs") except ImportError: pass # HPIPM # ===== hpipm_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], str, bool, ], Solution, ] ] = None hpipm_solve_qp: Optional[ Callable[ [ ndarray, ndarray, Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], str, bool, ], Optional[ndarray], ] ] = None try: from .hpipm_ import hpipm_solve_problem, hpipm_solve_qp solve_function["hpipm"] = hpipm_solve_problem available_solvers.append("hpipm") dense_solvers.append("hpipm") except ImportError: pass # jaxopt.OSQP # ========== jaxopt_osqp_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], bool, ], Solution, ] ] = None jaxopt_osqp_solve_qp: Optional[ Callable[ [ ndarray, ndarray, Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], bool, ], Optional[ndarray], ] ] = None try: from .jaxopt_osqp_ import jaxopt_osqp_solve_problem, jaxopt_osqp_solve_qp solve_function["jaxopt_osqp"] = jaxopt_osqp_solve_problem available_solvers.append("jaxopt_osqp") dense_solvers.append("jaxopt_osqp") except ImportError: pass # KVXOPT # ====== kvxopt_solve_problem: Optional[ Callable[ [ Problem, Optional[str], Optional[ndarray], bool, ], Solution, ] ] = None kvxopt_solve_qp: Optional[ Callable[ [ Union[ndarray, csc_matrix], ndarray, Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[str], Optional[ndarray], bool, ], Optional[ndarray], ] ] = None try: from .kvxopt_ import kvxopt_solve_problem, kvxopt_solve_qp solve_function["kvxopt"] = kvxopt_solve_problem available_solvers.append("kvxopt") dense_solvers.append("kvxopt") sparse_solvers.append("kvxopt") except ImportError: pass # MOSEK # ===== mosek_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], bool, ], Solution, ] ] = None mosek_solve_qp: Optional[ Callable[ [ Union[ndarray, csc_matrix], ndarray, Union[ndarray, csc_matrix], ndarray, Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], bool, ], Optional[ndarray], ] ] = None try: from .mosek_ import mosek_solve_problem, mosek_solve_qp solve_function["mosek"] = mosek_solve_problem available_solvers.append("mosek") sparse_solvers.append("mosek") except ImportError: pass # NPPro # ===== nppro_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], ], Solution, ] ] = None nppro_solve_qp: Optional[ Callable[ [ ndarray, ndarray, Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], ], Optional[ndarray], ] ] = None try: from .nppro_ import nppro_solve_problem, nppro_solve_qp solve_function["nppro"] = nppro_solve_problem available_solvers.append("nppro") dense_solvers.append("nppro") except ImportError: pass # OSQP # ==== osqp_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], bool, ], Solution, ] ] = None osqp_solve_qp: Optional[ Callable[ [ Union[ndarray, csc_matrix], ndarray, Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], bool, ], Optional[ndarray], ] ] = None try: from .osqp_ import osqp_solve_problem, osqp_solve_qp solve_function["osqp"] = osqp_solve_problem available_solvers.append("osqp") sparse_solvers.append("osqp") except ImportError: pass # PDHCG # ===== pdhcg_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], bool, ], Solution, ] ] = None pdhcg_solve_qp: Optional[ Callable[ [ Union[ndarray, csc_matrix], ndarray, Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], bool, ], Optional[ndarray], ] ] = None try: from .pdhcg_ import pdhcg_solve_problem, pdhcg_solve_qp solve_function["pdhcg"] = pdhcg_solve_problem available_solvers.append("pdhcg") dense_solvers.append("pdhcg") sparse_solvers.append("pdhcg") except ImportError: pass # PIQP # ======= piqp_solve_qp: Optional[ Callable[ [ Union[ndarray, csc_matrix], ndarray, Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], bool, Optional[str], ], Optional[ndarray], ] ] = None piqp_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], bool, Optional[str], ], Solution, ] ] = None try: from .piqp_ import piqp_solve_problem, piqp_solve_qp solve_function["piqp"] = piqp_solve_problem available_solvers.append("piqp") dense_solvers.append("piqp") sparse_solvers.append("piqp") except ImportError: pass # ProxQP # ======= proxqp_solve_qp: Optional[ Callable[ [ Union[ndarray, csc_matrix], ndarray, Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], bool, Optional[str], ], Optional[ndarray], ] ] = None proxqp_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], bool, Optional[str], ], Solution, ] ] = None try: from .proxqp_ import proxqp_solve_problem, proxqp_solve_qp solve_function["proxqp"] = proxqp_solve_problem available_solvers.append("proxqp") dense_solvers.append("proxqp") sparse_solvers.append("proxqp") except ImportError: pass # QPALM # ===== qpalm_solve_qp: Optional[ Callable[ [ Union[ndarray, csc_matrix], ndarray, Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], bool, ], Optional[ndarray], ] ] = None qpalm_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], bool, ], Solution, ] ] = None try: from .qpalm_ import qpalm_solve_problem, qpalm_solve_qp solve_function["qpalm"] = qpalm_solve_problem available_solvers.append("qpalm") sparse_solvers.append("qpalm") except ImportError: pass # qpax # ======== qpax_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], bool, ], Solution, ] ] = None qpax_solve_qp: Optional[ Callable[ [ ndarray, ndarray, Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], bool, ], Optional[ndarray], ] ] = None try: from .qpax_ import qpax_solve_problem, qpax_solve_qp solve_function["qpax"] = qpax_solve_problem available_solvers.append("qpax") dense_solvers.append("qpax") except ImportError: pass # qpOASES # ======= qpoases_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], bool, int, Optional[float], ], Solution, ] ] = None qpoases_solve_qp: Optional[ Callable[ [ ndarray, ndarray, Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], bool, int, Optional[float], ], Optional[ndarray], ] ] = None try: from .qpoases_ import qpoases_solve_problem, qpoases_solve_qp solve_function["qpoases"] = qpoases_solve_problem available_solvers.append("qpoases") dense_solvers.append("qpoases") except ImportError: pass # qpSWIFT # ======= qpswift_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], bool, ], Solution, ] ] = None qpswift_solve_qp: Optional[ Callable[ [ ndarray, ndarray, Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], bool, ], Optional[ndarray], ] ] = None try: from .qpswift_ import qpswift_solve_problem, qpswift_solve_qp solve_function["qpswift"] = qpswift_solve_problem available_solvers.append("qpswift") dense_solvers.append("qpswift") except ImportError: pass # QTQP # ==== qtqp_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], bool, ], Solution, ] ] = None qtqp_solve_qp: Optional[ Callable[ [ Union[ndarray, csc_matrix], ndarray, Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], bool, ], Optional[ndarray], ] ] = None try: from .qtqp_ import qtqp_solve_problem, qtqp_solve_qp solve_function["qtqp"] = qtqp_solve_problem available_solvers.append("qtqp") sparse_solvers.append("qtqp") except ImportError: pass # quadprog # ======== quadprog_solve_qp: Optional[ Callable[ [ ndarray, ndarray, Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], bool, ], Optional[ndarray], ] ] = None quadprog_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], bool, ], Solution, ] ] = None try: from .quadprog_ import quadprog_solve_problem, quadprog_solve_qp solve_function["quadprog"] = quadprog_solve_problem available_solvers.append("quadprog") dense_solvers.append("quadprog") except ImportError: pass # pyqpmad # ======= pyqpmad_solve_qp: Optional[ Callable[ [ ndarray, ndarray, Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], bool, ], Optional[ndarray], ] ] = None pyqpmad_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], bool, ], Solution, ] ] = None try: from .pyqpmad_ import pyqpmad_solve_problem, pyqpmad_solve_qp solve_function["pyqpmad"] = pyqpmad_solve_problem available_solvers.append("pyqpmad") dense_solvers.append("pyqpmad") except ImportError: pass # SCS # ======== scs_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], bool, ], Solution, ] ] = None scs_solve_qp: Optional[ Callable[ [ Union[ndarray, csc_matrix], ndarray, Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], bool, ], Optional[ndarray], ] ] = None try: from .scs_ import scs_solve_problem, scs_solve_qp solve_function["scs"] = scs_solve_problem available_solvers.append("scs") sparse_solvers.append("scs") except ImportError: pass # SIP # ======== sip_solve_problem: Optional[ Callable[ [ Problem, Optional[ndarray], bool, bool, ], Solution, ] ] = None sip_solve_qp: Optional[ Callable[ [ Union[ndarray, csc_matrix], ndarray, Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[Union[ndarray, csc_matrix]], Optional[ndarray], Optional[ndarray], Optional[ndarray], Optional[ndarray], bool, bool, ], Optional[ndarray], ] ] = None try: from .sip_ import sip_solve_problem, sip_solve_qp solve_function["sip"] = sip_solve_problem available_solvers.append("sip") sparse_solvers.append("sip") except ImportError: pass if not available_solvers: warnings.warn( "no QP solver found on your system, " "you can install solvers from PyPI by " "``pip install qpsolvers[open_source_solvers]``" ) __all__ = [ "available_solvers", "clarabel_solve_qp", "copt_solve_qp", "cvxopt_solve_qp", "daqp_solve_qp", "dense_solvers", "ecos_solve_qp", "gurobi_solve_qp", "highs_solve_qp", "hpipm_solve_qp", "jaxopt_osqp_solve_qp", "kvxopt_solve_qp", "mosek_solve_qp", "nppro_solve_qp", "osqp_solve_qp", "pdhcg_solve_qp", "piqp_solve_qp", "proxqp_solve_qp", "pyqpmad_solve_qp", "qpalm_solve_qp", "qpax_solve_qp", "qpoases_solve_qp", "qpswift_solve_qp", "qtqp_solve_qp", "quadprog_solve_qp", "scs_solve_qp", "sip_solve_qp", "solve_function", "sparse_solvers", ] ================================================ FILE: qpsolvers/solvers/clarabel_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2023 Inria """Solver interface for `Clarabel.rs`_. .. _Clarabel.rs: https://github.com/oxfordcontrol/Clarabel.rs Clarabel.rs is a Rust implementation of an interior point numerical solver for convex optimization problems using a novel homogeneous embedding. A paper describing the Clarabel solver algorithm and implementation will be forthcoming soon (retrieved: 2023-02-06). Until then, the authors ask that you cite its documentation if you have found Clarabel.rs useful in your work. **Warm-start:** this solver interface does not support warm starting ❄️ """ import time import warnings from typing import Optional, Union import clarabel import numpy as np import scipy.sparse as spa from ..conversions import ( ensure_sparse_matrices, linear_from_box_inequalities, split_dual_linear_box, ) from ..exceptions import ProblemError from ..problem import Problem from ..solution import Solution from ..solve_unconstrained import solve_unconstrained def clarabel_solve_problem( problem: Problem, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Solution: r"""Solve a quadratic program using Clarabel.rs. Parameters ---------- problem : Quadratic program to solve. initvals : This argument is not used by Clarabel. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP, if found, otherwise ``None``. Notes ----- Keyword arguments are forwarded as options to Clarabel.rs. For instance, we can call ``clarabel_solve_qp(P, q, G, h, u, tol_feas=1e-6)``. Clarabel options include the following: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``max_iter`` - Maximum number of iterations. * - ``time_limit`` - Time limit for solve run in seconds (can be fractional). * - ``tol_gap_abs`` - absolute duality-gap tolerance * - ``tol_gap_rel`` - relative duality-gap tolerance * - ``tol_feas`` - feasibility check tolerance (primal and dual) Check out the `API reference `_ for details. Lower values for absolute or relative tolerances yield more precise solutions at the cost of computation time. See *e.g.* [Caron2022]_ for a primer on solver tolerances and residuals. """ build_start_time = time.perf_counter() if initvals is not None and verbose: warnings.warn("Clarabel: warm-start values are ignored") P, q, G, h, A, b, lb, ub = problem.unpack() P, G, A = ensure_sparse_matrices("clarabel", P, G, A) if lb is not None or ub is not None: G, h = linear_from_box_inequalities(G, h, lb, ub, use_sparse=True) cones = [] A_list: list = [] b_list = [] if A is not None and b is not None: A_list.append(A) b_list.append(b) cones.append(clarabel.ZeroConeT(b.shape[0])) if G is not None and h is not None: A_list.append(G) b_list.append(h) cones.append(clarabel.NonnegativeConeT(h.shape[0])) if not A_list: warnings.warn( "QP is unconstrained: " "solving with SciPy's LSQR rather than clarabel" ) return solve_unconstrained(problem) settings = clarabel.DefaultSettings() settings.verbose = verbose for key, value in kwargs.items(): setattr(settings, key, value) A_stack = spa.vstack(A_list, format="csc") b_stack = np.concatenate(b_list) try: solver = clarabel.DefaultSolver( P, q, A_stack, b_stack, cones, settings ) except BaseException as exn: # The one we want to catch is a pyo3_runtime.PanicException # But see https://github.com/PyO3/pyo3/issues/2880 raise ProblemError("Solver failed to build problem") from exn solve_start_time = time.perf_counter() result = solver.solve() solve_end_time = time.perf_counter() solution = Solution(problem) solution.obj = result.obj_val solution.extras = { "s": result.s, "status": result.status, "solve_time": result.solve_time, } solution.found = result.status == clarabel.SolverStatus.Solved if not solution.found: warnings.warn(f"Clarabel.rs terminated with status {result.status}") solution.x = np.array(result.x) meq = A.shape[0] if A is not None else 0 solution.y = result.z[:meq] if meq > 0 else np.empty((0,)) if G is not None: z, z_box = split_dual_linear_box(np.array(result.z[meq:]), lb, ub) solution.z = z solution.z_box = z_box else: # G is None solution.z = np.empty((0,)) solution.z_box = np.empty((0,)) solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def clarabel_solve_qp( P: Union[np.ndarray, spa.csc_matrix], q: np.ndarray, G: Optional[Union[np.ndarray, spa.csc_matrix]] = None, h: Optional[np.ndarray] = None, A: Optional[Union[np.ndarray, spa.csc_matrix]] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using Clarabel.rs. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using `Clarabel.rs`_. Parameters ---------- P : Symmetric cost matrix. q : Cost vector. G : Linear inequality matrix. h : Linear inequality vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. initvals : This argument is not used by Clarabel. verbose : Set to `True` to print out extra information. Returns ------- : Primal solution to the QP, if found, otherwise ``None``. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = clarabel_solve_problem(problem, initvals, verbose, **kwargs) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/copt_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Solver interface for `COPT `__. The COPT Optimizer suite ships several solvers for mathematical programming, including problems that have linear constraints, bound constraints, integrality constraints, cone constraints, or quadratic constraints. It targets modern CPU/GPU architectures and multi-core processors, See the :ref:`installation page ` for additional instructions on installing this solver. """ import time import warnings from typing import Optional, Sequence, Union import coptpy import numpy as np import scipy.sparse as spa from coptpy import COPT from ..problem import Problem from ..solution import Solution def copt_solve_problem( problem: Problem, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Solution: """Solve a quadratic program using COPT. Parameters ---------- problem : Quadratic program to solve. initvals : This argument is not used by COPT. verbose : Set to `True` to print out extra information. Returns ------- : Solution returned by the solver. Notes ----- Keyword arguments are forwarded to COPT as parameters. For instance, we can call ``copt_solve_qp(P, q, G, h, u, FeasTol=1e-8, DualTol=1e-8)``. COPT settings include the following: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``FeasTol`` - Primal feasibility tolerance. * - ``DualTol`` - Dual feasibility tolerance. * - ``TimeLimit`` - Run time limit in seconds, 0 to disable. Check out the `Parameter Descriptions `_ documentation for all available COPT parameters. Lower values for primal or dual tolerances yield more precise solutions at the cost of computation time. See *e.g.* [Caron2022]_ for a primer of solver tolerances. """ build_start_time = time.perf_counter() if initvals is not None: warnings.warn("warm-start values are ignored by this wrapper") env_config = coptpy.EnvrConfig() if not verbose: env_config.set("nobanner", "1") env = coptpy.Envr(env_config) model = env.createModel() if not verbose: model.setParam(COPT.Param.Logging, 0) for param, value in kwargs.items(): model.setParam(param, value) P, q, G, h, A, b, lb, ub = problem.unpack() num_vars = P.shape[0] identity = np.eye(num_vars) x = model.addMVar( num_vars, lb=-COPT.INFINITY, ub=COPT.INFINITY, vtype=COPT.CONTINUOUS ) ineq_constr, eq_constr, lb_constr, ub_constr = None, None, None, None if G is not None and h is not None: ineq_constr = model.addMConstr( G, # type: ignore[arg-type] x, COPT.LESS_EQUAL, h, ) if A is not None and b is not None: eq_constr = model.addMConstr( A, # type: ignore[arg-type] x, COPT.EQUAL, b, ) if lb is not None: lb_constr = model.addMConstr(identity, x, COPT.GREATER_EQUAL, lb) if ub is not None: ub_constr = model.addMConstr(identity, x, COPT.LESS_EQUAL, ub) objective = 0.5 * (x @ P @ x) + q @ x # type: ignore[operator] model.setObjective(objective, sense=COPT.MINIMIZE) solve_start_time = time.perf_counter() model.solve() solve_end_time = time.perf_counter() solution = Solution(problem) solution.extras["status"] = model.status solution.found = model.status in (COPT.OPTIMAL, COPT.IMPRECISE) if solution.found: # COPT v8.0.0+ Changed the default Python matrix modeling API from # `numpy` to its own implementation. `coptpy.NdArray` does not support # operators such as ">=", so convert to `np.ndarray` solution.x = __to_numpy(x.X) # type: ignore[attr-defined] __retrieve_dual(solution, ineq_constr, eq_constr, lb_constr, ub_constr) solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def __retrieve_dual( solution: Solution, ineq_constr: Optional[coptpy.MConstr], eq_constr: Optional[coptpy.MConstr], lb_constr: Optional[coptpy.MConstr], ub_constr: Optional[coptpy.MConstr], ) -> None: solution.z = ( __to_numpy(-ineq_constr.Pi) # type: ignore[attr-defined] if ineq_constr is not None else np.empty((0,)) ) solution.y = ( __to_numpy(-eq_constr.Pi) # type: ignore[attr-defined] if eq_constr is not None else np.empty((0,)) ) if lb_constr is not None and ub_constr is not None: solution.z_box = __to_numpy( -ub_constr.Pi - lb_constr.Pi # type: ignore[attr-defined] ) elif ub_constr is not None: # lb_constr is None solution.z_box = __to_numpy( -ub_constr.Pi # type: ignore[attr-defined] ) elif lb_constr is not None: # ub_constr is None solution.z_box = __to_numpy( -lb_constr.Pi # type: ignore[attr-defined] ) else: # lb_constr is None and ub_constr is None solution.z_box = np.empty((0,)) def copt_solve_qp( P: Union[np.ndarray, spa.csc_matrix], q: np.ndarray, G: Optional[Union[np.ndarray, spa.csc_matrix]] = None, h: Optional[np.ndarray] = None, A: Optional[Union[np.ndarray, spa.csc_matrix]] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using COPT. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using `COPT `__. Parameters ---------- P : Primal quadratic cost matrix. q : Primal quadratic cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. initvals : This argument is not used by COPT. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP, if found, otherwise ``None``. Notes ----- Keyword arguments are forwarded to COPT as parameters. For instance, we can call ``copt_solve_qp(P, q, G, h, u, FeasTol=1e-8, DualTol=1e-8)``. COPT settings include the following: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``FeasTol`` - Primal feasibility tolerance. * - ``DualTol`` - Dual feasibility tolerance. * - ``TimeLimit`` - Run time limit in seconds, 0 to disable. Check out the `Parameter Descriptions `_ documentation for all available COPT parameters. Lower values for primal or dual tolerances yield more precise solutions at the cost of computation time. See *e.g.* [Caron2022]_ for a primer of solver tolerances. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = copt_solve_problem(problem, initvals, verbose, **kwargs) return solution.x if solution.found else None def __to_numpy( array_like: Union[ coptpy.NdArray, np.ndarray, float, int, Sequence[Union[float, int]] ], ) -> np.ndarray: """Convert COPT NdArray or array-like objects to numpy ndarray. This function ensures compatibility with COPT v8+, which changed the default Python matrix modeling API from numpy to its own implementation (``coptpy.NdArray``). Parameters ---------- array_like : Input array to convert. Supported types: - ``coptpy.NdArray`` from COPT v8+ (converted via ``tonumpy()``) - ``np.ndarray`` (returned as-is to avoid redundant copy) - Scalar values (float, int) → converted to 1-element 1D numpy array - Sequence types (list, tuple) of floats/ints → converted to 1D numpy array Returns ------- : Numpy array representation of the input (1D for scalars/sequences, same shape for COPT/numpy arrays). Raises ------ TypeError If the input type is not supported (e.g., dict, None, non-numeric sequence). RuntimeError If conversion from coptpy.NdArray to numpy fails (e.g., invalid COPT array). Notes ----- COPT v8.0.0+ uses ``coptpy.NdArray`` by default, which does not support operators such as ``>=``. This function converts such arrays to ``np.ndarray`` for further processing. Numpy arrays are returned as-is to avoid unnecessary memory copies. Examples -------- >>> # Convert COPT NdArray to numpy (when coptpy is available) >>> # copt_array = coptpy.NdArray([1.0, 2.0, 3.0]) # doctest: +SKIP >>> # np_array = __to_numpy(copt_array) # doctest: +SKIP >>> # isinstance(np_array, np.ndarray) # doctest: +SKIP >>> # True # doctest: +SKIP >>> # Convert scalar to 1D numpy array >>> __to_numpy(5.0).shape (1,) >>> # Convert list to numpy array >>> __to_numpy([1, 2, 3]).shape (3,) """ if array_like is None: raise TypeError( "Input 'array_like' cannot be None. Supported types: " "coptpy.NdArray, np.ndarray, float, int, list/tuple of numbers." ) if isinstance(array_like, np.ndarray): return array_like if isinstance(array_like, coptpy.NdArray): try: return array_like.tonumpy() except Exception as e: raise RuntimeError( f"Failed to convert coptpy.NdArray to numpy array: {str(e)}" ) from e try: if isinstance(array_like, (int, float)): return np.asarray([array_like]) if isinstance(array_like, (list, tuple)): return np.asarray(array_like) except Exception as e: raise RuntimeError( "Failed to convert input to numpy array. Input type: " f"{type(array_like).__name__}, error: {str(e)}" ) from e raise TypeError( f"Unsupported type '{type(array_like).__name__}' for 'array_like'. " f"Supported types: coptpy.NdArray, np.ndarray, float, int, list/tuple " "of numbers." ) ================================================ FILE: qpsolvers/solvers/cvxopt_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Solver interface for `CVXOPT `__. CVXOPT is a free software package for convex optimization in Python. Its main purpose is to make the development of software for convex optimization applications straightforward by building on Python’s extensive standard library and on the strengths of Python as a high-level programming language. If you are using CVXOPT in a scientific work, consider citing the corresponding report [Vandenberghe2010]_. **Warm-start:** this solver interface supports warm starting 🔥 """ import time import warnings from typing import Dict, Optional, Union import cvxopt import numpy as np import scipy.sparse as spa from cvxopt.solvers import qp from ..conversions import linear_from_box_inequalities, split_dual_linear_box from ..exceptions import ProblemError, SolverError from ..problem import Problem from ..solution import Solution cvxopt.solvers.options["show_progress"] = False # disable default verbosity def __to_cvxopt( M: Union[np.ndarray, spa.csc_matrix], ) -> Union[cvxopt.matrix, cvxopt.spmatrix]: """Convert matrix to CVXOPT format. Parameters ---------- M : Matrix in NumPy or CVXOPT format. Returns ------- : Matrix in CVXOPT format. """ if isinstance(M, np.ndarray): __infty__ = 1e10 # 1e20 tends to yield division-by-zero errors M_noinf = np.nan_to_num(M, posinf=__infty__, neginf=-__infty__) return cvxopt.matrix(M_noinf) coo = M.tocoo() return cvxopt.spmatrix( coo.data.tolist(), coo.row.tolist(), coo.col.tolist(), size=M.shape ) def cvxopt_solve_problem( problem: Problem, solver: Optional[str] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Solution: r"""Solve a quadratic program using CVXOPT. Parameters ---------- problem : Quadratic program to solve. solver : Set to 'mosek' to run MOSEK rather than CVXOPT. initvals : Warm-start guess vector. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP, if found, otherwise ``None``. Raises ------ ProblemError If the CVXOPT rank assumption is not satisfied. SolverError If CVXOPT failed with an error. Note ---- .. _CVXOPT rank assumptions: **Rank assumptions:** CVXOPT requires the QP matrices to satisfy the .. math:: \begin{split}\begin{array}{cc} \mathrm{rank}(A) = p & \mathrm{rank}([P\ A^T\ G^T]) = n \end{array}\end{split} where :math:`p` is the number of rows of :math:`A` and :math:`n` is the number of optimization variables. See the "Rank assumptions" paragraph in the report `The CVXOPT linear and quadratic cone program solvers `_ for details. Notes ----- CVXOPT only considers the lower entries of :math:`P`, therefore it will use a different cost than the one intended if a non-symmetric matrix is provided. Keyword arguments are forwarded as options to CVXOPT. For instance, we can call ``cvxopt_solve_qp(P, q, G, h, u, abstol=1e-4, reltol=1e-4)``. CVXOPT options include the following: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``abstol`` - Absolute tolerance on the duality gap. * - ``feastol`` - Tolerance on feasibility conditions, that is, on the primal residual. * - ``maxiters`` - Maximum number of iterations. * - ``refinement`` - Number of iterative refinement steps when solving KKT equations * - ``reltol`` - Relative tolerance on the duality gap. Check out `Algorithm Parameters `_ section of the solver documentation for details and default values of all solver parameters. See also [Caron2022]_ for a primer on the duality gap, primal and dual residuals. """ build_start_time = time.perf_counter() P, q, G, h, A, b, lb, ub = problem.unpack() if lb is not None or ub is not None: G, h = linear_from_box_inequalities( G, h, lb, ub, use_sparse=problem.has_sparse ) args = [__to_cvxopt(P), __to_cvxopt(q)] constraints = {"G": None, "h": None, "A": None, "b": None} if G is not None and h is not None: constraints["G"] = __to_cvxopt(G) constraints["h"] = __to_cvxopt(h) if A is not None and b is not None: constraints["A"] = __to_cvxopt(A) constraints["b"] = __to_cvxopt(b) initvals_dict: Optional[Dict[str, cvxopt.matrix]] = None if initvals is not None: if "mosek" in kwargs: warnings.warn("MOSEK: warm-start values are ignored") initvals_dict = {"x": __to_cvxopt(initvals)} kwargs["show_progress"] = verbose try: solve_start_time = time.perf_counter() res = qp( *args, solver=solver, initvals=initvals_dict, options=kwargs, **constraints, ) solve_end_time = time.perf_counter() except ValueError as exception: error = str(exception) if "Rank(A)" in error: raise ProblemError(error) from exception raise SolverError(error) from exception solution = Solution(problem) solution.extras = res solution.found = "optimal" in res["status"] solution.x = np.array(res["x"]).flatten() solution.y = ( np.array(res["y"]).flatten() if b is not None else np.empty((0,)) ) if h is not None and res["z"] is not None: z_cvxopt = np.array(res["z"]).flatten() if z_cvxopt.size == h.size: z, z_box = split_dual_linear_box(z_cvxopt, lb, ub) solution.z = z solution.z_box = z_box else: # h is None solution.z = np.empty((0,)) solution.z_box = np.empty((0,)) solution.obj = res["primal objective"] solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def cvxopt_solve_qp( P: Union[np.ndarray, spa.csc_matrix], q: np.ndarray, G: Optional[Union[np.ndarray, spa.csc_matrix]] = None, h: Optional[np.ndarray] = None, A: Optional[Union[np.ndarray, spa.csc_matrix]] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, solver: Optional[str] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using CVXOPT. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using `CVXOPT `__. Parameters ---------- P : Symmetric cost matrix. Together with :math:`A` and :math:`G`, it should satisfy :math:`\mathrm{rank}([P\ A^T\ G^T]) = n`, see the rank assumptions below. q : Cost vector. G : Linear inequality matrix. Together with :math:`P` and :math:`A`, it should satisfy :math:`\mathrm{rank}([P\ A^T\ G^T]) = n`, see the rank assumptions below. h : Linear inequality vector. A : Linear equality constraint matrix. It needs to be full row rank, and together with :math:`P` and :math:`G` satisfy :math:`\mathrm{rank}([P\ A^T\ G^T]) = n`. See the rank assumptions below. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. solver : Set to 'mosek' to run MOSEK rather than CVXOPT. initvals : Warm-start guess vector. verbose : Set to `True` to print out extra information. Returns ------- : Primal solution to the QP, if found, otherwise ``None``. Raises ------ ProblemError If the CVXOPT rank assumption is not satisfied. SolverError If CVXOPT failed with an error. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = cvxopt_solve_problem( problem, solver, initvals, verbose, **kwargs ) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/daqp_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Solver interface for `DAQP `__. DAQP is a dual active-set algorithm implemented in C [Arnstrom2022]_. It has been developed to solve small/medium scale dense problems. **Warm-start:** this solver interface supports warm starting 🔥 """ import time import warnings from ctypes import c_int from typing import Optional import daqp import numpy as np from ..problem import Problem from ..solution import Solution def daqp_solve_problem( problem: Problem, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Solution: """Solve a quadratic program using DAQP. Parameters ---------- problem : Quadratic program to solve. initvals : Warm-start guess vector for the primal solution. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP, if found, otherwise ``None``. Notes ----- Keyword arguments are forwarded to DAQP. For instance, we can call ``daqp_solve_qp(P, q, G, h, u, primal_tol=1e-6, iter_limit=1000)``. DAQP settings include the following: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``iter_limit`` - Maximum number of iterations. * - ``primal_tol`` - Primal feasibility tolerance. * - ``dual_tol`` - Dual feasibility tolerance. Check out the `DAQP settings `_ documentation for all available settings. """ build_start_time = time.perf_counter() if initvals is not None and verbose: warnings.warn("warm-start values are ignored by DAQP") H, f, G, h, A, b, lb, ub = problem.unpack_as_dense() # Determine constraint counts upfront to allow single pre-allocation meq = A.shape[0] if A is not None else 0 mineq = G.shape[0] if G is not None else 0 if ub is not None: ms = ub.size elif lb is not None: ms = lb.size else: ms = 0 mtot = ms + mineq + meq # Build bupper/blower. When there are no box constraints and only one # constraint block, reuse the existing arrays directly (zero extra copy). if ms == 0 and (mineq == 0 or meq == 0): bupper = h if (mineq > 0) else (b if meq > 0 else np.zeros(0)) blower = np.full(mineq + meq, -1e30) else: bupper = np.empty(mtot) blower = np.full(mtot, -1e30) if ms > 0: bupper[:ms] = ub if ub is not None else 1e30 if lb is not None: blower[:ms] = lb if mineq > 0: bupper[ms : ms + mineq] = h if meq > 0: bupper[ms + mineq :] = b # Build constraint matrix; stack only when both blocks are present if mineq > 0 and meq > 0: Atot = np.empty((mineq + meq, f.size)) Atot[:mineq] = G Atot[mineq:] = A elif mineq > 0: Atot = G # type: ignore[assignment] elif meq > 0: Atot = A # type: ignore[assignment] else: Atot = np.zeros((0, f.size)) sense = np.zeros(mtot, dtype=c_int) sense[ms + mineq :] = 5 solve_start_time = time.perf_counter() x, obj, exitflag, info = daqp.solve( H, f, Atot, bupper, blower, sense, primal_start=initvals, **kwargs ) solve_end_time = time.perf_counter() solution = Solution(problem) solution.found = exitflag > 0 if exitflag > 0: solution.x = x solution.obj = obj solution.z_box = info["lam"][:ms] solution.z = info["lam"][ms : ms + mineq] solution.y = info["lam"][ms + mineq :] solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def daqp_solve_qp( P: np.ndarray, q: np.ndarray, G: Optional[np.ndarray] = None, h: Optional[np.ndarray] = None, A: Optional[np.ndarray] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using DAQP. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using `DAQP `__. Parameters ---------- P : Symmetric cost matrix. q : Cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. initvals : Warm-start guess vector for the primal solution. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP, if found, otherwise ``None``. Notes ----- Keyword arguments are forwarded to DAQP. For instance, we can call ``daqp_solve_qp(P, q, G, h, u, primal_tol=1e-6, iter_limit=1000)``. DAQP settings include the following: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``iter_limit`` - Maximum number of iterations. * - ``primal_tol`` - Primal feasibility tolerance. * - ``dual_tol`` - Dual feasibility tolerance. * - ``time_limit`` - Time limit for solve run in seconds. Check out the `DAQP settings `_ documentation for all available settings. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = daqp_solve_problem(problem, initvals, verbose, **kwargs) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/ecos_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Solver interface for `ECOS `__. ECOS is an interior-point solver for convex second-order cone programs (SOCPs). designed specifically for embedded applications. ECOS is written in low footprint, single-threaded, library-free ANSI-C and so runs on most embedded platforms. For small problems, ECOS is faster than most existing SOCP solvers; it is still competitive for medium-sized problems up to tens of thousands of variables. If you are using ECOS in a scientific work, consider citing the corresponding paper [Domahidi2013]_. **Warm-start:** this solver interface does not support warm starting ❄️ """ import time import warnings from typing import Optional, Union import ecos import numpy as np from scipy import sparse as spa from ..conversions import ( linear_from_box_inequalities, socp_from_qp, split_dual_linear_box, ) from ..exceptions import ProblemError from ..problem import Problem from ..solution import Solution __exit_flag_meaning__ = { 0: "OPTIMAL", 1: "PINF: found certificate of primal infeasibility", 2: "DING: found certificate of dual infeasibility", 10: "INACC_OFFSET: inaccurate results", 11: "PINF_INACC: found inaccurate certificate of primal infeasibility", 12: "DING_INACC: found inaccurate certificate of dual infeasibility", -1: "MAXIT: maximum number of iterations reached", -2: "NUMERICS: search direction is unreliable", -3: "OUTCONE: primal or dual variables got outside of cone", -4: "SIGINT: solver interrupted", -7: "FATAL: unknown solver problem", } def ecos_solve_problem( problem: Problem, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Solution: """Solve a quadratic program using ECOS. Parameters ---------- P : Primal quadratic cost matrix. q : Primal quadratic cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. initvals : This argument is not used by ECOS. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP, if found, otherwise ``None``. Raises ------ ProblemError : If inequality constraints contain infinite values that the solver doesn't handle. ValueError : If the cost matrix is not positive definite. Notes ----- All other keyword arguments are forwarded as options to the ECOS solver. For instance, you can call ``qpswift_solve_qp(P, q, G, h, abstol=1e-5)``. For a quick overview, the solver accepts the following settings: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Effect * - ``feastol`` - Tolerance on the primal and dual residual. * - ``abstol`` - Absolute tolerance on the duality gap. * - ``reltol`` - Relative tolerance on the duality gap. * - ``feastol_inacc`` - Tolerance on the primal and dual residual if reduced precisions. * - ``abstol_inacc`` - Absolute tolerance on the duality gap if reduced precision. * - ``reltolL_inacc`` - Relative tolerance on the duality gap if reduced precision. * - ``max_iters`` - Maximum numer of iterations. * - ``nitref`` - Number of iterative refinement steps. See the `ECOS Python wrapper documentation `_ for more details. You can also check out [Caron2022]_ for a primer on primal-dual residuals or the duality gap. """ build_start_time = time.perf_counter() if initvals is not None: warnings.warn("warm-start values are ignored by this wrapper") P_, q, G_, h, A, b, lb, ub = problem.unpack() # P and G should be dense for socp_from_qp, but A should be sparse P: np.ndarray = P_.toarray() if isinstance(P_, spa.csc_matrix) else P_ G: Optional[np.ndarray] = ( G_.toarray() if isinstance(G_, spa.csc_matrix) else G_ ) if lb is not None or ub is not None: G2, h = linear_from_box_inequalities(G, h, lb, ub, use_sparse=False) G = G2.toarray() if isinstance(G2, spa.csc_matrix) else G2 # for mypy kwargs.update({"verbose": verbose}) c_socp, G_socp, h_socp, dims = socp_from_qp(P, q, G, h) if A is not None: A_sparse = ( spa.csc_matrix(A) if not isinstance(A, spa.csc_matrix) else A ) A_socp = spa.hstack( [A_sparse, spa.csc_matrix((A.shape[0], 1))], format="csc" ) solve_start_time = time.perf_counter() result = ecos.solve(c_socp, G_socp, h_socp, dims, A_socp, b, **kwargs) solve_end_time = time.perf_counter() else: solve_start_time = time.perf_counter() result = ecos.solve(c_socp, G_socp, h_socp, dims, **kwargs) solve_end_time = time.perf_counter() flag = result["info"]["exitFlag"] solution = Solution(problem) solution.extras = result["info"] solution.found = flag == 0 if not solution.found: if h is not None and not np.isfinite(h).all(): raise ProblemError( "ECOS does not handle infinite values in inequality vectors, " "try clipping them to a finite value suitable to your problem" ) meaning = __exit_flag_meaning__.get(flag, "unknown exit flag") warnings.warn(f"ECOS returned exit flag {flag} ({meaning})") solution.x = result["x"][:-1] if A is not None: solution.y = result["y"] if G is not None: z_ecos = result["z"][: G.shape[0]] z, z_box = split_dual_linear_box(z_ecos, lb, ub) solution.z = z solution.z_box = z_box solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def ecos_solve_qp( P: Union[np.ndarray, spa.csc_matrix], q: np.ndarray, G: Optional[Union[np.ndarray, spa.csc_matrix]] = None, h: Optional[np.ndarray] = None, A: Optional[Union[np.ndarray, spa.csc_matrix]] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using ECOS. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \end{array}\end{split} It is solved using `ECOS `__. Parameters ---------- P : Primal quadratic cost matrix. q : Primal quadratic cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. initvals : This argument is not used by ECOS. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP, if found, otherwise ``None``. Notes ----- All other keyword arguments are forwarded as options to the ECOS solver. For instance, you can call ``ecos_solve_qp(P, q, G, h, abstol=1e-5)``. For a quick overview, the solver accepts the following settings: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Effect * - ``feastol`` - Tolerance on the primal and dual residual. * - ``abstol`` - Absolute tolerance on the duality gap. * - ``reltol`` - Relative tolerance on the duality gap. * - ``feastol_inacc`` - Tolerance on the primal and dual residual if reduced precisions. * - ``abstol_inacc`` - Absolute tolerance on the duality gap if reduced precision. * - ``reltolL_inacc`` - Relative tolerance on the duality gap if reduced precision. * - ``max_iters`` - Maximum numer of iterations. * - ``nitref`` - Number of iterative refinement steps. See the `ECOS Python wrapper documentation `_ for more details. You can also check out [Caron2022]_ for a primer on primal-dual residuals or the duality gap. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = ecos_solve_problem(problem, initvals, verbose, **kwargs) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/gurobi_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors # Copyright 2021 Dustin Kenefake """Solver interface for `Gurobi `__. The Gurobi Optimizer suite ships several solvers for mathematical programming, including problems that have linear constraints, bound constraints, integrality constraints, cone constraints, or quadratic constraints. It targets modern CPU architectures and multi-core processors, See the :ref:`installation page ` for additional instructions on installing this solver. **Warm-start:** this solver interface does not support warm starting ❄️ """ import time import warnings from typing import Optional, Union import gurobipy import numpy as np import scipy.sparse as spa from gurobipy import GRB from ..problem import Problem from ..solution import Solution def gurobi_solve_problem( problem: Problem, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Solution: """Solve a quadratic program using Gurobi. Parameters ---------- problem : Quadratic program to solve. initvals : This argument is not used by Gurobi. verbose : Set to `True` to print out extra information. Returns ------- : Solution returned by the solver. Notes ----- Keyword arguments are forwarded to Gurobi as parameters. For instance, we can call ``gurobi_solve_qp(P, q, G, h, u, FeasibilityTol=1e-8, OptimalityTol=1e-8)``. Gurobi settings include the following: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``FeasibilityTol`` - Primal feasibility tolerance. * - ``OptimalityTol`` - Dual feasibility tolerance. * - ``PSDTol`` - Positive semi-definite tolerance. * - ``TimeLimit`` - Run time limit in seconds, 0 to disable. Check out the `Parameter Descriptions `__ documentation for all available Gurobi parameters. Lower values for primal or dual tolerances yield more precise solutions at the cost of computation time. See *e.g.* [Caron2022]_ for a primer of solver tolerances. """ build_start_time = time.perf_counter() if initvals is not None: warnings.warn("warm-start values are ignored by this wrapper") model = gurobipy.Model() if not verbose: model.setParam(GRB.Param.OutputFlag, 0) for param, value in kwargs.items(): model.setParam(param, value) P, q, G, h, A, b, lb, ub = problem.unpack() num_vars = P.shape[0] identity = spa.eye(num_vars) x = model.addMVar( num_vars, lb=-GRB.INFINITY, ub=GRB.INFINITY, vtype=GRB.CONTINUOUS ) ineq_constr, eq_constr, lb_constr, ub_constr = None, None, None, None if G is not None and h is not None: ineq_constr = model.addMConstr( G, # type: ignore[arg-type] x, GRB.LESS_EQUAL, h, ) if A is not None and b is not None: eq_constr = model.addMConstr( A, # type: ignore[arg-type] x, GRB.EQUAL, b, ) if lb is not None: lb_constr = model.addMConstr( identity, # type: ignore[call-overload] x, GRB.GREATER_EQUAL, lb, ) if ub is not None: ub_constr = model.addMConstr( identity, # type: ignore[call-overload] x, GRB.LESS_EQUAL, ub, ) objective = 0.5 * (x @ P @ x) + q @ x # type: ignore[operator] model.setObjective(objective, sense=GRB.MINIMIZE) solve_start_time = time.perf_counter() model.optimize() solve_end_time = time.perf_counter() solution = Solution(problem) solution.extras["status"] = model.status solution.found = model.status in (GRB.OPTIMAL, GRB.SUBOPTIMAL) if solution.found: solution.x = x.getAttr("X") __retrieve_dual(solution, ineq_constr, eq_constr, lb_constr, ub_constr) solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def __retrieve_dual( solution: Solution, ineq_constr: Optional[gurobipy.MConstr], eq_constr: Optional[gurobipy.MConstr], lb_constr: Optional[gurobipy.MConstr], ub_constr: Optional[gurobipy.MConstr], ) -> None: solution.z = ( -ineq_constr.getAttr("Pi") if ineq_constr is not None else np.empty((0,)) ) solution.y = ( -eq_constr.getAttr("Pi") if eq_constr is not None else np.empty((0,)) ) if lb_constr is not None and ub_constr is not None: solution.z_box = -ub_constr.getAttr("Pi") - lb_constr.getAttr("Pi") elif ub_constr is not None: # lb_constr is None solution.z_box = -ub_constr.getAttr("Pi") elif lb_constr is not None: # ub_constr is None solution.z_box = -lb_constr.getAttr("Pi") else: # lb_constr is None and ub_constr is None solution.z_box = np.empty((0,)) def gurobi_solve_qp( P: Union[np.ndarray, spa.csc_matrix], q: np.ndarray, G: Optional[Union[np.ndarray, spa.csc_matrix]] = None, h: Optional[np.ndarray] = None, A: Optional[Union[np.ndarray, spa.csc_matrix]] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using Gurobi. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using `Gurobi `__. Parameters ---------- P : Primal quadratic cost matrix. q : Primal quadratic cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. initvals : This argument is not used by Gurobi. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP, if found, otherwise ``None``. Notes ----- Keyword arguments are forwarded to Gurobi as parameters. For instance, we can call ``gurobi_solve_qp(P, q, G, h, u, FeasibilityTol=1e-8, OptimalityTol=1e-8)``. Gurobi settings include the following: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``FeasibilityTol`` - Primal feasibility tolerance. * - ``OptimalityTol`` - Dual feasibility tolerance. * - ``PSDTol`` - Positive semi-definite tolerance. * - ``TimeLimit`` - Run time limit in seconds, 0 to disable. Check out the `Parameter Descriptions `__ documentation for all available Gurobi parameters. Lower values for primal or dual tolerances yield more precise solutions at the cost of computation time. See *e.g.* [Caron2022]_ for a primer of solver tolerances. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = gurobi_solve_problem(problem, initvals, verbose, **kwargs) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/highs_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Solver interface for `HiGHS `__. HiGHS is an open source, serial and parallel solver for large scale sparse linear programming (LP), mixed-integer programming (MIP), and quadratic programming (QP). It is written mostly in C++11 and available under the MIT licence. HiGHS's QP solver implements a Nullspace Active Set method. It works best on moderately-sized dense problems. If you are using HiGHS in a scientific work, consider citing the corresponding paper [Huangfu2018]_. **Warm-start:** this solver interface supports warm starting 🔥 """ import time import warnings from typing import Optional, Union import highspy import numpy as np import scipy.sparse as spa from ..conversions import ensure_sparse_matrices from ..problem import Problem from ..solution import Solution def __set_hessian(model: highspy.HighsModel, P: spa.csc_matrix) -> None: """Set Hessian :math:`Q` of the cost :math:`(1/2) x^T Q x + c^T x`. Parameters ---------- model : HiGHS model. P : Positive semidefinite cost matrix. """ P_lower = spa.tril(P, format="csc") model.hessian_.dim_ = P_lower.shape[0] model.hessian_.start_ = P_lower.indptr model.hessian_.index_ = P_lower.indices model.hessian_.value_ = P_lower.data def __set_columns( model: highspy.HighsModel, q: np.ndarray, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, ) -> None: r"""Set columns of the model. Columns consist of: - Linear part :math:`c` of the cost :math:`(1/2) x^T Q x + c^T x` - Box inequalities :math:`l \leq x \leq u`` Parameters ---------- model : HiGHS model. q : Cost vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. """ n = q.shape[0] lp = model.lp_ lp.num_col_ = n lp.col_cost_ = q lp.col_lower_ = lb if lb is not None else np.full((n,), -highspy.kHighsInf) lp.col_upper_ = ub if ub is not None else np.full((n,), highspy.kHighsInf) def __set_rows( model: highspy.HighsModel, G: Optional[spa.csc_matrix] = None, h: Optional[np.ndarray] = None, A: Optional[spa.csc_matrix] = None, b: Optional[np.ndarray] = None, ) -> None: r"""Set rows :math:`L \leq A x \leq U`` of the model. Parameters ---------- model : HiGHS model. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. """ lp = model.lp_ lp.num_row_ = 0 row_list: list = [] row_lower: list = [] row_upper: list = [] if G is not None: lp.num_row_ += G.shape[0] row_list.append(G) row_lower.append(np.full((G.shape[0],), -highspy.kHighsInf)) row_upper.append(h) if A is not None: lp.num_row_ += A.shape[0] row_list.append(A) row_lower.append(b) row_upper.append(b) if not row_list: return row_matrix = spa.vstack(row_list, format="csc") lp.a_matrix_.format_ = highspy.MatrixFormat.kColwise lp.a_matrix_.start_ = row_matrix.indptr lp.a_matrix_.index_ = row_matrix.indices lp.a_matrix_.value_ = row_matrix.data lp.a_matrix_.num_row_ = row_matrix.shape[0] lp.a_matrix_.num_col_ = row_matrix.shape[1] lp.row_lower_ = np.hstack(row_lower) lp.row_upper_ = np.hstack(row_upper) def highs_solve_problem( problem: Problem, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Solution: """Solve a quadratic program using HiGHS. Parameters ---------- problem : Quadratic program to solve. initvals : Warm-start guess vector for the primal solution. verbose : Set to `True` to print out extra information. Returns ------- : Solution returned by the solver. Notes ----- Keyword arguments are forwarded to HiGHS as options. For instance, we can call ``highs_solve_qp(P, q, G, h, u, primal_feasibility_tolerance=1e-8, dual_feasibility_tolerance=1e-8)``. HiGHS settings include the following: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``dual_feasibility_tolerance`` - Dual feasibility tolerance. * - ``primal_feasibility_tolerance`` - Primal feasibility tolerance. * - ``time_limit`` - Run time limit in seconds. Check out the `HiGHS documentation `_ for more information on the solver. """ build_start_time = time.perf_counter() if initvals is not None: warnings.warn( "HiGHS: warm-start values are not available for this solver, " "see: https://github.com/qpsolvers/qpsolvers/issues/94" ) P, q, G, h, A, b, lb, ub = problem.unpack() P, G, A = ensure_sparse_matrices("highs", P, G, A) model = highspy.HighsModel() __set_hessian(model, P) __set_columns(model, q, lb, ub) __set_rows(model, G, h, A, b) solver = highspy.Highs() if verbose: solver.setOptionValue("log_to_console", True) solver.setOptionValue("log_dev_level", highspy.HighsLogType.kVerbose) solver.setOptionValue( "highs_debug_level", highspy.HighsLogType.kVerbose ) else: # not verbose solver.setOptionValue("log_to_console", False) for option, value in kwargs.items(): solver.setOptionValue(option, value) solver.passModel(model) solve_start_time = time.perf_counter() solver.run() solve_end_time = time.perf_counter() result = solver.getSolution() model_status = solver.getModelStatus() solution = Solution(problem) solution.found = model_status == highspy.HighsModelStatus.kOptimal solution.x = np.array(result.col_value) if G is not None: solution.z = -np.array(result.row_dual[: G.shape[0]]) solution.y = ( -np.array(result.row_dual[G.shape[0] :]) if A is not None else np.empty((0,)) ) else: # G is None solution.z = np.empty((0,)) solution.y = ( -np.array(result.row_dual) if A is not None else np.empty((0,)) ) solution.z_box = ( -np.array(result.col_dual) if lb is not None or ub is not None else np.empty((0,)) ) solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def highs_solve_qp( P: Union[np.ndarray, spa.csc_matrix], q: np.ndarray, G: Optional[Union[np.ndarray, spa.csc_matrix]] = None, h: Optional[np.ndarray] = None, A: Optional[Union[np.ndarray, spa.csc_matrix]] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using HiGHS. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using `HiGHS `__. Parameters ---------- P : Positive semidefinite cost matrix. q : Cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. initvals : Warm-start guess vector for the primal solution. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP, if found, otherwise ``None``. Notes ----- Keyword arguments are forwarded to HiGHS as options. For instance, we can call ``highs_solve_qp(P, q, G, h, u, primal_feasibility_tolerance=1e-8, dual_feasibility_tolerance=1e-8)``. HiGHS settings include the following: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``dual_feasibility_tolerance`` - Dual feasibility tolerance. * - ``primal_feasibility_tolerance`` - Primal feasibility tolerance. * - ``time_limit`` - Run time limit in seconds. Check out the `HiGHS documentation `_ for more information on the solver. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = highs_solve_problem(problem, initvals, verbose, **kwargs) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/hpipm_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Solver interface for `HPIPM `__. HPIPM is a high-performance interior point method for solving convex quadratic programs. It is designed to be efficient for small to medium-size problems arising in model predictive control and embedded optmization. If you are using HPIPM in a scientific work, consider citing the corresponding paper [Frison2020]_. **Warm-start:** this solver interface supports warm starting 🔥 """ import time import warnings from typing import Optional import hpipm_python.common as hpipm import numpy as np from ..problem import Problem from ..solution import Solution def hpipm_solve_problem( problem: Problem, initvals: Optional[np.ndarray] = None, mode: str = "balance", verbose: bool = False, **kwargs, ) -> Solution: """Solve a quadratic program using HPIPM. Parameters ---------- problem : Quadratic program to solve. initvals : Warm-start guess vector for the primal solution. mode : Solver mode, which provides a set of default solver arguments. Pick one of ["speed_abs", "speed", "balance", "robust"]. These modes are documented in section 4.2 *IPM implementation choices* of the reference paper [Frison2020]_. The default one is "balance". verbose : Set to `True` to print out extra information. Returns ------- : Solution returned by the solver. Notes ----- Keyword arguments are forwarded to HPIPM. For instance, we can call ``hpipm_solve_qp(P, q, G, h, u, tol_eq=1e-5)``. HPIPM settings include the following: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``iter_max`` - Maximum number of iterations. * - ``tol_eq`` - Equality constraint tolerance. * - ``tol_ineq`` - Inequality constraint tolerance. * - ``tol_comp`` - Complementarity condition tolerance. * - ``tol_dual_gap`` - Duality gap tolerance. * - ``tol_stat`` - Stationarity condition tolerance. """ build_start_time = time.perf_counter() P, q, G, h, A, b, lb, ub = problem.unpack() if verbose: warnings.warn("verbose keyword argument is ignored by HPIPM") # setup the problem dimensions nv = q.shape[0] ne = b.shape[0] if b is not None else 0 ng = h.shape[0] if h is not None else 0 nlb = lb.shape[0] if lb is not None else 0 nub = ub.shape[0] if ub is not None else 0 nb = max(nlb, nub) dim = hpipm.hpipm_dense_qp_dim() dim.set("nv", nv) dim.set("nb", nb) dim.set("ne", ne) dim.set("ng", ng) # setup the problem data qp = hpipm.hpipm_dense_qp(dim) qp.set("H", P) qp.set("g", q) if ng > 0: qp.set("C", G) qp.set("ug", h) # mask out the lower bound qp.set("lg_mask", np.zeros_like(h, dtype=bool)) if ne > 0: qp.set("A", A) qp.set("b", b) if nb > 0: # mark all variables as box-constrained qp.set("idxb", np.arange(nv)) # need to mask out lb or ub if the box constraints are only one-sided # we also mask out infinities (and set the now-irrelevant value to # zero), since HPIPM doesn't like infinities if nlb > 0 and lb is not None: # help mypy lb_mask = np.isinf(lb) lb[lb_mask] = 0.0 qp.set("lb", lb) qp.set("lb_mask", ~lb_mask) else: qp.set("lb_mask", np.zeros(nb, dtype=bool)) if nub > 0 and ub is not None: # help mypy ub_mask = np.isinf(ub) ub[ub_mask] = 0.0 qp.set("ub", ub) qp.set("ub_mask", ~ub_mask) else: qp.set("ub_mask", np.zeros(nb, dtype=bool)) solver_args = hpipm.hpipm_dense_qp_solver_arg(dim, mode) for key, val in kwargs.items(): solver_args.set(key, val) sol = hpipm.hpipm_dense_qp_sol(dim) if initvals is not None: solver_args.set("warm_start", 1) sol.set("v", initvals) solver = hpipm.hpipm_dense_qp_solver(dim, solver_args) solve_start_time = time.perf_counter() solver.solve(qp, sol) solve_end_time = time.perf_counter() status = solver.get("status") solution = Solution(problem) solution.extras = { "status": status, "max_res_stat": solver.get("max_res_stat"), "max_res_eq": solver.get("max_res_eq"), "max_res_ineq": solver.get("max_res_ineq"), "max_res_comp": solver.get("max_res_comp"), "iter": solver.get("iter"), "stat": solver.get("stat"), } solution.found = status == 0 if not solution.found: warnings.warn(f"HPIPM exited with status '{status}'") # the equality multipliers in HPIPM are opposite in sign compared to what # we expect here solution.x = sol.get("v").flatten() solution.y = -sol.get("pi").flatten() if ne > 0 else np.empty((0,)) solution.z = sol.get("lam_ug").flatten() if ng > 0 else np.empty((0,)) if nb > 0: solution.z_box = (sol.get("lam_ub") - sol.get("lam_lb")).flatten() else: solution.z_box = np.empty((0,)) solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def hpipm_solve_qp( P: np.ndarray, q: np.ndarray, G: Optional[np.ndarray] = None, h: Optional[np.ndarray] = None, A: Optional[np.ndarray] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, initvals: Optional[np.ndarray] = None, mode: str = "balance", verbose: bool = False, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using HPIPM. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using `HPIPM `__. Parameters ---------- P : Symmetric cost matrix. q : Cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. initvals : Warm-start guess vector for the primal solution. mode : Solver mode, which provides a set of default solver arguments. Pick one of ["speed_abs", "speed", "balance", "robust"]. Default is "balance". verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP, if found, otherwise ``None``. Notes ----- Keyword arguments are forwarded to HPIPM. For instance, we can call ``hpipm_solve_qp(P, q, G, h, u, tol_eq=1e-5)``. HPIPM settings include the following: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``iter_max`` - Maximum number of iterations. * - ``tol_eq`` - Equality constraint tolerance. * - ``tol_ineq`` - Inequality constraint tolerance. * - ``tol_comp`` - Complementarity condition tolerance. * - ``tol_dual_gap`` - Duality gap tolerance. * - ``tol_stat`` - Stationarity condition tolerance. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = hpipm_solve_problem(problem, initvals, mode, verbose, **kwargs) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/jaxopt_osqp_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2024 Lev Kozlov """ Solver interface for jaxopt's implementation of the OSQP algorithm. JAXopt is a library of hardware-accelerated, batchable and differentiable optimizers implemented with JAX. JAX itself is a library for array-oriented numerical computation that provides automatic differentiation and just-in-time compilation. **Warm-start:** this solver interface does not support warm starting ❄️ """ import time import warnings from typing import Optional import jax.numpy as jnp import jaxopt import numpy as np from ..conversions import linear_from_box_inequalities, split_dual_linear_box from ..problem import Problem from ..solution import Solution from ..solve_unconstrained import solve_unconstrained def jaxopt_osqp_solve_problem( problem: Problem, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Solution: """Solve a quadratic program with the OSQP algorithm implemented in jaxopt. Parameters ---------- problem : Quadratic program to solve. initvals : This argument is not used by jaxopt.OSQP. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP returned by the solver. Notes ----- All other keyword arguments are forwarded as keyword arguments to jaxopt.OSQP. For instance, you can call ``jaxopt_osqp_solve_qp(P, q, G, h, sigma=1e-5, momentum=0.9)``. Note that JAX by default uses 32-bit floating point numbers, which can lead to numerical instability. If you encounter numerical issues, consider using 64-bit floating point numbers by setting its `jax_enable_x64` configuration. """ build_start_time = time.perf_counter() if problem.is_unconstrained: warnings.warn( "QP is unconstrained: " "solving with SciPy's LSQR rather than jaxopt's OSQP" ) return solve_unconstrained(problem) if initvals is not None and verbose: warnings.warn("warm-start values are ignored by this wrapper") P, q, G_0, h_0, A, b, lb, ub = problem.unpack() G, h = linear_from_box_inequalities(G_0, h_0, lb, ub, use_sparse=False) osqp = jaxopt.OSQP(**kwargs) solve_start_time = time.perf_counter() result = osqp.run( params_obj=(jnp.array(P), jnp.array(q)), params_eq=(jnp.array(A), jnp.array(b)) if A is not None else None, params_ineq=(jnp.array(G), jnp.array(h)) if G is not None else None, ) solve_end_time = time.perf_counter() solution = Solution(problem) solution.x = np.array(result.params.primal) solution.found = result.state.status == jaxopt.BoxOSQP.SOLVED solution.y = ( np.array(result.params.dual_eq) if result.params.dual_eq is not None else np.empty((0,)) ) # split the dual variables into # the box constraints and the linear constraints solution.z, solution.z_box = split_dual_linear_box( np.array(result.params.dual_ineq), problem.lb, problem.ub ) solution.extras = { "iter_num": int(result.state.iter_num), "error": float(result.state.error), "status": int(result.state.status), } solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def jaxopt_osqp_solve_qp( P: np.ndarray, q: np.ndarray, G: Optional[np.ndarray] = None, h: Optional[np.ndarray] = None, A: Optional[np.ndarray] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a QP with the OSQP algorithm implemented in jaxopt. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{\mbox{minimize}}{x} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using `jaxopt.OSQP `__. Parameters ---------- P : Positive semidefinite cost matrix. q : Cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. verbose : Set to `True` to print out extra information. initvals : This argument is not used by jaxopt.OSQP. Returns ------- : Primal solution to the QP, if found, otherwise ``None``. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = jaxopt_osqp_solve_problem(problem, initvals, verbose, **kwargs) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/kvxopt_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Solver interface for `KVXOPT `__. KVXOPT is a fork from CVXOPT including more SuiteSparse functions and KLU sparse matrix solver. As CVXOPT, it is a free, open-source interior-point solver. **Warm-start:** this solver interface supports warm starting 🔥 """ import time import warnings from typing import Dict, Optional, Union import kvxopt import numpy as np import scipy.sparse as spa from kvxopt.base import matrix as KVXOPTMatrix from kvxopt.base import spmatrix as KVXOPTSpmatrix from kvxopt.solvers import qp from ..conversions import linear_from_box_inequalities, split_dual_linear_box from ..exceptions import ProblemError, SolverError from ..problem import Problem from ..solution import Solution kvxopt.solvers.options["show_progress"] = False # disable default verbosity def __to_cvxopt( M: Union[np.ndarray, spa.csc_matrix], ) -> Union[KVXOPTMatrix, KVXOPTSpmatrix]: """Convert matrix to CVXOPT format. Parameters ---------- M : Matrix in NumPy or CVXOPT format. Returns ------- : Matrix in CVXOPT format. """ if isinstance(M, np.ndarray): __infty__ = 1e10 # 1e20 tends to yield division-by-zero errors M_noinf = np.nan_to_num(M, posinf=__infty__, neginf=-__infty__) return KVXOPTMatrix(M_noinf) coo = M.tocoo() return KVXOPTSpmatrix( coo.data.tolist(), coo.row.tolist(), coo.col.tolist(), size=M.shape ) def kvxopt_solve_problem( problem: Problem, solver: Optional[str] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Solution: r"""Solve a quadratic program using KVXOPT. Parameters ---------- problem : Quadratic program to solve. solver : Set to 'mosek' to run MOSEK rather than KVXOPT. initvals : Warm-start guess vector for the primal solution. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP, if found, otherwise ``None``. Raises ------ ProblemError If the KVXOPT rank assumption is not satisfied. SolverError If KVXOPT failed with an error. Note ---- .. _KVXOPT rank assumptions: **Rank assumptions:** KVXOPT requires the QP matrices to satisfy the .. math:: \begin{split}\begin{array}{cc} \mathrm{rank}(A) = p & \mathrm{rank}([P\ A^T\ G^T]) = n \end{array}\end{split} where :math:`p` is the number of rows of :math:`A` and :math:`n` is the number of optimization variables. See the "Rank assumptions" paragraph in the report `The CVXOPT linear and quadratic cone program solvers `_ for details. Notes ----- KVXOPT only considers the lower entries of :math:`P`, therefore it will use a different cost than the one intended if a non-symmetric matrix is provided. Keyword arguments are forwarded as options to KVXOPT. For instance, we can call ``kvxopt_solve_qp(P, q, G, h, u, abstol=1e-4, reltol=1e-4)``. KVXOPT options include the following: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``abstol`` - Absolute tolerance on the duality gap. * - ``feastol`` - Tolerance on feasibility conditions, that is, on the primal residual. * - ``maxiters`` - Maximum number of iterations. * - ``refinement`` - Number of iterative refinement steps when solving KKT equations * - ``reltol`` - Relative tolerance on the duality gap. Check out `Algorithm Parameters `_ section of the solver documentation for details and default values of all solver parameters. See also [Caron2022]_ for a primer on the duality gap, primal and dual residuals. """ build_start_time = time.perf_counter() P, q, G, h, A, b, lb, ub = problem.unpack() if lb is not None or ub is not None: G, h = linear_from_box_inequalities( G, h, lb, ub, use_sparse=problem.has_sparse ) args = [__to_cvxopt(P), __to_cvxopt(q)] constraints: Dict[str, Optional[Union[KVXOPTMatrix, KVXOPTSpmatrix]]] = { "G": None, "h": None, "A": None, "b": None, } if G is not None and h is not None: constraints["G"] = __to_cvxopt(G) constraints["h"] = __to_cvxopt(h) if A is not None and b is not None: constraints["A"] = __to_cvxopt(A) constraints["b"] = __to_cvxopt(b) initvals_dict: Optional[Dict[str, Union[KVXOPTMatrix, KVXOPTSpmatrix]]] initvals_dict = None if initvals is not None: if "mosek" in kwargs: warnings.warn("MOSEK: warm-start values are ignored") initvals_dict = {"x": __to_cvxopt(initvals)} kwargs["show_progress"] = verbose try: solve_start_time = time.perf_counter() res = qp( *args, solver=solver, # type: ignore[arg-type] initvals=initvals_dict, # type: ignore[arg-type] options=kwargs, **constraints, # type: ignore[arg-type] ) solve_end_time = time.perf_counter() except ValueError as exception: error = str(exception) if "Rank(A)" in error: raise ProblemError(error) from exception raise SolverError(error) from exception solution = Solution(problem) solution.extras = res solution.found = "optimal" in res["status"] # type: ignore[operator] solution.x = np.array(res["x"]).flatten() solution.y = ( np.array(res["y"]).flatten() if b is not None else np.empty((0,)) ) if h is not None and res["z"] is not None: z_cvxopt = np.array(res["z"]).flatten() if z_cvxopt.size == h.size: z, z_box = split_dual_linear_box(z_cvxopt, lb, ub) solution.z = z solution.z_box = z_box else: # h is None solution.z = np.empty((0,)) solution.z_box = np.empty((0,)) solution.obj = res["primal objective"] # type: ignore[assignment] solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def kvxopt_solve_qp( P: Union[np.ndarray, spa.csc_matrix], q: np.ndarray, G: Optional[Union[np.ndarray, spa.csc_matrix]] = None, h: Optional[np.ndarray] = None, A: Optional[Union[np.ndarray, spa.csc_matrix]] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, solver: Optional[str] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using KVXOPT. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using `KVXOPT `__. Parameters ---------- P : Symmetric cost matrix. Together with :math:`A` and :math:`G`, it should satisfy :math:`\mathrm{rank}([P\ A^T\ G^T]) = n`, see the rank assumptions below. q : Cost vector. G : Linear inequality matrix. Together with :math:`P` and :math:`A`, it should satisfy :math:`\mathrm{rank}([P\ A^T\ G^T]) = n`, see the rank assumptions below. h : Linear inequality vector. A : Linear equality constraint matrix. It needs to be full row rank, and together with :math:`P` and :math:`G` satisfy :math:`\mathrm{rank}([P\ A^T\ G^T]) = n`. See the rank assumptions below. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. solver : Set to 'mosek' to run MOSEK rather than KVXOPT. initvals : Warm-start guess vector for the primal solution. verbose : Set to `True` to print out extra information. Returns ------- : Primal solution to the QP, if found, otherwise ``None``. Raises ------ ProblemError If the KVXOPT rank assumption is not satisfied. SolverError If KVXOPT failed with an error. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = kvxopt_solve_problem( problem, solver, initvals, verbose, **kwargs ) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/mosek_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Solver interface for `MOSEK `__. MOSEK is a solver for linear, mixed-integer linear, quadratic, mixed-integer quadratic, quadratically constraint, conic and convex nonlinear mathematical optimization problems. Its interior-point method is geared towards large scale sparse problems, in particular for linear or conic quadratic programs. **Warm-start:** this solver interface supports warm starting 🔥 """ import time import warnings from typing import Optional, Union import mosek import numpy as np import scipy.sparse as spa from ..problem import Problem from ..solution import Solution from ..solve_unconstrained import solve_unconstrained from ..solvers.cvxopt_ import cvxopt_solve_problem def mosek_solve_problem( problem: Problem, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Solution: """Solve a quadratic program using MOSEK. Parameters ---------- P : Symmetric cost matrix. q : Cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. initvals : Warm-start guess vector for the primal solution. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP, if found, otherwise ``None``. """ build_start_time = time.perf_counter() if problem.is_unconstrained: warnings.warn( "QP is unconstrained: solving with SciPy's LSQR rather than MOSEK" ) return solve_unconstrained(problem) if "mosek" not in kwargs: kwargs["mosek"] = {} kwargs["mosek"][mosek.iparam.log] = 1 if verbose else 0 solve_start_time = time.perf_counter() solution = cvxopt_solve_problem(problem, "mosek", initvals, **kwargs) solve_end_time = time.perf_counter() solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def mosek_solve_qp( P: Union[np.ndarray, spa.csc_matrix], q: np.ndarray, G: Optional[Union[np.ndarray, spa.csc_matrix]] = None, h: Optional[np.ndarray] = None, A: Optional[Union[np.ndarray, spa.csc_matrix]] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using MOSEK. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using the `MOSEK interface from CVXOPT `_. Parameters ---------- P : Symmetric cost matrix. q : Cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. initvals : Warm-start guess vector for the primal solution. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP, if found, otherwise ``None``. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = mosek_solve_problem(problem, initvals, verbose, **kwargs) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/nppro_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2023 Stéphane Caron and the qpsolvers contributors """Solver interface for NPPro. The NPPro solver implements an enhanced Newton Projection with Proportioning method for strictly convex quadratic programming. Currently, it is designed for dense problems only. """ import time import warnings from typing import Optional import nppro import numpy as np from ..problem import Problem from ..solution import Solution def nppro_solve_problem( problem: Problem, initvals: Optional[np.ndarray] = None, **kwargs, ) -> Solution: """Solve a quadratic program using NPPro. Parameters ---------- problem : Quadratic program to solve. initvals : Warm-start guess vector. Returns ------- : Solution returned by the solver. Notes ----- All other keyword arguments are forwarded as options to NPPro. For instance, you can call ``nppro_solve_qp(P, q, G, h, MaxIter=15)``. For a quick overview, the solver accepts the following settings: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Effect * - ``MaxIter`` - Maximum number of iterations. * - ``SkipPreprocessing`` - Skip preprocessing phase or not. * - ``SkipPhaseOne`` - Skip feasible starting point finding or not. * - ``InfVal`` - Values are assumed to be infinite above this threshold. * - ``HessianUpdates`` - Enable Hessian updates or not. """ build_start_time = time.perf_counter() P, q, G, h, A, b, lb, ub = problem.unpack_as_dense() n = P.shape[0] m_iq = G.shape[0] if G is not None else 0 m_eq = A.shape[0] if A is not None else 0 m = m_iq + m_eq A_ = None l_ = None u_ = None lb_ = np.full(q.shape, -np.inf) ub_ = np.full(q.shape, +np.inf) if G is not None and h is not None: A_ = G l_ = np.full(h.shape, -np.inf) u_ = h if A is not None and b is not None: A_ = A if A_ is None else np.vstack([A_, A]) l_ = b if l_ is None else np.hstack([l_, b]) u_ = b if u_ is None else np.hstack([u_, b]) if lb is not None: lb_ = lb if ub is not None: ub_ = ub # Create solver object solver = nppro.CreateSolver(n, m) # Use options from input if provided, defaults otherwise max_iter = kwargs.get("MaxIter", 100) skip_preprocessing = kwargs.get("SkipPreprocessing", False) skip_phase_one = kwargs.get("SkipPhaseOne", False) inf_val = kwargs.get("InfVal", 1e16) hessian_updates = kwargs.get("HessianUpdates", True) # Set options solver.setOption_MaxIter(max_iter) solver.setOption_SkipPreprocessing(skip_preprocessing) solver.setOption_SkipPhaseOne(skip_phase_one) solver.setOption_InfVal(inf_val) solver.setOption_HessianUpdates(hessian_updates) x0 = np.full(q.shape, 0) if initvals is not None: x0 = initvals # Conversion to datatype supported by the solver's C++ interface P = np.asarray(P, order="C", dtype=np.float64) q = np.asarray(q, order="C", dtype=np.float64) A_ = np.asarray(A_, order="C", dtype=np.float64) l_ = np.asarray(l_, order="C", dtype=np.float64) u_ = np.asarray(u_, order="C", dtype=np.float64) lb_ = np.asarray(lb_, order="C", dtype=np.float64) ub_ = np.asarray(ub_, order="C", dtype=np.float64) x0 = np.asarray(x0, order="C", dtype=np.float64) # Call solver solve_start_time = time.perf_counter() x, fval, exitflag, iter_ = solver.solve(P, q, A_, l_, u_, lb_, ub_, x0) solve_end_time = time.perf_counter() # Store solution exitflag_success = 1 solution = Solution(problem) solution.found = exitflag == exitflag_success and not np.isnan(x).any() if not solution.found: # The second condition typically handle positive semi-definite cases # that are not catched by the solver yet warnings.warn(f"NPPro exited with status {exitflag}") solution.x = x solution.z = None # not available yet solution.y = None # not available yet solution.z_box = None # not available yet solution.extras = { "cost": fval, "iter": iter_, } solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def nppro_solve_qp( P: np.ndarray, q: np.ndarray, G: Optional[np.ndarray] = None, h: Optional[np.ndarray] = None, A: Optional[np.ndarray] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, initvals: Optional[np.ndarray] = None, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using NPPro. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using NPPro. Parameters ---------- P : Positive definite cost matrix. q : Cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. initvals : Warm-start guess vector. Returns ------- : Solution to the QP, if found, otherwise ``None``. Notes ----- See the Notes section in :func:`nppro_solve_problem`. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = nppro_solve_problem(problem, initvals, **kwargs) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/osqp_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Solver interface for `OSQP `__. The OSQP solver implements an operator-splitting method, more precisely an alternating direction method of multipliers (ADMM). It is designed for both dense and sparse problems, and convexity is the only assumption it makes on problem data (for instance, it does not make any rank assumption, contrary to solvers such as :ref:`CVXOPT ` or :ref:`qpSWIFT `). If you are using OSQP in a scientific work, consider citing the corresponding paper [Stellato2020]_. **Warm-start:** this solver interface supports warm starting 🔥 """ import time import warnings from typing import Optional, Union import numpy as np import scipy.sparse as spa from osqp import OSQP, SolverStatus from scipy.sparse import csc_matrix from ..conversions import ensure_sparse_matrices from ..problem import Problem from ..solution import Solution def osqp_solve_problem( problem: Problem, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Solution: """Solve a quadratic program using OSQP. Parameters ---------- problem : Quadratic program to solve. initvals : Warm-start guess vector for the primal solution. verbose : Set to `True` to print out extra information. Returns ------- : Solution returned by the solver. Raises ------ ValueError If the problem is clearly non-convex. See `this recommendation `_. Note that OSQP may find the problem unfeasible if the problem is slightly non-convex (in this context, the meaning of "clearly" and "slightly" depends on how close the negative eigenvalues of :math:`P` are to zero). Note ---- OSQP requires a symmetric `P` and won't check for errors otherwise. Check out this point if you `get nan values `_ in your solutions. Notes ----- Keyword arguments are forwarded to OSQP. For instance, we can call ``osqp_solve_qp(P, q, G, h, u, eps_abs=1e-8, eps_rel=0.0)``. OSQP settings include the following: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``max_iter`` - Maximum number of iterations. * - ``time_limit`` - Run time limit in seconds, 0 to disable. * - ``eps_abs`` - Absolute feasibility tolerance. See `Convergence `__. * - ``eps_rel`` - Relative feasibility tolerance. See `Convergence `__. * - ``eps_prim_inf`` - Primal infeasibility tolerance. * - ``eps_dual_inf`` - Dual infeasibility tolerance. * - ``polish`` - Perform polishing. See `Polishing `_. Check out the `OSQP settings `_ documentation for all available settings. Lower values for absolute or relative tolerances yield more precise solutions at the cost of computation time. See *e.g.* [Caron2022]_ for an overview of solver tolerances. """ build_start_time = time.perf_counter() P, q, G, h, A, b, lb, ub = problem.unpack() P, G, A = ensure_sparse_matrices("osqp", P, G, A) A_osqp = None l_osqp = None u_osqp = None if G is not None and h is not None: A_osqp = G l_osqp = np.full(h.shape, -np.inf) u_osqp = h if A is not None and b is not None: A_osqp = A if A_osqp is None else spa.vstack([A_osqp, A], format="csc") l_osqp = b if l_osqp is None else np.hstack([l_osqp, b]) u_osqp = b if u_osqp is None else np.hstack([u_osqp, b]) if lb is not None or ub is not None: lb = lb if lb is not None else np.full(q.shape, -np.inf) ub = ub if ub is not None else np.full(q.shape, +np.inf) E = spa.eye(q.shape[0], format="csc") A_osqp = E if A_osqp is None else spa.vstack([A_osqp, E], format="csc") l_osqp = lb if l_osqp is None else np.hstack([l_osqp, lb]) u_osqp = ub if u_osqp is None else np.hstack([u_osqp, ub]) kwargs["verbose"] = verbose solver = OSQP() solver.setup(P=P, q=q, A=A_osqp, l=l_osqp, u=u_osqp, **kwargs) if initvals is not None: solver.warm_start(x=initvals) solve_start_time = time.perf_counter() res = solver.solve() solve_end_time = time.perf_counter() solution = Solution(problem) solution.extras = { "info": res.info, "dual_inf_cert": res.dual_inf_cert, "prim_inf_cert": res.prim_inf_cert, } solution.found = res.info.status_val == SolverStatus.OSQP_SOLVED if not solution.found: warnings.warn(f"OSQP exited with status '{res.info.status}'") solution.x = res.x m = G.shape[0] if G is not None else 0 meq = A.shape[0] if A is not None else 0 solution.z = res.y[:m] if G is not None else np.empty((0,)) solution.y = res.y[m : m + meq] if A is not None else np.empty((0,)) solution.z_box = ( res.y[m + meq :] if lb is not None or ub is not None else np.empty((0,)) ) solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def osqp_solve_qp( P: Union[np.ndarray, csc_matrix], q: np.ndarray, G: Optional[Union[np.ndarray, csc_matrix]] = None, h: Optional[np.ndarray] = None, A: Optional[Union[np.ndarray, csc_matrix]] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using OSQP. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using `OSQP `__. Parameters ---------- P : Symmetric cost matrix. q : Cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. initvals : Warm-start guess vector for the primal solution. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP, if found, otherwise ``None``. Raises ------ ValueError If the problem is clearly non-convex. See `this recommendation `_. Note that OSQP may find the problem unfeasible if the problem is slightly non-convex (in this context, the meaning of "clearly" and "slightly" depends on how close the negative eigenvalues of :math:`P` are to zero). Note ---- OSQP requires a symmetric `P` and won't check for errors otherwise. Check out this point if you `get nan values `_ in your solutions. Notes ----- Keyword arguments are forwarded to OSQP. For instance, we can call ``osqp_solve_qp(P, q, G, h, u, eps_abs=1e-8, eps_rel=0.0)``. OSQP settings include the following: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``max_iter`` - Maximum number of iterations. * - ``time_limit`` - Run time limit in seconds, 0 to disable. * - ``eps_abs`` - Absolute feasibility tolerance. See `Convergence `__. * - ``eps_rel`` - Relative feasibility tolerance. See `Convergence `__. * - ``eps_prim_inf`` - Primal infeasibility tolerance. * - ``eps_dual_inf`` - Dual infeasibility tolerance. * - ``polish`` - Perform polishing. See `Polishing `_. Check out the `OSQP settings `_ documentation for all available settings. Lower values for absolute or relative tolerances yield more precise solutions at the cost of computation time. See *e.g.* [Caron2022]_ for an overview of solver tolerances. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = osqp_solve_problem(problem, initvals, verbose, **kwargs) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/pdhcg_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Solver interface for PDHCG. PDHCG (Primal-Dual Hybrid Conjugate Gradient) is a high-performance, GPU-accelerated solver designed for large-scale convex Quadratic Programming (QP). It is particularly efficient for huge-scale problems by fully leveraging NVIDIA CUDA architectures. Note: To use this solver, you need an NVIDIA GPU and the ``pdhcg`` package installed via ``pip install pdhcg``. For advanced installation (e.g., custom CUDA paths), please refer to the `official PDHCG-II repository `_. References ---------- - `PDHCG-II `_ """ from typing import Any, List, Optional, Union import numpy as np import scipy.sparse as spa from pdhcg import Model from ..problem import Problem from ..solution import Solution def pdhcg_solve_problem( problem: Problem, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs: Any, ) -> Solution: r"""Solve a quadratic program using PDHCG. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} Parameters ---------- problem : Quadratic program to solve. initvals : Warm-start guess vector for the primal solution. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP, if found, otherwise ``None``. Notes ----- Keyword arguments are forwarded to PDHCG as solver parameters. For instance, you can call ``pdhcg_solve_qp(..., TimeLimit=60)``. Common PDHCG parameters include: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``TimeLimit`` - Maximum wall-clock time in seconds (default: 3600.0). * - ``IterationLimit`` - Maximum number of iterations. * - ``OptimalityTol`` - Relative tolerance for optimality gap (default: 1e-4). * - ``FeasibilityTol`` - Relative feasibility tolerance for residuals (default: 1e-4). * - ``OutputFlag`` - Enable (True) or disable (False) console logging output. For advanced parameters, please refer to the `PDHCG Documentation `_. """ P, q, G, h, A, b, lb, ub = problem.unpack() C_mats: List[Any] = [] l_bounds: List[Any] = [] u_bounds: List[Any] = [] if G is not None and h is not None: C_mats.append(G) l_bounds.append(np.full(h.shape, -np.inf)) u_bounds.append(h) if A is not None and b is not None: C_mats.append(A) l_bounds.append(b) u_bounds.append(b) constraint_matrix: Optional[ Union[np.ndarray, spa.csr_matrix, spa.csc_matrix] ] = None constraint_lower_bound: Optional[np.ndarray] = None constraint_upper_bound: Optional[np.ndarray] = None if C_mats: if any(spa.issparse(mat) for mat in C_mats): constraint_matrix = spa.vstack(C_mats, format="csr") # type: ignore else: constraint_matrix = np.vstack(C_mats) # type: ignore constraint_lower_bound = np.concatenate(l_bounds) # type: ignore constraint_upper_bound = np.concatenate(u_bounds) # type: ignore model = Model( objective_matrix=P, objective_vector=q, constraint_matrix=constraint_matrix, constraint_lower_bound=constraint_lower_bound, constraint_upper_bound=constraint_upper_bound, variable_lower_bound=lb, variable_upper_bound=ub, ) if verbose: model.setParam("OutputFlag", 2) else: model.setParam("OutputFlag", 0) if kwargs: model.setParams(**kwargs) if initvals is not None: model.setWarmStart(primal=initvals) model.optimize() solution = Solution(problem) status_str = str(model.Status).upper() if model.Status else "" solution.found = status_str == "OPTIMAL" if solution.found and model.X is not None: solution.x = np.array(model.X) solution.obj = model.ObjVal solution.extras["runtime"] = model.Runtime solution.extras["iter"] = model.IterCount solution.extras["status"] = status_str if solution.found and model.Pi is not None and C_mats: pi = np.array(model.Pi) idx = 0 if G is not None: num_g = G.shape[0] solution.z = -pi[idx : idx + num_g] idx += num_g else: solution.z = np.empty((0,)) if A is not None: num_a = A.shape[0] solution.y = -pi[idx : idx + num_a] else: solution.y = np.empty((0,)) else: solution.z = np.empty((0,)) if G is None else np.empty(G.shape[0]) solution.y = np.empty((0,)) if A is None else np.empty(A.shape[0]) return solution def pdhcg_solve_qp( P: Union[np.ndarray, spa.csc_matrix], q: np.ndarray, G: Optional[Union[np.ndarray, spa.csc_matrix]] = None, h: Optional[np.ndarray] = None, A: Optional[Union[np.ndarray, spa.csc_matrix]] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs: Any, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using PDHCG. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using `PDHCG `__. Parameters ---------- P : Positive semidefinite cost matrix. q : Cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. initvals : Warm-start guess vector for the primal solution. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP, if found, otherwise ``None``. Notes ----- Keyword arguments are forwarded to PDHCG as solver parameters. For instance, you can call ``pdhcg_solve_qp(..., TimeLimit=60)``. Common PDHCG parameters include: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``TimeLimit`` - Maximum wall-clock time in seconds (default: 3600.0). * - ``IterationLimit`` - Maximum number of iterations. * - ``OptimalityTol`` - Relative tolerance for optimality gap (default: 1e-4). * - ``FeasibilityTol`` - Relative feasibility tolerance for residuals (default: 1e-4). * - ``OutputFlag`` - Enable (True) or disable (False) console logging output. For advanced parameters, please refer to the `PDHCG Documentation `_. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = pdhcg_solve_problem(problem, initvals, verbose, **kwargs) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/piqp_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Solver interface for `PIQP`_. .. _PIQP: https://github.com/PREDICT-EPFL/piqp PIQP is a proximal interior-point quadratic programming solver for dense and sparse problems. Its algorithm combines an infeasible interior-point method with the proximal method of multipliers, and is designed to handle ill-conditioned convex problems without the need for linear independence of constraints. If you are using PIQP in a scientific work, consider citing the corresponding paper [Schwan2023]_. **Warm-start:** this solver interface does not support warm starting ❄️ """ import time import warnings from typing import Optional, Union import numpy as np import piqp import scipy.sparse as spa from ..conversions import ensure_sparse_matrices from ..exceptions import ParamError, ProblemError from ..problem import Problem from ..solution import Solution def __select_backend(backend: Optional[str], use_csc: bool): """Select backend function for PIQP. Parameters ---------- backend : PIQP backend to use in ``[None, "dense", "sparse"]``. If ``None`` (default), the backend is selected based on the type of ``P``. use_csc : If ``True``, use sparse matrices if the backend is not specified. Returns ------- : Backend solve function. Raises ------ ParamError If the required backend is not a valid PIQP backend. """ if backend is None: return piqp.SparseSolver() if use_csc else piqp.DenseSolver() if backend == "dense": return piqp.DenseSolver() if backend == "sparse": return piqp.SparseSolver() raise ParamError(f'Unknown PIQP backend "{backend}') def piqp_solve_problem( problem: Problem, initvals: Optional[np.ndarray] = None, verbose: bool = False, backend: Optional[str] = None, **kwargs, ) -> Solution: """Solve a quadratic program using PIQP. Parameters ---------- problem : Quadratic program to solve. initvals : This argument is not used by PIQP. backend : PIQP backend to use in ``[None, "dense", "sparse"]``. If ``None`` (default), the backend is selected based on the type of ``P``. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP returned by the solver. Notes ----- All other keyword arguments are forwarded as options to PIQP. For instance, you can call ``piqp_solve_qp(P, q, G, h, eps_abs=1e-6)``. For a quick overview, the solver accepts the following settings: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Effect * - ``rho_init`` - Initial value for the primal proximal penalty parameter rho. * - ``delta_init`` - Initial value for the augmented lagrangian penalty parameter delta. * - ``eps_abs`` - Absolute tolerance. * - ``eps_rel`` - Relative tolerance. * - ``check_duality_gap`` - Check terminal criterion on duality gap. * - ``eps_duality_gap_abs`` - Absolute tolerance on duality gap. * - ``eps_duality_gap_rel`` - Threshold value for infeasibility detection. * - ``infeasibility_threshold`` - Relative tolerance on duality gap. * - ``reg_lower_limit`` - Lower limit for regularization. * - ``reg_finetune_lower_limit`` - Fine tune lower limit regularization. * - ``reg_finetune_primal_update_threshold`` - Threshold of number of no primal updates to transition to fine tune mode. * - ``reg_finetune_dual_update_threshold`` - Threshold of number of no dual updates to transition to fine tune mode. * - ``max_iter`` - Maximum number of iterations. * - ``max_factor_retires`` - Maximum number of factorization retires before failure. * - ``preconditioner_scale_cost`` - Scale cost in Ruiz preconditioner. * - ``preconditioner_reuse_on_update`` - Reuse the preconditioner from previous setup/update. * - ``preconditioner_iter`` - Maximum of preconditioner iterations. * - ``tau`` - Maximum interior point step length. * - ``kkt_solver`` - KKT solver backend. * - ``iterative_refinement_always_enabled`` - Always run iterative refinement and not only on factorization failure. * - ``iterative_refinement_eps_abs`` - Iterative refinement absolute tolerance. * - ``iterative_refinement_eps_rel`` - Iterative refinement relative tolerance. * - ``iterative_refinement_max_iter`` - Maximum number of iterations for iterative refinement. * - ``iterative_refinement_min_improvement_rate`` - Minimum improvement rate for iterative refinement. * - ``iterative_refinement_static_regularization_eps`` - Static regularization for KKT system for iterative refinement. * - ``iterative_refinement_static_regularization_rel`` - Static regularization w.r.t. the maximum abs diagonal term of KKT system. * - ``verbose`` - Verbose printing. * - ``compute_timings`` - Measure timing information internally. This list is not exhaustive. Check out the `solver documentation `__ for details. """ build_start_time = time.perf_counter() if initvals is not None and verbose: warnings.warn("warm-start values are ignored by PIQP") P, q, G, h, A, b, lb, ub = problem.unpack() n: int = q.shape[0] if G is None and h is not None: raise ProblemError( "Inconsistent inequalities: G is not set but h is set" ) if G is not None and h is None: raise ProblemError("Inconsistent inequalities: G is set but h is None") if A is None and b is not None: raise ProblemError( "Inconsistent inequalities: A is not set but b is set" ) if A is not None and b is None: raise ProblemError("Inconsistent inequalities: A is set but b is None") # PIQP does not support A, b, G, and h to be None. use_csc: bool = ( not isinstance(P, np.ndarray) or (G is not None and not isinstance(G, np.ndarray)) or (A is not None and not isinstance(A, np.ndarray)) ) G_piqp: Union[np.ndarray, spa.csc_matrix] = ( G if G is not None else spa.csc_matrix(np.zeros((0, n))) if use_csc else np.zeros((0, n)) ) A_piqp: Union[np.ndarray, spa.csc_matrix] = ( A if A is not None else spa.csc_matrix(np.zeros((0, n))) if use_csc else np.zeros((0, n)) ) h_piqp = np.zeros((0,)) if h is None else h b_piqp = np.zeros((0,)) if b is None else b if use_csc: P, G_piqp_sparse, A_piqp_sparse = ensure_sparse_matrices( "piqp", P, G_piqp, A_piqp ) assert G_piqp_sparse is not None and A_piqp_sparse is not None G_piqp = G_piqp_sparse A_piqp = A_piqp_sparse solver = __select_backend(backend, use_csc) solver.settings.verbose = verbose for key, value in kwargs.items(): try: setattr(solver.settings, key, value) except AttributeError: if verbose: warnings.warn( f"Received an undefined solver setting {key}\ with value {value}" ) old_interface = ( float(piqp.__version__.split(".")[0]) == 0 and float(piqp.__version__.split(".")[1]) <= 5 ) if old_interface: solver.setup(P, q, A_piqp, b_piqp, G_piqp, h_piqp, lb, ub) else: solver.setup(P, q, A_piqp, b_piqp, G_piqp, None, h_piqp, lb, ub) solve_start_time = time.perf_counter() status = solver.solve() solve_end_time = time.perf_counter() success_status = piqp.PIQP_SOLVED solution = Solution(problem) solution.extras = {"info": solver.result.info} solution.found = status == success_status solution.x = solver.result.x if A is None: solution.y = np.empty((0,)) else: solution.y = solver.result.y if G is None: solution.z = np.empty((0,)) else: if old_interface: solution.z = solver.result.z else: solution.z = solver.result.z_u if lb is not None or ub is not None: if old_interface: solution.z_box = solver.result.z_ub - solver.result.z_lb else: solution.z_box = solver.result.z_bu - solver.result.z_bl else: solution.z_box = np.empty((0,)) solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def piqp_solve_qp( P: Union[np.ndarray, spa.csc_matrix], q: np.ndarray, G: Optional[Union[np.ndarray, spa.csc_matrix]] = None, h: Optional[np.ndarray] = None, A: Optional[Union[np.ndarray, spa.csc_matrix]] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, backend: Optional[str] = None, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using PIQP. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{\mbox{minimize}}{x} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using `PIQP `__. Parameters ---------- P : Positive semidefinite cost matrix. q : Cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. backend : PIQP backend to use in ``[None, "dense", "sparse"]``. If ``None`` (default), the backend is selected based on the type of ``P``. verbose : Set to `True` to print out extra information. initvals : This argument is not used by PIQP. Returns ------- : Primal solution to the QP, if found, otherwise ``None``. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = piqp_solve_problem( problem, initvals, verbose, backend, **kwargs ) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/proxqp_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Solver interface for `ProxQP`_. .. _ProxQP: https://github.com/Simple-Robotics/proxsuite#proxqp ProxQP is a primal-dual augmented Lagrangian method with proximal heuristics. It converges to the solution of feasible problems, or to the solution to the closest feasible one if the input problem is unfeasible. ProxQP is part of the ProxSuite collection of open-source solvers. If you use ProxQP in a scientific work, consider citing the corresponding paper [Bambade2022]_. **Warm-start:** this solver interface supports warm starting 🔥 """ import time from typing import Optional, Union import numpy as np import scipy.sparse as spa from proxsuite import proxqp from ..conversions import combine_linear_box_inequalities from ..exceptions import ParamError from ..problem import Problem from ..solution import Solution def __select_backend(backend: Optional[str], use_csc: bool): """Select backend function for ProxQP. Parameters ---------- backend : ProxQP backend to use in ``[None, "dense", "sparse"]``. If ``None`` (default), the backend is selected based on the type of ``P``. use_csc : If ``True``, use sparse matrices if the backend is not specified. Returns ------- : Backend solve function. Raises ------ ParamError If the required backend is not a valid ProxQP backend. """ if backend is None: return proxqp.sparse.solve if use_csc else proxqp.dense.solve if backend == "dense": return proxqp.dense.solve if backend == "sparse": return proxqp.sparse.solve raise ParamError(f'Unknown ProxQP backend "{backend}') def proxqp_solve_problem( problem: Problem, initvals: Optional[np.ndarray] = None, verbose: bool = False, backend: Optional[str] = None, **kwargs, ) -> Solution: """Solve a quadratic program using ProxQP. Parameters ---------- problem : Quadratic program to solve. initvals : Warm-start guess vector for the primal solution. backend : ProxQP backend to use in ``[None, "dense", "sparse"]``. If ``None`` (default), the backend is selected based on the type of ``P``. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP returned by the solver. Raises ------ ParamError If a warm-start value is given both in `initvals` and the `x` keyword argument. Notes ----- All other keyword arguments are forwarded as options to ProxQP. For instance, you can call ``proxqp_solve_qp(P, q, G, h, eps_abs=1e-6)``. For a quick overview, the solver accepts the following settings: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Effect * - ``x`` - Warm start value for the primal variable. * - ``y`` - Warm start value for the dual Lagrange multiplier for equality constraints. * - ``z`` - Warm start value for the dual Lagrange multiplier for inequality constraints. * - ``eps_abs`` - Asbolute stopping criterion of the solver (default: 1e-3, note that this is a laxer default than other solvers). See *e.g.* [Caron2022]_ for an overview of solver tolerances. * - ``eps_rel`` - Relative stopping criterion of the solver. See *e.g.* [Caron2022]_ for an overview of solver tolerances. * - ``check_duality_gap`` - If set to true (false by default), ProxQP will include the duality gap in absolute and relative stopping criteria. * - ``mu_eq`` - Proximal step size wrt equality constraints multiplier. * - ``mu_in`` - Proximal step size wrt inequality constraints multiplier. * - ``rho`` - Proximal step size wrt primal variable. * - ``compute_preconditioner`` - If ``True`` (default), the preconditioner will be derived. * - ``compute_timings`` - If ``True`` (default), timings will be computed by the solver (setup time, solving time, and run time = setup time + solving time). * - ``max_iter`` - Maximal number of authorized outer iterations. * - ``initial_guess`` - Sets the initial guess option for initilizing x, y and z. This list is not exhaustive. Check out the `solver documentation `__ for details. """ build_start_time = time.perf_counter() if initvals is not None: if "x" in kwargs: raise ParamError( "Warm-start value specified in both `initvals` and `x` kwargs" ) kwargs["x"] = initvals P, q, G, h, A, b, lb, ub = problem.unpack() n: int = q.shape[0] use_csc: bool = ( not isinstance(P, np.ndarray) or (G is not None and not isinstance(G, np.ndarray)) or (A is not None and not isinstance(A, np.ndarray)) ) Cx, ux, lx = combine_linear_box_inequalities(G, h, lb, ub, n, use_csc) solve = __select_backend(backend, use_csc) solve_start_time = time.perf_counter() result = solve( P, q, A, b, Cx, lx, ux, verbose=verbose, **kwargs, ) solve_end_time = time.perf_counter() solution = Solution(problem) solution.extras = {"info": result.info} solution.found = result.info.status == proxqp.QPSolverOutput.PROXQP_SOLVED solution.x = result.x solution.y = result.y if lb is not None or ub is not None: solution.z = result.z[:-n] solution.z_box = result.z[-n:] else: # lb is None and ub is None solution.z = result.z solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def proxqp_solve_qp( P: Union[np.ndarray, spa.csc_matrix], q: np.ndarray, G: Optional[Union[np.ndarray, spa.csc_matrix]] = None, h: Optional[np.ndarray] = None, A: Optional[Union[np.ndarray, spa.csc_matrix]] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, backend: Optional[str] = None, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using ProxQP. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{\mbox{minimize}}{x} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using `ProxQP `__. Parameters ---------- P : Positive semidefinite cost matrix. q : Cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. initvals : Warm-start guess vector for the primal solution. backend : ProxQP backend to use in ``[None, "dense", "sparse"]``. If ``None`` (default), the backend is selected based on the type of ``P``. verbose : Set to `True` to print out extra information. Returns ------- : Primal solution to the QP, if found, otherwise ``None``. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = proxqp_solve_problem( problem, initvals, verbose, backend, **kwargs ) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/pyqpmad_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Solver interface for `pyqpmad `__. pyqpmad is a Python wrapper for qpmad, a C++ implementation of Goldfarb-Idnani's dual active-set method [Goldfarb1983]_. It works best on well-conditioned dense problems with a positive-definite Hessian. `qpmad ` **Warm-start:** this solver interface supports warm starting 🌡️ """ import warnings from typing import Optional import numpy as np import pyqpmad from numpy import hstack, vstack from ..exceptions import ProblemError from ..problem import Problem from ..solution import Solution def pyqpmad_solve_problem( problem: Problem, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Solution: """Solve a quadratic program using pyqpmad. Parameters ---------- problem : Quadratic program to solve. initvals : Initial guess for the primal solution. pyqpmad uses this as a warm-start if provided. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP, if found, otherwise ``None``. Raises ------ ProblemError : If the problem has sparse matrices (pyqpmad is a dense solver). Notes ----- Keyword arguments are forwarded as attributes of a ``pyqpmad.SolverParameters`` object. Supported settings include: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``tolerance`` - Solver feasibility/optimality tolerance. * - ``max_iter`` - Maximum number of iterations. Check out the `qpmad documentation `_ for all available settings. """ if verbose: warnings.warn("pyqpmad does not support verbose output") if problem.has_sparse: raise ProblemError("pyqpmad does not support sparse matrices") P, q, G, h, A, b, lb, ub = problem.unpack_as_dense() n = q.shape[0] A_qpmad = None Alb_qpmad = None Aub_qpmad = None if A is not None and b is not None: A_qpmad = A Alb_qpmad = b Aub_qpmad = b if G is not None and h is not None: A_qpmad = G if A_qpmad is None else vstack([A_qpmad, G]) lb_G = np.full(h.shape, -np.inf) Alb_qpmad = lb_G if Alb_qpmad is None else hstack([Alb_qpmad, lb_G]) Aub_qpmad = h if Aub_qpmad is None else hstack([Aub_qpmad, h]) # qpmad requires the Hessian in Fortran (column-major) order. H_qpmad = np.asfortranarray(P, dtype=np.float64) if A_qpmad is not None: A_qpmad = np.asfortranarray(A_qpmad, dtype=np.float64) # Build solver parameters from keyword arguments. params = pyqpmad.SolverParameters() for key, val in kwargs.items(): if hasattr(params, key): setattr(params, key, val) elif verbose: warnings.warn(f"pyqpmad ignoring unknown parameter: {key!r}") # Initialize primal variable; warm start if initvals is provided. x = ( np.array(initvals, dtype=np.float64) if initvals is not None else np.zeros(n, dtype=np.float64) ) lb_qpmad = None ub_qpmad = None if lb is not None or ub is not None: lb_qpmad = ( np.asarray(lb, dtype=np.float64) if lb is not None else np.full(n, -np.inf) ) ub_qpmad = ( np.asarray(ub, dtype=np.float64) if ub is not None else np.full(n, np.inf) ) solver = pyqpmad.Solver() try: status = solver.solve( x, H_qpmad, np.asarray(q, dtype=np.float64), lb_qpmad, ub_qpmad, A_qpmad, Alb_qpmad, Aub_qpmad, params, ) except Exception: status = None # To make solution.found = False solution = Solution(problem) solution.found = status == pyqpmad.ReturnStatus.OK if solution.found: solution.x = x n_simple = n if (lb is not None or ub is not None) else 0 n_eq_orig = A.shape[0] if A is not None else 0 n_ineq_orig = G.shape[0] if G is not None else 0 # Initialise dual arrays (zeros covers inactive constraints). # z is always a non-None array (possibly empty) after a successful # solve. solution.y = np.zeros(n_eq_orig) solution.z = np.zeros(n_ineq_orig) if n_simple > 0: solution.z_box = np.zeros(n) # Reconstruct z and z_box from the active-set inequality duals. # qpmad index ordering: # - 0..n_simple-1 are simple bounds (lb/ub), # - n_simple..n_simple+n_eq_orig-1 are equality rows of A_qpmad, # - n_simple+n_eq_orig.. are inequality rows of A_qpmad (from G). ineq_dual = solver.get_inequality_dual() for i in range(len(ineq_dual.dual)): ci = int(ineq_dual.indices[i]) d = float(ineq_dual.dual[i]) if ci < n_simple: # Active box bound: sign follows qpsolvers convention # (negative for lower, positive for upper) solution.z_box[ci] = ( # type: ignore[index] # z_box is set to np.zeros(n) above when n_simple > 0 -d if ineq_dual.is_lower[i] else d ) else: j = ci - n_simple # row in A_qpmad if j >= n_eq_orig: # inequality row from G solution.z[j - n_eq_orig] = d # Compute equality duals y from KKT stationarity to avoid any # sign-convention ambiguity with qpmad's internal dual storage: # P x + q + A' y + G' z + z_box = 0 # => A' y = -(P x + q + G' z + z_box) if A is not None and n_eq_orig > 0: residual = P @ x + q if G is not None and solution.z is not None: residual = residual + G.T @ solution.z if solution.z_box is not None: residual = residual + solution.z_box solution.y, _, _, _ = np.linalg.lstsq(A.T, -residual, rcond=None) solution.extras = {"num_iterations": solver.get_num_iterations()} return solution def pyqpmad_solve_qp( P: np.ndarray, q: np.ndarray, G: Optional[np.ndarray] = None, h: Optional[np.ndarray] = None, A: Optional[np.ndarray] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using pyqpmad. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using `pyqpmad `__, a Python wrapper for the `qpmad `__ C++ solver. Parameters ---------- P : Symmetric positive-definite cost matrix. q : Cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. initvals : Initial guess for the primal solution (warm start). verbose : Set to `True` to print out extra information. Returns ------- : Primal solution to the QP, if found, otherwise ``None``. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = pyqpmad_solve_problem(problem, initvals, verbose, **kwargs) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/qpalm_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Solver interface for `QPALM`_. .. _QPALM: https://github.com/kul-optec/QPALM QPALM is a proximal augmented-Lagrangian solver for (possibly nonconvex) quadratic programs, implemented in the C programming language. If you use QPALM in a scientific work, consider citing the corresponding paper [Hermans2022]_. **Warm-start:** this solver interface supports warm starting 🔥 """ import time import warnings from typing import Optional, Union import numpy as np import qpalm import scipy.sparse as spa from ..conversions import ( combine_linear_box_inequalities, ensure_sparse_matrices, ) from ..exceptions import ParamError from ..problem import Problem from ..solution import Solution def qpalm_solve_problem( problem: Problem, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Solution: """Solve a quadratic program using QPALM. Parameters ---------- problem : Quadratic program to solve. initvals : Warm-start guess vector for the primal solution. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP returned by the solver. Raises ------ ParamError If a warm-start value is given both in `initvals` and the `x` keyword argument. Note ---- QPALM internally only uses the upper-triangular part of the cost matrix :math:`P`. Notes ----- Keyword arguments are forwarded as "settings" to QPALM. For instance, we can call ``qpalm_solve_qp(P, q, G, h, u, eps_abs=1e-4, eps_rel=1e-4)``. .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Effect * - ``max_iter`` - Maximum number of iterations. * - ``eps_abs`` - Asbolute stopping criterion of the solver. See *e.g.* [Caron2022]_ for an overview of solver tolerances. * - ``eps_rel`` - Relative stopping criterion of the solver. See *e.g.* [Caron2022]_ for an overview of solver tolerances. * - ``rho`` - Tolerance scaling factor. * - ``theta`` - Penalty update criterion parameter. * - ``delta`` - Penalty update factor. * - ``sigma_max`` - Penalty factor cap. * - ``proximal`` - Boolean, use proximal method of multipliers or not. This list is not exhaustive. Check out the `solver documentation `__ for details. """ build_start_time = time.perf_counter() if initvals is not None: if "x" in kwargs: raise ParamError( "Warm-start value specified in both `initvals` and `x` kwargs" ) kwargs["x"] = initvals P, q, G, h, A, b, lb, ub = problem.unpack() P, G, A = ensure_sparse_matrices("qpalm", P, G, A) n: int = q.shape[0] Cx, ux, lx = combine_linear_box_inequalities(G, h, lb, ub, n, use_csc=True) if A is not None and b is not None: Cx = spa.vstack((A, Cx), format="csc") if Cx is not None else A lx = np.hstack((b, lx)) if lx is not None else b ux = np.hstack((b, ux)) if ux is not None else b m: int = Cx.shape[0] if Cx is not None else 0 data = qpalm.Data(n, m) if Cx is not None: data.A = Cx data.bmax = ux data.bmin = lx data.Q = P data.q = q settings = qpalm.Settings() settings.verbose = verbose for key, value in kwargs.items(): try: setattr(settings, key, value) except AttributeError: if verbose: warnings.warn( f"Received an undefined solver setting {key}\ with value {value}" ) solver = qpalm.Solver(data, settings) solve_start_time = time.perf_counter() solver.solve() solve_end_time = time.perf_counter() solution = Solution(problem) solution.extras = {"info": solver.info} solution.found = solver.info.status == "solved" solution.x = solver.solution.x m_eq: int = A.shape[0] if A is not None else 0 m_leq: int = G.shape[0] if G is not None else 0 solution.y = solver.solution.y[0:m_eq] solution.z = solver.solution.y[m_eq : m_eq + m_leq] solution.z_box = solver.solution.y[m_eq + m_leq :] solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def qpalm_solve_qp( P: Union[np.ndarray, spa.csc_matrix], q: np.ndarray, G: Optional[Union[np.ndarray, spa.csc_matrix]] = None, h: Optional[np.ndarray] = None, A: Optional[Union[np.ndarray, spa.csc_matrix]] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using QPALM. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{\mbox{minimize}}{x} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using `QPALM `__. Parameters ---------- P : Positive semidefinite cost matrix. q : Cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. initvals : Warm-start guess vector for the primal solution. verbose : Set to `True` to print out extra information. Returns ------- : Primal solution to the QP, if found, otherwise ``None``. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = qpalm_solve_problem(problem, initvals, verbose, **kwargs) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/qpax_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2024 Lev Kozlov """ Solver interface for `qpax `__. qpax is an open-source QP solver that can be combined with JAX's jit and vmap functionality, as well as differentiated with reverse-mode differentiation. It is based on a primal-dual interior point algorithm. If you are using qpax in a scientific work, consider citing the corresponding paper [Tracy2024]_. **Warm-start:** this solver interface does not support warm starting ❄️ """ import time import warnings from typing import Optional import numpy as np import qpax import scipy.sparse as spa from ..conversions import linear_from_box_inequalities, split_dual_linear_box from ..exceptions import ProblemError from ..problem import Problem from ..solution import Solution def qpax_solve_problem( problem: Problem, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Solution: """Solve a quadratic program using qpax. Parameters ---------- problem : Quadratic program to solve. initvals : This argument is not used by qpax. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP returned by the solver. Notes ----- All other keyword arguments are forwarded as options to qpax. For instance, you can call ``qpax_solve_qp(P, q, G, h, solver_tol=1e-5)``. For a quick overview, the solver accepts the following settings: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Effect * - ``solver_tol`` - Tolerance for the solver. Note that `jax` by default uses 32-bit floating point numbers, which can lead to numerical instability. If you encounter numerical issues, consider using 64-bit floating point numbers by setting ```python import jax jax.config.update("jax_enable_x64", True) ``` """ build_start_time = time.perf_counter() if initvals is not None and verbose: warnings.warn("warm-start values are ignored by qpax") P, q, G, h, A, b, lb, ub = problem.unpack() n: int = q.shape[0] if G is None and h is not None: raise ProblemError( "Inconsistent inequalities: G is not set but h is set" ) if G is not None and h is None: raise ProblemError("Inconsistent inequalities: G is set but h is None") if A is None and b is not None: raise ProblemError( "Inconsistent inequalities: A is not set but b is set" ) if A is not None and b is None: raise ProblemError("Inconsistent inequalities: A is set but b is None") # construct the qpax problem G, h = linear_from_box_inequalities(G, h, lb, ub, use_sparse=False) if G is None: G = np.zeros((0, n)) h = np.zeros((0,)) # qpax does not support A, b to be None. A_qpax = np.zeros((0, n)) if A is None else A b_qpax = np.zeros((0)) if b is None else b # qpax does not support sparse matrices, # so we need to convert them to dense if isinstance(P, spa.csc_matrix): P = P.toarray() if isinstance(A_qpax, spa.csc_matrix): A_qpax = A_qpax.toarray() if isinstance(G, spa.csc_matrix): G = G.toarray() solve_start_time = time.perf_counter() x, s, z, y, converged, iters1 = qpax.solve_qp( P, q, A_qpax, b_qpax, G, h, **kwargs, ) solve_end_time = time.perf_counter() solution = Solution(problem) solution.x = x solution.found = converged solution.y = y # split the dual variables into # the box constraints and the linear constraints solution.z, solution.z_box = split_dual_linear_box( z, problem.lb, problem.ub ) # store information about the solution # and the resulted raw variables in the extras solution.extras = { "info": { "iterations": iters1, "converged": converged, }, "variables": { "x": x, "y": y, "z": z, "s": s, }, } solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def qpax_solve_qp( P: np.ndarray, q: np.ndarray, G: Optional[np.ndarray] = None, h: Optional[np.ndarray] = None, A: Optional[np.ndarray] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using qpax. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{\mbox{minimize}}{x} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using `qpax `__. `Paper: `__. Parameters ---------- P : Positive semidefinite cost matrix. q : Cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. verbose : Set to `True` to print out extra information. initvals : This argument is not used by qpax. Returns ------- : Primal solution to the QP, if found, otherwise ``None``. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = qpax_solve_problem(problem, initvals, verbose, **kwargs) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/qpoases_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Solver interface for `qpOASES `__. qpOASES is an open-source C++ implementation of the online active set strategy, which was inspired by observations from the field of parametric quadratic programming. It has theoretical features that make it suitable to model predictive control. Further numerical modifications have made qpOASES a reliable QP solver, even when tackling semi-definite, ill-posed or degenerated QP problems. If you are using qpOASES in a scientific work, consider citing the corresponding paper [Ferreau2014]_. See the :ref:`installation page ` for additional instructions on installing this solver. **Warm-start:** this solver interface supports warm starting 🔥 """ import time import warnings from typing import Any, List, Optional, Tuple import numpy as np from numpy import array, hstack, vstack from qpoases import PyOptions as Options from qpoases import PyPrintLevel as PrintLevel from qpoases import PyQProblem as QProblem from qpoases import PyQProblemB as QProblemB from qpoases import PyReturnValue as ReturnValue from ..exceptions import ParamError, ProblemError from ..problem import Problem from ..solution import Solution # See qpOASES/include/Constants.hpp __infty__ = 1.0e20 # Return codes not wrapped in qpoases.PyReturnValue RET_INIT_FAILED = 33 RET_INIT_FAILED_TQ = 34 RET_INIT_FAILED_CHOLESKY = 35 RET_INIT_FAILED_HOTSTART = 36 RET_INIT_FAILED_INFEASIBILITY = 37 RET_INIT_FAILED_UNBOUNDEDNESS = 38 RET_INIT_FAILED_REGULARISATION = 39 def __clamp_infinities(v: Optional[np.ndarray]): """Replace infinite values in an array by big finite ones. Note ---- qpOASES requires large bounds instead of infinite float values. See the following issue: https://github.com/coin-or/qpOASES/issues/126 """ if v is not None: v = np.nan_to_num(v, posinf=__infty__, neginf=-__infty__) return v def __prepare_options( verbose: bool, predefined_options: Optional[str], **kwargs, ) -> Options: """Prepare options for qpOASES. Parameters ---------- verbose : Set to `True` to print out extra information. predefined_options : Set solver options to one of the pre-defined choices provided by qpOASES: ``["default", "fast", "mpc", "reliable"]``. Returns ------- : Options for qpOASES. Raises ------ ParamError If predefined options are not a valid choice for qpOASES. """ options = Options() # Start from pre-defined options if predefined_options is None: pass elif predefined_options == "fast": options.setToFast() elif predefined_options == "default": options.setToDefault() elif predefined_options == "mpc": options.setToMPC() elif predefined_options == "reliable": options.setToReliable() else: raise ParamError( f"unknown qpOASES pre-defined options {predefined_options}'" ) # Override options with explicit ones options.printLevel = PrintLevel.MEDIUM if verbose else PrintLevel.NONE for param, value in kwargs.items(): setattr(options, param, value) return options def __convert_inequalities( G: Optional[np.ndarray] = None, h: Optional[np.ndarray] = None, A: Optional[np.ndarray] = None, b: Optional[np.ndarray] = None, ) -> Tuple[np.ndarray, Optional[np.ndarray], np.ndarray]: """Convert linear constraints to qpOASES format. Parameters ---------- G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. Returns ------- : Tuple ``(C, lb_C, ub_C)`` for qpOASES. """ C: np.ndarray = np.array([]) lb_C: Optional[np.ndarray] = None ub_C: np.ndarray = np.array([]) if G is not None and h is not None: h_neginf = np.full(h.shape, -__infty__) if A is not None and b is not None: C = vstack([G, A]) lb_C = hstack([h_neginf, b]) ub_C = hstack([h, b]) else: # no equality constraint C = G lb_C = h_neginf ub_C = h else: # no inequality constraint if A is not None and b is not None: C = A lb_C = b ub_C = b return C, lb_C, ub_C def qpoases_solve_problem( problem: Problem, initvals: Optional[np.ndarray] = None, verbose: bool = False, max_wsr: int = 1000, time_limit: Optional[float] = None, predefined_options: Optional[str] = None, **kwargs, ) -> Solution: """Solve a quadratic program using qpOASES. Parameters ---------- problem : Quadratic program to solve. initvals : Warm-start guess vector for the primal solution. verbose : Set to `True` to print out extra information. max_wsr : Maximum number of Working-Set Recalculations given to qpOASES. time_limit : Set a run time limit in seconds. predefined_options : Set solver options to one of the pre-defined choices provided by qpOASES, to pick in ``["default", "fast", "mpc", "reliable"]``. Returns ------- : Solution to the QP, if found, otherwise ``None``. Raises ------ ProblemError : If the problem is ill-formed in some way, for instance if some matrices are not dense. ValueError : If ``predefined_options`` is not a valid choice. Notes ----- This function relies on an update to qpOASES to allow empty bounds (`lb`, `ub`, `lbA` or `ubA`) in Python. This is possible in the C++ API but not by the Python API (as of version 3.2.0). If using them, be sure to update the Cython file (`qpoases.pyx`) in your distribution of qpOASES to convert ``None`` to the null pointer. Check out the `installation instructions `_. Keyword arguments are forwarded as options to qpOASES. For instance, we can call ``qpoases_solve_qp(P, q, G, h, u, terminationTolerance=1e-14)``. qpOASES options include the following: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``boundRelaxation`` - Initial relaxation of bounds to start homotopy and initial value for far bounds. * - ``epsNum`` - Numerator tolerance for ratio tests. * - ``epsDen`` - Denominator tolerance for ratio tests. * - ``numRefinementSteps`` - Maximum number of iterative refinement steps. * - ``terminationTolerance`` - Relative termination tolerance to stop homotopy. Check out pages 28 to 30 of `qpOASES User's Manual `_. for all available options. """ build_start_time = time.perf_counter() if initvals is not None: warnings.warn("qpOASES: warm-start values are ignored") P, q, G, h, A, b, lb, ub = problem.unpack_as_dense() n = P.shape[0] lb = np.full((n,), -np.inf) if lb is None else lb ub = np.full((n,), +np.inf) if ub is None else ub C, lb_C, ub_C = __convert_inequalities(G, h, A, b) lb_C = __clamp_infinities(lb_C) ub_C = __clamp_infinities(ub_C) lb = __clamp_infinities(lb) ub = __clamp_infinities(ub) args: List[Any] = [] n_wsr = np.array([max_wsr]) if C.shape[0] > 0: qp = QProblem(n, C.shape[0]) args = [P, q, C, lb, ub, lb_C, ub_C, n_wsr] else: # at most box constraints qp = QProblemB(n) args = [P, q, lb, ub, n_wsr] if time_limit is not None: args.append(array([time_limit])) options = __prepare_options(verbose, predefined_options, **kwargs) qp.setOptions(options) solve_start_time = time.perf_counter() try: return_value = qp.init(*args) except TypeError as error: raise ProblemError("problem has sparse matrices") from error solve_end_time = time.perf_counter() solution = Solution(problem) solution.extras = { "nWSR": n_wsr[0], } solution.found = True if RET_INIT_FAILED <= return_value <= RET_INIT_FAILED_REGULARISATION: solution.found = False if return_value == ReturnValue.MAX_NWSR_REACHED: warnings.warn(f"qpOASES reached the maximum number of WSR ({max_wsr})") solution.found = False x_opt = np.empty((n,)) z_opt = np.empty((n + C.shape[0],)) qp.getPrimalSolution(x_opt) # can't return RET_QP_NOT_SOLVED at this point qp.getDualSolution(z_opt) solution.x = x_opt solution.obj = qp.getObjVal() m = G.shape[0] if G is not None else 0 solution.y = ( -z_opt[n + m : n + m + A.shape[0]] if A is not None else np.empty((0,)) ) solution.z = -z_opt[n : n + m] if G is not None else np.empty((0,)) solution.z_box = ( -z_opt[:n] if lb is not None or ub is not None else np.empty((0,)) ) solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def qpoases_solve_qp( P: np.ndarray, q: np.ndarray, G: Optional[np.ndarray] = None, h: Optional[np.ndarray] = None, A: Optional[np.ndarray] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, max_wsr: int = 1000, time_limit: Optional[float] = None, predefined_options: Optional[str] = None, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using qpOASES. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using `qpOASES `__. Parameters ---------- P : Symmetric cost matrix. q : Cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. initvals : Warm-start guess vector for the primal solution. verbose : Set to `True` to print out extra information. max_wsr : Maximum number of Working-Set Recalculations given to qpOASES. time_limit : Set a run time limit in seconds. predefined_options : Set solver options to one of the pre-defined choices provided by qpOASES, to pick in ``["default", "fast", "mpc", "reliable"]``. Returns ------- : Solution to the QP, if found, otherwise ``None``. Raises ------ ProblemError : If the problem is ill-formed in some way, for instance if some matrices are not dense. ValueError : If ``predefined_options`` is not a valid choice. Notes ----- This function relies on an update to qpOASES to allow empty bounds (`lb`, `ub`, `lbA` or `ubA`) in Python. This is possible in the C++ API but not by the Python API (as of version 3.2.0). If using them, be sure to update the Cython file (`qpoases.pyx`) in your distribution of qpOASES to convert ``None`` to the null pointer. Check out the `installation instructions `_. Keyword arguments are forwarded as options to qpOASES. For instance, we can call ``qpoases_solve_qp(P, q, G, h, u, terminationTolerance=1e-14)``. qpOASES options include the following: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``boundRelaxation`` - Initial relaxation of bounds to start homotopy and initial value for far bounds. * - ``epsNum`` - Numerator tolerance for ratio tests. * - ``epsDen`` - Denominator tolerance for ratio tests. * - ``numRefinementSteps`` - Maximum number of iterative refinement steps. * - ``terminationTolerance`` - Relative termination tolerance to stop homotopy. Check out pages 28 to 30 of `qpOASES User's Manual `_. for all available options. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = qpoases_solve_problem( problem, initvals, verbose, max_wsr, time_limit, predefined_options, **kwargs, ) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/qpswift_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Solver interface for `qpSWIFT `__. qpSWIFT is a light-weight sparse Quadratic Programming solver targeted for embedded and robotic applications. It employs Primal-Dual Interior Point method with Mehrotra Predictor corrector step and Nesterov Todd scaling. For solving the linear system of equations, sparse LDL' factorization is used along with approximate minimum degree heuristic to minimize fill-in of the factorizations. If you use qpSWIFT in your research, consider citing the corresponding paper [Pandala2019]_. **Warm-start:** this solver interface supports warm starting 🔥 """ import time import warnings from typing import Optional import numpy as np import qpSWIFT from ..conversions import linear_from_box_inequalities, split_dual_linear_box from ..exceptions import ProblemError from ..problem import Problem from ..solution import Solution def qpswift_solve_problem( problem: Problem, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Solution: r"""Solve a quadratic program using qpSWIFT. Note ---- This solver does not handle problems without inequality constraints. Parameters ---------- problem : Quadratic program to solve. initvals : Warm-start guess vector for the primal solution. verbose : Set to `True` to print out extra information. Returns ------- : Solution returned by the solver. Raises ------ ProblemError : If the problem is ill-formed in some way, for instance if some matrices are not dense or the problem has no inequality constraint. Note ---- **Rank assumptions:** qpSWIFT requires the QP matrices to satisfy the .. math:: \begin{split}\begin{array}{cc} \mathrm{rank}(A) = p & \mathrm{rank}([P\ A^T\ G^T]) = n \end{array}\end{split} where :math:`p` is the number of rows of :math:`A` and :math:`n` is the number of optimization variables. This is the same requirement as :func:`cvxopt_solve_qp`, however qpSWIFT does not perform rank checks as it prioritizes performance. If the solver fails on your problem, try running CVXOPT on it for rank checks. Notes ----- All other keyword arguments are forwarded as options to the qpSWIFT solver. For instance, you can call ``qpswift_solve_qp(P, q, G, h, ABSTOL=1e-5)``. See the solver documentation for details. For a quick overview, the solver accepts the following settings: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Effect * - ``MAXITER`` - Maximum number of iterations needed. * - ``ABSTOL`` - Absolute tolerance on the duality gap. See *e.g.* [Caron2022]_ for a primer on the duality gap and solver tolerances. * - ``RELTOL`` - Relative tolerance on the residuals :math:`r_x = P x + G^T z + q` (dual residual), :math:`r_y = A x - b` (primal residual on equality constraints) and :math:`r_z = h - G x - s` (primal residual on inequality constraints). See equation (21) in [Pandala2019]_. * - ``SIGMA`` - Maximum centering allowed. If a verbose output shows that the maximum number of iterations is reached, check e.g. (1) the rank of your equality constraint matrix and (2) that your inequality constraint matrix does not have zero rows. As qpSWIFT does not sanity check its inputs, it should be used with a little more care than the other solvers. For instance, make sure you don't have zero rows in your input matrices, as it can `make the solver numerically unstable `_. """ build_start_time = time.perf_counter() if initvals is not None: warnings.warn("qpSWIFT: warm-start values are ignored") P, q, G, h, A, b, lb, ub = problem.unpack() if lb is not None or ub is not None: G, h = linear_from_box_inequalities(G, h, lb, ub, use_sparse=False) result: dict = {} kwargs.update( { "OUTPUT": 2, # include "sol", "basicInfo" and "advInfo" "VERBOSE": 1 if verbose else 0, } ) solve_start_time = time.perf_counter() try: if G is not None and h is not None: if A is not None and b is not None: result = qpSWIFT.run(q, h, P, G, A, b, kwargs) else: # no equality constraint result = qpSWIFT.run(q, h, P, G, opts=kwargs) else: # no inequality constraint # See https://github.com/qpSWIFT/qpSWIFT/issues/2 raise ProblemError("problem has no inequality constraint") except TypeError as error: raise ProblemError("problem has sparse matrices") from error solve_end_time = time.perf_counter() basic_info = result["basicInfo"] adv_info = result["advInfo"] solution = Solution(problem) solution.extras = { "basicInfo": basic_info, "advInfo": adv_info, } solution.obj = adv_info["fval"] exit_flag = basic_info["ExitFlag"] solution.found = exit_flag == 0 solution.x = result["sol"] solution.y = adv_info["y"] if A is not None else np.empty((0,)) z, z_box = split_dual_linear_box(adv_info["z"], lb, ub) solution.z = z solution.z_box = z_box solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def qpswift_solve_qp( P: np.ndarray, q: np.ndarray, G: Optional[np.ndarray] = None, h: Optional[np.ndarray] = None, A: Optional[np.ndarray] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using qpSWIFT. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using `qpSWIFT `__. Note ---- This solver does not handle problems without inequality constraints yet. Parameters ---------- P : Symmetric cost matrix. Together with :math:`A` and :math:`G`, it should satisfy :math:`\mathrm{rank}([P\ A^T\ G^T]) = n`, see the rank assumptions below. q : Cost vector. G : Linear inequality constraint matrix. Together with :math:`P` and :math:`A`, it should satisfy :math:`\mathrm{rank}([P\ A^T\ G^T]) = n`, see the rank assumptions below. h : Linear inequality constraint vector. A : Linear equality constraint matrix. It needs to be full row rank, and together with :math:`P` and :math:`G` satisfy :math:`\mathrm{rank}([P\ A^T\ G^T]) = n`. See the rank assumptions below. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. initvals : Warm-start guess vector for the primal solution. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP, if found, otherwise ``None``. Raises ------ ProblemError : If the problem is ill-formed in some way, for instance if some matrices are not dense or the problem has no inequality constraint. Note ---- .. _qpSWIFT rank assumptions: **Rank assumptions:** qpSWIFT requires the QP matrices to satisfy the .. math:: \begin{split}\begin{array}{cc} \mathrm{rank}(A) = p & \mathrm{rank}([P\ A^T\ G^T]) = n \end{array}\end{split} where :math:`p` is the number of rows of :math:`A` and :math:`n` is the number of optimization variables. This is the same requirement as :func:`cvxopt_solve_qp`, however qpSWIFT does not perform rank checks as it prioritizes performance. If the solver fails on your problem, try running CVXOPT on it for rank checks. Notes ----- All other keyword arguments are forwarded as options to the qpSWIFT solver. For instance, you can call ``qpswift_solve_qp(P, q, G, h, ABSTOL=1e-5)``. See the solver documentation for details. For a quick overview, the solver accepts the following settings: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Effect * - MAXITER - Maximum number of iterations needed. * - ABSTOL - Absolute tolerance on the duality gap. See *e.g.* [Caron2022]_ for a primer on the duality gap and solver tolerances. * - RELTOL - Relative tolerance on the residuals :math:`r_x = P x + G^T z + q` (dual residual), :math:`r_y = A x - b` (primal residual on equality constraints) and :math:`r_z = h - G x - s` (primal residual on inequality constraints). See equation (21) in [Pandala2019]_. * - SIGMA - Maximum centering allowed. If a verbose output shows that the maximum number of iterations is reached, check e.g. (1) the rank of your equality constraint matrix and (2) that your inequality constraint matrix does not have zero rows. As qpSWIFT does not sanity check its inputs, it should be used with a little more care than the other solvers. For instance, make sure you don't have zero rows in your input matrices, as it can `make the solver numerically unstable `_. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = qpswift_solve_problem(problem, initvals, verbose, **kwargs) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/qtqp_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later """Solver interface for `QTQP`_. .. _QTQP: https://github.com/google-deepmind/qtqp QTQP is a primal-dual interior point method for solving convex quadratic programs (QPs), implemented in pure Python. It is developed by Google DeepMind. **Warm-start:** this solver interface does not support warm starting ❄️ """ import time import warnings from typing import List, Optional, Union import numpy as np import qtqp import scipy.sparse as spa from ..conversions import ( ensure_sparse_matrices, linear_from_box_inequalities, split_dual_linear_box, ) from ..problem import Problem from ..solution import Solution from ..solve_unconstrained import solve_unconstrained def qtqp_solve_problem( problem: Problem, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Solution: r"""Solve a quadratic program using QTQP. Parameters ---------- problem : Quadratic program to solve. initvals : This argument is not used by QTQP. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP, if found, otherwise ``None``. Notes ----- Keyword arguments are forwarded as options to QTQP. For instance, we can call ``qtqp_solve_qp(P, q, G, h, atol=1e-8, max_iter=200)``. QTQP options include the following: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``atol`` - Absolute tolerance for optimality convergence (default: 1e-7). * - ``rtol`` - Relative tolerance for optimality convergence (default: 1e-8). * - ``atol_infeas`` - Absolute tolerance for infeasibility detection (default: 1e-8). * - ``rtol_infeas`` - Relative tolerance for infeasibility detection (default: 1e-9). * - ``max_iter`` - Maximum number of iterations (default: 100). * - ``step_size_scale`` - Scale factor in (0,1) for line search step size (default: 0.99). * - ``min_static_regularization`` - Diagonal regularization on KKT for robustness (default: 1e-7). * - ``max_iterative_refinement_steps`` - Maximum steps for iterative refinement (default: 50). * - ``linear_solver_atol`` - Absolute tolerance for iterative refinement (default: 1e-12). * - ``linear_solver_rtol`` - Relative tolerance for iterative refinement (default: 1e-12). * - ``linear_solver`` - KKT solver backend (default: qtqp.LinearSolver.SCIPY). * - ``equilibrate`` - Scale/equilibrate data for numerical stability (default: True). Check out the `QTQP repository `_ for details. Lower values for absolute or relative tolerances yield more precise solutions at the cost of computation time. """ build_start_time = time.perf_counter() if initvals is not None and verbose: warnings.warn("QTQP: warm-start values are ignored") P, q, G, h, A, b, lb, ub = problem.unpack() # Convert box constraints to linear inequalities if lb is not None or ub is not None: G, h = linear_from_box_inequalities(G, h, lb, ub, use_sparse=True) # Convert to CSC format as required by QTQP P, G, A = ensure_sparse_matrices("qtqp", P, G, A) # Check for unconstrained case if G is None and A is None: warnings.warn( "QP is unconstrained: solving with SciPy's LSQR rather than QTQP" ) return solve_unconstrained(problem) # Build the constraint matrix in QTQP format # QTQP expects: a @ x + s = b # where s[:z] == 0 (equality constraints) # s[z:] >= 0 (inequality constraints) constraint_matrices: List[spa.csc_matrix] = [] constraint_vectors = [] # Add equality constraints first (these form the zero cone) z = 0 if A is not None and b is not None: constraint_matrices.append(A) constraint_vectors.append(b) z = A.shape[0] # Add inequality constraints (these form the nonnegative cone) if G is not None and h is not None: constraint_matrices.append(G) constraint_vectors.append(h) # QTQP requires at least one inequality constraint (z < m) if G is None and A is not None: warnings.warn( "QTQP cannot solve problems with only equality constraints; " "at least one inequality constraint is required" ) solution = Solution(problem) solution.found = False return solution # Stack all constraints a_qtqp = spa.vstack(constraint_matrices, format="csc") b_qtqp = np.concatenate(constraint_vectors) # QTQP uses 'c' for the cost vector (qpsolvers uses 'q') c_qtqp = q # QTQP uses 'p' for the cost matrix (qpsolvers uses 'P') p_qtqp = P if P is not None else None # Create QTQP solver instance solver = qtqp.QTQP( p=p_qtqp, a=a_qtqp, b=b_qtqp, c=c_qtqp, z=z, ) # Solve the problem solve_start_time = time.perf_counter() result = solver.solve(verbose=verbose, **kwargs) solve_end_time = time.perf_counter() # Build solution solution = Solution(problem) solution.extras = { "status": result.status, "stats": result.stats, } # Check solution status solution.found = result.status == qtqp.SolutionStatus.SOLVED if not solution.found: warnings.warn(f"QTQP terminated with status {result.status.value}") # Extract primal solution solution.x = result.x # Extract dual variables # result.y contains dual variables for all constraints # First z entries correspond to equality constraints (y) # Remaining entries correspond to inequality constraints (z) meq = z if meq > 0: solution.y = result.y[:meq] else: solution.y = np.empty((0,)) # Extract inequality duals if G is not None: z_ineq = result.y[meq:] # Split dual variables for linear and box inequalities z_linear, z_box = split_dual_linear_box(z_ineq, lb, ub) solution.z = z_linear solution.z_box = z_box else: solution.z = np.empty((0,)) solution.z_box = np.empty((0,)) # Compute objective value if solution.found and solution.x is not None: solution.obj = ( 0.5 * solution.x @ (P @ solution.x) if P is not None else 0.0 ) solution.obj += q @ solution.x solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def qtqp_solve_qp( P: Union[np.ndarray, spa.csc_matrix], q: np.ndarray, G: Optional[Union[np.ndarray, spa.csc_matrix]] = None, h: Optional[np.ndarray] = None, A: Optional[Union[np.ndarray, spa.csc_matrix]] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using QTQP. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using `QTQP`_. Parameters ---------- P : Symmetric cost matrix. q : Cost vector. G : Linear inequality matrix. h : Linear inequality vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. initvals : This argument is not used by QTQP. verbose : Set to `True` to print out extra information. Returns ------- : Primal solution to the QP, if found, otherwise ``None``. Notes ----- Keyword arguments are forwarded to the solver. For example, we can call ``qtqp_solve_qp(P, q, G, h, atol=1e-8, max_iter=200)``. QTQP is a primal-dual interior point method that solves convex quadratic programs. It requires the cost matrix P to be positive semidefinite. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = qtqp_solve_problem(problem, initvals, verbose, **kwargs) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/quadprog_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Solver interface for `quadprog `__. quadprog is a C implementation of the Goldfarb-Idnani dual algorithm [Goldfarb1983]_. It works best on well-conditioned dense problems. **Warm-start:** this solver interface does not support warm starting ❄️ """ import time import warnings from typing import Optional import numpy as np import scipy.sparse as spa from numpy import hstack, vstack from quadprog import solve_qp from ..conversions import linear_from_box_inequalities, split_dual_linear_box from ..exceptions import ProblemError from ..problem import Problem from ..solution import Solution def quadprog_solve_problem( problem: Problem, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Solution: """Solve a quadratic program using quadprog. Parameters ---------- problem : Quadratic program to solve. initvals : This argument is not used by quadprog. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP, if found, otherwise ``None``. Raises ------ ProblemError : If the cost matrix of the quadratic program if not positive definite, or if the problem is ill-formed in some way, for instance if some matrices are not dense. Note ---- The quadprog solver only considers the lower entries of :math:`P`, therefore it will use a different cost than the one intended if a non-symmetric matrix is provided. Notes ----- All other keyword arguments are forwarded to the quadprog solver. For instance, you can call ``quadprog_solve_qp(P, q, G, h, factorized=True)``. See the solver documentation for details. """ build_start_time = time.perf_counter() if initvals is not None and verbose: warnings.warn("warm-start values are ignored by quadprog") if problem.has_sparse: raise ProblemError("problem has sparse matrices") P, q, G, h, A, b, lb, ub = problem.unpack_as_dense() if lb is not None or ub is not None: G_, h = linear_from_box_inequalities(G, h, lb, ub, use_sparse=False) G = G_.toarray() if isinstance(G_, spa.csc_matrix) else G_ # for mypy qp_G = P qp_a = -q qp_C: Optional[np.ndarray] = None qp_b: Optional[np.ndarray] = None if A is not None and b is not None: if G is not None and h is not None: qp_C = -vstack([A, G]).T qp_b = -hstack([b, h]) else: qp_C = -A.T qp_b = -b meq = A.shape[0] else: # no equality constraint if G is not None and h is not None: qp_C = -G.T qp_b = -h meq = 0 solution = Solution(problem) try: solve_start_time = time.perf_counter() x, obj, xu, iterations, y, iact = solve_qp( qp_G, qp_a, qp_C, qp_b, meq, **kwargs ) solve_end_time = time.perf_counter() solution.found = True solution.x = x solution.obj = obj z, z_box = split_dual_linear_box(y[meq:], lb, ub) solution.y = y[:meq] if meq > 0 else np.empty((0,)) solution.z = z solution.z_box = z_box solution.extras = { "iact": iact, "iterations": iterations, "xu": xu, } except ValueError as error: solve_end_time = time.perf_counter() solution.found = False error_message = str(error) if "matrix G is not positive definite" in error_message: # quadprog writes G the cost matrix that we write P in this package raise ProblemError("matrix P is not positive definite") from error if "no solution" not in error_message: warnings.warn(f"quadprog raised a ValueError: {error_message}") solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def quadprog_solve_qp( P: np.ndarray, q: np.ndarray, G: Optional[np.ndarray] = None, h: Optional[np.ndarray] = None, A: Optional[np.ndarray] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, initvals: Optional[np.ndarray] = None, verbose: bool = False, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using quadprog. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using `quadprog `__. Parameters ---------- P : Symmetric cost matrix. q : Cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. initvals : This argument is not used by quadprog. verbose : Set to `True` to print out extra information. Returns ------- : Primal solution to the QP, if found, otherwise ``None``. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = quadprog_solve_problem(problem, initvals, verbose, **kwargs) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/scs_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Solver interface for `SCS `__. SCS (Splitting Conic Solver) is a numerical optimization package for solving large-scale convex quadratic cone problems, which is a general class of problems that includes quadratic programming. If you use SCS in a scientific work, consider citing the corresponding paper [ODonoghue2021]_. **Warm-start:** this solver interface supports warm starting 🔥 """ import time import warnings from typing import Any, Dict, Optional, Union import numpy as np import scipy.sparse as spa from numpy import ndarray from scipy.sparse import csc_matrix from scs import solve from ..conversions import ensure_sparse_matrices from ..problem import Problem from ..solution import Solution from ..solve_unconstrained import solve_unconstrained # See https://www.cvxgrp.org/scs/api/exit_flags.html#exit-flags __status_val_meaning__ = { -7: "INFEASIBLE_INACCURATE", -6: "UNBOUNDED_INACCURATE", -5: "SIGINT", -4: "FAILED", -3: "INDETERMINATE", -2: "INFEASIBLE (primal infeasible, dual unbounded)", -1: "UNBOUNDED (primal unbounded, dual infeasible)", 0: "UNFINISHED (never returned, used as placeholder)", 1: "SOLVED", 2: "SOLVED_INACCURATE", } def __add_box_cone( n: int, lb: Optional[ndarray], ub: Optional[ndarray], cone: Dict[str, Any], data: Dict[str, Any], ) -> None: """Add box cone to the problem. Parameters ---------- n : Number of optimization variables. lb : Lower bound constraint vector. ub : Upper bound constraint vector. cone : SCS cone dictionary. data : SCS data dictionary. Notes ----- See the `SCS Cones `__ documentation for details. """ cone["bl"] = lb if lb is not None else np.full((n,), -np.inf) cone["bu"] = ub if ub is not None else np.full((n,), +np.inf) zero_row = csc_matrix((1, n)) data["A"] = spa.vstack( ((data["A"],) if "A" in data else ()) + (zero_row, -spa.eye(n)), format="csc", ) data["b"] = np.hstack( ((data["b"],) if "b" in data else ()) + (1.0, np.zeros(n)) ) def scs_solve_problem( problem: Problem, initvals: Optional[ndarray] = None, verbose: bool = False, **kwargs, ) -> Solution: """Solve a quadratic program using SCS. Parameters ---------- problem : Quadratic program to solve. initvals : Warm-start guess vector for the primal solution. verbose : Set to `True` to print out extra information. Returns ------- : Solution returned by the solver. Raises ------ ValueError If the quadratic program is not unbounded below. Notes ----- Keyword arguments are forwarded as is to SCS. For instance, we can call ``scs_solve_qp(P, q, G, h, eps_abs=1e-6, eps_rel=1e-4)``. SCS settings include the following: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``max_iters`` - Maximum number of iterations to run. * - ``time_limit_secs`` - Time limit for solve run in seconds (can be fractional). 0 is interpreted as no limit. * - ``eps_abs`` - Absolute feasibility tolerance. See `Termination criteria `__. * - ``eps_rel`` - Relative feasibility tolerance. See `Termination criteria `__. * - ``eps_infeas`` - Infeasibility tolerance (primal and dual), see `Certificate of infeasibility `_. * - ``normalize`` - Whether to perform heuristic data rescaling. See `Data equilibration `__. Check out the `SCS settings `_ documentation for all available settings. """ build_start_time = time.perf_counter() P, q, G, h, A, b, lb, ub = problem.unpack() P, G, A = ensure_sparse_matrices("scs", P, G, A) n = P.shape[0] data: Dict[str, Any] = {"P": P, "c": q} cone: Dict[str, Any] = {} if initvals is not None: data["x"] = initvals if A is not None and b is not None: if G is not None and h is not None: data["A"] = spa.vstack([A, G], format="csc") data["b"] = np.hstack([b, h]) cone["z"] = b.shape[0] # zero cone cone["l"] = h.shape[0] # positive cone else: # G is None and h is None data["A"] = A data["b"] = b cone["z"] = b.shape[0] # zero cone elif G is not None and h is not None: data["A"] = G data["b"] = h cone["l"] = h.shape[0] # positive cone elif lb is None and ub is None: # no constraint warnings.warn( "QP is unconstrained: solving with SciPy's LSQR rather than SCS" ) return solve_unconstrained(problem) if lb is not None or ub is not None: __add_box_cone(n, lb, ub, cone, data) kwargs["verbose"] = verbose solve_start_time = time.perf_counter() result = solve(data, cone, **kwargs) solve_end_time = time.perf_counter() solution = Solution(problem) solution.extras = result["info"] status_val = result["info"]["status_val"] solution.found = status_val == 1 if not solution.found: warnings.warn( f"SCS returned {status_val}: {__status_val_meaning__[status_val]}" ) solution.x = result["x"] meq = A.shape[0] if A is not None else 0 solution.y = result["y"][:meq] if A is not None else np.empty((0,)) solution.z = ( result["y"][meq : meq + G.shape[0]] if G is not None else np.empty((0,)) ) solution.z_box = ( -result["y"][-n:] if lb is not None or ub is not None else np.empty((0,)) ) solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def scs_solve_qp( P: Union[ndarray, csc_matrix], q: ndarray, G: Optional[Union[ndarray, csc_matrix]] = None, h: Optional[ndarray] = None, A: Optional[Union[ndarray, csc_matrix]] = None, b: Optional[ndarray] = None, lb: Optional[ndarray] = None, ub: Optional[ndarray] = None, initvals: Optional[ndarray] = None, verbose: bool = False, **kwargs, ) -> Optional[ndarray]: r"""Solve a quadratic program using SCS. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{x}{\mbox{minimize}} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using `SCS `__. Parameters ---------- P : Primal quadratic cost matrix. q : Primal quadratic cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. initvals : Warm-start guess vector for the primal solution. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP, if found, otherwise ``None``. Raises ------ ValueError If the quadratic program is not unbounded below. Notes ----- Keyword arguments are forwarded as is to SCS. For instance, we can call ``scs_solve_qp(P, q, G, h, eps_abs=1e-6, eps_rel=1e-4)``. SCS settings include the following: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Description * - ``max_iters`` - Maximum number of iterations to run. * - ``time_limit_secs`` - Time limit for solve run in seconds (can be fractional). 0 is interpreted as no limit. * - ``eps_abs`` - Absolute feasibility tolerance. See `Termination criteria `__. * - ``eps_rel`` - Relative feasibility tolerance. See `Termination criteria `__. * - ``eps_infeas`` - Infeasibility tolerance (primal and dual), see `Certificate of infeasibility `_. * - ``normalize`` - Whether to perform heuristic data rescaling. See `Data equilibration `__. Check out the `SCS settings `_ documentation for all available settings. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = scs_solve_problem(problem, initvals, verbose, **kwargs) return solution.x if solution.found else None ================================================ FILE: qpsolvers/solvers/sip_.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Solver interface for `SIP`_. .. _SIP: https://github.com/joaospinto/sip_python SIP is a general NLP solver based. It is based on the barrier augmented Lagrangian method, which combines the interior point and augmented Lagrangian methods. If you are using SIP in a scientific work, consider citing the corresponding GitHub repository (or paper, if one has been released). **Warm-start:** this solver interface supports warm starting 🔥 """ import time import warnings from typing import Optional, Union import numpy as np import scipy.sparse as spa import sip_python as sip from ..conversions import linear_from_box_inequalities, split_dual_linear_box from ..exceptions import ProblemError from ..problem import Problem from ..solution import Solution def sip_solve_problem( problem: Problem, initvals: Optional[np.ndarray] = None, verbose: bool = False, allow_non_psd_P: bool = False, **kwargs, ) -> Solution: """Solve a quadratic program using SIP. Parameters ---------- problem : Quadratic program to solve. initvals : Warm-start guess vector for the primal solution. verbose : Set to `True` to print out extra information. Returns ------- : Solution to the QP returned by the solver. Notes ----- All other keyword arguments are forwarded as options to SIP. For instance, you can call ``sip_solve_qp(P, q, G, h, eps_abs=1e-6)``. For a quick overview, the solver accepts the following settings: .. list-table:: :widths: 30 70 :header-rows: 1 * - Name - Effect * - max_iterations - The maximum number of iterations the solver can do. * - max_ls_iterations - The maximum cumulative number of line search iterations. * - min_iterations_for_convergence - The least number of iterations until we can declare convergence. * - num_iterative_refinement_steps - The number of iterative refinement steps. * - max_kkt_violation - The maximum allowed violation of the KKT system. * - max_merit_slope - The maximum allowed merit function slope. * - initial_regularization - The initial x-regularizatino to be applied on the LHS. * - regularization_decay_factor - The multiplicative decay of the x-regularization coefficient. * - tau - A parameter of the fraction-to-the-boundary rule. * - start_ls_with_alpha_s_max - Determines whether we start with alpha=alpha_s_max or alpha=1. * - initial_mu - The initial barrier function coefficient. * - mu_update_factor - Determines how much mu decreases per iteration. * - mu_min - The minimum barrier coefficient. * - initial_penalty_parameter - The initial penalty parameter of the Augmented Lagrangian. * - min_acceptable_constraint_violation_ratio - Least acceptable constraint violation ratio to not increase eta. * - penalty_parameter_increase_factor - By what factor to increase eta. * - penalty_parameter_decrease_factor - By what factor to decrease eta. * - max_penalty_parameter - The maximum allowed penalty parameter in the AL merit function. * - armijo_factor - Determines when we accept a line search step. * - line_search_factor - Determines how much to backtrack at each line search iteration. * - line_search_min_step_size - Determines when we declare a line search failure. * - min_merit_slope_to_skip_line_search - Min merit slope to skip the line search. * - dual_armijo_factor - Fraction of the primal merit decrease to allow on the dual update. * - min_allowed_merit_increase - The minimum allowed merit function increase in the dual update. * - enable_elastics - Whether to enable the usage of elastic variables. * - elastic_var_cost_coeff - Elastic variables cost coefficient. * - enable_line_search_failures - Halts the optimization process if a good step is not found. * - print_logs - Determines whether we should print the solver logs. * - print_line_search_logs - Determines whether we should print the line search logs. * - print_search_direction_logs - Whether we should print the search direction computation logs. * - print_derivative_check_logs - Whether to print derivative check logs when something looks off. * - only_check_search_direction_slope - Only derivative-check the search direction. * - assert_checks_pass - Handle checks with assert calls. This list may not be exhaustive. Check the `Settings` struct in the `solver code `__ for details. """ build_start_time = time.perf_counter() P, q, G_, h, A_, b, lb, ub = problem.unpack() if lb is not None or ub is not None: G_, h = linear_from_box_inequalities( G_, h, lb, ub, use_sparse=problem.has_sparse ) n: int = q.shape[0] # SIP does not support A, b, G, and h to be None. G: Union[np.ndarray, spa.csc_matrix, spa.csr_matrix] = ( G_ if G_ is not None else spa.csr_matrix(np.zeros((0, n))) ) A: Union[np.ndarray, spa.csc_matrix, spa.csr_matrix] = ( A_ if A_ is not None else spa.csr_matrix(np.zeros((0, n))) ) h = np.zeros((0,)) if h is None else h b = np.zeros((0,)) if b is None else b # Remove any infs from h. G[np.isinf(h), :] = 0.0 h[np.isinf(h)] = 1.0 if not isinstance(P, spa.csr_matrix): P = spa.csc_matrix(P) if verbose: warnings.warn("Converted P to a csc_matrix.") if not isinstance(G, spa.csr_matrix): G = spa.csr_matrix(G) if verbose: warnings.warn("Converted G to a csr_matrix.") if not isinstance(A, spa.csr_matrix): A = spa.csr_matrix(A) if verbose: warnings.warn("Converted A to a csr_matrix.") P.eliminate_zeros() G.eliminate_zeros() A.eliminate_zeros() P_T = spa.csc_matrix(P.T) if ( (P.indices != P_T.indices).any() or (P.indptr != P_T.indptr).any() or (P.data != P_T.data).any() ): raise ProblemError("P should be symmetric.") if G is None and h is not None: raise ProblemError( "Inconsistent inequalities: G is not set but h is set" ) if G is not None and h is None: raise ProblemError("Inconsistent inequalities: G is set but h is None") if A is None and b is not None: raise ProblemError( "Inconsistent inequalities: A is not set but b is set" ) if A is not None and b is None: raise ProblemError("Inconsistent inequalities: A is set but b is None") k = None if allow_non_psd_P: eigenvalues, _eigenvectors = spa.linalg.eigsh(P, k=1, which="SM") k = -min(eigenvalues[0], 0.0) + 1e-3 else: k = 1e-6 # hess_L = P + k * spa.eye(n); # the code below avoids potential index cancellations. hess_L = spa.coo_matrix(P) upp_hess_L_rows = np.concatenate([hess_L.row, np.arange(n)]) upp_hess_L_cols = np.concatenate([hess_L.col, np.arange(n)]) upp_hess_L_data = np.concatenate([hess_L.data, k * np.ones(n)]) hess_L = spa.coo_matrix( (upp_hess_L_data, (upp_hess_L_rows, upp_hess_L_cols)), shape=P.shape ) hess_L.sum_duplicates() upp_hess_L = spa.triu(hess_L.tocsc()) qs = sip.QDLDLSettings() qs.permute_kkt_system = True qs.kkt_pinv = sip.get_kkt_perm_inv( P=hess_L, A=A, G=G, ) pd = sip.ProblemDimensions() pd.x_dim = n pd.s_dim = h.shape[0] pd.y_dim = b.shape[0] pd.upper_hessian_lagrangian_nnz = upp_hess_L.nnz pd.jacobian_c_nnz = A.nnz pd.jacobian_g_nnz = G.nnz pd.kkt_nnz, pd.kkt_L_nnz = sip.get_kkt_and_L_nnzs( P=hess_L, A=A, G=G, perm_inv=qs.kkt_pinv, ) pd.is_jacobian_c_transposed = True pd.is_jacobian_g_transposed = True vars_ = sip.Variables(pd) if initvals is not None: vars_.x[:] = initvals # type: ignore[index] else: vars_.x[:] = 0.0 # type: ignore[index] vars_.s[:] = 1.0 # type: ignore[index] vars_.y[:] = 0.0 # type: ignore[index] vars_.e[:] = 0.0 # type: ignore[index] vars_.z[:] = 1.0 # type: ignore[index] ss = sip.Settings() ss.max_iterations = 100 ss.max_ls_iterations = 1000 ss.max_kkt_violation = 1e-8 ss.max_merit_slope = 1e-16 ss.penalty_parameter_increase_factor = 2.0 ss.mu_update_factor = 0.5 ss.mu_min = 1e-16 ss.max_penalty_parameter = 1e16 ss.assert_checks_pass = True ss.print_logs = verbose ss.print_line_search_logs = verbose ss.print_search_direction_logs = verbose ss.print_derivative_check_logs = False for key, value in kwargs.items(): try: setattr(ss, key, value) except AttributeError: if verbose: warnings.warn( f"Received an undefined solver setting {key}\ with value {value}" ) def mc(mci: sip.ModelCallbackInput) -> sip.ModelCallbackOutput: mco = sip.ModelCallbackOutput() Px = P.T @ mci.x # type: ignore[operator] mco.f = 0.5 * np.dot(Px, mci.x) + np.dot(q, mci.x) mco.c = A @ mci.x - b # type: ignore[operator] mco.g = G @ mci.x - h # type: ignore[operator] mco.gradient_f = Px + q mco.jacobian_c = A mco.jacobian_g = G mco.upper_hessian_lagrangian = upp_hess_L return mco solver = sip.Solver(ss, qs, pd, mc) solve_start_time = time.perf_counter() output = solver.solve(vars_) solve_end_time = time.perf_counter() solution = Solution(problem) solution.extras = {"sip_output": output, "sip_vars": vars_} solution.found = output.exit_status == sip.Status.SOLVED solution.obj = 0.5 * np.dot( P.T @ vars_.x, # type: ignore[operator] vars_.x, ) + np.dot(q, vars_.x) solution.x = np.array(vars_.x) solution.y = np.array(vars_.y) if h is not None and vars_.z is not None: z_sip = np.array(vars_.z) z, z_box = split_dual_linear_box(z_sip, lb, ub) solution.z = z solution.z_box = z_box solution.build_time = solve_start_time - build_start_time solution.solve_time = solve_end_time - solve_start_time return solution def sip_solve_qp( P: Union[np.ndarray, spa.csc_matrix], q: np.ndarray, G: Optional[Union[np.ndarray, spa.csc_matrix]] = None, h: Optional[np.ndarray] = None, A: Optional[Union[np.ndarray, spa.csc_matrix]] = None, b: Optional[np.ndarray] = None, lb: Optional[np.ndarray] = None, ub: Optional[np.ndarray] = None, initvals: Optional[np.ndarray] = None, allow_non_psd_P: bool = False, verbose: bool = False, **kwargs, ) -> Optional[np.ndarray]: r"""Solve a quadratic program using SIP. The quadratic program is defined as: .. math:: \begin{split}\begin{array}{ll} \underset{\mbox{minimize}}{x} & \frac{1}{2} x^T P x + q^T x \\ \mbox{subject to} & G x \leq h \\ & A x = b \\ & lb \leq x \leq ub \end{array}\end{split} It is solved using `SIP `__. Parameters ---------- P : Positive semidefinite cost matrix. q : Cost vector. G : Linear inequality constraint matrix. h : Linear inequality constraint vector. A : Linear equality constraint matrix. b : Linear equality constraint vector. lb : Lower bound constraint vector. ub : Upper bound constraint vector. verbose : Set to `True` to print out extra information. initvals : Warm-start guess vector for the primal solution. Returns ------- : Primal solution to the QP, if found, otherwise ``None``. """ problem = Problem(P, q, G, h, A, b, lb, ub) solution = sip_solve_problem( problem, initvals, verbose, allow_non_psd_P, **kwargs, ) return solution.x if solution.found else None ================================================ FILE: qpsolvers/utils.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Utility functions.""" from typing import Union import numpy as np import scipy.sparse as spa def print_matrix_vector( A: Union[np.ndarray, spa.csc_matrix], A_label: str, b: np.ndarray, b_label: str, column_width: int = 24, ) -> None: """Print a matrix and vector side by side to the terminal. Parameters ---------- A : Union[np.ndarray, spa.csc_matrix] to print. A_label : Label for A. b : np.ndarray to print. b_label : Label for b. column_width : Number of characters for the matrix and vector text columns. """ if isinstance(A, np.ndarray) and A.ndim == 1: A = A.reshape((1, A.shape[0])) if isinstance(A, spa.csc_matrix): A = A.toarray() if A.shape[0] == b.shape[0]: A_string = f"{A_label} =\n{A}" b_string = f"{b_label} =\n{b.reshape((A.shape[0], 1))}" elif A.shape[0] > b.shape[0]: m = b.shape[0] A_string = f"{A_label} =\n{A[:m]}" b_string = f"{b_label} =\n{b.reshape(m, 1)}" A_string += f"\n{A[m:]}" b_string += "\n " * (A.shape[0] - m) else: # A.shape[0] < b.shape[0] n = A.shape[0] k = b.shape[0] - n A_string = f"{A_label} =\n{A}" b_string = f"{b_label} =\n{b[:n].reshape(n, 1)}" A_string += "\n " * k b_string += f"\n{b[n:].reshape(k, 1)}" A_lines = A_string.splitlines() b_lines = b_string.splitlines() for i, A_line in enumerate(A_lines): print(A_line.ljust(column_width) + b_lines[i].ljust(column_width)) ================================================ FILE: qpsolvers/warnings.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2025 Inria """Warnings from qpsolvers.""" class QPWarning(UserWarning): """Base class for qpsolvers warnings.""" class SparseConversionWarning(QPWarning): """Warning issued when converting NumPy arrays to SciPy sparse matrices.""" ================================================ FILE: tests/__init__.py ================================================ # Make sure Python treats the test directory as a package. ================================================ FILE: tests/problems.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors import numpy as np from qpsolvers import Problem def get_sd3310_problem() -> Problem: """ Get a small dense problem with 3 optimization variables, 3 inequality constraints, 1 equality constraint and 0 box constraint. """ M = np.array([[1.0, 2.0, 0.0], [-8.0, 3.0, 2.0], [0.0, 1.0, 1.0]]) P = np.dot(M.T, M) # this is a positive definite matrix q = np.dot(np.array([3.0, 2.0, 3.0]), M).reshape((3,)) G = np.array([[1.0, 2.0, 1.0], [2.0, 0.0, 1.0], [-1.0, 2.0, -1.0]]) h = np.array([3.0, 2.0, -2.0]).reshape((3,)) A = np.array([1.0, 1.0, 1.0]) b = np.array([1.0]) return Problem(P, q, G, h, A, b) def get_qpmad_demo_problem(): """ Problem from qpmad's `demo.cpp `__. """ P = np.eye(20) q = np.ones((20,)) G = np.vstack([np.ones((1, 20)), -np.ones((1, 20))]) h = np.hstack([1.5, 1.5]) lb = np.array( [ 1.0, 2.0, 3.0, 4.0, -5.0, -5.0, -5.0, -5.0, -5.0, -5.0, -5.0, -5.0, -5.0, -5.0, -5.0, -5.0, -5.0, -5.0, -5.0, -5.0, ] ) ub = np.array( [ 1.0, 2.0, 3.0, 4.0, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, ] ) return Problem(P, q, G, h, lb=lb, ub=ub) ================================================ FILE: tests/test_clarabel.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for Clarabel.""" import unittest import warnings from qpsolvers.problems import get_qpsut01 try: import clarabel from qpsolvers.solvers.clarabel_ import clarabel_solve_problem class TestClarabel(unittest.TestCase): """Test fixture for the Clarabel.rs solver.""" def test_time_limit(self): """Call Clarabel.rs with an infeasibly low time limit.""" problem, ref_solution = get_qpsut01() solution = clarabel_solve_problem(problem, time_limit=1e-10) status = solution.extras["status"] self.assertEqual(status, clarabel.SolverStatus.MaxTime) # See https://github.com/oxfordcontrol/Clarabel.rs/issues/10 self.assertFalse(status != clarabel.SolverStatus.MaxTime) def test_status(self): """Check that result status is consistent with its string repr. Context: https://github.com/oxfordcontrol/Clarabel.rs/issues/10 """ problem, _ = get_qpsut01() solution = clarabel_solve_problem(problem) status = solution.extras["status"] check_1 = str(status) != "Solved" check_2 = status != clarabel.SolverStatus.Solved self.assertEqual(check_1, check_2) except ImportError as exn: # in case the solver is not installed warnings.warn(f"Skipping Clarabel.rs tests: {exn}") ================================================ FILE: tests/test_combine_linear_box_inequalities.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for the `solve_qp` function.""" import unittest import warnings import numpy as np from qpsolvers import available_solvers from qpsolvers.conversions import combine_linear_box_inequalities class TestCombineLinearBoxInequalities(unittest.TestCase): def setUp(self): """Prepare test fixture.""" warnings.simplefilter("ignore", category=DeprecationWarning) warnings.simplefilter("ignore", category=UserWarning) def get_dense_problem(self): """Get dense problem as a sextuple of values to unpack. Returns ------- P : numpy.ndarray Symmetric cost matrix . q : numpy.ndarray Cost vector. G : numpy.ndarray Linear inequality matrix. h : numpy.ndarray Linear inequality vector. A : numpy.ndarray, scipy.sparse.csc_matrix or cvxopt.spmatrix Linear equality matrix. b : numpy.ndarray Linear equality vector. """ M = np.array([[1.0, 2.0, 0.0], [-8.0, 3.0, 2.0], [0.0, 1.0, 1.0]]) P = np.dot(M.T, M) # this is a positive definite matrix q = np.dot(np.array([3.0, 2.0, 3.0]), M).reshape((3,)) G = np.array([[1.0, 2.0, 1.0], [2.0, 0.0, 1.0], [-1.0, 2.0, -1.0]]) h = np.array([3.0, 2.0, -2.0]).reshape((3,)) A = np.array([1.0, 1.0, 1.0]) b = np.array([1.0]) return P, q, G, h, A, b @staticmethod def get_test_all_shapes(solver: str): """Get test function for a given solver. This variant tries all possible shapes for matrix and vector parameters. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): P, q, G, h, _, _ = self.get_dense_problem() A = np.array([[1.0, 0.0, 0.0], [0.0, 0.4, 0.5]]) b = np.array([-0.5, -1.2]) lb = np.array([-0.5, -2, -0.8]) ub = np.array([+1.0, +1.0, +1.0]) ineq_variants = ((None, None), (G, h), (G[0], np.array([h[0]]))) eq_variants = ((None, None), (A, b), (A[0], np.array([b[0]]))) box_variants = ((None, None), (lb, None), (None, ub), (lb, ub)) cases = [ { "P": P, "q": q, "G": G_case, "h": h_case, "A": A_case, "b": b_case, "lb": lb_case, "ub": ub_case, } for (G_case, h_case) in ineq_variants for (A_case, b_case) in eq_variants for (lb_case, ub_case) in box_variants ] for i, test_case in enumerate(cases): G = test_case["G"] h = test_case["h"] lb = test_case["lb"] ub = test_case["ub"] n = test_case["q"].shape[0] if G is None and lb is None and ub is None: continue elif isinstance(G, np.ndarray) and G.ndim == 1: G = G.reshape((1, G.shape[0])) C, u, l = combine_linear_box_inequalities( G, h, lb, ub, n, use_csc=False ) self.assertTrue(isinstance(C, np.ndarray)) self.assertTrue(isinstance(u, np.ndarray)) self.assertTrue(isinstance(l, np.ndarray)) return test # Generate test fixtures for each solver for solver in available_solvers: setattr( TestCombineLinearBoxInequalities, f"test_all_shapes_{solver}", TestCombineLinearBoxInequalities.get_test_all_shapes(solver), ) ================================================ FILE: tests/test_conversions.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for internal conversion functions.""" import unittest import numpy as np import scipy.sparse as spa from qpsolvers.conversions import linear_from_box_inequalities class TestConversions(unittest.TestCase): """Test fixture for box to linear inequality conversion.""" def __test_linear_from_box_inequalities(self, G, h, lb, ub): G2, h2 = linear_from_box_inequalities(G, h, lb, ub, use_sparse=False) m = G.shape[0] if G is not None else 0 k = lb.shape[0] self.assertTrue(np.allclose(G2[m : m + k, :], -np.eye(k))) self.assertTrue(np.allclose(h2[m : m + k], -lb)) self.assertTrue(np.allclose(G2[m + k : m + 2 * k, :], np.eye(k))) self.assertTrue(np.allclose(h2[m + k : m + 2 * k], ub)) def test_concatenate_bounds(self): G = np.array([[1.0, 2.0, 1.0], [2.0, 0.0, 1.0], [-1.0, 2.0, -1.0]]) h = np.array([3.0, 2.0, -2.0]).reshape((3,)) lb = np.array([-1.0, -1.0, -1.0]) ub = np.array([1.0, 1.0, 1.0]) self.__test_linear_from_box_inequalities(G, h, lb, ub) def test_pure_bounds(self): lb = np.array([-1.0, -1.0, -1.0]) ub = np.array([1.0, 1.0, 1.0]) self.__test_linear_from_box_inequalities(None, None, lb, ub) def test_skip_infinite_bounds(self): """ TODO(scaron): infinite box bounds are skipped by the conversion. """ G = np.array([[1.0, 2.0, 1.0], [2.0, 0.0, 1.0], [-1.0, 2.0, -1.0]]) h = np.array([3.0, 2.0, -2.0]).reshape((3,)) lb = np.array([-np.inf, -np.inf, -np.inf]) ub = np.array([np.inf, np.inf, np.inf]) G2, h2 = linear_from_box_inequalities(G, h, lb, ub, use_sparse=False) if False: # TODO(scaron): update behavior self.assertTrue(np.allclose(G2, G)) self.assertTrue(np.allclose(h2, h)) def test_skip_partial_infinite_bounds(self): """ TODO(scaron): all values in the combined constraint vector are finite, even if some input box bounds are infinite. """ G = np.array([[1.0, 2.0, 1.0], [2.0, 0.0, 1.0], [-1.0, 2.0, -1.0]]) h = np.array([3.0, 2.0, -2.0]).reshape((3,)) lb = np.array([-1.0, -np.inf, -1.0]) ub = np.array([np.inf, 1.0, 1.0]) G2, h2 = linear_from_box_inequalities(G, h, lb, ub, use_sparse=False) if False: # TODO(scaron): update behavior self.assertTrue(np.isfinite(h2).all()) def test_sparse_conversion(self): """ Box concatenation on a sparse problem without linear inequality constraints yields a sparse problem. """ n = 1000 lb = np.full((n,), -1.0) ub = np.full((n,), +1.0) G, h = linear_from_box_inequalities( None, None, lb, ub, use_sparse=True ) self.assertTrue(isinstance(G, spa.csc_matrix)) ================================================ FILE: tests/test_copt.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for COPT.""" import unittest import warnings from .problems import get_sd3310_problem try: from qpsolvers.solvers.copt_ import copt_solve_qp class TestCOPT(unittest.TestCase): """Test fixture for the COPT solver.""" def test_copt_params(self): problem = get_sd3310_problem() x = copt_solve_qp( problem.P, problem.q, problem.G, problem.h, verbose=True, FeasTol=1e-8, DualTol=1e-8, Presolve=0 ) self.assertIsNotNone(x) except ImportError as exn: # solver not installed warnings.warn(f"Skipping COPT tests: {exn}") ================================================ FILE: tests/test_cvxopt.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for CVXOPT.""" import unittest import warnings from typing import Tuple import numpy as np import scipy.sparse as spa from numpy import array, ones from numpy.linalg import norm from qpsolvers.problems import get_qpsut01 from .problems import get_sd3310_problem try: import cvxopt from qpsolvers.solvers.cvxopt_ import cvxopt_solve_problem, cvxopt_solve_qp class TestCVXOPT(unittest.TestCase): """Test fixture for the CVXOPT solver.""" def setUp(self): """Prepare test fixture.""" warnings.simplefilter("ignore", category=UserWarning) def get_sparse_problem( self, ) -> Tuple[cvxopt.matrix, np.ndarray, cvxopt.matrix, np.ndarray]: """Get sparse problem as a quadruplet of values to unpack. Returns ------- P : Symmetric cost matrix. q : Cost vector. G : Linear inequality matrix. h : Linear inequality vector. """ n = 150 M = spa.lil_matrix(spa.eye(n)) for i in range(1, n - 1): M[i, i + 1] = -1 M[i, i - 1] = 1 P = spa.csc_matrix(M.dot(M.transpose())) q = -ones((n,)) G = spa.csc_matrix(-spa.eye(n)) h = -2.0 * ones((n,)) return P, q, G, h def test_sparse(self): """Test CVXOPT on a sparse problem.""" P, q, G, h = self.get_sparse_problem() x = cvxopt_solve_qp(P, q, G, h) self.assertIsNotNone(x, 'solver="cvxopt"') known_solution = array([2.0] * 149 + [3.0]) sol_tolerance = 1e-2 # aouch, not great! self.assertLess( norm(x - known_solution), sol_tolerance, 'solver="cvxopt"' ) self.assertLess(max(G.dot(x) - h), 1e-10, 'solver="cvxopt"') def test_extra_kwargs(self): """Call CVXOPT with various solver-specific settings.""" problem = get_sd3310_problem() x = cvxopt_solve_qp( problem.P, problem.q, problem.G, problem.h, problem.A, problem.b, maxiters=10, abstol=1e-1, reltol=1e-1, feastol=1e-2, refinement=3, ) self.assertIsNotNone(x, 'solver="cvxopt"') def test_infinite_linear_bounds(self): """CVXOPT does not yield a domain error on infinite bounds.""" problem, _ = get_qpsut01() problem.h[1] = +np.inf x = cvxopt_solve_problem(problem) self.assertIsNotNone(x, 'solver="cvxopt"') def test_infinite_box_bounds(self): """CVXOPT does not yield a domain error infinite box bounds.""" problem, _ = get_qpsut01() problem.lb[1] = -np.inf problem.ub[1] = +np.inf x = cvxopt_solve_problem(problem) self.assertIsNotNone(x, 'solver="cvxopt"') except ImportError as exn: # in case the solver is not installed warnings.warn(f"Skipping CVXOPT tests: {exn}") ================================================ FILE: tests/test_ecos.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for ECOS.""" import unittest import warnings import numpy as np from qpsolvers.exceptions import ProblemError from .problems import get_sd3310_problem try: from qpsolvers.solvers.ecos_ import ecos_solve_qp class TestECOS(unittest.TestCase): """Tests specific to ECOS.""" def test_problem(self): problem = get_sd3310_problem() P, q, G, h, A, b, lb, ub = problem.unpack() self.assertIsNotNone(ecos_solve_qp(P, q, G, h, A, b, lb, ub)) def test_infinite_inequality(self): problem = get_sd3310_problem() P, q, G, h, A, b, _, _ = problem.unpack() lb = np.array([-1.0, -np.inf, -1.0]) ub = np.array([np.inf, 1.0, 1.0]) with self.assertRaises(ProblemError): ecos_solve_qp(P, q, G, h, lb=lb, ub=ub) except ImportError as exn: # solver not installed warnings.warn(f"Skipping ECOS tests: {exn}") ================================================ FILE: tests/test_gurobi.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for Gurobi.""" import unittest import warnings from .problems import get_sd3310_problem try: from qpsolvers.solvers.gurobi_ import gurobi_solve_qp class TestGurobi(unittest.TestCase): """Test fixture for the Gurobi solver.""" def test_gurobi_params(self): problem = get_sd3310_problem() x = gurobi_solve_qp( problem.P, problem.q, problem.G, problem.h, TimeLimit=0.1, FeasibilityTol=1e-8, ) self.assertIsNotNone(x) except ImportError as exn: # solver not installed warnings.warn(f"Skipping Gurobi tests: {exn}") ================================================ FILE: tests/test_highs.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for HiGHS.""" import unittest import warnings from .problems import get_sd3310_problem try: from qpsolvers.solvers.highs_ import highs_solve_qp class TestHiGHS(unittest.TestCase): """Test fixture for the HiGHS solver.""" def setUp(self): """Prepare test fixture.""" warnings.simplefilter("ignore", category=UserWarning) def test_highs_tolerances(self): problem = get_sd3310_problem() x = highs_solve_qp( problem.P, problem.q, problem.G, problem.h, problem.A, problem.b, time_limit=0.1, primal_feasibility_tolerance=1e-1, dual_feasibility_tolerance=1e-1, ) self.assertIsNotNone(x) except ImportError as exn: # solver not installed warnings.warn(f"Skipping HiGHS tests: {exn}") ================================================ FILE: tests/test_jaxopt_osqp.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2025 Stéphane Caron and the qpsolvers contributors """Unit tests for jaxopt.OSQP.""" import unittest import warnings try: import jax.numpy as jnp from qpsolvers import solve_qp class TestKVXOPT(unittest.TestCase): """Test fixture for the KVXOPT solver.""" def setUp(self): """Prepare test fixture.""" warnings.simplefilter("ignore", category=UserWarning) def test_jax_array_input(self): """We can call ``solve_qp`` with jax.Array matrices.""" M = jnp.array([[1.0, 2.0, 0.0], [-8.0, 3.0, 2.0], [0.0, 1.0, 1.0]]) P = M.T @ M # this is a positive definite matrix q = jnp.array([3.0, 2.0, 3.0]) @ M G = jnp.array( [[1.0, 2.0, 1.0], [2.0, 0.0, 1.0], [-1.0, 2.0, -1.0]] ) h = jnp.array([3.0, 2.0, -2.0]) A = jnp.array([1.0, 1.0, 1.0]) b = jnp.array([1.0]) x = solve_qp(P, q, G, h, A, b, solver="jaxopt_osqp") self.assertIsNotNone(x) except ImportError as exn: # in case the solver is not installed warnings.warn(f"Skipping jaxopt.OSQP tests: {exn}") ================================================ FILE: tests/test_kvxopt.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for KVXOPT.""" import unittest import warnings from typing import Tuple import numpy as np import scipy.sparse as spa from numpy import array, ones from numpy.linalg import norm from qpsolvers.problems import get_qpsut01 from .problems import get_sd3310_problem try: import kvxopt from qpsolvers.solvers.kvxopt_ import kvxopt_solve_problem, kvxopt_solve_qp class TestKVXOPT(unittest.TestCase): """Test fixture for the KVXOPT solver.""" def setUp(self): """Prepare test fixture.""" warnings.simplefilter("ignore", category=UserWarning) def get_sparse_problem( self, ) -> Tuple[kvxopt.matrix, np.ndarray, kvxopt.matrix, np.ndarray]: """Get sparse problem as a quadruplet of values to unpack. Returns ------- P : Symmetric cost matrix. q : Cost vector. G : Linear inequality matrix. h : Linear inequality vector. """ n = 150 M = spa.lil_matrix(spa.eye(n)) for i in range(1, n - 1): M[i, i + 1] = -1 M[i, i - 1] = 1 P = spa.csc_matrix(M.dot(M.transpose())) q = -ones((n,)) G = spa.csc_matrix(-spa.eye(n)) h = -2.0 * ones((n,)) return P, q, G, h def test_sparse(self): """Test KVXOPT on a sparse problem.""" P, q, G, h = self.get_sparse_problem() x = kvxopt_solve_qp(P, q, G, h) self.assertIsNotNone(x, 'solver="kvxopt"') known_solution = array([2.0] * 149 + [3.0]) sol_tolerance = 1e-2 # aouch, not great! self.assertLess( norm(x - known_solution), sol_tolerance, 'solver="kvxopt"' ) self.assertLess(max(G.dot(x) - h), 1e-10, 'solver="kvxopt"') def test_extra_kwargs(self): """Call KVXOPT with various solver-specific settings.""" problem = get_sd3310_problem() x = kvxopt_solve_qp( problem.P, problem.q, problem.G, problem.h, problem.A, problem.b, maxiters=10, abstol=1e-1, reltol=1e-1, feastol=1e-2, refinement=3, ) self.assertIsNotNone(x, 'solver="kvxopt"') def test_infinite_linear_bounds(self): """KVXOPT does not yield a domain error on infinite bounds.""" problem, _ = get_qpsut01() problem.h[1] = +np.inf x = kvxopt_solve_problem(problem) self.assertIsNotNone(x, 'solver="kvxopt"') def test_infinite_box_bounds(self): """KVXOPT does not yield a domain error infinite box bounds.""" problem, _ = get_qpsut01() problem.lb[1] = -np.inf problem.ub[1] = +np.inf x = kvxopt_solve_problem(problem) self.assertIsNotNone(x, 'solver="kvxopt"') except ImportError as exn: # in case the solver is not installed warnings.warn(f"Skipping KVXOPT tests: {exn}") ================================================ FILE: tests/test_mosek.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for MOSEK.""" import unittest import warnings from .problems import get_sd3310_problem try: from qpsolvers.solvers.mosek_ import mosek_solve_qp class TestMOSEK(unittest.TestCase): """Tests specific to MOSEK.""" def test_problem(self): problem = get_sd3310_problem() P, q, G, h, A, b, lb, ub = problem.unpack() self.assertIsNotNone(mosek_solve_qp(P, q, G, h, A, b, lb, ub)) except ImportError as exn: # solver not installed warnings.warn(f"Skipping MOSEK tests: {exn}") ================================================ FILE: tests/test_nppro.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors import unittest import warnings from .problems import get_sd3310_problem try: from qpsolvers.solvers.nppro_ import nppro_solve_qp class TestNPPro(unittest.TestCase): """Tests specific to NPPro.""" def test_problem(self): problem = get_sd3310_problem() P, q, G, h, A, b, lb, ub = problem.unpack() self.assertIsNotNone(nppro_solve_qp(P, q, G, h, A, b, lb, ub)) except ImportError as exn: # solver not installed warnings.warn(f"Skipping NPPro tests: {exn}") ================================================ FILE: tests/test_osqp.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for OSQP.""" import unittest import warnings from .problems import get_sd3310_problem try: from qpsolvers.solvers.osqp_ import osqp_solve_qp class TestOSQP(unittest.TestCase): """Tests specific to OSQP.""" def test_problem(self): problem = get_sd3310_problem() P, q, G, h, A, b, lb, ub = problem.unpack() self.assertIsNotNone(osqp_solve_qp(P, q, G, h, A, b, lb, ub)) except ImportError as exn: # solver not installed warnings.warn(f"Skipping OSQP tests: {exn}") ================================================ FILE: tests/test_piqp.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for PIQP.""" import unittest import warnings from qpsolvers.exceptions import ParamError, ProblemError from .problems import get_sd3310_problem try: from qpsolvers.solvers.piqp_ import piqp_solve_qp class TestPIQP(unittest.TestCase): """Test fixture specific to the PIQP solver.""" def test_dense_backend(self): """Try the dense backend.""" problem = get_sd3310_problem() sol = piqp_solve_qp( problem.P, problem.q, problem.G, problem.h, problem.A, problem.b, backend="dense", ) self.assertIsNotNone(sol) def test_sparse_backend(self): """Try the sparse backend.""" problem = get_sd3310_problem() sol = piqp_solve_qp( problem.P, problem.q, problem.G, problem.h, problem.A, problem.b, backend="sparse", ) self.assertIsNotNone(sol) def test_invalid_backend(self): """Exception raised when asking for an invalid backend.""" problem = get_sd3310_problem() with self.assertRaises(ParamError): piqp_solve_qp( problem.P, problem.q, problem.G, problem.h, problem.A, problem.b, backend="invalid", ) def test_invalid_problems(self): """Exception raised when asking for an invalid backend.""" problem = get_sd3310_problem() with self.assertRaises(ProblemError): piqp_solve_qp( problem.P, problem.q, None, problem.h, problem.A, problem.b, backend="sparse", ) with self.assertRaises(ProblemError): piqp_solve_qp( problem.P, problem.q, problem.G, None, problem.A, problem.b, backend="sparse", ) with self.assertRaises(ProblemError): piqp_solve_qp( problem.P, problem.q, problem.G, problem.h, None, problem.b, backend="sparse", ) with self.assertRaises(ProblemError): piqp_solve_qp( problem.P, problem.q, problem.G, problem.h, problem.A, None, backend="sparse", ) except ImportError as exn: # solver not installed warnings.warn(f"Skipping PIQP tests: {exn}") ================================================ FILE: tests/test_problem.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for Problem class.""" import tempfile import unittest import numpy as np import scipy.sparse as spa from qpsolvers import ActiveSet, Problem, ProblemError from .problems import get_sd3310_problem class TestProblem(unittest.TestCase): """Test fixture for problems.""" def setUp(self): self.problem = get_sd3310_problem() def test_unpack(self): P, q, G, h, A, b, lb, ub = self.problem.unpack() self.assertEqual(P.shape, self.problem.P.shape) self.assertEqual(q.shape, self.problem.q.shape) self.assertEqual(G.shape, self.problem.G.shape) self.assertEqual(h.shape, self.problem.h.shape) self.assertEqual(A.shape, self.problem.A.shape) self.assertEqual(b.shape, self.problem.b.shape) self.assertIsNone(lb) self.assertIsNone(ub) def test_check_inequality_constraints(self): problem = get_sd3310_problem() P, q, G, h, A, b, _, _ = problem.unpack() with self.assertRaises(ProblemError): Problem(P, q, G, None, A, b).check_constraints() with self.assertRaises(ProblemError): Problem(P, q, None, h, A, b).check_constraints() def test_check_equality_constraints(self): problem = get_sd3310_problem() P, q, G, h, A, b, _, _ = problem.unpack() with self.assertRaises(ProblemError): Problem(P, q, G, h, A, None).check_constraints() with self.assertRaises(ProblemError): Problem(P, q, G, h, None, b).check_constraints() def test_cond(self): active_set = ActiveSet( G_indices=range(self.problem.G.shape[0]), lb_indices=[], ub_indices=[], ) self.assertGreater(self.problem.cond(active_set), 200.0) def test_cond_unconstrained(self): unconstrained = Problem(self.problem.P, self.problem.q) active_set = ActiveSet() self.assertAlmostEqual( unconstrained.cond(active_set), 124.257, places=4 ) def test_cond_no_equality(self): no_equality = Problem( self.problem.P, self.problem.q, self.problem.G, self.problem.h ) active_set = ActiveSet(G_indices=range(self.problem.G.shape[0])) self.assertGreater(no_equality.cond(active_set), 200.0) def test_cond_sparse(self): sparse = Problem(spa.csc_matrix(self.problem.P), self.problem.q) active_set = ActiveSet() with self.assertRaises(ProblemError): sparse.cond(active_set) def test_check_matrix_shapes(self): Problem(np.eye(1), np.ones(1)) Problem(np.array([1.0]), np.ones(1)) def test_check_vector_shapes(self): Problem(np.eye(3), np.ones(shape=(3, 1))) Problem(np.eye(3), np.ones(shape=(1, 3))) Problem(np.eye(3), np.ones(shape=(3,))) with self.assertRaises(ProblemError): Problem(np.eye(3), np.ones(shape=(3, 2))) with self.assertRaises(ProblemError): Problem(np.eye(3), np.ones(shape=(3, 1, 1))) with self.assertRaises(ProblemError): Problem(np.eye(3), np.ones(shape=(1, 3, 1))) def test_save_load(self): problem = Problem(np.eye(3), np.ones(shape=(3, 1))) save_path = tempfile.mktemp() + ".npz" problem.save(save_path) reloaded = Problem.load(save_path) self.assertTrue(np.allclose(reloaded.P, problem.P)) self.assertTrue(np.allclose(reloaded.q, problem.q)) self.assertIsNone(reloaded.G) self.assertIsNone(reloaded.h) self.assertIsNone(reloaded.A) self.assertIsNone(reloaded.b) self.assertIsNone(reloaded.lb) self.assertIsNone(reloaded.ub) ================================================ FILE: tests/test_proxqp.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for ProxQP.""" import unittest import warnings from qpsolvers.exceptions import ParamError, ProblemError from .problems import get_sd3310_problem try: from qpsolvers.solvers.proxqp_ import proxqp_solve_qp class TestProxQP(unittest.TestCase): """Test fixture specific to the ProxQP solver.""" def test_dense_backend(self): """Try the dense backend.""" problem = get_sd3310_problem() proxqp_solve_qp( problem.P, problem.q, problem.G, problem.h, problem.A, problem.b, backend="dense", ) def test_sparse_backend(self): """Try the sparse backend.""" problem = get_sd3310_problem() proxqp_solve_qp( problem.P, problem.q, problem.G, problem.h, problem.A, problem.b, backend="sparse", ) def test_invalid_backend(self): """Exception raised when asking for an invalid backend.""" problem = get_sd3310_problem() with self.assertRaises(ParamError): proxqp_solve_qp( problem.P, problem.q, problem.G, problem.h, problem.A, problem.b, backend="invalid", ) def test_double_warm_start(self): """Exception when two warm-start values are provided.""" problem = get_sd3310_problem() with self.assertRaises(ParamError): proxqp_solve_qp( problem.P, problem.q, problem.G, problem.h, problem.A, problem.b, initvals=problem.q, x=problem.q, ) def test_invalid_inequalities(self): """Check for inconsistent parameters. Raise an exception in an implementation-dependent inconsistent set of parameters. This may happen when :func:`proxqp_solve_qp` it is called directly. """ problem = get_sd3310_problem() with self.assertRaises(ProblemError): proxqp_solve_qp( problem.P, problem.q, G=problem.G, h=None, lb=problem.q, ) except ImportError as exn: # solver not installed warnings.warn(f"Skipping ProxQP tests: {exn}") ================================================ FILE: tests/test_pyqpmad.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2024 Stéphane Caron and the qpsolvers contributors """Unit tests for pyqpmad.""" import unittest import warnings import numpy as np import scipy.sparse as spa from qpsolvers.exceptions import ProblemError from .problems import get_sd3310_problem try: from qpsolvers.solvers.pyqpmad_ import pyqpmad_solve_qp class TestPyqpmad(unittest.TestCase): """Test fixture for the pyqpmad solver.""" def setUp(self): """Prepare test fixture.""" warnings.simplefilter("ignore", category=UserWarning) def test_not_sparse(self): """Raise a ProblemError on sparse problems.""" problem = get_sd3310_problem() P, q, G, h, A, b, _, _ = problem.unpack() P = spa.csc_matrix(P) with self.assertRaises(ProblemError): pyqpmad_solve_qp(P, q, G, h, A, b) def test_box_constraints(self): """Test solving a problem with only box constraints.""" P = np.array([[1.0, 0.0], [0.0, 1.0]]) q = np.array([-1.0, -1.0]) lb = np.array([0.0, 0.0]) ub = np.array([0.5, 0.5]) x = pyqpmad_solve_qp(P, q, lb=lb, ub=ub) self.assertIsNotNone(x) self.assertTrue(np.allclose(x, [0.5, 0.5])) except ImportError as exn: # solver not installed warnings.warn(f"Skipping pyqpmad tests: {exn}") ================================================ FILE: tests/test_qpax.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2024 Lev Kozlov """Unit tests for qpax.""" import unittest import warnings from .problems import get_sd3310_problem try: from qpsolvers.solvers.qpax_ import qpax_solve_qp class TestQpax(unittest.TestCase): """Tests specific to qpax.""" def test_problem(self): problem = get_sd3310_problem() P, q, G, h, A, b, lb, ub = problem.unpack() self.assertIsNotNone(qpax_solve_qp(P, q, G, h, A, b, lb, ub)) except ImportError as exn: # solver not installed warnings.warn(f"Skipping qpax tests: {exn}") ================================================ FILE: tests/test_qpoases.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for qpOASES.""" import unittest import warnings import numpy as np import scipy.sparse as spa from qpsolvers import ProblemError from .problems import get_sd3310_problem try: from qpsolvers.solvers.qpoases_ import qpoases_solve_qp class TestQpOASES(unittest.TestCase): """Test fixture specific to the qpOASES solver.""" def test_initvals(self): """Call the solver with a warm-start guess.""" problem = get_sd3310_problem() qpoases_solve_qp( problem.P, problem.q, problem.G, problem.h, problem.A, problem.b, initvals=problem.q, ) def test_params(self): """Call the solver with a time limit and other parameters.""" problem = get_sd3310_problem() qpoases_solve_qp( problem.P, problem.q, problem.G, problem.h, problem.A, problem.b, time_limit=0.1, terminationTolerance=1e-7, ) qpoases_solve_qp( problem.P, problem.q, time_limit=0.1, terminationTolerance=1e-7, ) def test_unfeasible(self): problem = get_sd3310_problem() P, q, G, h, A, b, _, _ = problem.unpack() custom_lb = np.ones(q.shape) custom_ub = -np.ones(q.shape) x = qpoases_solve_qp( problem.P, problem.q, problem.G, problem.h, problem.A, problem.b, custom_lb, custom_ub, ) self.assertIsNone(x) def test_not_sparse(self): """Raise a ProblemError on sparse problems.""" problem = get_sd3310_problem() problem.P = spa.csc_matrix(problem.P) with self.assertRaises(ProblemError): qpoases_solve_qp( problem.P, problem.q, problem.G, problem.h, ) except ImportError as exn: # solver not installed warnings.warn(f"Skipping qpOASES tests: {exn}") ================================================ FILE: tests/test_qpswift.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for qpSWIFT.""" import unittest import warnings import scipy.sparse as spa from qpsolvers import ProblemError from .problems import get_sd3310_problem try: from qpsolvers.solvers.qpswift_ import qpswift_solve_qp class TestQpSwift(unittest.TestCase): """Tests specific to qpSWIFT.""" def test_problem(self): problem = get_sd3310_problem() P, q, G, h, A, b, lb, ub = problem.unpack() self.assertIsNotNone(qpswift_solve_qp(P, q, G, h, A, b, lb, ub)) def test_not_sparse(self): problem = get_sd3310_problem() P, q, G, h, A, b, lb, ub = problem.unpack() P = spa.csc_matrix(P) with self.assertRaises(ProblemError): qpswift_solve_qp(P, q, G, h, A, b, lb, ub) except ImportError as exn: # solver not installed warnings.warn(f"Skipping qpSWIFT tests: {exn}") ================================================ FILE: tests/test_quadprog.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for quadprog.""" import unittest import warnings import numpy as np import scipy.sparse as spa from qpsolvers.exceptions import ProblemError from .problems import get_sd3310_problem try: from qpsolvers.solvers.quadprog_ import quadprog_solve_qp class TestQuadprog(unittest.TestCase): """Test fixture for the quadprog solver.""" def setUp(self): """Prepare test fixture.""" warnings.simplefilter("ignore", category=UserWarning) def test_non_psd_cost(self): problem = get_sd3310_problem() P, q, G, h, A, b, _, _ = problem.unpack() P -= np.eye(3) with self.assertRaises(ProblemError): quadprog_solve_qp(P, q, G, h, A, b) def test_quadprog_value_error(self): problem = get_sd3310_problem() P, q, G, h, A, b, _, _ = problem.unpack() q = q[1:] # raise "G and a must have the same dimension" self.assertIsNone(quadprog_solve_qp(P, q, G, h, A, b)) def test_not_sparse(self): """Raise a ProblemError on sparse problems.""" problem = get_sd3310_problem() P, q, G, h, A, b, _, _ = problem.unpack() P = spa.csc_matrix(P) with self.assertRaises(ProblemError): quadprog_solve_qp(P, q, G, h, A, b) except ImportError as exn: # solver not installed warnings.warn(f"Skipping quadprog tests: {exn}") ================================================ FILE: tests/test_scs.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for SCS.""" import unittest import warnings import numpy as np from qpsolvers import ProblemError from .problems import get_sd3310_problem try: from qpsolvers.solvers.scs_ import scs_solve_qp class TestSCS(unittest.TestCase): """Tests specific to SCS.""" def test_problem(self): problem = get_sd3310_problem() P, q, G, h, A, b, lb, ub = problem.unpack() self.assertIsNotNone(scs_solve_qp(P, q, G, h, A, b, lb, ub)) def test_unbounded_below(self): problem = get_sd3310_problem() P, q, _, _, _, _, _, _ = problem.unpack() P -= np.eye(3) # make problem unbounded with self.assertRaises(ProblemError): scs_solve_qp(P, q) except ImportError as exn: # solver not installed warnings.warn(f"Skipping SCS tests: {exn}") ================================================ FILE: tests/test_sip.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for SIP.""" import unittest import warnings import numpy as np from qpsolvers import ProblemError from .problems import get_sd3310_problem try: from qpsolvers.solvers.sip_ import sip_solve_qp class TestSIP(unittest.TestCase): """Tests specific to SIP.""" def test_problem(self): problem = get_sd3310_problem() P, q, G, h, A, b, lb, ub = problem.unpack() self.assertIsNotNone(sip_solve_qp(P, q, G, h, A, b, lb, ub)) except ImportError as exn: # solver not installed warnings.warn(f"Skipping SIP tests: {exn}") ================================================ FILE: tests/test_solution.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for Solution class.""" import unittest import numpy as np from qpsolvers import Solution, solve_problem from .problems import get_sd3310_problem class TestSolution(unittest.TestCase): """Test fixture for solutions.""" def test_found_default(self): solution = Solution(get_sd3310_problem()) self.assertFalse(solution.found) def test_residuals(self): """Test residuals at the solution of the SD3310 problem. Note ---- This function uses DAQP to find a solution. """ problem = get_sd3310_problem() solution = solve_problem(problem, solver="daqp") eps_abs = 1e-10 self.assertLess(solution.primal_residual(), eps_abs) self.assertLess(solution.dual_residual(), eps_abs) self.assertLess(solution.duality_gap(), eps_abs) self.assertTrue(solution.is_optimal(eps_abs)) def test_undefined_optimality(self): solution = Solution(get_sd3310_problem()) # solution is fully undefined self.assertEqual(solution.primal_residual(), np.inf) self.assertEqual(solution.dual_residual(), np.inf) self.assertEqual(solution.duality_gap(), np.inf) # solution was not found solution.found = False self.assertEqual(solution.primal_residual(), np.inf) self.assertEqual(solution.dual_residual(), np.inf) self.assertEqual(solution.duality_gap(), np.inf) solution.found = True solution.x = np.array([1.0, 2.0, 3.0]) self.assertNotEqual(solution.primal_residual(), np.inf) self.assertEqual(solution.dual_residual(), np.inf) self.assertEqual(solution.duality_gap(), np.inf) solution.z = np.array([-1.0, -2.0, -3.0]) self.assertEqual(solution.dual_residual(), np.inf) self.assertEqual(solution.duality_gap(), np.inf) solution.y = np.array([0.0]) self.assertNotEqual(solution.dual_residual(), np.inf) self.assertNotEqual(solution.duality_gap(), np.inf) # solution is now fully defined self.assertGreater(solution.primal_residual(), 1.0) self.assertGreater(solution.dual_residual(), 10.0) self.assertGreater(solution.duality_gap(), 10.0) ================================================ FILE: tests/test_solve_ls.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for the `solve_ls` function.""" import unittest import warnings import numpy as np import scipy.sparse as spa from numpy.linalg import norm from qpsolvers import available_solvers, solve_ls, sparse_solvers from qpsolvers.exceptions import NoSolverSelected, SolverNotFound from qpsolvers.problems import get_sparse_least_squares class TestSolveLS(unittest.TestCase): def setUp(self): """Prepare test fixture.""" warnings.simplefilter("ignore", category=DeprecationWarning) warnings.simplefilter("ignore", category=UserWarning) def get_problem_and_solution(self): """Get least-squares problem and its primal solution. Returns ------- R : Least-squares matrix. s : Least-squares vector. G : Linear inequality matrix. h : Linear inequality vector. A : Linear equality matrix. b : Linear equality vector. solution : Known solution. """ R = np.array([[1.0, 2.0, 0.0], [2.0, 3.0, 4.0], [0.0, 4.0, 1.0]]) s = np.array([3.0, 2.0, 3.0]) G = np.array([[1.0, 2.0, 1.0], [2.0, 0.0, 1.0], [-1.0, 2.0, -1.0]]) h = np.array([3.0, 2.0, -2.0]).reshape((3,)) A = np.array([1.0, 1.0, 1.0]) b = np.array([1.0]) solution = np.array([2.0 / 3, -1.0 / 3, 2.0 / 3]) return R, s, G, h, A, b, solution @staticmethod def get_test(solver: str): """Get test function for a given solver. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): R, s, G, h, A, b, solution = self.get_problem_and_solution() x = solve_ls( R, s, G, h, A, b, solver=solver, sparse_conversion=False ) x_sp = solve_ls( R, s, G, h, A, b, solver=solver, sparse_conversion=False ) self.assertIsNotNone(x, f"{solver=}") self.assertIsNotNone(x_sp, f"{solver=}") sol_tolerance = ( 5e-3 if solver in ["jaxopt_osqp", "osqp"] else ( 2e-5 if solver == "proxqp" else ( 1e-5 if solver in ["ecos", "qpalm", "qpax", "sip"] else 1e-6 ) ) ) eq_tolerance = ( 1e-4 if solver == "jaxopt_osqp" else ( 2e-6 if solver in ["qpalm", "qpax"] else 1e-7 if solver in ["osqp", "qtqp", "sip"] else 1e-9 ) ) ineq_tolerance = ( 1e-3 if solver == "osqp" else ( 1e-5 if solver == "proxqp" else ( 2e-7 if solver in ["scs", "qpax"] else 1e-8 if solver == "qtqp" else 1e-9 ) ) ) self.assertLess(norm(x - solution), sol_tolerance, f"{solver=}") self.assertLess(norm(x_sp - solution), sol_tolerance, f"{solver=}") self.assertLess(max(G.dot(x) - h), ineq_tolerance, f"{solver=}") self.assertLess(max(A.dot(x) - b), eq_tolerance, f"{solver=}") self.assertLess(min(A.dot(x) - b), eq_tolerance, f"{solver=}") return test def test_no_solver_selected(self): """Check that NoSolverSelected is raised when applicable.""" R, s, G, h, A, b, _ = self.get_problem_and_solution() with self.assertRaises(NoSolverSelected): solve_ls(R, s, G, h, A, b, solver=None) def test_solver_not_found(self): """SolverNotFound is raised when the solver does not exist.""" R, s, G, h, A, b, _ = self.get_problem_and_solution() with self.assertRaises(SolverNotFound): solve_ls(R, s, G, h, A, b, solver="ideal") @staticmethod def get_test_mixed_sparse_args(solver: str): """Get test function for mixed sparse problems with a given solver. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): _, s, G, h, A, b, _ = self.get_problem_and_solution() n = len(s) R_csc = spa.eye(n, format="csc") x_csc = solve_ls( R_csc, s, G, h, A, b, solver=solver, sparse_conversion=False ) self.assertIsNotNone(x_csc, f"{solver=}") R_dia = spa.eye(n) x_dia = solve_ls( R_dia, s, G, h, A, b, solver=solver, sparse_conversion=False ) self.assertIsNotNone(x_dia, f"{solver=}") x_np_dia = solve_ls( R_dia, s, G, h, A, b, W=np.eye(n), solver=solver, sparse_conversion=False, ) self.assertIsNotNone(x_np_dia, f"{solver=}") sol_tolerance = 1e-8 self.assertLess(norm(x_csc - x_dia), sol_tolerance, f"{solver=}") self.assertLess( norm(x_csc - x_np_dia), sol_tolerance, f"{solver=}" ) return test @staticmethod def get_test_medium_sparse(solver: str, sparse_conversion: bool, **kwargs): """Get test function for a large sparse problem with a given solver. Parameters ---------- solver : Name of the solver to test. sparse_conversion : Conversion strategy boolean. Returns ------- : Test function for that solver. """ def test(self): R, s, G, h, A, b, _, _ = get_sparse_least_squares(n=1500) x = solve_ls( R, s, G, h, A, b, solver=solver, sparse_conversion=sparse_conversion, **kwargs, ) self.assertIsNotNone(x, f"{solver=}") return test @staticmethod def get_test_large_sparse( solver: str, sparse_conversion: bool, obj_scaling: float = 1.0, **kwargs, ): """Get test function for a large sparse problem with a given solver. Parameters ---------- solver : Name of the solver to test. sparse_conversion : Whether to perform sparse or dense LS-to-QP conversion. obj_scaling: Scale objective matrices by this factor. Suitable values can help solvers that don't compute a preconditioner internally. Returns ------- : Test function for that solver. """ def test(self): R, s, G, h, A, b, _, _ = get_sparse_least_squares(n=15_000) x = solve_ls( obj_scaling * R, obj_scaling * s, G, h, A, b, solver=solver, sparse_conversion=sparse_conversion, **kwargs, ) self.assertIsNotNone(x, f"{solver=}") return test # Generate test fixtures for each solver for solver in available_solvers: setattr( TestSolveLS, "test_{}".format(solver), TestSolveLS.get_test(solver) ) for solver in sparse_solvers: setattr( TestSolveLS, "test_mixed_sparse_args_{}".format(solver), TestSolveLS.get_test_mixed_sparse_args(solver), ) for solver in sparse_solvers: # loop complexity warning ;p if solver not in ["gurobi", "qtqp"]: # Gurobi: model too large for size-limited license # QTQP: slow convergence on medium/large problems (pure Python) kwargs = {} if solver == "mosek": try: import mosek kwargs["mosek"] = {mosek.dparam.intpnt_qo_tol_rel_gap: 1e-6} except ImportError: pass if solver != "copt": # COPT: Size limitation for non-commercial use setattr( TestSolveLS, "test_medium_sparse_dense_conversion_{}".format(solver), TestSolveLS.get_test_medium_sparse( solver, sparse_conversion=False, **kwargs ), ) for solver in sparse_solvers: # loop complexity warning ;p if solver not in ["copt", "cvxopt", "kvxopt", "gurobi", "qtqp"]: # COPT: Size limitation for non-commercial use # CVXOPT and KVXOPT: sparse conversion breaks rank assumption # Gurobi: model too large for size-limited license # QTQP: slow convergence on medium/large problems (pure Python) setattr( TestSolveLS, "test_medium_sparse_sparse_conversion_{}".format(solver), TestSolveLS.get_test_medium_sparse(solver, sparse_conversion=True), ) for solver in sparse_solvers: # loop complexity warning ;p if solver not in ["gurobi", "highs", "qtqp"]: # Gurobi: model too large for size-limited license # HiGHS: model too large https://github.com/ERGO-Code/HiGHS/issues/992 # QTQP: slow convergence on large problems (pure Python implementation) kwargs = {"eps_infeas": 1e-12} if solver == "scs" else {} if solver == "mosek": try: import mosek kwargs["mosek"] = {mosek.dparam.intpnt_qo_tol_rel_gap: 1e-7} except ImportError: pass if solver != "copt": # COPT: Size limitation for non-commercial use setattr( TestSolveLS, "test_large_sparse_problem_dense_conversion_{}".format(solver), TestSolveLS.get_test_large_sparse( solver, sparse_conversion=False, obj_scaling=1e-3 if solver == "mosek" else 1.0, **kwargs, ), ) if solver not in ["copt", "cvxopt", "kvxopt"]: # COPT: Size limitation for non-commercial use # CVXOPT and KVXOPT: sparse conversion breaks rank assumption setattr( TestSolveLS, "test_large_sparse_problem_sparse_conversion_{}".format( solver ), TestSolveLS.get_test_large_sparse( solver, sparse_conversion=True, **kwargs ), ) ================================================ FILE: tests/test_solve_problem.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2023 Inria """Unit tests for the `solve_problem` function.""" import unittest import numpy as np from numpy.linalg import norm from qpsolvers import available_solvers, solve_problem from qpsolvers.problems import ( get_qpgurabs, get_qpgurdu, get_qpgureq, get_qpsut01, get_qpsut02, get_qpsut03, get_qpsut04, get_qpsut05, get_qptest, ) class TestSolveProblem(unittest.TestCase): """Test fixture for primal and dual solutions of a variety of problems. Notes ----- Solver-specific tests are implemented in static methods called ``get_test_{foo}`` that return the test function for a given solver. The corresponding test function ``test_{foo}_{solver}`` is then added to the fixture below the class definition. """ @staticmethod def get_test_qpsut01(solver: str): """Get test function for a given solver. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): problem, ref_solution = get_qpsut01() solution = solve_problem(problem, solver=solver) eps_abs = ( 5e-1 if solver in ["jaxopt_osqp", "osqp", "qpalm"] else ( 5e-3 if solver == "proxqp" else ( 1e-4 if solver == "ecos" else ( 5e-5 if solver in ["mosek", "qpax", "sip"] else ( 5e-6 if solver == "qtqp" else ( 1e-6 if solver in ["copt", "cvxopt", "kvxopt", "qpswift", "scs"] else 5e-7 if solver in ["gurobi"] else 1e-7 ) ) ) ) ) ) self.assertLess( norm(solution.x - ref_solution.x), eps_abs, f"{solver=}" ) # NB: in general the dual solution is not unique (that's why the # other tests check residuals). This test only works because the # dual solution is unique in this particular problem. self.assertLess( norm(solution.y - ref_solution.y), eps_abs, f"{solver=}" ) self.assertLess( norm(solution.z - ref_solution.z), eps_abs, f"{solver=}" ) self.assertLess( norm(solution.z_box - ref_solution.z_box), eps_abs, f"{solver=}", ) return test @staticmethod def get_test_qpsut02(solver: str): """Get test function for a given solver. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): problem, ref_solution = get_qpsut02() solution = solve_problem(problem, solver=solver) eps_abs = ( 5e-2 if solver in ["ecos", "jaxopt_osqp", "qpalm"] else ( 5e-4 if solver in ["proxqp", "scs", "qpax"] else ( 1e-4 if solver in ["cvxopt", "kvxopt", "qpax", "qtqp"] else ( 1e-5 if solver in ["copt", "highs", "osqp"] else ( 5e-7 if solver in [ "clarabel", "mosek", "qpswift", "piqp", "sip", ] else 1e-7 if solver in ["gurobi"] else 1e-8 ) ) ) ) ) self.assertLess( norm(solution.x - ref_solution.x), eps_abs, f"{solver=}" ) self.assertLess(solution.primal_residual(), eps_abs, f"{solver=}") self.assertLess(solution.dual_residual(), eps_abs, f"{solver=}") self.assertLess(solution.duality_gap(), eps_abs, f"{solver=}") return test @staticmethod def get_test_qpsut03(solver: str): """Get test function for a given solver. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): problem, ref_solution = get_qpsut03() solution = solve_problem(problem, solver=solver) self.assertEqual(solution.x.shape, (4,), f"{solver=}") self.assertEqual(solution.y.shape, (0,), f"{solver=}") self.assertEqual(solution.z.shape, (0,), f"{solver=}") self.assertEqual(solution.z_box.shape, (4,), f"{solver=}") tolerance = ( 1e-1 if solver in ["osqp", "qtqp"] else 1e-2 if solver == "scs" else 1e-3 ) self.assertTrue(solution.is_optimal(tolerance), f"{solver=}") return test @staticmethod def get_test_qpsut04(solver: str): """ Get test function for the QPSUT04 problem. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): problem, ref_solution = get_qpsut04() solution = solve_problem(problem, solver=solver) eps_abs = ( 2e-4 if solver in ["jaxopt_osqp", "osqp", "qpalm", "qpax", "sip"] else 1e-6 ) self.assertLess( norm(solution.x - ref_solution.x), eps_abs, f"{solver=}" ) self.assertLess( norm(solution.z - ref_solution.z), eps_abs, f"{solver=}" ) self.assertTrue(np.isfinite(solution.duality_gap()), f"{solver=}") return test @staticmethod def get_test_qpsut05(solver: str): """ Get test function for the QPSUT04 problem. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): problem, ref_solution = get_qpsut05() solution = solve_problem(problem, solver=solver) eps_abs = 2e-5 if solver == "ecos" else 1e-6 self.assertLess( norm(solution.x - ref_solution.x), eps_abs, f"{solver=}" ) self.assertTrue(np.isfinite(solution.duality_gap()), f"{solver=}") return test @staticmethod def get_test_qptest(solver: str): """Get test function for the QPTEST problem. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. Note ---- ECOS fails to solve this problem. """ def test(self): problem, solution = get_qptest() result = solve_problem(problem, solver=solver) tolerance = ( 1e1 if solver == "gurobi" else ( 1.0 if solver == "proxqp" else ( 2e-3 if solver == "osqp" else ( 5e-5 if solver in ["qpalm", "scs", "qpax"] else ( 1e-6 if solver == "mosek" else ( 1e-7 if solver == "highs" else ( 5e-7 if solver == "cvxopt" or solver == "kvxopt" else ( 5e-8 if solver == "clarabel" else 1e-8 ) ) ) ) ) ) ) ) self.assertIsNotNone(result.x, f"{solver=}") self.assertIsNotNone(result.z, f"{solver=}") self.assertIsNotNone(result.z_box, f"{solver=}") self.assertTrue(solution.is_optimal(tolerance), f"{solver=}") return test @staticmethod def get_test_infinite_box_bounds(solver: str): """Problem with some infinite box bounds. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): problem, _ = get_qpsut01() problem.lb[1] = -np.inf problem.ub[1] = +np.inf result = solve_problem(problem, solver=solver) self.assertIsNotNone(result.x, f"{solver=}") self.assertIsNotNone(result.z, f"{solver=}") self.assertIsNotNone(result.z_box, f"{solver=}") return test @staticmethod def get_test_infinite_linear_bounds(solver: str): """Problem with some infinite linear bounds. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): problem, _ = get_qpsut01() problem.h[0] = +np.inf result = solve_problem(problem, solver=solver) self.assertIsNotNone(result.x, f"{solver=}") self.assertIsNotNone(result.z, f"{solver=}") self.assertIsNotNone(result.z_box, f"{solver=}") return test @staticmethod def get_test_qpgurdu(solver: str): """Get test function for a given solver. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): problem, _ = get_qpgurdu() result = solve_problem(problem, solver) self.assertIsNotNone(result.x, f"{solver=}") self.assertIsNotNone(result.z, f"{solver=}") eps_abs = ( 6e-3 if solver in ("jaxopt_osqp", "osqp", "qpax") else ( 1e-3 if solver in ("scs", "sip") else ( 1e-4 if solver in ("ecos", "highs", "proxqp") else 1e-5 ) ) ) self.assertLess(result.primal_residual(), eps_abs, f"{solver=}") self.assertLess(result.dual_residual(), eps_abs, f"{solver=}") self.assertLess(result.duality_gap(), eps_abs, f"{solver=}") return test @staticmethod def get_test_qpgurabs(solver: str): """Get test function for a given solver. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): problem, _ = get_qpgurabs() result = solve_problem(problem, solver) self.assertIsNotNone(result.x, f"{solver=}") self.assertIsNotNone(result.z, f"{solver=}") eps_abs = ( 0.2 if solver == "osqp" else 3e-3 if solver in ["jaxopt_osqp", "proxqp"] else 1e-4 ) self.assertLess(result.primal_residual(), eps_abs, f"{solver=}") self.assertLess(result.dual_residual(), eps_abs, f"{solver=}") self.assertLess(result.duality_gap(), eps_abs, f"{solver=}") return test @staticmethod def get_test_qpgureq(solver: str): """Get test function for a given solver. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): if solver == "ecos": return problem, _ = get_qpgureq() result = solve_problem(problem, solver) self.assertIsNotNone(result.x, f"{solver=}") self.assertIsNotNone(result.z, f"{solver=}") eps_abs = ( 0.01 if solver in ["osqp", "qpax"] else 5e-3 if solver in ["jaxopt_osqp", "proxqp"] else 1e-4 ) self.assertLess(result.primal_residual(), eps_abs, f"{solver=}") self.assertLess(result.dual_residual(), eps_abs, f"{solver=}") self.assertLess(result.duality_gap(), eps_abs, f"{solver=}") return test # Generate test fixtures for each solver for solver in available_solvers: setattr( TestSolveProblem, f"test_qpsut01_{solver}", TestSolveProblem.get_test_qpsut01(solver), ) setattr( TestSolveProblem, f"test_qpsut02_{solver}", TestSolveProblem.get_test_qpsut02(solver), ) if solver not in ["ecos", "mosek", "qpswift", "qtqp"]: # ECOS: https://github.com/embotech/ecos-python/issues/49 # MOSEK: https://github.com/qpsolvers/qpsolvers/issues/229 # qpSWIFT: https://github.com/qpsolvers/qpsolvers/issues/159 # qpax: https://github.com/kevin-tracy/qpax/issues/5 # qtqp: does not handle problems without inequalities setattr( TestSolveProblem, f"test_qpsut03_{solver}", TestSolveProblem.get_test_qpsut03(solver), ) setattr( TestSolveProblem, f"test_qpsut04_{solver}", TestSolveProblem.get_test_qpsut04(solver), ) if solver not in ["osqp", "qpswift"]: # OSQP: see https://github.com/osqp/osqp-python/issues/104 setattr( TestSolveProblem, f"test_qpsut05_{solver}", TestSolveProblem.get_test_qpsut05(solver), ) if solver not in ["ecos", "qpswift"]: # ECOS: https://github.com/qpsolvers/qpsolvers/issues/160 # qpSWIFT: https://github.com/qpsolvers/qpsolvers/issues/159 # qpax: https://github.com/kevin-tracy/qpax/issues/4 setattr( TestSolveProblem, f"test_qptest_{solver}", TestSolveProblem.get_test_qptest(solver), ) if solver not in ["ecos", "qpswift"]: # See https://github.com/qpsolvers/qpsolvers/issues/159 # See https://github.com/qpsolvers/qpsolvers/issues/160 setattr( TestSolveProblem, f"test_infinite_box_bounds_{solver}", TestSolveProblem.get_test_infinite_box_bounds(solver), ) if solver not in ["ecos", "qpswift", "scs"]: # See https://github.com/qpsolvers/qpsolvers/issues/159 # See https://github.com/qpsolvers/qpsolvers/issues/160 # See https://github.com/qpsolvers/qpsolvers/issues/161 setattr( TestSolveProblem, f"test_infinite_linear_bounds_{solver}", TestSolveProblem.get_test_infinite_linear_bounds(solver), ) setattr( TestSolveProblem, f"test_qpgurdu_{solver}", TestSolveProblem.get_test_qpgurdu(solver), ) setattr( TestSolveProblem, f"test_qpgurabs_{solver}", TestSolveProblem.get_test_qpgurabs(solver), ) if solver != "qtqp": # QTQP requires at least one inequality constraint setattr( TestSolveProblem, f"test_qpgureq_{solver}", TestSolveProblem.get_test_qpgureq(solver), ) ================================================ FILE: tests/test_solve_qp.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for the `solve_qp` function.""" import unittest import warnings import numpy as np import scipy from numpy import array, dot, ones, random from numpy.linalg import norm from scipy.sparse import csc_matrix from qpsolvers import ( NoSolverSelected, ProblemError, SolverNotFound, available_solvers, solve_qp, sparse_solvers, ) from .problems import get_qpmad_demo_problem # Raising a ValueError when the problem is unbounded below is desired but not # achieved by some solvers. Here are the behaviors observed as of March 2022. # Unit tests only cover solvers that raise successfully: behavior_on_unbounded = { "raise": ["cvxopt", "kvxopt", "ecos", "quadprog", "scs"], "return_crazy_solution": ["qpoases"], "return_none": ["osqp"], } class TestSolveQP(unittest.TestCase): """Test fixture for a variety of quadratic programs. Solver-specific tests are implemented in static methods called ``get_test_{foo}`` that return the test function for a given solver. The corresponding test function ``test_{foo}_{solver}`` is then added to the fixture below the class definition. """ def setUp(self): """Prepare test fixture.""" warnings.simplefilter("ignore", category=DeprecationWarning) warnings.simplefilter("ignore", category=UserWarning) def get_dense_problem(self): """Get dense problem as a sextuple of values to unpack. Returns ------- P : numpy.ndarray Symmetric cost matrix . q : numpy.ndarray Cost vector. G : numpy.ndarray Linear inequality matrix. h : numpy.ndarray Linear inequality vector. A : numpy.ndarray, scipy.sparse.csc_matrix or cvxopt.spmatrix Linear equality matrix. b : numpy.ndarray Linear equality vector. """ M = array([[1.0, 2.0, 0.0], [-8.0, 3.0, 2.0], [0.0, 1.0, 1.0]]) P = dot(M.T, M) # this is a positive definite matrix q = dot(array([3.0, 2.0, 3.0]), M).reshape((3,)) G = array([[1.0, 2.0, 1.0], [2.0, 0.0, 1.0], [-1.0, 2.0, -1.0]]) h = array([3.0, 2.0, -2.0]).reshape((3,)) A = array([1.0, 1.0, 1.0]) b = array([1.0]) return P, q, G, h, A, b def get_sparse_problem(self): """Get sparse problem as a quadruplet of values to unpack. Returns ------- P : scipy.sparse.csc_matrix Symmetric cost matrix. q : numpy.ndarray Cost vector. G : scipy.sparse.csc_matrix Linear inequality matrix. h : numpy.ndarray Linear inequality vector. """ n = 150 M = scipy.sparse.lil_matrix(scipy.sparse.eye(n)) for i in range(1, n - 1): M[i, i + 1] = -1 M[i, i - 1] = 1 P = csc_matrix(M.dot(M.transpose())) q = -ones((n,)) G = csc_matrix(-scipy.sparse.eye(n)) h = -2.0 * ones((n,)) return P, q, G, h def test_no_solver_selected(self): """Check that NoSolverSelected is raised when applicable.""" P, q, G, h, A, b = self.get_dense_problem() with self.assertRaises(NoSolverSelected): solve_qp(P, q, G, h, A, b, solver=None) def test_solver_not_found(self): """SolverNotFound is raised when the solver does not exist.""" P, q, G, h, A, b = self.get_dense_problem() with self.assertRaises(SolverNotFound): solve_qp(P, q, G, h, A, b, solver="ideal") @staticmethod def get_test(solver: str): """Get test function for a given solver. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): P, q, G, h, A, b = self.get_dense_problem() x = solve_qp(P, q, G, h, A, b, solver=solver) x_sp = solve_qp(P, q, G, h, A, b, solver=solver) self.assertIsNotNone(x, f"{solver=}") self.assertIsNotNone(x_sp, f"{solver=}") known_solution = array([0.30769231, -0.69230769, 1.38461538]) sol_tolerance = ( 5e-4 if solver in ["osqp", "qpalm", "scs"] else ( 1e-4 if solver in ["ecos", "jaxopt_osqp"] else 5e-6 if solver in ["proxqp", "qpax", "sip"] else 1e-8 ) ) eq_tolerance = ( 1e-5 if solver in ["jaxopt_osqp", "sip"] else 1e-7 if solver in ["qpax"] else 1e-10 ) ineq_tolerance = ( 2e-4 if solver in ["jaxopt_osqp", "qpalm", "scs"] else 5e-6 if solver in ["proxqp", "qpax"] else 1e-10 ) self.assertLess( norm(x - known_solution), sol_tolerance, f"{solver=}" ) self.assertLess( norm(x_sp - known_solution), sol_tolerance, f"{solver=}" ) self.assertLess(max(dot(G, x) - h), ineq_tolerance, f"{solver=}") self.assertLess(max(dot(A, x) - b), eq_tolerance, f"{solver=}") self.assertLess(min(dot(A, x) - b), eq_tolerance, f"{solver=}") return test @staticmethod def get_test_all_shapes(solver: str): """Get test function for a given solver. This variant tries all possible shapes for matrix and vector parameters. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. Note ---- This function uses DAQP to find groundtruth solutions. """ def test(self): P, q, G, h, _, _ = self.get_dense_problem() A = array([[1.0, 0.0, 0.0], [0.0, 0.4, 0.5]]) b = array([-0.5, -1.2]) lb = array([-0.5, -2, -0.8]) ub = array([+1.0, +1.0, +1.0]) ineq_variants = ((None, None), (G, h), (G[0], array([h[0]]))) eq_variants = ((None, None), (A, b), (A[0], array([b[0]]))) box_variants = ((None, None), (lb, None), (None, ub), (lb, ub)) cases = [ { "P": P, "q": q, "G": G_case, "h": h_case, "A": A_case, "b": b_case, "lb": lb_case, "ub": ub_case, } for (G_case, h_case) in ineq_variants for (A_case, b_case) in eq_variants for (lb_case, ub_case) in box_variants ] for i, test_case in enumerate(cases): no_inequality = "G" not in test_case or test_case["G"] is None if no_inequality and solver in ["qpswift", "qpax"]: # QPs without inequality constraints are not handled by # qpSWIFT or qpax continue no_equality = "A" not in test_case or test_case["A"] is None if no_equality and solver in ["qpax"]: # QPs without equality constraints not handled by qpax continue no_box = ( test_case.get("lb") is None and test_case.get("ub") is None ) if ( no_inequality and no_box and not no_equality and solver == "qtqp" ): # QTQP requires at least one inequality constraint, so # equality-only problems (no G, no box bounds) are not # supported continue has_one_equality = ( "A" in test_case and test_case["A"] is not None and test_case["A"].ndim == 1 ) has_lower_box = ( "lb" in test_case and test_case["lb"] is not None ) if has_one_equality and has_lower_box and solver == "hpipm": # Skipping this test for HPIPM for now # See https://github.com/giaf/hpipm/issues/136 continue test_comp = { k: v.shape if v is not None else "None" for k, v in test_case.items() } daqp_solution = solve_qp(solver="daqp", **test_case) self.assertIsNotNone( daqp_solution, f"Baseline failed on parameters: {test_comp}", ) solver_solution = solve_qp(solver=solver, **test_case) sol_tolerance = ( 2e-2 if solver == "proxqp" else ( 5e-3 if solver in ["jaxopt_osqp"] else ( 2e-3 if solver in ["osqp", "qpalm", "scs"] else 5e-4 if solver == "ecos" else 2e-4 ) ) ) self.assertLess( norm(solver_solution - daqp_solution), sol_tolerance, f"Solver failed on parameters: {test_comp}", ) return test @staticmethod def get_test_bounds(solver: str): """Get test function for a given solver. This variant adds vector bounds. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): P, q, G, h, A, b = self.get_dense_problem() lb = array([-1.0, -2.0, -0.5]) ub = array([1.0, -0.2, 1.0]) x = solve_qp(P, q, G, h, A, b, lb, ub, solver=solver) self.assertIsNotNone(x) known_solution = array([0.41463415, -0.41463415, 1.0]) sol_tolerance = ( 2e-3 if solver == "proxqp" else ( 5e-3 if solver in ["jaxopt_osqp", "osqp"] else ( 5e-5 if solver == "scs" else ( 1e-6 if solver in ["qpalm", "ecos", "qpax", "sip"] else 1e-8 ) ) ) ) eq_tolerance = ( 1e-5 if solver in ["proxqp", "sip"] else 1e-7 if solver in ["qpax"] else 1e-10 ) ineq_tolerance = 1e-5 if solver == "proxqp" else 1e-10 self.assertLess( norm(x - known_solution), sol_tolerance, f"{solver=}" ) self.assertLess(max(dot(G, x) - h), ineq_tolerance, f"{solver=}") self.assertLess(max(dot(A, x) - b), eq_tolerance, f"{solver=}") self.assertLess(min(dot(A, x) - b), eq_tolerance, f"{solver=}") return test @staticmethod def get_test_no_cons(solver: str): """Get test function for a given solver. In this variant, there is no equality nor inequality constraint. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): P, q, G, h, A, b = self.get_dense_problem() x = solve_qp(P, q, solver=solver) self.assertIsNotNone(x) known_solution = array([-0.64705882, -1.17647059, -1.82352941]) sol_tolerance = ( 1e-3 if solver == "ecos" else 1e-5 if solver in ["osqp", "qpalm"] else 1e-6 ) self.assertLess( norm(x - known_solution), sol_tolerance, f"{solver=}" ) return test @staticmethod def get_test_no_eq(solver: str): """Get test function for a given solver. In this variant, there is no equality constraint. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): P, q, G, h, A, b = self.get_dense_problem() x = solve_qp(P, q, G, h, solver=solver) self.assertIsNotNone(x) known_solution = array([-0.49025721, -1.57755261, -0.66484801]) sol_tolerance = ( 1e-3 if solver in ["ecos", "jaxopt_osqp"] else ( 1e-4 if solver in ["qpalm", "sip"] else 1e-5 if solver == "osqp" else 1e-6 ) ) ineq_tolerance = ( 1e-3 if solver == "jaxopt_osqp" else ( 1e-5 if solver == "osqp" else ( 1e-6 if solver == "proxqp" else 1e-7 if solver == "scs" else 1e-10 ) ) ) self.assertLess( norm(x - known_solution), sol_tolerance, f"{solver=}" ) self.assertLess(max(dot(G, x) - h), ineq_tolerance, f"{solver=}") return test @staticmethod def get_test_no_ineq(solver: str): """Get test function for a given solver. In this variant, there is no inequality constraint. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): P, q, G, h, A, b = self.get_dense_problem() x = solve_qp(P, q, A=A, b=b, solver=solver) self.assertIsNotNone(x) known_solution = array([0.28026906, -1.55156951, 2.27130045]) sol_tolerance = ( 1e-3 if solver in ["jaxopt_osqp", "osqp", "qpalm"] else ( 1e-5 if solver in ["ecos", "scs", "qpax"] else ( 1e-6 if solver == "highs" else 1e-7 if solver == "proxqp" else 1e-8 ) ) ) eq_tolerance = ( 1e-5 if solver == "jaxopt_osqp" else ( 1e-6 if solver in ["qpax", "sip"] else 1e-7 if solver == "osqp" else 1e-9 ) ) self.assertLess( norm(x - known_solution), sol_tolerance, f"{solver=}" ) self.assertLess(max(dot(A, x) - b), eq_tolerance, f"{solver=}") self.assertLess(min(dot(A, x) - b), eq_tolerance, f"{solver=}") return test @staticmethod def get_test_one_ineq(solver: str): """Get test function for a given solver. In this variant, there is only one inequality constraint. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): P, q, G, h, A, b = self.get_dense_problem() G, h = G[1], h[1].reshape((1,)) x = solve_qp(P, q, G, h, A, b, solver=solver) self.assertIsNotNone(x) known_solution = array([0.30769231, -0.69230769, 1.38461538]) sol_tolerance = ( 1e-3 if solver in ["jaxopt_osqp", "qpalm"] else ( 5e-4 if solver == "osqp" else ( 1e-5 if solver == "scs" else ( 5e-6 if solver in ["proxqp", "sip"] else ( 1e-6 if solver in ["copt", "cvxopt", "kvxopt", "ecos", "qpax"] else 5e-8 if solver == "qpswift" else 1e-8 ) ) ) ) ) eq_tolerance = ( 1e-3 if solver in ["jaxopt_osqp"] else ( 1e-4 if solver in ["qpalm", "qpax", "sip"] else 1e-8 if solver in ["osqp", "qtqp", "scs"] else 1e-10 ) ) ineq_tolerance = ( 1e-5 if solver == "proxqp" else ( 1e-6 if solver == "qpax" else (1e-7 if solver in ["qpswift", "scs"] else 1e-8) ) ) self.assertLess( norm(x - known_solution), sol_tolerance, f"{solver=}" ) self.assertLess(max(dot(G, x) - h), ineq_tolerance, f"{solver=}") self.assertLess(max(dot(A, x) - b), eq_tolerance, f"{solver=}") self.assertLess(min(dot(A, x) - b), eq_tolerance, f"{solver=}") return test @staticmethod def get_test_sparse(solver: str): """Get test function for a given solver. This variant tests a sparse problem. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): P, q, G, h = self.get_sparse_problem() kwargs = {} tol_solvers = ("osqp", "proxqp", "qpalm", "scs") if solver in tol_solvers: kwargs["eps_abs"] = 2e-4 x = solve_qp(P, q, G, h, solver=solver) self.assertIsNotNone(x) known_solution = array([2.0] * 149 + [3.0]) sol_tolerance = ( 5e-3 if solver == "cvxopt" or solver == "kvxopt" else ( 2e-3 if solver in ["osqp", "qpalm", "qpax", "qtqp"] else ( 1e-3 if solver in ["gurobi", "piqp"] else ( 5e-4 if solver in ["clarabel", "mosek"] else ( 1e-4 if solver in ["scs", "sip"] else ( 2e-5 if solver in ["copt", "proxqp"] else 1e-6 if solver == "highs" else 1e-7 ) ) ) ) ) ) ineq_tolerance = 1e-4 if solver in tol_solvers else 1e-7 self.assertLess( norm(x - known_solution), sol_tolerance, f"{solver=}" ) self.assertLess(max(G * x - h), ineq_tolerance, f"{solver=}") return test @staticmethod def get_test_sparse_bounds(solver: str): """Get test function for a given solver. This variant tests a sparse problem with additional vector lower and upper bounds. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): P, q, G, h = self.get_sparse_problem() lb = +2.2 * ones(q.shape) ub = +2.4 * ones(q.shape) x = solve_qp(P, q, G, h, lb=lb, ub=ub, solver=solver) self.assertIsNotNone(x) known_solution = array([2.2] * 149 + [2.4]) sol_tolerance = ( 1e-3 if solver in ["gurobi", "copt", "osqp", "qpalm", "sip"] else ( 5e-6 if solver in ["mosek", "proxqp"] else ( 1e-7 if solver in ["cvxopt", "kvxopt", "scs"] else 1e-8 ) ) ) ineq_tolerance = 1e-10 self.assertLess( norm(x - known_solution), sol_tolerance, f"{solver=}" ) self.assertLess(max(G * x - h), ineq_tolerance, f"{solver=}") return test @staticmethod def get_test_sparse_unfeasible(solver: str): """Get test function for a given solver. This variant tests an unfeasible sparse problem with additional vector lower and upper bounds. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): P, q, G, h = self.get_sparse_problem() lb = +0.5 * ones(q.shape) ub = +1.5 * ones(q.shape) if solver == "cvxopt" or solver == "kvxopt": # Skipping this test for CVXOPT and KVXOPT for now # See https://github.com/cvxopt/cvxopt/issues/229 return x = solve_qp(P, q, G, h, lb=lb, ub=ub, solver=solver) self.assertIsNone(x) return test @staticmethod def get_test_warmstart(solver: str): """Get test function for a given solver. This variant warm starts. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): P, q, G, h, A, b = self.get_dense_problem() known_solution = array([0.30769231, -0.69230769, 1.38461538]) initvals = known_solution + 0.1 * random.random(3) x = solve_qp( P, q, G, h, A, b, solver=solver, initvals=initvals, verbose=True, # increases coverage ) self.assertIsNotNone(x) sol_tolerance = ( 1e-3 if solver in ["osqp", "qpalm", "scs"] else ( 1e-4 if solver in ["ecos", "jaxopt_osqp"] else 5e-6 if solver in ["proxqp", "qpax", "sip"] else 1e-8 ) ) eq_tolerance = ( 1e-4 if solver in ["jaxopt_osqp", "osqp", "sip"] else 1e-7 if solver == "qpax" else 1e-10 ) ineq_tolerance = ( 1e-3 if solver in ["osqp", "qpalm", "scs"] else ( 1e-5 if solver in ["jaxopt_osqp", "proxqp", "qpax"] else 1e-10 ) ) self.assertLess( norm(x - known_solution), sol_tolerance, f"{solver=}" ) self.assertLess(max(dot(G, x) - h), ineq_tolerance, f"{solver=}") self.assertLess(max(dot(A, x) - b), eq_tolerance, f"{solver=}") self.assertLess(min(dot(A, x) - b), eq_tolerance, f"{solver=}") return test @staticmethod def get_test_raise_on_unbounded_below(solver: str): """ValueError is raised when the problem is unbounded below. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. Notes ----- Detecting non-convexity is not a trivial problem and most solvers leave it to the user. See for instance the `recommendation from OSQP `_. We only run this test for functions that successfully detect unbounded problems when the eigenvalues of :math:`P` are close to zero. """ def test(self): v = array([5.4, -1.2, -1e-2, 1e4]) P = dot(v.reshape(4, 1), v.reshape(1, 4)) q = array([-1.0, -2, 0, 3e-4]) # q is in the nullspace of P, so the problem is unbounded below with self.assertRaises(ProblemError): solve_qp(P, q, solver=solver) return test @staticmethod def get_test_qpmad_demo(solver: str): """Get test function for a given solver. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): problem = get_qpmad_demo_problem() P, q, G, h, _, _, lb, ub = problem.unpack() x = solve_qp(P, q, G, h, lb=lb, ub=ub, solver=solver) known_solution = array( [ 1.0, 2.0, 3.0, 4.0, -0.71875, -0.71875, -0.71875, -0.71875, -0.71875, -0.71875, -0.71875, -0.71875, -0.71875, -0.71875, -0.71875, -0.71875, -0.71875, -0.71875, -0.71875, -0.71875, ] ) sol_tolerance = ( 1e-2 if solver in ["jaxopt_osqp", "osqp"] else ( 5e-4 if solver in ["qpalm", "scs", "qpax"] else ( 2e-5 if solver == "proxqp" else ( 1e-6 if solver in [ "cvxopt", "kvxopt", "mosek", "piqp", "qpswift", "qtqp", "sip", ] else 1e-8 ) ) ) ) self.assertIsNotNone(x) self.assertLess(np.linalg.norm(x - known_solution), sol_tolerance) return test # Generate test fixtures for each solver for solver in available_solvers: setattr(TestSolveQP, f"test_{solver}", TestSolveQP.get_test(solver)) setattr( TestSolveQP, f"test_all_shapes_{solver}", TestSolveQP.get_test_all_shapes(solver), ) setattr( TestSolveQP, f"test_bounds_{solver}", TestSolveQP.get_test_bounds(solver), ) if solver not in ["qpswift"]: # qpSWIFT: https://github.com/qpSWIFT/qpSWIFT/issues/2 setattr( TestSolveQP, f"test_no_cons_{solver}", TestSolveQP.get_test_no_cons(solver), ) setattr( TestSolveQP, f"test_no_eq_{solver}", TestSolveQP.get_test_no_eq(solver), ) if solver not in ["qpswift", "qtqp"]: # qpSWIFT: https://github.com/qpSWIFT/qpSWIFT/issues/2 # QTQP: requires at least one inequality constraint setattr( TestSolveQP, f"test_no_ineq_{solver}", TestSolveQP.get_test_no_ineq(solver), ) setattr( TestSolveQP, f"test_one_ineq_{solver}", TestSolveQP.get_test_one_ineq(solver), ) if solver in sparse_solvers: setattr( TestSolveQP, f"test_sparse_{solver}", TestSolveQP.get_test_sparse(solver), ) setattr( TestSolveQP, f"test_sparse_bounds_{solver}", TestSolveQP.get_test_sparse_bounds(solver), ) setattr( TestSolveQP, f"test_sparse_unfeasible_{solver}", TestSolveQP.get_test_sparse_unfeasible(solver), ) setattr( TestSolveQP, f"test_warmstart_{solver}", TestSolveQP.get_test_warmstart(solver), ) if solver in behavior_on_unbounded["raise"]: setattr( TestSolveQP, f"test_raise_on_unbounded_below_{solver}", TestSolveQP.get_test_raise_on_unbounded_below(solver), ) setattr( TestSolveQP, f"test_qpmad_demo_{solver}", TestSolveQP.get_test_qpmad_demo(solver), ) ================================================ FILE: tests/test_timings.py ================================================ import unittest import warnings from qpsolvers import available_solvers, solve_problem from .problems import get_qpmad_demo_problem class TestTimings(unittest.TestCase): def test_timings_recorded(self): problem = get_qpmad_demo_problem() for solver in available_solvers: with self.subTest(solver=solver): try: with warnings.catch_warnings(): warnings.simplefilter("ignore") solution = solve_problem(problem, solver=solver) self.assertTrue(solution.found, f"Solver {solver} did not find a solution") self.assertIsNotNone(solution.build_time) self.assertIsNotNone(solution.solve_time) self.assertGreaterEqual(solution.build_time, 0.0) self.assertGreaterEqual(solution.solve_time, 0.0) print(f"[{solver}] build_time: {solution.build_time:.6f}s, solve_time: {solution.solve_time:.6f}s") except Exception as e: print(f"[{solver}] Failed to solve: {e}") if __name__ == "__main__": unittest.main() ================================================ FILE: tests/test_unfeasible_problem.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests with unfeasible problems.""" import unittest import warnings from numpy import array, dot from qpsolvers import available_solvers, solve_qp class UnfeasibleProblem(unittest.TestCase): """ Test fixture for an unfeasible quadratic program (inequality and equality constraints are inconsistent). """ def setUp(self): """ Prepare test fixture. """ warnings.simplefilter("ignore", category=UserWarning) def get_unfeasible_problem(self): """ Get problem as a sextuple of values to unpack. Returns ------- P : Symmetric cost matrix. q : Cost vector. G : Linear inequality matrix. h : Linear inequality vector. A : Linear equality matrix. b : Linear equality vector. """ M = array([[1.0, 2.0, 0.0], [-8.0, 3.0, 2.0], [0.0, 1.0, 1.0]]) P = dot(M.T, M) # this is a positive definite matrix q = dot(array([3.0, 2.0, 3.0]), M).reshape((3,)) G = array([[1.0, 1.0, 1.0], [2.0, 0.0, 1.0], [-1.0, 2.0, -1.0]]) h = array([3.0, 2.0, -2.0]).reshape((3,)) A = array([1.0, 1.0, 1.0]) b = array([42.0]) return P, q, G, h, A, b @staticmethod def get_test(solver: str): """ Closure of test function for a given solver. Parameters ---------- solver : Name of the solver to test. Returns ------- : Test function for that solver. """ def test(self): P, q, G, h, A, b = self.get_unfeasible_problem() x = solve_qp(P, q, G, h, A, b, solver=solver) self.assertIsNone(x) return test # Generate test fixtures for each solver for solver in available_solvers: if solver == "qpoases": # Unfortunately qpOASES returns an invalid solution in the face of this # problem being unfeasible. Skipping it. continue setattr( UnfeasibleProblem, "test_{}".format(solver), UnfeasibleProblem.get_test(solver), ) ================================================ FILE: tests/test_utils.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- # # SPDX-License-Identifier: LGPL-3.0-or-later # Copyright 2016-2022 Stéphane Caron and the qpsolvers contributors """Unit tests for utility functions.""" import io import sys import unittest import numpy as np from qpsolvers.utils import print_matrix_vector class TestUtils(unittest.TestCase): """Test fixture for utility functions.""" def setUp(self): self.G = np.array([[1.3, 2.1], [2.6, 0.3], [2.2, -1.6]]) self.h = np.array([3.4, 1.8, -2.7]).reshape((3,)) def test_print_matrix_vector(self): """Printing a matrix-vector pair outputs the proper labels.""" def run_test(G, h): stdout_capture = io.StringIO() sys.stdout = stdout_capture print_matrix_vector(G, "ineq_matrix", h, "ineq_vector") sys.stdout = sys.__stdout__ output = stdout_capture.getvalue() self.assertIn("ineq_matrix =", output) self.assertIn(str(G[0][1]), output) self.assertIn("ineq_vector =", output) self.assertIn(str(h[1]), output) run_test(self.G, self.h) run_test(self.G, self.h[:-1]) run_test(self.G[:-1], self.h)