From 81253c819c45e71d1c4b169ace8b4bced16a799b Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 6 Sep 2026 21:14:39 +0400 Subject: [PATCH 1/3] Add doctest examples to the public SCS API and run them in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The package carried prose docstrings on every public symbol but zero runnable examples, so nothing verified that what they claim is still true — the failure mode where a docstring keeps rendering perfectly after the behaviour beneath it changes. 33 examples across `LinearSolver`, `SCS`, `SCS.solve`, `SCS.update` and the legacy `solve()`. Each is self-contained and deterministic: tight eps_abs/eps_rel, verbose=False, values rounded before printing. Also fills two gaps in the same file: `SCS` had no class docstring, and the public legacy `solve()` had only a `#` comment above it. `test/test_doctests.py` runs them against the installed package and asserts `attempted > 0` as well as `failed == 0`, so a module that silently loses its examples fails rather than passes. Closes #240 Co-Authored-By: Claude Opus 5 (1M context) --- scs/py/__init__.py | 105 +++++++++++++++++++++++++++++++++++++++++- test/test_doctests.py | 24 ++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 test/test_doctests.py diff --git a/scs/py/__init__.py b/scs/py/__init__.py index 07a5e93e..9a64cf7a 100644 --- a/scs/py/__init__.py +++ b/scs/py/__init__.py @@ -26,7 +26,20 @@ class LinearSolver(enum.Enum): - """Linear system solver backend for SCS.""" + """Linear system solver backend for SCS. + + Pass a member (or its string value) as the `linear_solver` setting. Which + members are importable depends on the build; `AUTO` picks the best one + available on the platform. + + Example: + + >>> import scs + >>> scs.LinearSolver.QDLDL.value + 'qdldl' + >>> scs.LinearSolver("cpu_indirect") is scs.LinearSolver.CPU_INDIRECT + True + """ AUTO = "auto" QDLDL = "qdldl" CPU_INDIRECT = "cpu_indirect" @@ -85,6 +98,26 @@ def _has_lower_tri(P): class SCS(object): + """A solver instance bound to one problem's data and cone. + + Construct it once, then call `solve` as often as needed; `update` swaps in + a new `b` or `c` without rebuilding the workspace. + + Example: + + >>> import numpy as np + >>> import scipy.sparse as sp + >>> import scs + >>> # maximize x subject to 0 <= x <= 1 + >>> data = { + ... "A": sp.csc_matrix(np.array([[1.0], [-1.0]])), + ... "b": np.array([1.0, 0.0]), + ... "c": np.array([-1.0]), + ... } + >>> solver = scs.SCS(data, {"l": 2}, verbose=False) + >>> solver.solve()["info"]["status"] + 'solved' + """ def __init__(self, data, cone, **settings): """Initialize the SCS solver. @@ -199,6 +232,27 @@ def solve(self, warm_start=True, x=None, y=None, s=None): 's' - primal slack solution 'y' - dual solution 'info' - information dictionary (see docs) + + Example: + + >>> import numpy as np + >>> import scipy.sparse as sp + >>> import scs + >>> # maximize x subject to 0 <= x <= 1 + >>> data = { + ... "A": sp.csc_matrix(np.array([[1.0], [-1.0]])), + ... "b": np.array([1.0, 0.0]), + ... "c": np.array([-1.0]), + ... } + >>> solver = scs.SCS(data, {"l": 2}, eps_abs=1e-9, eps_rel=1e-9, + ... verbose=False) + >>> sol = solver.solve() + >>> sorted(sol) + ['info', 's', 'x', 'y'] + >>> sol["info"]["status"] + 'solved' + >>> float(np.round(sol["x"][0], 6)) + 1.0 """ return self._solver.solve(warm_start, x, y, s) @@ -210,12 +264,59 @@ def update(self, b=None, c=None): @param b New `b` vector. @param c New `c` vector. + + Example: + + >>> import numpy as np + >>> import scipy.sparse as sp + >>> import scs + >>> data = { + ... "A": sp.csc_matrix(np.array([[1.0], [-1.0]])), + ... "b": np.array([1.0, 0.0]), + ... "c": np.array([-1.0]), + ... } + >>> solver = scs.SCS(data, {"l": 2}, eps_abs=1e-9, eps_rel=1e-9, + ... verbose=False) + >>> float(np.round(solver.solve()["x"][0], 6)) # maximize x + 1.0 + >>> solver.update(c=np.array([1.0])) # now minimize x + >>> float(np.round(abs(solver.solve()["x"][0]), 6)) + 0.0 """ self._solver.update(b, c) -# Backwards compatible helper function that simply calls the main API. def solve(data, cone, **settings): + """Solve a problem in one call, without holding on to a solver instance. + + Backwards compatible helper that simply calls the main API. Warm-start + vectors are read out of `data` under the keys `x`, `y` and `s`, which is + how the pre-`SCS`-class API passed them. + + @param data Dictionary containing keys `A`, `b`, `c`, optionally `P`, + and optionally the warm-start vectors `x`, `y`, `s`. + @param cone Dictionary containing cone information. + @param settings Settings as kwargs, see docs. + + @return the same dictionary `SCS.solve` returns. + + Example: + + >>> import numpy as np + >>> import scipy.sparse as sp + >>> import scs + >>> data = { + ... "A": sp.csc_matrix(np.array([[1.0], [-1.0]])), + ... "b": np.array([1.0, 0.0]), + ... "c": np.array([-1.0]), + ... } + >>> sol = scs.solve(data, {"l": 2}, eps_abs=1e-9, eps_rel=1e-9, + ... verbose=False) + >>> sol["info"]["status"] + 'solved' + >>> float(np.round(sol["x"][0], 6)) + 1.0 + """ solver = SCS(data, cone, **settings) # Hack out the warm start data from old API diff --git a/test/test_doctests.py b/test/test_doctests.py new file mode 100644 index 00000000..ed4a7772 --- /dev/null +++ b/test/test_doctests.py @@ -0,0 +1,24 @@ +"""Execute the doctest examples carried by the public `scs` API. + +Docstring *coverage* only says a docstring exists; this says the examples +inside it still evaluate to what they claim. Runs against the installed +package, which is the one users import. +""" + +import doctest + +import scs + + +def test_docstring_examples_run(capsys): + """Every `>>>` example in `scs` still produces its documented output.""" + results = doctest.testmod( + scs, + optionflags=doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS, + verbose=False, + ) + report = capsys.readouterr().out + + # A module that has lost its examples would otherwise pass silently. + assert results.attempted > 0, "no doctest examples found in scs" + assert results.failed == 0, report From f72ecf226bd014a3d38447a78fc6c765ae1f1363 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 6 Sep 2026 21:14:28 +0400 Subject: [PATCH 2/3] Enforce the existing 100% coverage of the Python layer in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `--cov=scs` measures nothing when pytest runs from the repo root: `scs/` has no `__init__.py`, so coverage resolves the name to that namespace-package directory rather than to the installed package, and reports 0% with "No data was collected". `source_pkgs` states that `scs` is an importable package name, so coverage follows the import to site-packages instead. The threshold lives in `[tool.coverage.report]`, so the CI invocation is a bare `pytest --cov` with no inline flags. Gate added to build_openmp only; the other test jobs run plain pytest, where the coverage config is inert. Also gitignore the coverage data files — the existing entry was `python/.coverage` from the pre-meson layout. Closes #238 Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build.yml | 9 +++++++-- .gitignore | 6 ++++++ pyproject.toml | 12 ++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 06a32c77..2b1474c4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,11 +40,16 @@ jobs: run: echo "$(pwd)/.pixi/envs/default/bin" >> $GITHUB_PATH shell: bash - name: Install dependencies - run: pixi add --platform linux-64 scipy numpy pip pytest meson meson-python openblas python==${{ matrix.python-version }} + run: pixi add --platform linux-64 scipy numpy pip pytest pytest-cov meson meson-python openblas python==${{ matrix.python-version }} - name: Test + # This job is the coverage gate for the Python layer. The threshold and + # the source resolution both live in pyproject.toml ([tool.coverage.*]), + # so `--cov` takes no inline flags: `source_pkgs` is what makes coverage + # follow the import to the installed package instead of measuring the + # repo-root `scs/` directory, which has no __init__.py and reports 0%. run: | pixi r python -m pip install -v . --no-build-isolation -Csetup-args=-Duse_openmp=true - pixi r pytest + pixi r pytest --cov rm -rf build/ build_mkl: diff --git a/.gitignore b/.gitignore index 7b0df7db..42dbd848 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,9 @@ out # pixi environments .pixi *.egg-info + +# coverage +.coverage +.coverage.* +htmlcov/ +coverage.xml diff --git a/pyproject.toml b/pyproject.toml index 6925fa2e..7c48cf91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,3 +108,15 @@ markers = [ # pytest-run-parallel: functions that are not thread-safe # (capsys/capfd are detected automatically by pytest-run-parallel) thread_unsafe_functions = ["numpy.random.seed"] + +# `--cov=scs` measures nothing when pytest runs from the repo root: the source +# directory `scs/` has no `__init__.py`, so coverage resolves the name to that +# namespace-package directory rather than to the installed `scs` package, and +# reports 0% with "No data was collected". `source_pkgs` states that `scs` is an +# importable package name, so coverage follows the import to site-packages. +[tool.coverage.run] +source_pkgs = ["scs"] + +[tool.coverage.report] +show_missing = true +fail_under = 100 From c0e1653ca01e1790329cd054e1cf075814af45c5 Mon Sep 17 00:00:00 2001 From: Thomas Schmelzer Date: Sun, 6 Sep 2026 21:29:16 +0400 Subject: [PATCH 3/3] Add a code-quality CI job: ruff, mypy, interrogate, deptry and pip-audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No linter, type checker, docstring gate or dependency audit ran anywhere in this repo. The build and correctness side of CI is thorough — wheel ELF audit, pristine-container smoke test, TSan/ASan, free-threading race detection — but nothing checked the code itself. New `.github/workflows/quality.yml`: static checks only, no submodules and no compilation, so it reports in well under a minute. Every rule set and threshold lives in pyproject.toml, so running these locally gives the same verdict as CI. Tool versions are pinned because ruff in particular enables new rules by default in each release, which would otherwise turn an unrelated ruff release into a red build. Lint config is deliberately narrow — pyflakes, the pycodestyle error subset and import sorting — rather than ruff's rolling defaults. E741 is ignored because `l` is the SCS cone key for the non-negative cone, not a careless name. F401 and I001 are ignored under test/: the backend modules import an extension purely to discover whether the build has it, and `_scs_mkl` sets the MKL interface layer at import time, so import order there carries meaning a sorter cannot see. What the gates found and this commit fixes: - `test_problems_with_longs` in test_scs_basic.py was unreachable. Its guard, `platform.python_version_tuple() < ("3", "0", "0")`, cannot be true on any supported interpreter (requires-python is >=3.9), and its body calls the Python 2 builtin `long`. The suite count is unchanged at 397, confirming it was never collected. - Nine dead bindings where the call matters but the name is unused, including `sol1 = solver.solve()` in test_warm_start_with_spectral, whose solve seeds the warm start the next line depends on — kept as a bare call. Also adds type annotations to the eight public signatures, a module docstring and a `_load_module` docstring (interrogate now reports 100%), and drops the meaningless shebang from a package `__init__.py`. Closes #236 Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/quality.yml | 47 +++++++++++++++++++++++++ pyproject.toml | 42 ++++++++++++++++++++++ scs/py/__init__.py | 44 +++++++++++++++++------ test/solve_random_cone_prob.py | 4 +-- test/test_scs_basic.py | 22 +----------- test/test_scs_coverage.py | 5 --- test/test_spectral_and_complex_cones.py | 2 +- 7 files changed, 126 insertions(+), 40 deletions(-) create mode 100644 .github/workflows/quality.yml diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 00000000..3dff6827 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,47 @@ +name: quality + +on: + pull_request: + push: + branches: + - master + +jobs: + quality: + # Static checks only — no submodules, no compilation, no pixi, so this + # reports in well under a minute and is the fastest signal on a PR. + # + # Every rule set and threshold lives in pyproject.toml, so a contributor + # running these locally gets the same verdict as CI. The tool versions are + # pinned for the same reason: ruff in particular enables new rules by + # default in each release, which would otherwise turn an unrelated ruff + # release into a red build here. + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + python-version: "3.12" + + - name: Lint (ruff) + run: uvx ruff@0.16.6 check scs/py test + + - name: Type check (mypy) + # numpy ships its own stubs, so installing it means arrays are really + # checked rather than silently degraded to Any. + run: uvx --with numpy --with scipy mypy@2.3.1 + + - name: Docstring coverage (interrogate) + run: uvx interrogate@1.7.0 -v scs/py + + - name: Dependency hygiene (deptry) + run: uvx deptry@0.25.1 scs/py + + - name: Resolve the declared dependency closure + run: uv pip compile pyproject.toml --python-version 3.12 -o /tmp/audit-requirements.txt + + - name: Audit dependencies (pip-audit) + run: uvx pip-audit@2.10.1 -r /tmp/audit-requirements.txt diff --git a/pyproject.toml b/pyproject.toml index 7c48cf91..545c0395 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,3 +120,45 @@ source_pkgs = ["scs"] [tool.coverage.report] show_missing = true fail_under = 100 + +[tool.ruff] +extend-exclude = ["scs_source", "scs/pythoncapi-compat", "legacy_setup.py"] + +[tool.ruff.lint] +# Pyflakes, the error subset of pycodestyle, and import sorting. Deliberately +# not ruff's rolling default set: that set grows with each release, so a ruff +# upgrade would turn into a CI failure unrelated to any change here. +select = ["E4", "E7", "E9", "F", "I"] +# `l` is the SCS cone key for the non-negative cone. It is the domain's name +# for that quantity, not a careless one-letter variable. +ignore = ["E741"] + +[tool.ruff.lint.per-file-ignores] +# The backend test modules import an extension solely to discover whether this +# build has it, inside a try/except that skips the module when it does not. +# The unused import *is* the test. +# I001 too: these modules' import order is left as the maintainers wrote it. +# `_scs_mkl` sets the MKL interface layer at import time, so import order +# around numpy/scipy carries meaning here that a sorter cannot see. +"test/*" = ["F401", "I001"] + +[tool.mypy] +files = ["scs/py"] +warn_redundant_casts = true +warn_unused_ignores = true +no_implicit_optional = true + +[[tool.mypy.overrides]] +# scipy ships no inline types and its stub package is a separate, partial +# project; numpy's own stubs are used, so arrays are still checked. +module = ["scipy.*"] +ignore_missing_imports = true + +[tool.interrogate] +fail-under = 100 + +[tool.deptry] +# `scs` is this project's own package: `scs/py/__init__.py` imports its +# compiled siblings via `from scs import _scs_direct`, which is not a +# third-party dependency. +known_first_party = ["scs"] diff --git a/scs/py/__init__.py b/scs/py/__init__.py index 9a64cf7a..43600267 100644 --- a/scs/py/__init__.py +++ b/scs/py/__init__.py @@ -1,10 +1,21 @@ -#!/usr/bin/env python +"""Python interface to SCS, the splitting conic solver. + +Exposes the `SCS` class, the legacy `solve` helper, and the `LinearSolver` +enum naming the linear-system backends a given build can dispatch to. +""" + import enum import sys +import warnings +from types import ModuleType +from typing import Any, Optional + import numpy as np from scipy import sparse -from scs import _scs_direct -import warnings + +# The `_scs_*` extensions are produced by the meson build, so they do not +# exist in the source tree for a static checker to resolve. +from scs import _scs_direct # type: ignore[attr-defined] __version__ = _scs_direct.version() __sizeof_int__ = _scs_direct.sizeof_int() @@ -50,12 +61,13 @@ class LinearSolver(enum.Enum): CUDSS = "cudss" -def _load_module(name): +def _load_module(name: str) -> ModuleType: + """Import one of the sibling `_scs_*` C extension modules by name.""" from importlib import import_module return import_module(f"scs.{name}") -def _resolve_auto(): +def _resolve_auto() -> ModuleType: """Auto-detect the best available direct solver for this platform.""" if sys.platform == "darwin": # Prefer the bundled QDLDL on macOS over Apple Accelerate. @@ -79,7 +91,7 @@ def _resolve_auto(): } -def _select_scs_module(stgs): +def _select_scs_module(stgs: dict) -> ModuleType: """Choose which SCS C extension to import based on settings.""" linear_solver = stgs.pop("linear_solver", LinearSolver.AUTO) if isinstance(linear_solver, str): @@ -87,7 +99,7 @@ def _select_scs_module(stgs): return _SOLVER_DISPATCH[linear_solver]() -def _has_lower_tri(P): +def _has_lower_tri(P: Any) -> bool: """Fast check for strictly lower triangular entries in a sorted CSC matrix.""" nnz_per_col = np.diff(P.indptr) nonempty = nnz_per_col > 0 @@ -119,7 +131,7 @@ class SCS(object): 'solved' """ - def __init__(self, data, cone, **settings): + def __init__(self, data: dict, cone: dict, **settings: Any) -> None: """Initialize the SCS solver. @param data Dictionary containing keys `P`, `A`, `b`, `c`. @@ -216,7 +228,13 @@ def __init__(self, data, cone, **settings): **self._settings, ) - def solve(self, warm_start=True, x=None, y=None, s=None): + def solve( + self, + warm_start: bool = True, + x: Optional[np.ndarray] = None, + y: Optional[np.ndarray] = None, + s: Optional[np.ndarray] = None, + ) -> dict: """Solve the optimization problem. @param warm_start Whether to warm-start. By default the solution of @@ -256,7 +274,11 @@ def solve(self, warm_start=True, x=None, y=None, s=None): """ return self._solver.solve(warm_start, x, y, s) - def update(self, b=None, c=None): + def update( + self, + b: Optional[np.ndarray] = None, + c: Optional[np.ndarray] = None, + ) -> None: """Update the `b` vector, `c` vector, or both, before another solve. After a solve we can reuse the SCS workspace in another solve if the @@ -286,7 +308,7 @@ def update(self, b=None, c=None): self._solver.update(b, c) -def solve(data, cone, **settings): +def solve(data: dict, cone: dict, **settings: Any) -> dict: """Solve a problem in one call, without holding on to a solver instance. Backwards compatible helper that simply calls the main API. Warm-start diff --git a/test/solve_random_cone_prob.py b/test/solve_random_cone_prob.py index 4d145a99..d6542fb2 100644 --- a/test/solve_random_cone_prob.py +++ b/test/solve_random_cone_prob.py @@ -60,7 +60,7 @@ def solve_infeasible(linear_solver, rng): m = tools.get_scs_cone_dims(K) data = tools.gen_infeasible(K, n=m // 3, rng=rng) params = {"normalize": True, "scale": 0.5} - sol = scs.solve(data, K, linear_solver=linear_solver, **params) + scs.solve(data, K, linear_solver=linear_solver, **params) def solve_unbounded(linear_solver, rng): @@ -76,7 +76,7 @@ def solve_unbounded(linear_solver, rng): m = tools.get_scs_cone_dims(K) data = tools.gen_unbounded(K, n=m // 3, rng=rng) params = {"normalize": True, "scale": 0.5} - sol = scs.solve(data, K, linear_solver=linear_solver, **params) + scs.solve(data, K, linear_solver=linear_solver, **params) if __name__ == "__main__": diff --git a/test/test_scs_basic.py b/test/test_scs_basic.py index d73f4ea3..5cc760d5 100644 --- a/test/test_scs_basic.py +++ b/test/test_scs_basic.py @@ -72,26 +72,6 @@ def test_problems(cone, linear_solver, expected): assert_almost_equal(sol["x"][0], expected, decimal=2) -if platform.python_version_tuple() < ("3", "0", "0"): - - @pytest.mark.parametrize( - "cone,linear_solver,expected", - [ - ({"q": [], "l": long(2)}, scs.LinearSolver.AUTO, 1), - ({"q": [], "l": long(2)}, scs.LinearSolver.QDLDL, 1), - ({"q": [], "l": long(2)}, scs.LinearSolver.CPU_INDIRECT, 1), - ({"q": [long(2)], "l": 0}, scs.LinearSolver.AUTO, 0.5), - ({"q": [long(2)], "l": 0}, scs.LinearSolver.QDLDL, 0.5), - ({"q": [long(2)], "l": 0}, scs.LinearSolver.CPU_INDIRECT, 0.5), - ], - ) - def test_problems_with_longs(cone, linear_solver, expected): - sol = scs.solve( - data, cone=cone, linear_solver=linear_solver, verbose=False - ) - assert_almost_equal(sol["x"][0], expected, decimal=2) - - def test_failures(): with pytest.raises(TypeError): scs.solve() @@ -111,4 +91,4 @@ def test_failures(): scs.solve(data, {"q": [], "l": 2}, max_iters=1.1) with pytest.raises(ValueError): - sol = scs.solve(data, {"q": [1], "l": 0}, verbose=False) + scs.solve(data, {"q": [1], "l": 0}, verbose=False) diff --git a/test/test_scs_coverage.py b/test/test_scs_coverage.py index 53cd0b61..b45d91e7 100644 --- a/test/test_scs_coverage.py +++ b/test/test_scs_coverage.py @@ -263,7 +263,6 @@ def test_P_full_symmetric_extracts_upper(): """For a 2x2 P, lower triangular entries should be discarded.""" # min (1/2)||x||^2 + c'x over x ∈ [0,1]^2 (two separate LP constraints) # Optimal is where gradient = Px + c = 0 projected onto box. - n = 2 A2 = sp.block_diag([sp.csc_matrix([[1.0], [-1.0]]), sp.csc_matrix([[1.0], [-1.0]])], format="csc") b2 = np.array([1.0, 0.0, 1.0, 0.0]) @@ -1442,7 +1441,6 @@ def test_P_lower_triangular_stripped(): P = [[2, 1], [1, 2]] (symmetric) should behave the same as the upper- triangular version P = [[2, 1], [0, 2]]. """ - n = 2 A2 = sp.block_diag( [sp.csc_matrix([[1.0], [-1.0]]), sp.csc_matrix([[1.0], [-1.0]])], format="csc", @@ -1943,7 +1941,6 @@ def test_large_random_lp(): rng = np.random.RandomState(3) K_lp = {"z": 0, "l": 40, "q": [], "s": [], "ep": 0, "ed": 0, "p": []} - m_lp = tools.get_scs_cone_dims(K_lp) data, pstar = tools.gen_feasible(K_lp, n=20, density=0.3, rng=rng) solver = scs.SCS( @@ -1992,7 +1989,6 @@ def test_P_forced_unsorted_indices_sorted_and_solved(): in SCS.__init__ even when the underlying data is already ordered. The solve must still produce the correct QP answer. """ - n = 2 A2 = sp.csc_matrix(np.array([ [1., 0.], [-1., 0.], [0., 1.], [0., -1.] ])) @@ -2653,7 +2649,6 @@ def test_large_max_iters_solves(): def test_box_cone_numpy_bounds(): """bu/bl specified as numpy float arrays should work.""" - n = 2 # box cone: dim = d + 1 = 3 (t, x1, x2) # s = b - Ax, s in box cone => bl <= x <= bu A = sp.csc_matrix(np.array([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]])) diff --git a/test/test_spectral_and_complex_cones.py b/test/test_spectral_and_complex_cones.py index 0eabf347..7a179b61 100644 --- a/test/test_spectral_and_complex_cones.py +++ b/test/test_spectral_and_complex_cones.py @@ -398,6 +398,6 @@ def test_warm_start_with_spectral(self): cone = {"ell1": [4]} data = _gen_feasible_qp(cone, rng=rng) solver = scs.SCS(data, cone, verbose=False, max_iters=500) - sol1 = solver.solve() + solver.solve() sol2 = solver.solve(warm_start=True) assert sol2["info"]["status_val"] in (1, 2)