Skip to content

Commit 1ef159e

Browse files
committed
chnages from code review with dance
1 parent cde8bc0 commit 1ef159e

8 files changed

Lines changed: 52 additions & 79 deletions

File tree

CLAUDE.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,10 @@ Parameters are treated as affine (not constant) for curvature analysis. `param *
101101
`Dnlp2Smooth` converts DNLP expressions to smooth forms. Canonicalizers in `dnlp2smooth/canonicalizers/` handle atoms like log, exp, sin, power, geo_mean, etc. Registered in `SMOOTH_CANON_METHODS` dict.
102102

103103
### Diff engine (`cvxpy/reductions/solvers/nlp_solvers/diff_engine/`)
104-
- `C_problem.py`: Wraps SparseDiffPy C library for AD (objective, gradient, Jacobian, Hessian)
105-
- `converters.py`: Maps CVXPY atoms to C diff engine nodes via `convert_expr()` (40+ atoms supported)
106-
- `ConvertContext`: Manages variable/parameter node dictionaries during conversion
104+
- `c_problem.py`: Wraps SparseDiffPy C library for AD (objective, gradient, Jacobian, Hessian)
105+
- `converters.py`: Entry point — `convert_expr(expr, var_dict, n_vars, param_dict=None)` recursively converts CVXPY expressions to C diff engine nodes
106+
- `registry.py`: `ATOM_CONVERTERS` dict mapping atom names to converter functions (40+ atoms)
107+
- `helpers.py`: Shared utilities (`build_var_dict`, `build_param_dict`, matmul helpers, `normalize_shape`)
107108

108109
### Solver interfaces (`cvxpy/reductions/solvers/nlp_solvers/`)
109110
- `nlp_solver.py`: Base `NLPsolver` class with `Bounds` (constraint extraction) and `Oracles` (diff engine wrapper)
@@ -124,7 +125,7 @@ Register in `canonicalizers/__init__.py` → `CANON_METHODS[my_atom] = my_atom_c
124125
### 3. Create smooth canonicalizer in `cvxpy/reductions/dnlp2smooth/canonicalizers/` (if atom is smooth)
125126
Register in `SMOOTH_CANON_METHODS`.
126127

127-
### 4. Add converter in `cvxpy/reductions/solvers/nlp_solvers/diff_engine/converters.py` (for NLP support)
128+
### 4. Add converter in `cvxpy/reductions/solvers/nlp_solvers/diff_engine/registry.py``ATOM_CONVERTERS` (for NLP support)
128129

129130
### 5. Export in `cvxpy/atoms/__init__.py`
130131

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

Lines changed: 20 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,20 @@ def _convert_matmul(expr, children, var_dict, n_vars, param_dict):
3737

3838
if left_arg.is_constant():
3939
A = left_arg.value
40-
param_node = (convert_expr(left_arg, var_dict, n_vars, param_dict)
41-
if param_dict else None)
40+
if isinstance(left_arg, Parameter):
41+
param_node = param_dict[left_arg.id]
42+
else:
43+
param_node = None
4244
if sparse.issparse(A):
4345
return _make_sparse_left_matmul(param_node, children[1], A)
4446
return _make_dense_left_matmul(param_node, children[1], A)
4547

4648
elif right_arg.is_constant():
4749
A = right_arg.value
48-
param_node = (convert_expr(right_arg, var_dict, n_vars, param_dict)
49-
if param_dict else None)
50+
if isinstance(right_arg, Parameter):
51+
param_node = param_dict[right_arg.id]
52+
else:
53+
param_node = None
5054
if sparse.issparse(A):
5155
return _make_sparse_right_matmul(param_node, children[0], A)
5256
return _make_dense_right_matmul(param_node, children[0], A)
@@ -60,44 +64,18 @@ def _convert_multiply(expr, children, var_dict, n_vars, param_dict):
6064
left_arg, right_arg = expr.args
6165

