Skip to content

Commit 0e15dc3

Browse files
Transurgeonclaude
andcommitted
Rename ESR/HSR to linearizable_convex/linearizable_concave
Spell out opaque acronyms for clarity per PR review feedback: is_atom_esr → is_atom_linearizable_convex, is_atom_hsr → is_atom_linearizable_concave, is_esr → is_linearizable_convex, is_hsr → is_linearizable_concave, is_smooth → is_linearizable. Docstrings clarify that "linearizable convex" means the expression is convex after linearizing all smooth subexpressions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0c4f10e commit 0e15dc3

35 files changed

Lines changed: 221 additions & 198 deletions

CLAUDE.md

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,15 @@ prob.solve(nlp=True, solver=cp.IPOPT, best_of=5)
7676
- Target Python version: 3.11+
7777
- Line length: 100 characters
7878
- **IMPORTANT: IMPORTS AT THE TOP** of files - circular imports are the only exception
79+
- Ruff excludes all `*__init__.py` files (configured in `pyproject.toml`)
80+
- Pre-commit hooks include: ruff (with `--fix`), check-jsonschema (GitHub workflows/dependabot), actionlint, and validate-pyproject
7981

8082
## License Header
8183

82-
New files should include the Apache 2.0 license header:
84+
New files should include the Apache 2.0 license header (matching the convention used in the majority of the codebase):
8385
```python
8486
"""
85-
Copyright 2025, the CVXPY developers
87+
Copyright, the CVXPY authors
8688
8789
Licensed under the Apache License, Version 2.0 (the "License");
8890
you may not use this file except in compliance with the License.
@@ -129,6 +131,8 @@ Key reduction classes in `cvxpy/reductions/`:
129131

130132
For DNLP: `CvxAttr2Constr``Dnlp2Smooth``NLPSolver`
131133

134+
Note: The standard `SolvingChain` in `solving_chain.py` handles DCP/DGP/DQCP solver selection automatically. NLP solving is triggered explicitly via `prob.solve(nlp=True)` and bypasses the standard chain.
135+
132136
### Solver Categories
133137

134138
- **ConicSolvers** (`cvxpy/reductions/solvers/conic_solvers/`) - SCS, Clarabel, ECOS, etc.
@@ -142,12 +146,20 @@ The NLP infrastructure provides oracle-based interfaces for nonlinear solvers:
142146
- `Bounds` class: extracts variable/constraint bounds from problem
143147
- `Oracles` class: provides function and derivative oracles (objective, gradient, constraints, jacobian, hessian)
144148
- `dnlp2smooth.py` - Transforms DNLP problems to smooth form via `Dnlp2Smooth` reduction
145-
- DNLP validation: expressions must be smooth (ESR and HSR)
149+
- DNLP validation: expressions must be linearizable (linearizable convex and linearizable concave)
146150
- Problem validity checked via `problem.is_dnlp()` method
147151

148152
### Diff Engine (SparseDiffPy)
149153

150-
The automatic differentiation engine is provided by the [SparseDiffPy](https://github.com/SparseDifferentiation/SparseDiffPy) package (`pip install sparsediffpy`), which wraps the [SparseDiffEngine](https://github.com/SparseDifferentiation/SparseDiffEngine) C library. It builds expression trees from CVXPY problems and computes derivatives (gradients, Jacobians, Hessians) for NLP solvers. New diff engine atoms require C-level additions in SparseDiffPy.
154+
The automatic differentiation engine is provided by the [SparseDiffPy](https://github.com/SparseDifferentiation/SparseDiffPy) package (installed automatically as a hard dependency), which wraps the [SparseDiffEngine](https://github.com/SparseDifferentiation/SparseDiffEngine) C library. It builds expression trees from CVXPY problems and computes derivatives (gradients, Jacobians, Hessians) for NLP solvers.
155+
156+
Adding a new diff engine atom requires:
157+
1. C-level implementation in SparseDiffPy
158+
2. Python-side converter in `cvxpy/reductions/solvers/nlp_solvers/diff_engine/converters.py` (add to `ATOM_CONVERTERS` dict)
159+
160+
The diff engine supports CVXPY `Parameter` objects: `C_problem` registers parameters with the C engine and `update_params()` re-pushes values without rebuilding the expression tree. Sparse parameter values are fused into sparse matmul operations.
161+
162+
`DerivativeChecker` in `nlp_solver.py` provides finite-difference verification of gradients, Jacobians, and Hessians during development.
151163

152164
## Implementing New Atoms
153165

@@ -164,15 +176,15 @@ The automatic differentiation engine is provided by the [SparseDiffPy](https://g
164176
1. Create a canonicalizer in `cvxpy/reductions/dnlp2smooth/canonicalizers/`
165177
2. The canonicalizer converts non-smooth atoms to smooth equivalents using auxiliary variables
166178
3. Register in `canonicalizers/__init__.py` by adding to `SMOOTH_CANON_METHODS` dict
167-
4. Ensure the atom has proper `is_smooth()`, `is_esr()`, `is_hsr()` methods
179+
4. Ensure the atom has proper `is_linearizable()`, `is_linearizable_convex()`, `is_linearizable_concave()` methods
168180

169-
### DNLP Rules (ESR/HSR)
181+
### DNLP Rules (Linearizable Convex / Linearizable Concave)
170182

171-
- **Smooth**: functions that are both ESR and HSR (analogous to affine in DCP)
172-
- **ESR** (Essentially Smooth Respecting): can be minimized or appear in `<= 0` constraints
173-
- **HSR** (Hierarchically Smooth Respecting): can be maximized or appear in `>= 0` constraints
183+
- **Linearizable**: functions that are both linearizable convex and linearizable concave (analogous to affine in DCP)
184+
- **Linearizable Convex**: can be minimized or appear in `<= 0` constraints
185+
- **Linearizable Concave**: can be maximized or appear in `>= 0` constraints
174186

175-
Use `expr.is_smooth()`, `expr.is_esr()`, `expr.is_hsr()` to check expression properties.
187+
Use `expr.is_linearizable()`, `expr.is_linearizable_convex()`, `expr.is_linearizable_concave()` to check expression properties.
176188

177189
## Testing
178190

@@ -192,3 +204,12 @@ class TestMyFeature(BaseTest):
192204
```
193205

