Skip to content

Commit 706e17d

Browse files
committed
clean up
1 parent e6221a8 commit 706e17d

3 files changed

Lines changed: 110 additions & 43 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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+
import numpy as np
17+
18+
from cvxpy.expressions.variable import Variable
19+
20+
"""Common DNLP canonicalizers for smooth atoms. There are four
21+
categories of canonicalizers, grouped by the domain type of the atom:
22+
23+
- Full domain: Atoms like exp, sin, cos that have full domain.
24+
Two variants exist depending on whether the atom's chain rule is
25+
implemented in the differentiation engine. Eventually, all atoms
26+
in this category should use the chain rule variant and the non-chain-rule
27+
variant can be removed.
28+
- Nonnegative domain: Atoms like log and entr that require nonnegative inputs.
29+
- Bounded domain: Atoms like tan and atanh whose domain is an interval.
30+
31+
All canon functions follow the standard signature
32+
33+
def canon(expr, args) -> (new_expr, constraints)
34+
35+
where args are the (already-canonicalized) arguments of expr.
36+
"""
37+
38+
MIN_INIT_NONNEG_CANON = 1e-3
39+
40+
41+
def smooth_full_domain_canon_non_chain_rule(expr, args):
42+
"""Canonicalize a smooth atom with full domain (and one argument)
43+
whose chain rule is not implemented in the differentiation engine."""
44+
if isinstance(args[0], Variable):
45+
return expr.copy([args[0]]), []
46+
t = Variable(args[0].shape)
47+
if args[0].value is not None:
48+
t.value = args[0].value
49+
return expr.copy([t]), [t == args[0]]
50+
51+
def smooth_full_domain_canon_chain_rule(expr, args):
52+
"""Canonicalize a smooth atom with full domain (and potentially multiple
53+
arguments) """
54+
return expr.copy(args), []
55+
56+
def smooth_nonnegative_dom_canon(expr, args):
57+
"""Canonicalize a smooth atom (with one argument) whose domain is the
58+
nonnegative reals."""
59+
t = Variable(args[0].shape, nonneg=True)
60+
if args[0].value is not None:
61+
t.value = np.maximum(args[0].value, MIN_INIT_NONNEG_CANON)
62+
return expr.copy([t]), [t == args[0]]
63+
64+
def make_smooth_range_dom_canon(lower, upper):
65+
"""Wrapper for canonicalizers whose domain is a bounded interval."""
66+
def smooth_range_dom_canon(expr, args):
67+
t = Variable(args[0].shape, bounds=[lower, upper])
68+
# If the initial value is less than 10% of the total interval length from
69+
# one of the bounds, we initialize at the midpoint of the interval instead.
70+
margin = 0.1 * (upper - lower)
71+
midpoint = (lower + upper) / 2.0
72+
if args[0].value is not None:
73+
safe_idxs = (args[0].value >= lower + margin) & (args[0].value <= upper - margin)
74+
trimmed_value = np.where(safe_idxs, args[0].value, midpoint)
75+
t.value = trimmed_value
76+
return expr.copy([t]), [t == args[0]]
77+
return smooth_range_dom_canon
78+
79+
# atoms with domain equal to the nonnegative reals
80+
entr_canon = smooth_nonnegative_dom_canon
81+
log_canon = smooth_nonnegative_dom_canon
82+
83+
# atoms that do not yet have chain rule implemented in diff engine
84+
prod_canon = smooth_full_domain_canon_non_chain_rule
85+
86+
# elementwise atoms with chain rule implemented in diff engine
87+
exp_canon = smooth_full_domain_canon_chain_rule
88+
sin_canon = smooth_full_domain_canon_chain_rule
89+
cos_canon = smooth_full_domain_canon_chain_rule
90+
sinh_canon = smooth_full_domain_canon_chain_rule
91+
tanh_canon = smooth_full_domain_canon_chain_rule
92+
asinh_canon = smooth_full_domain_canon_chain_rule
93+
logistic_canon = smooth_full_domain_canon_chain_rule
94+
normcdf_canon = smooth_full_domain_canon_chain_rule
95+
96+
# other atoms with chain rule implemented in diff engine
97+
multiply_canon = smooth_full_domain_canon_chain_rule
98+
matmul_canon = smooth_full_domain_canon_chain_rule
99+
quad_form_canon = smooth_full_domain_canon_chain_rule
100+
101+
102+
# atoms with domain equal to a bounded interval
103+
atanh_canon = make_smooth_range_dom_canon(-1, 1)
104+
tan_canon = make_smooth_range_dom_canon(-3.14159/2, 3.14159/2)
105+
106+
107+

cvxpy/reductions/dnlp2smooth/canonicalizers/div_canon.py

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -41,40 +41,3 @@ def div_canon(expr, args):
4141

4242
# TODO (dance858): should. multiply and power be other canons?
4343
return multiply(args[0], power(z, -1)), [z == args[1]]
44-
45-
46-
# ----------------------------------------------------------------------------------
47-
# Old version
48-
# ----------------------------------------------------------------------------------
49-
# We canonicalize div(f(x), g(x)) as z * y = f(x), y = g(x), y >= 0.
50-
# In other words, it assumes that the denominator is nonnegative.
51-
#def div_canon_old(expr, args):
52-
#
53-
# # raise an error if the denominator is not nonnegative
54-
# if not args[1].is_nonneg():
55-
# raise ValueError("The denominator of a division must be nonnegative. "
56-
# "Did you forget to specify bounds?")
57-
#
58-
# dim = args[0].shape
59-
# sgn_z = args[0].sign
60-
#
61-
# if sgn_z == 'NONNEGATIVE':
62-
# z = Variable(dim, nonneg=True)
63-
# elif sgn_z == 'NONPOSITIVE':
64-
# z = Variable(dim, nonpos=True)
65-
# else:
66-
# z = Variable(dim)
67-
#
68-
# y = Variable(args[1].shape, nonneg=True)
69-
#
70-
# if args[0].value is not None and args[1].value is not None:
71-
# y.value = np.maximum(args[1].value, MIN_INIT)
72-
# val = args[0].value / y.value
73-
#
74-
# # dimension hack
75-
# if dim == () and val.shape == (1,):
76-
# z.value = val[0]
77-
# else:
78-
# z.value = val
79-
#
80-
# return z, [multiply(z, y) == args[0], y == args[1]]

cvxpy/reductions/dnlp2smooth/canonicalizers/power_canon.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,22 @@
2020
from cvxpy.expressions.constants import Constant
2121
from cvxpy.expressions.variable import Variable
2222

23-
MIN_INIT = 1e-4
23+
MIN_INIT = 1e-3
2424

2525
def power_canon(expr, args):
2626
x = args[0]
2727
p = expr.p_used
2828
shape = expr.shape
29-
ones = Constant(np.ones(shape))
3029
if p == 0:
31-
return ones, []
30+
return Constant(np.ones(shape)), []
3231
elif p == 1:
3332
return x, []
3433
elif isinstance(p, int) and p > 1:
35-
return expr.copy([x]), []
34+
return expr.copy(args), []
3635
elif p > 0:
3736
t = Variable(shape, nonneg=True)
38-
3937
if x.value is not None:
4038
t.value = np.maximum(x.value, MIN_INIT)
41-
4239
return expr.copy([t]), [t == x]
4340
else:
4441
raise NotImplementedError(f'The power {p} is not yet supported.')

0 commit comments

Comments
 (0)