6266
if left_arg.is_constant():
63-
if param_dict and left_arg.parameters():
64-
param_node = convert_expr(left_arg, var_dict, n_vars, param_dict)
65-
if left_arg.size == 1:
66-
return _diffengine.make_param_scalar_mult(
67-
param_node, children[1])
68-
return _diffengine.make_param_vector_mult(
69-
param_node, children[1])
70-
71-
a = _to_dense_float(left_arg.value)
72-
if a.size == 1:
73-
scalar = float(a.flat[0])
74-
if scalar == 1.0:
75-
return children[1]
76-
return _diffengine.make_const_scalar_mult(children[1], scalar)
77-
return _diffengine.make_const_vector_mult(
78-
children[1], a.flatten(order='F'))
79-
67+
if left_arg.size == 1:
68+
return _diffengine.make_param_scalar_mult(children[0], children[1])
69+
else:
70+
return _diffengine.make_param_vector_mult(children[0], children[1])
8071
elif right_arg.is_constant():
81-
if param_dict and right_arg.parameters():
82-
param_node = convert_expr(right_arg, var_dict, n_vars, param_dict)
83-
if right_arg.size == 1:
84-
return _diffengine.make_param_scalar_mult(
85-
param_node, children[0])
86-
return _diffengine.make_param_vector_mult(
87-
param_node, children[0])
88-
89-
a = _to_dense_float(right_arg.value)
90-
if a.size == 1:
91-
scalar = float(a.flat[0])
92-
if scalar == 1.0:
93-
return children[0]
94-
return _diffengine.make_const_scalar_mult(children[0], scalar)
95-
return _diffengine.make_const_vector_mult(
96-
children[0], a.flatten(order='F'))
97-
98-
# Neither is constant, use general multiply
99-
return _diffengine.make_multiply(children[0], children[1])
100-
72+
# TODO is this correct? Do we need to swap the arguments?
73+
if right_arg.size == 1:
74+
return _diffengine.make_param_scalar_mult(children[0], children[1])
75+
else:
76+
return _diffengine.make_param_vector_mult(children[0], children[1])
77+
else:
78+
return _diffengine.make_multiply(children[0], children[1])
10179

10280

10381
def convert_expr(expr, var_dict, n_vars, param_dict=None):
@@ -121,7 +99,7 @@ def convert_expr(expr, var_dict, n_vars, param_dict=None):
12199
if isinstance(expr, cp.Constant):
122100
c = _to_dense_float(expr.value)
123101
d1, d2 = normalize_shape(expr.shape)
124-
return _diffengine.make_constant(d1, d2, n_vars, c.flatten(order='F'))
102+
return _diffengine.make_parameter(d1, d2, -1, n_vars, c.flatten(order='F'))
125103

