Skip to content

Commit 87ec462

Browse files
authored
Merge pull request #2 from cvxgrp/expr2smooth
initial attempts at adding a smooth canon for maximum
2 parents a6bdefb + a530fba commit 87ec462

File tree

6 files changed

+215
-0
lines changed

6 files changed

+215
-0
lines changed

cvxpy/atoms/atom.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,9 @@ def is_atom_log_log_affine(self) -> bool:
154154
"""
155155
return self.is_atom_log_log_concave() and self.is_atom_log_log_convex()
156156

157+
def is_atom_smooth(self) -> bool:
158+
pass
159+
157160
@abc.abstractmethod
158161
def is_incr(self, idx) -> bool:
159162
"""Is the composition non-decreasing in argument idx?
@@ -210,6 +213,16 @@ def is_dpp(self, context='dcp') -> bool:
210213
else:
211214
raise ValueError('Unsupported context ', context)
212215

216+
def is_smooth(self) -> bool:
217+
"The expression is smooth"
218+
if self.is_constant():
219+
return True
220+
elif self.is_smooth_atom(self):
221+
for idx, arg in enumerate(self.args):
222+
if not arg.is_smooth():
223+
return False
224+
return True
225+
213226
@perf.compute_once
214227
def is_log_log_convex(self) -> bool:
215228
"""Is the expression log-log convex?

cvxpy/atoms/max.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ def is_atom_log_log_concave(self) -> bool:
104104
"""
105105
return False
106106

107+
def is_smooth(self):
108+
"""max is not a smooth function of its args"""
109+
return False
110+
107111
def is_incr(self, idx) -> bool:
108112
"""Is the composition non-decreasing in argument idx?
109113
"""
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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+
"""
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
from cvxpy.atoms import maximum
17+
from cvxpy.reductions.expr2smooth.canonicalizers.maximum_canon import maximum_canon
18+
19+
CANON_METHODS = {
20+
maximum : maximum_canon,
21+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
import operator
18+
from functools import reduce
19+
20+
from cvxpy.atoms.affine.wraps import nonneg_wrap, nonpos_wrap
21+
from cvxpy.expressions.variable import Variable
22+
23+
24+
def maximum_canon(expr, args):
25+
shape = expr.shape
26+
t = Variable(shape)
27+
28+
if expr.is_nonneg():
29+
t = nonneg_wrap(t)
30+
if expr.is_nonpos():
31+
t = nonpos_wrap(t)
32+
33+
constraints = [t >= elem for elem in args]
34+
terms = [t - elem for elem in args]
35+
constraints.append(reduce(operator.mul, terms) == 0)
36+
return t, constraints
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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 typing import Tuple
18+
19+
import cvxpy as cp
20+
from cvxpy import problems
21+
from cvxpy.expressions.expression import Expression
22+
from cvxpy.reductions.canonicalization import Canonicalization
23+
from cvxpy.reductions.expr2smooth.canonicalizers import CANON_METHODS as smooth_canon_methods
24+
from cvxpy.reductions.inverse_data import InverseData
25+
26+
27+
class Expr2smooth(Canonicalization):
28+
"""Reduce Expressions to an equivalent smooth program
29+
30+
This reduction takes as input (minimization) expressions and converts
31+
them into smooth expressions.
32+
"""
33+
def __init__(self, problem=None, quad_obj: bool = False) -> None:
34+
super(Canonicalization, self).__init__(problem=problem)
35+
self.smooth_canon_methods = smooth_canon_methods
36+
self.quad_obj = quad_obj
37+
38+
def accepts(self, problem):
39+
"""A problem is always accepted"""
40+
return True
41+
42+
def apply(self, problem):
43+
"""Converts an expr to a smooth program"""
44+
inverse_data = InverseData(problem)
45+
46+
# smoothen objective function
47+
canon_objective, canon_constraints = self.canonicalize_tree(
48+
problem.objective, True)
49+
50+
# smoothen constraints
51+
for constraint in problem.constraints:
52+
# canon_constr is the constraint re-expressed in terms of
53+
# its canonicalized arguments, and aux_constr are the constraints
54+
# generated while canonicalizing the arguments of the original
55+
# constraint
56+
canon_constr, aux_constr = self.canonicalize_tree(
57+
constraint, False)
58+
canon_constraints += aux_constr + [canon_constr]
59+
inverse_data.cons_id_map.update({constraint.id: canon_constr.id})
60+
61+
new_problem = problems.problem.Problem(canon_objective,
62+
canon_constraints)
63+
return new_problem, inverse_data
64+
65+
def canonicalize_tree(self, expr, affine_above: bool) -> Tuple[Expression, list]:
66+
"""Recursively canonicalize an Expression.
67+
68+
Parameters
69+
----------
70+
expr : The expression tree to canonicalize.
71+
affine_above : The path up to the root node is all affine atoms.
72+
73+
Returns
74+
-------
75+
A tuple of the canonicalized expression and generated constraints.
76+
"""
77+
# TODO don't copy affine expressions?
78+
affine_atom = type(expr) not in self.smooth_canon_methods
79+
canon_args = []
80+
constrs = []
81+
for arg in expr.args:
82+
canon_arg, c = self.canonicalize_tree(arg, affine_atom and affine_above)
83+
canon_args += [canon_arg]
84+
constrs += c
85+
canon_expr, c = self.canonicalize_expr(expr, canon_args, affine_above)
86+
constrs += c
87+
return canon_expr, constrs
88+
89+
def canonicalize_expr(self, expr, args, affine_above: bool) -> Tuple[Expression, list]:
90+
"""Canonicalize an expression, w.r.t. canonicalized arguments.
91+
92+
Parameters
93+
----------
94+
expr : The expression tree to canonicalize.
95+
args : The canonicalized arguments of expr.
96+
affine_above : The path up to the root node is all affine atoms.
97+
98+
Returns
99+
-------
100+
A tuple of the canonicalized expression and generated constraints.
101+
"""
102+
# Constant trees are collapsed, but parameter trees are preserved.
103+
if isinstance(expr, Expression) and (
104+
expr.is_constant() and not expr.parameters()):
105+
return expr, []
106+
107+
if type(expr) in self.smooth_canon_methods:
108+
return self.smooth_canon_methods[type(expr)](expr, args)
109+
110+
return expr.copy(args), []
111+
112+
def example_max():
113+
# Define variables
114+
x = cp.Variable(1)
115+
y = cp.Variable(1)
116+
117+
objective = cp.Minimize(-cp.maximum(x,y))
118+
119+
constraints = [x - 14 == 0, y - 6 == 0]
120+
121+
problem = cp.Problem(objective, constraints)
122+
return problem
123+
124+
prob = example_max()
125+
new_problem, inverse = Expr2smooth(prob).apply(prob)
126+
print(new_problem)

0 commit comments

Comments
 (0)