Coverage for gpkit/constraints/relax.py: 99%
120 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-07 22:13 -0500
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-07 22:13 -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
10class ConstraintsRelaxedEqually(ConstraintSet):
11 """Relax constraints the same amount, as in Eqn. 10 of [Boyd2007].
13 Arguments
14 ---------
15 constraints : iterable
16 Constraints which will be relaxed (made easier).
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.
27 [Boyd2007] : "A tutorial on geometric programming", Optim Eng 8:67-122
29 """
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
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()]
43 ConstraintSet.__init__(self, {
44 "minimum relaxation": self.relaxvar >= 1,
45 "relaxed constraints": relaxed_constraints}, original_substitutions)
47 def process_result(self, result):
48 "Warns if any constraints were relaxed"
49 super().process_result(result)
50 self.check_relaxed(result)
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")
61class ConstraintsRelaxed(ConstraintSet):
62 """Relax constraints, as in Eqn. 11 of [Boyd2007].
64 Arguments
65 ---------
66 constraints : iterable
67 Constraints which will be relaxed (made easier).
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.
78 [Boyd2007] : "A tutorial on geometric programming", Optim Eng 8:67-122
80 """
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")
90 with SignomialsEnabled():
91 relaxed_constraints = [
92 c.relaxed(self.relaxvars[i])
93 for i, c in enumerate(original_constraints.flat())]
95 ConstraintSet.__init__(self, {
96 "minimum relaxation": self.relaxvars >= 1,
97 "relaxed constraints": relaxed_constraints}, original_substitutions)
99 def process_result(self, result):
100 "Warns if any constraints were relaxed"
101 super().process_result(result)
102 self.check_relaxed(result)
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")
131class ConstantsRelaxed(ConstraintSet):
132 """Relax constants in a constraintset.
134 Arguments
135 ---------
136 constraints : iterable
137 Constraints which will be relaxed (made easier).
139 include_only : set (optional)
140 variable names must be in this set to be relaxed
142 exclude : set (optional)
143 variable names in this set will never be relaxed
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.
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})
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
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)
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")
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