Skip to content

Commit b433869

Browse files
committed
Diffengine extractor: DNLP solving with differentiation engine
Squashed 163 commits from diffengine-extractor branch. Includes: - DNLP (Disciplined Nonlinear Programming) framework - NLP solver interfaces (IPOPT, COPT, Knitro, Uno) - Differentiation engine with sparse Jacobian/Hessian support - dnlp2smooth canonicalizers for all supported atoms - NLP solving chain and problem reduction pipeline - Comprehensive NLP test suite
1 parent e6df2dd commit b433869

24 files changed

Lines changed: 1091 additions & 100 deletions

CLAUDE.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,36 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
15
# CVXPY Development Guide
26

37
## Quick Reference
48

59
### Commands
610
```bash
7-
# Install in development mode
11+
# Install in development mode (includes C++ extensions)
812
pip install -e .
913

1014
# Install pre-commit hooks (required)
1115
pip install pre-commit && pre-commit install
1216

17+
# Run linter manually
18+
ruff check cvxpy/ --fix
19+
1320
# Run all tests
1421
pytest cvxpy/tests/
1522

1623
# Run specific test
1724
pytest cvxpy/tests/test_atoms.py::TestAtoms::test_norm_inf
25+
26+
# Run benchmarks (uses asv - airspeed velocity)
27+
cd benchmarks && pip install -e . && asv run
1828
```
1929

2030
## Code Style
2131

32+
- **Python >= 3.11** required
33+
- **Linter**: Ruff (enforced via pre-commit hooks; auto-fixes code but fails the commit if fixes were needed). Note: `__init__.py` files are excluded from linting.
2234
- **Line length**: 100 characters
2335
- **IMPORTANT: IMPORTS AT THE TOP** of files - circular imports are the only exception
2436
- **IMPORTANT:** Add Apache 2.0 license header to all new files
@@ -126,14 +138,13 @@ Check with `problem.is_dpp()`. See `cvxpy/utilities/scopes.py` for implementatio
126138
Location: `cvxpy/atoms/` or `cvxpy/atoms/elementwise/`
127139

