Coverage for gpkit/constraints/relax.py: 99%
120 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-28 11:42 -0400
« prev ^ index » next coverage.py v6.4.2, created at 2022-07-28 11:42 -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)
118 if padding > 0:
119 oldleftstr = " " * padding + oldleftstr
120 elif padding < 0:
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)
166 if linked:
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
183 const.descr.pop("gradients", None) # nothing wants an old gradient
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