Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use the SCS box cone API #67

Merged
merged 27 commits into from
Jul 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
bdff4e1
Special treatment for SCS with bounds
stephane-caron Jun 24, 2022
7ce76df
[scs] Handle bounds using the box cone API
stephane-caron Jun 24, 2022
290070c
WIP: Add example with lower and upper bounds
stephane-caron Jun 24, 2022
2bc5bfc
Maintain CSC format after vstack
stephane-caron Jun 24, 2022
efb6a7a
Add lower bounds to benchmarks to test SCS
stephane-caron Jun 24, 2022
3b177aa
[minor] Test scs_box in dense QP example
stephane-caron Jun 27, 2022
514dc61
[scs] Remove unused definition of bsize
stephane-caron Jun 27, 2022
d193039
Merge branch 'master' into feature/scs_box_cone
stephane-caron Jul 15, 2022
967825b
[minor] Update CHANGELOG
stephane-caron Jul 15, 2022
dd6b5e6
Benchmark against all solvers again
stephane-caron Jul 15, 2022
7e487e5
[SCS] Use box API when there is either lb or ub
stephane-caron Jul 15, 2022
02f70d1
[minor] Clean out unused import
stephane-caron Jul 15, 2022
17de1dd
Bounds need not be both specified at the same time
stephane-caron Jul 15, 2022
6bce4e6
Merge branch 'master' into feature/scs_box_cone
stephane-caron Jul 15, 2022
80d7cb9
[SCS] Add type annotations to help mypy
stephane-caron Jul 15, 2022
534309b
Handle partial lower/upper bounds
stephane-caron Jul 15, 2022
857bf68
Fix f-strings
stephane-caron Jul 15, 2022
3f31feb
Don't change size range of random benchmark problems
stephane-caron Jul 15, 2022
046c8ad
[minor] Update CHANGELOG
stephane-caron Jul 15, 2022
65cd968
Remove box bounds from benchmark problems
stephane-caron Jul 15, 2022
c17f89d
Select solver randomly in box bounds example
stephane-caron Jul 15, 2022
3f10727
Revert debug print instruction
stephane-caron Jul 15, 2022
532dd67
Simplify box inequalities example
stephane-caron Jul 25, 2022
58aa674
Box inequalities without kwargs in QP example
stephane-caron Jul 25, 2022
d301a98
[minor] Test expression manicure :lipstick:
stephane-caron Jul 25, 2022
f3162b0
[doc] Update SCS QP equation
stephane-caron Jul 25, 2022
cacccdc
[minor] Keep QP example as it is in master
stephane-caron Jul 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ All notable changes to this project will be documented in this file.
### Added

- Document how to add a new QP solver to the library
- Example with (box) lower and upper bounds
- Test case where `lb` XOR `ub` is set

### Changed

- SCS: use the box cone API when lower/upper bounds are set

## [2.0.0] - 2022/07/05

### Added
Expand Down
59 changes: 59 additions & 0 deletions examples/box_inequalities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2016-2022 Stéphane Caron and the qpsolvers contributors.
#
# This file is part of qpsolvers.
#
# qpsolvers is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# qpsolvers is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with qpsolvers. If not, see <http://www.gnu.org/licenses/>.

"""
Test one of the available QP solvers on a small problem with box inequalities.
"""

import random

from time import perf_counter

import numpy as np

from qpsolvers import available_solvers, print_matrix_vector, solve_qp

M = np.array([[1.0, 2.0, 0.0], [-8.0, 3.0, 2.0], [0.0, 1.0, 1.0]])
P = np.dot(M.T, M) # this is a positive definite matrix
q = np.dot(np.array([3.0, 2.0, 3.0]), M)
A = np.array([1.0, 1.0, 1.0])
b = np.array([1.0])
lb = -0.5 * np.ones(3)
ub = 1.0 * np.ones(3)

if __name__ == "__main__":
start_time = perf_counter()
solver = random.choice(available_solvers)
x = solve_qp(P, q, A=A, b=b, lb=lb, ub=ub, solver=solver)
end_time = perf_counter()

