Skip to content

Commit ba3af1e

Browse files
committed
merge
2 parents aca6ea3 + 934e338 commit ba3af1e

5 files changed

Lines changed: 275 additions & 6 deletions

File tree

cvxpy/reductions/solvers/nlp_solvers/diff_engine/registry.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,14 @@ def convert_prod(expr, children):
189189
raise NotImplementedError(f"prod with axis={axis} not supported in DNLP")
190190

191191
def convert_transpose(expr, children):
192-
# 1D transpose is a numpy no-op; C stores 1D as (1, n), don't flip to (n, 1).
192+
# If a user calls transpose on a (n, ) expression, CVXPY treats it as a
193+
# no-op and keeps the shape as (n, ). The diff engine represents all 1D
194+
# expressions as (1, n), so we need to check for this case and avoid
195+
# transposing if it's just a 1D expression.
193196
if len(expr.args[0].shape) <= 1:
194197
return children[0]
195198

199+
# If the child is a vector (shape (n,1) or (1,n), use reshape to transpose
196200
child_shape = normalize_shape(expr.args[0].shape)
197201
if 1 in child_shape:
198202
return _diffengine.make_reshape(children[0], child_shape[1], child_shape[0])

cvxpy/reductions/solvers/nlp_solving_chain.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,12 +209,17 @@ def solve_nlp(problem, solver, warm_start, verbose, **kwargs):
209209
verbose, solver_opts=kwargs,
210210
solver_cache=solver_cache)
211211

212-
# Unpack to get the objective value in the original problem space
212+
# unpack to get the objective value in the original problem space.
213+
# (+inf for infeasible runs, -inf for unbounded runs)
213214
problem.unpack_results(solution, nlp_chain, inverse_data)
214-
obj_value = problem.objective.value
215+
obj_value = problem.value
215216

