Skip to content
Merged
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
7 changes: 7 additions & 0 deletions cvxpy/cvxcore/python/canonInterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,13 @@ def get_problem_matrix(linOps,
default_canon_backend = get_default_canon_backend()
canon_backend = default_canon_backend if not canon_backend else canon_backend

# DIFFENGINE is a reduction-layer replacement for ConeMatrixStuffing and
# has no lin_ops backend of its own. When code paths reach this matrix
# builder directly (e.g. tests that bypass the stuffing reduction), fall
# through to the CPP backend so the lin_ops pipeline still works.
if canon_backend == s.DIFFENGINE_BACKEND:
canon_backend = s.CPP_CANON_BACKEND

if canon_backend == s.CPP_CANON_BACKEND:
from cvxpy.cvxcore.python.cppbackend import build_matrix
return build_matrix(id_to_col, param_to_size, param_to_col, var_length, constr_length, linOps)
Expand Down
20 changes: 16 additions & 4 deletions cvxpy/reductions/dcp2cone/canonicalizers/perspective_canon.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,22 @@ def perspective_canon(expr, args, solver_context: SolverInfo | None = None):
prob_canon = chain.apply(aux_prob)[0] # grab problem instance
# get cone representation of c, A, and b for some problem.

q = prob_canon.q.toarray().flatten()[:-1]
d = prob_canon.q.toarray().flatten()[-1]
Ab = prob_canon.A.toarray().reshape((-1, len(q) + 1), order="F")
A, b = Ab[:, :-1], Ab[:, -1]
# Extract cone representation q, d, A, b.
# ParamConeProg stores q as sparse (n+1, n_params+1) tensor with d
# embedded in the last row, and A as a flattened tensor with b embedded.
# DiffengineConeProgram stores q, d, A, b as separate concrete arrays.
if hasattr(prob_canon, 'd') and not hasattr(prob_canon.q, 'toarray'):
# DiffengineConeProgram: concrete matrices, d stored separately.
q = prob_canon.q
d = prob_canon.d
A = prob_canon.A.toarray() if hasattr(prob_canon.A, 'toarray') else prob_canon.A
b = prob_canon.b
else:
# ParamConeProg: sparse tensor with embedded offsets.
q = prob_canon.q.toarray().flatten()[:-1]
d = prob_canon.q.toarray().flatten()[-1]
Ab = prob_canon.A.toarray().reshape((-1, len(q) + 1), order="F")
A, b = Ab[:, :-1], Ab[:, -1]

# given f in epigraph form, aka epi f = \{(x,t) | f(x) \leq t\}
# = \{(x,t) | Fx +tg + e \in K} for K a cone, the epigraph of the
Expand Down
81 changes: 68 additions & 13 deletions cvxpy/reductions/solvers/nlp_solvers/diff_engine/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,32 +30,65 @@
from cvxpy.reductions.solvers.nlp_solvers.diff_engine.registry import ATOM_CONVERTERS


def _matmul_normalize_1d(A, side):
"""Reshape a 1D numpy array to 2D for matmul.

NumPy matmul treats 1D arrays differently depending on which side:
Left 1D: (k,) → (1, k) — row vector
Right 1D: (k,) → (k, 1) — column vector
2D input is returned unchanged.
"""
if A.ndim == 1:
return A.reshape(1, -1) if side == 'left' else A.reshape(-1, 1)
return A


def convert_matmul(expr, children, var_dict, n_vars, param_dict):
"""Convert matrix multiplication A @ f(x), f(x) @ A, or X @ Y."""
"""Convert matrix multiplication A @ f(x), f(x) @ A, or X @ Y.

NumPy matmul semantics for 1D arrays:
(n,) @ (m,k) → treat left as (1,n) — normalize_shape already does this
(m,k) @ (n,) → treat right as (n,1) — must reshape from (1,n) storage
(n,) @ (n,) → dot product: (1,n) @ (n,1) → scalar

The C engine only has 2D nodes. 1D expressions are stored as (1,n) by
normalize_shape. All 1D→2D matmul normalization is handled here so that
helper functions always receive properly shaped 2D data.
"""
left_arg, right_arg = expr.args
left_child, right_child = children

# Right 1D child: C stores as (1, n) but matmul needs (n, 1).
# Do this once, before branching — used by all three branches.
if len(right_arg.shape) <= 1 and right_arg.size > 1:
right_child = _diffengine.make_reshape(right_child, right_arg.size, 1)

if left_arg.is_constant():
A = left_arg.value
A = _matmul_normalize_1d(left_arg.value, 'left')
if isinstance(left_arg, cp.Parameter):
param_node = param_dict[left_arg.id]
elif left_arg.parameters():
param_node = left_child
else:
param_node = None
if sparse.issparse(A):
return make_sparse_left_matmul(param_node, children[1], A)
return make_dense_left_matmul(param_node, children[1], A)
return make_sparse_left_matmul(param_node, right_child, A)
return make_dense_left_matmul(param_node, right_child, A)

elif right_arg.is_constant():
A = right_arg.value
A = _matmul_normalize_1d(right_arg.value, 'right')
if isinstance(right_arg, cp.Parameter):
param_node = param_dict[right_arg.id]
elif right_arg.parameters():
param_node = right_child
else:
param_node = None
if sparse.issparse(A):
return make_sparse_right_matmul(param_node, children[0], A)
return make_dense_right_matmul(param_node, children[0], A)
return make_sparse_right_matmul(param_node, left_child, A)
return make_dense_right_matmul(param_node, left_child, A)

else:
return _diffengine.make_matmul(children[0], children[1])
return _diffengine.make_matmul(left_child, right_child)

# TODO we should support sparse elementwise multiply at some point.
def convert_multiply(expr, children, var_dict, n_vars, param_dict):
Expand Down Expand Up @@ -98,11 +131,22 @@ def convert_expr(expr, var_dict, n_vars, param_dict=None):
return param_dict[expr.id]

# Base case: constant (in the diff engine, a constant is a parameter with ID -1)
# Also handles atoms applied to pure constants (e.g. PnormApprox(Constant), floor(Constant))
# that weren't folded by Dcp2Cone.
if isinstance(expr, cp.Constant):
c = to_dense_float(expr.value)
d1, d2 = normalize_shape(expr.shape)
return _diffengine.make_parameter(d1, d2, -1, n_vars, c.flatten(order='F'))

# Constant atom: no variables/parameters, so it must have a concrete value.
# Handles atoms like floor(NegExpression(Constant(5.0))) after EvalParams.
# Check variables()/parameters() before .value to avoid triggering
# numeric() on atoms that don't support it (e.g. cached SymbolicQuadForm).
if not expr.variables() and not expr.parameters() and expr.value is not None:
c = to_dense_float(expr.value)
d1, d2 = normalize_shape(expr.shape)
return _diffengine.make_parameter(d1, d2, -1, n_vars, c.flatten(order='F'))

# Recursive case: atoms
atom_name = type(expr).__name__
children = [convert_expr(arg, var_dict, n_vars, param_dict) for arg in expr.args]
Expand All @@ -113,6 +157,11 @@ def convert_expr(expr, var_dict, n_vars, param_dict=None):
C_expr = convert_matmul(expr, children, var_dict, n_vars, param_dict)
elif atom_name == "multiply":
C_expr = convert_multiply(expr, children, var_dict, n_vars, param_dict)
elif atom_name in ("QuadForm", "SymbolicQuadForm"):
from cvxpy.reductions.solvers.nlp_solvers.diff_engine.registry import (
convert_quad_form,
)
C_expr = convert_quad_form(expr, children, n_vars)
elif atom_name in ATOM_CONVERTERS:
C_expr = ATOM_CONVERTERS[atom_name](expr, children)
else:
Expand All @@ -123,10 +172,16 @@ def convert_expr(expr, var_dict, n_vars, param_dict=None):
d1_Python, d2_Python = normalize_shape(expr.shape)

if d1_C != d1_Python or d2_C != d2_Python:
raise ValueError(
f"Dimension mismatch for atom '{atom_name}': "
f"C dimensions ({d1_C}, {d2_C}) vs Python dimensions ({d1_Python}, {d2_Python})"
)

# 1D Python shapes (n,) normalize to (1, n), but the C engine may
# produce (n, 1) — e.g. matrix @ scalar or transpose of a vector.
# Both represent the same 1D data; reshape to match Python convention.
if len(expr.shape) <= 1 and d1_C * d2_C == d1_Python * d2_Python:
C_expr = _diffengine.make_reshape(C_expr, d1_Python, d2_Python)
else:
raise ValueError(
f"Dimension mismatch for atom '{atom_name}': "
f"C dimensions ({d1_C}, {d2_C}) vs "
f"Python dimensions ({d1_Python}, {d2_Python})"
)

return C_expr
4 changes: 2 additions & 2 deletions cvxpy/reductions/solvers/nlp_solvers/diff_engine/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def make_sparse_left_matmul(param_node, child, A):


def make_dense_left_matmul(param_node, child, A):
m, n = normalize_shape(A.shape)
m, n = A.shape
return _diffengine.make_left_matmul(
param_node, child, 'dense', A.flatten(order='C'), m, n)

Expand All @@ -70,7 +70,7 @@ def make_sparse_right_matmul(param_node, child, A):


def make_dense_right_matmul(param_node, child, A):
m, n = normalize_shape(A.shape)
m, n = A.shape
return _diffengine.make_right_matmul(
param_node, child, 'dense', A.flatten(order='C'), m, n)

Expand Down
Loading
Loading