├── .flake8 ├── .github ├── dependabot.yaml └── workflows │ ├── build_cuda.yml │ ├── build_cuda_windows.yml │ ├── build_default.yml │ ├── build_mkl.yml │ ├── build_mkl_windows.yml │ ├── build_wasm.yml │ └── pre_commit.yml ├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── CMakeLists.txt ├── LICENSE ├── MANIFEST.in ├── README.md ├── backend ├── cuda │ ├── cibuildwheel.toml │ └── pyproject.toml └── mkl │ ├── cibuildwheel.toml │ └── pyproject.toml ├── cibuildwheel.toml ├── cmake ├── README.md ├── memory.h └── printing.h ├── examples ├── basic_usage.py ├── code_generation.py ├── exception_handling.py ├── update_matrices.py └── update_vectors.py ├── pyproject.toml └── src ├── bindings.cpp.in ├── osqp ├── __init__.py ├── builtin.py ├── codegen │ ├── __init__.py │ └── pywrapper │ │ ├── CMakeLists.txt.jinja │ │ ├── __init__.py │ │ ├── bindings.cpp.jinja │ │ └── setup.py.jinja ├── cuda.py ├── interface.py ├── mkl.py ├── nn │ ├── __init__.py │ └── torch.py └── tests │ ├── basic_test.py │ ├── codegen_matrices_test.py │ ├── codegen_vectors_test.py │ ├── conftest.py │ ├── derivative_test.py │ ├── dual_infeasibility_test.py │ ├── feasibility_test.py │ ├── multithread_test.py │ ├── nn_test.py │ ├── non_convex_test.py │ ├── polishing_test.py │ ├── primal_infeasibility_test.py │ ├── solutions │ ├── __init__.py │ ├── test_basic_QP.npz │ ├── test_dual_infeasibility.npz │ ├── test_feasibility_problem.npz │ ├── test_polish_random.npz │ ├── test_polish_simple.npz │ ├── test_polish_unconstrained.npz │ ├── test_primal_infeasibility.npz │ ├── test_solve.npz │ ├── test_unconstrained_problem.npz │ ├── test_update_A.npz │ ├── test_update_A_allind.npz │ ├── test_update_P.npz │ ├── test_update_P_A_allind.npz │ ├── test_update_P_A_indA.npz │ ├── test_update_P_A_indP.npz │ ├── test_update_P_A_indP_indA.npz │ ├── test_update_P_allind.npz │ ├── test_update_bounds.npz │ ├── test_update_l.npz │ ├── test_update_q.npz │ └── test_update_u.npz │ ├── unconstrained_test.py │ ├── update_matrices_test.py │ ├── utils.py │ └── warm_start_test.py └── osqppurepy ├── __init__.py ├── _osqp.py └── interface.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | extend-ignore = E203,E266,E731,E741,W503 3 | max-line-length = 120 4 | max-complexity = 99 5 | select = B,C,E,F,W,T4,B9 6 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # Check the GitHub actions for updates 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | -------------------------------------------------------------------------------- /.github/workflows/build_cuda.yml: -------------------------------------------------------------------------------- 1 | name: Build CUDA Linux 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | build_wheels: 15 | name: Build wheel on ${{ matrix.os }} 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | os: [ubuntu-latest] 21 | 22 | steps: 23 | - uses: actions/checkout@master 24 | 25 | - name: Build wheels 26 | uses: pypa/cibuildwheel@v2.23 27 | with: 28 | package-dir: backend/cuda 29 | config-file: backend/cuda/cibuildwheel.toml 30 | output-dir: wheelhouse 31 | 32 | - name: Upload artifacts to github 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: wheels-cuda-${{ matrix.os }} 36 | path: ./wheelhouse 37 | -------------------------------------------------------------------------------- /.github/workflows/build_cuda_windows.yml: -------------------------------------------------------------------------------- 1 | name: Build CUDA Windows 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | env: 14 | CUDATOOLKIT_URL: https://developer.download.nvidia.com/compute/cuda/12.6.3/local_installers/cuda_12.6.3_561.17_windows.exe 15 | CUDATOOLKIT_COMPONENTS: nvcc_12.6 cudart_12.6 cublas_dev_12.6 curand_dev_12.6 cusparse_dev_12.6 thrust_12.6 visual_studio_integration_12.6 16 | 17 | jobs: 18 | build_wheels: 19 | name: Build wheel on ${{ matrix.os }} 20 | runs-on: ${{ matrix.os }} 21 | defaults: 22 | run: 23 | shell: cmd 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | os: [windows-2022] 28 | 29 | steps: 30 | - uses: actions/checkout@master 31 | 32 | - name: Add msbuild to PATH 33 | uses: microsoft/setup-msbuild@v2.0.0 34 | 35 | - name: Add Windows SDK 36 | run: | 37 | choco install windows-sdk-8.1 38 | 39 | - name: cache install cuda 40 | id: cache-install 41 | uses: actions/cache@v4 42 | with: 43 | path: C:\Program Files (x86)\Intel\oneAPI\ 44 | key: install-${{ env.CUDATOOLKIT_URL }}-${{ env.CUDATOOLKIT_COMPONENTS }} 45 | 46 | - name: install cuda 47 | if: steps.cache-install.outputs.cache-hit != 'true' 48 | run: | 49 | curl.exe --output %TEMP%\cuda.exe --url %CUDATOOLKIT_URL% --retry 5 --retry-delay 5 50 | start /b /wait %TEMP%\cuda.exe -s %CUDATOOLKIT_COMPONENTS% 51 | del %TEMP%\cuda.exe 52 | 53 | - name: Build wheels 54 | uses: pypa/cibuildwheel@v2.23 55 | with: 56 | package-dir: backend/cuda 57 | config-file: backend/cuda/cibuildwheel.toml 58 | output-dir: wheelhouse 59 | 60 | - name: Upload artifacts to github 61 | uses: actions/upload-artifact@v4 62 | with: 63 | name: wheels-cuda-${{ matrix.os }} 64 | path: ./wheelhouse 65 | -------------------------------------------------------------------------------- /.github/workflows/build_default.yml: -------------------------------------------------------------------------------- 1 | name: Build Default 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | build_sdist: 15 | name: Build source 16 | runs-on: ubuntu-latest 17 | strategy: 18 | fail-fast: false 19 | steps: 20 | - uses: actions/checkout@master 21 | with: 22 | submodules: 'recursive' 23 | 24 | - name: Build source 25 | run: | 26 | python -m pip install build 27 | python -m build --sdist --outdir=wheelhouse 28 | 29 | - name: Upload sdist to github 30 | uses: actions/upload-artifact@v4 31 | with: 32 | name: wheels-sdist 33 | path: wheelhouse/*.tar.gz 34 | if-no-files-found: error 35 | 36 | build_wheels: 37 | name: Build wheel on ${{ matrix.os }} for ${{ matrix.cibw_archs }} 38 | runs-on: ${{ matrix.os }} 39 | strategy: 40 | fail-fast: false 41 | matrix: 42 | include: 43 | - os: ubuntu-latest 44 | cibw_archs: "x86_64" 45 | - os: windows-2022 46 | cibw_archs: "auto64" 47 | # Include macos-13 to get Intel x86_64 macs and maos-latest to get the Aaarch64 macs 48 | - os: macos-13 49 | cibw_archs: "x86_64" 50 | - os: macos-latest 51 | cibw_archs: "arm64" 52 | 53 | steps: 54 | - uses: actions/checkout@master 55 | 56 | - name: Build wheels 57 | uses: pypa/cibuildwheel@v2.23 58 | with: 59 | config-file: cibuildwheel.toml 60 | output-dir: wheelhouse 61 | env: 62 | CIBW_ENVIRONMENT_MACOS: CMAKE_OSX_ARCHITECTURES=${{ matrix.cibw_archs }} 63 | 64 | - name: Upload artifacts to github 65 | uses: actions/upload-artifact@v4 66 | with: 67 | name: wheels-${{ runner.os }}-${{ matrix.cibw_archs }} 68 | path: ./wheelhouse/*.whl 69 | if-no-files-found: error 70 | 71 | publish_to_pypi: 72 | name: Publish wheels to PyPi 73 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') 74 | needs: [build_sdist, build_wheels] 75 | runs-on: ubuntu-latest 76 | steps: 77 | - name: Download packages 78 | uses: actions/download-artifact@v4 79 | with: 80 | pattern: wheels-* 81 | path: dist 82 | merge-multiple: true 83 | 84 | - name: Print out packages 85 | run: ls -la dist/* 86 | 87 | - name: Upload wheels to pypi 88 | env: 89 | TWINE_USERNAME: __token__ 90 | TWINE_PASSWORD: ${{ secrets.pypi_password }} 91 | run: | 92 | python -m pip install --upgrade twine 93 | twine upload dist/* 94 | -------------------------------------------------------------------------------- /.github/workflows/build_mkl.yml: -------------------------------------------------------------------------------- 1 | name: Build MKL Mac/Linux 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | build_wheels: 15 | name: Build wheel on ${{ matrix.os }} 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | # macos-latest now uses arm64 runners, but MKL is x86_64 only, so restrict to the macos-13 runners 21 | # to get x86_64 architecture. 22 | os: [ubuntu-latest, macos-13] 23 | 24 | steps: 25 | - uses: actions/checkout@master 26 | 27 | - name: Build wheels 28 | uses: pypa/cibuildwheel@v2.23 29 | with: 30 | package-dir: backend/mkl 31 | config-file: backend/mkl/cibuildwheel.toml 32 | output-dir: wheelhouse 33 | 34 | - name: Upload artifacts to github 35 | uses: actions/upload-artifact@v4 36 | with: 37 | name: wheels-mkl-${{ matrix.os }} 38 | path: ./wheelhouse 39 | -------------------------------------------------------------------------------- /.github/workflows/build_mkl_windows.yml: -------------------------------------------------------------------------------- 1 | name: Build MKL Windows 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | env: 14 | # update urls for oneapi packages according to 15 | # https://github.com/oneapi-src/oneapi-ci/blob/master/.github/workflows/build_all.yml 16 | WINDOWS_BASEKIT_URL: https:/registrationcenter-download.intel.com/akdlm/IRC_NAS/c961e083-5685-4f0b-ada5-c6cf16f561dd/w_BaseKit_p_2023.1.0.47256_offline.exe 17 | WINDOWS_BASEKIT_COMPONENTS: intel.oneapi.win.mkl.devel 18 | 19 | 20 | jobs: 21 | build_wheels: 22 | name: Build wheel on ${{ matrix.os }} 23 | runs-on: ${{ matrix.os }} 24 | defaults: 25 | run: 26 | shell: cmd 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | os: [windows-2022] 31 | 32 | steps: 33 | - uses: actions/checkout@master 34 | 35 | - name: cache install oneapi 36 | id: cache-install 37 | uses: actions/cache@v4 38 | with: 39 | path: C:\Program Files (x86)\Intel\oneAPI\ 40 | key: install-${{ env.WINDOWS_BASEKIT_URL }}-${{ env.WINDOWS_BASEKIT_COMPONENTS }} 41 | 42 | - name: install oneapi mkl 43 | if: steps.cache-install.outputs.cache-hit != 'true' 44 | run: | 45 | curl.exe --output %TEMP%\webimage_base.exe --url %WINDOWS_BASEKIT_URL% --retry 5 --retry-delay 5 46 | start /b /wait %TEMP%\webimage_base.exe -s -x -f webimage_base_extracted --log extract_base.log 47 | del %TEMP%\webimage_base.exe 48 | webimage_base_extracted\bootstrapper.exe -s --action install --components=%WINDOWS_BASEKIT_COMPONENTS% --eula=accept -p=NEED_VS2017_INTEGRATION=0 -p=NEED_VS2019_INTEGRATION=0 --log-dir=. 49 | rd /s/q "webimage_base_extracted" 50 | 51 | - name: Build wheels 52 | uses: pypa/cibuildwheel@v2.23 53 | with: 54 | package-dir: backend/mkl 55 | config-file: backend/mkl/cibuildwheel.toml 56 | output-dir: wheelhouse 57 | 58 | - name: Upload artifacts to github 59 | uses: actions/upload-artifact@v4 60 | with: 61 | name: wheels-mkl-${{ matrix.os }} 62 | path: ./wheelhouse 63 | -------------------------------------------------------------------------------- /.github/workflows/build_wasm.yml: -------------------------------------------------------------------------------- 1 | name: Build WASM 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | build_wheels: 15 | name: Build wasm32 wheels 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@master 20 | 21 | - name: Build wheels 22 | uses: pypa/cibuildwheel@v2.23 23 | env: 24 | CIBW_PLATFORM: pyodide 25 | with: 26 | config-file: cibuildwheel.toml 27 | output-dir: wheelhouse 28 | 29 | - name: Upload artifacts to github 30 | uses: actions/upload-artifact@v4 31 | with: 32 | name: wheels-wasm32 33 | path: ./wheelhouse 34 | -------------------------------------------------------------------------------- /.github/workflows/pre_commit.yml: -------------------------------------------------------------------------------- 1 | name: Pre-commit checks 2 | 3 | on: 4 | push: 5 | # Run this workflow on all branches because it is good to flag these errors 6 | # and this workflow is "cheap" 7 | branches: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | precommit: 15 | name: Pre-commit checks 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | os: [ubuntu-latest] 21 | 22 | steps: 23 | - uses: actions/checkout@master 24 | 25 | - uses: actions/setup-python@v5 26 | name: Install Python 27 | with: 28 | python-version: '3.9' 29 | 30 | - name: Install package with dev dependencies 31 | run: | 32 | python -m pip install .[dev] 33 | 34 | - name: Pre-commit checks 35 | run: | 36 | pre-commit run --all-files 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editor files 2 | # ------------------------------------------------------------------- 3 | *.swp 4 | .ycm_extra_conf.py 5 | *.swo 6 | *.vscode/ 7 | *#* 8 | *~ 9 | 10 | 11 | # Tags 12 | # ------------------------------------------------------------------- 13 | tags 14 | .tags 15 | .tags1 16 | TAGS 17 | 18 | # Python Language 19 | # ------------------------------------------------------------------- 20 | # Byte-compiled / optimized / DLL files 21 | __pycache__/ 22 | *.py[cod] 23 | *$py.class 24 | 25 | # C extensions 26 | *.so 27 | 28 | # Pytest 29 | .pytest_cache/ 30 | 31 | # Distribution / packaging 32 | .Python 33 | env/ 34 | build/ 35 | develop-eggs/ 36 | dist/ 37 | downloads/ 38 | eggs/ 39 | .eggs/ 40 | lib/ 41 | lib64/ 42 | parts/ 43 | sdist/ 44 | var/ 45 | *.egg-info/ 46 | .installed.cfg 47 | *.egg 48 | 49 | # PyInstaller 50 | # Usually these files are written by a python script from a template 51 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 52 | *.manifest 53 | *.spec 54 | 55 | # Installer logs 56 | pip-log.txt 57 | pip-delete-this-directory.txt 58 | 59 | # Unit test / coverage reports 60 | htmlcov/ 61 | .tox/ 62 | .coverage 63 | .coverage.* 64 | .cache 65 | nosetests.xml 66 | coverage.xml 67 | *,cover 68 | .hypothesis/ 69 | 70 | # Translations 71 | *.mo 72 | *.pot 73 | 74 | # Django stuff: 75 | *.log 76 | local_settings.py 77 | 78 | # Flask stuff: 79 | instance/ 80 | .webassets-cache 81 | 82 | # Scrapy stuff: 83 | .scrapy 84 | 85 | # Sphinx documentation 86 | docs/_build/ 87 | 88 | # PyBuilder 89 | target/ 90 | 91 | # IPython Notebook 92 | .ipynb_checkpoints 93 | 94 | # pyenv 95 | .python-version 96 | 97 | # celery beat schedule file 98 | celerybeat-schedule 99 | 100 | # dotenv 101 | .env 102 | 103 | # virtualenv 104 | .venv/ 105 | venv/ 106 | ENV/ 107 | 108 | # Spyder project settings 109 | .spyderproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # Python 3 porting backup files 115 | *.bak 116 | 117 | # Mac OSX Files 118 | # ------------------------------------------------------------------- 119 | .DS_Store 120 | 121 | 122 | .gdb_history 123 | .cquery_cached_index/ 124 | pip-wheel-metadata/ 125 | scratch/ 126 | 127 | # setuptools-scm managed version file 128 | src/osqp/_version.py 129 | 130 | # files that are modified in the build process 131 | src/osqp/bindings.cpp 132 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/.gitmodules -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v3.2.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | 11 | - repo: https://github.com/pycqa/flake8 12 | rev: '5.0.4' 13 | hooks: 14 | - id: flake8 15 | 16 | - repo: https://github.com/grantjenks/blue.git 17 | rev: v0.9.1 18 | hooks: 19 | - id: blue 20 | args: [--line-length=120] 21 | 22 | - repo: https://github.com/MarcoGorelli/absolufy-imports 23 | rev: v0.3.1 24 | hooks: 25 | - id: absolufy-imports 26 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15...3.26) 2 | project(ext) 3 | 4 | set(PYTHON "ON") 5 | set(OSQP_BUILD_UNITTESTS "OFF") 6 | set(OSQP_USE_LONG "OFF") 7 | set(OSQP_CUSTOM_PRINTING "${CMAKE_CURRENT_SOURCE_DIR}/cmake/printing.h") 8 | set(OSQP_CUSTOM_MEMORY "${CMAKE_CURRENT_SOURCE_DIR}/cmake/memory.h") 9 | set(OSQP_CODEGEN_INSTALL_DIR "codegen/codegen_src" CACHE PATH "" FORCE) 10 | 11 | if(APPLE) 12 | message(STATUS "Building for Apple arches: ${CMAKE_OSX_ARCHITECTURES}") 13 | endif() 14 | 15 | include(FetchContent) 16 | 17 | # 03/05/24 - Use modern python discovery 18 | set(PYBIND11_FINDPYTHON "ON") 19 | 20 | find_package(pybind11 CONFIG REQUIRED) 21 | 22 | # 03/05/24 - Workaround because OSQP CMakeLists.txt is using old variable names 23 | set(PYTHON_FOUND "ON") 24 | set(PYTHON_INCLUDE_DIRS ${Python_INCLUDE_DIRS}) 25 | 26 | message(STATUS "Fetching/configuring OSQP") 27 | list(APPEND CMAKE_MESSAGE_INDENT " ") 28 | FetchContent_Declare( 29 | osqp 30 | GIT_REPOSITORY https://github.com/osqp/osqp.git 31 | GIT_TAG v1.0.0 32 | ) 33 | list(POP_BACK CMAKE_MESSAGE_INDENT) 34 | FetchContent_MakeAvailable(osqp) 35 | 36 | configure_file(${CMAKE_CURRENT_SOURCE_DIR}/src/bindings.cpp.in 37 | ${CMAKE_CURRENT_SOURCE_DIR}/src/bindings.cpp) 38 | pybind11_add_module(${OSQP_EXT_MODULE_NAME} src/bindings.cpp) 39 | install(TARGETS ${OSQP_EXT_MODULE_NAME} DESTINATION . COMPONENT python) 40 | 41 | # TODO: We shouldn't have to do this once the interfaces are set up correctly 42 | if(${OSQP_ALGEBRA_BACKEND} STREQUAL "builtin") 43 | target_link_libraries(ext_builtin PUBLIC pybind11::module osqpstatic) 44 | elseif(${OSQP_ALGEBRA_BACKEND} STREQUAL "mkl") 45 | if(APPLE) 46 | target_link_libraries(osqp_mkl PUBLIC pybind11::module osqpstatic) 47 | else() 48 | target_link_libraries(osqp_mkl PUBLIC pybind11::module osqpstatic $) 49 | endif() 50 | elseif(${OSQP_ALGEBRA_BACKEND} STREQUAL "cuda") 51 | enable_language(CUDA) 52 | find_package(CUDA) 53 | include_directories(${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES}) 54 | target_link_directories(osqp_cuda PUBLIC ${CMAKE_CUDA_HOST_IMPLICIT_LINK_DIRECTORIES}) 55 | target_link_libraries(osqp_cuda PUBLIC pybind11::module osqpstatic cublas cusparse) 56 | endif() 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE 2 | include src/osqp/tests/solutions/*.npz 3 | recursive-include src/extension * 4 | recursive-include src/osqp/codegen/pywrapper * 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PyPI version](https://badge.fury.io/py/osqp.svg)](https://badge.fury.io/py/osqp) 2 | [![Python 3.8‒3.13](https://img.shields.io/badge/python-3.8%E2%80%923.13-blue)](https://www.python.org) 3 | [![Build](https://github.com/osqp/osqp-python/actions/workflows/build_default.yml/badge.svg)](https://github.com/osqp/osqp-python/actions/workflows/build_default.yml) 4 | 5 | # OSQP Python 6 | Python wrapper for [OSQP](https://osqp.org): The Operator Splitting QP solver. 7 | 8 | The OSQP (Operator Splitting Quadratic Program) solver is a numerical 9 | optimization package for solving problems in the form 10 | 11 | $$\begin{array}{ll} 12 | \mbox{minimize} & \frac{1}{2} x^T P x + q^T x \\ 13 | \mbox{subject to} & l \le A x \le u 14 | \end{array} 15 | $$ 16 | 17 | where $\( x \in \mathbf{R}^n \)$ is the optimization variable and $\( P \in \mathbf{S}^{n}_{+} \)$ is a positive semidefinite matrix. 18 | 19 | ## Installation 20 | To install `osqp` for python, make sure that you're using a recent version of `pip` (`pip install --upgrade pip`) 21 | and then use ``pip install osqp``. 22 | 23 | To install `osqp` from source, clone the repository (`git clone https://github.com/osqp/osqp-python`) 24 | and run `pip install .` from inside the cloned folder. 25 | 26 | ## Documentation 27 | The interface is documented [here](https://osqp.org/docs/interfaces/python.html). 28 | -------------------------------------------------------------------------------- /backend/cuda/cibuildwheel.toml: -------------------------------------------------------------------------------- 1 | [tool.cibuildwheel] 2 | build = "cp3*" 3 | skip = ["cp36-*", "cp37-*", "*-win32", "*-manylinux_i686", "*-musllinux_*"] 4 | build-verbosity = 1 5 | before-build = "rm -rf {package}/osqp_sources/build" 6 | repair-wheel-command = "" 7 | 8 | [tool.cibuildwheel.linux] 9 | before-all = [ 10 | "yum-config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel9/x86_64/cuda-rhel9.repo", 11 | "yum search cuda-toolkit*", 12 | "yum install -y cuda-toolkit-12-6" 13 | ] 14 | environment = { CMAKE_CUDA_COMPILER = "/usr/local/cuda-12.6/bin/nvcc" } 15 | 16 | [tool.cibuildwheel.windows] 17 | environment = { CMAKE_CUDA_COMPILER = "C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v12.6/bin/nvcc.exe", CUDA_TOOLKIT_ROOT_DIR = "C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v12.6", CMAKE_GENERATOR_TOOLSET = "cuda=C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v12.6" } 18 | -------------------------------------------------------------------------------- /backend/cuda/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["scikit-build-core", "pybind11"] 3 | build-backend = "scikit_build_core.build" 4 | 5 | [project] 6 | name = "osqp-cu12" 7 | dynamic = ["version"] 8 | description = "OSQP: The Operator Splitting QP Solver" 9 | requires-python = ">=3.8" 10 | authors = [ 11 | { name = "Bartolomeo Stellato", email = "bartolomeo.stellato@gmail.com" }, 12 | { name = "Goran Banjac" }, 13 | { name = "Vineet Bansal", email = "vineetbansal@protonmail.com" }, 14 | { name = "Amit Solomon", email = "as3993@princeton.edu" }, 15 | { name = "Henry Schreiner", email = "HenrySchreinerIII@gmail.com" }, 16 | ] 17 | dependencies = [ 18 | "osqp>=1.0.0a0", 19 | ] 20 | 21 | [project.urls] 22 | Homepage = "https://osqp.org/" 23 | 24 | [tool.scikit-build] 25 | install.components = ["python"] 26 | metadata.version.provider = "scikit_build_core.metadata.setuptools_scm" 27 | minimum-version = "0.8" 28 | cmake.source-dir = "../.." 29 | 30 | [tool.scikit-build.cmake.define] 31 | OSQP_ALGEBRA_BACKEND = "cuda" 32 | OSQP_EXT_MODULE_NAME = "osqp_cuda" 33 | CMAKE_CUDA_COMPILER = {env="CMAKE_CUDA_COMPILER"} 34 | CUDA_TOOLKIT_ROOT_DIR = {env="CUDA_TOOLKIT_ROOT_DIR"} 35 | 36 | [tool.setuptools_scm] 37 | root = "../.." 38 | -------------------------------------------------------------------------------- /backend/mkl/cibuildwheel.toml: -------------------------------------------------------------------------------- 1 | [tool.cibuildwheel] 2 | build = "cp3*" 3 | skip = ["cp36-*", "cp37-*", "*-win32", "*-manylinux_i686", "*-musllinux_*"] 4 | build-verbosity = 1 5 | before-build = "rm -rf {package}/osqp_sources/build" 6 | 7 | [tool.cibuildwheel.linux] 8 | before-all = [ 9 | "yum-config-manager --add-repo https://yum.repos.intel.com/oneapi", 10 | "rpm --import https://yum.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB", 11 | "yum --nogpgcheck install -y intel-oneapi-mkl-devel-2023.0.0" 12 | ] 13 | environment = { MKL_ROOT = "/opt/intel/oneapi/mkl/latest" } 14 | repair-wheel-command = "" 15 | 16 | [tool.cibuildwheel.macos] 17 | before-all = [ 18 | # See https://github.com/oneapi-src/oneapi-ci for installer URLs 19 | "wget -q https://registrationcenter-download.intel.com/akdlm/IRC_NAS/cd013e6c-49c4-488b-8b86-25df6693a9b7/m_BaseKit_p_2023.2.0.49398.dmg", 20 | "hdiutil attach -noverify -noautofsck m_BaseKit_p_2023.2.0.49398.dmg", 21 | "sudo /Volumes/m_BaseKit_p_2023.2.0.49398/bootstrapper.app/Contents/MacOS/bootstrapper --silent --eula accept --components intel.oneapi.mac.mkl.devel", 22 | "pip install 'cmake==3.18.4'" 23 | ] 24 | environment = { MKL_ROOT = "/opt/intel/oneapi/mkl/latest" } 25 | repair-wheel-command = "" 26 | 27 | [tool.cibuildwheel.windows] 28 | before-all = "pip install delvewheel" 29 | environment = { MKL_ROOT = "C:/Program Files (x86)/Intel/oneAPI/mkl/latest", MKL_DIR = "C:/Program Files (x86)/Intel/oneAPI/mkl/latest/lib/cmake/mkl" } 30 | repair-wheel-command = "delvewheel repair {wheel} --wheel-dir {dest_dir} --no-mangle-all --add-path \"C:/Program Files (x86)/Intel/oneAPI/mkl/latest/redist/intel64\" --add-dll \"mkl_sequential.2.dll;mkl_def.2.dll;mkl_intel_thread.2.dll\"" 31 | -------------------------------------------------------------------------------- /backend/mkl/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["scikit-build-core", "pybind11"] 3 | build-backend = "scikit_build_core.build" 4 | 5 | [project] 6 | name = "osqp-mkl" 7 | dynamic = ["version"] 8 | description = "OSQP: The Operator Splitting QP Solver" 9 | requires-python = ">=3.8" 10 | authors = [ 11 | { name = "Bartolomeo Stellato", email = "bartolomeo.stellato@gmail.com" }, 12 | { name = "Goran Banjac" }, 13 | { name = "Vineet Bansal", email = "vineetbansal@protonmail.com" }, 14 | { name = "Amit Solomon", email = "as3993@princeton.edu" }, 15 | { name = "Henry Schreiner", email = "HenrySchreinerIII@gmail.com" }, 16 | ] 17 | dependencies = [ 18 | "osqp>=1.0.0a0", 19 | ] 20 | 21 | [project.urls] 22 | Homepage = "https://osqp.org/" 23 | 24 | [tool.scikit-build] 25 | install.components = ["python"] 26 | metadata.version.provider = "scikit_build_core.metadata.setuptools_scm" 27 | minimum-version = "0.8" 28 | cmake.source-dir = "../.." 29 | 30 | [tool.scikit-build.cmake.define] 31 | OSQP_ALGEBRA_BACKEND = "mkl" 32 | OSQP_EXT_MODULE_NAME = "osqp_mkl" 33 | 34 | [tool.setuptools_scm] 35 | root = "../.." 36 | -------------------------------------------------------------------------------- /cibuildwheel.toml: -------------------------------------------------------------------------------- 1 | [tool.cibuildwheel] 2 | build = "cp3*" 3 | skip = ["cp36-*", "cp37-*", "*-win32", "*-manylinux_i686", "*-musllinux_*"] 4 | build-verbosity = 1 5 | before-build = "rm -rf {package}/osqp_sources/build" 6 | # Install CPU-only version of torch beforehand since that allows cibuildwheel 7 | # to satisfy the "test" dependency group install, but much faster. The runtime 8 | # cost of torch-based osqp tests are considered negligible so torch-cpu is ok. 9 | before-test = "pip install torch --index-url https://download.pytorch.org/whl/cpu" 10 | test-groups = ["test"] 11 | test-command = "python -m pytest -s {project}/src/osqp/tests" 12 | 13 | [tool.cibuildwheel.macos] 14 | # 02/13/25 - Skip testing on cp313-macosx_x86_64 because torch/numpy deps are unsatisfiable 15 | test-skip = "cp313-macosx_x86_64" 16 | 17 | [tool.cibuildwheel.pyodide] 18 | build = "cp312-pyodide_wasm32" 19 | before-test = "" 20 | test-groups = ["test-no-nn"] 21 | test-command = "python -m pytest -s {project}/src/osqp/tests --continue-on-collection-errors --ignore={project}/src/osqp/tests/multithread_test.py --ignore={project}/src/osqp/tests/nn_test.py --ignore-glob=\"{project}/src/osqp/tests/codegen*.py\"" 22 | environment = { OSQP_ENABLE_INTERRUPT = "OFF", OSQP_CODEGEN = "OFF", OSQP_BUILD_SHARED_LIB = "OFF" } 23 | -------------------------------------------------------------------------------- /cmake/README.md: -------------------------------------------------------------------------------- 1 | This folder contains custom printing/memory management routines for the Python wrapper for OSQP. 2 | 3 | During build time for osqp, the following options are passed on to cmake by setup.py: 4 | ``` 5 | cmake -DOSQP_CUSTOM_PRINTING=/path/to/printing.h -DOSQP_CUSTOM_MEMORY=/path/to/memory.h 6 | ``` 7 | -------------------------------------------------------------------------------- /cmake/memory.h: -------------------------------------------------------------------------------- 1 | // Define memory allocation for python. Note that in Python 2 memory manager 2 | // Calloc is not implemented 3 | # include 4 | # if PY_MAJOR_VERSION >= 3 5 | // https://docs.python.org/3/c-api/memory.html 6 | // The following function sets are wrappers to the system allocator. These functions are thread-safe, the GIL does not need to be held. 7 | // The default raw memory allocator uses the following functions: malloc(), calloc(), realloc() and free(); call malloc(1) (or calloc(1, 1)) when requesting zero bytes. 8 | # define c_malloc PyMem_RawMalloc 9 | # define c_calloc PyMem_RawCalloc 10 | # define c_free PyMem_RawFree 11 | # define c_realloc PyMem_RawRealloc 12 | # else /* if PY_MAJOR_VERSION >= 3 */ 13 | # define c_malloc PyMem_Malloc 14 | # define c_free PyMem_Free 15 | # define c_realloc PyMem_Realloc 16 | static void* c_calloc(size_t num, size_t size) { 17 | void *m = PyMem_Malloc(num * size); 18 | memset(m, 0, num * size); 19 | return m; 20 | } 21 | # endif /* if PY_MAJOR_VERSION >= 3 */ 22 | -------------------------------------------------------------------------------- /cmake/printing.h: -------------------------------------------------------------------------------- 1 | # include 2 | # define c_print(...) \ 3 | { \ 4 | PyGILState_STATE gilstate = PyGILState_Ensure(); \ 5 | PySys_WriteStdout(__VA_ARGS__); \ 6 | PyGILState_Release(gilstate); \ 7 | } 8 | -------------------------------------------------------------------------------- /examples/basic_usage.py: -------------------------------------------------------------------------------- 1 | import osqp 2 | import numpy as np 3 | from scipy import sparse 4 | 5 | 6 | if __name__ == '__main__': 7 | # Define problem data 8 | P = sparse.csc_matrix([[4, 1], [1, 2]]) 9 | q = np.array([1, 1]) 10 | A = sparse.csc_matrix([[1, 1], [1, 0], [0, 1]]) 11 | l = np.array([1, 0, 0]) 12 | u = np.array([1, 0.7, 0.7]) 13 | 14 | # Create an OSQP object 15 | prob = osqp.OSQP() 16 | 17 | # Setup workspace and change alpha parameter 18 | prob.setup(P, q, A, l, u, alpha=1.0) 19 | 20 | # Settings can be changed using .update_settings() 21 | prob.update_settings(polishing=1) 22 | 23 | # Solve problem 24 | res = prob.solve(raise_error=True) 25 | 26 | # Check solver status 27 | # For all values, see https://osqp.org/docs/interfaces/status_values.html 28 | assert res.info.status_val == osqp.SolverStatus.OSQP_SOLVED 29 | 30 | print('Status:', res.info.status) 31 | print('Objective value:', res.info.obj_val) 32 | print('Optimal solution x:', res.x) 33 | -------------------------------------------------------------------------------- /examples/code_generation.py: -------------------------------------------------------------------------------- 1 | import osqp 2 | import numpy as np 3 | from scipy import sparse 4 | 5 | 6 | if __name__ == '__main__': 7 | # Define problem data 8 | P = sparse.csc_matrix([[4, 1], [1, 2]]) 9 | q = np.array([1, 1]) 10 | A = sparse.csc_matrix([[1, 1], [1, 0], [0, 1]]) 11 | l = np.array([1, 0, 0]) 12 | u = np.array([1, 0.7, 0.7]) 13 | 14 | # Create an OSQP object 15 | prob = osqp.OSQP() 16 | 17 | # Setup workspace and change alpha parameter 18 | prob.setup(P, q, A, l, u, alpha=1.0) 19 | 20 | # The OSQP object has "capabilities" that define what it can do. 21 | assert prob.has_capability('OSQP_CAPABILITY_CODEGEN') 22 | 23 | # Generate C code 24 | # fmt: off 25 | prob.codegen( 26 | 'out', # Output folder for auto-generated code 27 | prefix='prob1_', # Prefix for filenames and C variables; useful if generating multiple problems 28 | force_rewrite=True, # Force rewrite if output folder exists? 29 | parameters='vectors', # What do we wish to update in the generated code? 30 | # One of 'vectors' (allowing update of q/l/u through prob.update_data_vec) 31 | # or 'matrices' (allowing update of P/A/q/l/u 32 | # through prob.update_data_vec or prob.update_data_mat) 33 | use_float=False, # Use single precision in generated code? 34 | printing_enable=False, # Enable solver printing? 35 | profiling_enable=False, # Enable solver profiling? 36 | interrupt_enable=False, # Enable user interrupt (Ctrl-C)? 37 | include_codegen_src=True, # Include headers/sources/Makefile in the output folder, 38 | # creating a self-contained compilable folder? 39 | extension_name='pyosqp', # Name of the generated python extension; generates a setup.py; Set None to skip 40 | compile=False, # Compile the above python extension into an importable module 41 | # (allowing "import pyosqp")? 42 | ) 43 | # fmt: on 44 | -------------------------------------------------------------------------------- /examples/exception_handling.py: -------------------------------------------------------------------------------- 1 | import osqp 2 | import numpy as np 3 | from scipy import sparse 4 | 5 | 6 | """ 7 | 8 | `osqp.OSQPException`s might be raised during `.setup()`, `.update_settings()`, 9 | or `.solve()`. This example demonstrates how to catch an `osqp.OSQPException` 10 | raised during `.setup()`, and how to compare it to a specific `osqp.SolverError`. 11 | 12 | Exceptions other than `osqp.OSQPException` might also be raised, but these 13 | are typically errors in using the wrapper, and are not raised by the underlying 14 | `osqp` library itself. 15 | 16 | """ 17 | 18 | if __name__ == '__main__': 19 | 20 | P = sparse.triu([[2.0, 5.0], [5.0, 1.0]], format='csc') 21 | q = np.array([3.0, 4.0]) 22 | A = sparse.csc_matrix([[-1.0, 0.0], [0.0, -1.0], [-1.0, 3.0], [2.0, 5.0], [3.0, 4]]) 23 | l = -np.inf * np.ones(A.shape[0]) 24 | u = np.array([0.0, 0.0, -15.0, 100.0, 80.0]) 25 | 26 | prob = osqp.OSQP() 27 | 28 | try: 29 | prob.setup(P, q, A, l, u) 30 | except osqp.OSQPException as e: 31 | # Our problem is non-convex, so we get a osqp.OSQPException 32 | # during .setup() 33 | assert e == osqp.SolverError.OSQP_NONCVX_ERROR 34 | -------------------------------------------------------------------------------- /examples/update_matrices.py: -------------------------------------------------------------------------------- 1 | import osqp 2 | import numpy as np 3 | from scipy import sparse 4 | 5 | if __name__ == '__main__': 6 | # Define problem data 7 | P = sparse.csc_matrix([[4, 1], [1, 2]]) 8 | q = np.array([1, 1]) 9 | A = sparse.csc_matrix([[1, 1], [1, 0], [0, 1]]) 10 | l = np.array([1, 0, 0]) 11 | u = np.array([1, 0.7, 0.7]) 12 | 13 | # Create an OSQP object 14 | prob = osqp.OSQP() 15 | 16 | # Setup workspace 17 | prob.setup(P, q, A, l, u) 18 | 19 | # Solve problem 20 | res = prob.solve() 21 | 22 | # Update problem 23 | # IMPORTANT: The sparsity structure of P/A should remain the same, 24 | # so we only update Px and Ax 25 | # (i.e. the actual data values at indices with nonzero values) 26 | # NB: Update only upper triangular part of P 27 | P_new = sparse.csc_matrix([[5, 1.5], [1.5, 1]]) 28 | A_new = sparse.csc_matrix([[1.2, 1.1], [1.5, 0], [0, 0.8]]) 29 | prob.update(Px=sparse.triu(P_new).data, Ax=A_new.data) 30 | 31 | # Solve updated problem 32 | res = prob.solve() 33 | 34 | print('Status:', res.info.status) 35 | print('Objective value:', res.info.obj_val) 36 | print('Optimal solution x:', res.x) 37 | -------------------------------------------------------------------------------- /examples/update_vectors.py: -------------------------------------------------------------------------------- 1 | import osqp 2 | import numpy as np 3 | from scipy import sparse 4 | 5 | if __name__ == '__main__': 6 | # Define problem data 7 | P = sparse.csc_matrix([[4, 1], [1, 2]]) 8 | q = np.array([1, 1]) 9 | A = sparse.csc_matrix([[1, 1], [1, 0], [0, 1]]) 10 | l = np.array([1, 0, 0]) 11 | u = np.array([1, 0.7, 0.7]) 12 | 13 | # Create an OSQP object 14 | prob = osqp.OSQP() 15 | 16 | # Setup workspace 17 | prob.setup(P, q, A, l, u) 18 | 19 | # Solve problem 20 | res = prob.solve() 21 | 22 | # Update problem 23 | q_new = np.array([2, 3]) 24 | l_new = np.array([2, -1, -1]) 25 | u_new = np.array([2, 2.5, 2.5]) 26 | prob.update(q=q_new, l=l_new, u=u_new) 27 | 28 | # Solve updated problem 29 | res = prob.solve() 30 | 31 | print('Status:', res.info.status) 32 | print('Objective value:', res.info.obj_val) 33 | print('Optimal solution x:', res.x) 34 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["scikit-build-core", "pybind11"] 3 | build-backend = "scikit_build_core.build" 4 | 5 | [project] 6 | name = "osqp" 7 | dynamic = ["version"] 8 | description = "OSQP: The Operator Splitting QP Solver" 9 | readme = "README.md" 10 | requires-python = ">=3.8" 11 | authors = [ 12 | { name = "Bartolomeo Stellato", email = "bartolomeo.stellato@gmail.com" }, 13 | { name = "Goran Banjac" }, 14 | { name = "Vineet Bansal", email = "vineetbansal@protonmail.com" }, 15 | { name = "Amit Solomon", email = "as3993@princeton.edu" }, 16 | { name = "Henry Schreiner", email = "HenrySchreinerIII@gmail.com" }, 17 | ] 18 | dependencies = [ 19 | "jinja2", 20 | "numpy>=1.7", 21 | "scipy>=0.13.2", 22 | "setuptools", 23 | "joblib", 24 | ] 25 | 26 | [project.urls] 27 | Homepage = "https://osqp.org/" 28 | 29 | [project.optional-dependencies] 30 | mkl = [ 31 | "osqp-mkl", 32 | ] 33 | # Include this `dev` extra till pip completely supports PEP735 34 | dev = [ 35 | "pre-commit", 36 | "pytest>=6", 37 | "torch", 38 | 39 | # Exclude scipy 1.12 because the random sparse array function started returning 40 | # the transpose of the original, breaking the unit tests. This was fixed in 1.13.0. 41 | # This shouldn't actually affect the users, so there shouldn't be a need to exclude 42 | # 1.12 on a user's machine. 43 | # ref: https://github.com/scipy/scipy/issues/20027 44 | "scipy!=1.12.0", 45 | ] 46 | cu12 = [ 47 | "osqp-cu12", 48 | ] 49 | 50 | [dependency-groups] 51 | # Exclude scipy 1.12 because the random sparse array function started returning 52 | # the transpose of the original, breaking the unit tests. This was fixed in 1.13.0. 53 | # This shouldn't actually affect the users, so there shouldn't be a need to exclude 54 | # 1.12 on a user's machine. 55 | # ref: https://github.com/scipy/scipy/issues/20027 56 | 57 | # Some newer platforms (e.g. cp313-macosx_x86_64 as of 03/13/25), do not 58 | # support installation of torch at all, which is why it is useful to have this 59 | # dependency group. 60 | test-no-nn = ["pytest>=6"] 61 | test = ["torch", "scipy!=1.12.0", { include-group = "test-no-nn" }] 62 | dev = ["pre-commit", { include-group = "test" }] 63 | 64 | [tool.scikit-build] 65 | install.components = ["python", "codegen"] 66 | metadata.version.provider = "scikit_build_core.metadata.setuptools_scm" 67 | minimum-version = "0.8" 68 | wheel.install-dir = "osqp" 69 | sdist.include = ["src/osqp/_version.py"] 70 | 71 | [tool.scikit-build.cmake.define] 72 | OSQP_ALGEBRA_BACKEND = "builtin" 73 | OSQP_EXT_MODULE_NAME = "ext_builtin" 74 | OSQP_ENABLE_INTERRUPT = {env="OSQP_ENABLE_INTERRUPT"} 75 | OSQP_CODEGEN = {env="OSQP_CODEGEN"} 76 | OSQP_BUILD_SHARED_LIB = {env="OSQP_BUILD_SHARED_LIB"} 77 | CMAKE_OSX_ARCHITECTURES = {env="CMAKE_OSX_ARCHITECTURES"} 78 | 79 | [tool.pytest.ini_options] 80 | testpaths = ["src/osqp/tests"] 81 | 82 | [tool.setuptools_scm] 83 | write_to = "src/osqp/_version.py" 84 | -------------------------------------------------------------------------------- /src/bindings.cpp.in: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | namespace py = pybind11; 6 | using namespace pybind11::literals; 7 | 8 | #include "osqp_api_functions.h" 9 | #include "osqp_api_types.h" 10 | 11 | class CSC { 12 | public: 13 | CSC(py::object A); 14 | ~CSC(); 15 | OSQPCscMatrix& getcsc() const; 16 | py::array_t _p; 17 | py::array_t _i; 18 | py::array_t _x; 19 | OSQPInt m; 20 | OSQPInt n; 21 | OSQPInt nz; 22 | OSQPInt nzmax; 23 | private: 24 | OSQPCscMatrix* _csc; 25 | }; 26 | 27 | CSC::CSC(py::object A) { 28 | py::object spa = py::module::import("scipy.sparse"); 29 | 30 | py::tuple dim = A.attr("shape"); 31 | int m = dim[0].cast(); 32 | int n = dim[1].cast(); 33 | 34 | if (!spa.attr("isspmatrix_csc")(A)) A = spa.attr("csc_matrix")(A); 35 | 36 | this->_p = A.attr("indptr").cast>(); 37 | this->_i = A.attr("indices").cast>(); 38 | this->_x = A.attr("data").cast>(); 39 | 40 | this->_csc = new OSQPCscMatrix(); 41 | this->_csc->m = m; 42 | this->_csc->n = n; 43 | this->_csc->p = (OSQPInt *)this->_p.data(); 44 | this->_csc->i = (OSQPInt *)this->_i.data(); 45 | this->_csc->x = (OSQPFloat *)this->_x.data(); 46 | this->_csc->nzmax = A.attr("nnz").cast(); 47 | this->_csc->nz = -1; 48 | 49 | this->m = this->_csc->m; 50 | this->n = this->_csc->n; 51 | this->nzmax = this->_csc->nzmax; 52 | this->nz = this->_csc->nz; 53 | } 54 | 55 | OSQPCscMatrix& CSC::getcsc() const { 56 | return *this->_csc; 57 | } 58 | 59 | CSC::~CSC() { 60 | delete this->_csc; 61 | } 62 | 63 | class PyOSQPSolution { 64 | public: 65 | PyOSQPSolution(OSQPSolution&, OSQPInt, OSQPInt); 66 | py::array_t get_x(); 67 | py::array_t get_y(); 68 | py::array_t get_prim_inf_cert(); 69 | py::array_t get_dual_inf_cert(); 70 | private: 71 | OSQPInt _m; 72 | OSQPInt _n; 73 | OSQPSolution& _solution; 74 | }; 75 | 76 | PyOSQPSolution::PyOSQPSolution(OSQPSolution& solution, OSQPInt m, OSQPInt n): _m(m), _n(n), _solution(solution) {} 77 | 78 | py::array_t PyOSQPSolution::get_x() { 79 | return py::array_t( 80 | { this->_n }, 81 | { sizeof(OSQPFloat) }, 82 | this->_solution.x); 83 | } 84 | 85 | py::array_t PyOSQPSolution::get_y() { 86 | return py::array_t( 87 | { this->_m }, 88 | { sizeof(OSQPFloat) }, 89 | this->_solution.y); 90 | } 91 | 92 | py::array_t PyOSQPSolution::get_prim_inf_cert() { 93 | return py::array_t( 94 | { this->_m }, 95 | { sizeof(OSQPFloat) }, 96 | this->_solution.prim_inf_cert); 97 | } 98 | 99 | py::array_t PyOSQPSolution::get_dual_inf_cert() { 100 | return py::array_t( 101 | { this->_n }, 102 | { sizeof(OSQPFloat) }, 103 | this->_solution.dual_inf_cert); 104 | } 105 | 106 | class PyOSQPSolver { 107 | public: 108 | PyOSQPSolver(const CSC&, const py::array_t, const CSC&, const py::array_t, const py::array_t, OSQPInt, OSQPInt, const OSQPSettings*); 109 | ~PyOSQPSolver(); 110 | 111 | OSQPSettings* get_settings(); 112 | PyOSQPSolution& get_solution(); 113 | OSQPInfo* get_info(); 114 | 115 | OSQPInt update_settings(const OSQPSettings&); 116 | OSQPInt update_rho(OSQPFloat); 117 | OSQPInt update_data_vec(py::object, py::object, py::object); 118 | OSQPInt update_data_mat(py::object, py::object, py::object, py::object); 119 | OSQPInt warm_start(py::object, py::object); 120 | OSQPInt solve(); 121 | OSQPInt adjoint_derivative_compute(py::object, py::object); 122 | OSQPInt adjoint_derivative_get_mat(CSC&, CSC&); 123 | OSQPInt adjoint_derivative_get_vec(py::object, py::object, py::object); 124 | 125 | OSQPInt codegen(const char*, const char*, OSQPCodegenDefines&); 126 | private: 127 | OSQPInt m; 128 | OSQPInt n; 129 | const CSC& _P; 130 | py::array_t _q; 131 | py::array_t _l; 132 | const CSC& _A; 133 | py::array_t _u; 134 | OSQPSolver *_solver; 135 | }; 136 | 137 | PyOSQPSolver::PyOSQPSolver( 138 | const CSC& P, 139 | const py::array_t q, 140 | const CSC& A, 141 | const py::array_t l, 142 | const py::array_t u, 143 | OSQPInt m, 144 | OSQPInt n, 145 | const OSQPSettings *settings 146 | ): m(m), n(n), _P(P), _A(A) { 147 | this->_solver = new OSQPSolver(); 148 | this->_q = q; 149 | this->_l = l; 150 | this->_u = u; 151 | 152 | OSQPInt status = osqp_setup(&this->_solver, &this->_P.getcsc(), (OSQPFloat *)this->_q.data(), &this->_A.getcsc(), (OSQPFloat *)this->_l.data(), (OSQPFloat *)this->_u.data(), m, n, settings); 153 | if (status) { 154 | throw py::value_error(std::to_string(status)); 155 | } 156 | } 157 | 158 | PyOSQPSolver::~PyOSQPSolver() { 159 | osqp_cleanup(this->_solver); 160 | } 161 | 162 | OSQPSettings* PyOSQPSolver::get_settings() { 163 | return this->_solver->settings; 164 | } 165 | 166 | PyOSQPSolution& PyOSQPSolver::get_solution() { 167 | PyOSQPSolution* solution = new PyOSQPSolution(*this->_solver->solution, this->m, this->n); 168 | return *solution; 169 | } 170 | 171 | OSQPInfo* PyOSQPSolver::get_info() { 172 | return this->_solver->info; 173 | } 174 | 175 | OSQPInt PyOSQPSolver::warm_start(py::object x, py::object y) { 176 | OSQPFloat* _x; 177 | OSQPFloat* _y; 178 | 179 | if (x.is_none()) { 180 | _x = NULL; 181 | } else { 182 | _x = (OSQPFloat *)py::array_t(x).data(); 183 | } 184 | if (y.is_none()) { 185 | _y = NULL; 186 | } else { 187 | _y = (OSQPFloat *)py::array_t(y).data(); 188 | } 189 | 190 | return osqp_warm_start(this->_solver, _x, _y); 191 | } 192 | 193 | OSQPInt PyOSQPSolver::solve() { 194 | py::gil_scoped_release release; 195 | OSQPInt results = osqp_solve(this->_solver); 196 | py::gil_scoped_acquire acquire; 197 | return results; 198 | } 199 | 200 | OSQPInt PyOSQPSolver::update_settings(const OSQPSettings& new_settings) { 201 | OSQPInt status = osqp_update_settings(this->_solver, &new_settings); 202 | if (status) { 203 | throw py::value_error(std::to_string(status)); 204 | } else { 205 | return status; 206 | } 207 | } 208 | 209 | OSQPInt PyOSQPSolver::update_rho(OSQPFloat rho_new) { 210 | return osqp_update_rho(this->_solver, rho_new); 211 | } 212 | 213 | OSQPInt PyOSQPSolver::update_data_vec(py::object q, py::object l, py::object u) { 214 | OSQPFloat* _q; 215 | OSQPFloat* _l; 216 | OSQPFloat* _u; 217 | 218 | if (q.is_none()) { 219 | _q = NULL; 220 | } else { 221 | _q = (OSQPFloat *)py::array_t(q).data(); 222 | } 223 | if (l.is_none()) { 224 | _l = NULL; 225 | } else { 226 | _l = (OSQPFloat *)py::array_t(l).data(); 227 | } 228 | if (u.is_none()) { 229 | _u = NULL; 230 | } else { 231 | _u = (OSQPFloat *)py::array_t(u).data(); 232 | } 233 | 234 | return osqp_update_data_vec(this->_solver, _q, _l, _u); 235 | } 236 | 237 | OSQPInt PyOSQPSolver::update_data_mat(py::object P_x, py::object P_i, py::object A_x, py::object A_i) { 238 | OSQPFloat* _P_x; 239 | OSQPInt* _P_i; 240 | OSQPInt _P_n = 0; 241 | OSQPFloat* _A_x; 242 | OSQPInt* _A_i; 243 | OSQPInt _A_n = 0; 244 | 245 | if (P_x.is_none()) { 246 | _P_x = NULL; 247 | } else { 248 | auto _P_x_array = py::array_t(P_x); 249 | _P_x = (OSQPFloat *)_P_x_array.data(); 250 | _P_n = _P_x_array.size(); 251 | } 252 | 253 | if (P_i.is_none()) { 254 | _P_i = NULL; 255 | } else { 256 | auto _P_i_array = py::array_t(P_i); 257 | _P_i = (OSQPInt *)_P_i_array.data(); 258 | _P_n = _P_i_array.size(); 259 | } 260 | 261 | if (A_x.is_none()) { 262 | _A_x = NULL; 263 | } else { 264 | auto _A_x_array = py::array_t(A_x); 265 | _A_x = (OSQPFloat *)_A_x_array.data(); 266 | _A_n = _A_x_array.size(); 267 | } 268 | 269 | if (A_i.is_none()) { 270 | _A_i = NULL; 271 | } else { 272 | auto _A_i_array = py::array_t(A_i); 273 | _A_i = (OSQPInt *)_A_i_array.data(); 274 | _A_n = _A_i_array.size(); 275 | } 276 | 277 | return osqp_update_data_mat(this->_solver, _P_x, _P_i, _P_n, _A_x, _A_i, _A_n); 278 | } 279 | 280 | OSQPInt PyOSQPSolver::adjoint_derivative_compute(const py::object dx, const py::object dy) { 281 | OSQPFloat* _dx; 282 | OSQPFloat* _dy; 283 | 284 | if (dx.is_none()) { 285 | _dx = NULL; 286 | } else { 287 | auto _dx_array = py::array_t(dx); 288 | _dx = (OSQPFloat *)_dx_array.data(); 289 | } 290 | 291 | if (dy.is_none()) { 292 | _dy = NULL; 293 | } else { 294 | auto _dy_array = py::array_t(dy); 295 | _dy = (OSQPFloat *)_dy_array.data(); 296 | } 297 | 298 | 299 | return osqp_adjoint_derivative_compute(this->_solver, _dx, _dy); 300 | 301 | } 302 | 303 | OSQPInt PyOSQPSolver::adjoint_derivative_get_mat(CSC& dP, CSC& dA) { 304 | OSQPCscMatrix& _dP = dP.getcsc(); 305 | OSQPCscMatrix& _dA = dA.getcsc(); 306 | 307 | return osqp_adjoint_derivative_get_mat(this->_solver, &_dP, &_dA); 308 | } 309 | 310 | OSQPInt PyOSQPSolver::adjoint_derivative_get_vec(py::object dq, py::object dl, py::object du) { 311 | OSQPFloat* _dq = (OSQPFloat *)py::array_t(dq).data(); 312 | OSQPFloat* _dl = (OSQPFloat *)py::array_t(dl).data(); 313 | OSQPFloat* _du = (OSQPFloat *)py::array_t(du).data(); 314 | 315 | return osqp_adjoint_derivative_get_vec(this->_solver, _dq, _dl, _du); 316 | } 317 | 318 | OSQPInt PyOSQPSolver::codegen(const char *output_dir, const char *file_prefix, OSQPCodegenDefines& defines) { 319 | return osqp_codegen(this->_solver, output_dir, file_prefix, &defines); 320 | } 321 | 322 | PYBIND11_MODULE(@OSQP_EXT_MODULE_NAME@, m) { 323 | 324 | #ifdef OSQP_USE_FLOAT 325 | m.attr("OSQP_USE_FLOAT") = 1; 326 | #else 327 | m.attr("OSQP_USE_FLOAT") = 0; 328 | #endif 329 | 330 | #ifdef OSQP_USE_LONG 331 | m.attr("OSQP_USE_LONG") = 1; 332 | #else 333 | m.attr("OSQP_USE_LONG") = 0; 334 | #endif 335 | 336 | // Any constants that we wish to make directly accessible in the extension module 337 | m.attr("OSQP_INFTY") = OSQP_INFTY; 338 | 339 | // Enum values that are directly accessible 340 | py::enum_(m, "osqp_linsys_solver_type", py::module_local()) 341 | .value("OSQP_DIRECT_SOLVER", OSQP_DIRECT_SOLVER) 342 | .value("OSQP_INDIRECT_SOLVER", OSQP_INDIRECT_SOLVER) 343 | .export_values(); 344 | 345 | // Enum values that are directly accessible 346 | py::enum_(m, "osqp_status_type", py::module_local()) 347 | .value("OSQP_SOLVED", OSQP_SOLVED) 348 | .value("OSQP_SOLVED_INACCURATE", OSQP_SOLVED_INACCURATE) 349 | .value("OSQP_PRIMAL_INFEASIBLE", OSQP_PRIMAL_INFEASIBLE) 350 | .value("OSQP_PRIMAL_INFEASIBLE_INACCURATE", OSQP_PRIMAL_INFEASIBLE_INACCURATE) 351 | .value("OSQP_DUAL_INFEASIBLE", OSQP_DUAL_INFEASIBLE) 352 | .value("OSQP_DUAL_INFEASIBLE_INACCURATE", OSQP_DUAL_INFEASIBLE_INACCURATE) 353 | .value("OSQP_MAX_ITER_REACHED", OSQP_MAX_ITER_REACHED) 354 | .value("OSQP_TIME_LIMIT_REACHED", OSQP_TIME_LIMIT_REACHED) 355 | .value("OSQP_NON_CVX", OSQP_NON_CVX) 356 | .value("OSQP_SIGINT", OSQP_SIGINT) 357 | .value("OSQP_UNSOLVED", OSQP_UNSOLVED) 358 | .export_values(); 359 | 360 | // Solver Errors 361 | py::enum_(m, "osqp_error_type", py::module_local()) 362 | .value("OSQP_NO_ERROR", OSQP_NO_ERROR) 363 | .value("OSQP_DATA_VALIDATION_ERROR", OSQP_DATA_VALIDATION_ERROR) 364 | .value("OSQP_SETTINGS_VALIDATION_ERROR", OSQP_SETTINGS_VALIDATION_ERROR) 365 | .value("OSQP_LINSYS_SOLVER_INIT_ERROR", OSQP_LINSYS_SOLVER_INIT_ERROR) 366 | .value("OSQP_NONCVX_ERROR", OSQP_NONCVX_ERROR) 367 | .value("OSQP_MEM_ALLOC_ERROR", OSQP_MEM_ALLOC_ERROR) 368 | .value("OSQP_WORKSPACE_NOT_INIT_ERROR", OSQP_WORKSPACE_NOT_INIT_ERROR) 369 | .value("OSQP_ALGEBRA_LOAD_ERROR", OSQP_ALGEBRA_LOAD_ERROR) 370 | .value("OSQP_CODEGEN_DEFINES_ERROR", OSQP_CODEGEN_DEFINES_ERROR) 371 | .value("OSQP_DATA_NOT_INITIALIZED", OSQP_DATA_NOT_INITIALIZED) 372 | .value("OSQP_FUNC_NOT_IMPLEMENTED", OSQP_FUNC_NOT_IMPLEMENTED); 373 | 374 | // Preconditioner Type 375 | py::enum_(m, "osqp_precond_type", py::module_local()) 376 | .value("OSQP_NO_PRECONDITIONER", OSQP_NO_PRECONDITIONER) 377 | .value("OSQP_DIAGONAL_PRECONDITIONER", OSQP_DIAGONAL_PRECONDITIONER) 378 | .export_values(); 379 | 380 | // CSC 381 | py::class_(m, "CSC", py::module_local()) 382 | .def(py::init()) 383 | .def_readonly("m", &CSC::m) 384 | .def_readonly("n", &CSC::n) 385 | .def_readonly("p", &CSC::_p) 386 | .def_readonly("i", &CSC::_i) 387 | .def_readonly("x", &CSC::_x) 388 | .def_readonly("nzmax", &CSC::nzmax) 389 | .def_readonly("nz", &CSC::nz); 390 | 391 | // Capabilities 392 | py::enum_(m, "osqp_capabilities_type", py::module_local()) 393 | .value("OSQP_CAPABILITY_DIRECT_SOLVER", OSQP_CAPABILITY_DIRECT_SOLVER) 394 | .value("OSQP_CAPABILITY_INDIRECT_SOLVER", OSQP_CAPABILITY_INDIRECT_SOLVER) 395 | .value("OSQP_CAPABILITY_CODEGEN", OSQP_CAPABILITY_CODEGEN) 396 | .value("OSQP_CAPABILITY_UPDATE_MATRICES", OSQP_CAPABILITY_UPDATE_MATRICES) 397 | .value("OSQP_CAPABILITY_DERIVATIVES", OSQP_CAPABILITY_DERIVATIVES); 398 | 399 | m.def("osqp_capabilities", &osqp_capabilities); 400 | 401 | // Settings 402 | py::class_(m, "OSQPSettings", py::module_local()) 403 | .def(py::init([]() { 404 | return new OSQPSettings(); 405 | })) 406 | .def_readwrite("device", &OSQPSettings::device) 407 | .def_readwrite("linsys_solver", &OSQPSettings::linsys_solver) 408 | .def_readwrite("verbose", &OSQPSettings::verbose) 409 | .def_readwrite("warm_starting", &OSQPSettings::warm_starting) 410 | .def_readwrite("scaling", &OSQPSettings::scaling) 411 | .def_readwrite("polishing", &OSQPSettings::polishing) 412 | 413 | // Settings - ADMM 414 | .def_readwrite("rho", &OSQPSettings::rho) 415 | .def_readwrite("rho_is_vec", &OSQPSettings::rho_is_vec) 416 | .def_readwrite("sigma", &OSQPSettings::sigma) 417 | .def_readwrite("alpha", &OSQPSettings::alpha) 418 | 419 | // Settings - CG 420 | .def_readwrite("cg_max_iter", &OSQPSettings::cg_max_iter) 421 | .def_readwrite("cg_tol_reduction", &OSQPSettings::cg_tol_reduction) 422 | .def_readwrite("cg_tol_fraction", &OSQPSettings::cg_tol_fraction) 423 | .def_readwrite("cg_precond", &OSQPSettings::cg_precond) 424 | 425 | // Settings - Adaptive rho 426 | .def_readwrite("adaptive_rho", &OSQPSettings::adaptive_rho) 427 | .def_readwrite("adaptive_rho_interval", &OSQPSettings::adaptive_rho_interval) 428 | .def_readwrite("adaptive_rho_fraction", &OSQPSettings::adaptive_rho_fraction) 429 | .def_readwrite("adaptive_rho_tolerance", &OSQPSettings::adaptive_rho_tolerance) 430 | 431 | // Settings - Termination parameters 432 | .def_readwrite("max_iter", &OSQPSettings::max_iter) 433 | .def_readwrite("eps_abs", &OSQPSettings::eps_abs) 434 | .def_readwrite("eps_rel", &OSQPSettings::eps_rel) 435 | .def_readwrite("eps_prim_inf", &OSQPSettings::eps_prim_inf) 436 | .def_readwrite("eps_dual_inf", &OSQPSettings::eps_dual_inf) 437 | .def_readwrite("scaled_termination", &OSQPSettings::scaled_termination) 438 | .def_readwrite("check_termination", &OSQPSettings::check_termination) 439 | .def_readwrite("time_limit", &OSQPSettings::time_limit) 440 | 441 | // Settings - Polishing 442 | .def_readwrite("delta", &OSQPSettings::delta) 443 | .def_readwrite("polish_refine_iter", &OSQPSettings::polish_refine_iter); 444 | 445 | m.def("osqp_set_default_settings", &osqp_set_default_settings); 446 | 447 | // Codegen Defines 448 | py::class_(m, "OSQPCodegenDefines", py::module_local()) 449 | .def(py::init([]() { 450 | return new OSQPCodegenDefines(); 451 | })) 452 | .def_readwrite("embedded_mode", &OSQPCodegenDefines::embedded_mode) 453 | .def_readwrite("float_type", &OSQPCodegenDefines::float_type) 454 | .def_readwrite("printing_enable", &OSQPCodegenDefines::printing_enable) 455 | .def_readwrite("profiling_enable", &OSQPCodegenDefines::profiling_enable) 456 | .def_readwrite("interrupt_enable", &OSQPCodegenDefines::interrupt_enable) 457 | .def_readwrite("derivatives_enable", &OSQPCodegenDefines::derivatives_enable); 458 | 459 | m.def("osqp_set_default_codegen_defines", &osqp_set_default_codegen_defines); 460 | 461 | // Solution 462 | py::class_(m, "OSQPSolution", py::module_local()) 463 | .def_property_readonly("x", &PyOSQPSolution::get_x) 464 | .def_property_readonly("y", &PyOSQPSolution::get_y) 465 | .def_property_readonly("prim_inf_cert", &PyOSQPSolution::get_prim_inf_cert) 466 | .def_property_readonly("dual_inf_cert", &PyOSQPSolution::get_dual_inf_cert); 467 | 468 | // Info 469 | py::class_(m, "OSQPInfo", py::module_local()) 470 | .def_readonly("status", &OSQPInfo::status) 471 | .def_readonly("status_val", &OSQPInfo::status_val) 472 | .def_readonly("status_polish", &OSQPInfo::status_polish) 473 | // obj_val is readwrite because Python wrappers may overwrite this value based on status_val 474 | .def_readwrite("obj_val", &OSQPInfo::obj_val) 475 | .def_readonly("prim_res", &OSQPInfo::prim_res) 476 | .def_readonly("dual_res", &OSQPInfo::dual_res) 477 | .def_readonly("iter", &OSQPInfo::iter) 478 | .def_readonly("rho_updates", &OSQPInfo::rho_updates) 479 | .def_readonly("rho_estimate", &OSQPInfo::rho_estimate) 480 | .def_readonly("setup_time", &OSQPInfo::setup_time) 481 | .def_readonly("solve_time", &OSQPInfo::solve_time) 482 | .def_readonly("update_time", &OSQPInfo::update_time) 483 | .def_readonly("polish_time", &OSQPInfo::polish_time) 484 | .def_readonly("run_time", &OSQPInfo::run_time); 485 | 486 | // Solver 487 | py::class_(m, "OSQPSolver", py::module_local()) 488 | .def(py::init, const CSC&, const py::array_t, const py::array_t, OSQPInt, OSQPInt, const OSQPSettings*>(), 489 | "P"_a, "q"_a.noconvert(), "A"_a, "l"_a.noconvert(), "u"_a.noconvert(), "m"_a, "n"_a, "settings"_a) 490 | .def_property_readonly("solution", &PyOSQPSolver::get_solution, py::return_value_policy::reference) 491 | .def_property_readonly("info", &PyOSQPSolver::get_info) 492 | .def("warm_start", &PyOSQPSolver::warm_start, "x"_a.none(true), "y"_a.none(true)) 493 | .def("solve", &PyOSQPSolver::solve) 494 | .def("update_data_vec", &PyOSQPSolver::update_data_vec, "q"_a.none(true), "l"_a.none(true), "u"_a.none(true)) 495 | .def("update_data_mat", &PyOSQPSolver::update_data_mat, "P_x"_a.none(true), "P_i"_a.none(true), "A_x"_a.none(true), "A_i"_a.none(true)) 496 | .def("update_settings", &PyOSQPSolver::update_settings) 497 | .def("update_rho", &PyOSQPSolver::update_rho) 498 | .def("get_settings", &PyOSQPSolver::get_settings, py::return_value_policy::reference) 499 | 500 | .def("adjoint_derivative_compute", &PyOSQPSolver::adjoint_derivative_compute, "dx"_a.none(true), "dy"_a.none(true)) 501 | .def("adjoint_derivative_get_mat", &PyOSQPSolver::adjoint_derivative_get_mat, "dP"_a, "dA"_a) 502 | .def("adjoint_derivative_get_vec", &PyOSQPSolver::adjoint_derivative_get_vec, "dq"_a, "dl"_a, "du"_a) 503 | 504 | .def("codegen", &PyOSQPSolver::codegen, "output_dir"_a, "file_prefix"_a, "defines"_a); 505 | 506 | } 507 | -------------------------------------------------------------------------------- /src/osqp/__init__.py: -------------------------------------------------------------------------------- 1 | # The _version.py file is managed by setuptools-scm 2 | # and is not in version control. 3 | from osqp._version import version as __version__ # noqa: F401 4 | from osqp.interface import ( # noqa: F401 5 | OSQPException, 6 | OSQP, 7 | constant, 8 | algebra_available, 9 | algebras_available, 10 | default_algebra, 11 | SolverStatus, 12 | SolverError, 13 | ) 14 | -------------------------------------------------------------------------------- /src/osqp/builtin.py: -------------------------------------------------------------------------------- 1 | from osqp.interface import OSQP as _OSQP 2 | 3 | 4 | class OSQP(_OSQP): 5 | def __init__(self, *args, **kwargs): 6 | super(OSQP, self).__init__(*args, **kwargs, algebra='builtin') 7 | -------------------------------------------------------------------------------- /src/osqp/codegen/__init__.py: -------------------------------------------------------------------------------- 1 | # This module is populated with codegen src/header files at build time. 2 | -------------------------------------------------------------------------------- /src/osqp/codegen/pywrapper/CMakeLists.txt.jinja: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | project(osqp_ext_{{extension_name}}) 3 | 4 | set(CMAKE_VERBOSE_MAKEFILE ON) 5 | set(CMAKE_POSITION_INDEPENDENT_CODE ON) # -fPIC 6 | set(OSQP_EMBEDDED_MODE "1") 7 | 8 | file( 9 | GLOB 10 | OSQP_SOURCES 11 | src/*.c) 12 | 13 | add_library(osqpstatic 14 | STATIC 15 | ${OSQP_SOURCES} 16 | ) 17 | 18 | target_include_directories(osqpstatic PUBLIC inc/public inc/private .) 19 | target_include_directories(osqpstatic PRIVATE inc/private .) 20 | 21 | include(FetchContent) 22 | FetchContent_Declare( 23 | pybind11 24 | GIT_REPOSITORY https://github.com/pybind/pybind11.git) 25 | FetchContent_MakeAvailable(pybind11) 26 | 27 | file( 28 | GLOB 29 | EXT_SOURCES 30 | {{prefix}}workspace.c 31 | {{prefix}}workspace.h 32 | bindings.cpp) 33 | 34 | pybind11_add_module({{extension_name}} ${OSQP_SOURCES} ${EXT_SOURCES}) 35 | target_link_libraries({{extension_name}} PUBLIC pybind11::module osqpstatic) 36 | -------------------------------------------------------------------------------- /src/osqp/codegen/pywrapper/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/codegen/pywrapper/__init__.py -------------------------------------------------------------------------------- /src/osqp/codegen/pywrapper/bindings.cpp.jinja: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | namespace py = pybind11; 6 | using namespace pybind11::literals; 7 | 8 | #include "osqp_api_functions.h" 9 | #include "osqp_api_types.h" 10 | #include "{{prefix}}workspace.h" 11 | 12 | py::tuple solve() { 13 | py::gil_scoped_release release; 14 | OSQPInt status = osqp_solve(&{{prefix}}solver); 15 | py::gil_scoped_acquire acquire; 16 | 17 | if (status != 0) throw std::runtime_error("Solve failed"); 18 | 19 | OSQPInt m; 20 | OSQPInt n; 21 | osqp_get_dimensions(&{{prefix}}solver, &m, &n); 22 | 23 | auto x = py::array_t({n}, {sizeof(OSQPFloat)}, (&{{prefix}}solver)->solution->x); 24 | auto y = py::array_t({m}, {sizeof(OSQPFloat)}, (&{{prefix}}solver)->solution->y); 25 | 26 | py::tuple results = py::make_tuple(x, y, status, (&{{prefix}}solver)->info->iter, (&{{prefix}}solver)->info->run_time); 27 | return results; 28 | } 29 | 30 | OSQPInt update_data_vec(py::object q, py::object l, py::object u) { 31 | OSQPFloat* _q; 32 | OSQPFloat* _l; 33 | OSQPFloat* _u; 34 | 35 | if (q.is_none()) { 36 | _q = NULL; 37 | } else { 38 | _q = (OSQPFloat *)py::array_t(q).data(); 39 | } 40 | if (l.is_none()) { 41 | _l = NULL; 42 | } else { 43 | _l = (OSQPFloat *)py::array_t(l).data(); 44 | } 45 | if (u.is_none()) { 46 | _u = NULL; 47 | } else { 48 | _u = (OSQPFloat *)py::array_t(u).data(); 49 | } 50 | 51 | return osqp_update_data_vec(&{{prefix}}solver, _q, _l, _u); 52 | } 53 | 54 | #if OSQP_EMBEDDED_MODE == 2 55 | OSQPInt update_data_mat(py::object P_x, py::object P_i, py::object A_x, py::object A_i) { 56 | OSQPFloat* _P_x; 57 | OSQPInt* _P_i; 58 | OSQPInt _P_n = 0; 59 | OSQPFloat* _A_x; 60 | OSQPInt* _A_i; 61 | OSQPInt _A_n = 0; 62 | 63 | if (P_x.is_none()) { 64 | _P_x = NULL; 65 | } else { 66 | auto _P_x_array = py::array_t(P_x); 67 | _P_x = (OSQPFloat *)_P_x_array.data(); 68 | _P_n = _P_x_array.size(); 69 | } 70 | 71 | if (P_i.is_none()) { 72 | _P_i = NULL; 73 | } else { 74 | auto _P_i_array = py::array_t(P_i); 75 | _P_i = (OSQPInt *)_P_i_array.data(); 76 | _P_n = _P_i_array.size(); 77 | } 78 | 79 | if (A_x.is_none()) { 80 | _A_x = NULL; 81 | } else { 82 | auto _A_x_array = py::array_t(A_x); 83 | _A_x = (OSQPFloat *)_A_x_array.data(); 84 | _A_n = _A_x_array.size(); 85 | } 86 | 87 | if (A_i.is_none()) { 88 | _A_i = NULL; 89 | } else { 90 | auto _A_i_array = py::array_t(A_i); 91 | _A_i = (OSQPInt *)_A_i_array.data(); 92 | _A_n = _A_i_array.size(); 93 | } 94 | 95 | return osqp_update_data_mat(&{{prefix}}solver, _P_x, _P_i, _P_n, _A_x, _A_i, _A_n); 96 | } 97 | #endif 98 | 99 | PYBIND11_MODULE({{extension_name}}, m) { 100 | m.def("solve", &solve); 101 | m.def("update_data_vec", &update_data_vec, "Update q/l/u", py::arg("q") = py::none(), py::arg("l") = py::none(), py::arg("u") = py::none()); 102 | #if OSQP_EMBEDDED_MODE == 2 103 | m.def("update_data_mat", &update_data_mat, "Update P/A", py::arg("P_x") = py::none(), py::arg("P_i") = py::none(), py::arg("A_x") = py::none(), py::arg("A_i") = py::none()); 104 | #endif 105 | } 106 | -------------------------------------------------------------------------------- /src/osqp/codegen/pywrapper/setup.py.jinja: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import sys 4 | from platform import system 5 | from subprocess import check_call 6 | 7 | from setuptools import setup, Extension 8 | from setuptools.command.build_ext import build_ext 9 | 10 | 11 | class CMakeExtension(Extension): 12 | def __init__(self, name, cmake_args=None): 13 | Extension.__init__(self, name, sources=[]) 14 | self.cmake_args = cmake_args 15 | 16 | 17 | class CmdCMakeBuild(build_ext): 18 | def build_extension(self, ext): 19 | extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name))) 20 | this_dir = os.path.abspath(os.path.dirname(__file__)) 21 | cmake_args = [ 22 | f'-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={extdir}', 23 | f'-DPYTHON_EXECUTABLE={sys.executable}', 24 | ] 25 | 26 | build_args = [] 27 | cfg = 'Debug' if self.debug else 'Release' 28 | 29 | if system() != "Darwin": 30 | build_args += [f'--config={cfg}'] 31 | 32 | if system() == "Windows": 33 | cmake_args += ['-G', 'Visual Studio 17 2022'] 34 | cmake_args += ['-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{}={}'.format(cfg.upper(), extdir)] 35 | if sys.maxsize > 2 ** 32: 36 | cmake_args += ['-A', 'x64'] 37 | build_args += ['--', '/m'] 38 | else: 39 | cmake_args += ['-DCMAKE_BUILD_TYPE=' + cfg] 40 | build_args += ['--', '-j2'] 41 | 42 | if os.path.exists(self.build_temp): 43 | shutil.rmtree(self.build_temp) 44 | os.makedirs(self.build_temp) 45 | 46 | if ext.cmake_args is not None: 47 | cmake_args.extend(ext.cmake_args) 48 | 49 | check_call(['cmake', this_dir] + cmake_args, cwd=self.build_temp) 50 | check_call(['cmake', '--build', '.'] + build_args, cwd=self.build_temp) 51 | 52 | 53 | setup( 54 | name='{{extension_name}}', 55 | author='Bartolomeo Stellato, Goran Banjac', 56 | author_email='bartolomeo.stellato@gmail.com', 57 | description='OSQP: The Operator Splitting QP Solver', 58 | license='Apache 2.0', 59 | url="https://osqp.org/", 60 | 61 | python_requires='>=3.8', 62 | setup_requires=["numpy >= 1.7"], 63 | install_requires=['numpy >= 1.7'], 64 | 65 | ext_modules=[CMakeExtension('{{extension_name}}', cmake_args=['-DOSQP_EMBEDDED_MODE={{embedded_mode}}'])], 66 | cmdclass={'build_ext': CmdCMakeBuild}, 67 | zip_safe=False 68 | ) 69 | -------------------------------------------------------------------------------- /src/osqp/cuda.py: -------------------------------------------------------------------------------- 1 | from osqp.interface import OSQP as _OSQP 2 | 3 | 4 | class OSQP(_OSQP): 5 | def __init__(self, *args, **kwargs): 6 | super(OSQP, self).__init__(*args, **kwargs, algebra='cuda') 7 | -------------------------------------------------------------------------------- /src/osqp/interface.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | from types import SimpleNamespace 4 | from enum import IntEnum 5 | import shutil 6 | import subprocess 7 | import warnings 8 | import importlib 9 | import importlib.resources 10 | import numpy as np 11 | import scipy.sparse as spa 12 | from jinja2 import Environment, PackageLoader, select_autoescape 13 | 14 | _ALGEBRAS = ( 15 | 'cuda', 16 | 'mkl', 17 | 'builtin', 18 | ) # Highest->Lowest priority of algebras that are tried in turn 19 | # Mapping from algebra to loadable module 20 | _ALGEBRA_MODULES = { 21 | 'cuda': 'osqp_cuda', 22 | 'mkl': 'osqp_mkl', 23 | 'builtin': 'osqp.ext_builtin', 24 | } 25 | OSQP_ALGEBRA_BACKEND = os.environ.get('OSQP_ALGEBRA_BACKEND') # If envvar is set, that algebra is used by default 26 | 27 | 28 | def algebra_available(algebra): 29 | assert algebra in _ALGEBRAS, f'Unknown algebra {algebra}' 30 | module = _ALGEBRA_MODULES[algebra] 31 | 32 | try: 33 | importlib.import_module(module) 34 | except ImportError: 35 | return False 36 | else: 37 | return True 38 | 39 | 40 | def algebras_available(): 41 | return [algebra for algebra in _ALGEBRAS if algebra_available(algebra)] 42 | 43 | 44 | def default_algebra(): 45 | if OSQP_ALGEBRA_BACKEND is not None: 46 | return OSQP_ALGEBRA_BACKEND 47 | for algebra in _ALGEBRAS: 48 | if algebra_available(algebra): 49 | return algebra 50 | raise RuntimeError('No algebra backend available!') 51 | 52 | 53 | def default_algebra_module(): 54 | """ 55 | Get the default algebra module. 56 | Note: importlib.import_module is cached so we pay almost no penalty 57 | for repeated calls to this function. 58 | """ 59 | return importlib.import_module(_ALGEBRA_MODULES[default_algebra()]) 60 | 61 | 62 | def constant(which, algebra='builtin'): 63 | """ 64 | Get a named constant from the extension module. 65 | Since constants are typically consistent across osqp algebras, 66 | we use the `builtin` algebra (always guaranteed to be available) 67 | by default. 68 | """ 69 | m = importlib.import_module(_ALGEBRA_MODULES[algebra]) 70 | _constant = getattr(m, which, None) 71 | 72 | if which in m.osqp_status_type.__members__: 73 | warnings.warn( 74 | 'Direct access to osqp status values will be deprecated. Please use the SolverStatus enum instead.', 75 | PendingDeprecationWarning, 76 | ) 77 | 78 | # If the constant was exported directly as an atomic type in the extension, use it; 79 | # Otherwise it's an enum out of which we can obtain the raw value 80 | if isinstance(_constant, (int, float, str)): 81 | return _constant 82 | elif _constant is not None: 83 | return _constant.value 84 | else: 85 | # Handle special cases 86 | if which == 'OSQP_NAN': 87 | return np.nan 88 | 89 | raise RuntimeError(f'Unknown constant {which}') 90 | 91 | 92 | def construct_enum(name, binding_enum_name): 93 | """ 94 | Dynamically construct an IntEnum from available enum members. 95 | For all values, see https://osqp.org/docs/interfaces/status_values.html 96 | """ 97 | m = default_algebra_module() 98 | binding_enum = getattr(m, binding_enum_name) 99 | return IntEnum(name, [(v.name, v.value) for v in binding_enum.__members__.values()]) 100 | 101 | 102 | SolverStatus = construct_enum('SolverStatus', 'osqp_status_type') 103 | SolverError = construct_enum('SolverError', 'osqp_error_type') 104 | 105 | 106 | class OSQPException(Exception): 107 | """ 108 | OSQPException is raised by the wrapper interface when it encounters an 109 | exception by the underlying OSQP solver. 110 | """ 111 | 112 | def __init__(self, error_code=None): 113 | if error_code: 114 | self.args = (error_code,) 115 | 116 | def __eq__(self, error_code): 117 | return len(self.args) > 0 and self.args[0] == error_code 118 | 119 | 120 | class OSQP: 121 | 122 | """ 123 | For OSQP bindings (see bindings.cpp.in) that throw `ValueError`s 124 | (through `throw py::value_error(...)`), we catch and re-raise them 125 | as `OSQPException`s, with the correct int value as args[0]. 126 | """ 127 | 128 | @classmethod 129 | def raises_error(cls, fn, *args, **kwargs): 130 | try: 131 | return_value = fn(*args, **kwargs) 132 | except ValueError as e: 133 | if e.args: 134 | error_code = None 135 | try: 136 | error_code = int(e.args[0]) 137 | except ValueError: 138 | pass 139 | raise OSQPException(error_code) 140 | else: 141 | return return_value 142 | 143 | def __init__(self, *args, **kwargs): 144 | self.m = None 145 | self.n = None 146 | 147 | self.algebra = kwargs.pop('algebra') if 'algebra' in kwargs else default_algebra() 148 | if not algebra_available(self.algebra): 149 | raise RuntimeError(f'Algebra {self.algebra} not available') 150 | self.ext = importlib.import_module(_ALGEBRA_MODULES[self.algebra]) 151 | 152 | self._dtype = np.float32 if self.ext.OSQP_USE_FLOAT == 1 else np.float64 153 | self._itype = np.int64 if self.ext.OSQP_USE_LONG == 1 else np.int32 154 | 155 | # The following attributes are populated on setup() 156 | self._solver = None 157 | self._derivative_cache = {} 158 | 159 | def __str__(self): 160 | if self._solver is None: 161 | return f'Uninitialized OSQP with algebra={self.algebra}' 162 | else: 163 | return f'OSQP with algebra={self.algebra} ({self.solver_type})' 164 | 165 | def _infer_mnpqalu(self, P=None, q=None, A=None, l=None, u=None): 166 | # infer as many parameters of the problems as we can, and return them as a tuple 167 | if P is None: 168 | if q is not None: 169 | n = len(q) 170 | elif A is not None: 171 | n = A.shape[1] 172 | else: 173 | raise ValueError('The problem does not have any variables') 174 | else: 175 | n = P.shape[0] 176 | 177 | m = 0 if A is None else A.shape[0] 178 | 179 | if A is None: 180 | assert (l is None) and (u is None), 'If A is unspecified, leave l/u unspecified too.' 181 | else: 182 | assert (l is not None) or (u is not None), 'If A is specified, specify at least one of l/u.' 183 | if l is None: 184 | l = -np.inf * np.ones(A.shape[0]) 185 | if u is None: 186 | u = np.inf * np.ones(A.shape[0]) 187 | 188 | if P is None: 189 | P = spa.csc_matrix( 190 | ( 191 | np.zeros((0,), dtype=self._dtype), # data 192 | np.zeros((0,), dtype=self._itype), # indices 193 | np.zeros((n + 1,), dtype=self._itype), 194 | ), # indptr 195 | shape=(n, n), 196 | ) 197 | if q is None: 198 | q = np.zeros(n) 199 | 200 | if A is None: 201 | A = spa.csc_matrix( 202 | ( 203 | np.zeros((0,), dtype=self._dtype), # data 204 | np.zeros((0,), dtype=self._itype), # indices 205 | np.zeros((n + 1,), dtype=self._itype), 206 | ), # indptr 207 | shape=(m, n), 208 | ) 209 | l = np.zeros(A.shape[0]) 210 | u = np.zeros(A.shape[0]) 211 | 212 | assert len(q) == n, 'Incorrect dimension of q' 213 | assert len(l) == m, 'Incorrect dimension of l' 214 | assert len(u) == m, 'Incorrect dimension of u' 215 | 216 | if not spa.issparse(P) and isinstance(P, np.ndarray) and P.ndim == 2: 217 | raise TypeError('P is required to be a sparse matrix') 218 | if not spa.issparse(A) and isinstance(A, np.ndarray) and A.ndim == 2: 219 | raise TypeError('A is required to be a sparse matrix') 220 | 221 | if spa.tril(P, -1).data.size > 0: 222 | P = spa.triu(P, format='csc') 223 | 224 | # Convert matrices in CSC form to individual pointers 225 | if not spa.isspmatrix_csc(P): 226 | warnings.warn('Converting sparse P to a CSC matrix. This may take a while...') 227 | P = P.tocsc() 228 | if not spa.isspmatrix_csc(A): 229 | warnings.warn('Converting sparse A to a CSC matrix. This may take a while...') 230 | A = A.tocsc() 231 | 232 | if not P.has_sorted_indices: 233 | P.sort_indices() 234 | if not A.has_sorted_indices: 235 | A.sort_indices() 236 | 237 | u = np.minimum(u, self.constant('OSQP_INFTY')) 238 | l = np.maximum(l, -self.constant('OSQP_INFTY')) 239 | 240 | return m, n, P, q, A, l, u 241 | 242 | @property 243 | def capabilities(self): 244 | return int(self.ext.osqp_capabilities()) 245 | 246 | def has_capability(self, capability: str): 247 | try: 248 | cap = int(self.ext.osqp_capabilities_type.__members__[capability]) 249 | except KeyError: 250 | raise RuntimeError(f'Unrecognized capability {capability}') 251 | 252 | return (self.capabilities & cap) != 0 253 | 254 | @property 255 | def solver_type(self): 256 | return ( 257 | 'direct' 258 | if self.settings.linsys_solver == self.ext.osqp_linsys_solver_type.OSQP_DIRECT_SOLVER 259 | else 'indirect' 260 | ) 261 | 262 | @property 263 | def cg_preconditioner(self): 264 | return 'diagonal' if self.settings.cg_precond == self.ext.OSQP_DIAGONAL_PRECONDITIONER else None 265 | 266 | def _as_dense(self, m): 267 | assert isinstance(m, self.ext.CSC) 268 | _m_csc = spa.csc_matrix((m.x, m.i, m.p)) 269 | return np.array(_m_csc.todense()) 270 | 271 | def _csc_triu_as_csc_full(self, m): 272 | _m_triu_dense = self._as_dense(m) 273 | _m_full_dense = np.tril(_m_triu_dense.T, -1) + _m_triu_dense 274 | _m_full_csc = spa.csc_matrix(_m_full_dense) 275 | return self.ext.CSC(_m_full_csc) 276 | 277 | def constant(self, which): 278 | return constant(which, algebra=self.algebra) 279 | 280 | def update_settings(self, **kwargs): 281 | assert self.settings is not None 282 | 283 | # Some setting names have changed. Support the old names for now, but warn the caller. 284 | renamed_settings = { 285 | 'polish': 'polishing', 286 | 'warm_start': 'warm_starting', 287 | } 288 | for k, v in renamed_settings.items(): 289 | if k in kwargs: 290 | warnings.warn( 291 | f'"{k}" is deprecated. Please use "{v}" instead.', 292 | DeprecationWarning, 293 | ) 294 | kwargs[v] = kwargs[k] 295 | del kwargs[k] 296 | 297 | settings_changed = False 298 | 299 | if 'rho' in kwargs and self._solver is not None: 300 | self._solver.update_rho(kwargs.pop('rho')) 301 | if 'solver_type' in kwargs: 302 | value = kwargs.pop('solver_type') 303 | assert value in ('direct', 'indirect') 304 | self.settings.linsys_solver = ( 305 | self.ext.osqp_linsys_solver_type.OSQP_DIRECT_SOLVER 306 | if value == 'direct' 307 | else self.ext.osqp_linsys_solver_type.OSQP_INDIRECT_SOLVER 308 | ) 309 | settings_changed = True 310 | if 'cg_preconditioner' in kwargs: 311 | value = kwargs.pop('cg_preconditioner') 312 | assert value in (None, 'diagonal') 313 | self.settings.cg_precond = ( 314 | self.ext.OSQP_DIAGONAL_PRECONDITIONER if value == 'diagonal' else self.ext.OSQP_NO_PRECONDITIONER 315 | ) 316 | settings_changed = True 317 | 318 | for k in self.ext.OSQPSettings.__dict__: 319 | if not k.startswith('__'): 320 | if k in kwargs: 321 | setattr(self.settings, k, kwargs.pop(k)) 322 | settings_changed = True 323 | 324 | if kwargs: 325 | raise ValueError(f'Unrecognized settings {list(kwargs.keys())}') 326 | 327 | if settings_changed and self._solver is not None: 328 | self.raises_error(self._solver.update_settings, self.settings) 329 | 330 | def update(self, **kwargs): 331 | # TODO: sanity-check on types/dimensions 332 | 333 | q, l, u = kwargs.get('q'), kwargs.get('l'), kwargs.get('u') 334 | if l is not None: 335 | l = np.maximum(l, -self.constant('OSQP_INFTY')) 336 | if u is not None: 337 | u = np.minimum(u, self.constant('OSQP_INFTY')) 338 | 339 | if q is not None or l is not None or u is not None: 340 | self._solver.update_data_vec(q=q, l=l, u=u) 341 | if 'Px' in kwargs or 'Px_idx' in kwargs or 'Ax' in kwargs or 'Ax_idx' in kwargs: 342 | self._solver.update_data_mat( 343 | P_x=kwargs.get('Px'), 344 | P_i=kwargs.get('Px_idx'), 345 | A_x=kwargs.get('Ax'), 346 | A_i=kwargs.get('Ax_idx'), 347 | ) 348 | 349 | if q is not None: 350 | self._derivative_cache['q'] = q 351 | if l is not None: 352 | self._derivative_cache['l'] = l 353 | if u is not None: 354 | self._derivative_cache['u'] = u 355 | 356 | for _var in ('P', 'A'): 357 | _varx = f'{_var}x' 358 | if kwargs.get(_varx) is not None: 359 | if kwargs.get(f'{_varx}_idx') is None: 360 | self._derivative_cache[_var].data = kwargs[_varx] 361 | else: 362 | self._derivative_cache[_var].data[kwargs[f'{_varx}_idx']] = kwargs[_varx] 363 | 364 | # delete results from self._derivative_cache to prohibit 365 | # taking the derivative of unsolved problems 366 | self._derivative_cache.pop('results', None) 367 | self._derivative_cache.pop('solver', None) 368 | self._derivative_cache.pop('M', None) 369 | 370 | def setup(self, P, q, A, l, u, **settings): 371 | m, n, P, q, A, l, u = self._infer_mnpqalu(P=P, q=q, A=A, l=l, u=u) 372 | self._derivative_cache.update({'P': P, 'q': q, 'A': A, 'l': l, 'u': u}) 373 | self.m = m 374 | self.n = n 375 | P = self.ext.CSC(P.astype(self._dtype)) 376 | q = q.astype(self._dtype) 377 | A = self.ext.CSC(A.astype(self._dtype)) 378 | l = l.astype(self._dtype) 379 | u = u.astype(self._dtype) 380 | 381 | self.settings = self.ext.OSQPSettings() 382 | self.ext.osqp_set_default_settings(self.settings) 383 | self.update_settings(**settings) 384 | 385 | self._solver = self.raises_error( 386 | self.ext.OSQPSolver, 387 | P, 388 | q, 389 | A, 390 | l, 391 | u, 392 | self.m, 393 | self.n, 394 | self.settings, 395 | ) 396 | if 'rho' in settings: 397 | self._solver.update_rho(settings['rho']) 398 | 399 | def warm_start(self, x=None, y=None): 400 | # TODO: sanity checks on types/dimensions 401 | return self._solver.warm_start(x, y) 402 | 403 | def solve(self, raise_error=None): 404 | if raise_error is None: 405 | warnings.warn( 406 | 'The default value of raise_error will change to True in the future.', 407 | PendingDeprecationWarning, 408 | ) 409 | raise_error = False 410 | 411 | self._solver.solve() 412 | 413 | info = self._solver.info 414 | if info.status_val == SolverStatus.OSQP_NON_CVX: 415 | info.obj_val = np.nan 416 | 417 | if info.status_val != SolverStatus.OSQP_SOLVED and raise_error: 418 | raise OSQPException(info.status_val) 419 | 420 | # Create a Namespace of OSQPInfo keys and associated values 421 | _info = SimpleNamespace(**{k: getattr(info, k) for k in info.__class__.__dict__ if not k.startswith('__')}) 422 | 423 | # TODO: The following structure is only to maintain backward compatibility, where x/y are attributes 424 | # directly inside the returned object on solve(). This should be simplified! 425 | results = SimpleNamespace( 426 | x=self._solver.solution.x, 427 | y=self._solver.solution.y, 428 | prim_inf_cert=self._solver.solution.prim_inf_cert, 429 | dual_inf_cert=self._solver.solution.dual_inf_cert, 430 | info=_info, 431 | ) 432 | 433 | self._derivative_cache['results'] = results 434 | return results 435 | 436 | def _render_pywrapper_files(self, output_folder, **kwargs): 437 | env = Environment( 438 | loader=PackageLoader('osqp.codegen.pywrapper', package_path=''), 439 | autoescape=select_autoescape(), 440 | ) 441 | 442 | for template_name in env.list_templates(extensions='.jinja'): 443 | template = env.get_template(template_name) 444 | template_base_name = os.path.splitext(template_name)[0] 445 | 446 | with open(os.path.join(output_folder, template_base_name), 'w') as f: 447 | f.write(template.render(**kwargs)) 448 | 449 | def codegen( 450 | self, 451 | folder, 452 | parameters='vectors', 453 | extension_name='emosqp', 454 | force_rewrite=False, 455 | use_float=False, 456 | printing_enable=False, 457 | profiling_enable=False, 458 | interrupt_enable=False, 459 | include_codegen_src=True, 460 | prefix='', 461 | compile=False, 462 | ): 463 | assert self.has_capability('OSQP_CAPABILITY_CODEGEN'), 'This OSQP object does not support codegen' 464 | assert parameters in ( 465 | 'vectors', 466 | 'matrices', 467 | ), 'Unknown parameters specification' 468 | 469 | defines = self.ext.OSQPCodegenDefines() 470 | self.ext.osqp_set_default_codegen_defines(defines) 471 | 472 | defines.embedded_mode = 1 if parameters == 'vectors' else 2 473 | defines.float_type = 1 if use_float else 0 474 | defines.printing_enable = 1 if printing_enable else 0 475 | defines.profiling_enable = 1 if profiling_enable else 0 476 | defines.interrupt_enable = 1 if interrupt_enable else 0 477 | defines.derivatives_enable = 0 478 | 479 | folder = os.path.abspath(folder) 480 | if include_codegen_src: 481 | # https://github.com/python/importlib_resources/issues/85 482 | try: 483 | codegen_src_path = importlib.resources.files('osqp.codegen').joinpath('codegen_src') 484 | shutil.copytree(codegen_src_path, folder, dirs_exist_ok=force_rewrite) 485 | except AttributeError: 486 | handle = importlib.resources.path('osqp.codegen', 'codegen_src') 487 | with handle as codegen_src_path: 488 | shutil.copytree(codegen_src_path, folder, dirs_exist_ok=force_rewrite) 489 | 490 | # The C codegen call expects the folder to exist and have a trailing slash 491 | os.makedirs(folder, exist_ok=True) 492 | if not folder.endswith(os.path.sep): 493 | folder += os.path.sep 494 | 495 | status = self._solver.codegen(folder, prefix, defines) 496 | assert status == 0, f'Codegen failed with error code {status}' 497 | 498 | if extension_name is not None: 499 | assert include_codegen_src, 'If generating python wrappers, include_codegen_src must be True' 500 | template_vars = dict( 501 | prefix=prefix, 502 | extension_name=extension_name, 503 | embedded_mode=defines.embedded_mode, 504 | ) 505 | self._render_pywrapper_files(folder, **template_vars) 506 | if compile: 507 | subprocess.check_call( 508 | [ 509 | sys.executable, 510 | 'setup.py', 511 | 'build_ext', 512 | '--inplace', 513 | ], 514 | cwd=folder, 515 | ) 516 | 517 | return folder 518 | 519 | def adjoint_derivative_compute(self, dx=None, dy=None): 520 | """ 521 | Compute adjoint derivative after solve. 522 | """ 523 | 524 | assert self.has_capability('OSQP_CAPABILITY_DERIVATIVES'), 'This OSQP object does not support derivatives' 525 | 526 | try: 527 | results = self._derivative_cache['results'] 528 | except KeyError: 529 | raise ValueError( 530 | 'Problem has not been solved. ' 'You cannot take derivatives. ' 'Please call the solve function.' 531 | ) 532 | 533 | if results.info.status_val != SolverStatus.OSQP_SOLVED: 534 | raise ValueError('Problem has not been solved to optimality. ' 'You cannot take derivatives') 535 | 536 | if dy is None: 537 | dy = np.zeros(self.m) 538 | 539 | self._solver.adjoint_derivative_compute(dx, dy) 540 | 541 | def adjoint_derivative_get_mat(self, as_dense=True, dP_as_triu=True): 542 | """ 543 | Get dP/dA matrices after an invocation of adjoint_derivative_compute 544 | """ 545 | 546 | assert self.has_capability('OSQP_CAPABILITY_DERIVATIVES'), 'This OSQP object does not support derivatives' 547 | 548 | try: 549 | results = self._derivative_cache['results'] 550 | except KeyError: 551 | raise ValueError( 552 | 'Problem has not been solved. ' 'You cannot take derivatives. ' 'Please call the solve function.' 553 | ) 554 | 555 | if results.info.status_val != SolverStatus.OSQP_SOLVED: 556 | raise ValueError('Problem has not been solved to optimality. ' 'You cannot take derivatives') 557 | 558 | P, _ = self._derivative_cache['P'], self._derivative_cache['q'] 559 | A = self._derivative_cache['A'] 560 | 561 | dP = self.ext.CSC(P.copy()) 562 | dA = self.ext.CSC(A.copy()) 563 | 564 | self._solver.adjoint_derivative_get_mat(dP, dA) 565 | 566 | if not dP_as_triu: 567 | dP = self._csc_triu_as_csc_full(dP) 568 | 569 | if as_dense: 570 | dP = self._as_dense(dP) 571 | dA = self._as_dense(dA) 572 | 573 | return dP, dA 574 | 575 | def adjoint_derivative_get_vec(self): 576 | """ 577 | Get dq/dl/du vectors after an invocation of adjoint_derivative_compute 578 | """ 579 | 580 | assert self.has_capability('OSQP_CAPABILITY_DERIVATIVES'), 'This OSQP object does not support derivatives' 581 | 582 | try: 583 | results = self._derivative_cache['results'] 584 | except KeyError: 585 | raise ValueError( 586 | 'Problem has not been solved. ' 'You cannot take derivatives. ' 'Please call the solve function.' 587 | ) 588 | 589 | if results.info.status_val != SolverStatus.OSQP_SOLVED: 590 | raise ValueError('Problem has not been solved to optimality. ' 'You cannot take derivatives') 591 | 592 | dq = np.empty(self.n).astype(self._dtype) 593 | dl = np.zeros(self.m).astype(self._dtype) 594 | du = np.zeros(self.m).astype(self._dtype) 595 | 596 | self._solver.adjoint_derivative_get_vec(dq, dl, du) 597 | 598 | return dq, dl, du 599 | -------------------------------------------------------------------------------- /src/osqp/mkl.py: -------------------------------------------------------------------------------- 1 | from osqp.interface import OSQP as _OSQP 2 | 3 | 4 | class OSQP(_OSQP): 5 | def __init__(self, *args, **kwargs): 6 | super(OSQP, self).__init__(*args, **kwargs, algebra='mkl') 7 | -------------------------------------------------------------------------------- /src/osqp/nn/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/nn/__init__.py -------------------------------------------------------------------------------- /src/osqp/nn/torch.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy.sparse as spa 3 | 4 | import torch 5 | from torch.nn import Module 6 | from torch.autograd import Function 7 | from joblib import Parallel, delayed 8 | import multiprocessing 9 | 10 | import osqp 11 | 12 | 13 | def to_numpy(t): 14 | if t is None: 15 | return None 16 | elif t.nelement() == 0: 17 | return np.array([]) 18 | else: 19 | return t.cpu().detach().numpy() 20 | 21 | 22 | class OSQP(Module): 23 | def __init__( 24 | self, 25 | P_idx, 26 | P_shape, 27 | A_idx, 28 | A_shape, 29 | eps_rel=1e-5, 30 | eps_abs=1e-5, 31 | verbose=False, 32 | max_iter=10000, 33 | algebra='builtin', 34 | solver_type='direct', 35 | ): 36 | super().__init__() 37 | self.P_idx, self.P_shape = P_idx, P_shape 38 | self.A_idx, self.A_shape = A_idx, A_shape 39 | self.eps_rel, self.eps_abs = eps_rel, eps_abs 40 | self.verbose = verbose 41 | self.max_iter = max_iter 42 | self.algebra = algebra 43 | self.solver_type = solver_type 44 | 45 | def forward(self, P_val, q_val, A_val, l_val, u_val): 46 | return _OSQP_Fn( 47 | P_idx=self.P_idx, 48 | P_shape=self.P_shape, 49 | A_idx=self.A_idx, 50 | A_shape=self.A_shape, 51 | eps_rel=self.eps_rel, 52 | eps_abs=self.eps_abs, 53 | verbose=self.verbose, 54 | max_iter=self.max_iter, 55 | algebra=self.algebra, 56 | solver_type=self.solver_type, 57 | )(P_val, q_val, A_val, l_val, u_val) 58 | 59 | 60 | def _OSQP_Fn( 61 | P_idx, 62 | P_shape, 63 | A_idx, 64 | A_shape, 65 | eps_rel, 66 | eps_abs, 67 | verbose, 68 | max_iter, 69 | algebra, 70 | solver_type, 71 | ): 72 | solvers = [] 73 | 74 | m, n = A_shape # Problem size 75 | 76 | class _OSQP_FnFn(Function): 77 | @staticmethod 78 | def forward(ctx, P_val, q_val, A_val, l_val, u_val): 79 | """Solve a batch of QPs using OSQP. 80 | 81 | This function solves a batch of QPs, each optimizing over 82 | `n` variables and having `m` constraints. 83 | 84 | The optimization problem for each instance in the batch 85 | (dropping indexing from the notation) is of the form 86 | 87 | \\hat x = argmin_x 1/2 x' P x + q' x 88 | subject to l <= Ax <= u 89 | 90 | where P \\in S^{n,n}, 91 | S^{n,n} is the set of all positive semi-definite matrices, 92 | q \\in R^{n} 93 | A \\in R^{m,n} 94 | l \\in R^{m} 95 | u \\in R^{m} 96 | 97 | These parameters should all be passed to this function as 98 | Variable- or Parameter-wrapped Tensors. 99 | (See torch.autograd.Variable and torch.nn.parameter.Parameter) 100 | 101 | If you want to solve a batch of QPs where `n` and `m` 102 | are the same, but some of the contents differ across the 103 | minibatch, you can pass in tensors in the standard way 104 | where the first dimension indicates the batch example. 105 | This can be done with some or all of the coefficients. 106 | 107 | You do not need to add an extra dimension to coefficients 108 | that will not change across all of the minibatch examples. 109 | This function is able to infer such cases. 110 | 111 | If you don't want to use any constraints, you can set the 112 | appropriate values to: 113 | 114 | e = Variable(torch.Tensor()) 115 | 116 | """ 117 | 118 | def _get_update_flag(n_batch: int) -> bool: 119 | """ 120 | This is a helper function that returns a flag if we need to update the solvers 121 | or generate them. Raises an RuntimeError if the number of solvers is invalid. 122 | """ 123 | num_solvers = len(solvers) 124 | if num_solvers not in (0, n_batch): 125 | raise RuntimeError(f'Invalid number of solvers: expected 0 or {n_batch}, but got {num_solvers}.') 126 | return num_solvers == n_batch 127 | 128 | def _inner_solve(i, update_flag, q, l, u, P_val, P_idx, A_val, A_idx, solver_type, eps_abs, eps_rel): 129 | """ 130 | This inner function solves for each solver. update_flag has to be passed from 131 | outside to make sure it doesn't change during a parallel run. 132 | """ 133 | # Solve QP 134 | # TODO: Cache solver object in between 135 | # P = spa.csc_matrix((to_numpy(P_val[i]), P_idx), shape=P_shape) 136 | if update_flag: 137 | solver = solvers[i] 138 | solver.update( 139 | q=q[i], l=l[i], u=u[i], Px=to_numpy(P_val[i]), Px_idx=P_idx, Ax=to_numpy(A_val[i]), Ax_idx=A_idx 140 | ) 141 | else: 142 | P = spa.csc_matrix((to_numpy(P_val[i]), P_idx), shape=P_shape) 143 | A = spa.csc_matrix((to_numpy(A_val[i]), A_idx), shape=A_shape) 144 | # TODO: Deep copy when available 145 | solver = osqp.OSQP(algebra=algebra) 146 | solver.setup( 147 | P, 148 | q[i], 149 | A, 150 | l[i], 151 | u[i], 152 | solver_type=solver_type, 153 | verbose=verbose, 154 | eps_abs=eps_abs, 155 | eps_rel=eps_rel, 156 | ) 157 | result = solver.solve() 158 | status = result.info.status_val 159 | if status != osqp.SolverStatus.OSQP_SOLVED: 160 | # TODO: We can replace this with something calmer and 161 | # add some more options around potentially ignoring this. 162 | raise RuntimeError(f'Unable to solve QP, status: {status}') 163 | 164 | return solver, result.x 165 | 166 | params = [P_val, q_val, A_val, l_val, u_val] 167 | 168 | for p in params: 169 | assert p.ndimension() <= 2, 'Unexpected number of dimensions' 170 | 171 | batch_mode = np.any([t.ndimension() > 1 for t in params]) 172 | if not batch_mode: 173 | n_batch = 1 174 | else: 175 | batch_sizes = [t.size(0) if t.ndimension() == 2 else 1 for t in params] 176 | n_batch = max(batch_sizes) 177 | 178 | dtype = P_val.dtype 179 | device = P_val.device 180 | 181 | # TODO (Bart): create CSC matrix during initialization. Then 182 | # just reassign the mat.data vector with A_val and P_val 183 | 184 | for i, p in enumerate(params): 185 | if p.ndimension() == 1: 186 | params[i] = p.unsqueeze(0).expand(n_batch, p.size(0)) 187 | 188 | [P_val, q_val, A_val, l_val, u_val] = params 189 | assert A_val.size(1) == len(A_idx[0]), 'Unexpected size of A' 190 | assert P_val.size(1) == len(P_idx[0]), 'Unexpected size of P' 191 | 192 | q = [to_numpy(q_val[i]) for i in range(n_batch)] 193 | l = [to_numpy(l_val[i]) for i in range(n_batch)] 194 | u = [to_numpy(u_val[i]) for i in range(n_batch)] 195 | 196 | # Perform forward step solving the QPs 197 | x_torch = torch.zeros((n_batch, n), dtype=dtype, device=device) 198 | 199 | update_flag = _get_update_flag(n_batch) 200 | n_jobs = multiprocessing.cpu_count() 201 | res = Parallel(n_jobs=n_jobs, prefer='threads')( 202 | delayed(_inner_solve)( 203 | i=i, 204 | update_flag=update_flag, 205 | q=q, 206 | l=l, 207 | u=u, 208 | P_val=P_val, 209 | P_idx=P_idx, 210 | A_val=A_val, 211 | A_idx=A_idx, 212 | solver_type=solver_type, 213 | eps_abs=eps_abs, 214 | eps_rel=eps_rel, 215 | ) 216 | for i in range(n_batch) 217 | ) 218 | solvers_loop, x = zip(*res) 219 | for i in range(n_batch): 220 | if update_flag: 221 | solvers[i] = solvers_loop[i] 222 | else: 223 | solvers.append(solvers_loop[i]) 224 | x_torch[i] = torch.from_numpy(x[i]) 225 | 226 | # Return solutions 227 | if not batch_mode: 228 | x_torch = x_torch.squeeze(0) 229 | 230 | return x_torch 231 | 232 | @staticmethod 233 | def backward(ctx, dl_dx_val): 234 | def _loop_adjoint_derivative(solver, dl_dx): 235 | """ 236 | This inner function calculates dp[i] dl[i], du[i], dP[i], dA[i] 237 | using solvers[i], dl_dx[i]. 238 | """ 239 | solver.adjoint_derivative_compute(dx=dl_dx) 240 | dPi_np, dAi_np = solver.adjoint_derivative_get_mat(as_dense=False, dP_as_triu=False) 241 | dqi_np, dli_np, dui_np = solver.adjoint_derivative_get_vec() 242 | dq, dl, du = [torch.from_numpy(d) for d in [dqi_np, dli_np, dui_np]] 243 | dP, dA = [torch.from_numpy(d.x) for d in [dPi_np, dAi_np]] 244 | return dq, dl, du, dP, dA 245 | 246 | dtype = dl_dx_val.dtype 247 | device = dl_dx_val.device 248 | 249 | batch_mode = dl_dx_val.ndimension() == 2 250 | 251 | if not batch_mode: 252 | dl_dx_val = dl_dx_val.unsqueeze(0) 253 | 254 | n_batch = dl_dx_val.size(0) 255 | dtype = dl_dx_val.dtype 256 | device = dl_dx_val.device 257 | 258 | # Convert dl_dx to numpy 259 | dl_dx = to_numpy(dl_dx_val) 260 | 261 | # Convert to torch tensors 262 | nnz_P = len(P_idx[0]) 263 | nnz_A = len(A_idx[0]) 264 | dP = torch.zeros((n_batch, nnz_P), dtype=dtype, device=device) 265 | dq = torch.zeros((n_batch, n), dtype=dtype, device=device) 266 | dA = torch.zeros((n_batch, nnz_A), dtype=dtype, device=device) 267 | dl = torch.zeros((n_batch, m), dtype=dtype, device=device) 268 | du = torch.zeros((n_batch, m), dtype=dtype, device=device) 269 | 270 | n_jobs = multiprocessing.cpu_count() 271 | res = Parallel(n_jobs=n_jobs, prefer='threads')( 272 | delayed(_loop_adjoint_derivative)(solvers[i], dl_dx[i]) for i in range(n_batch) 273 | ) 274 | dq_vec, dl_vec, du_vec, dP_vec, dA_vec = zip(*res) 275 | for i in range(n_batch): 276 | dq[i] = dq_vec[i] 277 | dl[i] = dl_vec[i] 278 | du[i] = du_vec[i] 279 | dP[i] = dP_vec[i] 280 | dA[i] = dA_vec[i] 281 | 282 | grads = [dP, dq, dA, dl, du] 283 | 284 | if not batch_mode: 285 | for i, g in enumerate(grads): 286 | grads[i] = g.squeeze() 287 | 288 | return tuple(grads) 289 | 290 | return _OSQP_FnFn.apply 291 | -------------------------------------------------------------------------------- /src/osqp/tests/basic_test.py: -------------------------------------------------------------------------------- 1 | from types import SimpleNamespace 2 | import pytest 3 | from scipy import sparse 4 | import numpy as np 5 | import numpy.testing as nptest 6 | from osqp import OSQP 7 | from osqp.tests.utils import load_high_accuracy 8 | 9 | 10 | @pytest.fixture 11 | def self(algebra, solver_type, atol, rtol, decimal_tol): 12 | ns = SimpleNamespace() 13 | ns.P = sparse.diags([11.0, 0.0], format='csc') 14 | ns.q = np.array([3, 4]) 15 | ns.A = sparse.csc_matrix([[-1, 0], [0, -1], [-1, -3], [2, 5], [3, 4]]) 16 | ns.u = np.array([0.0, 0.0, -15, 100, 80]) 17 | ns.l = -1e06 * np.ones(len(ns.u)) 18 | ns.n = ns.P.shape[0] 19 | ns.m = ns.A.shape[0] 20 | ns.opts = { 21 | 'verbose': False, 22 | 'eps_abs': 1e-09, 23 | 'eps_rel': 1e-09, 24 | 'max_iter': 2500, 25 | 'rho': 0.1, 26 | 'adaptive_rho': False, 27 | 'polishing': False, 28 | 'check_termination': 1, 29 | 'warm_starting': True, 30 | 'solver_type': solver_type, 31 | } 32 | ns.model = OSQP(algebra=algebra) 33 | ns.model.setup(P=ns.P, q=ns.q, A=ns.A, l=ns.l, u=ns.u, **ns.opts) 34 | ns.atol = atol 35 | ns.rtol = rtol 36 | ns.decimal_tol = decimal_tol 37 | return ns 38 | 39 | 40 | def test_basic_QP(self): 41 | res = self.model.solve() 42 | 43 | x_sol, y_sol, obj_sol = load_high_accuracy('test_basic_QP') 44 | nptest.assert_allclose(res.x, x_sol, rtol=self.rtol, atol=self.atol) 45 | nptest.assert_allclose(res.y, y_sol, rtol=self.rtol, atol=self.atol) 46 | nptest.assert_almost_equal(res.info.obj_val, obj_sol, decimal=self.decimal_tol) 47 | 48 | 49 | def test_update_q(self): 50 | # Update linear cost 51 | q_new = np.array([10, 20]) 52 | self.model.update(q=q_new) 53 | res = self.model.solve() 54 | 55 | x_sol, y_sol, obj_sol = load_high_accuracy('test_update_q') 56 | nptest.assert_allclose(res.x, x_sol, rtol=self.rtol, atol=self.atol) 57 | nptest.assert_allclose(res.y, y_sol, rtol=self.rtol, atol=self.atol) 58 | nptest.assert_almost_equal(res.info.obj_val, obj_sol, decimal=self.decimal_tol) 59 | 60 | 61 | def test_update_l(self): 62 | # Update lower bound 63 | l_new = -50 * np.ones(self.m) 64 | self.model.update(l=l_new) 65 | res = self.model.solve() 66 | 67 | x_sol, y_sol, obj_sol = load_high_accuracy('test_update_l') 68 | nptest.assert_allclose(res.x, x_sol, rtol=self.rtol, atol=self.atol) 69 | nptest.assert_allclose(res.y, y_sol, rtol=self.rtol, atol=self.atol) 70 | nptest.assert_almost_equal(res.info.obj_val, obj_sol, decimal=self.decimal_tol) 71 | 72 | 73 | def test_update_u(self): 74 | # Update lower bound 75 | u_new = 1000 * np.ones(self.m) 76 | self.model.update(u=u_new) 77 | res = self.model.solve() 78 | 79 | x_sol, y_sol, obj_sol = load_high_accuracy('test_update_u') 80 | nptest.assert_allclose(res.x, x_sol, rtol=self.rtol, atol=self.atol) 81 | nptest.assert_allclose(res.y, y_sol, rtol=self.rtol, atol=self.atol) 82 | nptest.assert_almost_equal(res.info.obj_val, obj_sol, decimal=self.decimal_tol) 83 | 84 | 85 | def test_update_bounds(self): 86 | # Update lower bound 87 | l_new = -100 * np.ones(self.m) 88 | # Update lower bound 89 | u_new = 1000 * np.ones(self.m) 90 | self.model.update(u=u_new, l=l_new) 91 | res = self.model.solve() 92 | 93 | x_sol, y_sol, obj_sol = load_high_accuracy('test_update_bounds') 94 | if self.model.algebra != 'cuda': # pytest-todo 95 | nptest.assert_allclose(res.x, x_sol, rtol=self.rtol, atol=self.atol) 96 | nptest.assert_allclose(res.y, y_sol, rtol=self.rtol, atol=self.atol) 97 | nptest.assert_almost_equal(res.info.obj_val, obj_sol, decimal=self.decimal_tol) 98 | else: 99 | assert res.info.status_val == self.model.constant('OSQP_PRIMAL_INFEASIBLE') 100 | 101 | 102 | def test_update_max_iter(self): 103 | self.model.update_settings(max_iter=80) 104 | res = self.model.solve() 105 | 106 | assert res.info.status_val == self.model.constant('OSQP_MAX_ITER_REACHED') 107 | 108 | 109 | def test_update_check_termination(self): 110 | self.model.update_settings(check_termination=0) 111 | res = self.model.solve() 112 | 113 | assert res.info.iter == self.opts['max_iter'] 114 | 115 | 116 | def test_update_rho(self): 117 | res_default = self.model.solve() 118 | 119 | # Setup with different rho and update 120 | default_opts = self.opts.copy() 121 | default_opts['rho'] = 0.7 122 | model = OSQP(algebra=self.model.algebra) 123 | model.setup(P=self.P, q=self.q, A=self.A, l=self.l, u=self.u, **default_opts) 124 | model.update_settings(rho=self.opts['rho']) 125 | res_updated_rho = model.solve() 126 | 127 | # Assert same number of iterations 128 | assert res_default.info.iter == res_updated_rho.info.iter 129 | 130 | 131 | def test_upper_triangular_P(self): 132 | res_default = self.model.solve() 133 | 134 | # Get upper triangular P 135 | P_triu = sparse.triu(self.P, format='csc') 136 | 137 | # Setup and solve with upper triangular part only 138 | model = OSQP(algebra=self.model.algebra) 139 | model.setup(P=P_triu, q=self.q, A=self.A, l=self.l, u=self.u, **self.opts) 140 | res_triu = model.solve() 141 | 142 | nptest.assert_allclose(res_default.x, res_triu.x, rtol=self.rtol, atol=self.atol) 143 | nptest.assert_allclose(res_default.y, res_triu.y, rtol=self.rtol, atol=self.atol) 144 | nptest.assert_almost_equal( 145 | res_default.info.obj_val, 146 | res_triu.info.obj_val, 147 | decimal=self.decimal_tol, 148 | ) 149 | 150 | 151 | def test_update_invalid(self): 152 | # can't update unsupported setting 153 | with pytest.raises(ValueError): 154 | self.model.update_settings(foo=42) 155 | -------------------------------------------------------------------------------- /src/osqp/tests/codegen_matrices_test.py: -------------------------------------------------------------------------------- 1 | import osqp 2 | import numpy as np 3 | from scipy import sparse 4 | import unittest 5 | import pytest 6 | import numpy.testing as nptest 7 | import shutil as sh 8 | import sys 9 | 10 | 11 | @pytest.mark.skipif(not osqp.algebra_available('builtin'), reason='Builtin Algebra not available') 12 | class codegen_matrices_tests(unittest.TestCase): 13 | @classmethod 14 | def setUpClass(cls): 15 | P = sparse.diags([11.0, 0.1], format='csc') 16 | P_new = sparse.eye(2, format='csc') 17 | q = np.array([3, 4]) 18 | A = sparse.csc_matrix([[-1, 0], [0, -1], [-1, -3], [2, 5], [3, 4]]) 19 | A_new = sparse.csc_matrix([[-1, 0], [0, -1], [-2, -2], [2, 5], [3, 4]]) 20 | u = np.array([0, 0, -15, 100, 80]) 21 | l = -np.inf * np.ones(len(u)) 22 | n = P.shape[0] 23 | m = A.shape[0] 24 | opts = { 25 | 'verbose': False, 26 | 'eps_abs': 1e-08, 27 | 'eps_rel': 1e-08, 28 | 'alpha': 1.6, 29 | 'max_iter': 3000, 30 | 'warm_starting': True, 31 | } 32 | 33 | model = osqp.OSQP(algebra='builtin') 34 | if not model.has_capability('OSQP_CAPABILITY_DERIVATIVES'): 35 | pytest.skip('No derivatives capability') 36 | model.setup(P=P, q=q, A=A, l=l, u=u, **opts) 37 | 38 | model_dir = model.codegen( 39 | 'codegen_mat_out', 40 | extension_name='mat_emosqp', 41 | include_codegen_src=True, 42 | force_rewrite=True, 43 | parameters='matrices', 44 | prefix='bar', 45 | compile=True, 46 | ) 47 | sys.path.append(model_dir) 48 | 49 | cls.m = m 50 | cls.n = n 51 | cls.P = P 52 | cls.P_new = P_new 53 | cls.q = q 54 | cls.A = A 55 | cls.A_new = A_new 56 | cls.l = l 57 | cls.u = u 58 | cls.opts = opts 59 | 60 | @classmethod 61 | def tearDownClass(cls): 62 | sh.rmtree('codegen_mat_out', ignore_errors=True) 63 | 64 | def setUp(self): 65 | self.model = osqp.OSQP(algebra='builtin') 66 | self.model.setup(P=self.P, q=self.q, A=self.A, l=self.l, u=self.u, **self.opts) 67 | 68 | def test_solve(self): 69 | import mat_emosqp 70 | 71 | # Solve problem 72 | x, y, _, _, _ = mat_emosqp.solve() 73 | 74 | # Assert close 75 | nptest.assert_array_almost_equal(x, np.array([0.0, 5.0]), decimal=5) 76 | nptest.assert_array_almost_equal(y, np.array([1.5, 0.0, 1.5, 0.0, 0.0]), decimal=5) 77 | 78 | def test_update_P(self): 79 | import mat_emosqp 80 | 81 | # Update matrix P 82 | Px = self.P_new.data 83 | Px_idx = np.arange(self.P_new.nnz) 84 | mat_emosqp.update_data_mat(P_x=Px) 85 | 86 | # Solve problem 87 | x, y, _, _, _ = mat_emosqp.solve() 88 | 89 | # Assert close 90 | nptest.assert_array_almost_equal(x, np.array([0.0, 5.0]), decimal=5) 91 | nptest.assert_array_almost_equal(y, np.array([0.0, 0.0, 3.0, 0.0, 0.0]), decimal=5) 92 | 93 | # Update matrix P to the original value 94 | Px = self.P.data 95 | Px_idx = np.arange(self.P.nnz) 96 | mat_emosqp.update_data_mat(P_x=Px, P_i=Px_idx) 97 | 98 | def test_update_P_allind(self): 99 | import mat_emosqp 100 | 101 | # Update matrix P 102 | Px = self.P_new.data 103 | mat_emosqp.update_data_mat(P_x=Px, P_i=None) 104 | x, y, _, _, _ = mat_emosqp.solve() 105 | 106 | # Assert close 107 | nptest.assert_array_almost_equal(x, np.array([0.0, 5.0]), decimal=5) 108 | nptest.assert_array_almost_equal(y, np.array([0.0, 0.0, 3.0, 0.0, 0.0]), decimal=5) 109 | 110 | # Update matrix P to the original value 111 | Px_idx = np.arange(self.P.nnz) 112 | mat_emosqp.update_data_mat(P_x=Px, P_i=Px_idx) 113 | 114 | def test_update_A(self): 115 | import mat_emosqp 116 | 117 | # Update matrix A 118 | Ax = self.A_new.data 119 | Ax_idx = np.arange(self.A_new.nnz) 120 | mat_emosqp.update_data_mat(A_x=Ax, A_i=Ax_idx) 121 | 122 | # Solve problem 123 | x, y, _, _, _ = mat_emosqp.solve() 124 | 125 | # Assert close 126 | nptest.assert_array_almost_equal(x, np.array([0.15765766, 7.34234234]), decimal=5) 127 | nptest.assert_array_almost_equal(y, np.array([0.0, 0.0, 2.36711712, 0.0, 0.0]), decimal=5) 128 | 129 | # Update matrix A to the original value 130 | Ax = self.A.data 131 | Ax_idx = np.arange(self.A.nnz) 132 | mat_emosqp.update_data_mat(A_x=Ax, A_i=Ax_idx) 133 | 134 | def test_update_A_allind(self): 135 | import mat_emosqp 136 | 137 | # Update matrix A 138 | Ax = self.A_new.data 139 | mat_emosqp.update_data_mat(A_x=Ax, A_i=None) 140 | x, y, _, _, _ = mat_emosqp.solve() 141 | 142 | # Assert close 143 | nptest.assert_array_almost_equal(x, np.array([0.15765766, 7.34234234]), decimal=5) 144 | nptest.assert_array_almost_equal(y, np.array([0.0, 0.0, 2.36711712, 0.0, 0.0]), decimal=5) 145 | 146 | # Update matrix A to the original value 147 | Ax = self.A.data 148 | Ax_idx = np.arange(self.A.nnz) 149 | mat_emosqp.update_data_mat(A_x=Ax, A_i=Ax_idx) 150 | 151 | def test_update_P_A_indP_indA(self): 152 | import mat_emosqp 153 | 154 | # Update matrices P and A 155 | Px = self.P_new.data 156 | Px_idx = np.arange(self.P_new.nnz) 157 | Ax = self.A_new.data 158 | Ax_idx = np.arange(self.A_new.nnz) 159 | mat_emosqp.update_data_mat(P_x=Px, P_i=Px_idx, A_x=Ax, A_i=Ax_idx) 160 | 161 | # Solve problem 162 | x, y, _, _, _ = mat_emosqp.solve() 163 | 164 | # Assert close 165 | nptest.assert_array_almost_equal(x, np.array([4.25, 3.25]), decimal=5) 166 | nptest.assert_array_almost_equal(y, np.array([0.0, 0.0, 3.625, 0.0, 0.0]), decimal=5) 167 | 168 | # Update matrices P and A to the original values 169 | Px = self.P.data 170 | Ax = self.A.data 171 | mat_emosqp.update_data_mat(P_x=Px, P_i=None, A_x=Ax, A_i=None) 172 | 173 | def test_update_P_A_indP(self): 174 | import mat_emosqp 175 | 176 | # Update matrices P and A 177 | Px = self.P_new.data 178 | Px_idx = np.arange(self.P_new.nnz) 179 | Ax = self.A_new.data 180 | mat_emosqp.update_data_mat(P_x=Px, P_i=Px_idx, A_x=Ax, A_i=None) 181 | x, y, _, _, _ = mat_emosqp.solve() 182 | 183 | # Assert close 184 | nptest.assert_array_almost_equal(x, np.array([4.25, 3.25]), decimal=5) 185 | nptest.assert_array_almost_equal(y, np.array([0.0, 0.0, 3.625, 0.0, 0.0]), decimal=5) 186 | 187 | # Update matrices P and A to the original values 188 | Px = self.P.data 189 | Ax = self.A.data 190 | mat_emosqp.update_data_mat(P_x=Px, P_i=None, A_x=Ax, A_i=None) 191 | 192 | def test_update_P_A_indA(self): 193 | import mat_emosqp 194 | 195 | # Update matrices P and A 196 | Px = self.P_new.data 197 | Ax = self.A_new.data 198 | Ax_idx = np.arange(self.A_new.nnz) 199 | mat_emosqp.update_data_mat(P_x=Px, P_i=None, A_x=Ax, A_i=Ax_idx) 200 | x, y, _, _, _ = mat_emosqp.solve() 201 | 202 | # Assert close 203 | nptest.assert_array_almost_equal(x, np.array([4.25, 3.25]), decimal=5) 204 | nptest.assert_array_almost_equal(y, np.array([0.0, 0.0, 3.625, 0.0, 0.0]), decimal=5) 205 | 206 | # Update matrix P to the original value 207 | Px = self.P.data 208 | Px_idx = np.arange(self.P.nnz) 209 | Ax = self.A.data 210 | Ax_idx = np.arange(self.A.nnz) 211 | mat_emosqp.update_data_mat(P_x=Px, P_i=Px_idx, A_x=Ax, A_i=Ax_idx) 212 | 213 | def test_update_P_A_allind(self): 214 | import mat_emosqp 215 | 216 | # Update matrices P and A 217 | Px = self.P_new.data 218 | Ax = self.A_new.data 219 | mat_emosqp.update_data_mat(P_x=Px, P_i=None, A_x=Ax, A_i=None) 220 | x, y, _, _, _ = mat_emosqp.solve() 221 | 222 | # Assert close 223 | nptest.assert_array_almost_equal(x, np.array([4.25, 3.25]), decimal=5) 224 | nptest.assert_array_almost_equal(y, np.array([0.0, 0.0, 3.625, 0.0, 0.0]), decimal=5) 225 | 226 | # Update matrices P and A to the original values 227 | Px = self.P.data 228 | Ax = self.A.data 229 | mat_emosqp.update_data_mat(P_x=Px, P_i=None, A_x=Ax, A_i=None) 230 | -------------------------------------------------------------------------------- /src/osqp/tests/codegen_vectors_test.py: -------------------------------------------------------------------------------- 1 | import osqp 2 | import numpy as np 3 | from scipy import sparse 4 | import unittest 5 | import pytest 6 | import numpy.testing as nptest 7 | import shutil as sh 8 | import sys 9 | 10 | 11 | @pytest.mark.skipif(not osqp.algebra_available('builtin'), reason='Builtin Algebra not available') 12 | class codegen_vectors_tests(unittest.TestCase): 13 | @classmethod 14 | def setUpClass(cls): 15 | P = sparse.diags([11.0, 0.0], format='csc') 16 | q = np.array([3, 4]) 17 | A = sparse.csc_matrix([[-1, 0], [0, -1], [-1, -3], [2, 5], [3, 4]]) 18 | u = np.array([0, 0, -15, 100, 80]) 19 | l = -np.inf * np.ones(len(u)) 20 | n = P.shape[0] 21 | m = A.shape[0] 22 | opts = { 23 | 'verbose': False, 24 | 'eps_abs': 1e-08, 25 | 'eps_rel': 1e-08, 26 | 'rho': 0.01, 27 | 'alpha': 1.6, 28 | 'max_iter': 10000, 29 | 'warm_starting': True, 30 | } 31 | 32 | model = osqp.OSQP(algebra='builtin') 33 | if not model.has_capability('OSQP_CAPABILITY_DERIVATIVES'): 34 | pytest.skip('No derivatives capability') 35 | model.setup(P=P, q=q, A=A, l=l, u=u, **opts) 36 | 37 | model_dir = model.codegen( 38 | 'codegen_vec_out', 39 | extension_name='vec_emosqp', 40 | include_codegen_src=True, 41 | force_rewrite=True, 42 | prefix='foo', 43 | compile=True, 44 | ) 45 | sys.path.append(model_dir) 46 | 47 | cls.m = m 48 | cls.n = n 49 | cls.P = P 50 | cls.q = q 51 | cls.A = A 52 | cls.l = l 53 | cls.u = u 54 | cls.opts = opts 55 | 56 | @classmethod 57 | def tearDownClass(cls): 58 | sh.rmtree('codegen_vec_out', ignore_errors=True) 59 | 60 | def setUp(self): 61 | self.model = osqp.OSQP(algebra='builtin') 62 | self.model.setup(P=self.P, q=self.q, A=self.A, l=self.l, u=self.u, **self.opts) 63 | 64 | def test_solve(self): 65 | import vec_emosqp 66 | 67 | # Solve problem 68 | x, y, _, _, _ = vec_emosqp.solve() 69 | 70 | # Assert close 71 | nptest.assert_array_almost_equal(x, np.array([0.0, 5.0]), decimal=5) 72 | nptest.assert_array_almost_equal(y, np.array([1.66666667, 0.0, 1.33333333, 0.0, 0.0]), decimal=5) 73 | 74 | def test_update_q(self): 75 | import vec_emosqp 76 | 77 | # Update linear cost and solve the problem 78 | q_new = np.array([10.0, 20.0]) 79 | vec_emosqp.update_data_vec(q=q_new) 80 | x, y, _, _, _ = vec_emosqp.solve() 81 | 82 | # Assert close 83 | nptest.assert_array_almost_equal(x, np.array([0.0, 5.0]), decimal=5) 84 | nptest.assert_array_almost_equal(y, np.array([3.33333334, 0.0, 6.66666667, 0.0, 0.0]), decimal=5) 85 | 86 | # Update linear cost to the original value 87 | vec_emosqp.update_data_vec(q=self.q) 88 | 89 | def test_update_l(self): 90 | import vec_emosqp 91 | 92 | # Update lower bound 93 | l_new = -100.0 * np.ones(self.m) 94 | vec_emosqp.update_data_vec(l=l_new) 95 | x, y, _, _, _ = vec_emosqp.solve() 96 | 97 | # Assert close 98 | nptest.assert_array_almost_equal(x, np.array([0.0, 5.0]), decimal=5) 99 | nptest.assert_array_almost_equal(y, np.array([1.66666667, 0.0, 1.33333333, 0.0, 0.0]), decimal=5) 100 | 101 | # Update lower bound to the original value 102 | vec_emosqp.update_data_vec(l=self.l) 103 | 104 | def test_update_u(self): 105 | import vec_emosqp 106 | 107 | # Update upper bound 108 | u_new = 1000.0 * np.ones(self.m) 109 | vec_emosqp.update_data_vec(u=u_new) 110 | x, y, _, _, _ = vec_emosqp.solve() 111 | 112 | # Assert close 113 | nptest.assert_array_almost_equal(x, np.array([-1.51515152e-01, -3.33282828e02]), decimal=4) 114 | nptest.assert_array_almost_equal(y, np.array([0.0, 0.0, 1.33333333, 0.0, 0.0]), decimal=4) 115 | 116 | # Update upper bound to the original value 117 | vec_emosqp.update_data_vec(u=self.u) 118 | 119 | def test_update_bounds(self): 120 | import vec_emosqp 121 | 122 | # Update upper bound 123 | l_new = -100.0 * np.ones(self.m) 124 | u_new = 1000.0 * np.ones(self.m) 125 | vec_emosqp.update_data_vec(l=l_new, u=u_new) 126 | x, y, _, _, _ = vec_emosqp.solve() 127 | 128 | # Assert close 129 | nptest.assert_array_almost_equal(x, np.array([-0.12727273, -19.94909091]), decimal=4) 130 | nptest.assert_array_almost_equal(y, np.array([0.0, 0.0, 0.0, -0.8, 0.0]), decimal=4) 131 | 132 | # Update upper bound to the original value 133 | vec_emosqp.update_data_vec(l=self.l, u=self.u) 134 | -------------------------------------------------------------------------------- /src/osqp/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | from osqp import algebra_available 3 | 4 | 5 | def pytest_generate_tests(metafunc): 6 | 7 | # detect env vars to decide which algebras to include/skip 8 | algebras_include = os.environ.get('OSQP_TEST_ALGEBRA_INCLUDE', 'builtin mkl-direct mkl-indirect cuda').split() 9 | algebras_skip = os.environ.get('OSQP_TEST_ALGEBRA_SKIP', '').split() 10 | algebras = [x for x in algebras_include if x not in algebras_skip] 11 | 12 | parameters = ('algebra', 'solver_type', 'atol', 'rtol', 'decimal_tol') 13 | values = [] 14 | if algebra_available('builtin') and 'builtin' in algebras: 15 | values.append( 16 | ('builtin', 'direct', 1e-3, 1e-4, 4), 17 | ) 18 | if algebra_available('mkl') and 'mkl-direct' in algebras: 19 | values.append( 20 | ('mkl', 'direct', 1e-3, 1e-4, 4), 21 | ) 22 | if algebra_available('mkl') and 'mkl-indirect' in algebras: 23 | values.append( 24 | ('mkl', 'indirect', 1e-3, 1e-4, 3), 25 | ) 26 | if algebra_available('cuda') and 'cuda' in algebras: 27 | values.append( 28 | ('cuda', 'indirect', 1e-2, 1e-3, 2), 29 | ) 30 | 31 | metafunc.parametrize(parameters, values) 32 | -------------------------------------------------------------------------------- /src/osqp/tests/derivative_test.py: -------------------------------------------------------------------------------- 1 | import osqp 2 | import numpy as np 3 | import numpy.random as npr 4 | from scipy import sparse 5 | from scipy.optimize import approx_fprime 6 | import numpy.testing as npt 7 | import unittest 8 | import pytest 9 | 10 | 11 | npr.seed(1) 12 | 13 | # Tests settings 14 | grad_precision = 1e-5 15 | rel_tol = 5e-3 16 | abs_tol = 5e-3 17 | rel_tol_relaxed = 1e-2 18 | abs_tol_relaxed = 1e-2 19 | 20 | # OSQP settings 21 | eps_abs = 1e-9 22 | eps_rel = 1e-9 23 | max_iter = 500000 24 | 25 | 26 | @pytest.mark.skipif(not osqp.algebra_available('builtin'), reason='Builtin Algebra not available') 27 | class derivative_tests(unittest.TestCase): 28 | def setUp(self): 29 | npr.seed(1) 30 | 31 | def get_prob(self, n=10, m=3, P_scale=1.0, A_scale=1.0): 32 | L = np.random.randn(n, n - 1) 33 | # P = sparse.csc_matrix(L.dot(L.T) + 5. * sparse.eye(n)) 34 | P = sparse.csc_matrix(L.dot(L.T) + 0.1 * sparse.eye(n)) 35 | # P = sparse.csc_matrix(L.dot(L.T)) 36 | x_0 = npr.randn(n) 37 | s_0 = npr.rand(m) 38 | A = sparse.csc_matrix(npr.randn(m, n)) 39 | u = A.dot(x_0) + s_0 40 | # l = -10 - 10 * npr.rand(m) 41 | l = A.dot(x_0) - s_0 42 | q = npr.randn(n) 43 | true_x = npr.randn(n) 44 | true_y = npr.randn(m) 45 | 46 | return [P, q, A, l, u, true_x, true_y] 47 | 48 | def get_grads(self, P, q, A, l, u, true_x, true_y=None): 49 | # Get gradients by solving with osqp 50 | m = osqp.OSQP(algebra='builtin') 51 | m.setup( 52 | P, 53 | q, 54 | A, 55 | l, 56 | u, 57 | eps_abs=eps_abs, 58 | eps_rel=eps_rel, 59 | max_iter=max_iter, 60 | verbose=True, 61 | ) 62 | results = m.solve() 63 | if results.info.status_val != osqp.SolverStatus.OSQP_SOLVED: 64 | raise ValueError('Problem not solved!') 65 | x = results.x 66 | y = results.y 67 | if true_y is None: 68 | m.adjoint_derivative_compute(dx=x - true_x) 69 | else: 70 | m.adjoint_derivative_compute(dx=x - true_x, dy=y - true_y) 71 | 72 | dP, dA = m.adjoint_derivative_get_mat() 73 | dq, dl, du = m.adjoint_derivative_get_vec() 74 | grads = dP, dq, dA, dl, du 75 | 76 | return grads 77 | 78 | def get_forward_grads(self, P, q, A, l, u, dP, dq, dA, dl, du): 79 | # Get gradients by solving with osqp 80 | m = osqp.OSQP(algebra='builtin', eps_rel=eps_rel, eps_abs=eps_abs) 81 | m.setup( 82 | P, 83 | q, 84 | A, 85 | l, 86 | u, 87 | eps_abs=eps_abs, 88 | eps_rel=eps_rel, 89 | max_iter=max_iter, 90 | verbose=False, 91 | ) 92 | results = m.solve() 93 | if results.info.status_val != osqp.SolverStatus.OSQP_SOLVED: 94 | raise ValueError('Problem not solved!') 95 | grads = m.forward_derivative(dP=dP, dq=dq, dA=dA, dl=dl, du=du) 96 | return grads 97 | 98 | @pytest.mark.skip(reason='forward derivatives not implemented yet') 99 | def test_dsol_dq(self, verbose=False): 100 | n, m = 5, 5 101 | 102 | prob = self.get_prob(n=n, m=m, P_scale=100.0, A_scale=100.0) 103 | P, q, A, l, u, true_x, true_y = prob 104 | 105 | def grad(dq): 106 | [dx, dyl, dyu] = self.get_forward_grads(P, q, A, l, u, None, dq, None, None, None) 107 | return dx, dyl, dyu 108 | 109 | dq = np.random.normal(size=(n)) 110 | dx_computed, dyl_computed, dyu_computed = grad(dq) 111 | 112 | osqp_solver = osqp.OSQP(algebra='builtin') 113 | osqp_solver.setup( 114 | P, 115 | q, 116 | A, 117 | l, 118 | u, 119 | eps_abs=eps_abs, 120 | eps_rel=eps_rel, 121 | max_iter=max_iter, 122 | verbose=False, 123 | ) 124 | res = osqp_solver.solve() 125 | if res.info.status_val != osqp.SolverStatus.OSQP_SOLVED: 126 | raise ValueError('Problem not solved!') 127 | x1 = res.x 128 | y1 = res.y 129 | 130 | eps = grad_precision 131 | osqp_solver.setup( 132 | P, 133 | q + eps * dq, 134 | A, 135 | l, 136 | u, 137 | eps_abs=eps_abs, 138 | eps_rel=eps_rel, 139 | max_iter=max_iter, 140 | verbose=False, 141 | ) 142 | res = osqp_solver.solve() 143 | if res.info.status_val != osqp.SolverStatus.OSQP_SOLVED: 144 | raise ValueError('Problem not solved!') 145 | x2 = res.x 146 | y2 = res.y 147 | 148 | dx_fd = (x2 - x1) / eps 149 | dy_fd = (y2 - y1) / eps 150 | dyl_fd = np.zeros(m) 151 | dyl_fd[y1 < 0] = -dy_fd[y1 < 0] 152 | dyu_fd = np.zeros(m) 153 | dyu_fd[y1 >= 0] = dy_fd[y1 >= 0] 154 | 155 | if verbose: 156 | print('dx_fd: ', np.round(dx_fd, decimals=4)) 157 | print('dx: ', np.round(dx_computed, decimals=4)) 158 | 159 | npt.assert_allclose(dx_fd, dx_computed, rtol=rel_tol, atol=abs_tol) 160 | npt.assert_allclose(dyl_fd, dyl_computed, rtol=rel_tol, atol=abs_tol) 161 | npt.assert_allclose(dyu_fd, dyu_computed, rtol=rel_tol, atol=abs_tol) 162 | 163 | @pytest.mark.skip(reason='forward derivatives not implemented yet') 164 | def test_eq_inf_forward(self, verbose=False): 165 | n, m = 10, 10 166 | 167 | prob = self.get_prob(n=n, m=m, P_scale=100.0, A_scale=100.0) 168 | P, q, A, l, u, true_x, true_y = prob 169 | l[:5] = u[:5] 170 | l[5:] = -osqp.constant('OSQP_INFTY', algebra='builtin') 171 | 172 | def grad(dq): 173 | [dx, dyl, dyu] = self.get_forward_grads(P, q, A, l, u, None, dq, None, None, None) 174 | return dx, dyl, dyu 175 | 176 | dq = np.random.normal(size=(n)) 177 | dx_computed, dyl_computed, dyu_computed = grad(dq) 178 | osqp_solver = osqp.OSQP(algebra='builtin') 179 | osqp_solver.setup( 180 | P, 181 | q, 182 | A, 183 | l, 184 | u, 185 | eps_abs=eps_abs, 186 | eps_rel=eps_rel, 187 | max_iter=max_iter, 188 | verbose=False, 189 | ) 190 | res = osqp_solver.solve() 191 | if res.info.status_val != osqp.SolverStatus.OSQP_SOLVED: 192 | raise ValueError('Problem not solved!') 193 | x1 = res.x 194 | y1 = res.y 195 | 196 | eps = grad_precision 197 | osqp_solver.setup( 198 | P, 199 | q + eps * dq, 200 | A, 201 | l, 202 | u, 203 | eps_abs=eps_abs, 204 | eps_rel=eps_rel, 205 | max_iter=max_iter, 206 | verbose=False, 207 | ) 208 | res = osqp_solver.solve() 209 | if res.info.status_val != osqp.SolverStatus.OSQP_SOLVED: 210 | raise ValueError('Problem not solved!') 211 | x2 = res.x 212 | y2 = res.y 213 | 214 | dx_fd = (x2 - x1) / eps 215 | dy_fd = (y2 - y1) / eps 216 | dyl_fd = np.zeros(m) 217 | dyl_fd[y1 < 0] = -dy_fd[y1 < 0] 218 | dyu_fd = np.zeros(m) 219 | dyu_fd[y1 >= 0] = dy_fd[y1 >= 0] 220 | 221 | if verbose: 222 | print('dx_fd: ', np.round(dx_fd, decimals=4)) 223 | print('dx: ', np.round(dx_computed, decimals=4)) 224 | 225 | npt.assert_allclose(dx_fd, dx_computed, rtol=rel_tol, atol=abs_tol) 226 | npt.assert_allclose(dyl_fd, dyl_computed, rtol=rel_tol, atol=abs_tol) 227 | npt.assert_allclose(dyu_fd, dyu_computed, rtol=rel_tol, atol=abs_tol) 228 | 229 | @pytest.mark.skip(reason='forward derivatives not implemented yet') 230 | def test_multiple_forward_derivative(self, verbose=False): 231 | n, m = 5, 5 232 | 233 | prob = self.get_prob(n=n, m=m, P_scale=100.0, A_scale=100.0) 234 | P, q, A, l, u, true_x, true_y = prob 235 | 236 | def grad(dP, dq, dA, dl, du): 237 | [dx, dyl, dyu] = self.get_forward_grads(P, q, A, l, u, dP, dq, dA, dl, du) 238 | return dx, dyl, dyu 239 | 240 | dq = np.random.normal(size=(n)) 241 | dA = sparse.csc_matrix(np.random.normal(size=(m, n))) 242 | dl = np.random.normal(size=(m)) 243 | du = np.random.normal(size=(m)) 244 | dL = np.random.normal(size=(n, n)) 245 | dP = dL + dL.T 246 | dx_computed, dyl_computed, dyu_computed = grad(dP, dq, dA, dl, du) 247 | osqp_solver = osqp.OSQP(algebra='builtin') 248 | osqp_solver.setup( 249 | P, 250 | q, 251 | A, 252 | l, 253 | u, 254 | eps_abs=eps_abs, 255 | eps_rel=eps_rel, 256 | max_iter=max_iter, 257 | verbose=False, 258 | ) 259 | res = osqp_solver.solve() 260 | if res.info.status_val != osqp.SolverStatus.OSQP_SOLVED: 261 | raise ValueError('Problem not solved!') 262 | x1 = res.x 263 | y1 = res.y 264 | 265 | eps = grad_precision 266 | osqp_solver.setup( 267 | P + eps * dP, 268 | q + eps * dq, 269 | A + eps * dA, 270 | l + eps * dl, 271 | u + eps * du, 272 | eps_abs=eps_abs, 273 | eps_rel=eps_rel, 274 | max_iter=max_iter, 275 | verbose=False, 276 | ) 277 | res = osqp_solver.solve() 278 | if res.info.status_val != osqp.SolverStatus.OSQP_SOLVED: 279 | raise ValueError('Problem not solved!') 280 | x2 = res.x 281 | y2 = res.y 282 | 283 | dx_fd = (x2 - x1) / eps 284 | dy_fd = (y2 - y1) / eps 285 | dyl_fd = np.zeros(m) 286 | dyl_fd[y1 < 0] = -dy_fd[y1 < 0] 287 | dyu_fd = np.zeros(m) 288 | dyu_fd[y1 >= 0] = dy_fd[y1 >= 0] 289 | 290 | if verbose: 291 | print('dx_fd: ', np.round(dx_fd, decimals=4)) 292 | print('dx: ', np.round(dx_computed, decimals=4)) 293 | 294 | npt.assert_allclose(dx_fd, dx_computed, rtol=rel_tol, atol=abs_tol) 295 | npt.assert_allclose(dyl_fd, dyl_computed, rtol=rel_tol, atol=abs_tol) 296 | npt.assert_allclose(dyu_fd, dyu_computed, rtol=rel_tol, atol=abs_tol) 297 | 298 | def test_dl_dq(self, verbose=False): 299 | n, m = 5, 5 300 | 301 | prob = self.get_prob(n=n, m=m, P_scale=100.0, A_scale=100.0) 302 | P, q, A, l, u, true_x, true_y = prob 303 | 304 | def grad(q): 305 | dP, dq, _, _, _ = self.get_grads(P, q, A, l, u, true_x) 306 | return dq 307 | 308 | def f(q): 309 | m = osqp.OSQP(algebra='builtin') 310 | m.setup( 311 | P, 312 | q, 313 | A, 314 | l, 315 | u, 316 | eps_abs=eps_abs, 317 | eps_rel=eps_rel, 318 | max_iter=max_iter, 319 | verbose=False, 320 | ) 321 | res = m.solve() 322 | if res.info.status_val != osqp.SolverStatus.OSQP_SOLVED: 323 | raise ValueError('Problem not solved!') 324 | x_hat = res.x 325 | 326 | return 0.5 * np.sum(np.square(x_hat - true_x)) 327 | 328 | dq = grad(q) 329 | dq_fd = approx_fprime(q, f, grad_precision) 330 | 331 | if verbose: 332 | print('dq_fd: ', np.round(dq_fd, decimals=4)) 333 | print('dq: ', np.round(dq, decimals=4)) 334 | 335 | npt.assert_allclose(dq_fd, dq, rtol=rel_tol, atol=abs_tol) 336 | 337 | def test_dl_dP(self, verbose=False): 338 | n, m = 3, 3 339 | 340 | prob = self.get_prob(n=n, m=m, P_scale=100.0, A_scale=100.0) 341 | P, q, A, l, u, true_x, true_y = prob 342 | P_idx = P.nonzero() 343 | 344 | def grad(P_val): 345 | P_qp = sparse.csc_matrix((P_val, P_idx), shape=P.shape) 346 | dP, _, _, _, _ = self.get_grads(P_qp, q, A, l, u, true_x) 347 | return dP 348 | 349 | def f(P_val): 350 | P_qp = sparse.csc_matrix((P_val, P_idx), shape=P.shape) 351 | m = osqp.OSQP(algebra='builtin') 352 | m.setup( 353 | P_qp, 354 | q, 355 | A, 356 | l, 357 | u, 358 | eps_abs=eps_abs, 359 | eps_rel=eps_rel, 360 | max_iter=max_iter, 361 | verbose=False, 362 | ) 363 | res = m.solve() 364 | if res.info.status_val != osqp.SolverStatus.OSQP_SOLVED: 365 | raise ValueError('Problem not solved!') 366 | x_hat = res.x 367 | 368 | return 0.5 * np.sum(np.square(x_hat - true_x)) 369 | 370 | dP = grad(P.data) 371 | dP_fd_val = approx_fprime(P.data, f, grad_precision) 372 | dP_fd = sparse.csc_matrix((dP_fd_val, P_idx), shape=P.shape) 373 | dP_fd = (dP_fd + dP_fd.T) / 2 374 | 375 | if verbose: 376 | print('dP_fd: ', np.round(dP_fd.data, decimals=4)) 377 | print('dA: ', np.round(dP.data, decimals=4)) 378 | 379 | npt.assert_allclose(np.triu(dP), np.triu(dP_fd.todense()), rtol=rel_tol, atol=abs_tol) 380 | 381 | def test_dl_dA(self, verbose=False): 382 | n, m = 3, 3 383 | 384 | prob = self.get_prob(n=n, m=m, P_scale=100.0, A_scale=100.0) 385 | P, q, A, l, u, true_x, true_y = prob 386 | A_idx = A.nonzero() 387 | 388 | def grad(A_val): 389 | A_qp = sparse.csc_matrix((A_val, A_idx), shape=A.shape) 390 | _, _, dA, _, _ = self.get_grads(P, q, A_qp, l, u, true_x) 391 | return dA 392 | 393 | def f(A_val): 394 | A_qp = sparse.csc_matrix((A_val, A_idx), shape=A.shape) 395 | m = osqp.OSQP(algebra='builtin') 396 | m.setup( 397 | P, 398 | q, 399 | A_qp, 400 | l, 401 | u, 402 | eps_abs=eps_abs, 403 | eps_rel=eps_rel, 404 | max_iter=max_iter, 405 | verbose=False, 406 | ) 407 | res = m.solve() 408 | if res.info.status_val != osqp.SolverStatus.OSQP_SOLVED: 409 | raise ValueError('Problem not solved!') 410 | x_hat = res.x 411 | 412 | return 0.5 * np.sum(np.square(x_hat - true_x)) 413 | 414 | dA = grad(A.data) 415 | dA_fd_val = approx_fprime(A.data, f, grad_precision) 416 | dA_fd = sparse.csc_matrix((dA_fd_val, A_idx), shape=A.shape) 417 | 418 | if verbose: 419 | print('dA_fd: ', np.round(dA_fd.data, decimals=4)) 420 | print('dA: ', np.round(dA.data, decimals=4)) 421 | 422 | npt.assert_allclose(dA, dA_fd.todense(), rtol=rel_tol, atol=abs_tol) 423 | 424 | def test_dl_dl(self, verbose=False): 425 | n, m = 30, 30 426 | 427 | prob = self.get_prob(n=n, m=m, P_scale=100.0, A_scale=100.0) 428 | P, q, A, l, u, true_x, true_y = prob 429 | 430 | def grad(l): 431 | _, _, _, dl, _ = self.get_grads(P, q, A, l, u, true_x) 432 | return dl 433 | 434 | def f(l): 435 | m = osqp.OSQP(algebra='builtin') 436 | m.setup( 437 | P, 438 | q, 439 | A, 440 | l, 441 | u, 442 | eps_abs=eps_abs, 443 | eps_rel=eps_rel, 444 | max_iter=max_iter, 445 | verbose=False, 446 | ) 447 | res = m.solve() 448 | if res.info.status_val != osqp.SolverStatus.OSQP_SOLVED: 449 | raise ValueError('Problem not solved!') 450 | x_hat = res.x 451 | 452 | return 0.5 * np.sum(np.square(x_hat - true_x)) 453 | 454 | dl_computed = grad(l) 455 | dl_fd = approx_fprime(l, f, grad_precision) 456 | 457 | if verbose: 458 | print('dl_fd: ', np.round(dl_fd, decimals=4).tolist()) 459 | print('dl_computed: ', np.round(dl_computed, decimals=4).tolist()) 460 | 461 | npt.assert_allclose(dl_fd, dl_computed, rtol=rel_tol, atol=abs_tol) 462 | 463 | def test_dl_du(self, verbose=False): 464 | n, m = 10, 20 465 | 466 | prob = self.get_prob(n=n, m=m, P_scale=100.0, A_scale=100.0) 467 | P, q, A, l, u, true_x, true_y = prob 468 | 469 | def grad(u): 470 | _, _, _, _, du = self.get_grads(P, q, A, l, u, true_x) 471 | return du 472 | 473 | def f(u): 474 | m = osqp.OSQP(algebra='builtin') 475 | m.setup( 476 | P, 477 | q, 478 | A, 479 | l, 480 | u, 481 | eps_abs=eps_abs, 482 | eps_rel=eps_rel, 483 | max_iter=max_iter, 484 | verbose=False, 485 | ) 486 | res = m.solve() 487 | if res.info.status_val != osqp.SolverStatus.OSQP_SOLVED: 488 | raise ValueError('Problem not solved!') 489 | x_hat = res.x 490 | 491 | return 0.5 * np.sum(np.square(x_hat - true_x)) 492 | 493 | du_computed = grad(u) 494 | du_fd = approx_fprime(u, f, grad_precision) 495 | 496 | if verbose: 497 | print('du_fd: ', np.round(du_fd, decimals=4)) 498 | print('du: ', np.round(du_computed, decimals=4)) 499 | 500 | npt.assert_allclose(du_fd, du_computed, rtol=rel_tol, atol=abs_tol) 501 | 502 | def test_dl_dA_eq(self, verbose=False): 503 | n, m = 30, 20 504 | 505 | prob = self.get_prob(n=n, m=m, P_scale=100.0, A_scale=100.0) 506 | P, q, A, l, u, true_x, true_y = prob 507 | # u = l 508 | # l[10:20] = -osqp.constant('OSQP_INFTY', algebra='builtin') 509 | u[:10] = l[:10] 510 | 511 | A_idx = A.nonzero() 512 | 513 | def grad(A_val): 514 | A_qp = sparse.csc_matrix((A_val, A_idx), shape=A.shape) 515 | _, _, dA, _, _ = self.get_grads(P, q, A_qp, l, u, true_x) 516 | return dA 517 | 518 | def f(A_val): 519 | A_qp = sparse.csc_matrix((A_val, A_idx), shape=A.shape) 520 | m = osqp.OSQP(algebra='builtin') 521 | m.setup( 522 | P, 523 | q, 524 | A_qp, 525 | l, 526 | u, 527 | eps_abs=eps_abs, 528 | eps_rel=eps_rel, 529 | max_iter=max_iter, 530 | verbose=False, 531 | ) 532 | res = m.solve() 533 | if res.info.status_val != osqp.SolverStatus.OSQP_SOLVED: 534 | raise ValueError('Problem not solved!') 535 | x_hat = res.x 536 | 537 | return 0.5 * np.sum(np.square(x_hat - true_x)) 538 | 539 | dA_computed = grad(A.data) 540 | dA_fd_val = approx_fprime(A.data, f, grad_precision) 541 | dA_fd = sparse.csc_matrix((dA_fd_val, A_idx), shape=A.shape) 542 | 543 | if verbose: 544 | print('dA_fd: ', np.round(dA_fd.data, decimals=6)) 545 | print('dA_computed: ', np.round(dA_computed.data, decimals=6)) 546 | 547 | npt.assert_allclose(dA_computed, dA_fd.todense(), rtol=rel_tol, atol=abs_tol) 548 | 549 | def test_dl_dq_eq(self, verbose=False): 550 | n, m = 20, 15 551 | 552 | prob = self.get_prob(n=n, m=m, P_scale=1.0, A_scale=1.0) 553 | P, q, A, l, u, true_x, true_y = prob 554 | # u = l 555 | # l[20:40] = -osqp.constant('OSQP_INFTY', algebra='builtin') 556 | u[:20] = l[:20] 557 | 558 | def grad(q): 559 | _, dq, _, _, _ = self.get_grads(P, q, A, l, u, true_x) 560 | return dq 561 | 562 | def f(q): 563 | m = osqp.OSQP(algebra='builtin') 564 | m.setup( 565 | P, 566 | q, 567 | A, 568 | l, 569 | u, 570 | eps_abs=eps_abs, 571 | eps_rel=eps_rel, 572 | max_iter=max_iter, 573 | verbose=False, 574 | ) 575 | res = m.solve() 576 | if res.info.status_val != osqp.SolverStatus.OSQP_SOLVED: 577 | raise ValueError('Problem not solved!') 578 | x_hat = res.x 579 | 580 | return 0.5 * np.sum(np.square(x_hat - true_x)) 581 | 582 | dq_computed = grad(q) 583 | dq_fd = approx_fprime(q, f, grad_precision) 584 | 585 | if verbose: 586 | print('dq_fd: ', np.round(dq_fd, decimals=4)) 587 | print('dq_computed: ', np.round(dq_computed, decimals=4)) 588 | 589 | npt.assert_allclose(dq_fd, dq_computed, rtol=rel_tol, atol=abs_tol) 590 | 591 | def test_dl_dq_eq_large(self, verbose=False): 592 | n, m = 100, 120 593 | 594 | prob = self.get_prob(n=n, m=m, P_scale=1.0, A_scale=1.0) 595 | P, q, A, l, u, true_x, true_y = prob 596 | 597 | l[20:40] = -osqp.constant('OSQP_INFTY', algebra='builtin') 598 | u[:20] = l[:20] 599 | 600 | def grad(q): 601 | _, dq, _, _, _ = self.get_grads(P, q, A, l, u, true_x) 602 | return dq 603 | 604 | def f(q): 605 | m = osqp.OSQP(algebra='builtin') 606 | m.setup( 607 | P, 608 | q, 609 | A, 610 | l, 611 | u, 612 | eps_abs=eps_abs, 613 | eps_rel=eps_rel, 614 | max_iter=max_iter, 615 | verbose=False, 616 | ) 617 | res = m.solve() 618 | if res.info.status_val != osqp.SolverStatus.OSQP_SOLVED: 619 | raise ValueError('Problem not solved!') 620 | x_hat = res.x 621 | 622 | return 0.5 * np.sum(np.square(x_hat - true_x)) 623 | 624 | dq_computed = grad(q) 625 | dq_fd = approx_fprime(q, f, grad_precision) 626 | 627 | if verbose: 628 | print('dq_fd: ', np.round(dq_fd, decimals=4)) 629 | print('dq_computed: ', np.round(dq_computed, decimals=4)) 630 | 631 | npt.assert_allclose(dq_fd, dq_computed, rtol=rel_tol_relaxed, atol=abs_tol_relaxed) 632 | 633 | def _test_dl_dq_nonzero_dy(self, verbose=False): 634 | n, m = 6, 3 635 | 636 | prob = self.get_prob(n=n, m=m, P_scale=1.0, A_scale=1.0) 637 | P, q, A, l, u, true_x, true_y = prob 638 | # u = l 639 | # l[20:40] = -osqp.constant('OSQP_INFTY', algebra='builtin') 640 | num_eq = 2 641 | u[:num_eq] = l[:num_eq] 642 | 643 | def grad(q): 644 | _, dq, _, _, _ = self.get_grads(P, q, A, l, u, true_x, true_y) 645 | return dq 646 | 647 | def f(q): 648 | m = osqp.OSQP(algebra='builtin') 649 | m.setup( 650 | P, 651 | q, 652 | A, 653 | l, 654 | u, 655 | eps_abs=eps_abs, 656 | eps_rel=eps_rel, 657 | max_iter=max_iter, 658 | verbose=False, 659 | ) 660 | res = m.solve() 661 | if res.info.status_val != osqp.SolverStatus.OSQP_SOLVED: 662 | raise ValueError('Problem not solved!') 663 | x_hat = res.x 664 | y_hat = res.y 665 | yu_hat = np.maximum(y_hat, 0) 666 | yl_hat = -np.minimum(y_hat, 0) 667 | 668 | true_yu = np.maximum(true_y, 0) 669 | true_yl = -np.minimum(true_y, 0) 670 | # return 0.5 * np.sum(np.square(x_hat - true_x)) + np.sum(yl_hat) + np.sum(yu_hat) 671 | return 0.5 * ( 672 | np.sum(np.square(x_hat - true_x)) 673 | + np.sum(np.square(yl_hat - true_yl)) 674 | + np.sum(np.square(yu_hat - true_yu)) 675 | ) 676 | 677 | dq_computed = grad(q) 678 | dq_fd = approx_fprime(q, f, grad_precision) 679 | 680 | if verbose: 681 | print('dq_fd: ', np.round(dq_fd, decimals=4)) 682 | print('dq_computed: ', np.round(dq_computed, decimals=4)) 683 | 684 | npt.assert_allclose(dq_fd, dq_computed, rtol=rel_tol, atol=abs_tol) 685 | -------------------------------------------------------------------------------- /src/osqp/tests/dual_infeasibility_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | from types import SimpleNamespace 3 | import osqp 4 | import numpy as np 5 | from scipy import sparse 6 | 7 | import pytest 8 | 9 | 10 | @pytest.fixture 11 | def self(algebra, solver_type, atol, rtol, decimal_tol): 12 | ns = SimpleNamespace() 13 | ns.opts = { 14 | 'verbose': False, 15 | 'eps_abs': 1e-05, 16 | 'eps_rel': 1e-05, 17 | 'eps_prim_inf': 1e-15, # Focus only on dual infeasibility 18 | 'eps_dual_inf': 1e-6, 19 | 'scaling': 3, 20 | 'max_iter': 2500, 21 | 'polishing': False, 22 | 'check_termination': 1, 23 | 'polish_refine_iter': 4, 24 | 'solver_type': solver_type, 25 | } 26 | 27 | ns.model = osqp.OSQP(algebra=algebra) 28 | return ns 29 | 30 | 31 | def test_dual_infeasible_lp(self): 32 | # Dual infeasible example 33 | self.P = sparse.csc_matrix((2, 2)) 34 | self.q = np.array([2, -1]) 35 | self.A = sparse.eye(2, format='csc') 36 | self.l = np.array([0.0, 0.0]) 37 | self.u = np.array([np.inf, np.inf]) 38 | 39 | self.model.setup(P=self.P, q=self.q, A=self.A, l=self.l, u=self.u, **self.opts) 40 | 41 | # Solve problem with OSQP 42 | res = self.model.solve() 43 | 44 | assert res.info.status_val == self.model.constant('OSQP_DUAL_INFEASIBLE') 45 | 46 | normalized_dual_inf_cert = res.dual_inf_cert / np.linalg.norm(res.dual_inf_cert) 47 | normalized_dual_inf_cert_correct = np.load( 48 | os.path.join(os.path.dirname(__file__), 'solutions', 'test_dual_infeasibility.npz') 49 | )['lp_normalized_dual_inf_cert_correct'] 50 | assert np.allclose(normalized_dual_inf_cert, normalized_dual_inf_cert_correct) 51 | 52 | 53 | def test_dual_infeasible_qp(self): 54 | # Dual infeasible example 55 | self.P = sparse.diags([4.0, 0.0], format='csc') 56 | self.q = np.array([0, 2]) 57 | self.A = sparse.csc_matrix([[1.0, 1.0], [-1.0, 1.0]]) 58 | self.l = np.array([-np.inf, -np.inf]) 59 | self.u = np.array([2.0, 3.0]) 60 | 61 | self.model.setup(P=self.P, q=self.q, A=self.A, l=self.l, u=self.u, **self.opts) 62 | 63 | # Solve problem with OSQP 64 | res = self.model.solve() 65 | 66 | assert res.info.status_val == self.model.constant('OSQP_DUAL_INFEASIBLE') 67 | 68 | normalized_dual_inf_cert = res.dual_inf_cert / np.linalg.norm(res.dual_inf_cert) 69 | normalized_dual_inf_cert_correct = np.load( 70 | os.path.join(os.path.dirname(__file__), 'solutions', 'test_dual_infeasibility.npz') 71 | )['qp_normalized_dual_inf_cert_correct'] 72 | assert np.allclose(normalized_dual_inf_cert, normalized_dual_inf_cert_correct) 73 | 74 | 75 | def test_primal_and_dual_infeasible_problem(self): 76 | self.n = 2 77 | self.m = 4 78 | self.P = sparse.csc_matrix((2, 2)) 79 | self.q = np.array([-1.0, -1.0]) 80 | self.A = sparse.csc_matrix([[1.0, -1.0], [-1.0, 1.0], [1.0, 0.0], [0.0, 1.0]]) 81 | self.l = np.array([1.0, 1.0, 0.0, 0.0]) 82 | self.u = np.inf * np.ones(self.m) 83 | 84 | self.model.setup(P=self.P, q=self.q, A=self.A, l=self.l, u=self.u, **self.opts) 85 | 86 | # Warm start to avoid infeasibility detection at first step 87 | x0 = 25.0 * np.ones(self.n) 88 | y0 = -2.0 * np.ones(self.m) 89 | self.model.warm_start(x=x0, y=y0) 90 | 91 | # Solve 92 | res = self.model.solve() 93 | 94 | assert res.info.status_val in ( 95 | self.model.constant('OSQP_PRIMAL_INFEASIBLE'), 96 | self.model.constant('OSQP_DUAL_INFEASIBLE'), 97 | ) 98 | -------------------------------------------------------------------------------- /src/osqp/tests/feasibility_test.py: -------------------------------------------------------------------------------- 1 | from types import SimpleNamespace 2 | import osqp 3 | import numpy as np 4 | from scipy import sparse 5 | import pytest 6 | import numpy.testing as nptest 7 | from osqp.tests.utils import load_high_accuracy 8 | 9 | 10 | @pytest.fixture 11 | def self(algebra, solver_type, atol, rtol, decimal_tol): 12 | self = SimpleNamespace() 13 | 14 | np.random.seed(4) 15 | 16 | self.n = 30 17 | self.m = 30 18 | self.P = sparse.csc_matrix((self.n, self.n)) 19 | self.q = np.zeros(self.n) 20 | self.A = sparse.random(self.m, self.n, density=1.0, format='csc') 21 | self.u = np.random.rand(self.m) 22 | self.l = self.u 23 | self.opts = { 24 | 'verbose': False, 25 | 'eps_abs': 1e-06, 26 | 'eps_rel': 1e-06, 27 | 'scaling': True, 28 | 'alpha': 1.6, 29 | 'max_iter': 5000, 30 | 'polishing': False, 31 | 'warm_starting': True, 32 | 'polish_refine_iter': 4, 33 | 'solver_type': solver_type, 34 | } 35 | 36 | self.model = osqp.OSQP(algebra=algebra) 37 | self.model.setup(P=self.P, q=self.q, A=self.A, l=self.l, u=self.u, **self.opts) 38 | 39 | self.rtol = rtol 40 | self.atol = atol 41 | self.decimal_tol = decimal_tol 42 | 43 | return self 44 | 45 | 46 | def test_feasibility_problem(self): 47 | res = self.model.solve() 48 | 49 | x_sol, y_sol, obj_sol = load_high_accuracy('test_feasibility_problem') 50 | 51 | if self.model.solver_type == 'direct': # pytest-todo 52 | nptest.assert_allclose(res.x, x_sol, rtol=self.rtol, atol=self.atol) 53 | nptest.assert_allclose(res.y, y_sol, rtol=self.rtol, atol=self.atol) 54 | nptest.assert_almost_equal(res.info.obj_val, obj_sol, decimal=self.decimal_tol) 55 | else: 56 | assert res.info.status_val == self.model.constant('OSQP_MAX_ITER_REACHED') 57 | -------------------------------------------------------------------------------- /src/osqp/tests/multithread_test.py: -------------------------------------------------------------------------------- 1 | import osqp 2 | from multiprocessing.pool import ThreadPool 3 | import time 4 | import numpy as np 5 | from scipy import sparse 6 | import unittest 7 | import pytest 8 | 9 | 10 | @pytest.mark.skipif(not osqp.algebra_available('builtin'), reason='Builtin Algebra not available') 11 | class multithread_tests(unittest.TestCase): 12 | def test_multithread(self): 13 | data = [] 14 | 15 | n_rep = 50 16 | 17 | for i in range(n_rep): 18 | m = 1000 19 | n = 500 20 | Ad = sparse.random(m, n, density=0.3, format='csc') 21 | b = np.random.randn(m) 22 | 23 | # OSQP data 24 | P = sparse.block_diag([sparse.csc_matrix((n, n)), sparse.eye(m)], format='csc') 25 | q = np.zeros(n + m) 26 | A = sparse.vstack( 27 | [ 28 | sparse.hstack([Ad, -sparse.eye(m)]), 29 | sparse.hstack([sparse.eye(n), sparse.csc_matrix((n, m))]), 30 | ], 31 | format='csc', 32 | ) 33 | l = np.hstack([b, np.zeros(n)]) 34 | u = np.hstack([b, np.ones(n)]) 35 | 36 | data.append((P, q, A, l, u)) 37 | 38 | def f(i): 39 | P, q, A, l, u = data[i] 40 | m = osqp.OSQP(algebra='builtin') 41 | m.setup(P, q, A, l, u, verbose=False) 42 | m.solve() 43 | 44 | pool = ThreadPool(2) 45 | 46 | tic = time.time() 47 | for i in range(n_rep): 48 | f(i) 49 | t_serial = time.time() - tic 50 | 51 | tic = time.time() 52 | pool.map(f, range(n_rep)) 53 | t_parallel = time.time() - tic 54 | 55 | self.assertLess(t_parallel, t_serial) 56 | -------------------------------------------------------------------------------- /src/osqp/tests/nn_test.py: -------------------------------------------------------------------------------- 1 | import numpy.random as npr 2 | import numpy as np 3 | import torch 4 | import numpy.testing as npt 5 | import scipy.sparse as spa 6 | from scipy.optimize import approx_fprime 7 | import pytest 8 | 9 | import osqp 10 | from osqp.nn.torch import OSQP 11 | 12 | ATOL = 1e-2 13 | RTOL = 1e-4 14 | EPS = 1e-5 15 | 16 | cuda = False 17 | verbose = True 18 | 19 | 20 | # Note (02/13/24) 21 | # Some versions of Python/torch/numpy cannot coexist on certain platforms. 22 | # This is a problem seen with numpy>=2. 23 | # Support is gradually being added. Rather than keep track of which versions 24 | # are supported and on what platforms (which is likely to change frequently), 25 | # we do an early check that is seen to raise RuntimeError in these cases, 26 | # and skip testing this module entirely. 27 | try: 28 | torch.ones(1).cpu().numpy() 29 | except RuntimeError: 30 | pytest.skip('torch/numpy mutual incompatibility', allow_module_level=True) 31 | 32 | 33 | def get_grads( 34 | n_batch=1, 35 | n=10, 36 | m=3, 37 | P_scale=1.0, 38 | A_scale=1.0, 39 | u_scale=1.0, 40 | l_scale=1.0, 41 | algebra=None, 42 | solver_type=None, 43 | ): 44 | assert n_batch == 1 45 | npr.seed(1) 46 | L = np.random.randn(n, n) 47 | P = spa.csc_matrix(P_scale * L.dot(L.T)) 48 | x_0 = npr.randn(n) 49 | s_0 = npr.rand(m) 50 | A = spa.csc_matrix(A_scale * npr.randn(m, n)) 51 | u = A.dot(x_0) + A_scale * s_0 52 | l = -10 * A_scale * npr.rand(m) 53 | q = npr.randn(n) 54 | true_x = npr.randn(n) 55 | 56 | P, q, A, l, u, true_x = [x.astype(np.float64) for x in [P, q, A, l, u, true_x]] 57 | 58 | grads = get_grads_torch(P, q, A, l, u, true_x, algebra, solver_type) 59 | return [P, q, A, l, u, true_x], grads 60 | 61 | 62 | def get_grads_torch(P, q, A, l, u, true_x, algebra, solver_type): 63 | P_idx = P.nonzero() 64 | P_shape = P.shape 65 | A_idx = A.nonzero() 66 | A_shape = A.shape 67 | 68 | P_torch, q_torch, A_torch, l_torch, u_torch, true_x_torch = [ 69 | torch.DoubleTensor(x) if len(x) > 0 else torch.DoubleTensor() for x in [P.data, q, A.data, l, u, true_x] 70 | ] 71 | if cuda: 72 | P_torch, q_torch, A_torch, l_torch, u_torch, true_x_torch = [ 73 | x.cuda() for x in [P.data, q, A.data, l, u, true_x] 74 | ] 75 | 76 | for x in [P_torch, q_torch, A_torch, l_torch, u_torch]: 77 | x.requires_grad = True 78 | 79 | x_hats = OSQP( 80 | P_idx, 81 | P_shape, 82 | A_idx, 83 | A_shape, 84 | algebra=algebra, 85 | solver_type=solver_type, 86 | )(P_torch, q_torch, A_torch, l_torch, u_torch) 87 | 88 | dl_dxhat = x_hats.data - true_x_torch 89 | x_hats.backward(dl_dxhat) 90 | 91 | grads = [x.grad.data.squeeze(0).cpu().numpy() for x in [P_torch, q_torch, A_torch, l_torch, u_torch]] 92 | return grads 93 | 94 | 95 | def test_dl_dq(algebra, solver_type, atol, rtol, decimal_tol): 96 | n, m = 5, 5 97 | 98 | model = osqp.OSQP(algebra=algebra) 99 | if not model.has_capability('OSQP_CAPABILITY_DERIVATIVES'): 100 | pytest.skip('No derivatives capability') 101 | 102 | [P, q, A, l, u, true_x], [dP, dq, dA, dl, du] = get_grads( 103 | n=n, 104 | m=m, 105 | P_scale=100.0, 106 | A_scale=100.0, 107 | algebra=algebra, 108 | solver_type=solver_type, 109 | ) 110 | 111 | def f(q): 112 | model.setup(P, q, A, l, u, solver_type=solver_type, verbose=False) 113 | res = model.solve() 114 | x_hat = res.x 115 | 116 | return 0.5 * np.sum(np.square(x_hat - true_x)) 117 | 118 | dq_fd = approx_fprime(q, f, epsilon=EPS) 119 | if verbose: 120 | print('dq_fd: ', np.round(dq_fd, decimals=4)) 121 | print('dq: ', np.round(dq, decimals=4)) 122 | npt.assert_allclose(dq_fd, dq, rtol=RTOL, atol=ATOL) 123 | 124 | 125 | def test_dl_dP(algebra, solver_type, atol, rtol, decimal_tol): 126 | n, m = 5, 5 127 | 128 | model = osqp.OSQP(algebra=algebra) 129 | if not model.has_capability('OSQP_CAPABILITY_DERIVATIVES'): 130 | pytest.skip('No derivatives capability') 131 | 132 | [P, q, A, l, u, true_x], [dP, dq, dA, dl, du] = get_grads( 133 | n=n, 134 | m=m, 135 | P_scale=100.0, 136 | A_scale=100.0, 137 | algebra=algebra, 138 | solver_type=solver_type, 139 | ) 140 | 141 | def f(P): 142 | P = P.reshape(n, n) 143 | P = spa.csc_matrix(P) 144 | model.setup(P, q, A, l, u, solver_type=solver_type, verbose=False) 145 | res = model.solve() 146 | x_hat = res.x 147 | 148 | return 0.5 * np.sum(np.square(x_hat - true_x)) 149 | 150 | dP_fd = approx_fprime(P.toarray().flatten(), f, epsilon=EPS) 151 | if verbose: 152 | print('dP_fd: ', np.round(dP_fd, decimals=4)) 153 | print('dP: ', np.round(dP, decimals=4)) 154 | npt.assert_allclose(dP_fd, dP, rtol=RTOL, atol=ATOL) 155 | 156 | 157 | def test_dl_dA(algebra, solver_type, atol, rtol, decimal_tol): 158 | n, m = 5, 5 159 | 160 | model = osqp.OSQP(algebra=algebra) 161 | if not model.has_capability('OSQP_CAPABILITY_DERIVATIVES'): 162 | pytest.skip('No derivatives capability') 163 | 164 | [P, q, A, l, u, true_x], [dP, dq, dA, dl, du] = get_grads( 165 | n=n, 166 | m=m, 167 | P_scale=100.0, 168 | A_scale=100.0, 169 | algebra=algebra, 170 | solver_type=solver_type, 171 | ) 172 | 173 | def f(A): 174 | A = A.reshape((m, n)) 175 | A = spa.csc_matrix(A) 176 | model.setup(P, q, A, l, u, solver_type=solver_type, verbose=False) 177 | res = model.solve() 178 | x_hat = res.x 179 | 180 | return 0.5 * np.sum(np.square(x_hat - true_x)) 181 | 182 | dA_fd = approx_fprime(A.toarray().flatten(), f, epsilon=EPS) 183 | if verbose: 184 | print('dA_fd: ', np.round(dA_fd, decimals=4)) 185 | print('dA: ', np.round(dA, decimals=4)) 186 | npt.assert_allclose(dA_fd, dA, rtol=RTOL, atol=ATOL) 187 | 188 | 189 | def test_dl_dl(algebra, solver_type, atol, rtol, decimal_tol): 190 | n, m = 5, 5 191 | 192 | model = osqp.OSQP(algebra=algebra) 193 | if not model.has_capability('OSQP_CAPABILITY_DERIVATIVES'): 194 | pytest.skip('No derivatives capability') 195 | 196 | [P, q, A, l, u, true_x], [dP, dq, dA, dl, du] = get_grads( 197 | n=n, 198 | m=m, 199 | P_scale=100.0, 200 | A_scale=100.0, 201 | algebra=algebra, 202 | solver_type=solver_type, 203 | ) 204 | 205 | def f(l): 206 | model.setup(P, q, A, l, u, solver_type=solver_type, verbose=False) 207 | res = model.solve() 208 | x_hat = res.x 209 | 210 | return 0.5 * np.sum(np.square(x_hat - true_x)) 211 | 212 | dl_fd = approx_fprime(l, f, epsilon=EPS) 213 | if verbose: 214 | print('dl_fd: ', np.round(dl_fd, decimals=4)) 215 | print('dl: ', np.round(dl, decimals=4)) 216 | npt.assert_allclose(dl_fd, dl, rtol=RTOL, atol=ATOL) 217 | 218 | 219 | def test_dl_du(algebra, solver_type, atol, rtol, decimal_tol): 220 | n, m = 5, 5 221 | 222 | model = osqp.OSQP(algebra=algebra) 223 | if not model.has_capability('OSQP_CAPABILITY_DERIVATIVES'): 224 | pytest.skip('No derivatives capability') 225 | 226 | [P, q, A, l, u, true_x], [dP, dq, dA, dl, du] = get_grads( 227 | n=n, 228 | m=m, 229 | P_scale=100.0, 230 | A_scale=100.0, 231 | algebra=algebra, 232 | solver_type=solver_type, 233 | ) 234 | 235 | def f(u): 236 | model.setup(P, q, A, l, u, solver_type=solver_type, verbose=False) 237 | res = model.solve() 238 | x_hat = res.x 239 | 240 | return 0.5 * np.sum(np.square(x_hat - true_x)) 241 | 242 | du_fd = approx_fprime(u, f, epsilon=EPS) 243 | if verbose: 244 | print('du_fd: ', np.round(du_fd, decimals=4)) 245 | print('du: ', np.round(du, decimals=4)) 246 | npt.assert_allclose(du_fd, du, rtol=RTOL, atol=ATOL) 247 | -------------------------------------------------------------------------------- /src/osqp/tests/non_convex_test.py: -------------------------------------------------------------------------------- 1 | from types import SimpleNamespace 2 | import osqp 3 | import numpy as np 4 | from scipy import sparse 5 | 6 | import pytest 7 | import numpy.testing as nptest 8 | 9 | 10 | @pytest.fixture 11 | def self(algebra, solver_type, atol, rtol, decimal_tol): 12 | ns = SimpleNamespace() 13 | ns.P = sparse.triu([[2.0, 5.0], [5.0, 1.0]], format='csc') 14 | ns.q = np.array([3, 4]) 15 | ns.A = sparse.csc_matrix([[-1.0, 0.0], [0.0, -1.0], [-1.0, 3.0], [2.0, 5.0], [3.0, 4]]) 16 | ns.u = np.array([0.0, 0.0, -15, 100, 80]) 17 | ns.l = -np.inf * np.ones(len(ns.u)) 18 | ns.model = osqp.OSQP(algebra=algebra) 19 | return ns 20 | 21 | 22 | def test_non_convex_small_sigma(self, solver_type): 23 | if solver_type == 'direct': 24 | with pytest.raises(osqp.OSQPException): 25 | self.model.setup( 26 | P=self.P, 27 | q=self.q, 28 | A=self.A, 29 | l=self.l, 30 | u=self.u, 31 | solver_type=solver_type, 32 | sigma=1e-6, 33 | ) 34 | else: 35 | self.model.setup( 36 | P=self.P, 37 | q=self.q, 38 | A=self.A, 39 | l=self.l, 40 | u=self.u, 41 | solver_type=solver_type, 42 | sigma=1e-6, 43 | ) 44 | res = self.model.solve() 45 | 46 | assert res.info.status_val in ( 47 | self.model.constant('OSQP_MAX_ITER_REACHED'), 48 | self.model.constant('OSQP_NON_CVX'), 49 | ) 50 | 51 | 52 | def test_non_convex_big_sigma(self): 53 | self.model.setup(P=self.P, q=self.q, A=self.A, l=self.l, u=self.u, sigma=5) 54 | res = self.model.solve() 55 | 56 | assert res.info.status_val == self.model.constant('OSQP_NON_CVX') 57 | 58 | 59 | def test_nan(self): 60 | nptest.assert_approx_equal(self.model.constant('OSQP_NAN'), np.nan) 61 | -------------------------------------------------------------------------------- /src/osqp/tests/polishing_test.py: -------------------------------------------------------------------------------- 1 | from types import SimpleNamespace 2 | import numpy as np 3 | from scipy import sparse 4 | import pytest 5 | import numpy.testing as nptest 6 | import osqp 7 | from osqp.tests.utils import load_high_accuracy 8 | 9 | 10 | @pytest.fixture 11 | def self(algebra, solver_type, atol, rtol, decimal_tol): 12 | ns = SimpleNamespace() 13 | ns.opts = { 14 | 'verbose': False, 15 | 'eps_abs': 1e-03, 16 | 'eps_rel': 1e-03, 17 | 'scaling': True, 18 | 'rho': 0.1, 19 | 'alpha': 1.6, 20 | 'max_iter': 2500, 21 | 'polishing': True, 22 | 'polish_refine_iter': 4, 23 | 'solver_type': solver_type, 24 | } 25 | ns.model = osqp.OSQP(algebra=algebra) 26 | ns.atol = atol 27 | ns.rtol = rtol 28 | ns.decimal_tol = decimal_tol 29 | return ns 30 | 31 | 32 | def test_polish_simple(self): 33 | # Simple QP problem 34 | self.P = sparse.diags([11.0, 0.0], format='csc') 35 | self.q = np.array([3, 4]) 36 | self.A = sparse.csc_matrix([[-1, 0], [0, -1], [-1, -3], [2, 5], [3, 4]]) 37 | self.u = np.array([0, 0, -15, 100, 80]) 38 | self.l = -1e05 * np.ones(len(self.u)) 39 | self.n = self.P.shape[0] 40 | self.m = self.A.shape[0] 41 | self.model.setup(P=self.P, q=self.q, A=self.A, l=self.l, u=self.u, **self.opts) 42 | 43 | # Solve problem 44 | res = self.model.solve() 45 | 46 | x_sol, y_sol, obj_sol = load_high_accuracy('test_polish_simple') 47 | # Assert close 48 | nptest.assert_allclose(res.x, x_sol, rtol=self.rtol, atol=self.atol) 49 | nptest.assert_allclose(res.y, y_sol, rtol=self.rtol, atol=self.atol) 50 | nptest.assert_almost_equal(res.info.obj_val, obj_sol, decimal=self.decimal_tol) 51 | 52 | 53 | def test_polish_unconstrained(self): 54 | # Unconstrained QP problem 55 | np.random.seed(4) 56 | 57 | self.n = 30 58 | self.m = 0 59 | P = sparse.diags(np.random.rand(self.n)) + 0.2 * sparse.eye(self.n) 60 | self.P = P.tocsc() 61 | self.q = np.random.randn(self.n) 62 | self.A = sparse.csc_matrix((self.m, self.n)) 63 | self.l = np.array([]) 64 | self.u = np.array([]) 65 | self.model.setup(P=self.P, q=self.q, A=self.A, l=self.l, u=self.u, **self.opts) 66 | 67 | # Solve problem 68 | res = self.model.solve() 69 | 70 | x_sol, _, obj_sol = load_high_accuracy('test_polish_unconstrained') 71 | # Assert close 72 | nptest.assert_allclose(res.x, x_sol, rtol=self.rtol, atol=self.atol) 73 | nptest.assert_almost_equal(res.info.obj_val, obj_sol, decimal=self.decimal_tol) 74 | 75 | 76 | def test_polish_random(self): 77 | # Random QP problem 78 | np.random.seed(6) 79 | 80 | self.n = 30 81 | self.m = 50 82 | Pt = sparse.random(self.n, self.n) 83 | self.P = Pt.T @ Pt 84 | self.q = np.random.randn(self.n) 85 | self.A = sparse.csc_matrix(np.random.randn(self.m, self.n)) 86 | self.l = -3 + np.random.randn(self.m) 87 | self.u = 3 + np.random.randn(self.m) 88 | model = osqp.OSQP(algebra=self.model.algebra) 89 | model.setup(P=self.P, q=self.q, A=self.A, l=self.l, u=self.u, **self.opts) 90 | assert model.solver_type == self.opts['solver_type'] 91 | 92 | # Solve problem 93 | res = model.solve() 94 | 95 | x_sol, y_sol, obj_sol = load_high_accuracy('test_polish_random') 96 | # Assert close 97 | nptest.assert_allclose(res.x, x_sol, rtol=self.rtol, atol=self.atol) 98 | nptest.assert_allclose(res.y, y_sol, rtol=self.rtol, atol=self.atol) 99 | nptest.assert_almost_equal(res.info.obj_val, obj_sol, decimal=self.decimal_tol) 100 | -------------------------------------------------------------------------------- /src/osqp/tests/primal_infeasibility_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | from types import SimpleNamespace 3 | import osqp 4 | from scipy import sparse 5 | import numpy as np 6 | import pytest 7 | 8 | 9 | @pytest.fixture 10 | def self(algebra, solver_type, atol, rtol, decimal_tol): 11 | self = SimpleNamespace() 12 | self.opts = { 13 | 'verbose': False, 14 | 'eps_abs': 1e-05, 15 | 'eps_rel': 1e-05, 16 | 'eps_dual_inf': 1e-20, 17 | 'max_iter': 2500, 18 | 'polishing': False, 19 | 'solver_type': solver_type, 20 | } 21 | self.model = osqp.OSQP(algebra=algebra) 22 | return self 23 | 24 | 25 | def test_primal_infeasible_problem(self): 26 | # Simple QP problem 27 | np.random.seed(4) 28 | 29 | self.n = 50 30 | self.m = 500 31 | # Generate random Matrices 32 | Pt = np.random.rand(self.n, self.n) 33 | self.P = sparse.triu(Pt.T.dot(Pt), format='csc') 34 | self.q = np.random.rand(self.n) 35 | self.A = sparse.random(self.m, self.n).tolil() # Lil for efficiency 36 | self.u = 3 + np.random.randn(self.m) 37 | self.l = -3 + np.random.randn(self.m) 38 | 39 | # Make random problem primal infeasible 40 | self.A[int(self.n / 2), :] = self.A[int(self.n / 2) + 1, :] 41 | self.l[int(self.n / 2)] = self.u[int(self.n / 2) + 1] + 10 * np.random.rand() 42 | self.u[int(self.n / 2)] = self.l[int(self.n / 2)] + 0.5 43 | 44 | # Convert A to csc 45 | self.A = self.A.tocsc() 46 | 47 | self.model.setup(P=self.P, q=self.q, A=self.A, l=self.l, u=self.u, **self.opts) 48 | 49 | # Solve problem with OSQP 50 | res = self.model.solve() 51 | 52 | assert res.info.status_val == self.model.constant('OSQP_PRIMAL_INFEASIBLE') 53 | 54 | normalized_prim_inf_cert = res.prim_inf_cert / np.linalg.norm(res.prim_inf_cert) 55 | normalized_prim_inf_cert_correct = np.load( 56 | os.path.join(os.path.dirname(__file__), 'solutions', 'test_primal_infeasibility.npz') 57 | )['normalized_prim_inf_cert_correct'] 58 | assert np.allclose(normalized_prim_inf_cert, normalized_prim_inf_cert_correct) 59 | 60 | 61 | def test_primal_and_dual_infeasible_problem(self): 62 | self.n = 2 63 | self.m = 4 64 | self.P = sparse.csc_matrix((2, 2)) 65 | self.q = np.array([-1.0, -1.0]) 66 | self.A = sparse.csc_matrix([[1.0, -1.0], [-1.0, 1.0], [1.0, 0.0], [0.0, 1.0]]) 67 | self.l = np.array([1.0, 1.0, 0.0, 0.0]) 68 | self.u = np.inf * np.ones(self.m) 69 | 70 | self.model.setup(P=self.P, q=self.q, A=self.A, l=self.l, u=self.u, **self.opts) 71 | 72 | res = self.model.solve() 73 | 74 | assert res.info.status_val in ( 75 | self.model.constant('OSQP_PRIMAL_INFEASIBLE'), 76 | self.model.constant('OSQP_DUAL_INFEASIBLE'), 77 | ) 78 | -------------------------------------------------------------------------------- /src/osqp/tests/solutions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/tests/solutions/__init__.py -------------------------------------------------------------------------------- /src/osqp/tests/solutions/test_basic_QP.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/tests/solutions/test_basic_QP.npz -------------------------------------------------------------------------------- /src/osqp/tests/solutions/test_dual_infeasibility.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/tests/solutions/test_dual_infeasibility.npz -------------------------------------------------------------------------------- /src/osqp/tests/solutions/test_feasibility_problem.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/tests/solutions/test_feasibility_problem.npz -------------------------------------------------------------------------------- /src/osqp/tests/solutions/test_polish_random.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/tests/solutions/test_polish_random.npz -------------------------------------------------------------------------------- /src/osqp/tests/solutions/test_polish_simple.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/tests/solutions/test_polish_simple.npz -------------------------------------------------------------------------------- /src/osqp/tests/solutions/test_polish_unconstrained.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/tests/solutions/test_polish_unconstrained.npz -------------------------------------------------------------------------------- /src/osqp/tests/solutions/test_primal_infeasibility.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/tests/solutions/test_primal_infeasibility.npz -------------------------------------------------------------------------------- /src/osqp/tests/solutions/test_solve.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/tests/solutions/test_solve.npz -------------------------------------------------------------------------------- /src/osqp/tests/solutions/test_unconstrained_problem.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/tests/solutions/test_unconstrained_problem.npz -------------------------------------------------------------------------------- /src/osqp/tests/solutions/test_update_A.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/tests/solutions/test_update_A.npz -------------------------------------------------------------------------------- /src/osqp/tests/solutions/test_update_A_allind.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/tests/solutions/test_update_A_allind.npz -------------------------------------------------------------------------------- /src/osqp/tests/solutions/test_update_P.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/tests/solutions/test_update_P.npz -------------------------------------------------------------------------------- /src/osqp/tests/solutions/test_update_P_A_allind.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/tests/solutions/test_update_P_A_allind.npz -------------------------------------------------------------------------------- /src/osqp/tests/solutions/test_update_P_A_indA.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/tests/solutions/test_update_P_A_indA.npz -------------------------------------------------------------------------------- /src/osqp/tests/solutions/test_update_P_A_indP.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/tests/solutions/test_update_P_A_indP.npz -------------------------------------------------------------------------------- /src/osqp/tests/solutions/test_update_P_A_indP_indA.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/tests/solutions/test_update_P_A_indP_indA.npz -------------------------------------------------------------------------------- /src/osqp/tests/solutions/test_update_P_allind.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/tests/solutions/test_update_P_allind.npz -------------------------------------------------------------------------------- /src/osqp/tests/solutions/test_update_bounds.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/tests/solutions/test_update_bounds.npz -------------------------------------------------------------------------------- /src/osqp/tests/solutions/test_update_l.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/tests/solutions/test_update_l.npz -------------------------------------------------------------------------------- /src/osqp/tests/solutions/test_update_q.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/tests/solutions/test_update_q.npz -------------------------------------------------------------------------------- /src/osqp/tests/solutions/test_update_u.npz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osqp/osqp-python/a8b8eaefc3bb3bccaa992e2b2e456bd6cc8fcfaa/src/osqp/tests/solutions/test_update_u.npz -------------------------------------------------------------------------------- /src/osqp/tests/unconstrained_test.py: -------------------------------------------------------------------------------- 1 | import osqp 2 | from osqp.tests.utils import load_high_accuracy 3 | import numpy as np 4 | from scipy import sparse 5 | import pytest 6 | import numpy.testing as nptest 7 | 8 | 9 | @pytest.fixture 10 | def self(algebra, solver_type, atol, rtol, decimal_tol): 11 | np.random.seed(4) 12 | 13 | self.n = 30 14 | self.m = 0 15 | P = sparse.diags(np.random.rand(self.n)) + 0.2 * sparse.eye(self.n) 16 | self.P = P.tocsc() 17 | self.q = np.random.randn(self.n) 18 | self.A = sparse.csc_matrix((self.m, self.n)) 19 | self.l = np.array([]) 20 | self.u = np.array([]) 21 | self.opts = { 22 | 'verbose': False, 23 | 'eps_abs': 1e-08, 24 | 'eps_rel': 1e-08, 25 | 'polishing': False, 26 | } 27 | self.model = osqp.OSQP(algebra=algebra) 28 | self.model.setup(P=self.P, q=self.q, A=self.A, l=self.l, u=self.u, solver_type=solver_type, **self.opts) 29 | 30 | self.rtol = rtol 31 | self.atol = atol 32 | self.decimal_tol = decimal_tol 33 | 34 | return self 35 | 36 | 37 | def test_unconstrained_problem(self): 38 | # Solve problem 39 | res = self.model.solve() 40 | 41 | # Assert close 42 | x_sol, _, obj_sol = load_high_accuracy('test_unconstrained_problem') 43 | # Assert close 44 | nptest.assert_allclose(res.x, x_sol, rtol=self.rtol, atol=self.atol) 45 | nptest.assert_almost_equal(res.info.obj_val, obj_sol, decimal=self.decimal_tol) 46 | -------------------------------------------------------------------------------- /src/osqp/tests/update_matrices_test.py: -------------------------------------------------------------------------------- 1 | from types import SimpleNamespace 2 | import osqp 3 | from osqp.tests.utils import load_high_accuracy 4 | import numpy as np 5 | from scipy import sparse 6 | import pytest 7 | import numpy.testing as nptest 8 | 9 | 10 | @pytest.fixture 11 | def self(algebra, solver_type, atol, rtol, decimal_tol): 12 | self = SimpleNamespace() 13 | 14 | np.random.seed(1) 15 | 16 | self.n = 5 17 | self.m = 8 18 | p = 0.7 19 | 20 | Pt = sparse.random(self.n, self.n, density=p) 21 | Pt_new = Pt.copy() 22 | Pt_new.data += 0.1 * np.random.randn(Pt.nnz) 23 | 24 | self.P = (Pt.T.dot(Pt) + sparse.eye(self.n)).tocsc() 25 | self.P_new = (Pt_new.T.dot(Pt_new) + sparse.eye(self.n)).tocsc() 26 | self.P_triu = sparse.triu(self.P) 27 | self.P_triu_new = sparse.triu(self.P_new) 28 | self.q = np.random.randn(self.n) 29 | self.A = sparse.random(self.m, self.n, density=p, format='csc') 30 | self.A_new = self.A.copy() 31 | self.A_new.data += np.random.randn(self.A_new.nnz) 32 | self.l = np.zeros(self.m) 33 | self.u = 30 + np.random.randn(self.m) 34 | self.opts = {'eps_abs': 1e-08, 'eps_rel': 1e-08, 'verbose': False} 35 | self.model = osqp.OSQP(algebra=algebra) 36 | self.model.setup(P=self.P, q=self.q, A=self.A, l=self.l, u=self.u, solver_type=solver_type, **self.opts) 37 | 38 | self.rtol = rtol 39 | self.atol = atol 40 | self.decimal_tol = decimal_tol 41 | 42 | return self 43 | 44 | 45 | def test_solve(self): 46 | res = self.model.solve() 47 | 48 | x_sol, y_sol, obj_sol = load_high_accuracy('test_solve') 49 | nptest.assert_allclose(res.x, x_sol, rtol=self.rtol, atol=self.atol) 50 | nptest.assert_allclose(res.y, y_sol, rtol=self.rtol, atol=self.atol) 51 | nptest.assert_almost_equal(res.info.obj_val, obj_sol, decimal=self.decimal_tol) 52 | 53 | 54 | def test_update_P(self): 55 | # Update matrix P 56 | Px = self.P_triu_new.data 57 | Px_idx = np.arange(self.P_triu_new.nnz) 58 | self.model.update(Px=Px, Px_idx=Px_idx) 59 | res = self.model.solve() 60 | 61 | x_sol, y_sol, obj_sol = load_high_accuracy('test_update_P') 62 | 63 | if self.model.algebra != 'cuda': # pytest-todo 64 | nptest.assert_allclose(res.x, x_sol, rtol=self.rtol, atol=self.atol) 65 | nptest.assert_allclose(res.y, y_sol, rtol=self.rtol, atol=self.atol) 66 | nptest.assert_almost_equal(res.info.obj_val, obj_sol, decimal=self.decimal_tol) 67 | 68 | 69 | def test_update_P_allind(self): 70 | # Update matrix P 71 | Px = self.P_triu_new.data 72 | self.model.update(Px=Px) 73 | res = self.model.solve() 74 | 75 | x_sol, y_sol, obj_sol = load_high_accuracy('test_update_P_allind') 76 | # Assert close 77 | nptest.assert_allclose(res.x, x_sol, rtol=self.rtol, atol=self.atol) 78 | nptest.assert_allclose(res.y, y_sol, rtol=self.rtol, atol=self.atol) 79 | nptest.assert_almost_equal(res.info.obj_val, obj_sol, decimal=self.decimal_tol) 80 | 81 | 82 | def test_update_A(self): 83 | # Update matrix A 84 | Ax = self.A_new.data 85 | Ax_idx = np.arange(self.A_new.nnz) 86 | self.model.update(Ax=Ax, Ax_idx=Ax_idx) 87 | res = self.model.solve() 88 | 89 | x_sol, y_sol, obj_sol = load_high_accuracy('test_update_A') 90 | 91 | if self.model.algebra != 'cuda': # pytest-todo 92 | nptest.assert_allclose(res.x, x_sol, rtol=self.rtol, atol=self.atol) 93 | nptest.assert_allclose(res.y, y_sol, rtol=self.rtol, atol=self.atol) 94 | nptest.assert_almost_equal(res.info.obj_val, obj_sol, decimal=self.decimal_tol) 95 | 96 | 97 | def test_update_A_allind(self): 98 | # Update matrix A 99 | Ax = self.A_new.data 100 | self.model.update(Ax=Ax) 101 | res = self.model.solve() 102 | 103 | x_sol, y_sol, obj_sol = load_high_accuracy('test_update_A_allind') 104 | # Assert close 105 | nptest.assert_allclose(res.x, x_sol, rtol=self.rtol, atol=self.atol) 106 | nptest.assert_allclose(res.y, y_sol, rtol=self.rtol, atol=self.atol) 107 | nptest.assert_almost_equal(res.info.obj_val, obj_sol, decimal=self.decimal_tol) 108 | 109 | 110 | def test_update_P_A_indP_indA(self): 111 | # Update matrices P and A 112 | Px = self.P_triu_new.data 113 | Px_idx = np.arange(self.P_triu_new.nnz) 114 | Ax = self.A_new.data 115 | Ax_idx = np.arange(self.A_new.nnz) 116 | self.model.update(Px=Px, Px_idx=Px_idx, Ax=Ax, Ax_idx=Ax_idx) 117 | res = self.model.solve() 118 | 119 | x_sol, y_sol, obj_sol = load_high_accuracy('test_update_P_A_indP_indA') 120 | 121 | if self.model.algebra != 'cuda': # pytest-todo 122 | nptest.assert_allclose(res.x, x_sol, rtol=self.rtol, atol=self.atol) 123 | nptest.assert_allclose(res.y, y_sol, rtol=self.rtol, atol=self.atol) 124 | nptest.assert_almost_equal(res.info.obj_val, obj_sol, decimal=self.decimal_tol) 125 | 126 | 127 | def test_update_P_A_indP(self): 128 | # Update matrices P and A 129 | Px = self.P_triu_new.data 130 | Px_idx = np.arange(self.P_triu_new.nnz) 131 | Ax = self.A_new.data 132 | self.model.update(Px=Px, Px_idx=Px_idx, Ax=Ax) 133 | res = self.model.solve() 134 | 135 | x_sol, y_sol, obj_sol = load_high_accuracy('test_update_P_A_indP') 136 | 137 | if self.model.algebra != 'cuda': # pytest-todo 138 | nptest.assert_allclose(res.x, x_sol, rtol=self.rtol, atol=self.atol) 139 | nptest.assert_allclose(res.y, y_sol, rtol=self.rtol, atol=self.atol) 140 | nptest.assert_almost_equal(res.info.obj_val, obj_sol, decimal=self.decimal_tol) 141 | 142 | 143 | def test_update_P_A_indA(self): 144 | # Update matrices P and A 145 | Px = self.P_triu_new.data 146 | Ax = self.A_new.data 147 | Ax_idx = np.arange(self.A_new.nnz) 148 | self.model.update(Px=Px, Ax=Ax, Ax_idx=Ax_idx) 149 | res = self.model.solve() 150 | 151 | x_sol, y_sol, obj_sol = load_high_accuracy('test_update_P_A_indA') 152 | 153 | if self.model.algebra != 'cuda': # pytest-todo 154 | nptest.assert_allclose(res.x, x_sol, rtol=self.rtol, atol=self.atol) 155 | nptest.assert_allclose(res.y, y_sol, rtol=self.rtol, atol=self.atol) 156 | nptest.assert_almost_equal(res.info.obj_val, obj_sol, decimal=self.decimal_tol) 157 | 158 | 159 | def test_update_P_A_allind(self): 160 | # Update matrices P and A 161 | Px = self.P_triu_new.data 162 | Ax = self.A_new.data 163 | self.model.update(Px=Px, Ax=Ax) 164 | res = self.model.solve() 165 | 166 | x_sol, y_sol, obj_sol = load_high_accuracy('test_update_P_A_allind') 167 | 168 | nptest.assert_allclose(res.x, x_sol, rtol=self.rtol, atol=self.atol) 169 | nptest.assert_allclose(res.y, y_sol, rtol=self.rtol, atol=self.atol) 170 | nptest.assert_almost_equal(res.info.obj_val, obj_sol, decimal=self.decimal_tol) 171 | -------------------------------------------------------------------------------- /src/osqp/tests/utils.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import numpy as np 3 | 4 | 5 | def load_high_accuracy(test_name): 6 | npz = os.path.join(os.path.dirname(__file__), 'solutions', f'{test_name}.npz') 7 | npzfile = np.load(npz) 8 | return npzfile['x_val'], npzfile['y_val'], npzfile['obj'] 9 | -------------------------------------------------------------------------------- /src/osqp/tests/warm_start_test.py: -------------------------------------------------------------------------------- 1 | from types import SimpleNamespace 2 | import numpy as np 3 | from scipy import sparse 4 | import pytest 5 | import osqp 6 | 7 | 8 | @pytest.fixture 9 | def self(algebra, solver_type, atol, rtol, decimal_tol): 10 | ns = SimpleNamespace() 11 | ns.opts = { 12 | 'verbose': True, 13 | 'adaptive_rho': False, 14 | 'eps_abs': 1e-08 if solver_type == 'direct' else 1e-2, 15 | 'eps_rel': 1e-08 if solver_type == 'direct' else 1e-2, 16 | 'polishing': False, 17 | 'check_termination': 1, 18 | 'solver_type': solver_type, 19 | } 20 | 21 | ns.model = osqp.OSQP(algebra=algebra) 22 | return ns 23 | 24 | 25 | def test_warm_start(self): 26 | # Big problem 27 | np.random.seed(2) 28 | self.n = 100 29 | self.m = 200 30 | self.A = sparse.random(self.m, self.n, density=0.9, format='csc') 31 | self.l = -np.random.rand(self.m) * 2.0 32 | self.u = np.random.rand(self.m) * 2.0 33 | 34 | P = sparse.random(self.n, self.n, density=0.9) 35 | self.P = sparse.triu(P.dot(P.T), format='csc') 36 | self.q = np.random.randn(self.n) 37 | 38 | # Setup solver 39 | self.model.setup(P=self.P, q=self.q, A=self.A, l=self.l, u=self.u, **self.opts) 40 | 41 | # Solve problem with OSQP 42 | res = self.model.solve() 43 | 44 | # Store optimal values 45 | x_opt = res.x 46 | y_opt = res.y 47 | tot_iter = res.info.iter 48 | 49 | # Warm start with zeros and check if number of iterations is the same 50 | self.model.warm_start(x=np.zeros(self.n), y=np.zeros(self.m)) 51 | res = self.model.solve() 52 | assert res.info.iter == tot_iter 53 | 54 | # Warm start with optimal values and check that number of iter < 10 55 | self.model.warm_start(x=x_opt, y=y_opt) 56 | res = self.model.solve() 57 | assert res.info.iter < 10 58 | -------------------------------------------------------------------------------- /src/osqppurepy/__init__.py: -------------------------------------------------------------------------------- 1 | from osqppurepy.interface import OSQP # noqa: F401 2 | -------------------------------------------------------------------------------- /src/osqppurepy/interface.py: -------------------------------------------------------------------------------- 1 | """ 2 | OSQP solver pure python implementation 3 | """ 4 | from builtins import object 5 | import osqppurepy._osqp as _osqp # Internal low level module 6 | from warnings import warn 7 | import numpy as np 8 | from scipy import sparse 9 | 10 | 11 | class OSQP(object): 12 | def __init__(self): 13 | self._model = _osqp.OSQP() 14 | 15 | def version(self): 16 | return self._model.version() 17 | 18 | def setup(self, P=None, q=None, A=None, l=None, u=None, **settings): 19 | """ 20 | Setup OSQP solver problem of the form 21 | 22 | minimize 1/2 x' * P * x + q' * x 23 | subject to l <= A * x <= u 24 | 25 | solver settings can be specified as additional keyword arguments 26 | """ 27 | 28 | # 29 | # Get problem dimensions 30 | # 31 | 32 | if P is None: 33 | if q is not None: 34 | n = len(q) 35 | elif A is not None: 36 | n = A.shape[1] 37 | else: 38 | raise ValueError('The problem does not have any variables') 39 | else: 40 | n = P.shape[0] 41 | if A is None: 42 | m = 0 43 | else: 44 | m = A.shape[0] 45 | 46 | # 47 | # Create parameters if they are None 48 | # 49 | 50 | if (A is None and (l is not None or u is not None)) or (A is not None and (l is None and u is None)): 51 | raise ValueError('A must be supplied together ' + 'with at least one bound l or u') 52 | 53 | # Add infinity bounds in case they are not specified 54 | if A is not None and l is None: 55 | l = -np.inf * np.ones(A.shape[0]) 56 | if A is not None and u is None: 57 | u = np.inf * np.ones(A.shape[0]) 58 | 59 | # Create elements if they are not specified 60 | if P is None: 61 | P = sparse.csc_matrix( 62 | ( 63 | np.zeros((0,), dtype=np.double), 64 | np.zeros((0,), dtype=np.int), 65 | np.zeros((n + 1,), dtype=np.int), 66 | ), 67 | shape=(n, n), 68 | ) 69 | if q is None: 70 | q = np.zeros(n) 71 | 72 | if A is None: 73 | A = sparse.csc_matrix( 74 | ( 75 | np.zeros((0,), dtype=np.double), 76 | np.zeros((0,), dtype=np.int), 77 | np.zeros((n + 1,), dtype=np.int), 78 | ), 79 | shape=(m, n), 80 | ) 81 | l = np.zeros(A.shape[0]) 82 | u = np.zeros(A.shape[0]) 83 | 84 | # 85 | # Check vector dimensions (not checked from C solver) 86 | # 87 | 88 | # Check if second dimension of A is correct 89 | # if A.shape[1] != n: 90 | # raise ValueError("Dimension n in A and P does not match") 91 | if len(q) != n: 92 | raise ValueError('Incorrect dimension of q') 93 | if len(l) != m: 94 | raise ValueError('Incorrect dimension of l') 95 | if len(u) != m: 96 | raise ValueError('Incorrect dimension of u') 97 | 98 | # 99 | # Check or Sparsify Matrices 100 | # 101 | if not sparse.issparse(P) and isinstance(P, np.ndarray) and len(P.shape) == 2: 102 | raise TypeError('P is required to be a sparse matrix') 103 | if not sparse.issparse(A) and isinstance(A, np.ndarray) and len(A.shape) == 2: 104 | raise TypeError('A is required to be a sparse matrix') 105 | 106 | # Convert matrices in CSC form and to individual pointers 107 | if not sparse.isspmatrix_csc(P): 108 | warn('Converting sparse P to a CSC ' + '(compressed sparse column) matrix. (It may take a while...)') 109 | P = P.tocsc() 110 | if not sparse.isspmatrix_csc(A): 111 | warn('Converting sparse A to a CSC ' + '(compressed sparse column) matrix. (It may take a while...)') 112 | A = A.tocsc() 113 | 114 | # Check if P an A have sorted indices 115 | if not P.has_sorted_indices: 116 | P.sort_indices() 117 | if not A.has_sorted_indices: 118 | A.sort_indices() 119 | 120 | # Convert infinity values to OSQP Infinity 121 | u = np.minimum(u, self._model.constant('OSQP_INFTY')) 122 | l = np.maximum(l, -self._model.constant('OSQP_INFTY')) 123 | 124 | self._model.setup((n, m), P.data, P.indices, P.indptr, q, A.data, A.indices, A.indptr, l, u, **settings) 125 | 126 | def update(self, q=None, l=None, u=None, P=None, A=None): 127 | """ 128 | Update OSQP problem arguments 129 | """ 130 | 131 | # Get problem dimensions 132 | (n, m) = (self._model.work.data.n, self._model.work.data.m) 133 | 134 | if P is not None: 135 | if P.shape != (n, n): 136 | raise ValueError('P must have shape (n x n)') 137 | if A is None: 138 | self._model.update_P(P) 139 | 140 | if A is not None: 141 | if A.shape != (m, n): 142 | raise ValueError('A must have shape (m x n)') 143 | if P is None: 144 | self._model.update_A(A) 145 | 146 | if P is not None and A is not None: 147 | self._model.update_P_A(P, A) 148 | 149 | if q is not None: 150 | if q.shape != (n,): 151 | raise ValueError('q must have shape (n,)') 152 | self._model.update_lin_cost(q) 153 | 154 | if l is not None: 155 | if l.shape != (m,): 156 | raise ValueError('l must have shape (m,)') 157 | 158 | # Convert values to OSQP_INFTY 159 | l = np.maximum(l, -self._model.constant('OSQP_INFTY')) 160 | 161 | if u is None: 162 | self._model.update_lower_bound(l) 163 | 164 | if u is not None: 165 | if u.shape != (m,): 166 | raise ValueError('u must have shape (m,)') 167 | 168 | # Convert values to OSQP_INFTY 169 | u = np.minimum(u, self._model.constant('OSQP_INFTY')) 170 | 171 | if l is None: 172 | self._model.update_upper_bound(u) 173 | 174 | if l is not None and u is not None: 175 | self._model.update_bounds(l, u) 176 | 177 | if q is None and l is None and u is None and P is None and A is None: 178 | raise ValueError('No updatable data has been specified!') 179 | 180 | def update_settings(self, **kwargs): 181 | """ 182 | Update OSQP solver settings 183 | 184 | It is possible to change: 'max_iter', 'eps_abs', 'eps_rel', 'rho, 'alpha', 185 | 'delta', 'polish', 'polish_refine_iter', 186 | 'verbose', 'scaled_termination', 187 | 'check_termination' 188 | """ 189 | 190 | # get arguments 191 | max_iter = kwargs.pop('max_iter', None) 192 | eps_abs = kwargs.pop('eps_abs', None) 193 | eps_rel = kwargs.pop('eps_rel', None) 194 | rho = kwargs.pop('rho', None) 195 | alpha = kwargs.pop('alpha', None) 196 | delta = kwargs.pop('delta', None) 197 | polish = kwargs.pop('polish', None) 198 | polish_refine_iter = kwargs.pop('polish_refine_iter', None) 199 | verbose = kwargs.pop('verbose', None) 200 | scaled_termination = kwargs.pop('scaled_termination', None) 201 | check_termination = kwargs.pop('check_termination', None) 202 | warm_start = kwargs.pop('warm_start', None) 203 | 204 | # update them 205 | if max_iter is not None: 206 | self._model.update_max_iter(max_iter) 207 | 208 | if eps_abs is not None: 209 | self._model.update_eps_abs(eps_abs) 210 | 211 | if eps_rel is not None: 212 | self._model.update_eps_rel(eps_rel) 213 | 214 | if rho is not None: 215 | self._model.update_rho(rho) 216 | 217 | if alpha is not None: 218 | self._model.update_alpha(alpha) 219 | 220 | if delta is not None: 221 | self._model.update_delta(delta) 222 | 223 | if polish is not None: 224 | self._model.update_polish(polish) 225 | 226 | if polish_refine_iter is not None: 227 | self._model.update_polish_refine_iter(polish_refine_iter) 228 | 229 | if verbose is not None: 230 | self._model.update_verbose(verbose) 231 | 232 | if scaled_termination is not None: 233 | self._model.update_scaled_termination(scaled_termination) 234 | 235 | if check_termination is not None: 236 | self._model.update_check_termination(check_termination) 237 | 238 | if warm_start is not None: 239 | self._model.update_warm_start(warm_start) 240 | 241 | if ( 242 | max_iter is None 243 | and eps_abs is None 244 | and eps_rel is None 245 | and rho is None 246 | and alpha is None 247 | and delta is None 248 | and polish is None 249 | and polish_refine_iter is None 250 | and verbose is None 251 | and scaled_termination is None 252 | and check_termination is None 253 | and warm_start is None 254 | ): 255 | raise ValueError('No updatable settings has been specified!') 256 | 257 | def solve(self): 258 | """ 259 | Solve QP Problem 260 | """ 261 | # Solve QP 262 | return self._model.solve() 263 | 264 | def constant(self, constant_name): 265 | """ 266 | Return solver constant 267 | """ 268 | return self._model.constant(constant_name) 269 | 270 | def warm_start(self, x=None, y=None): 271 | """ 272 | Warm start primal or dual variables 273 | """ 274 | # get problem dimensions 275 | (n, m) = (self._model.work.data.n, self._model.work.data.m) 276 | 277 | if x is not None: 278 | if len(x) != n: 279 | raise ValueError('Wrong dimension for variable x') 280 | 281 | if y is None: 282 | self._model.warm_start_x(x) 283 | 284 | if y is not None: 285 | if len(y) != m: 286 | raise ValueError('Wrong dimension for variable y') 287 | 288 | if x is None: 289 | self._model.warm_start_y(y) 290 | 291 | if x is not None and y is not None: 292 | self._model.warm_start(x, y) 293 | --------------------------------------------------------------------------------