Skip to content

Commit 59cbc6c

Browse files
Add cp.nlp submodule namespace for NLP atoms (cvxpy#3222)
* Add cp.nlp submodule namespace for NLP atoms Closes cvxpy#3198 Creates a dedicated cvxpy.nlp namespace for atoms that require an NLP solver (nlp=True). Users can now write: import cvxpy as cp x = cp.Variable() prob = cp.Problem(cp.Minimize(cp.nlp.sin(x)), [x >= 0]) prob.solve(nlp=True) Atoms available under cp.nlp: - Trigonometric: sin, cos, tan - Hyperbolic: sinh, tanh, asinh, atanh Note: cp.sin etc. still work as before via cp.atoms import. cp.nlp provides a dedicated namespace making it clear these atoms require NLP solvers. AI assistance was used for codebase exploration, as per CVXPY's AI disclosure policy. * Remove NLP atoms from top-level cp namespace, keep only under cp.nlp Per PTNobel's review: NLP atoms (sin, cos, tan, sinh, tanh, asinh, atanh) should not be in the top-level cp namespace since they require nlp=True. They are now only accessible via cp.nlp.sin etc. Also update tests to reflect new design: - Remove test_atoms_same_as_direct (cp.sin no longer exists) - Add test_atoms_not_in_top_level to enforce the new policy - Add test_atoms_same_class_as_direct_import to verify cp.nlp points to the correct classes * Update NLP tests to use cp.nlp namespace NLP atoms (sin, cos, tan, sinh, tanh, asinh, atanh) are no longer exported at the top-level cp namespace. Update all nlp_tests to use cp.nlp.sin, cp.nlp.cos etc. consistently. * Fix ruff linting: remove unused imports and sort import block in test_nlp_namespace.py * update dnlp docs --------- Co-authored-by: Steven Diamond <diamond@cs.stanford.edu>
1 parent ab93616 commit 59cbc6c

10 files changed

Lines changed: 93 additions & 31 deletions

File tree

cvxpy/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
suppfunc as suppfunc,
6565
)
6666
from cvxpy import logic as logic
67+
from cvxpy import nlp as nlp
6768
from cvxpy.reductions.solvers.defines import installed_solvers as installed_solvers
6869
from cvxpy.settings import (
6970
CBC as CBC,

cvxpy/atoms/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@
7272
from cvxpy.atoms.elementwise.sqrt import sqrt
7373
from cvxpy.atoms.elementwise.square import square
7474
from cvxpy.atoms.elementwise.xexp import xexp
75-
from cvxpy.atoms.elementwise.trig import sin, cos, tan
76-
from cvxpy.atoms.elementwise.hyperbolic import sinh, asinh, tanh, atanh
75+
# NLP atoms (require nlp=True solver) are accessible via cp.nlp namespace only.
76+
# e.g. cp.nlp.sin, cp.nlp.cos — not at the top-level cp namespace.
7777
from cvxpy.atoms.eye_minus_inv import eye_minus_inv, resolvent
7878
from cvxpy.atoms.gen_lambda_max import gen_lambda_max
7979
from cvxpy.atoms.geo_mean import GeoMean, GeoMeanApprox, geo_mean

cvxpy/nlp/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""
2+
cvxpy.nlp — Namespace for NLP (nonlinear programming) atoms.
3+
4+
These atoms require a solver that supports nlp=True (e.g. IPOPT, UNO).
5+
6+
Example usage:
7+
import cvxpy as cp
8+
x = cp.Variable()
9+
prob = cp.Problem(cp.Minimize(cp.nlp.sin(x)), [x >= 0])
10+
prob.solve(nlp=True)
11+
"""
12+
13+
from cvxpy.atoms.elementwise.trig import sin, cos, tan
14+
from cvxpy.atoms.elementwise.hyperbolic import sinh, tanh, asinh, atanh
15+
16+
__all__ = [
17+
"sin", "cos", "tan",
18+
"sinh", "tanh", "asinh", "atanh",
19+
]

cvxpy/tests/nlp_tests/test_hyperbolic.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class TestHyperbolic():
2727
def test_sinh(self):
2828
n = 10
2929
x = cp.Variable(n)
30-
prob = cp.Problem(cp.Minimize(cp.sum(cp.sinh(cp.logistic(x * 2)))),
30+
prob = cp.Problem(cp.Minimize(cp.sum(cp.nlp.sinh(cp.logistic(x * 2)))),
3131
[x >= 0.1, cp.sum(x) == 10])
3232
prob.solve(nlp=True, solver=cp.IPOPT)
3333
assert prob.status == cp.OPTIMAL
@@ -38,7 +38,7 @@ def test_sinh(self):
3838
def test_tanh(self):
3939
n = 10
4040
x = cp.Variable(n)
41-
prob = cp.Problem(cp.Minimize(cp.sum(cp.tanh(cp.logistic(x * 2)))),
41+
prob = cp.Problem(cp.Minimize(cp.sum(cp.nlp.tanh(cp.logistic(x * 2)))),
4242
[x >= 0.1, cp.sum(x) == 10])
4343
prob.solve(nlp=True, solver=cp.IPOPT)
4444
assert prob.status == cp.OPTIMAL
@@ -49,7 +49,7 @@ def test_tanh(self):
4949
def test_asinh(self):
5050
n = 10
5151
x = cp.Variable(n)
52-
prob = cp.Problem(cp.Minimize(cp.sum(cp.asinh(cp.logistic(x * 3)))),
52+
prob = cp.Problem(cp.Minimize(cp.sum(cp.nlp.asinh(cp.logistic(x * 3)))),
5353
[x >= 0.1, cp.sum(x) == 10])
5454
prob.solve(nlp=True, solver=cp.IPOPT)
5555
assert prob.status == cp.OPTIMAL
@@ -60,7 +60,7 @@ def test_asinh(self):
6060
def test_atanh(self):
6161
n = 10
6262
x = cp.Variable(n)
63-
prob = cp.Problem(cp.Minimize(cp.sum(cp.atanh(cp.logistic(x * 0.1)))),
63+
prob = cp.Problem(cp.Minimize(cp.sum(cp.nlp.atanh(cp.logistic(x * 0.1)))),
6464
[x >= 0.1, cp.sum(x) == 10])
6565
prob.solve(nlp=True, solver=cp.IPOPT)
6666
assert prob.status == cp.OPTIMAL

cvxpy/tests/nlp_tests/test_matmul.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def test_matmul_with_function_right(self):
6868
X = np.random.rand(m, n)
6969
Y = cp.Variable((n, p), bounds=[-2, 2], name='Y')
7070
Y.value = np.random.rand(n, p)
71-
obj = cp.sum(cp.matmul(X, cp.cos(Y)))
71+
obj = cp.sum(cp.matmul(X, cp.nlp.cos(Y)))
7272
problem = cp.Problem(cp.Minimize(obj))
7373

7474
problem.solve(solver=cp.IPOPT, nlp=True, hessian_approximation='exact',
@@ -84,7 +84,7 @@ def test_matmul_with_function_left(self):
8484
X = cp.Variable((m, n), bounds=[-2, 2], name='X')
8585
Y = np.random.rand(n, p)
8686
X.value = np.random.rand(m, n)
87-
obj = cp.sum(cp.matmul(cp.cos(X), Y))
87+
obj = cp.sum(cp.matmul(cp.nlp.cos(X), Y))
8888
problem = cp.Problem(cp.Minimize(obj))
8989

9090
problem.solve(solver=cp.IPOPT, nlp=True, hessian_approximation='exact',
@@ -101,7 +101,7 @@ def test_matmul_with_functions_both_sides(self):
101101
Y = cp.Variable((n, p), bounds=[-2, 2], name='Y')
102102
X.value = np.random.rand(m, n)
103103
Y.value = np.random.rand(n, p)
104-
obj = cp.sum(cp.matmul(cp.cos(X), cp.sin(Y)))
104+
obj = cp.sum(cp.matmul(cp.nlp.cos(X), cp.nlp.sin(Y)))
105105
problem = cp.Problem(cp.Minimize(obj))
106106

107107
problem.solve(solver=cp.IPOPT, nlp=True, hessian_approximation='exact',

cvxpy/tests/nlp_tests/test_nlp_solvers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -459,13 +459,13 @@ def test_clnlbeam(self, solver):
459459
u = cp.Variable(N+1)
460460
u.value = np.zeros(N+1)
461461
control_terms = cp.multiply(0.5 * h, cp.power(u[1:], 2) + cp.power(u[:-1], 2))
462-
trigonometric_terms = cp.multiply(0.5 * alpha * h, cp.cos(t[1:]) + cp.cos(t[:-1]))
462+
trigonometric_terms = cp.multiply(0.5 * alpha * h, cp.nlp.cos(t[1:]) + cp.nlp.cos(t[:-1]))
463463
objective_terms = cp.sum(control_terms + trigonometric_terms)
464464

465465
objective = cp.Minimize(objective_terms)
466466
constraints = []
467467
position_constraints = (x[1:] - x[:-1] -
468-
cp.multiply(0.5 * h, cp.sin(t[1:]) + cp.sin(t[:-1])) == 0)
468+
cp.multiply(0.5 * h, cp.nlp.sin(t[1:]) + cp.nlp.sin(t[:-1])) == 0)
469469
constraints.append(position_constraints)
470470
angle_constraint = (t[1:] - t[:-1] - 0.5 * h * (u[1:] + u[:-1]) == 0)
471471
constraints.append(angle_constraint)

cvxpy/tests/nlp_tests/test_power_flow.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def test_power_flow_dense_formulation(self):
9595
v = cp.Variable((N, 1), bounds=[v_min, v_max])
9696
p = cp.Variable(N, bounds=[p_min, p_max])
9797
q = cp.Variable(N, bounds=[q_min, q_max])
98-
C, S = cp.cos(theta - theta.T), cp.sin(theta - theta.T)
98+
C, S = cp.nlp.cos(theta - theta.T), cp.nlp.sin(theta - theta.T)
9999

100100
constr = [theta[0] == 0, p == cp.sum(P, axis=1), q == cp.sum(Q, axis=1),
101101
P == cp.multiply(v @ v.T, cp.multiply(G, C) + cp.multiply(B, S)),

cvxpy/tests/nlp_tests/test_scalar_and_matrix_problems.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -132,67 +132,67 @@ def test_power_fractional_matrix(self):
132132

133133
def test_scalar_trig(self):
134134
x = cp.Variable()
135-
prob = cp.Problem(cp.Minimize(cp.tan(x)), [x >= 0.1])
135+
prob = cp.Problem(cp.Minimize(cp.nlp.tan(x)), [x >= 0.1])
136136
prob.solve(nlp=True, solver=cp.IPOPT)
137137
assert prob.status == cp.OPTIMAL
138138
checker = DerivativeChecker(prob)
139139
checker.run_and_assert()
140140

141-
prob = cp.Problem(cp.Minimize(cp.sin(x)), [x >= 0.1])
141+
prob = cp.Problem(cp.Minimize(cp.nlp.sin(x)), [x >= 0.1])
142142
prob.solve(nlp=True, solver=cp.IPOPT)
143143
assert prob.status == cp.OPTIMAL
144144
checker = DerivativeChecker(prob)
145145
checker.run_and_assert()
146146

147-
prob = cp.Problem(cp.Minimize(cp.cos(x)), [x >= 0.1])
147+
prob = cp.Problem(cp.Minimize(cp.nlp.cos(x)), [x >= 0.1])
148148
prob.solve(nlp=True, solver=cp.IPOPT)
149149
assert prob.status == cp.OPTIMAL
150150
checker = DerivativeChecker(prob)
151151
checker.run_and_assert()
152152

153153
def test_matrix_trig(self):
154154
x = cp.Variable((3, 2))
155-
prob = cp.Problem(cp.Minimize(cp.sum(cp.tan(x))), [x >= 0.1])
155+
prob = cp.Problem(cp.Minimize(cp.sum(cp.nlp.tan(x))), [x >= 0.1])
156156
prob.solve(nlp=True, solver=cp.IPOPT)
157157
assert prob.status == cp.OPTIMAL
158158
checker = DerivativeChecker(prob)
159159
checker.run_and_assert()
160160

161-
prob = cp.Problem(cp.Minimize(cp.sum(cp.sin(x))), [x >= 0.1])
161+
prob = cp.Problem(cp.Minimize(cp.sum(cp.nlp.sin(x))), [x >= 0.1])
162162
prob.solve(nlp=True, solver=cp.IPOPT)
163163
assert prob.status == cp.OPTIMAL
164164
checker = DerivativeChecker(prob)
165165
checker.run_and_assert()
166166

167-
prob = cp.Problem(cp.Minimize(cp.sum(cp.cos(x))), [x >= 0.1])
167+
prob = cp.Problem(cp.Minimize(cp.sum(cp.nlp.cos(x))), [x >= 0.1])
168168
prob.solve(nlp=True, solver=cp.IPOPT)
169169
assert prob.status == cp.OPTIMAL
170170
checker = DerivativeChecker(prob)
171171
checker.run_and_assert()
172172

173173
def test_matrix_hyperbolic(self):
174174
x = cp.Variable((3, 2))
175-
prob = cp.Problem(cp.Minimize(cp.sum(cp.sinh(x))), [x >= 0.1])
175+
prob = cp.Problem(cp.Minimize(cp.sum(cp.nlp.sinh(x))), [x >= 0.1])
176176
prob.solve(nlp=True, solver=cp.IPOPT)
177177
assert prob.status == cp.OPTIMAL
178178
checker = DerivativeChecker(prob)
179179
checker.run_and_assert()
180180

181-
prob = cp.Problem(cp.Minimize(cp.sum(cp.tanh(x))), [x >= 0.1])
181+
prob = cp.Problem(cp.Minimize(cp.sum(cp.nlp.tanh(x))), [x >= 0.1])
182182
prob.solve(nlp=True, solver=cp.IPOPT)
183183
assert prob.status == cp.OPTIMAL
184184
checker = DerivativeChecker(prob)
185185
checker.run_and_assert()
186186

187187
def test_scalar_hyperbolic(self):
188188
x = cp.Variable()
189-
prob = cp.Problem(cp.Minimize(cp.sinh(x)), [x >= 0.1])
189+
prob = cp.Problem(cp.Minimize(cp.nlp.sinh(x)), [x >= 0.1])
190190
prob.solve(nlp=True, solver=cp.IPOPT)
191191
assert prob.status == cp.OPTIMAL
192192
checker = DerivativeChecker(prob)
193193
checker.run_and_assert()
194194

195-
prob = cp.Problem(cp.Minimize(cp.tanh(x)), [x >= 0.1])
195+
prob = cp.Problem(cp.Minimize(cp.nlp.tanh(x)), [x >= 0.1])
196196
prob.solve(nlp=True, solver=cp.IPOPT)
197197
assert prob.status == cp.OPTIMAL
198198
checker = DerivativeChecker(prob)

cvxpy/tests/test_nlp_namespace.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Tests for cp.nlp namespace."""
2+
import cvxpy as cp
3+
from cvxpy.atoms.elementwise.hyperbolic import tanh
4+
from cvxpy.atoms.elementwise.trig import cos, sin
5+
6+
7+
class TestNLPNamespace:
8+
def test_nlp_namespace_accessible(self):
9+
"""Test that cp.nlp submodule is accessible."""
10+
assert hasattr(cp, 'nlp')
11+
12+
def test_trig_atoms(self):
13+
x = cp.Variable()
14+
assert cp.nlp.sin(x) is not None
15+
assert cp.nlp.cos(x) is not None
16+
assert cp.nlp.tan(x) is not None
17+
18+
def test_hyperbolic_atoms(self):
19+
x = cp.Variable()
20+
assert cp.nlp.sinh(x) is not None
21+
assert cp.nlp.tanh(x) is not None
22+
assert cp.nlp.asinh(x) is not None
23+
assert cp.nlp.atanh(x) is not None
24+
25+
def test_atoms_not_in_top_level(self):
26+
"""NLP atoms should only be accessible via cp.nlp, not cp directly."""
27+
assert not hasattr(cp, 'sin')
28+
assert not hasattr(cp, 'cos')
29+
assert not hasattr(cp, 'tan')
30+
assert not hasattr(cp, 'sinh')
31+
assert not hasattr(cp, 'tanh')
32+
assert not hasattr(cp, 'asinh')
33+
assert not hasattr(cp, 'atanh')
34+
35+
def test_atoms_same_class_as_direct_import(self):
36+
"""cp.nlp.sin should be the same class as a direct import."""
37+
assert cp.nlp.sin is sin
38+
assert cp.nlp.cos is cos
39+
assert cp.nlp.tanh is tanh

doc/source/tutorial/dnlp/index.rst

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@ but in DNLP, ``multiply`` is smooth and can be used with two variable arguments.
149149
- depends on sign
150150

151151
In addition, DNLP introduces the following new smooth atoms that are neither convex
152-
nor concave. These atoms can only be used in DNLP problems.
152+
nor concave. These atoms can only be used in DNLP problems and are available in the
153+
``cp.nlp`` module.
153154

154155
.. list-table::
155156
:header-rows: 1
@@ -159,50 +160,52 @@ nor concave. These atoms can only be used in DNLP problems.
159160
- Domain
160161
- Monotonicity
161162

162-
* - sin(x)
163+
* - cp.nlp.sin(x)
163164

164165
- :math:`\sin(x)`
165166
- :math:`x \in \mathbf{R}`
166167
- none
167168

168-
* - cos(x)
169+
* - cp.nlp.cos(x)
169170

170171
- :math:`\cos(x)`
171172
- :math:`x \in \mathbf{R}`
172173
- none
173174

174-
* - tan(x)
175+
* - cp.nlp.tan(x)
175176

176177
- :math:`\tan(x)`
177178
- :math:`x \in (-\pi/2, \pi/2)`
178179
- none
179180

180-
* - sinh(x)
181+
* - cp.nlp.sinh(x)
181182

182183
- :math:`(e^x - e^{-x})/2`
183184
- :math:`x \in \mathbf{R}`
184185
- incr.
185186

186-
* - tanh(x)
187+
* - cp.nlp.tanh(x)
187188

188189
- :math:`(e^x - e^{-x})/(e^x + e^{-x})`
189190
- :math:`x \in \mathbf{R}`
190191
- incr.
191192

192-
* - asinh(x)
193+
* - cp.nlp.asinh(x)
193194

194195
- :math:`\ln(x + \sqrt{x^2 + 1})`
195196
- :math:`x \in \mathbf{R}`
196197
- incr.
197198

198-
* - atanh(x)
199+
* - cp.nlp.atanh(x)
199200

200201
- :math:`\frac{1}{2} \ln \frac{1+x}{1-x}`
201202
- :math:`x \in (-1, 1)`
202203
- incr.
203204

204-
* - sigmoid(x)
205+
..
206+
TODO: re-add the sigmoid row below once the ``sigmoid`` atom is implemented.
205207
208+
* - sigmoid(x)
206209
- :math:`\frac{1}{1 + e^{-x}}`
207210
- :math:`x \in \mathbf{R}`
208211
- incr.

0 commit comments

Comments
 (0)