|
| 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 | + |
0 commit comments