Skip to content

Commit 49ea9eb

Browse files
committed
address Parth's reviews
1 parent 775133a commit 49ea9eb

5 files changed

Lines changed: 44 additions & 21 deletions

File tree

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,18 @@
3131

3232

3333
def convert_matmul(expr, children, var_dict, n_vars, param_dict):
34-
"""Convert matrix multiplication A @ f(x), f(x) @ A, or X @ Y."""
34+
"""Convert matrix multiplication A @ f(x), f(x) @ A, or X @ Y.
35+
36+
numpy convention: a 1D left operand acts as a row vector, a 1D right
37+
operand acts as a column vector. The C engine normalizes (n,) to (1, n),
38+
so 1D right operands need an explicit reshape to (n, 1).
39+
"""
3540
left_arg, right_arg = expr.args
3641

3742
if left_arg.is_constant():
3843
A = left_arg.value
44+
if A.ndim == 1:
45+
A = A.reshape(1, -1)
3946
param_node = children[0] if left_arg.parameters() else None
4047
if sparse.issparse(A):
4148
return make_sparse_left_matmul(param_node, children[1], A)
@@ -51,17 +58,16 @@ def convert_matmul(expr, children, var_dict, n_vars, param_dict):
5158
return make_dense_right_matmul(param_node, children[0], A)
5259

5360
else:
54-
return _diffengine.make_matmul(children[0], children[1])
61+
right_node = children[1]
62+
if len(right_arg.shape) == 1:
63+
right_node = _diffengine.make_reshape(right_node, right_arg.shape[0], 1)
64+
return _diffengine.make_matmul(children[0], right_node)
5565

5666
# TODO we should support sparse elementwise multiply at some point.
5767
def convert_multiply(expr, children, var_dict, n_vars, param_dict):
5868
"""Convert elementwise multiplication."""
5969
left_arg, right_arg = expr.args
6070

61-
# TODO: would be nice to catch promote here so we correctly create a
62-
# a scalar multiply. What is even the convention with promoting a parameter?
63-
# This is a very deep question.
64-
6571
if left_arg.is_constant():
6672
if left_arg.size == 1:
6773
return _diffengine.make_param_scalar_mult(children[0], children[1])

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,20 @@ def normalize_shape(shape):
2929
def to_dense_float(value):
3030
"""Convert a value to a dense float64 numpy array."""
3131
if sparse.issparse(value):
32-
value = value.todense()
32+
value = value.toarray()
3333
return np.asarray(value, dtype=np.float64)
3434

3535

3636
def chain_add(children):
37-
"""Chain multiple children with binary adds: a + b + c -> add(add(a, b), c)."""
38-
result = children[0]
39-
for child in children[1:]:
40-
result = _diffengine.make_add(result, child)
41-
return result
37+
"""Combine children with a balanced binary tree of adds.
38+
39+
Tree depth is ceil(log2(N)) instead of N-1, which keeps the AD graph shallow
40+
when summing many terms.
41+
"""
42+
if len(children) == 1:
43+
return children[0]
44+
mid = len(children) // 2
45+
return _diffengine.make_add(chain_add(children[:mid]), chain_add(children[mid:]))
4246

4347

4448
def make_sparse_left_matmul(param_node, child, A):
@@ -53,8 +57,6 @@ def make_sparse_left_matmul(param_node, child, A):
5357

5458

5559
def make_dense_left_matmul(param_node, child, A):
56-
if A.ndim == 1:
57-
A = A.reshape(1, -1)
5860
m, n = A.shape
5961
return _diffengine.make_left_matmul(
6062
param_node, child, 'dense', A.flatten(order='C'), m, n)

cvxpy/reductions/solvers/nlp_solvers/uno_nlpif.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,7 @@ def constraints_callback(x, constraint_values):
197197
# Define Jacobian callback
198198
# Signature: jacobian(x, jacobian_values) -> void
199199
def jacobian_callback(x, jacobian_values):
200-
jac_vals = oracles.jacobian(x)
201-
# Flatten in case it's returned as a 2D memoryview
202-
jacobian_values[:] = np.asarray(jac_vals).flatten()
200+
jacobian_values[:] = oracles.jacobian(x)
203201

204202
# set_constraints(number_constraints, constraint_functions,
205203
# constraints_lower_bounds, constraints_upper_bounds,
@@ -224,9 +222,7 @@ def jacobian_callback(x, jacobian_values):
224222
# Define Lagrangian Hessian callback
225223
# Signature: hessian(x, objective_multiplier, multipliers, hessian_values)
226224
def hessian_callback(x, objective_multiplier, multipliers, hessian_values):
227-
hess_vals = oracles.hessian(x, multipliers, objective_multiplier)
228-
# Flatten in case it's returned as a 2D array
229-
hessian_values[:] = np.asarray(hess_vals).flatten()
225+
hessian_values[:] = oracles.hessian(x, multipliers, objective_multiplier)
230226

231227
# set_lagrangian_hessian(number_hessian_nonzeros, hessian_triangular_part,
232228
# hessian_row_indices, hessian_column_indices, lagrangian_hessian)

cvxpy/tests/nlp_tests/test_affine_atoms.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,22 @@ def test_convolve_alias(self):
147147

148148
checker = DerivativeChecker(problem)
149149
checker.run_and_assert()
150+
151+
def test_matmul_1d_dot_product(self):
152+
"""x.T @ x for a 1D variable. Pins both the transpose 1D no-op and
153+
the var-var matmul handling of a 1D right operand (numpy column-vector
154+
convention)."""
155+
np.random.seed(0)
156+
n = 5
157+
x = cp.Variable(n, bounds=[-1, 1], name='x')
158+
x.value = np.random.rand(n)
159+
assert (cp.sin(x).T @ cp.sin(x)).shape == ()
160+
161+
obj = cp.sin(x).T @ cp.sin(x)
162+
problem = cp.Problem(cp.Minimize(obj))
163+
164+
problem.solve(solver=cp.IPOPT, nlp=True, verbose=False)
165+
assert(problem.status == cp.OPTIMAL)
166+
167+
checker = DerivativeChecker(problem)
168+
checker.run_and_assert()

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",
88+
"sparsediffpy >= 0.2.2, < 0.3.0",
8989
]
9090
requires-python = ">=3.11"
9191
urls = {Homepage = "https://github.com/cvxpy/cvxpy"}

0 commit comments

Comments
 (0)