Coverage for picos/constraints/con_sumexp.py: 95.60%

91 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-03-26 07:46 +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# ------------------------------------------------------------------------------ 

18 

19"""Implementation of :class:`SumExponentialsConstraint`.""" 

20 

21import math 

22from collections import namedtuple 

23 

24from .. import glyphs 

25from ..apidoc import api_end, api_start 

26from ..caching import cached_property 

27from .constraint import Constraint, ConstraintConversion 

28 

29_API_START = api_start(globals()) 

30# ------------------------------- 

31 

32 

33class SumExponentialsConstraint(Constraint): 

34 """Upper bound on a sum of exponentials.""" 

35 

36 class ConicConversion(ConstraintConversion): 

37 """Sum of exponentials to exponential cone constraint conversion.""" 

38 

39 @classmethod 

40 def predict(cls, subtype, options): 

41 """Implement :meth:`~.constraint.ConstraintConversion.predict`.""" 

42 from ..expressions import RealVariable 

43 from . import AffineConstraint, ExpConeConstraint 

44 

45 n = subtype.argdim 

46 

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) 

50 

51 @classmethod 

52 def convert(cls, con, options): 

53 """Implement :meth:`~.constraint.ConstraintConversion.convert`.""" 

54 from ..expressions import ExponentialCone 

55 from ..modeling import Problem 

56 

57 x = con.numerator 

58 y = con.denominator 

59 n = con.theSum.n 

60 b = con.upperBound 

61 

62 P = Problem() 

63 

64 u = P.add_variable("__u", n) 

65 P.add_constraint((u | 1) <= b) 

66 

67 for i in range(n): 

68 P.add_constraint((u[i] // y[i] // x[i]) << ExponentialCone()) 

69 

70 return P 

71 

72 class LogSumExpConversion(ConstraintConversion): 

73 """Sum of exponentials to logarithm of the sum constraint conversion.""" 

74 

75 @classmethod 

76 def predict(cls, subtype, options): 

77 """Implement :meth:`~.constraint.ConstraintConversion.predict`.""" 

78 from . import LogSumExpConstraint 

79 

80 n = subtype.argdim 

81 

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) 

89 

90 @classmethod 

91 def convert(cls, con, options): 

92 """Implement :meth:`~.constraint.ConstraintConversion.convert`.""" 

93 from ..expressions import LogSumExp 

94 from ..modeling import Problem 

95 

96 x = con.numerator 

97 b = con.upperBound 

98 

99 P = Problem() 

100 

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) 

106 

107 return P 

108 

109 def __init__(self, theSum, upperBound): 

110 """Construct a :class:`SumExponentialsConstraint`. 

111 

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 

118 

119 assert isinstance(theSum, SumExponentials) 

120 assert isinstance(upperBound, AffineExpression) 

121 assert len(upperBound) == 1 

122 

123 self.theSum = theSum 

124 self.upperBound = upperBound 

125 

126 super(SumExponentialsConstraint, self).__init__(theSum._typeStr) 

127 

128 @property 

129 def numerator(self): 

130 """The :math:`x` of the sum.""" 

131 return self.theSum.x 

132 

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 

141 

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 

147 

148 if not self.upperBound.constant: 

149 return False 

150 

151 if self.upperBound.value < 0: 

152 return False 

153 

154 return True 

155 

156 Subtype = namedtuple("Subtype", ("argdim", "lse_representable")) 

157 

158 def _subtype(self): 

159 return self.Subtype(self.theSum.n, self.lse_representable) 

160 

161 @classmethod 

162 def _cost(cls, subtype): 

163 # NOTE: Twice the argument dimension due to the denominator. 

164 return 2*subtype.argdim + 1 

165 

166 def _expression_names(self): 

167 yield "theSum" 

168 yield "upperBound" 

169 

170 def _str(self): 

171 return glyphs.le(self.theSum.string, self.upperBound.string) 

172 

173 def _get_slack(self): 

174 return self.upperBound.safe_value - self.theSum.safe_value 

175 

176 

177# -------------------------------------- 

178__all__ = api_end(_API_START, globals())