From bdff4e16d62baddbf335bde2525e7995c38483ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Fri, 24 Jun 2022 19:44:22 +0200 Subject: [PATCH 01/25] Special treatment for SCS with bounds --- qpsolvers/solve_qp.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/qpsolvers/solve_qp.py b/qpsolvers/solve_qp.py index 4aa9c10f..2118c37d 100644 --- a/qpsolvers/solve_qp.py +++ b/qpsolvers/solve_qp.py @@ -24,13 +24,12 @@ from typing import Optional -from numpy import eye, hstack, ones, ndarray, vstack, zeros +from numpy import eye, hstack, ndarray, ones, vstack, zeros from .check_problem_constraints import check_problem_constraints from .concatenate_bounds import concatenate_bounds from .exceptions import SolverNotFound -from .solvers import dense_solvers -from .solvers import solve_function +from .solvers import dense_solvers, solve_function from .typing import Matrix, Vector @@ -133,11 +132,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 solver == "scs" and lb is not None: + 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 available") from e From 7ce76df9d9f6489bc813c2fc83691a8ac88404b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Fri, 24 Jun 2022 19:44:50 +0200 Subject: [PATCH 02/25] [scs] Handle bounds using the box cone API --- qpsolvers/solvers/__init__.py | 5 ++-- qpsolvers/solvers/scs_.py | 44 ++++++++++++++++++++++------------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/qpsolvers/solvers/__init__.py b/qpsolvers/solvers/__init__.py index 3412e6e8..666b149e 100644 --- a/qpsolvers/solvers/__init__.py +++ b/qpsolvers/solvers/__init__.py @@ -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 = [] @@ -310,6 +309,8 @@ Optional[DenseOrCSCMatrix], Optional[ndarray], Optional[ndarray], + Optional[ndarray], + Optional[ndarray], float, float, bool, diff --git a/qpsolvers/solvers/scs_.py b/qpsolvers/solvers/scs_.py index 1696c1e1..10a92e52 100644 --- a/qpsolvers/solvers/scs_.py +++ b/qpsolvers/solvers/scs_.py @@ -23,14 +23,12 @@ from typing import 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__ = { @@ -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: @@ -89,6 +89,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 @@ -117,13 +121,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( @@ -140,17 +144,17 @@ def scs_solve_qp( 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: @@ -159,6 +163,14 @@ def scs_solve_qp( "q has component in the nullspace of P" ) return x + if lb is not None and ub is not None: + cone["bl"] = lb + cone["bu"] = ub + k = lb.shape[0] + zero_row = sparse.csc_matrix((1, k)) + data["A"] = sparse.vstack((data["A"], zero_row, -sparse.eye(k))) + data["b"] = np.hstack((data["b"], 1.0, np.zeros(k))) + cone["bsize"] = k + 1 solution = solve(data, cone, **kwargs) status_val = solution["info"]["status_val"] if status_val != 1: From 290070c215dc0b58771b986b974265370dff448c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Fri, 24 Jun 2022 19:45:08 +0200 Subject: [PATCH 03/25] WIP: Add example with lower and upper bounds --- examples/lower_upper_bounds.py | 63 ++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 examples/lower_upper_bounds.py diff --git a/examples/lower_upper_bounds.py b/examples/lower_upper_bounds.py new file mode 100644 index 00000000..21816619 --- /dev/null +++ b/examples/lower_upper_bounds.py @@ -0,0 +1,63 @@ +#!/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 . + +""" +Test the "quadprog" QP solver on a small dense problem. +""" + +from time import perf_counter + +import numpy as np + +from qpsolvers import available_solvers, print_matrix_vector, solve_qp + +M = np.array([[1.0, 2.0, 0.0], [-8.0, 3.0, 2.0], [0.0, 1.0, 1.0]]) +P = np.dot(M.T, M) # this is a positive definite matrix +q = np.dot(np.array([3.0, 2.0, 3.0]), M) +G = np.array([[1.0, 2.0, 1.0], [2.0, 0.0, 1.0], [-1.0, 2.0, -1.0]]) +h = np.array([3.0, 2.0, -2.0]) +A = np.array([1.0, 1.0, 1.0]) +b = np.array([1.0]) +lb = -0.5 * np.ones(3) +ub = 1.0 * np.ones(3) + +if __name__ == "__main__": + start_time = perf_counter() + solver = "scs" if "scs" in available_solvers else available_solvers[0] + x = solve_qp(P, q, A=A, b=b, lb=lb, ub=ub, solver="scs") + end_time = perf_counter() + + print("") + print(" min. 1/2 x^T P x + q^T x") + print(" s.t. G * x <= h") + print(" A * x == b") + print("") + print_matrix_vector(P, "P", q, "q") + print("") + print_matrix_vector(G, "G", h, "h") + print("") + print_matrix_vector(A, "A", b, "b") + print("") + print(f"Solution: x = {x}") + print(f"Solve time: {1e6 * (end_time - start_time):.0f} [us]") + print(f"Solver: {solver}") + + x = solve_qp(P, q, A=A, b=b, lb=lb, ub=ub, solver="quadprog") + print(f"Solution (quadprog): x = {x}") From 2bc5bfc4b14cbd215464928a2d6f28ceb1fa51ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Fri, 24 Jun 2022 20:41:54 +0200 Subject: [PATCH 04/25] Maintain CSC format after vstack --- qpsolvers/solvers/scs_.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qpsolvers/solvers/scs_.py b/qpsolvers/solvers/scs_.py index 10a92e52..24941d54 100644 --- a/qpsolvers/solvers/scs_.py +++ b/qpsolvers/solvers/scs_.py @@ -168,7 +168,9 @@ def scs_solve_qp( cone["bu"] = ub k = lb.shape[0] zero_row = sparse.csc_matrix((1, k)) - data["A"] = sparse.vstack((data["A"], zero_row, -sparse.eye(k))) + data["A"] = sparse.vstack( + (data["A"], zero_row, -sparse.eye(k)), format="csc", + ) data["b"] = np.hstack((data["b"], 1.0, np.zeros(k))) cone["bsize"] = k + 1 solution = solve(data, cone, **kwargs) From efb6a7ab458f99818f1d9311396664f226df54f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Fri, 24 Jun 2022 21:03:13 +0200 Subject: [PATCH 05/25] Add lower bounds to benchmarks to test SCS See https://github.com/bodono/scs-python/issues/63 --- examples/benchmark_dense_problem.py | 21 +++++++++++++-------- examples/benchmark_random_problems.py | 27 +++++++++++++++++++++------ examples/benchmark_sparse_problem.py | 21 ++++++++++++--------- qpsolvers/solve_qp.py | 2 +- qpsolvers/solvers/scs_.py | 2 +- 5 files changed, 48 insertions(+), 25 deletions(-) diff --git a/examples/benchmark_dense_problem.py b/examples/benchmark_dense_problem.py index 35a88097..5e72e6ec 100644 --- a/examples/benchmark_dense_problem.py +++ b/examples/benchmark_dense_problem.py @@ -22,21 +22,26 @@ Test all available QP solvers on a dense quadratic program. """ +from os.path import basename + +import numpy as np from IPython import get_ipython from numpy import array, dot from numpy.linalg import norm -from os.path import basename from scipy.sparse import csc_matrix -from qpsolvers import dense_solvers, sparse_solvers -from qpsolvers import solve_qp +from qpsolvers import dense_solvers, solve_qp, sparse_solvers +dense_solvers = ["scs", "scs_box"] +sparse_solvers = [] M = array([[1.0, 2.0, 0.0], [-8.0, 3.0, 2.0], [0.0, 1.0, 1.0]]) P = dot(M.T, M) q = dot(array([3.0, 2.0, 3.0]), M) G = array([[1.0, 2.0, 1.0], [2.0, 0.0, 1.0], [-1.0, 2.0, -1.0]]) h = array([3.0, 2.0, -2.0]) +lb = -0.5 * np.ones(3) +ub = 1.0 * np.ones(3) P_csc = csc_matrix(P) G_csc = csc_matrix(G) @@ -50,24 +55,24 @@ exit() dense_instr = { - solver: f"u = solve_qp(P, q, G, h, solver='{solver}')" + solver: f"u = solve_qp(P, q, G, h, lb=lb, ub=ub, solver='{solver}')" for solver in dense_solvers } sparse_instr = { - solver: f"u = solve_qp(P_csc, q, G_csc, h, solver='{solver}')" + solver: f"u = solve_qp(P_csc, q, G_csc, h, lb=lb, ub=ub, solver='{solver}')" for solver in sparse_solvers } print("\nTesting all QP solvers on a dense quadratic program...") - sol0 = solve_qp(P, q, G, h, solver=dense_solvers[0]) + sol0 = solve_qp(P, q, G, h, lb=lb, ub=ub, solver=dense_solvers[0]) abstol = 2e-4 # tolerance on absolute solution error for solver in dense_solvers: - sol = solve_qp(P, q, G, h, solver=solver) + sol = solve_qp(P, q, G, h, lb=lb, ub=ub, solver=solver) delta = norm(sol - sol0) assert delta < abstol, f"{solver}'s solution offset by {delta:.1e}" for solver in sparse_solvers: - sol = solve_qp(P_csc, q, G_csc, h, solver=solver) + sol = solve_qp(P_csc, q, G_csc, h, lb=lb, ub=ub, solver=solver) delta = norm(sol - sol0) assert delta < abstol, f"{solver}'s solution offset by {delta:.1e}" diff --git a/examples/benchmark_random_problems.py b/examples/benchmark_random_problems.py index 3a70bb37..c0c1f5ce 100644 --- a/examples/benchmark_random_problems.py +++ b/examples/benchmark_random_problems.py @@ -30,16 +30,19 @@ print("This example requires IPython, try installing ipython3") sys.exit(-1) -from numpy import dot, linspace, ones, random from os.path import basename -from scipy.linalg import toeplitz from timeit import timeit +from numpy import dot, linspace, ones, random +from scipy.linalg import toeplitz + from qpsolvers import available_solvers, solve_qp +available_solvers = ["scs", "scs_box"] + nb_iter = 10 -sizes = [10, 20, 50, 100, 200, 500, 1000, 2000] +sizes = [10, 20, 50, 100, 200, 500, 1000] def solve_random_qp(n, solver): @@ -49,13 +52,25 @@ def solve_random_qp(n, solver): [1.0, 0.0, 0.0] + [0.0] * (n - 3), [1.0, 2.0, 3.0] + [0.0] * (n - 3) ) h = ones(n) - return solve_qp(P, q, G, h, solver=solver) + lb = -0.03 * ones(n) + ub = +0.03 * ones(n) + return solve_qp(P, q, G, h, lb=lb, ub=ub, solver=solver) def plot_results(perfs): try: - from pylab import clf, get_cmap, grid, ion, legend, plot - from pylab import xlabel, xscale, ylabel, yscale + from pylab import ( + clf, + get_cmap, + grid, + ion, + legend, + plot, + xlabel, + xscale, + ylabel, + yscale, + ) except ImportError: print("Cannot plot results, try installing python3-matplotlib") print("Results are stored in the global `perfs` dictionary") diff --git a/examples/benchmark_sparse_problem.py b/examples/benchmark_sparse_problem.py index 0aa70d5b..fd2cce12 100644 --- a/examples/benchmark_sparse_problem.py +++ b/examples/benchmark_sparse_problem.py @@ -22,17 +22,18 @@ Test all available QP solvers on a sparse quadratic program. """ +from os.path import basename + import numpy as np import scipy.sparse - from IPython import get_ipython from numpy.linalg import norm -from os.path import basename from scipy.sparse import csc_matrix -from qpsolvers import dense_solvers, sparse_solvers -from qpsolvers import solve_qp +from qpsolvers import dense_solvers, solve_qp, sparse_solvers +dense_solvers = [] +sparse_solvers = ["scs", "scs_box"] n = 500 M = scipy.sparse.lil_matrix(scipy.sparse.eye(n)) @@ -43,20 +44,22 @@ q = -np.ones((n,)) G = csc_matrix(-scipy.sparse.eye(n)) h = -2 * np.ones((n,)) +lb = h +ub = -lb P_array = np.array(P.todense()) G_array = np.array(G.todense()) def check_same_solutions(tol=0.05): - sol0 = solve_qp(P, q, G, h, solver=sparse_solvers[0]) + sol0 = solve_qp(P, q, G, h, lb=lb, ub=ub, solver=sparse_solvers[0]) for solver in sparse_solvers: - sol = solve_qp(P, q, G, h, solver=solver) + sol = solve_qp(P, q, G, h, lb=lb, ub=ub, solver=solver) relvar = norm(sol - sol0) / norm(sol0) assert ( relvar < tol ), f"{solver}'s solution offset by {100.0 * relvar:.1f}%" for solver in dense_solvers: - sol = solve_qp(P_array, q, G_array, h, solver=solver) + sol = solve_qp(P_array, q, G_array, h, lb=lb, ub=ub, solver=solver) relvar = norm(sol - sol0) / norm(sol0) assert ( relvar < tol @@ -65,7 +68,7 @@ def check_same_solutions(tol=0.05): def time_dense_solvers(): instructions = { - solver: f"u = solve_qp(P_array, q, G_array, h, solver='{solver}')" + solver: f"u = solve_qp(P_array, q, G_array, h, lb=lb, ub=ub, solver='{solver}')" for solver in dense_solvers } print("\nDense solvers\n-------------") @@ -76,7 +79,7 @@ def time_dense_solvers(): def time_sparse_solvers(): instructions = { - solver: f"u = solve_qp(P, q, G, h, solver='{solver}')" + solver: f"u = solve_qp(P, q, G, h, lb=lb, ub=ub, solver='{solver}')" for solver in sparse_solvers } print("\nSparse solvers\n--------------") diff --git a/qpsolvers/solve_qp.py b/qpsolvers/solve_qp.py index 2118c37d..3fde3f77 100644 --- a/qpsolvers/solve_qp.py +++ b/qpsolvers/solve_qp.py @@ -135,7 +135,7 @@ def solve_qp( kwargs["initvals"] = initvals kwargs["verbose"] = verbose try: - if solver == "scs" and lb is not None: + if solver == "scs_box" and lb is not None: 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) diff --git a/qpsolvers/solvers/scs_.py b/qpsolvers/solvers/scs_.py index 24941d54..8caa673f 100644 --- a/qpsolvers/solvers/scs_.py +++ b/qpsolvers/solvers/scs_.py @@ -176,7 +176,7 @@ def scs_solve_qp( solution = solve(data, cone, **kwargs) status_val = solution["info"]["status_val"] if status_val != 1: - warn( + print( f"SCS returned {status_val}: {__status_val_meaning__[status_val]}" ) if status_val != 2: From 3b177aa1f416c6ba0067c98dc50c589df7327c0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Mon, 27 Jun 2022 13:42:17 +0200 Subject: [PATCH 06/25] [minor] Test scs_box in dense QP example --- examples/quadratic_programming.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/quadratic_programming.py b/examples/quadratic_programming.py index a3d7cd7b..a75e603e 100644 --- a/examples/quadratic_programming.py +++ b/examples/quadratic_programming.py @@ -34,11 +34,13 @@ h = np.array([3.0, 2.0, -2.0]) 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 = "quadprog" # see qpsolvers.available_solvers - x = solve_qp(P, q, G, h, A, b, solver=solver) + solver = "scs_box" # see qpsolvers.available_solvers + x = solve_qp(P, q, G, h, A, b, lb=lb, ub=ub, solver=solver) end_time = perf_counter() print("") From 514dc610f3a4800d4f1199d4c8531e6db4b44f99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Mon, 27 Jun 2022 13:42:31 +0200 Subject: [PATCH 07/25] [scs] Remove unused definition of bsize See https://github.com/bodono/scs-python/issues/63#issuecomment-1167213893 --- qpsolvers/solvers/scs_.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qpsolvers/solvers/scs_.py b/qpsolvers/solvers/scs_.py index 8caa673f..e165370f 100644 --- a/qpsolvers/solvers/scs_.py +++ b/qpsolvers/solvers/scs_.py @@ -172,7 +172,6 @@ def scs_solve_qp( (data["A"], zero_row, -sparse.eye(k)), format="csc", ) data["b"] = np.hstack((data["b"], 1.0, np.zeros(k))) - cone["bsize"] = k + 1 solution = solve(data, cone, **kwargs) status_val = solution["info"]["status_val"] if status_val != 1: From 967825b0f5c6d544806792b1454a5bb12b25c4d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Fri, 15 Jul 2022 10:41:41 +0200 Subject: [PATCH 08/25] [minor] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30767fa8..c20c4420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file. - Document how to add a new QP solver to the library +### Changed + +- SCS: use the box cone API when lower/upper bounds are set + ## [2.0.0] - 2022/07/05 ### Added From dd6b5e600e22d9480839b2e0707a5c616c55d106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Fri, 15 Jul 2022 10:42:06 +0200 Subject: [PATCH 09/25] Benchmark against all solvers again --- examples/benchmark_dense_problem.py | 6 ++---- examples/benchmark_random_problems.py | 2 -- examples/benchmark_sparse_problem.py | 6 ++---- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/examples/benchmark_dense_problem.py b/examples/benchmark_dense_problem.py index 5e72e6ec..afcec128 100644 --- a/examples/benchmark_dense_problem.py +++ b/examples/benchmark_dense_problem.py @@ -32,9 +32,6 @@ from qpsolvers import dense_solvers, solve_qp, sparse_solvers -dense_solvers = ["scs", "scs_box"] -sparse_solvers = [] - M = array([[1.0, 2.0, 0.0], [-8.0, 3.0, 2.0], [0.0, 1.0, 1.0]]) P = dot(M.T, M) q = dot(array([3.0, 2.0, 3.0]), M) @@ -59,7 +56,8 @@ for solver in dense_solvers } sparse_instr = { - solver: f"u = solve_qp(P_csc, q, G_csc, h, lb=lb, ub=ub, solver='{solver}')" + solver: "u = solve_qp(" + "P_csc, q, G_csc, h, lb=lb, ub=ub, solver='{solver}')" for solver in sparse_solvers } diff --git a/examples/benchmark_random_problems.py b/examples/benchmark_random_problems.py index c0c1f5ce..bd369a1b 100644 --- a/examples/benchmark_random_problems.py +++ b/examples/benchmark_random_problems.py @@ -38,8 +38,6 @@ from qpsolvers import available_solvers, solve_qp -available_solvers = ["scs", "scs_box"] - nb_iter = 10 sizes = [10, 20, 50, 100, 200, 500, 1000] diff --git a/examples/benchmark_sparse_problem.py b/examples/benchmark_sparse_problem.py index fd2cce12..0de58078 100644 --- a/examples/benchmark_sparse_problem.py +++ b/examples/benchmark_sparse_problem.py @@ -32,9 +32,6 @@ from qpsolvers import dense_solvers, solve_qp, sparse_solvers -dense_solvers = [] -sparse_solvers = ["scs", "scs_box"] - n = 500 M = scipy.sparse.lil_matrix(scipy.sparse.eye(n)) for i in range(1, n - 1): @@ -68,7 +65,8 @@ def check_same_solutions(tol=0.05): def time_dense_solvers(): instructions = { - solver: f"u = solve_qp(P_array, q, G_array, h, lb=lb, ub=ub, solver='{solver}')" + solver: "u = solve_qp(P_array, q, G_array, h, lb=lb, ub=ub, " + "solver='{solver}')" for solver in dense_solvers } print("\nDense solvers\n-------------") From 7e487e54267ec3730c0baef7416590d1fbcca55a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Fri, 15 Jul 2022 10:42:51 +0200 Subject: [PATCH 10/25] [SCS] Use box API when there is either lb or ub --- qpsolvers/solve_qp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qpsolvers/solve_qp.py b/qpsolvers/solve_qp.py index 9d107867..a345a55f 100644 --- a/qpsolvers/solve_qp.py +++ b/qpsolvers/solve_qp.py @@ -146,7 +146,7 @@ def solve_qp( kwargs["initvals"] = initvals kwargs["verbose"] = verbose try: - if solver == "scs_box" and lb is not None: + if solver == "scs" and (lb is not None or ub is not None): 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) From 02f70d112daf0a57363aad3950dbb8b07bd5b5dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Fri, 15 Jul 2022 10:49:14 +0200 Subject: [PATCH 11/25] [minor] Clean out unused import --- qpsolvers/solvers/scs_.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qpsolvers/solvers/scs_.py b/qpsolvers/solvers/scs_.py index e165370f..9269f337 100644 --- a/qpsolvers/solvers/scs_.py +++ b/qpsolvers/solvers/scs_.py @@ -21,7 +21,6 @@ """Solver interface for SCS""" from typing import Optional -from warnings import warn import numpy as np from numpy.linalg import norm From 17de1dd83ce37efd18cf2b736a5acf64543a1d90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Fri, 15 Jul 2022 10:53:21 +0200 Subject: [PATCH 12/25] Bounds need not be both specified at the same time See https://github.com/bodono/scs-python/issues/63#issuecomment-1185326252 --- qpsolvers/solvers/scs_.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qpsolvers/solvers/scs_.py b/qpsolvers/solvers/scs_.py index 9269f337..b76da75e 100644 --- a/qpsolvers/solvers/scs_.py +++ b/qpsolvers/solvers/scs_.py @@ -162,13 +162,14 @@ def scs_solve_qp( "q has component in the nullspace of P" ) return x - if lb is not None and ub is not None: + if lb is not None or ub is not None: cone["bl"] = lb cone["bu"] = ub k = lb.shape[0] zero_row = sparse.csc_matrix((1, k)) data["A"] = sparse.vstack( - (data["A"], zero_row, -sparse.eye(k)), format="csc", + (data["A"], zero_row, -sparse.eye(k)), + format="csc", ) data["b"] = np.hstack((data["b"], 1.0, np.zeros(k))) solution = solve(data, cone, **kwargs) From 80d7cb9b4c144a472cc43554ed6ec46899aaeebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Fri, 15 Jul 2022 11:11:38 +0200 Subject: [PATCH 13/25] [SCS] Add type annotations to help mypy --- qpsolvers/solvers/scs_.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qpsolvers/solvers/scs_.py b/qpsolvers/solvers/scs_.py index b76da75e..7be7e997 100644 --- a/qpsolvers/solvers/scs_.py +++ b/qpsolvers/solvers/scs_.py @@ -20,7 +20,7 @@ """Solver interface for SCS""" -from typing import Optional +from typing import Any, Dict, Optional import numpy as np from numpy.linalg import norm @@ -136,8 +136,8 @@ 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: From 534309bf2f0455f61bf01bf87dd1d00dcc112bc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Fri, 15 Jul 2022 11:12:23 +0200 Subject: [PATCH 14/25] Handle partial lower/upper bounds See https://github.com/bodono/scs-python/issues/63#issuecomment-1185326252 --- qpsolvers/solvers/scs_.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/qpsolvers/solvers/scs_.py b/qpsolvers/solvers/scs_.py index 7be7e997..8a033840 100644 --- a/qpsolvers/solvers/scs_.py +++ b/qpsolvers/solvers/scs_.py @@ -163,15 +163,15 @@ def scs_solve_qp( ) return x if lb is not None or ub is not None: - cone["bl"] = lb - cone["bu"] = ub - k = lb.shape[0] - zero_row = sparse.csc_matrix((1, k)) + 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(k)), + (data["A"], zero_row, -sparse.eye(n)), format="csc", ) - data["b"] = np.hstack((data["b"], 1.0, np.zeros(k))) + 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: From 857bf680f1d940cf54d485b9538c1dea53b6e641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Fri, 15 Jul 2022 11:18:19 +0200 Subject: [PATCH 15/25] Fix f-strings --- examples/benchmark_dense_problem.py | 2 +- examples/benchmark_sparse_problem.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/benchmark_dense_problem.py b/examples/benchmark_dense_problem.py index afcec128..00ef0962 100644 --- a/examples/benchmark_dense_problem.py +++ b/examples/benchmark_dense_problem.py @@ -57,7 +57,7 @@ } sparse_instr = { solver: "u = solve_qp(" - "P_csc, q, G_csc, h, lb=lb, ub=ub, solver='{solver}')" + f"P_csc, q, G_csc, h, lb=lb, ub=ub, solver='{solver}')" for solver in sparse_solvers } diff --git a/examples/benchmark_sparse_problem.py b/examples/benchmark_sparse_problem.py index 0de58078..e7eca03c 100644 --- a/examples/benchmark_sparse_problem.py +++ b/examples/benchmark_sparse_problem.py @@ -66,7 +66,7 @@ def check_same_solutions(tol=0.05): def time_dense_solvers(): instructions = { solver: "u = solve_qp(P_array, q, G_array, h, lb=lb, ub=ub, " - "solver='{solver}')" + f"solver='{solver}')" for solver in dense_solvers } print("\nDense solvers\n-------------") From 3f31febd7dde8f74781a0bec9bcb00ea9c171e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Fri, 15 Jul 2022 11:21:52 +0200 Subject: [PATCH 16/25] Don't change size range of random benchmark problems --- examples/benchmark_random_problems.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/benchmark_random_problems.py b/examples/benchmark_random_problems.py index bd369a1b..26295872 100644 --- a/examples/benchmark_random_problems.py +++ b/examples/benchmark_random_problems.py @@ -40,7 +40,7 @@ nb_iter = 10 -sizes = [10, 20, 50, 100, 200, 500, 1000] +sizes = [10, 20, 50, 100, 200, 500, 1000, 2000] def solve_random_qp(n, solver): From 046c8adcde4e2980834b65e28c447b79e6e473a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Fri, 15 Jul 2022 11:25:35 +0200 Subject: [PATCH 17/25] [minor] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a4fd08a..64f387b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ 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 From 65cd96839a62d1ebd24d0ef7c27861d7cf573a73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Fri, 15 Jul 2022 11:26:10 +0200 Subject: [PATCH 18/25] Remove box bounds from benchmark problems This will avoid discontinuity in the benchmark. Let's make a separate case for box bounds later on. --- examples/benchmark_dense_problem.py | 21 +++++++++------------ examples/benchmark_random_problems.py | 23 +++++------------------ examples/benchmark_sparse_problem.py | 21 ++++++++++----------- 3 files changed, 24 insertions(+), 41 deletions(-) diff --git a/examples/benchmark_dense_problem.py b/examples/benchmark_dense_problem.py index 00ef0962..35a88097 100644 --- a/examples/benchmark_dense_problem.py +++ b/examples/benchmark_dense_problem.py @@ -22,23 +22,21 @@ Test all available QP solvers on a dense quadratic program. """ -from os.path import basename - -import numpy as np from IPython import get_ipython from numpy import array, dot from numpy.linalg import norm +from os.path import basename from scipy.sparse import csc_matrix -from qpsolvers import dense_solvers, solve_qp, sparse_solvers +from qpsolvers import dense_solvers, sparse_solvers +from qpsolvers import solve_qp + M = array([[1.0, 2.0, 0.0], [-8.0, 3.0, 2.0], [0.0, 1.0, 1.0]]) P = dot(M.T, M) q = dot(array([3.0, 2.0, 3.0]), M) G = array([[1.0, 2.0, 1.0], [2.0, 0.0, 1.0], [-1.0, 2.0, -1.0]]) h = array([3.0, 2.0, -2.0]) -lb = -0.5 * np.ones(3) -ub = 1.0 * np.ones(3) P_csc = csc_matrix(P) G_csc = csc_matrix(G) @@ -52,25 +50,24 @@ exit() dense_instr = { - solver: f"u = solve_qp(P, q, G, h, lb=lb, ub=ub, solver='{solver}')" + solver: f"u = solve_qp(P, q, G, h, solver='{solver}')" for solver in dense_solvers } sparse_instr = { - solver: "u = solve_qp(" - f"P_csc, q, G_csc, h, lb=lb, ub=ub, solver='{solver}')" + solver: f"u = solve_qp(P_csc, q, G_csc, h, solver='{solver}')" for solver in sparse_solvers } print("\nTesting all QP solvers on a dense quadratic program...") - sol0 = solve_qp(P, q, G, h, lb=lb, ub=ub, solver=dense_solvers[0]) + sol0 = solve_qp(P, q, G, h, solver=dense_solvers[0]) abstol = 2e-4 # tolerance on absolute solution error for solver in dense_solvers: - sol = solve_qp(P, q, G, h, lb=lb, ub=ub, solver=solver) + sol = solve_qp(P, q, G, h, solver=solver) delta = norm(sol - sol0) assert delta < abstol, f"{solver}'s solution offset by {delta:.1e}" for solver in sparse_solvers: - sol = solve_qp(P_csc, q, G_csc, h, lb=lb, ub=ub, solver=solver) + sol = solve_qp(P_csc, q, G_csc, h, solver=solver) delta = norm(sol - sol0) assert delta < abstol, f"{solver}'s solution offset by {delta:.1e}" diff --git a/examples/benchmark_random_problems.py b/examples/benchmark_random_problems.py index 26295872..3a70bb37 100644 --- a/examples/benchmark_random_problems.py +++ b/examples/benchmark_random_problems.py @@ -30,11 +30,10 @@ print("This example requires IPython, try installing ipython3") sys.exit(-1) -from os.path import basename -from timeit import timeit - from numpy import dot, linspace, ones, random +from os.path import basename from scipy.linalg import toeplitz +from timeit import timeit from qpsolvers import available_solvers, solve_qp @@ -50,25 +49,13 @@ def solve_random_qp(n, solver): [1.0, 0.0, 0.0] + [0.0] * (n - 3), [1.0, 2.0, 3.0] + [0.0] * (n - 3) ) h = ones(n) - lb = -0.03 * ones(n) - ub = +0.03 * ones(n) - return solve_qp(P, q, G, h, lb=lb, ub=ub, solver=solver) + return solve_qp(P, q, G, h, solver=solver) def plot_results(perfs): try: - from pylab import ( - clf, - get_cmap, - grid, - ion, - legend, - plot, - xlabel, - xscale, - ylabel, - yscale, - ) + from pylab import clf, get_cmap, grid, ion, legend, plot + from pylab import xlabel, xscale, ylabel, yscale except ImportError: print("Cannot plot results, try installing python3-matplotlib") print("Results are stored in the global `perfs` dictionary") diff --git a/examples/benchmark_sparse_problem.py b/examples/benchmark_sparse_problem.py index e7eca03c..0aa70d5b 100644 --- a/examples/benchmark_sparse_problem.py +++ b/examples/benchmark_sparse_problem.py @@ -22,15 +22,17 @@ Test all available QP solvers on a sparse quadratic program. """ -from os.path import basename - import numpy as np import scipy.sparse + from IPython import get_ipython from numpy.linalg import norm +from os.path import basename from scipy.sparse import csc_matrix -from qpsolvers import dense_solvers, solve_qp, sparse_solvers +from qpsolvers import dense_solvers, sparse_solvers +from qpsolvers import solve_qp + n = 500 M = scipy.sparse.lil_matrix(scipy.sparse.eye(n)) @@ -41,22 +43,20 @@ q = -np.ones((n,)) G = csc_matrix(-scipy.sparse.eye(n)) h = -2 * np.ones((n,)) -lb = h -ub = -lb P_array = np.array(P.todense()) G_array = np.array(G.todense()) def check_same_solutions(tol=0.05): - sol0 = solve_qp(P, q, G, h, lb=lb, ub=ub, solver=sparse_solvers[0]) + sol0 = solve_qp(P, q, G, h, solver=sparse_solvers[0]) for solver in sparse_solvers: - sol = solve_qp(P, q, G, h, lb=lb, ub=ub, solver=solver) + sol = solve_qp(P, q, G, h, solver=solver) relvar = norm(sol - sol0) / norm(sol0) assert ( relvar < tol ), f"{solver}'s solution offset by {100.0 * relvar:.1f}%" for solver in dense_solvers: - sol = solve_qp(P_array, q, G_array, h, lb=lb, ub=ub, solver=solver) + sol = solve_qp(P_array, q, G_array, h, solver=solver) relvar = norm(sol - sol0) / norm(sol0) assert ( relvar < tol @@ -65,8 +65,7 @@ def check_same_solutions(tol=0.05): def time_dense_solvers(): instructions = { - solver: "u = solve_qp(P_array, q, G_array, h, lb=lb, ub=ub, " - f"solver='{solver}')" + solver: f"u = solve_qp(P_array, q, G_array, h, solver='{solver}')" for solver in dense_solvers } print("\nDense solvers\n-------------") @@ -77,7 +76,7 @@ def time_dense_solvers(): def time_sparse_solvers(): instructions = { - solver: f"u = solve_qp(P, q, G, h, lb=lb, ub=ub, solver='{solver}')" + solver: f"u = solve_qp(P, q, G, h, solver='{solver}')" for solver in sparse_solvers } print("\nSparse solvers\n--------------") From c17f89d10d0520d18254c95a2dc041be55aa120a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Fri, 15 Jul 2022 11:27:57 +0200 Subject: [PATCH 19/25] Select solver randomly in box bounds example --- examples/lower_upper_bounds.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/lower_upper_bounds.py b/examples/lower_upper_bounds.py index 21816619..b9856c57 100644 --- a/examples/lower_upper_bounds.py +++ b/examples/lower_upper_bounds.py @@ -22,6 +22,8 @@ Test the "quadprog" QP solver on a small dense problem. """ +import random + from time import perf_counter import numpy as np @@ -41,7 +43,8 @@ if __name__ == "__main__": start_time = perf_counter() solver = "scs" if "scs" in available_solvers else available_solvers[0] - x = solve_qp(P, q, A=A, b=b, lb=lb, ub=ub, solver="scs") + 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("") @@ -58,6 +61,3 @@ print(f"Solution: x = {x}") print(f"Solve time: {1e6 * (end_time - start_time):.0f} [us]") print(f"Solver: {solver}") - - x = solve_qp(P, q, A=A, b=b, lb=lb, ub=ub, solver="quadprog") - print(f"Solution (quadprog): x = {x}") From 3f107272d683d9c7b88f375e1a388bf08294621d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Fri, 15 Jul 2022 11:28:47 +0200 Subject: [PATCH 20/25] Revert debug print instruction --- qpsolvers/solvers/scs_.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qpsolvers/solvers/scs_.py b/qpsolvers/solvers/scs_.py index 8a033840..39340af4 100644 --- a/qpsolvers/solvers/scs_.py +++ b/qpsolvers/solvers/scs_.py @@ -21,6 +21,7 @@ """Solver interface for SCS""" from typing import Any, Dict, Optional +from warnings import warn import numpy as np from numpy.linalg import norm @@ -175,7 +176,7 @@ def scs_solve_qp( solution = solve(data, cone, **kwargs) status_val = solution["info"]["status_val"] if status_val != 1: - print( + warn( f"SCS returned {status_val}: {__status_val_meaning__[status_val]}" ) if status_val != 2: From 532dd67c581fe107230d6be0dc07d522b3315e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Mon, 25 Jul 2022 17:04:53 +0200 Subject: [PATCH 21/25] Simplify box inequalities example --- ...wer_upper_bounds.py => box_inequalities.py} | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) rename examples/{lower_upper_bounds.py => box_inequalities.py} (77%) diff --git a/examples/lower_upper_bounds.py b/examples/box_inequalities.py similarity index 77% rename from examples/lower_upper_bounds.py rename to examples/box_inequalities.py index b9856c57..cd71b1cd 100644 --- a/examples/lower_upper_bounds.py +++ b/examples/box_inequalities.py @@ -19,7 +19,7 @@ # along with qpsolvers. If not, see . """ -Test the "quadprog" QP solver on a small dense problem. +Test one of the available QP solvers on a small problem with box inequalities. """ import random @@ -33,8 +33,6 @@ M = np.array([[1.0, 2.0, 0.0], [-8.0, 3.0, 2.0], [0.0, 1.0, 1.0]]) P = np.dot(M.T, M) # this is a positive definite matrix q = np.dot(np.array([3.0, 2.0, 3.0]), M) -G = np.array([[1.0, 2.0, 1.0], [2.0, 0.0, 1.0], [-1.0, 2.0, -1.0]]) -h = np.array([3.0, 2.0, -2.0]) A = np.array([1.0, 1.0, 1.0]) b = np.array([1.0]) lb = -0.5 * np.ones(3) @@ -42,22 +40,20 @@ if __name__ == "__main__": start_time = perf_counter() - solver = "scs" if "scs" in available_solvers else available_solvers[0] 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. G * x <= h") - print(" A * x == b") + print(" s.t. A * x == b") + print(" lb <= x <= ub") print("") print_matrix_vector(P, "P", q, "q") print("") - print_matrix_vector(G, "G", h, "h") - print("") print_matrix_vector(A, "A", b, "b") print("") - print(f"Solution: x = {x}") - print(f"Solve time: {1e6 * (end_time - start_time):.0f} [us]") - print(f"Solver: {solver}") + 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}") From 58aa67450bd579333175b943c41d46cbe2ceab99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Mon, 25 Jul 2022 17:34:17 +0200 Subject: [PATCH 22/25] Box inequalities without kwargs in QP example --- examples/box_inequalities.py | 2 +- examples/quadratic_programming.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/box_inequalities.py b/examples/box_inequalities.py index cd71b1cd..b6065da5 100644 --- a/examples/box_inequalities.py +++ b/examples/box_inequalities.py @@ -46,7 +46,7 @@ print("") print(" min. 1/2 x^T P x + q^T x") - print(" s.t. A * x == b") + print(" s.t. A * x == b") print(" lb <= x <= ub") print("") print_matrix_vector(P, "P", q, "q") diff --git a/examples/quadratic_programming.py b/examples/quadratic_programming.py index 00fbd087..98ab8d7b 100644 --- a/examples/quadratic_programming.py +++ b/examples/quadratic_programming.py @@ -50,13 +50,14 @@ start_time = perf_counter() solver = random.choice(available_solvers) - x = solve_qp(P, q, G, h, A, b, lb=lb, ub=ub, solver=solver) + x = solve_qp(P, q, G, h, A, b, lb, ub, solver=solver) end_time = perf_counter() print("") print(" min. 1/2 x^T P x + q^T x") - print(" s.t. G * x <= h") - print(" A * x == b") + print(" s.t. G * x <= h") + print(" A * x == b") + print(" lb <= x <= ub") print("") print_matrix_vector(P, "P", q, "q") print("") @@ -64,6 +65,8 @@ print("") print_matrix_vector(A, "A", b, "b") print("") + print_matrix_vector(lb.reshape((3, 1)), "lb", ub, "ub") + print("") print(f"Solution: x = {x}") print(f"Solve time: {1e6 * (end_time - start_time):.0f} [us]") print(f"Solver: {solver}") From d301a98b621ea3d4e5cc01e384e41cdc6c3a1a6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Mon, 25 Jul 2022 17:37:57 +0200 Subject: [PATCH 23/25] [minor] Test expression manicure :lipstick: --- qpsolvers/solve_qp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qpsolvers/solve_qp.py b/qpsolvers/solve_qp.py index a345a55f..3008a4f3 100644 --- a/qpsolvers/solve_qp.py +++ b/qpsolvers/solve_qp.py @@ -146,7 +146,7 @@ def solve_qp( kwargs["initvals"] = initvals kwargs["verbose"] = verbose try: - if solver == "scs" and (lb is not None or ub is not None): + 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) From f3162b0ac356c73c0e6e32602ba60ed67a7e58fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Mon, 25 Jul 2022 17:42:08 +0200 Subject: [PATCH 24/25] [doc] Update SCS QP equation --- qpsolvers/solvers/scs_.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qpsolvers/solvers/scs_.py b/qpsolvers/solvers/scs_.py index 39340af4..c3e52557 100644 --- a/qpsolvers/solvers/scs_.py +++ b/qpsolvers/solvers/scs_.py @@ -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 `_. From cacccdc95d13ef4e1ae786bbff3511c470047d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Caron?= Date: Mon, 25 Jul 2022 17:43:32 +0200 Subject: [PATCH 25/25] [minor] Keep QP example as it is in master --- examples/quadratic_programming.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/examples/quadratic_programming.py b/examples/quadratic_programming.py index 98ab8d7b..1e06c742 100644 --- a/examples/quadratic_programming.py +++ b/examples/quadratic_programming.py @@ -37,8 +37,6 @@ h = np.array([3.0, 2.0, -2.0]) 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__": if not available_solvers: @@ -50,14 +48,13 @@ start_time = perf_counter() solver = random.choice(available_solvers) - x = solve_qp(P, q, G, h, A, b, lb, ub, solver=solver) + x = solve_qp(P, q, G, h, A, b, solver=solver) end_time = perf_counter() print("") print(" min. 1/2 x^T P x + q^T x") - print(" s.t. G * x <= h") - print(" A * x == b") - print(" lb <= x <= ub") + print(" s.t. G * x <= h") + print(" A * x == b") print("") print_matrix_vector(P, "P", q, "q") print("") @@ -65,8 +62,6 @@ print("") print_matrix_vector(A, "A", b, "b") print("") - print_matrix_vector(lb.reshape((3, 1)), "lb", ub, "ub") - print("") print(f"Solution: x = {x}") print(f"Solve time: {1e6 * (end_time - start_time):.0f} [us]") print(f"Solver: {solver}")