From 8331cf8842e2eea5eb9ae6bdfc26b0ab3248c546 Mon Sep 17 00:00:00 2001 From: "codspeed-hq[bot]" <117304815+codspeed-hq[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 18:33:15 +0000 Subject: [PATCH 1/2] Add CodSpeed performance benchmarks --- .github/workflows/codspeed.yml | 41 +++++++ README.md | 1 + benchmarks/bench_solver.py | 202 +++++++++++++++++++++++++++++++++ 3 files changed, 244 insertions(+) create mode 100644 .github/workflows/codspeed.yml create mode 100644 benchmarks/bench_solver.py diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml new file mode 100644 index 0000000..e0f753b --- /dev/null +++ b/.github/workflows/codspeed.yml @@ -0,0 +1,41 @@ +name: CodSpeed + +on: + push: + branches: + - master + pull_request: + branches: + - master + # `workflow_dispatch` allows CodSpeed to trigger backtest + # performance analysis in order to generate initial data. + workflow_dispatch: + +permissions: + contents: read + id-token: write + +jobs: + benchmarks: + name: Run benchmarks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: 'recursive' + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + pip install --upgrade pip + pip install -e "." + pip install pytest pytest-codspeed + + - name: Run benchmarks + uses: CodSpeedHQ/action@v4 + with: + mode: simulation + run: python -m pytest benchmarks/ --codspeed diff --git a/README.md b/README.md index fd18ac6..430d166 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ [![PyPI version](https://badge.fury.io/py/osqp.svg)](https://badge.fury.io/py/osqp) [![Python 3.8‒3.14](https://img.shields.io/badge/python-3.8%E2%80%923.14-blue)](https://www.python.org) [![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) +[![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/osqp/osqp-python?utm_source=badge) # OSQP Python Python wrapper for [OSQP](https://osqp.org): The Operator Splitting QP solver. diff --git a/benchmarks/bench_solver.py b/benchmarks/bench_solver.py new file mode 100644 index 0000000..cd366e1 --- /dev/null +++ b/benchmarks/bench_solver.py @@ -0,0 +1,202 @@ +"""Benchmarks for the OSQP solver.""" + +import numpy as np +import pytest +from scipy import sparse + +import osqp + + +def _make_small_qp(): + """Create a small 2-variable QP problem.""" + P = sparse.csc_matrix([[4.0, 1.0], [1.0, 2.0]]) + q = np.array([1.0, 1.0]) + A = sparse.csc_matrix([[1.0, 1.0], [1.0, 0.0], [0.0, 1.0]]) + l = np.array([1.0, 0.0, 0.0]) + u = np.array([1.0, 0.7, 0.7]) + return P, q, A, l, u + + +def _make_medium_qp(n=50, seed=0): + """Create a medium-sized random QP problem.""" + np.random.seed(seed) + M = sparse.random(n, n, density=0.3, format='csc') + P = (M.T @ M + 0.1 * sparse.eye(n)).tocsc() + q = np.random.randn(n) + m = 2 * n + A = sparse.random(m, n, density=0.3, format='csc') + l = -2.0 + np.random.randn(m) + u = 2.0 + np.random.randn(m) + return P, q, A, l, u + + +def _make_large_qp(n=200, seed=1): + """Create a larger random QP problem.""" + np.random.seed(seed) + M = sparse.random(n, n, density=0.1, format='csc') + P = (M.T @ M + 0.5 * sparse.eye(n)).tocsc() + q = np.random.randn(n) + m = int(1.5 * n) + A = sparse.random(m, n, density=0.15, format='csc') + l = -3.0 + np.random.randn(m) + u = 3.0 + np.random.randn(m) + return P, q, A, l, u + + +# --------------------------------------------------------------------------- +# Setup benchmarks +# --------------------------------------------------------------------------- + + +def test_bench_setup_small(benchmark): + """Benchmark solver setup for a small QP problem.""" + P, q, A, l, u = _make_small_qp() + + def _setup(): + solver = osqp.OSQP() + solver.setup(P, q, A, l, u, verbose=False) + + benchmark(_setup) + + +def test_bench_setup_medium(benchmark): + """Benchmark solver setup for a medium QP problem (n=50).""" + P, q, A, l, u = _make_medium_qp() + + def _setup(): + solver = osqp.OSQP() + solver.setup(P, q, A, l, u, verbose=False) + + benchmark(_setup) + + +def test_bench_setup_large(benchmark): + """Benchmark solver setup for a large QP problem (n=200).""" + P, q, A, l, u = _make_large_qp() + + def _setup(): + solver = osqp.OSQP() + solver.setup(P, q, A, l, u, verbose=False) + + benchmark(_setup) + + +# --------------------------------------------------------------------------- +# Solve benchmarks +# --------------------------------------------------------------------------- + + +def test_bench_solve_small(benchmark): + """Benchmark solve for a small QP problem.""" + P, q, A, l, u = _make_small_qp() + solver = osqp.OSQP() + solver.setup(P, q, A, l, u, verbose=False) + + benchmark(solver.solve) + + +def test_bench_solve_medium(benchmark): + """Benchmark solve for a medium QP problem (n=50).""" + P, q, A, l, u = _make_medium_qp() + solver = osqp.OSQP() + solver.setup(P, q, A, l, u, verbose=False, max_iter=4000) + + benchmark(solver.solve) + + +def test_bench_solve_large(benchmark): + """Benchmark solve for a large QP problem (n=200).""" + P, q, A, l, u = _make_large_qp() + solver = osqp.OSQP() + solver.setup(P, q, A, l, u, verbose=False, max_iter=4000) + + benchmark(solver.solve) + + +# --------------------------------------------------------------------------- +# Update benchmarks +# --------------------------------------------------------------------------- + + +def test_bench_update_vectors(benchmark): + """Benchmark updating linear cost and bounds.""" + P, q, A, l, u = _make_medium_qp() + solver = osqp.OSQP() + solver.setup(P, q, A, l, u, verbose=False) + + np.random.seed(42) + q_new = np.random.randn(len(q)) + l_new = -2.0 + np.random.randn(len(l)) + u_new = 2.0 + np.random.randn(len(u)) + + def _update_and_solve(): + solver.update(q=q_new, l=l_new, u=u_new) + solver.solve() + + benchmark(_update_and_solve) + + +# --------------------------------------------------------------------------- +# Setup + solve (end-to-end) +# --------------------------------------------------------------------------- + + +def test_bench_end_to_end_small(benchmark): + """Benchmark full setup and solve cycle for a small QP problem.""" + P, q, A, l, u = _make_small_qp() + + def _setup_and_solve(): + solver = osqp.OSQP() + solver.setup(P, q, A, l, u, verbose=False) + solver.solve() + + benchmark(_setup_and_solve) + + +def test_bench_end_to_end_medium(benchmark): + """Benchmark full setup and solve cycle for a medium QP problem (n=50).""" + P, q, A, l, u = _make_medium_qp() + + def _setup_and_solve(): + solver = osqp.OSQP() + solver.setup(P, q, A, l, u, verbose=False, max_iter=4000) + solver.solve() + + benchmark(_setup_and_solve) + + +# --------------------------------------------------------------------------- +# Polishing benchmark +# --------------------------------------------------------------------------- + + +def test_bench_solve_with_polishing(benchmark): + """Benchmark solve with polishing enabled.""" + P, q, A, l, u = _make_medium_qp() + solver = osqp.OSQP() + solver.setup(P, q, A, l, u, verbose=False, polishing=True, max_iter=4000) + + benchmark(solver.solve) + + +# --------------------------------------------------------------------------- +# Warm start benchmark +# --------------------------------------------------------------------------- + + +def test_bench_warm_start_solve(benchmark): + """Benchmark solve with warm starting from a previous solution.""" + P, q, A, l, u = _make_medium_qp() + solver = osqp.OSQP() + solver.setup(P, q, A, l, u, verbose=False, warm_starting=True) + + # Solve once to get initial solution + res = solver.solve() + x0 = res.x + y0 = res.y + + def _warm_start_and_solve(): + solver.warm_start(x=x0, y=y0) + solver.solve() + + benchmark(_warm_start_and_solve) From d64b77180e320187eda0beaeb8e6a35eb17ba19d Mon Sep 17 00:00:00 2001 From: "codspeed-hq[bot]" <117304815+codspeed-hq[bot]@users.noreply.github.com> Date: Mon, 25 May 2026 18:36:07 +0000 Subject: [PATCH 2/2] Rename benchmark file to match pytest discovery pattern --- benchmarks/{bench_solver.py => test_bench_solver.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename benchmarks/{bench_solver.py => test_bench_solver.py} (100%) diff --git a/benchmarks/bench_solver.py b/benchmarks/test_bench_solver.py similarity index 100% rename from benchmarks/bench_solver.py rename to benchmarks/test_bench_solver.py