Skip to content

Commit bc91ef8

Browse files
committed
add changes to registry
1 parent 14ec3af commit bc91ef8

1 file changed

Lines changed: 149 additions & 13 deletions

File tree

  • cvxpy/reductions/solvers/nlp_solvers/diff_engine

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

Lines changed: 149 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,49 @@ def convert_vstack(expr, children):
4040
return _diffengine.make_vstack(children)
4141

4242

43+
def convert_conv(expr, children):
44+
"""Convert cp.conv / cp.convolve to _diffengine.make_convolve.
45+
46+
Both CVXPY atoms take args = [constant_kernel, signal] with the kernel
47+
validated as constant. The C node computes full 1D convolution
48+
(length m + n - 1) given a length-m kernel parameter capsule and a
49+
length-n child. convert_expr has already built both children as
50+
appropriate capsules (PARAM_FIXED for Constants, param_dict lookup
51+
for Parameters, recursive build for affine-of-parameter).
52+
"""
53+
return _diffengine.make_convolve(children[0], children[1])
54+
55+
56+
def convert_concatenate(expr, children):
57+
"""Convert Concatenate along an existing axis via hstack/vstack.
58+
59+
Concatenate semantics:
60+
ndim=1, axis=0 → flat concat, same as hstack on 1D
61+
ndim=2, axis=0 → stack rows, same as vstack
62+
ndim=2, axis=1 → stack cols, same as hstack
63+
"""
64+
axis = expr.axis
65+
arg_shapes = [arg.shape for arg in expr.args]
66+
ndims = {len(s) for s in arg_shapes}
67+
if len(ndims) != 1:
68+
raise NotImplementedError(
69+
"Concatenate across inputs of mixed dimensionality is not "
70+
"supported by the diffengine backend."
71+
)
72+
ndim = ndims.pop()
73+
74+
if ndim == 1 and axis == 0:
75+
return _diffengine.make_hstack(children)
76+
if ndim == 2 and axis == 0:
77+
return _diffengine.make_vstack(children)
78+
if ndim == 2 and axis == 1:
79+
return _diffengine.make_hstack(children)
80+
raise NotImplementedError(
81+
f"Concatenate with ndim={ndim}, axis={axis} is not supported "
82+
"by the diffengine backend."
83+
)
84+
85+
4386
def convert_div(expr, children):
4487
"""Convert x / c by multiplying x by the elementwise reciprocal of c."""
4588
from cvxpy.reductions.solvers.nlp_solvers.diff_engine.helpers import to_dense_float
@@ -92,33 +135,122 @@ def convert_rel_entr(expr, children):
92135
return _diffengine.make_rel_entr(children[0], children[1])
93136

94137

138+
def _diagonal_weights(P_val):
139+
"""If P_val is (numerically) diagonal, return its diagonal as a 1D
140+
float64 array; otherwise return None."""
141+
if sparse.issparse(P_val):
142+
P_val = P_val.toarray()
143+
P = np.asarray(P_val, dtype=np.float64)
144+
if P.ndim != 2 or P.shape[0] != P.shape[1]:
145+
return None
146+
w = np.diag(P)
147+
if np.allclose(P, np.diag(w)):
148+
return w
149+
return None
150+
151+
95152
def convert_quad_form(expr, children, n_vars):
96153
"""Convert quadratic form x.T @ P @ x.
97154
98-
Currently only handles scalar quad forms (shape ()). Vector-shaped
99-
SymbolicQuadForm (e.g. from huber, quad_over_lin with axis) requires
100-
native block quadform support in SparseDiffPy.
155+
Scalar output (shape ()) uses the native `make_quad_form` C node.
156+
157+
Vector-shaped SymbolicQuadForm is lowered using existing atoms when P
158+
is diagonal (every CVXPY canonicalizer that produces a vector-shaped
159+
SymbolicQuadForm today builds it with P = eye or P = α·eye):
160+
161+
- `block_indices is None`, P = diag(w):
162+
y = w ⊙ x**2
163+
- `block_indices` given with uniform block size m, P = α·I:
164+
y_j = α · Σ_{i ∈ I_j} x_i**2
165+
(gather → square → axis-0 sum → scalar-mult by α)
166+
167+
A genuinely non-diagonal vector quad_form still raises
168+
NotImplementedError — that case needs a native block quad_form node
169+
in SparseDiffPy.
101170
"""
102171
P = expr.args[1]
103172

104173
if not P.is_constant():
105174
raise NotImplementedError("quad_form requires P to be a constant matrix")
106175

