Skip to content

Commit 97b08bb

Browse files
Transurgeonclaude
andcommitted
Split converters.py into helpers.py, registry.py, and converters.py
- helpers.py: shared utilities, matmul helpers, var/param dict builders - registry.py: atom converter functions and ATOM_CONVERTERS dict - converters.py: convert_expr entry point and param-aware matmul/multiply Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 918b1cc commit 97b08bb

5 files changed

Lines changed: 354 additions & 309 deletions

File tree

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@
2222
"""
2323

2424
from cvxpy.reductions.solvers.nlp_solvers.diff_engine.c_problem import C_problem
25-
from cvxpy.reductions.solvers.nlp_solvers.diff_engine.converters import (
26-
ATOM_CONVERTERS,
25+
from cvxpy.reductions.solvers.nlp_solvers.diff_engine.converters import convert_expr
26+
from cvxpy.reductions.solvers.nlp_solvers.diff_engine.helpers import (
2727
build_param_dict,
2828
build_var_dict,
29-
convert_expr,
3029
)
30+
from cvxpy.reductions.solvers.nlp_solvers.diff_engine.registry import ATOM_CONVERTERS
3131

3232
__all__ = [
3333
"C_problem",

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717

1818
import cvxpy as cp
1919
from cvxpy.reductions.inverse_data import InverseData
20-
from cvxpy.reductions.solvers.nlp_solvers.diff_engine.converters import (
20+
from cvxpy.reductions.solvers.nlp_solvers.diff_engine.converters import convert_expr
21+
from cvxpy.reductions.solvers.nlp_solvers.diff_engine.helpers import (
2122
build_param_dict,
2223
build_var_dict,
23-
convert_expr,
2424
)
2525

2626

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

Lines changed: 12 additions & 304 deletions
Original file line numberDiff line numberDiff line change
@@ -13,105 +13,25 @@
1313
See the License for the specific language governing permissions and
1414
limitations under the License.
1515
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.
2017
"""
21-
import numpy as np
2218
from scipy import sparse
2319
from sparsediffpy import _sparsediffengine as _diffengine
2420

2521
import cvxpy as cp
2622
from cvxpy.expressions.constants.parameter import Parameter
2723

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
11535

11636

11737
# ---------------------------------------------------------------------------
@@ -186,218 +106,6 @@ def _convert_multiply(expr, children, var_dict, n_vars, param_dict):
186106
return _diffengine.make_multiply(children[0], children[1])
187107

188108

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-
401109
# ---------------------------------------------------------------------------
402110
# Main conversion entry point
403111
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)