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

91 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-12 07:53 +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 import RealVariable 

57 from ..expressions.algebra import rsoc 

58 from ..modeling import Problem 

59 

60 geoMean = con.geoMean 

61 lowerBound = con.lowerBound 

62 

63 P = Problem() 

64 x = geoMean.x 

65 m = len(x) 

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

67 K = [] 

68 depth = 0 

69 u = {} 

70 

71 while len(lm) > 1: 

72 depth += 1 

73 nlm = [] 

74 while lm: 

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

76 if lm: 

77 i2 = lm.pop()[0] 

78 else: 

79 i2 = 'x' 

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

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

82 K.append(k) 

83 u[k] = RealVariable('__u[' + k + ']') 

84 lm = nlm 

85 

86 root = K[-1] 

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

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

89 u[root] = lowerBound 

90 

91 for k in K: 

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

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

94 if i2 != 'x': 

95 i2 = int(i2) 

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

97 if i2 != 'x': 

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

99 else: 

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

101 else: 

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

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

104 k2pot = [ki for ki in K 

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

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

107 k1 = k2pot[0] 

108 if len(k2pot) == 2: 

109 k2 = k2pot[1] 

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

111 else: 

112 P.add_constraint( 

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

114 else: 

115 k1 = [ki for ki in K 

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

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

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

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

120 

121 return P 

122 

123 def __init__(self, geoMean, lowerBound): 

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

125 

126 :param ~picos.expressions.GeometricMean lhs: 

127 Constrained expression. 

128 :param ~picos.expressions.AffineExpression lowerBound: 

129 Lower bound on the expression. 

130 """ 

131 from ..expressions import AffineExpression, GeometricMean 

132 

133 assert isinstance(geoMean, GeometricMean) 

134 assert isinstance(lowerBound, AffineExpression) 

135 assert len(lowerBound) == 1 

136 

137 self.geoMean = geoMean 

138 self.lowerBound = lowerBound 

139 

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

141 

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

143 

144 def _subtype(self): 

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

146 

147 @classmethod 

148 def _cost(cls, subtype): 

149 return subtype.argdim + 1 

150 

151 def _expression_names(self): 

152 yield "geoMean" 

153 yield "lowerBound" 

154 

155 def _str(self): 

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

157 

158 def _get_slack(self): 

159 return self.geoMean.safe_value - self.lowerBound.safe_value 

160 

161 

162# -------------------------------------- 

163__all__ = api_end(_API_START, globals())