@@ -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+
4386def 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+
95152def 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