Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions tests/test_dual_variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@

torch.set_default_dtype(torch.double)

# ``gradcheck`` compares the analytic derivative against a finite difference with a step
# of ~1e-6, so the forward solve has to be accurate to well below that or the difference
# quotient is dominated by solver noise rather than by curvature. SCS's default tolerance
# is not enough: it caps the accuracy of both the finite difference *and* the implicit
# derivative at ~1e-4, which is the tolerance these tests assert. Ask for a tight solve
# instead of loosening the assertions - see issue #244.
GRADCHECK_SOLVER_ARGS = {"eps": 1e-10}


def test_equality_constraint_dual():
"""Test returning dual variable for equality constraint."""
Expand Down Expand Up @@ -220,6 +228,7 @@ def test_dual_gradcheck_equality():
prob,
parameters=[c, b],
variables=[x, eq_con.dual_variables[0]],
solver_args=GRADCHECK_SOLVER_ARGS,
)

# Function that returns dual variable for gradcheck
Expand All @@ -246,6 +255,7 @@ def test_dual_gradcheck_inequality():
prob,
parameters=[c],
variables=[x, ineq_con.dual_variables[0]],
solver_args=GRADCHECK_SOLVER_ARGS,
)

# Function that returns dual variable for gradcheck
Expand All @@ -272,6 +282,7 @@ def test_dual_gradcheck_mixed():
prob,
parameters=[c, b],
variables=[x, eq_con.dual_variables[0]],
solver_args=GRADCHECK_SOLVER_ARGS,
)

# Function that returns both primal and dual for gradcheck
Expand Down Expand Up @@ -300,6 +311,7 @@ def test_dual_gradcheck_vector_equality():
prob,
parameters=[A, b],
variables=[x, eq_con.dual_variables[0]],
solver_args=GRADCHECK_SOLVER_ARGS,
)

def f(A_t, b_t):
Expand Down Expand Up @@ -357,6 +369,7 @@ def test_soc_gradcheck():
prob,
parameters=[c, t],
variables=[x, soc_con.dual_variables[0]],
solver_args=GRADCHECK_SOLVER_ARGS,
)

def f(c_t, t_t):
Expand Down Expand Up @@ -446,6 +459,7 @@ def test_exp_cone_gradcheck():
exp_con.dual_variables[1],
exp_con.dual_variables[2],
],
solver_args=GRADCHECK_SOLVER_ARGS,
)

def f(t_t):
Expand Down Expand Up @@ -564,6 +578,7 @@ def test_psd_gradcheck():
prob,
parameters=[C],
variables=[X, psd_con.dual_variables[0]],
solver_args=GRADCHECK_SOLVER_ARGS,
)

def f(C_t):
Expand Down Expand Up @@ -718,6 +733,7 @@ def test_gp_dual_gradcheck():
parameters=[a, b],
variables=[x, y, ineq_con.dual_variables[0]],
gp=True,
solver_args=GRADCHECK_SOLVER_ARGS,
)

def f(a_t, b_t):
Expand Down Expand Up @@ -792,6 +808,7 @@ def test_soc_explicit_multi_dual_gradcheck():
prob,
parameters=[c, t_param],
variables=[x, t, soc_con.dual_variables[0], soc_con.dual_variables[1]],
solver_args=GRADCHECK_SOLVER_ARGS,
)

def f(c_t, t_t):
Expand Down
66 changes: 54 additions & 12 deletions tests/test_torch.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@

torch.set_default_dtype(torch.double)

# ``gradcheck`` compares the analytic derivative against a finite difference with a step
# of ~1e-6, so the forward solve has to be accurate to well below that or the difference
# quotient is dominated by solver noise rather than by curvature. SCS's default tolerance
# is not enough: it caps the accuracy of both the finite difference *and* the implicit
# derivative at ~1e-4, which is the tolerance these tests assert. Ask for a tight solve
# instead of loosening the assertions - see issue #244.
GRADCHECK_SOLVER_ARGS = {"eps": 1e-10}


