Coverage for gpkit/constraints/relax.py: 99%

120 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-07 22:15 -0500

1"""Models for assessing primal feasibility""" 

2from .set import ConstraintSet 

3from ..nomials import Variable, VectorVariable, parse_subs, NomialArray 

4from ..keydict import KeyDict 

5from .. import NamedVariables, SignomialsEnabled 

6from ..small_scripts import appendsolwarning, initsolwarning, mag 

7# pylint: disable=consider-using-f-string 

8 

9 

10class ConstraintsRelaxedEqually(ConstraintSet): 

11 """Relax constraints the same amount, as in Eqn. 10 of [Boyd2007]. 

12 

13 Arguments 

14 --------- 

15 constraints : iterable 

16 Constraints which will be relaxed (made easier). 

17 

18 

19 Attributes 

20 ---------- 

21 relaxvar : Variable 

22 The variable controlling the relaxation. A solved value of 1 means no 

23 relaxation. Higher values indicate the amount by which all constraints 

24 have been made easier: e.g., a value of 1.5 means all constraints were 

25 50 percent easier in the final solution than in the original problem. 

26 

27 [Boyd2007] : "A tutorial on geometric programming", Optim Eng 8:67-122 

28 

29 """ 

30 

31 def __init__(self, original_constraints): 

32 if not isinstance(original_constraints, ConstraintSet): 

33 original_constraints = ConstraintSet(original_constraints) 

34 self.original_constraints = original_constraints 

35 original_substitutions = original_constraints.substitutions 

36 

37 with NamedVariables("Relax"): 

38 self.relaxvar = Variable("C") 

39 with SignomialsEnabled(): 

40 relaxed_constraints = [c.relaxed(self.relaxvar) 

41 for c in original_constraints.flat()] 

42 

43 ConstraintSet.__init__(self, { 

44 "minimum relaxation": self.relaxvar >= 1, 

45 "relaxed constraints": relaxed_constraints}, original_substitutions) 

46 

47 def process_result(self, result): 

48 "Warns if any constraints were relaxed" 

49 super().process_result(result) 

50 self.check_relaxed(result) 

51 

52 def check_relaxed(self, result): 

53 "Adds relaxation warnings to the result" 

54 initsolwarning(result, "Relaxed Constraints") 

55 for val, msg in get_relaxed([result["freevariables"][self.relaxvar]], 

56 ["All constraints relaxed by %i%%"]): 

57 appendsolwarning(msg % (0.9+(val-1)*100), self, result, 

58 "Relaxed Constraints") 

59 

60 

61class ConstraintsRelaxed(ConstraintSet): 

62 """Relax constraints, as in Eqn. 11 of [Boyd2007]. 

63 

64 Arguments 

65 --------- 

66 constraints : iterable 

67 Constraints which will be relaxed (made easier). 

68 

69 Attributes 

70 ---------- 

71 relaxvars : Variable 

72 The variables controlling the relaxation. A solved value of 1 means no 

73 relaxation was necessary or optimal for a particular constraint. 

74 Higher values indicate the amount by which that constraint has been 

75 made easier: e.g., a value of 1.5 means it was made 50 percent easier 

76 in the final solution than in the original problem. 

77 

78 [Boyd2007] : "A tutorial on geometric programming", Optim Eng 8:67-122 

79 

80 """ 

81 

82 def __init__(self, original_constraints): 

83 if not isinstance(original_constraints, ConstraintSet): 

84 original_constraints = ConstraintSet(original_constraints) 

85 self.original_constraints = original_constraints 

86 original_substitutions = original_constraints.substitutions 

87 with NamedVariables("Relax"): 

88 self.relaxvars = VectorVariable(len(original_constraints), "C") 

89 

90 with SignomialsEnabled(): 

91 relaxed_constraints = [ 

92 c.relaxed(self.relaxvars[i]) 

93 for i, c in enumerate(original_constraints.flat())] 

94 

95 ConstraintSet.__init__(self, { 

96 "minimum relaxation": self.relaxvars >= 1, 

97 "relaxed constraints": relaxed_constraints}, original_substitutions) 

98 

99 def process_result(self, result): 

100 "Warns if any constraints were relaxed" 

101 super().process_result(result) 

102 self.check_relaxed(result) 

103 

104 def check_relaxed(self, result): 

105 "Adds relaxation warnings to the result" 

106 relaxed = get_relaxed(result["freevariables"][self.relaxvars], 

107 range(len(self["relaxed constraints"]))) 

108 initsolwarning(result, "Relaxed Constraints") 

109 for relaxval, i in relaxed: 

110 relax_percent = "%i%%" % (0.5+(relaxval-1)*100) 

111 oldconstraint = self.original_constraints[i] 

112 newconstraint = self["relaxed constraints"][i][0] 

113 subs = {self.relaxvars[i]: relaxval} 

114 relaxdleft = newconstraint.left.sub(subs) 

115 relaxdright = newconstraint.right.sub(subs) 

116 oldleftstr = str(oldconstraint.left) 

117 relaxedleftstr = str(relaxdleft) 

118 padding = len(relaxedleftstr) - len(oldleftstr) 

119 if padding > 0: 

120 oldleftstr = " " * padding + oldleftstr 

121 elif padding < 0: 

122 relaxedleftstr = " " * padding + relaxedleftstr 

123 msg = (" %3i: %5s relaxed, from %s %s %s\n" 

124 " to %s %s %s" 

125 % (i, relax_percent, oldleftstr, 

126 oldconstraint.oper, oldconstraint.right, 

127 relaxedleftstr, newconstraint.oper, relaxdright)) 

