import math
import numpy as np
import pytest
import scipy.sparse as sp
import herculesabqp
class DiagonalOperator:
def __init__(self, diag):
self.diag = np.asarray(diag, dtype=np.float64)
self.n = int(self.diag.size)
def matvec(self, x):
x = np.asarray(x, dtype=np.float64)
return self.diag * x
def diagonal(self):
return self.diag
def gershgorin_upper_bound(self):
return float(np.max(self.diag))
class IdentityOperator:
def __init__(self, n):
self.n = int(n)
def matvec(self, x):
return np.asarray(x, dtype=np.float64)
@pytest.fixture
def explicit_problem():
q = np.array([[4.0, 1.0], [1.0, 3.0]], dtype=np.float64)
c = np.array([-1.0, -0.5], dtype=np.float64)
lb = np.zeros(2, dtype=np.float64)
ub = np.ones(2, dtype=np.float64)
return q, c, lb, ub
@pytest.fixture
def sparse_explicit_problem(explicit_problem):
q, c, lb, ub = explicit_problem
return sp.csr_matrix(q), c, lb, ub
@pytest.fixture
def implicit_problem():
op = DiagonalOperator([2.0, 2.0, 2.0])
c = np.array([-1.0, 0.5, -3.0], dtype=np.float64)
lb = np.zeros(3, dtype=np.float64)
ub = np.ones(3, dtype=np.float64)
return op, c, lb, ub
@pytest.fixture
def prepared_implicit_problem():
op = DiagonalOperator([4.0, 9.0])
c = np.array([-2.0, 3.0], dtype=np.float64)
lb = np.zeros(2, dtype=np.float64)
ub = np.ones(2, dtype=np.float64)
return op, c, lb, ub
def test_explicit_dense_solve(explicit_problem):
q, c, lb, ub = explicit_problem
result = herculesabqp.solve_box_qp(
q,
c,
lb,
ub,
assume_symmetric=True,
scaling="none",
)
assert result["kkt_inf"] < 1e-5
assert result["gap"] >= 0.0
assert math.isfinite(result["certified_lower_bound"])
assert abs(
result["objective"] - (result["certified_lower_bound"] + result["gap"])
) < 1e-8
assert 0.0 <= result["x"][0] <= 1.0
assert 0.0 <= result["x"][1] <= 1.0
def test_explicit_prepared_sparse_solve(sparse_explicit_problem):
q, c, lb, ub = sparse_explicit_problem
solver = herculesabqp.PreparedSolver(
q,
c,
assume_symmetric=True,
scaling="none",
)
result = solver.solve(lb, ub)
assert result["kkt_inf"] < 1e-5
assert len(result["x"]) == 2
def test_implicit_solve(implicit_problem):
op, c, lb, ub = implicit_problem
result = herculesabqp.solve_box_qp_implicit(
op,
c,
lb,
ub,
assume_symmetric=True,
scaling="hessian_diag",
)
assert result["kkt_inf"] < 1e-5
assert result["scaling_applied"]
assert result["scaling_name"] == "hessian_diag"
def test_implicit_falls_back_to_unscaled_without_diagonal():
op = IdentityOperator(2)
c = np.array([-0.25, 0.5], dtype=np.float64)
lb = np.zeros(2, dtype=np.float64)
ub = np.ones(2, dtype=np.float64)
result = herculesabqp.solve_box_qp_implicit(
op,
c,
lb,
ub,
assume_symmetric=True,
scaling="hessian_diag",
)
assert not result["scaling_applied"]
assert result["scaling_name"] == "none"
def test_prepared_implicit_solve(prepared_implicit_problem):
op, c, lb, ub = prepared_implicit_problem
solver = herculesabqp.PreparedImplicitSolver(
op,
c,
assume_symmetric=True,
scaling="hessian_diag",
)
result = solver.solve(lb, ub)
assert math.isfinite(result["objective"])
assert result["scaling_applied"]