Coverage for picos/constraints/con_geomean.py: 97.80%

91 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-03-26 07:46 +0000

1# ------------------------------------------------------------------------------ 

2# Copyright (C) 2012-2017 Guillaume Sagnol 

3# Copyright (C) 2018-2019 Maximilian Stahlberg 

4# 

5# This file is part of PICOS. 

6# 

7# PICOS is free software: you can redistribute it and/or modify it under the 

8# terms of the GNU General Public License as published by the Free Software 

9# Foundation, either version 3 of the License, or (at your option) any later 

10# version. 

11# 

12# PICOS is distributed in the hope that it will be useful, but WITHOUT ANY 

13# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 

14# A PARTICULAR PURPOSE. See the GNU General Public License for more details. 

15# 

16# You should have received a copy of the GNU General Public License along with 

17# this program. If not, see <http://www.gnu.org/licenses/>. 

18# ------------------------------------------------------------------------------ 

19 

20"""Implementation of :class:`GeometricMeanConstraint`.""" 

21 

22import math 

23from collections import namedtuple 

24 

25from .. import glyphs 

26from ..apidoc import api_end, api_start 

27from .constraint import Constraint, ConstraintConversion 

28 

29_API_START = api_start(globals()) 

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

31 

32 

33class GeometricMeanConstraint(Constraint): 

34 """Lower bound on a geometric mean.""" 

35 

36 class RSOCConversion(ConstraintConversion): 

37 """Geometric mean to rotated second order 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 RSOCConstraint 

44 

45 j = subtype.argdim - 1 

46 k = int(math.log(j, 2)) 

47 l = j + k - "{:b}".format(j - 2**k).count("1") 

48 

49 yield ("var", RealVariable.make_var_type(dim=1, bnd=0), l - 1) 

50 yield ("con", RSOCConstraint.make_type(argdim=1), l) 

51 

52 # TODO: Refactor to be human readable. 

53 @classmethod 

54 def convert(cls, con, options): 

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

56 from ..expressions.algebra import rsoc 

57 from ..modeling import Problem 

58 

59 geoMean = con.geoMean 

60 lowerBound = con.lowerBound 

61 

62 P = Problem() 

63 x = geoMean.x 

64 m = len(x) 

65 lm = [[i] for i in range(m - 1, -1, -1)] 

66 K = [] 

67 depth = 0 

68 u = {} 

69 

70 while len(lm) > 1: 

71 depth += 1 

72 nlm = [] 

73 while lm: 

74 i1 = lm.pop()[-1] 

75 if lm: 

76 i2 = lm.pop()[0] 

77 else: 

78 i2 = 'x' 

79 nlm.insert(0, (i2, i1)) 

80 k = str(depth) + ':' + str(i1) + '-' + str(i2) 

81 K.append(k) 

82 u[k] = P.add_variable('__u[' + k + ']', 1) 

83 lm = nlm 

84 

85 root = K[-1] 

86 maxd = int(K[-1].split(':')[0]) 

87 P.remove_variable(u[root].name) # TODO: Update for new expressions. 

88 u[root] = lowerBound 

89 

90 for k in K: 

91 i1 = int(k.split('-')[0].split(':')[1]) 

92 i2 = k.split('-')[1] 

93 if i2 != 'x': 

94 i2 = int(i2) 

95 if k[:2] == '1:': 

96 if i2 != 'x': 

97 P.add_constraint((x[i1] & x[i2] & u[k]) << rsoc()) 

98 else: 

99 P.add_constraint((x[i1] & lowerBound & u[k]) << rsoc()) 

100 else: 

101 d = int(k.split(':')[0]) 

102 if i2 == 'x' and d < maxd: 

103 k2pot = [ki for ki in K 

104 if ki.startswith(str(d - 1) + ':') 

105 and int(ki.split(':')[1].split('-')[0]) >= i1] 

106 k1 = k2pot[0] 

107 if len(k2pot) == 2: 

108 k2 = k2pot[1] 

109 P.add_constraint((u[k1] & u[k2] & u[k]) << rsoc()) 

110 else: 

111 P.add_constraint( 

112 (u[k1] & lowerBound & u[k]) << rsoc()) 

113 else: 

114 k1 = [ki for ki in K 

115 if ki.startswith(str(d - 1) + ':' + str(i1))][0] 

116 k2 = [ki for ki in K if ki.startswith(str(d - 1) + ':') 

117 and ki.endswith('-' + str(i2))][0] 

118 P.add_constraint((u[k1] & u[k2] & u[k]) << rsoc()) 

119 

120 return P 

121 

122 def __init__(self, geoMean, lowerBound): 

123 """Construct a :class:`GeometricMeanConstraint`. 

124 

125 :param ~picos.expressions.GeometricMean lhs: 

126 Constrained expression. 

127 :param ~picos.expressions.AffineExpression lowerBound: 

128 Lower bound on the expression. 

129 """ 

130 from ..expressions import AffineExpression, GeometricMean 

131 

132 assert isinstance(geoMean, GeometricMean) 

133 assert isinstance(lowerBound, AffineExpression) 

134 assert len(lowerBound) == 1 

135 

136 self.geoMean = geoMean 

137 self.lowerBound = lowerBound 

138 

139 super(GeometricMeanConstraint, self).__init__(geoMean._typeStr) 

140 

141 Subtype = namedtuple("Subtype", ("argdim",)) 

142 

143 def _subtype(self): 

144 return self.Subtype(len(self.geoMean.x)) 

145 

146 @classmethod 

147 def _cost(cls, subtype): 

148 return subtype.argdim + 1 

149 

150 def _expression_names(self): 

151 yield "geoMean" 

152 yield "lowerBound" 

153 

154 def _str(self): 

155 return glyphs.ge(self.geoMean.string, self.lowerBound.string) 

156 

157 def _get_slack(self): 

158 return self.geoMean.safe_value - self.lowerBound.safe_value 

159 

160 

161# -------------------------------------- 

162__all__ = api_end(_API_START, globals())