128 appendsolwarning(msg, oldconstraint, result, "Relaxed Constraints") 

129 

130 

131class ConstantsRelaxed(ConstraintSet): 

132 """Relax constants in a constraintset. 

133 

134 Arguments 

135 --------- 

136 constraints : iterable 

137 Constraints which will be relaxed (made easier). 

138 

139 include_only : set (optional) 

140 variable names must be in this set to be relaxed 

141 

142 exclude : set (optional) 

143 variable names in this set will never be relaxed 

144 

145 

146 Attributes 

147 ---------- 

148 relaxvars : Variable 

149 The variables controlling the relaxation. A solved value of 1 means no 

150 relaxation was necessary or optimal for a particular constant. 

151 Higher values indicate the amount by which that constant has been 

152 made easier: e.g., a value of 1.5 means it was made 50 percent easier 

153 in the final solution than in the original problem. Of course, this 

154 can also be determined by looking at the constant's new value directly. 

155 """ 

156 # pylint:disable=too-many-locals 

157 def __init__(self, constraints, *, include_only=None, exclude=None): 

158 exclude = frozenset(exclude) if exclude else frozenset() 

159 include_only = frozenset(include_only) if include_only else frozenset() 

160 with NamedVariables("Relax") as (self.lineage, _): 

161 pass # gives this model the correct lineage. 

162 

163 if not isinstance(constraints, ConstraintSet): 

164 constraints = ConstraintSet(constraints) 

165 substitutions = KeyDict(constraints.substitutions) 

166 constants, _, linked = parse_subs(constraints.vks, substitutions) 

167 if linked: 

168 kdc = KeyDict(constants) 

169 constrained_varkeys = constraints.constrained_varkeys() 

170 constants.update({k: f(kdc) for k, f in linked.items() 

171 if k in constrained_varkeys}) 

172 

173 self._derelax_map = {} 

174 relaxvars, self.freedvars, relaxation_constraints = [], [], {} 

175 for const, val in sorted(constants.items(), key=lambda i: i[0].eqstr): 

176 if val == 0: 

177 substitutions[const] = 0 

178 continue 

179 if include_only and const.name not in include_only: 

180 continue 

181 if const.name in exclude: 

182 continue 

183 # set up the lineage 

184 const.descr.pop("gradients", None) # nothing wants an old gradient 

185 newconstd = const.descr.copy() 

186 newconstd.pop("veckey", None) # only const wants an old veckey 

187 # everything but const wants a new lineage, to distinguish them 

188 newconstd["lineage"] = (newconstd.pop("lineage", ()) 

189 + (self.lineage[-1],)) 

190 # make the relaxation variable, free to get as large as it needs 

191 relaxedd = newconstd.copy() 

192 relaxedd["unitrepr"] = "-" # and unitless, importantly 

193 relaxvar = Variable(**relaxedd) 

194 relaxvars.append(relaxvar) 

195 # the newly freed const can acquire a new value 

196 del substitutions[const] 

197 freed = Variable(**const.descr) 

198 self.freedvars.append(freed) 

199 # becuase the make the newconst will take its old value 

200 newconstd["lineage"] += (("OriginalValues", 0),) 

201 newconst = Variable(**newconstd) 

202 substitutions[newconst] = val 

203 self._derelax_map[newconst.key] = const 

204 # add constraints so the newly freed's wiggle room 

205 # is proportional to the value relaxvar, and it can't antirelax 

206 relaxation_constraints[str(const)] = [relaxvar >= 1, 

207 newconst/relaxvar <= freed, 

208 freed <= newconst*relaxvar] 

209 ConstraintSet.__init__(self, { 

210 "original constraints": constraints, 

211 "relaxation constraints": relaxation_constraints}) 

212 self.relaxvars = NomialArray(relaxvars) # so they can be .prod()'d 

213 self.substitutions = substitutions 

214 self.constants = constants 

215 

216 def process_result(self, result): 

217 "Transfers constant sensitivities back to the original constants" 

218 super().process_result(result) 

219 constant_senss = result["sensitivities"]["variables"] 

220 for new_constant, former_constant in self._derelax_map.items(): 

221 constant_senss[former_constant] = constant_senss[new_constant] 

222 del constant_senss[new_constant] 

223 self.check_relaxed(result) 

224 

225 

226 def check_relaxed(self, result): 

227 "Adds relaxation warnings to the result" 

228 relaxed = get_relaxed([result["freevariables"][r] 

229 for r in self.relaxvars], self.freedvars) 

230 initsolwarning(result, "Relaxed Constants") 

231 for (_, freed) in relaxed: 

232 msg = (" %s: relaxed from %-.4g to %-.4g" 

233 % (freed, 

234 mag(self.constants[freed.key]), 

235 mag(result["freevariables"][freed]))) 

236 appendsolwarning(msg, freed, result, "Relaxed Constants") 

237 

238 

239def get_relaxed(relaxvals, mapped_list): 

240 "Returns 'relaxed' vals (those above an arbitrary threshold of 1.01)." 

241 sortrelaxed = sorted(zip(relaxvals, mapped_list), key=lambda x: -x[0]) 

242 mostrelaxed = max(sortrelaxed[0][0], 1.01) 

243 for i, (val, _) in enumerate(sortrelaxed): 

244 if val <= 1.01 and (val-1) <= (mostrelaxed-1)/10: 

245 return sortrelaxed[:i] 

246 return sortrelaxed