print("")
print(" min. 1/2 x^T P x + q^T x")
print(" s.t. A * x == b")
print(" lb <= x <= ub")
print("")
print_matrix_vector(P, "P", q, "q")
print("")
print_matrix_vector(A, "A", b, "b")
print("")
print_matrix_vector(lb.reshape((3, 1)), "lb", ub, "ub")
print("")
print(f"Solution:\n\n x = {x}\n")
print(f"Found in {1e6 * (end_time - start_time):.0f} [us] with {solver}")
7 changes: 5 additions & 2 deletions qpsolvers/solve_qp.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,14 @@ def solve_qp(
if isinstance(G, ndarray) and G.ndim == 1:
G = G.reshape((1, G.shape[0]))
check_problem_constraints(G, h, A, b)
G, h = concatenate_bounds(G, h, lb, ub)
kwargs["initvals"] = initvals
kwargs["verbose"] = verbose
try:
return solve_function[solver](P, q, G, h, A, b, **kwargs)
if (lb is not None or ub is not None) and solver == "scs":
return solve_function["scs"](P, q, G, h, A, b, lb, ub, **kwargs)
else: # all other solvers, bounds or not
G, h = concatenate_bounds(G, h, lb, ub)
return solve_function[solver](P, q, G, h, A, b, **kwargs)
except KeyError as e:
raise SolverNotFound(
f"solver '{solver}' is not in the list "
Expand Down
5 changes: 3 additions & 2 deletions qpsolvers/solvers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@

from numpy import ndarray

from .typing import CvxoptReadyMatrix
from .typing import DenseOrCSCMatrix
from .typing import CvxoptReadyMatrix, DenseOrCSCMatrix

available_solvers = []
dense_solvers = []
Expand Down Expand Up @@ -310,6 +309,8 @@
Optional[DenseOrCSCMatrix],
Optional[ndarray],
Optional[ndarray],
Optional[ndarray],
Optional[ndarray],
float,
float,
bool,
Expand Down
55 changes: 35 additions & 20 deletions qpsolvers/solvers/scs_.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,15 @@

"""Solver interface for SCS"""

from typing import Optional
from typing import Any, Dict, Optional
from warnings import warn

from numpy import hstack, ndarray
import numpy as np
from numpy.linalg import norm
from scipy import sparse
from scs import solve

from .typing import DenseOrCSCMatrix
from .typing import warn_about_sparse_conversion

from .typing import DenseOrCSCMatrix, warn_about_sparse_conversion

# See https://www.cvxgrp.org/scs/api/exit_flags.html#exit-flags
__status_val_meaning__ = {
Expand All @@ -49,17 +47,19 @@

def scs_solve_qp(
P: DenseOrCSCMatrix,
q: ndarray,
q: np.ndarray,
G: Optional[DenseOrCSCMatrix] = None,
h: Optional[ndarray] = None,
h: Optional[np.ndarray] = None,
A: Optional[DenseOrCSCMatrix] = None,
b: Optional[ndarray] = None,
initvals: Optional[ndarray] = None,
b: Optional[np.ndarray] = None,
lb: Optional[np.ndarray] = None,
ub: Optional[np.ndarray] = None,
initvals: Optional[np.ndarray] = None,
eps_abs: float = 1e-7,
eps_rel: float = 1e-7,
verbose: bool = False,
**kwargs,
) -> Optional[ndarray]:
) -> Optional[np.ndarray]:
"""
Solve a Quadratic Program defined as:

Expand All @@ -70,7 +70,8 @@ def scs_solve_qp(
\\frac{1}{2} x^T P x + q^T x \\\\
\\mbox{subject to}
& G x \\leq h \\\\
& A x = b
& A x = b \\\\
& lb \\leq x \\leq ub
\\end{array}\\end{split}

using `SCS <https://github.com/cvxgrp/scs>`_.
Expand All @@ -89,6 +90,10 @@ def scs_solve_qp(
Linear equality constraint matrix.
b :
Linear equality constraint vector.
lb:
Lower bound constraint vector.
ub:
Upper bound constraint vector.
initvals :
Warm-start guess vector (not used).
eps_abs : float
Expand Down Expand Up @@ -117,13 +122,13 @@ def scs_solve_qp(
that SCS behaves closer to the other solvers. If you don't need that much
precision, increase them for better performance.
"""
if isinstance(P, ndarray):
if isinstance(P, np.ndarray):
warn_about_sparse_conversion("P")
P = sparse.csc_matrix(P)
if isinstance(G, ndarray):
if isinstance(G, np.ndarray):
warn_about_sparse_conversion("G")
G = sparse.csc_matrix(G)
if isinstance(A, ndarray):
if isinstance(A, np.ndarray):
warn_about_sparse_conversion("A")
A = sparse.csc_matrix(A)
kwargs.update(
Expand All @@ -133,24 +138,24 @@ def scs_solve_qp(
"verbose": verbose,
}
)
data = {"P": P, "c": q}
cone = {}
data: Dict[str, Any] = {"P": P, "c": q}
cone: Dict[str, Any] = {}
if initvals is not None:
data["x"] = initvals
if A is not None and b is not None:
if G is not None and h is not None:
data["A"] = sparse.vstack([A, G], format="csc")
data["b"] = hstack([b, h])
data["b"] = np.hstack([b, h])
cone["z"] = b.shape[0] # zero cone
cone["l"] = h.shape[0] # positive orthant
else: # A is not None and b is not None
cone["l"] = h.shape[0] # positive cone
else: # G is None and h is None
data["A"] = A
data["b"] = b
cone["z"] = b.shape[0] # zero cone
elif G is not None and h is not None:
data["A"] = G
data["b"] = h
cone["l"] = h.shape[0] # positive orthant
cone["l"] = h.shape[0] # positive cone
else: # no constraint
x = sparse.linalg.lsqr(P, -q)[0]
if norm(P @ x + q) > 1e-9:
Expand All @@ -159,6 +164,16 @@ def scs_solve_qp(
"q has component in the nullspace of P"
)
return x
if lb is not None or ub is not None:
n = P.shape[1]
cone["bl"] = lb if lb is not None else np.full((n,), -np.inf)
cone["bu"] = ub if ub is not None else np.full((n,), +np.inf)
zero_row = sparse.csc_matrix((1, n))
data["A"] = sparse.vstack(
(data["A"], zero_row, -sparse.eye(n)),
format="csc",
)
data["b"] = np.hstack((data["b"], 1.0, np.zeros(n)))
solution = solve(data, cone, **kwargs)
status_val = solution["info"]["status_val"]
if status_val != 1:
Expand Down