|
13 | 13 | See the License for the specific language governing permissions and |
14 | 14 | limitations under the License. |
15 | 15 |
|
16 | | -Converters from CVXPY expressions to C diff engine expressions. |
17 | | -
|
18 | | -This module provides the mapping between CVXPY atom types and their |
19 | | -corresponding SparseDiffPy constructors. |
| 16 | +Main entry point for converting CVXPY expressions to C diff engine expressions. |
20 | 17 | """ |
21 | | -import numpy as np |
22 | 18 | from scipy import sparse |
23 | 19 | from sparsediffpy import _sparsediffengine as _diffengine |
24 | 20 |
|
25 | 21 | import cvxpy as cp |
26 | 22 | from cvxpy.expressions.constants.parameter import Parameter |
27 | 23 |
|
28 | | -# --------------------------------------------------------------------------- |
29 | | -# Utilities |
30 | | -# --------------------------------------------------------------------------- |
31 | | - |
32 | | -def normalize_shape(shape): |
33 | | - """Normalize shape to 2D (d1, d2) for the C engine.""" |
34 | | - shape = tuple(shape) |
35 | | - return (1,) * (2 - len(shape)) + shape |
36 | | - |
37 | | - |
38 | | -def _to_dense_float(value): |
39 | | - """Convert a value to a dense float64 numpy array.""" |
40 | | - if sparse.issparse(value): |
41 | | - value = value.todense() |
42 | | - return np.asarray(value, dtype=np.float64) |
43 | | - |
44 | | - |
45 | | -def _chain_add(children): |
46 | | - """Chain multiple children with binary adds: a + b + c -> add(add(a, b), c).""" |
47 | | - result = children[0] |
48 | | - for child in children[1:]: |
49 | | - result = _diffengine.make_add(result, child) |
50 | | - return result |
51 | | - |
52 | | - |
53 | | -# --------------------------------------------------------------------------- |
54 | | -# Matmul helpers (handle param_node insertion for backward compat) |
55 | | -# --------------------------------------------------------------------------- |
56 | | - |
57 | | -def _make_sparse_left_matmul(param_node, child, A): |
58 | | - if not isinstance(A, sparse.csr_matrix): |
59 | | - A = sparse.csr_matrix(A) |
60 | | - return _diffengine.make_sparse_left_matmul( |
61 | | - param_node, child, |
62 | | - A.data.astype(np.float64, copy=False), |
63 | | - A.indices.astype(np.int32, copy=False), |
64 | | - A.indptr.astype(np.int32, copy=False), |
65 | | - A.shape[0], A.shape[1]) |
66 | | - |
67 | | - |
68 | | -def _make_dense_left_matmul(param_node, child, A): |
69 | | - m, n = normalize_shape(A.shape) |
70 | | - return _diffengine.make_dense_left_matmul( |
71 | | - param_node, child, A.flatten(order='C'), m, n) |
72 | | - |
73 | | - |
74 | | -def _make_sparse_right_matmul(param_node, child, A): |
75 | | - if not isinstance(A, sparse.csr_matrix): |
76 | | - A = sparse.csr_matrix(A) |
77 | | - return _diffengine.make_sparse_right_matmul( |
78 | | - param_node, child, |
79 | | - A.data.astype(np.float64, copy=False), |
80 | | - A.indices.astype(np.int32, copy=False), |
81 | | - A.indptr.astype(np.int32, copy=False), |
82 | | - A.shape[0], A.shape[1]) |
83 | | - |
84 | | - |
85 | | -def _make_dense_right_matmul(param_node, child, A): |
86 | | - m, n = normalize_shape(A.shape) |
87 | | - return _diffengine.make_dense_right_matmul( |
88 | | - param_node, child, A.flatten(order='C'), m, n) |
89 | | - |
90 | | - |
91 | | -# --------------------------------------------------------------------------- |
92 | | -# Variable / parameter dict builders |
93 | | -# --------------------------------------------------------------------------- |
94 | | - |
95 | | -def build_var_dict(inverse_data): |
96 | | - """Build {var_id: C variable capsule} mapping from InverseData.""" |
97 | | - n_vars = inverse_data.x_length |
98 | | - var_dict = {} |
99 | | - for var_id, (offset, _) in inverse_data.id_map.items(): |
100 | | - d1, d2 = normalize_shape(inverse_data.var_shapes[var_id]) |
101 | | - var_dict[var_id] = _diffengine.make_variable(d1, d2, offset, n_vars) |
102 | | - return var_dict, n_vars |
103 | | - |
104 | | - |
105 | | -def build_param_dict(inverse_data): |
106 | | - """Build {param_id: C parameter capsule} mapping from InverseData.""" |
107 | | - n_vars = inverse_data.x_length |
108 | | - param_dict = {} |
109 | | - for param_id, offset in inverse_data.param_id_map.items(): |
110 | | - if param_id not in inverse_data.param_shapes: |
111 | | - continue |
112 | | - d1, d2 = normalize_shape(inverse_data.param_shapes[param_id]) |
113 | | - param_dict[param_id] = _diffengine.make_parameter(d1, d2, offset, n_vars) |
114 | | - return param_dict |
| 24 | +from cvxpy.reductions.solvers.nlp_solvers.diff_engine.helpers import ( |
| 25 | + _make_dense_left_matmul, |
| 26 | + _make_dense_right_matmul, |
| 27 | + _make_sparse_left_matmul, |
| 28 | + _make_sparse_right_matmul, |
| 29 | + _to_dense_float, |
| 30 | + build_param_dict, |
| 31 | + build_var_dict, |
| 32 | + normalize_shape, |
| 33 | +) |
| 34 | +from cvxpy.reductions.solvers.nlp_solvers.diff_engine.registry import ATOM_CONVERTERS |
115 | 35 |
|
116 | 36 |
|
117 | 37 | # --------------------------------------------------------------------------- |
@@ -186,218 +106,6 @@ def _convert_multiply(expr, children, var_dict, n_vars, param_dict): |
186 | 106 | return _diffengine.make_multiply(children[0], children[1]) |
187 | 107 |
|
188 | 108 |
|
189 | | -# --------------------------------------------------------------------------- |
190 | | -# Atom converters: (expr, children) -> C expression |
191 | | -# These are kept exactly as in the original code. |
192 | | -# --------------------------------------------------------------------------- |
193 | | - |
194 | | -def _convert_hstack(expr, children): |
195 | | - """Convert horizontal stack (hstack) of expressions.""" |
196 | | - return _diffengine.make_hstack(children) |
197 | | - |
198 | | - |
199 | | -def _extract_flat_indices_from_index(expr): |
200 | | - """Extract flattened indices from CVXPY index expression.""" |
201 | | - parent_shape = expr.args[0].shape |
202 | | - indices_per_dim = [np.arange(s.start, s.stop, s.step) for s in expr.key] |
203 | | - |
204 | | - if len(indices_per_dim) == 1: |
205 | | - return indices_per_dim[0].astype(np.int32) |
206 | | - elif len(indices_per_dim) == 2: |
207 | | - # Fortran order: idx = row + col * n_rows |
208 | | - return ( |
209 | | - np.add.outer(indices_per_dim[0], indices_per_dim[1] * parent_shape[0]) |
210 | | - .flatten(order="F") |
211 | | - .astype(np.int32) |
212 | | - ) |
213 | | - else: |
214 | | - raise NotImplementedError("index with >2 dimensions not supported") |
215 | | - |
216 | | - |
217 | | -def _extract_flat_indices_from_special_index(expr): |
218 | | - """Extract flattened indices from CVXPY special_index expression.""" |
219 | | - return np.reshape(expr._select_mat, expr._select_mat.size, order="F").astype(np.int32) |
220 | | - |
221 | | - |
222 | | -def _convert_rel_entr(expr, children): |
223 | | - """Convert rel_entr(x, y) = x * log(x/y) elementwise. |
224 | | -
|
225 | | - Uses specialized functions based on argument shapes: |
226 | | - - Both scalar or both same size: make_rel_entr (elementwise) |
227 | | - - First arg vector, second scalar: make_rel_entr_vector_scalar |
228 | | - - First arg scalar, second vector: make_rel_entr_scalar_vector |
229 | | - """ |
230 | | - x_arg, y_arg = expr.args |
231 | | - x_size = x_arg.size |
232 | | - y_size = y_arg.size |
233 | | - |
234 | | - # Determine which variant to use based on sizes |
235 | | - if x_size == y_size: |
236 | | - return _diffengine.make_rel_entr(children[0], children[1]) |
237 | | - elif x_size > 1 and y_size == 1: |
238 | | - return _diffengine.make_rel_entr_vector_scalar(children[0], children[1]) |
239 | | - elif x_size == 1 and y_size > 1: |
240 | | - return _diffengine.make_rel_entr_scalar_vector(children[0], children[1]) |
241 | | - else: |
242 | | - raise ValueError( |
243 | | - f"rel_entr requires arguments to be either both scalars, both same size, " |
244 | | - f"or one scalar and one vector. Got sizes: x={x_size}, y={y_size}" |
245 | | - ) |
246 | | - |
247 | | - |
248 | | -def _convert_quad_form(expr, children): |
249 | | - """Convert quadratic form x.T @ P @ x.""" |
250 | | - |
251 | | - P = expr.args[1] |
252 | | - |
253 | | - if not isinstance(P, cp.Constant): |
254 | | - raise NotImplementedError("quad_form requires P to be a constant matrix") |
255 | | - |
256 | | - P = P.value |
257 | | - |
258 | | - if not isinstance(P, sparse.csr_matrix): |
259 | | - P = sparse.csr_matrix(P) |
260 | | - |
261 | | - return _diffengine.make_quad_form( |
262 | | - children[0], |
263 | | - P.data.astype(np.float64), |
264 | | - P.indices.astype(np.int32), |
265 | | - P.indptr.astype(np.int32), |
266 | | - P.shape[0], |
267 | | - P.shape[1], |
268 | | - ) |
269 | | - |
270 | | - |
271 | | -def _convert_reshape(expr, children): |
272 | | - """Convert reshape - only Fortran order is supported. |
273 | | -
|
274 | | - Note: Only order='F' (Fortran/column-major) is supported. |
275 | | - """ |
276 | | - if expr.order != "F": |
277 | | - raise NotImplementedError( |
278 | | - f"reshape with order='{expr.order}' not supported. " |
279 | | - "Only order='F' (Fortran) is currently supported." |
280 | | - ) |
281 | | - |
282 | | - d1, d2 = normalize_shape(expr.shape) |
283 | | - return _diffengine.make_reshape(children[0], d1, d2) |
284 | | - |
285 | | -def _convert_broadcast(expr, children): |
286 | | - d1, d2 = expr.broadcast_shape |
287 | | - d1_C, d2_C = _diffengine.get_expr_dimensions(children[0]) |
288 | | - if d1_C == d1 and d2_C == d2: |
289 | | - return children[0] |
290 | | - |
291 | | - return _diffengine.make_broadcast(children[0], d1, d2) |
292 | | - |
293 | | -def _convert_sum(expr, children): |
294 | | - axis = expr.axis |
295 | | - if axis is None: |
296 | | - axis = -1 |
297 | | - return _diffengine.make_sum(children[0], axis) |
298 | | - |
299 | | -def _convert_promote(expr, children): |
300 | | - d1, d2 = normalize_shape(expr.shape) |
301 | | - return _diffengine.make_promote(children[0], d1, d2) |
302 | | - |
303 | | -def _convert_NegExpression(_expr, children): |
304 | | - return _diffengine.make_neg(children[0]) |
305 | | - |
306 | | -def _convert_quad_over_lin(_expr, children): |
307 | | - return _diffengine.make_quad_over_lin(children[0], children[1]) |
308 | | - |
309 | | -def _convert_index(expr, children): |
310 | | - idxs = _extract_flat_indices_from_index(expr) |
311 | | - d1, d2 = normalize_shape(expr.shape) |
312 | | - return _diffengine.make_index(children[0], d1, d2, idxs) |
313 | | - |
314 | | -def _convert_special_index(expr, children): |
315 | | - idxs = _extract_flat_indices_from_special_index(expr) |
316 | | - d1, d2 = normalize_shape(expr.shape) |
317 | | - return _diffengine.make_index(children[0], d1, d2, idxs) |
318 | | - |
319 | | -def _convert_prod(expr, children): |
320 | | - axis = expr.axis |
321 | | - if axis is None: |
322 | | - return _diffengine.make_prod(children[0]) |
323 | | - elif axis == 0: |
324 | | - return _diffengine.make_prod_axis_zero(children[0]) |
325 | | - elif axis == 1: |
326 | | - return _diffengine.make_prod_axis_one(children[0]) |
327 | | - |
328 | | -def _convert_transpose(expr, children): |
329 | | - # If the child is a vector (shape (n,) or (n,1) or (1,n)), use reshape to transpose |
330 | | - child_shape = normalize_shape(expr.args[0].shape) |
331 | | - |
332 | | - if 1 in child_shape: |
333 | | - return _diffengine.make_reshape(children[0], child_shape[1], child_shape[0]) |
334 | | - else: |
335 | | - return _diffengine.make_transpose(children[0]) |
336 | | - |
337 | | -def _convert_trace(_expr, children): |
338 | | - return _diffengine.make_trace(children[0]) |
339 | | - |
340 | | -def _convert_diag_vec(expr, children): |
341 | | - # C implementation only supports k=0 (main diagonal) |
342 | | - if expr.k != 0: |
343 | | - raise NotImplementedError("diag_vec with k != 0 not supported in diff engine") |
344 | | - return _diffengine.make_diag_vec(children[0]) |
345 | | - |
346 | | - |
347 | | -# --------------------------------------------------------------------------- |
348 | | -# Atom converter registry |
349 | | -# Converters receive (expr, children) where expr is the CVXPY expression. |
350 | | -# matmul and multiply are handled separately (they need param_dict). |
351 | | -# --------------------------------------------------------------------------- |
352 | | - |
353 | | -ATOM_CONVERTERS = { |
354 | | - # Elementwise unary |
355 | | - "log": lambda _expr, children: _diffengine.make_log(children[0]), |
356 | | - "exp": lambda _expr, children: _diffengine.make_exp(children[0]), |
357 | | - # Affine unary |
358 | | - "NegExpression": _convert_NegExpression, |
359 | | - "Promote": _convert_promote, |
360 | | - # N-ary (handles 2+ args) |
361 | | - "AddExpression": lambda _expr, children: _chain_add(children), |
362 | | - # Reductions |
363 | | - "Sum": _convert_sum, |
364 | | - # Bivariate |
365 | | - "QuadForm": _convert_quad_form, |
366 | | - "quad_over_lin": _convert_quad_over_lin, |
367 | | - "rel_entr": _convert_rel_entr, |
368 | | - # Elementwise univariate with parameter |
369 | | - "Power": lambda expr, children: _diffengine.make_power(children[0], float(expr.p.value)), |
370 | | - "PowerApprox": lambda expr, children: _diffengine.make_power(children[0], float(expr.p.value)), |
371 | | - # Trigonometric |
372 | | - "sin": lambda _expr, children: _diffengine.make_sin(children[0]), |
373 | | - "cos": lambda _expr, children: _diffengine.make_cos(children[0]), |
374 | | - "tan": lambda _expr, children: _diffengine.make_tan(children[0]), |
375 | | - # Hyperbolic |
376 | | - "sinh": lambda _expr, children: _diffengine.make_sinh(children[0]), |
377 | | - "tanh": lambda _expr, children: _diffengine.make_tanh(children[0]), |
378 | | - "asinh": lambda _expr, children: _diffengine.make_asinh(children[0]), |
379 | | - "atanh": lambda _expr, children: _diffengine.make_atanh(children[0]), |
380 | | - # Other elementwise |
381 | | - "entr": lambda _expr, children: _diffengine.make_entr(children[0]), |
382 | | - "logistic": lambda _expr, children: _diffengine.make_logistic(children[0]), |
383 | | - "xexp": lambda _expr, children: _diffengine.make_xexp(children[0]), |
384 | | - "normcdf": lambda _expr, children: _diffengine.make_normal_cdf(children[0]), |
385 | | - # Indexing/slicing |
386 | | - "index": _convert_index, |
387 | | - "special_index": _convert_special_index, |
388 | | - "reshape": _convert_reshape, |
389 | | - "broadcast_to": _convert_broadcast, |
390 | | - # Reductions returning scalar |
391 | | - "Prod": _convert_prod, |
392 | | - "transpose": _convert_transpose, |
393 | | - # Horizontal stack |
394 | | - "Hstack": _convert_hstack, |
395 | | - "Trace": _convert_trace, |
396 | | - # Diagonal |
397 | | - "diag_vec": _convert_diag_vec, |
398 | | -} |
399 | | - |
400 | | - |
401 | 109 | # --------------------------------------------------------------------------- |
402 | 110 | # Main conversion entry point |
403 | 111 | # --------------------------------------------------------------------------- |
|
0 commit comments