216217
all_objs[run] = obj_value
217-
if obj_value is not None and obj_value < best_obj:
218+
219+
# always set best_solution with the first run so that even an
220+
# all-infeasible best_of has a solution to unpack at the end (its
221+
# INFEASIBLE status then propagates through unpack_results).
222+
if best_solution is None or obj_value < best_obj:
218223
best_obj = obj_value
219224
best_solution = solution
220225

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
"""
2+
Copyright, the CVXPY authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
"""
16+
17+
import numpy as np
18+
import pytest
19+
import scipy.sparse as sp
20+
21+
import cvxpy as cp
22+
from cvxpy.reductions.solvers.defines import INSTALLED_SOLVERS
23+
from cvxpy.tests.nlp_tests.derivative_checker import DerivativeChecker
24+
25+
26+
@pytest.mark.skipif('IPOPT' not in INSTALLED_SOLVERS, reason='IPOPT is not installed.')
27+
class TestPermutedDense:
28+
# Stress tests for the permuted_dense (PD) Jacobian/Hessian path in the diff engine.
29+
# PD originates only at left_matmul when a dense constant multiplies a leaf vector
30+
# variable, so all tests here use vector variables.
31+
32+
def test_multiply_pd_pd(self):
33+
# A dense, B dense
34+
np.random.seed(0)
35+
n, m = 5, 6
36+
A = np.random.rand(m, n)
37+
B = np.random.rand(m, n)
38+
x = cp.Variable(n, bounds=[-1, 1])
39+
y = cp.Variable(n, bounds=[-1, 1])
40+
obj = cp.Minimize(cp.sum(cp.multiply(cp.sin(A @ x), cp.cos(B @ y))))
41+
prob = cp.Problem(obj)
42+
prob.solve(nlp=True)
43+
checker = DerivativeChecker(prob)
44+
checker.run_and_assert()
45+
46+
def test_multiply_pd_sparse(self):
47+
# A dense, B sparse
48+
np.random.seed(0)
49+
n, m = 5, 6
50+
A = np.random.rand(m, n)
51+
B = sp.random(m, n, density=0.5, format='csr')
52+
x = cp.Variable(n, bounds=[-1, 1])
53+
y = cp.Variable(n, bounds=[-1, 1])
54+
obj = cp.Minimize(cp.sum(cp.multiply(cp.sin(A @ x), cp.cos(B @ y))))
55+
prob = cp.Problem(obj)
56+
prob.solve(nlp=True)
57+
checker = DerivativeChecker(prob)
58+
checker.run_and_assert()
59+
60+
def test_multiply_sparse_pd(self):
61+
# A sparse, B dense
62+
np.random.seed(0)
63+
n, m = 5, 6
64+
A = sp.random(m, n, density=0.5, format='csr')
65+
B = np.random.rand(m, n)
66+
x = cp.Variable(n, bounds=[-1, 1])
67+
y = cp.Variable(n, bounds=[-1, 1])
68+
obj = cp.Minimize(cp.sum(cp.multiply(cp.sin(A @ x), cp.cos(B @ y))))
69+
prob = cp.Problem(obj)
70+
prob.solve(nlp=True)
71+
checker = DerivativeChecker(prob)
72+
checker.run_and_assert()
73+
74+
def test_multiply_pd_plain_var(self):
75+
np.random.seed(0)
76+
n, m = 5, 6
77+
A = np.random.rand(m, n)
78+
x = cp.Variable(n, bounds=[-1, 1])
79+
y = cp.Variable(m, bounds=[-1, 1])
80+
obj = cp.Minimize(cp.sum(cp.multiply(cp.sin(A @ x), cp.cos(y))))
81+
prob = cp.Problem(obj)
82+
prob.solve(nlp=True)
83+
checker = DerivativeChecker(prob)
84+
checker.run_and_assert()
85+
86+
def test_multiply_plain_var_pd(self):
87+
np.random.seed(0)
88+
n, m = 5, 6
89+
A = np.random.rand(m, n)
90+
x = cp.Variable(n, bounds=[-1, 1])
91+
y = cp.Variable(m, bounds=[-1, 1])
92+
obj = cp.Minimize(cp.sum(cp.multiply(cp.sin(y), cp.cos(A @ x))))
93+
prob = cp.Problem(obj)
94+
prob.solve(nlp=True)
95+
checker = DerivativeChecker(prob)
96+
checker.run_and_assert()
97+
98+
def test_pd_index_propagation(self):
99+
# Indexing into a permuted dense propagates permuted dense via index_alloc /
100+
# index_fill_values. Use a non-sorted index with duplicates to stress the
101+
# permutation path.
102+
np.random.seed(0)
103+
n, m = 5, 8
104+
A = np.random.rand(m, n)
105+
B = np.random.rand(m, n)
106+
x = cp.Variable(n, bounds=[-1, 1])
107+
y = cp.Variable(n, bounds=[-1, 1])
108+
idx_A = [0, 2, 4, 1, 3, 0, 7]
109+
idx_B = [0, 4, 2, 3, 1, 0, 7]
110+
obj = cp.Minimize(
111+
cp.sum(cp.multiply(cp.sin((A @ x)[idx_A]), cp.cos((B @ y)[idx_B])))
112+
)
113+
prob = cp.Problem(obj)
114+
prob.solve(nlp=True)
115+
checker = DerivativeChecker(prob)
116+
checker.run_and_assert()
117+
118+
def test_pd_transpose_propagation(self):
119+
# Transpose of a PD result. Column-shape variables make .T non-trivial:
120+
# (A @ x) is (m, 1), (A @ x).T is (1, m).
121+
np.random.seed(0)
122+
n, m = 5, 6
123+
A = np.random.rand(m, n)
124+
B = np.random.rand(m, n)
125+
x = cp.Variable((n, 1), bounds=[-1, 1])
126+
y = cp.Variable((n, 1), bounds=[-1, 1])
127+
obj = cp.Minimize(cp.sum(cp.multiply(cp.sin((A @ x).T), cp.cos((B @ y).T))))
128+
prob = cp.Problem(obj)
129+
prob.solve(nlp=True)
130+
checker = DerivativeChecker(prob)
131+
checker.run_and_assert()
132+
133+
def test_pd_broadcast_propagation(self):
134+
# Reshape PD results to column / row vectors and let multiply broadcast.
135+
np.random.seed(0)
136+
n, m = 5, 6
137+
A = np.random.rand(m, n)
138+
B = np.random.rand(m, n)
139+
x = cp.Variable(n, bounds=[-1, 1])
140+
y = cp.Variable(n, bounds=[-1, 1])
141+
obj = cp.Minimize(cp.sum(cp.multiply(
142+
cp.reshape(cp.sin(A @ x), (m, 1), order='F'),
143+
cp.reshape(cp.cos(B @ y), (1, m), order='F'),
144+
)))
145+
prob = cp.Problem(obj)
146+
prob.solve(nlp=True)
147+
checker = DerivativeChecker(prob)
148+
checker.run_and_assert()
149+
150+
def test_deep_composition(self):
151+
# A deep composition of PD results
152+
np.random.seed(0)
153+
n, m = 5, 10
154+
A = np.random.rand(m, n)
155+
B = sp.random(n, m, density=0.5, format='csr')
156+
C = np.random.rand(m, n)
157+
x = cp.Variable(n, bounds=[-1, 1])
158+
y = cp.Variable(n, bounds=[-1, 1])
159+
obj = cp.Minimize(cp.sum(cp.multiply(
160+
cp.sin(A @ cp.cos(B @ cp.logistic(C @ x))),
161+
cp.cos(A @ cp.cos(B @ cp.logistic(C @ y))),
162+
)))
163+
prob = cp.Problem(obj)
164+
prob.solve(nlp=True)
165+
checker = DerivativeChecker(prob)
166+
checker.run_and_assert()
167+
168+
def test_multiply_pd_pd_right(self):
169+
# Right matmul with dense A and dense B
170+
np.random.seed(0)
171+
n, m = 5, 6
172+
A = np.random.rand(n, m)
173+
B = np.random.rand(n, m)
174+
x = cp.Variable(n, bounds=[-1, 1])
175+
y = cp.Variable(n, bounds=[-1, 1])
176+
obj = cp.Minimize(cp.sum(cp.multiply(cp.sin(x @ A), cp.cos(y @ B))))
177+
prob = cp.Problem(obj)
178+
prob.solve(nlp=True)
179+
checker = DerivativeChecker(prob)
180+
checker.run_and_assert()
181+
182+
def test_multiply_pd_sparse_right(self):
183+
# Right matmul with dense A and sparse B
184+
np.random.seed(0)
185+
n, m = 5, 6
186+
A = np.random.rand(n, m)
187+
B = sp.random(n, m, density=0.5, format='csr')
188+
x = cp.Variable(n, bounds=[-1, 1])
189+
y = cp.Variable(n, bounds=[-1, 1])
190+
obj = cp.Minimize(cp.sum(cp.multiply(cp.sin(x @ A), cp.cos(y @ B))))
191+
prob = cp.Problem(obj)
192+
prob.solve(nlp=True)
193+
checker = DerivativeChecker(prob)
194+
checker.run_and_assert()
195+
196+
def test_pd_index_propagation_right(self):
197+
# Right matmul with index
198+
np.random.seed(0)
199+
n, m = 5, 8
200+
A = np.random.rand(n, m)
201+
B = np.random.rand(n, m)
202+
x = cp.Variable(n, bounds=[-1, 1])
203+
y = cp.Variable(n, bounds=[-1, 1])
204+
idx_A = [0, 2, 4, 1, 3, 0, 7]
205+
idx_B = [0, 4, 2, 3, 1, 0, 7]
206+
obj = cp.Minimize(
207+
cp.sum(cp.multiply(cp.sin((x @ A)[idx_A]), cp.cos((y @ B)[idx_B])))
208+
)
209+
prob = cp.Problem(obj)
210+
prob.solve(nlp=True)
211+
checker = DerivativeChecker(prob)
212+
checker.run_and_assert()
213+
214+
def test_pd_transpose_propagation_right(self):
215+
# Right matmul with transpose
216+
np.random.seed(0)
217+
n, m = 5, 6
218+
A = np.random.rand(n, m)
219+
B = np.random.rand(n, m)
220+
x = cp.Variable((1, n), bounds=[-1, 1])
221+
y = cp.Variable((1, n), bounds=[-1, 1])
222+
obj = cp.Minimize(cp.sum(cp.multiply(cp.sin((x @ A).T), cp.cos((y @ B).T))))
223+
prob = cp.Problem(obj)
224+
prob.solve(nlp=True)
225+
checker = DerivativeChecker(prob)
226+
checker.run_and_assert()
227+
228+
def test_pd_broadcast_propagation_right(self):
229+
# Reshape right-rooted PD results and force (m, 1) * (1, m) broadcast.
230+
np.random.seed(0)
231+
n, m = 5, 6
232+
A = np.random.rand(n, m)
233+
B = np.random.rand(n, m)
234+
x = cp.Variable(n, bounds=[-1, 1])
235+
y = cp.Variable(n, bounds=[-1, 1])
236+
obj = cp.Minimize(cp.sum(cp.multiply(
237+
cp.reshape(cp.sin(x @ A), (m, 1), order='F'),
238+
cp.reshape(cp.cos(y @ B), (1, m), order='F'),
239+
)))
240+
prob = cp.Problem(obj)
241+
prob.solve(nlp=True)
242+
checker = DerivativeChecker(prob)
243+
checker.run_and_assert()

