Skip to content

Commit d58baf8

Browse files
authored
Smith form affine (#13)
* adding cleaning up of torch * adding jacobian structure and jacobian only value computation in interface * making analytic center of ellipsoid work * much more progress on everything, sparse jacobians and smith form --------- Co-authored-by: William Zijie Zhang <[email protected]>
1 parent 7f44f3e commit d58baf8

File tree

14 files changed

+202
-306
lines changed

14 files changed

+202
-306
lines changed

cvxpy/atoms/affine/binary_operators.py

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
class BinaryOperator(AffAtom):
4242
"""
4343
Base class for expressions involving binary operators. (other than addition)
44-
4544
"""
4645

4746
OP_NAME = 'BINARY_OP'
@@ -172,37 +171,40 @@ def is_decr(self, idx) -> bool:
172171
return self.args[1-idx].is_nonpos()
173172

174173
def _grad(self, values):
175-
"""Gives the (sub/super)gradient of the atom w.r.t. each argument.
176-
177-
Matrix expressions are vectorized, so the gradient is a matrix.
178-
174+
"""Compute the gradient of matrix multiplication w.r.t. each argument.
175+
176+
For Z = X @ Y, this computes the Jacobian matrices:
177+
- ∂vec(Z)/∂vec(X) = Y.T ⊗ I_m where X is (m, n)
178+
- ∂vec(Z)/∂vec(Y) = I_p ⊗ X where Y is (n, p)
179+
179180
Args:
180-
values: A list of numeric values for the arguments.
181-
181+
values: A list of numeric values for the arguments [X, Y].
182+
182183
Returns:
183-
A list of SciPy CSC sparse matrices or None.
184+
A list of SciPy CSC sparse matrices [DX, DY].
184185
"""
186+
# Handle constant cases
185187
if self.args[0].is_constant() or self.args[1].is_constant():
186188
return super(MulExpression, self)._grad(values)
187-
188-
# TODO(akshayka): Verify that the following code is correct for
189-
# non-affine arguments.
189+
190190
X = values[0]
191191
Y = values[1]
192-
193-
DX_rows = self.args[0].size
194-
cols = self.args[0].size
195-
196-
# DX = [diag(Y11), diag(Y12), ...]
197-
# [diag(Y21), diag(Y22), ...]
198-
# [ ... ... ...]
199-
DX = sp.dok_array((DX_rows, cols))
200-
for k in range(self.args[0].shape[0]):
201-
DX[k::self.args[0].shape[0], k::self.args[0].shape[0]] = Y
202-
DX = sp.csc_array(DX)
203-
cols = 1 if len(self.args[1].shape) == 1 else self.args[1].shape[1]
204-
DY = sp.block_diag([X.T for k in range(cols)], 'csc')
205-
192+
193+
# Get dimensions
194+
m, n = self.args[0].shape if len(self.args[0].shape) == 2 else (self.args[0].size, 1)
195+
n2, p = self.args[1].shape if len(self.args[1].shape) == 2 else (self.args[1].size, 1)
196+
197+
# Verify dimension compatibility
198+
assert n == n2, f"Inner dimensions must match for multiplication: {n} != {n2}"
199+
200+
# Compute ∂vec(Z)/∂vec(X) = Y.T ⊗ I_m
201+
# This is a (m*p) × (m*n) matrix
202+
DX = sp.kron(Y.T, sp.eye(m, format='csc'), format='csc')
203+
204+
# Compute ∂vec(Z)/∂vec(Y) = I_p ⊗ X
205+
# This is a (m*p) × (n*p) matrix
206+
DY = sp.kron(sp.eye(p, format='csc'), X, format='csc')
207+
206208
return [DX, DY]
207209

208210
def graph_implementation(

cvxpy/atoms/atom.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,10 @@ def _domain(self) -> List['Constraint']:
465465
"""
466466
# Default is no constraints.
467467
return []
468+
469+
def point_in_domain(self) -> np.ndarray:
470+
"""default point in domain of zero"""
471+
return np.zeros(self.shape)
468472

469473
@staticmethod
470474
def numpy_numeric(numeric_func):

cvxpy/atoms/elementwise/log.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,8 @@ def _domain(self) -> List[Constraint]:
9595
"""Returns constraints describing the domain of the node.
9696
"""
9797
return [self.args[0] >= 0]
98+
99+
def point_in_domain(self) -> np.ndarray:
100+
"""Returns a point in the domain of the node.
101+
"""
102+
return np.ones(self.size)

cvxpy/atoms/elementwise/trig.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,13 @@ def _domain(self) -> List[Constraint]:
7575
"""
7676
return []
7777

78-
def _grad(self) -> List[Constraint]:
78+
def _grad(self, values) -> List[Constraint]:
7979
"""Returns the gradient of the node.
8080
"""
81-
return []
81+
rows = self.args[0].size
82+
cols = self.size
83+
grad_vals = np.cos(values[0])
84+
return [sin.elemwise_grad_to_diag(grad_vals, rows, cols)]
8285

8386

8487
class cos(Elementwise):
@@ -135,10 +138,13 @@ def _domain(self) -> List[Constraint]:
135138
"""
136139
return []
137140

138-
def _grad(self) -> List[Constraint]:
141+
def _grad(self, values) -> List[Constraint]:
139142
"""Returns the gradient of the node.
140143
"""
141-
return []
144+
rows = self.args[0].size
145+
cols = self.size
146+
grad_vals = -np.sin(values[0])
147+
return [cos.elemwise_grad_to_diag(grad_vals, rows, cols)]
142148

143149

144150
class tan(Elementwise):
@@ -195,8 +201,10 @@ def _domain(self) -> List[Constraint]:
195201
"""
196202
return []
197203

198-
def _grad(self) -> List[Constraint]:
204+
def _grad(self, values) -> List[Constraint]:
199205
"""Returns the gradient of the node.
200206
"""
201-
return []
202-
207+
rows = self.args[0].size
208+
cols = self.size
209+
grad_vals = 1/np.cos(values[0])**2
210+
return [tan.elemwise_grad_to_diag(grad_vals, rows, cols)]

cvxpy/reductions/expr2smooth/canonicalizers/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
limitations under the License.
1515
"""
1616
from cvxpy.atoms import maximum
17+
from cvxpy.atoms.elementwise.log import log
1718
from cvxpy.atoms.elementwise.minimum import minimum
1819
from cvxpy.atoms.elementwise.power import power
1920
from cvxpy.atoms.pnorm import Pnorm
2021
from cvxpy.atoms.elementwise.abs import abs
22+
from cvxpy.reductions.expr2smooth.canonicalizers.log_canon import log_canon
2123
from cvxpy.reductions.expr2smooth.canonicalizers.minimum_canon import minimum_canon
2224
from cvxpy.reductions.expr2smooth.canonicalizers.abs_canon import abs_canon
2325
from cvxpy.reductions.expr2smooth.canonicalizers.pnorm_canon import pnorm_canon
@@ -28,8 +30,8 @@
2830
abs: abs_canon,
2931
maximum : maximum_canon,
3032
minimum: minimum_canon,
31-
# log: log_canon,
33+
log: log_canon,
3234
power: power_canon,
33-
#Pnorm : pnorm_canon,
35+
Pnorm : pnorm_canon,
3436
# inv: inv_pos_canon,
3537
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""
2+
Copyright 2025 CVXPY developers
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
"""
16+
17+
from cvxpy.expressions.variable import Variable
18+
19+
20+
def log_canon(expr, args):
21+
t = Variable(args[0].size)
22+
t.value = expr.point_in_domain()
23+
return expr.copy([t]), [t==args[0]]

cvxpy/reductions/expr2smooth/canonicalizers/power_canon.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,11 @@ def power_canon(expr, args):
3939
t.value = np.power(np.abs(x.value), p)
4040
return t, [t**(1/p) == x, t >= 0]
4141
elif p > 1:
42-
return x**p, []
43-
else: # p < 0
44-
raise ValueError(
45-
"Power canonicalization does not support negative powers."
46-
)
42+
t = Variable(args[0].shape)
43+
if args[0].value is not None:
44+
t.value = np.power(args[0].value, p)
45+
else:
46+
t.value = expr.point_in_domain()
47+
return expr.copy([t]), [t==args[0]]
48+
else:
49+
pass

cvxpy/reductions/expr2smooth/expr2smooth.py

Lines changed: 8 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from cvxpy import problems
2020
from cvxpy.expressions.expression import Expression
21+
from cvxpy.expressions.variable import Variable
2122
from cvxpy.problems.objective import Minimize
2223
from cvxpy.reductions.canonicalization import Canonicalization
2324
from cvxpy.reductions.expr2smooth.canonicalizers import CANON_METHODS as smooth_canon_methods
@@ -76,7 +77,6 @@ def canonicalize_tree(self, expr, affine_above: bool) -> Tuple[Expression, list]
7677
-------
7778
A tuple of the canonicalized expression and generated constraints.
7879
"""
79-
# TODO don't copy affine expressions?
8080
affine_atom = type(expr) not in self.smooth_canon_methods
8181
canon_args = []
8282
constrs = []
@@ -108,56 +108,11 @@ def canonicalize_expr(self, expr, args, affine_above: bool) -> Tuple[Expression,
108108

109109
if type(expr) in self.smooth_canon_methods:
110110
return self.smooth_canon_methods[type(expr)](expr, args)
111-
111+
"""
112+
elif hasattr(expr, "curvature") and expr.curvature == "AFFINE":
113+
if type(expr) is Variable:
114+
return expr, []
115+
t = Variable(expr.shape)
116+
return t, [t == expr.copy(args)]
117+
"""
112118
return expr.copy(args), []
113-
114-
"""
115-
def example_max():
116-
# Define variables
117-
x = cp.Variable(1)
118-
y = cp.Variable(1)
119-
120-
objective = cp.Minimize(-cp.maximum(x,y))
121-
122-
constraints = [x - 14 == 0, y - 6 == 0]
123-
124-
problem = cp.Problem(objective, constraints)
125-
return problem
126-
127-
def example_sqrt():
128-
# Define variables
129-
x = cp.Variable(1)
130-
131-
objective = cp.Minimize(cp.sqrt(x))
132-
133-
constraints = [x - 4 == 0]
134-
135-
problem = cp.Problem(objective, constraints)
136-
return problem
137-
138-
def example_pnorm_even():
139-
# Define variables
140-
x = cp.Variable(2)
141-
142-
objective = cp.Minimize(cp.pnorm(x, p=2))
143-
144-
constraints = [x[0] - 3 == 0, x[1] - 4 == 0]
145-
146-
problem = cp.Problem(objective, constraints)
147-
return problem
148-
149-
def example_pnorm_odd():
150-
# Define variables
151-
x = cp.Variable(2)
152-
153-
objective = cp.Minimize(cp.pnorm(x, p=3))
154-
155-
constraints = [x[0] - 3 == 0, x[1] - 4 == 0]
156-
157-
problem = cp.Problem(objective, constraints)
158-
return problem
159-
160-
prob = example_sqrt()
161-
new_problem, inverse = Expr2smooth(prob).apply(prob)
162-
print(new_problem)
163-
"""

0 commit comments

Comments
 (0)