107-
# TODO: vector-shaped SymbolicQuadForm (e.g. from huber, quad_over_lin
108-
# with axis) needs native block quadform support in SparseDiffPy rather
109-
# than decomposing into diag(P) * power(x, 2).
110-
if expr.size > 1:
176+
P_val = P.value
177+
if P_val is None:
111178
raise NotImplementedError(
112-
f"Vector-shaped quad form (shape {expr.shape}) not yet supported "
113-
"by the diffengine backend. Needs native block quadform in SparseDiffPy."
179+
"quad_form with a symbolic P (e.g. eye/parameter) is not yet "
180+
"supported by the diffengine backend."
114181
)
115182

116-
P_val = P.value
183+
if expr.size > 1:
184+
w = _diagonal_weights(P_val)
185+
if w is None:
186+
raise NotImplementedError(
187+
f"Non-diagonal vector-shaped quad form (shape {expr.shape}) "
188+
"is not supported by the diffengine backend. Needs native "
189+
"block quadform in SparseDiffPy."
190+
)
191+
192+
block_indices = getattr(expr, "block_indices", None)
193+
child = children[0]
194+
uniform_alpha = float(w[0]) if np.allclose(w, w[0]) else None
195+
196+
if block_indices is None:
197+
# Case A: y_j = w_j * x_j**2 (elementwise, K == N).
198+
x_sq = _diffengine.make_power(child, 2.0)
199+
if uniform_alpha is not None and np.isclose(uniform_alpha, 1.0):
200+
out = x_sq
201+
elif uniform_alpha is not None:
202+
alpha_node = _diffengine.make_parameter(
203+
1, 1, -1, n_vars,
204+
np.array([uniform_alpha], dtype=np.float64))
205+
out = _diffengine.make_param_scalar_mult(alpha_node, x_sq)
206+
else:
207+
d1, d2 = normalize_shape(expr.shape)
208+
w_node = _diffengine.make_parameter(
209+
d1, d2, -1, n_vars,
210+
np.asarray(w, dtype=np.float64).flatten(order="F"))
211+
out = _diffengine.make_param_vector_mult(w_node, x_sq)
212+
else:
213+
# Case B: y_j = α · Σ_{i ∈ I_j} x_i**2 (requires uniform blocks).
214+
if uniform_alpha is None:
215+
raise NotImplementedError(
216+
"quad_form with block_indices and non-uniform P diagonal "
217+
"is not supported by the diffengine backend."
218+
)
219+
block_lens = {len(b) for b in block_indices}
220+
if len(block_lens) != 1:
221+
raise NotImplementedError(
222+
"quad_form with non-uniform block_indices sizes is not "
223+
"supported by the diffengine backend."
224+
)
225+
m = block_lens.pop()
226+
K = len(block_indices)
227+
flat_idx = np.concatenate(
228+
[np.asarray(b, dtype=np.int32) for b in block_indices]
229+
).astype(np.int32)
230+
gathered = _diffengine.make_index(child, m, K, flat_idx)
231+
sq = _diffengine.make_power(gathered, 2.0)
232+
summed = _diffengine.make_sum(sq, 0)
233+
if np.isclose(uniform_alpha, 1.0):
234+
out = summed
235+
else:
236+
alpha_node = _diffengine.make_parameter(
237+
1, 1, -1, n_vars,
238+
np.array([uniform_alpha], dtype=np.float64))
239+
out = _diffengine.make_param_scalar_mult(alpha_node, summed)
240+
241+
# Match the declared output shape (convert_expr's final dim check
242+
# compares C dims to normalize_shape(expr.shape)).
243+
d1_out, d2_out = normalize_shape(expr.shape)
244+
d1_c, d2_c = _diffengine.get_expr_dimensions(out)
245+
if (d1_c, d2_c) != (d1_out, d2_out):
246+
out = _diffengine.make_reshape(out, d1_out, d2_out)
247+
return out
248+
249+
# Scalar output: native quad_form node.
117250
if sparse.issparse(P_val):
118251
P_val = P_val.toarray()
119-
P_val = np.asarray(P_val, dtype=np.float64)
120-
121-
P_csr = sparse.csr_matrix(P_val)
252+
P_arr = np.asarray(P_val, dtype=np.float64)
253+
P_csr = sparse.csr_matrix(P_arr)
122254
return _diffengine.make_quad_form(
123255
children[0],
124256
P_csr.data.astype(np.float64),
@@ -280,6 +412,10 @@ def convert_upper_tri(_expr, children):
280412
# Horizontal/vertical stack
281413
"Hstack": convert_hstack,
282414
"Vstack": convert_vstack,
415+
"Concatenate": convert_concatenate,
416+
# 1D full convolution
417+
"conv": convert_conv,
418+
"convolve": convert_conv,
283419
"Trace": convert_trace,
284420
# Diagonal and triangular
285421
"diag_vec": convert_diag_vec,

0 commit comments

Comments
 (0)