cvxpy/tests/nlp_tests/test_best_of.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,21 @@ def test_path_planning_best_of_five(self):
114114
all_objs = prob.solver_stats.extra_stats['all_objs_from_best_of']
115115
assert len(all_objs) == 3
116116

117-
# TODO add a test that best_of actually caches the sparsity pattern between solves
117+
def test_best_of_infeasible_problem(self):
118+
# test that if the problem is infeasible, then best_of returns inf as the objective value
119+
x = cp.Variable(bounds=[-5, 5])
120+
y = cp.Variable(bounds=[-3, 3])
121+
constraints = [x + y == 10]
122+
obj = cp.Minimize((x - 1) ** 2 + (y - 2) ** 2)
123+
prob = cp.Problem(obj, constraints)
124+
prob.solve(nlp=True, best_of=20, verbose=True)
125+
assert prob.value == float("inf")
126+
127+
def test_best_of_with_unbounded(self):
128+
# test that if the problem is unbounded, then best_of returns -inf as the objective value
129+
x = cp.Variable()
130+
x.sample_bounds = [-5, 5]
131+
obj = cp.Minimize(x)
132+
prob = cp.Problem(obj)
133+
prob.solve(nlp=True, best_of=20, verbose=True)
134+
assert prob.value == float("-inf")

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ dependencies = [
8585
"scipy >= 1.13.0",
8686
"highspy >= 1.11.0",
8787
"qdldl >= 0.1.7.post0",
88-
"sparsediffpy >= 0.2.2, < 0.3.0",
88+
"sparsediffpy >= 0.3.0, < 0.4.0",
8989
]
9090
requires-python = ">=3.11"
9191
urls = {Homepage = "https://github.com/cvxpy/cvxpy"}

0 commit comments

Comments
 (0)