def set_seed(x: int) -> np.random.Generator:
"""Set the random seed for torch and return a numpy random generator.
Expand Down Expand Up @@ -77,7 +85,7 @@ def test_simple_batch_socp():
constraints = [A @ x == b, cp.norm(x) <= 1]
prob = cp.Problem(cp.Minimize(objective), constraints)

prob_tch = CvxpyLayer(prob, [P_sqrt, q, A, b], [x])
prob_tch = CvxpyLayer(prob, [P_sqrt, q, A, b], [x], solver_args=GRADCHECK_SOLVER_ARGS)

P_sqrt_tch = torch.randn(batch_size, n, n, requires_grad=True)
q_tch = torch.randn(batch_size, n, 1, requires_grad=True)
Expand Down Expand Up @@ -182,7 +190,7 @@ def test_logistic_regression():
)
prob = cp.Problem(cp.Minimize(-log_likelihood + lam * cp.sum_squares(a)))

fit_logreg = CvxpyLayer(prob, [X, lam], [a])
fit_logreg = CvxpyLayer(prob, [X, lam], [a], solver_args=GRADCHECK_SOLVER_ARGS)

torch.autograd.gradcheck(fit_logreg, (X_th, lam_th), atol=1e-4)

Expand All @@ -206,7 +214,7 @@ def test_entropy_maximization():
obj = cp.Maximize(cp.sum(cp.entr(x)) - 0.01 * cp.sum_squares(x))
constraints = [A @ x == b, F @ x <= g]
prob = cp.Problem(obj, constraints)
layer = CvxpyLayer(prob, [A, b, F, g], [x])
layer = CvxpyLayer(prob, [A, b, F, g], [x], solver_args=GRADCHECK_SOLVER_ARGS)

A_th, b_th, F_th, g_th = map(
lambda x: torch.from_numpy(x).requires_grad_(),
Expand All @@ -224,7 +232,7 @@ def test_lml():
obj = -x @ y - cp.sum(cp.entr(y)) - cp.sum(cp.entr(1.0 - y))
cons = [cp.sum(y) == k]
prob = cp.Problem(cp.Minimize(obj), cons)
lml = CvxpyLayer(prob, [x], [y])
lml = CvxpyLayer(prob, [x], [y], solver_args=GRADCHECK_SOLVER_ARGS)

x_th = torch.tensor([1.0, -1.0, -1.0, -1.0]).requires_grad_()
torch.autograd.gradcheck(lml, (x_th,), atol=1e-3)
Expand All @@ -240,12 +248,16 @@ def test_sdp():
trace_con = cp.trace(X) == 1
prob = cp.Problem(cp.Minimize(cp.trace(C @ X)), [psd_con, trace_con])

layer = CvxpyLayer(prob, parameters=[C], variables=[X])
layer = CvxpyLayer(prob, parameters=[C], variables=[X], solver_args=GRADCHECK_SOLVER_ARGS)

# Use a well-conditioned symmetric matrix
C_t = torch.tensor([[2.0, 0.5, 0.1], [0.5, 3.0, 0.2], [0.1, 0.2, 1.5]], requires_grad=True)

torch.autograd.gradcheck(layer, (C_t,), atol=1e-4, rtol=1e-3)
# The optimum is the rank-one projector onto the smallest eigenvector of C, so most
# entries of dX/dC are near zero and are compared against ``atol`` alone. Even with a
# tight solve, the default 1e-6 step leaves those entries at the finite-difference
# floor (~1e-4); a larger step trades truncation error for noise and gives real margin.
torch.autograd.gradcheck(layer, (C_t,), eps=1e-4, atol=1e-4, rtol=1e-3)


def test_not_enough_parameters():
Expand Down Expand Up @@ -419,7 +431,7 @@ def test_equality():
x = cp.Variable(n)
b = cp.Parameter(n)
prob = cp.Problem(cp.Minimize(cp.sum_squares(x)), [A @ x == b])
layer = CvxpyLayer(prob, parameters=[b], variables=[x])
layer = CvxpyLayer(prob, parameters=[b], variables=[x], solver_args=GRADCHECK_SOLVER_ARGS)

b_th = torch.randn(n).double().requires_grad_()

Expand All @@ -441,7 +453,13 @@ def test_basic_gp():
problem = cp.Problem(cp.Minimize(objective_fn), constraints)
problem.solve(cp.CLARABEL, gp=True)

layer = CvxpyLayer(problem, parameters=[a, b, c], variables=[x, y, z], gp=True)
layer = CvxpyLayer(
problem,
parameters=[a, b, c],
variables=[x, y, z],
gp=True,
solver_args=GRADCHECK_SOLVER_ARGS,
)
a_th = torch.tensor([2.0]).requires_grad_()
b_th = torch.tensor([1.0]).requires_grad_()
c_th = torch.tensor([0.5]).requires_grad_()
Expand Down Expand Up @@ -476,7 +494,13 @@ def test_batched_gp():
problem = cp.Problem(cp.Minimize(objective_fn), constraints)

# Create layer
layer = CvxpyLayer(problem, parameters=[a, b, c], variables=[x, y, z], gp=True)
layer = CvxpyLayer(
problem,
parameters=[a, b, c],
variables=[x, y, z],
gp=True,
solver_args=GRADCHECK_SOLVER_ARGS,
)

# Batched parameters - test with batch size 4 (double precision)
# For scalar parameters, batching means 1D tensors
Expand Down Expand Up @@ -536,7 +560,13 @@ def test_gp_without_param_values():
problem = cp.Problem(cp.Minimize(objective_fn), constraints)

# This should work WITHOUT needing to set a.value, b.value, c.value
layer = CvxpyLayer(problem, parameters=[a, b, c], variables=[x, y, z], gp=True)
layer = CvxpyLayer(
problem,
parameters=[a, b, c],
variables=[x, y, z],
gp=True,
solver_args=GRADCHECK_SOLVER_ARGS,
)

# Now use the layer with actual parameter values
a_th = torch.tensor([2.0], dtype=torch.float64, requires_grad=True)
Expand Down Expand Up @@ -586,8 +616,20 @@ def test_gp_reversed_parameter_order():
problem = cp.Problem(cp.Minimize(objective_fn), constraints)

# Create layers with parameters in different orders
layer_abc = CvxpyLayer(problem, parameters=[a, b, c], variables=[x, y, z], gp=True)
layer_cba = CvxpyLayer(problem, parameters=[c, b, a], variables=[x, y, z], gp=True)
layer_abc = CvxpyLayer(
problem,
parameters=[a, b, c],
variables=[x, y, z],
gp=True,
solver_args=GRADCHECK_SOLVER_ARGS,
)
layer_cba = CvxpyLayer(
problem,
parameters=[c, b, a],
variables=[x, y, z],
gp=True,
solver_args=GRADCHECK_SOLVER_ARGS,
)

a_th = torch.tensor([2.0], dtype=torch.float64, requires_grad=True)
b_th = torch.tensor([1.0], dtype=torch.float64, requires_grad=True)
Expand Down