128140
```python
129-
from typing import Tuple
130141
from cvxpy.atoms.atom import Atom
131142

132143
class my_atom(Atom):
133144
def __init__(self, x) -> None:
134145
super().__init__(x)
135146

136-
def shape_from_args(self) -> Tuple[int, ...]:
147+
def shape_from_args(self) -> tuple[int, ...]:
137148
return self.args[0].shape
138149

139150
def sign_from_args(self) -> Tuple[bool, bool]:

cvxpy/atoms/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,12 @@
163163
ptp
164164
]
165165

166+
NON_SMOOTH_ATOMS = [
167+
abs,
168+
maximum,
169+
minimum,
170+
]
171+
166172
# DGP atoms whose Dgp2Dcp canonicalization produces ExpCone-requiring DCP atoms.
167173
GP_EXP_ATOMS = [
168174
AddExpression,

cvxpy/atoms/affine/transpose.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ def graph_implementation(
116116
"""
117117
return (lu.transpose(arg_objs[0], self.axes), [])
118118

119+
119120
def permute_dims(expr, axes: List[int]):
120121
"""Permute the dimensions of the expression.
121122

cvxpy/atoms/elementwise/entr.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,6 @@ def _domain(self) -> List[Constraint]:
9797
"""Returns constraints describing the domain of the node.
9898
"""
9999
return [self.args[0] >= 0]
100+
101+
def point_in_domain(self):
102+
return np.ones(self.shape)

cvxpy/atoms/elementwise/kl_div.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,6 @@ def _domain(self) -> List[Constraint]:
9898
"""Returns constraints describing the domain of the node.
9999
"""
100100
return [self.args[0] >= 0, self.args[1] >= 0]
101+
102+
def point_in_domain(self, argument=0):
103+
return np.ones(self.args[argument].shape)

cvxpy/atoms/elementwise/log.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,8 @@ def _domain(self) -> List[Constraint]:
105105
"""Returns constraints describing the domain of the node.
106106
"""
107107
return [self.args[0] >= 0]
108-
108+
109+
def point_in_domain(self) -> np.ndarray:
110+
"""Returns a point in the domain of the node.
111+
"""
112+
return np.ones(self.shape)

cvxpy/atoms/elementwise/rel_entr.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,6 @@ def _domain(self):
9999
"""Returns constraints describing the domain of the node.
100100
"""
101101
return [self.args[0] >= 0, self.args[1] >= 0]
102-
102+
103+
def point_in_domain(self, argument=0):
104+
return np.ones(self.args[argument].shape)

cvxpy/atoms/elementwise/xexp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,4 @@ def _grad(self, values):
9696
def _domain(self) -> List[Constraint]:
9797
"""Returns constraints describing the domain of the node.
9898
"""
99-
return [self.args[0] >= 0]
99+
return [self.args[0] >= 0]

cvxpy/reductions/chain.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import time
2+
13
from cvxpy import settings as s
24
from cvxpy.reductions.reduction import Reduction
35

@@ -73,7 +75,11 @@ def apply(self, problem, verbose: bool = False):
7375
for r in self.reductions:
7476
if verbose:
7577
s.LOGGER.info('Applying reduction %s', type(r).__name__)
78+
start = time.perf_counter()
7679
problem, inv = r.apply(problem)
80+
elapsed = time.perf_counter() - start
81+
if verbose:
82+
s.LOGGER.info(' %s took %.4f seconds', type(r).__name__, elapsed)
7783
inverse_data.append(inv)
7884
return problem, inverse_data
7985

cvxpy/reductions/dcp2cone/cone_matrix_stuffing.py

Lines changed: 113 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,53 @@ def split_solution(self, sltn, active_vars=None):
322322
value, var.shape, order='F')
323323
return sltn_dict
324324

325+
def apply_restruct_mat(self, restruct_mat, restruct_mat_op):
326+
"""Apply restructuring matrix to parametric tensor A.
327+
328+
Parameters
329+
----------
330+
restruct_mat : list
331+
List of sparse matrices or linear operators (unused for this path).
332+
restruct_mat_op : LinearOperator
333+
Block diagonal linear operator for restructuring.
334+
335+
Returns
336+
-------
337+
ParamConeProg
338+
New program with restructured A tensor.
339+
"""
340+
if restruct_mat_op is not None:
341+
unspecified, _ = np.divmod(self.A.shape[0] * self.A.shape[1],
342+
restruct_mat_op.shape[1], dtype=np.int64)
343+
reshaped_A = self.A.reshape(restruct_mat_op.shape[1],
344+
unspecified, order='F').tocsr()
345+
restructured_A = restruct_mat_op(reshaped_A).tocoo()
346+
# Because of a bug in scipy versions < 1.20, `reshape`
347+
# can overflow if indices are int32s.
348+
restructured_A.row = restructured_A.row.astype(np.int64)
349+
restructured_A.col = restructured_A.col.astype(np.int64)
350+
restructured_A = restructured_A.reshape(
351+
np.int64(restruct_mat_op.shape[0]) * (np.int64(self.x.size) + 1),
352+
self.A.shape[1], order='F')
353+
else:
354+
restructured_A = self.A
355+
return ParamConeProg(
356+
self.q,
357+
self.x,
358+
restructured_A,
359+
self.variables,
360+
self.var_id_to_col,
361+
self.constraints,
362+
self.parameters,
363+
self.param_id_to_col,
364+
P=self.P,
365+
formatted=True,
366+
lower_bounds=self.lower_bounds,
367+
upper_bounds=self.upper_bounds,
368+
lb_tensor=self.lb_tensor,
369+
ub_tensor=self.ub_tensor,
370+
)
371+
325372
def split_adjoint(self, del_vars=None):
326373
"""Adjoint of split_solution.
327374
"""
@@ -339,6 +386,65 @@ def split_adjoint(self, del_vars=None):
339386
return var_vec
340387

341388

389+
def lower_and_order_constraints(constraints):
390+
"""Lower equality/inequality constraints and reorder by cone type.
391+
392+
Converts Equality -> Zero, Inequality/NonPos -> NonNeg, and normalizes
393+
SOC/PowCone/ExpCone axes. Returns constraints ordered as:
394+
Zero, NonNeg, SOC, PSD, ExpCone, PowCone3D, PowConeND.
395+
396+
Parameters
397+
----------
398+
constraints : list
399+
The problem constraints to lower and reorder.
400+
401+
Returns
402+
-------
403+
ordered_cons : list
404+
The lowered and reordered constraints.
405+
cons_id_map : dict
406+
Mapping from constraint id to constraint id (identity map).
407+
"""
408+
cons = []
409+
for con in constraints:
410+
if isinstance(con, Equality):
411+
con = lower_equality(con)
412+
elif isinstance(con, Inequality):
413+
con = lower_ineq_to_nonneg(con)
414+
elif isinstance(con, NonPos):
415+
con = nonpos2nonneg(con)
416+
elif isinstance(con, SOC) and con.axis == 1:
417+
con = SOC(con.args[0], con.args[1].T, axis=0,
418+
constr_id=con.constr_id)
419+
elif isinstance(con, PowCone3D) and con.args[0].ndim > 1:
420+
x, y, z = con.args
421+
alpha = con.alpha
422+
con = PowCone3D(x.flatten(order='F'),
423+
y.flatten(order='F'),
424+
z.flatten(order='F'),
425+
alpha.flatten(order='F'),
426+
constr_id=con.constr_id)
427+
elif isinstance(con, PowConeND) and con.axis == 1:
428+
alpha = con.alpha.T
429+
W = con.W.T
430+
con = PowConeND(W, con.z.flatten(order='F'),
431+
alpha,
432+
axis=0,
433+
constr_id=con.constr_id)
434+
elif isinstance(con, ExpCone) and con.args[0].ndim > 1:
435+
x, y, z = con.args
436+
con = ExpCone(x.flatten(order='F'), y.flatten(order='F'), z.flatten(order='F'),
437+
constr_id=con.constr_id)
438+
cons.append(con)
439+
# Reorder constraints to Zero, NonNeg, SOC, PSD, EXP, PowCone3D, PowConeND
440+
constr_map = group_constraints(cons)
441+
ordered_cons = constr_map[Zero] + constr_map[NonNeg] + \
442+
constr_map[SOC] + constr_map[PSD] + constr_map[ExpCone] + \
443+
constr_map[PowCone3D] + constr_map[PowConeND]
444+
cons_id_map = {con.id: con.id for con in ordered_cons}
445+
return ordered_cons, cons_id_map
446+
447+
342448
class ConeMatrixStuffing(MatrixStuffing):
343449
"""Construct matrices for linear cone problems.
344450
@@ -380,51 +486,20 @@ def stuffed_objective(self, problem, extractor):
380486

381487
def apply(self, problem):
382488
inverse_data = InverseData(problem)
383-
# Lower equality and inequality to Zero and NonNeg.
384-
cons = []
385-
for con in problem.constraints:
386-
if isinstance(con, Equality):
387-
con = lower_equality(con)
388-
elif isinstance(con, Inequality):
389-
con = lower_ineq_to_nonneg(con)
390-
elif isinstance(con, SOC) and con.axis == 1:
391-
con = SOC(con.args[0], con.args[1].T, axis=0,
392-
constr_id=con.constr_id)
393-
elif isinstance(con, PowCone3D) and con.args[0].ndim > 1:
394-
x, y, z = con.args
395-
alpha = con.alpha
396-
con = PowCone3D(x.flatten(order='F'),
397-
y.flatten(order='F'),
398-
z.flatten(order='F'),
399-
alpha.flatten(order='F'),
400-
constr_id=con.constr_id)
401-
elif isinstance(con, PowConeND) and con.axis == 1:
402-
alpha = con.alpha.T
403-
W = con.W.T
404-
con = PowConeND(W, con.z.flatten(order='F'),
405-
alpha,
406-
axis=0,
407-
constr_id=con.constr_id)
408-
elif isinstance(con, ExpCone) and con.args[0].ndim > 1:
409-
x, y, z = con.args
410-
con = ExpCone(x.flatten(order='F'), y.flatten(order='F'), z.flatten(order='F'),
411-
constr_id=con.constr_id)
412-
cons.append(con)
489+
# Lower equality and inequality constraints and reorder by cone type.
490+
ordered_cons, cons_id_map = lower_and_order_constraints(problem.constraints)
491+
inverse_data.cons_id_map = cons_id_map
492+
inverse_data.constraints = ordered_cons
493+
413494
# Need to check that intended canonicalization backend still works.
414-
lowered_con_problem = problem.copy([problem.objective, cons])
495+
lowered_con_problem = problem.copy(
496+
[problem.objective, ordered_cons])
415497
canon_backend = get_canon_backend(lowered_con_problem, self.canon_backend)
416498
# Form the constraints
417499
extractor = CoeffExtractor(inverse_data, canon_backend)
418500
params_to_P, params_to_c, flattened_variable = self.stuffed_objective(
419501
problem, extractor)
420-
# Reorder constraints to Zero, NonNeg, SOC, PSD, EXP, PowCone3D, PowConeND
421-
constr_map = group_constraints(cons)
422-
ordered_cons = constr_map[Zero] + constr_map[NonNeg] + \
423-
constr_map[SOC] + constr_map[PSD] + constr_map[ExpCone] + \
424-
constr_map[PowCone3D] + constr_map[PowConeND]
425-
inverse_data.cons_id_map = {con.id: con.id for con in ordered_cons}
426502

427-
inverse_data.constraints = ordered_cons
428503
# Batch expressions together, then split apart.
429504
expr_list = [arg for c in ordered_cons for arg in c.args]
430505
params_to_problem_data = extractor.affine(expr_list)

0 commit comments

Comments
 (0)