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

## 120 statements

, created at 2023-09-23 21:28 -0400

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

9class ConstraintsRelaxedEqually(ConstraintSet):

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

12 Arguments

13 ---------

14 constraints : iterable

15 Constraints which will be relaxed (made easier).

18 Attributes

19 ----------

20 relaxvar : Variable

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

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

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

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

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

28 """

30 def __init__(self, original_constraints):

31 if not isinstance(original_constraints, ConstraintSet):

32 original_constraints = ConstraintSet(original_constraints)

33 self.original_constraints = original_constraints

34 original_substitutions = original_constraints.substitutions

36 with NamedVariables("Relax"):

37 self.relaxvar = Variable("C")

38 with SignomialsEnabled():

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

40 for c in original_constraints.flat()]

42 ConstraintSet.__init__(self, {

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

44 "relaxed constraints": relaxed_constraints}, original_substitutions)

46 def process_result(self, result):

47 "Warns if any constraints were relaxed"

48 super().process_result(result)

49 self.check_relaxed(result)

51 def check_relaxed(self, result):

52 "Adds relaxation warnings to the result"

53 initsolwarning(result, "Relaxed Constraints")

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

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

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

57 "Relaxed Constraints")

60class ConstraintsRelaxed(ConstraintSet):

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

63 Arguments

64 ---------

65 constraints : iterable

66 Constraints which will be relaxed (made easier).

68 Attributes

69 ----------

70 relaxvars : Variable

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

72 relaxation was necessary or optimal for a particular constraint.

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

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

75 in the final solution than in the original problem.

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

79 """

81 def __init__(self, original_constraints):

82 if not isinstance(original_constraints, ConstraintSet):

83 original_constraints = ConstraintSet(original_constraints)

84 self.original_constraints = original_constraints

85 original_substitutions = original_constraints.substitutions

86 with NamedVariables("Relax"):

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

89 with SignomialsEnabled():

90 relaxed_constraints = [

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

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

94 ConstraintSet.__init__(self, {

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

96 "relaxed constraints": relaxed_constraints}, original_substitutions)

98 def process_result(self, result):

99 "Warns if any constraints were relaxed"

100 super().process_result(result)

101 self.check_relaxed(result)

103 def check_relaxed(self, result):

104 "Adds relaxation warnings to the result"

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

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

107 initsolwarning(result, "Relaxed Constraints")

108 for relaxval, i in relaxed:

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

110 oldconstraint = self.original_constraints[i]

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

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

113 relaxdleft = newconstraint.left.sub(subs)

114 relaxdright = newconstraint.right.sub(subs)

115 oldleftstr = str(oldconstraint.left)

116 relaxedleftstr = str(relaxdleft)

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

119 oldleftstr = " " * padding + oldleftstr

121 relaxedleftstr = " " * padding + relaxedleftstr

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

123 " to %s %s %s"

124 % (i, relax_percent, oldleftstr,

125 oldconstraint.oper, oldconstraint.right,

126 relaxedleftstr, newconstraint.oper, relaxdright))

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

130class ConstantsRelaxed(ConstraintSet):

131 """Relax constants in a constraintset.

133 Arguments

134 ---------

135 constraints : iterable

136 Constraints which will be relaxed (made easier).

138 include_only : set (optional)

139 variable names must be in this set to be relaxed

141 exclude : set (optional)

142 variable names in this set will never be relaxed

145 Attributes

146 ----------

147 relaxvars : Variable

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

149 relaxation was necessary or optimal for a particular constant.

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

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

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

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

154 """

155 # pylint:disable=too-many-locals

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

157 exclude = frozenset(exclude) if exclude else frozenset()

158 include_only = frozenset(include_only) if include_only else frozenset()

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

160 pass # gives this model the correct lineage.

162 if not isinstance(constraints, ConstraintSet):

163 constraints = ConstraintSet(constraints)

164 substitutions = KeyDict(constraints.substitutions)

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

167 kdc = KeyDict(constants)

168 constrained_varkeys = constraints.constrained_varkeys()

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

170 if k in constrained_varkeys})

172 self._derelax_map = {}

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

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

175 if val == 0:

176 substitutions[const] = 0

177 continue

178 if include_only and const.name not in include_only:

179 continue

180 if const.name in exclude:

181 continue

182 # set up the lineage

184 newconstd = const.descr.copy()

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

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

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

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

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

190 relaxedd = newconstd.copy()

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

192 relaxvar = Variable(**relaxedd)

193 relaxvars.append(relaxvar)

194 # the newly freed const can acquire a new value

195 del substitutions[const]

196 freed = Variable(**const.descr)

197 self.freedvars.append(freed)

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

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

200 newconst = Variable(**newconstd)

201 substitutions[newconst] = val

202 self._derelax_map[newconst.key] = const

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

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

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

206 newconst/relaxvar <= freed,

207 freed <= newconst*relaxvar]

208 ConstraintSet.__init__(self, {

209 "original constraints": constraints,

210 "relaxation constraints": relaxation_constraints})

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

212 self.substitutions = substitutions

213 self.constants = constants

215 def process_result(self, result):

216 "Transfers constant sensitivities back to the original constants"

217 super().process_result(result)

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

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

220 constant_senss[former_constant] = constant_senss[new_constant]

221 del constant_senss[new_constant]

222 self.check_relaxed(result)

225 def check_relaxed(self, result):

226 "Adds relaxation warnings to the result"

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

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

229 initsolwarning(result, "Relaxed Constants")

230 for (_, freed) in relaxed:

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

232 % (freed,

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

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

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

238def get_relaxed(relaxvals, mapped_list):

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

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

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

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

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

244 return sortrelaxed[:i]

245 return sortrelaxed