Coverage for picos/constraints/con_sumexp.py: 95.60%
91 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-15 14:21 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-15 14:21 +0000
1# ------------------------------------------------------------------------------
2# Copyright (C) 2018-2019 Maximilian Stahlberg
3#
4# This file is part of PICOS.
5#
6# PICOS is free software: you can redistribute it and/or modify it under the
7# terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# PICOS is distributed in the hope that it will be useful, but WITHOUT ANY
12# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
13# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License along with
16# this program. If not, see <http://www.gnu.org/licenses/>.
17# ------------------------------------------------------------------------------
19"""Implementation of :class:`SumExponentialsConstraint`."""
21import math
22from collections import namedtuple
24from .. import glyphs
25from ..apidoc import api_end, api_start
26from ..caching import cached_property
27from .constraint import Constraint, ConstraintConversion
29_API_START = api_start(globals())
30# -------------------------------
33class SumExponentialsConstraint(Constraint):
34 """Upper bound on a sum of exponentials."""
36 class ConicConversion(ConstraintConversion):
37 """Sum of exponentials to exponential cone constraint conversion."""
39 @classmethod
40 def predict(cls, subtype, options):
41 """Implement :meth:`~.constraint.ConstraintConversion.predict`."""
42 from ..expressions import RealVariable
43 from . import AffineConstraint, ExpConeConstraint
45 n = subtype.argdim
47 yield ("var", RealVariable.make_var_type(dim=n, bnd=0), 1)
48 yield ("con", AffineConstraint.make_type(dim=1, eq=False), 1)
49 yield ("con", ExpConeConstraint.make_type(), n)
51 @classmethod
52 def convert(cls, con, options):
53 """Implement :meth:`~.constraint.ConstraintConversion.convert`."""
54 from ..expressions import ExponentialCone
55 from ..modeling import Problem
57 x = con.numerator
58 y = con.denominator
59 n = con.theSum.n
60 b = con.upperBound
62 P = Problem()
64 u = P.add_variable("__u", n)
65 P.add_constraint((u | 1) <= b)
67 for i in range(n):
68 P.add_constraint((u[i] // y[i] // x[i]) << ExponentialCone())
70 return P
72 class LogSumExpConversion(ConstraintConversion):
73 """Sum of exponentials to logarithm of the sum constraint conversion."""
75 @classmethod
76 def predict(cls, subtype, options):
77 """Implement :meth:`~.constraint.ConstraintConversion.predict`."""
78 from . import LogSumExpConstraint
80 n = subtype.argdim
82 if subtype.lse_representable:
83 yield ("con", LogSumExpConstraint.make_type(argdim=n), 1)
84 else:
85 # HACK: Return the input constraint type.
86 # TODO: Handle partial subtype support differently, e.g. by
87 # introducing ConstraintConversion.supports.
88 yield ("con", SumExponentialsConstraint.make_type(*subtype), 1)
90 @classmethod
91 def convert(cls, con, options):
92 """Implement :meth:`~.constraint.ConstraintConversion.convert`."""
93 from ..expressions import LogSumExp
94 from ..modeling import Problem
96 x = con.numerator
97 b = con.upperBound
99 P = Problem()
101 if con.lse_representable:
102 P.add_constraint(LogSumExp(x) <= math.log(b.value))
103 else:
104 # HACK: See predict.
105 P.add_constraint(con)
107 return P
109 def __init__(self, theSum, upperBound):
110 """Construct a :class:`SumExponentialsConstraint`.
112 :param ~picos.expressions.SumExponentials theSum:
113 Constrained expression.
114 :param ~picos.expressions.AffineExpression upperBound:
115 Upper bound on the expression.
116 """
117 from ..expressions import AffineExpression, SumExponentials
119 assert isinstance(theSum, SumExponentials)
120 assert isinstance(upperBound, AffineExpression)
121 assert len(upperBound) == 1
123 self.theSum = theSum
124 self.upperBound = upperBound
126 super(SumExponentialsConstraint, self).__init__(theSum._typeStr)
128 @property
129 def numerator(self):
130 """The :math:`x` of the sum."""
131 return self.theSum.x
133 @cached_property
134 def denominator(self):
135 """The :math:`y` of the sum, or :math:`1`."""
136 if self.theSum.y is None:
137 from ..expressions import AffineExpression
138 return AffineExpression.from_constant(1, self.theSum.x.shape)
139 else:
140 return self.theSum.y
142 @property
143 def lse_representable(self):
144 """Whether this can be converted to a logarithmic constraint."""
145 if self.theSum.y is not None:
146 return False
148 if not self.upperBound.constant:
149 return False
151 if self.upperBound.value < 0:
152 return False
154 return True
156 Subtype = namedtuple("Subtype", ("argdim", "lse_representable"))
158 def _subtype(self):
159 return self.Subtype(self.theSum.n, self.lse_representable)
161 @classmethod
162 def _cost(cls, subtype):
163 # NOTE: Twice the argument dimension due to the denominator.
164 return 2*subtype.argdim + 1
166 def _expression_names(self):
167 yield "theSum"
168 yield "upperBound"
170 def _str(self):
171 return glyphs.le(self.theSum.string, self.upperBound.string)
173 def _get_slack(self):
174 return self.upperBound.safe_value - self.theSum.safe_value
177# --------------------------------------
178__all__ = api_end(_API_START, globals())