126104
# Recursive case: atoms
127105
atom_name = type(expr).__name__

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ def _chain_add(children):
4545
def _make_sparse_left_matmul(param_node, child, A):
4646
if not isinstance(A, sparse.csr_matrix):
4747
A = sparse.csr_matrix(A)
48-
return _diffengine.make_sparse_left_matmul(
49-
param_node, child,
48+
return _diffengine.make_left_matmul(
49+
param_node, child, 'sparse',
5050
A.data.astype(np.float64, copy=False),
5151
A.indices.astype(np.int32, copy=False),
5252
A.indptr.astype(np.int32, copy=False),
@@ -55,15 +55,15 @@ def _make_sparse_left_matmul(param_node, child, A):
5555

5656
def _make_dense_left_matmul(param_node, child, A):
5757
m, n = normalize_shape(A.shape)
58-
return _diffengine.make_dense_left_matmul(
59-
param_node, child, A.flatten(order='C'), m, n)
58+
return _diffengine.make_left_matmul(
59+
param_node, child, 'dense', A.flatten(order='C'), m, n)
6060

6161

6262
def _make_sparse_right_matmul(param_node, child, A):
6363
if not isinstance(A, sparse.csr_matrix):
6464
A = sparse.csr_matrix(A)
65-
return _diffengine.make_sparse_right_matmul(
66-
param_node, child,
65+
return _diffengine.make_right_matmul(
66+
param_node, child, 'sparse',
6767
A.data.astype(np.float64, copy=False),
6868
A.indices.astype(np.int32, copy=False),
6969
A.indptr.astype(np.int32, copy=False),
@@ -72,8 +72,8 @@ def _make_sparse_right_matmul(param_node, child, A):
7272

7373
def _make_dense_right_matmul(param_node, child, A):
7474
m, n = normalize_shape(A.shape)
75-
return _diffengine.make_dense_right_matmul(
76-
param_node, child, A.flatten(order='C'), m, n)
75+
return _diffengine.make_right_matmul(
76+
param_node, child, 'dense', A.flatten(order='C'), m, n)
7777

7878

7979

@@ -92,6 +92,7 @@ def build_param_dict(inverse_data):
9292
n_vars = inverse_data.x_length
9393
param_dict = {}
9494
for param_id, offset in inverse_data.param_id_map.items():
95+
# this is needed to not get key errors with Constants.
9596
if param_id not in inverse_data.param_shapes:
9697
continue
9798
d1, d2 = normalize_shape(inverse_data.param_shapes[param_id])

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

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -59,29 +59,19 @@ def _extract_flat_indices_from_special_index(expr):
5959

6060

6161
def _convert_rel_entr(expr, children):
62-
"""Convert rel_entr(x, y) = x * log(x/y) elementwise.
62+
"""Convert rel_entr(x, y) = x * log(x/y).
6363
64-
Uses specialized functions based on argument shapes:
65-
- Both scalar or both same size: make_rel_entr (elementwise)
66-
- First arg vector, second scalar: make_rel_entr_vector_scalar
67-
- First arg scalar, second vector: make_rel_entr_scalar_vector
64+
The C engine auto-dispatches based on argument dimensions
65+
(elementwise, vector-scalar, or scalar-vector).
6866
"""
69-
x_arg, y_arg = expr.args
70-
x_size = x_arg.size
71-
y_size = y_arg.size
72-
73-
# Determine which variant to use based on sizes
74-
if x_size == y_size:
75-
return _diffengine.make_rel_entr(children[0], children[1])
76-
elif x_size > 1 and y_size == 1:
77-
return _diffengine.make_rel_entr_vector_scalar(children[0], children[1])
78-
elif x_size == 1 and y_size > 1:
79-
return _diffengine.make_rel_entr_scalar_vector(children[0], children[1])
80-
else:
67+
x_size = expr.args[0].size
68+
y_size = expr.args[1].size
69+
if x_size > 1 and y_size > 1 and x_size != y_size:
8170
raise ValueError(
82-
f"rel_entr requires arguments to be either both scalars, both same size, "
83-
f"or one scalar and one vector. Got sizes: x={x_size}, y={y_size}"
71+
f"rel_entr requires compatible argument sizes. "
72+
f"Got: x={x_size}, y={y_size}"
8473
)
74+
return _diffengine.make_rel_entr(children[0], children[1])
8575

8676

8777
def _convert_quad_form(expr, children):

cvxpy/reductions/solvers/nlp_solvers/ipopt_nlpif.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,11 @@ def solve_via_data(self, data, warm_start: bool, verbose: bool, solver_opts, sol
155155
oracles = Oracles(bounds.new_problem, verbose=verbose, use_hessian=use_hessian)
156156
elif 'oracles' in solver_cache:
157157
oracles = solver_cache['oracles']
158+
# revisit updating parameters for problems with no parameters and best_of
158159
oracles.update_params(bounds.new_problem)
159160
else:
161+
# do we need to update params here? Or can we ensure that they always have values on the
162+
# first iteration. cvxpy ensures that parameters have values.
160163
oracles = Oracles(bounds.new_problem, verbose=verbose, use_hessian=use_hessian)
161164
solver_cache['oracles'] = oracles
162165

cvxpy/reductions/solvers/nlp_solving_chain.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,9 @@ def solve_nlp(problem, solver, warm_start, verbose, **kwargs):
178178

179179
# Reuse cached Oracles across solve() calls when problem has parameters.
180180
# Parameter updates happen inside solve_via_data when reusing.
181+
# Also initialize an empty cache for best_of.
181182
solver_cache = problem._solver_cache.get('NLP')
182-
if solver_cache is None and problem.parameters():
183+
if solver_cache is None and (problem.parameters() or "best_of" in kwargs):
183184
solver_cache = {}
184185
problem._solver_cache['NLP'] = solver_cache
185186

cvxpy/tests/nlp_tests/test_best_of.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,5 @@ def test_path_planning_best_of_five(self):
113113
prob.solve(nlp=True, best_of=3)
114114
all_objs = prob.solver_stats.extra_stats['all_objs_from_best_of']
115115
assert len(all_objs) == 3
116+
117+
# TODO add a test that best_of actually caches the sparsity pattern between solves

cvxpy/tests/nlp_tests/test_nlp_parameters.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,9 @@
1919
import cvxpy as cp
2020
from cvxpy.reductions.solvers.defines import INSTALLED_SOLVERS
2121

22-
pytestmark = pytest.mark.skipif(
23-
'IPOPT' not in INSTALLED_SOLVERS,
24-
reason="IPOPT is not installed")
2522

26-
27-
class TestNLPParameters:
23+
@pytest.mark.skipif('IPOPT' not in INSTALLED_SOLVERS, reason='IPOPT is not installed.')
24+
class Test_NLP_parameters:
2825

2926
def test_scalar_parameter(self):
3027
"""min p * x^2 + x, analytical solution: val = -1/(4p)."""

0 commit comments

Comments
 (0)