194206
NLP tests are in `cvxpy/tests/nlp_tests/` with Jacobian and Hessian verification tests.
207+
208+
## Benchmarks
209+
210+
Benchmarks use [Airspeed Velocity](https://asv.readthedocs.io/) and live in the `benchmarks/` directory. To run locally:
211+
```bash
212+
cd benchmarks
213+
pip install -e .
214+
asv run
215+
```

cvxpy/atoms/affine/affine_atom.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,13 @@ def is_atom_concave(self) -> bool:
5757
"""
5858
return True
5959

60-
def is_atom_esr(self) -> bool:
61-
"""Is the atom esr?
60+
def is_atom_linearizable_convex(self) -> bool:
61+
"""Is the atom convex after linearizing?
6262
"""
6363
return True
6464

65-
def is_atom_hsr(self) -> bool:
66-
"""Is the atom hsr?
65+
def is_atom_linearizable_concave(self) -> bool:
66+
"""Is the atom concave after linearizing?
6767
"""
6868
return True
6969

cvxpy/atoms/atom.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -188,16 +188,16 @@ def is_atom_affine(self) -> bool:
188188
"""
189189
return self.is_atom_concave() and self.is_atom_convex()
190190

191-
def is_atom_esr(self) -> bool:
192-
"""Is the atom esr?
191+
def is_atom_linearizable_convex(self) -> bool:
192+
"""Is the atom convex after linearizing?
193193
"""
194-
raise NotImplementedError("is_atom_esr not implemented for %s."
194+
raise NotImplementedError("is_atom_linearizable_convex not implemented for %s."
195195
% self.__class__.__name__)
196196

197-
def is_atom_hsr(self) -> bool:
198-
"""Is the atom hsr?
197+
def is_atom_linearizable_concave(self) -> bool:
198+
"""Is the atom concave after linearizing?
199199
"""
200-
raise NotImplementedError("is_atom_hsr not implemented for %s."
200+
raise NotImplementedError("is_atom_linearizable_concave not implemented for %s."
201201
% self.__class__.__name__)
202202

203203
def is_atom_log_log_convex(self) -> bool:
@@ -272,34 +272,34 @@ def is_concave(self) -> bool:
272272
return False
273273

274274
@perf.compute_once
275-
def is_esr(self) -> bool:
276-
"""Is the expression epigraph smooth representable?
275+
def is_linearizable_convex(self) -> bool:
276+
"""Is the expression convex after linearizing all smooth subexpressions?
277277
"""
278278
# Applies DNLP composition rule.
279279
if self.is_constant():
280280
return True
281-
elif self.is_atom_esr():
281+
elif self.is_atom_linearizable_convex():
282282
for idx, arg in enumerate(self.args):
283-
if not (arg.is_smooth() or
284-
(arg.is_esr() and self.is_incr(idx)) or
285-
(arg.is_hsr() and self.is_decr(idx))):
283+
if not (arg.is_linearizable() or
284+
(arg.is_linearizable_convex() and self.is_incr(idx)) or
285+
(arg.is_linearizable_concave() and self.is_decr(idx))):
286286
return False
287287
return True
288288
else:
289289
return False
290290

291291
@perf.compute_once
292-
def is_hsr(self) -> bool:
293-
"""Is the expression hypograph smooth representable?
292+
def is_linearizable_concave(self) -> bool:
293+
"""Is the expression concave after linearizing all smooth subexpressions?
294294
"""
295295
# Applies DNLP composition rule.
296296
if self.is_constant():
297297
return True
298-
elif self.is_atom_hsr():
298+
elif self.is_atom_linearizable_concave():
299299
for idx, arg in enumerate(self.args):
300-
if not (arg.is_smooth() or
301-
(arg.is_hsr() and self.is_incr(idx)) or
302-
(arg.is_esr() and self.is_decr(idx))):
300+
if not (arg.is_linearizable() or
301+
(arg.is_linearizable_concave() and self.is_incr(idx)) or
302+
(arg.is_linearizable_convex() and self.is_decr(idx))):
303303
return False
304304
return True
305305
else:

cvxpy/atoms/elementwise/abs.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,13 @@ def is_atom_concave(self) -> bool:
5656
"""
5757
return False
5858

59-
def is_atom_esr(self) -> bool:
60-
"""Is the atom esr?
59+
def is_atom_linearizable_convex(self) -> bool:
60+
"""Is the atom convex after linearizing?
6161
"""
6262
return True
6363

64-
def is_atom_hsr(self) -> bool:
65-
"""Is the atom hsr?
64+
def is_atom_linearizable_concave(self) -> bool:
65+
"""Is the atom concave after linearizing?
6666
"""
6767
return False
6868

cvxpy/atoms/elementwise/entr.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,13 @@ def is_atom_concave(self) -> bool:
5858
"""
5959
return True
6060

61-
def is_atom_esr(self) -> bool:
62-
"""Is the atom esr?
61+
def is_atom_linearizable_convex(self) -> bool:
62+
"""Is the atom convex after linearizing?
6363
"""
6464
return True
6565

66-
def is_atom_hsr(self) -> bool:
67-
"""Is the atom hsr?
66+
def is_atom_linearizable_concave(self) -> bool:
67+
"""Is the atom concave after linearizing?
6868
"""
6969
return True
7070

cvxpy/atoms/elementwise/exp.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,13 @@ def is_atom_concave(self) -> bool:
5555
"""
5656
return False
5757

58-
def is_atom_esr(self) -> bool:
59-
"""Is the atom esr?
58+
def is_atom_linearizable_convex(self) -> bool:
59+
"""Is the atom convex after linearizing?
6060
"""
6161
return True
6262

63-
def is_atom_hsr(self) -> bool:
64-
"""Is the atom hsr?
63+
def is_atom_linearizable_concave(self) -> bool:
64+
"""Is the atom concave after linearizing?
6565
"""
6666
return True
6767

cvxpy/atoms/elementwise/huber.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,13 @@ def is_atom_concave(self) -> bool:
7171
"""Is the atom concave?"""
7272
return False
7373

74-
def is_atom_esr(self) -> bool:
75-
"""Is the atom esr?
74+
def is_atom_linearizable_convex(self) -> bool:
75+
"""Is the atom convex after linearizing?
7676
"""
7777
return True
7878

79-
def is_atom_hsr(self) -> bool:
80-
"""Is the atom hsr?
79+
def is_atom_linearizable_concave(self) -> bool:
80+
"""Is the atom concave after linearizing?
8181
"""
8282
return False
8383

cvxpy/atoms/elementwise/hyperbolic.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,13 @@ def is_atom_concave(self) -> bool:
5050
"""
5151
return False
5252

53-
def is_atom_esr(self) -> bool:
54-
"""Is the atom esr?
53+
def is_atom_linearizable_convex(self) -> bool:
54+
"""Is the atom convex after linearizing?
5555
"""
5656
return True
5757

58-
def is_atom_hsr(self) -> bool:
59-
"""Is the atom hsr?
58+
def is_atom_linearizable_concave(self) -> bool:
59+
"""Is the atom concave after linearizing?
6060
"""
6161
return True
6262

@@ -108,13 +108,13 @@ def is_atom_concave(self) -> bool:
108108
"""
109109
return False
110110

111-
def is_atom_esr(self) -> bool:
112-
"""Is the atom esr?
111+
def is_atom_linearizable_convex(self) -> bool:
112+
"""Is the atom convex after linearizing?
113113
"""
114114
return True
115115

116-
def is_atom_hsr(self) -> bool:
117-
"""Is the atom hsr?
116+
def is_atom_linearizable_concave(self) -> bool:
117+
"""Is the atom concave after linearizing?
118118
"""
119119
return True
120120

@@ -160,10 +160,10 @@ def is_atom_convex(self) -> bool:
160160
def is_atom_concave(self) -> bool:
161161
return False
162162

163-
def is_atom_esr(self) -> bool:
163+
def is_atom_linearizable_convex(self) -> bool:
164164
return True
165165

166-
def is_atom_hsr(self) -> bool:
166+
def is_atom_linearizable_concave(self) -> bool:
167167
return True
168168

169169
def is_incr(self, idx) -> bool:
@@ -202,10 +202,10 @@ def is_atom_convex(self) -> bool:
202202
def is_atom_concave(self) -> bool:
203203
return False
204204

205-
def is_atom_esr(self) -> bool:
205+
def is_atom_linearizable_convex(self) -> bool:
206206
return True
207207

208-
def is_atom_hsr(self) -> bool:
208+
def is_atom_linearizable_concave(self) -> bool:
209209
return True
210210

211211
def is_incr(self, idx) -> bool:

cvxpy/atoms/elementwise/kl_div.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,13 @@ def is_atom_concave(self) -> bool:
5555
"""
5656
return False
5757

58-
def is_atom_esr(self) -> bool:
59-
"""Is the atom esr?
58+
def is_atom_linearizable_convex(self) -> bool:
59+
"""Is the atom convex after linearizing?
6060
"""
6161
return True
6262

63-
def is_atom_hsr(self) -> bool:
64-
"""Is the atom hsr?
63+
def is_atom_linearizable_concave(self) -> bool:
64+
"""Is the atom concave after linearizing?
6565
"""
6666
return True
6767

cvxpy/atoms/elementwise/log.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,13 @@ def is_atom_concave(self) -> bool:
5656
"""
5757
return True
5858

59-
def is_atom_esr(self) -> bool:
60-
"""Is the atom esr?
59+
def is_atom_linearizable_convex(self) -> bool:
60+
"""Is the atom convex after linearizing?
6161
"""
6262
return True
6363

64-
def is_atom_hsr(self) -> bool:
65-
"""Is the atom hsr?
64+
def is_atom_linearizable_concave(self) -> bool:
65+
"""Is the atom concave after linearizing?
6666
"""
6767
return True
6868

0 commit comments

Comments
 (0)