Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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, 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 for val, msg in get_relaxed([result["freevariables"][self.relaxvar]],
54 ["All constraints relaxed by %i%%"]):
55 appendsolwarning(msg % (0.9+(val-1)*100), self, result,
56 "Relaxed Constraints")
59class ConstraintsRelaxed(ConstraintSet):
60 """Relax constraints, as in Eqn. 11 of [Boyd2007].
62 Arguments
63 ---------
64 constraints : iterable
65 Constraints which will be relaxed (made easier).
67 Attributes
68 ----------
69 relaxvars : Variable
70 The variables controlling the relaxation. A solved value of 1 means no
71 relaxation was necessary or optimal for a particular constraint.
72 Higher values indicate the amount by which that constraint has been
73 made easier: e.g., a value of 1.5 means it was made 50 percent easier
74 in the final solution than in the original problem.
76 [Boyd2007] : "A tutorial on geometric programming", Optim Eng 8:67-122
78 """
80 def __init__(self, original_constraints):
81 if not isinstance(original_constraints, ConstraintSet):
82 original_constraints = ConstraintSet(original_constraints)
83 self.original_constraints = original_constraints
84 original_substitutions = original_constraints.substitutions
85 with NamedVariables("Relax"):
86 self.relaxvars = VectorVariable(len(original_constraints), "C")
88 with SignomialsEnabled():
89 relaxed_constraints = [
90 c.relaxed(self.relaxvars[i])
91 for i, c in enumerate(original_constraints.flat())]
93 ConstraintSet.__init__(self, {
94 "minimum relaxation": self.relaxvars >= 1,
95 "relaxed constraints": relaxed_constraints}, original_substitutions)
97 def process_result(self, result):
98 "Warns if any constraints were relaxed"
99 super().process_result(result)
100 self.check_relaxed(result)
102 def check_relaxed(self, result):
103 "Adds relaxation warnings to the result"
104 relaxed = get_relaxed(result["freevariables"][self.relaxvars],
105 range(len(self["relaxed constraints"])))
106 for relaxval, i in relaxed:
107 relax_percent = "%i%%" % (0.5+(relaxval-1)*100)
108 oldconstraint = self.original_constraints[i]
109 newconstraint = self["relaxed constraints"][i][0]
110 subs = {self.relaxvars[i]: relaxval}
111 relaxdleft = newconstraint.left.sub(subs)
112 relaxdright = newconstraint.right.sub(subs)
113 oldleftstr = str(oldconstraint.left)
114 relaxedleftstr = str(relaxdleft)
115 padding = len(relaxedleftstr) - len(oldleftstr)
116 if padding > 0:
117 oldleftstr = " " * padding + oldleftstr
118 elif padding < 0:
119 relaxedleftstr = " " * padding + relaxedleftstr
120 msg = (" %3i: %5s relaxed, from %s %s %s\n"
121 " to %s %s %s"
122 % (i, relax_percent, oldleftstr,
123 oldconstraint.oper, oldconstraint.right,
124 relaxedleftstr, newconstraint.oper, relaxdright))
125 appendsolwarning(msg, oldconstraint, result, "Relaxed Constraints")
128class ConstantsRelaxed(ConstraintSet):
129 """Relax constants in a constraintset.
131 Arguments
132 ---------
133 constraints : iterable
134 Constraints which will be relaxed (made easier).
136 include_only : set (optional)
137 variable names must be in this set to be relaxed
139 exclude : set (optional)
140 variable names in this set will never be relaxed
143 Attributes
144 ----------
145 relaxvars : Variable
146 The variables controlling the relaxation. A solved value of 1 means no
147 relaxation was necessary or optimal for a particular constant.
148 Higher values indicate the amount by which that constant has been
149 made easier: e.g., a value of 1.5 means it was made 50 percent easier
150 in the final solution than in the original problem. Of course, this
151 can also be determined by looking at the constant's new value directly.
152 """
153 # pylint:disable=too-many-locals
154 def __init__(self, constraints, *, include_only=None, exclude=None):
155 exclude = frozenset(exclude) if exclude else frozenset()
156 include_only = frozenset(include_only) if include_only else frozenset()
157 with NamedVariables("Relax") as (self.lineage, _):
158 pass # gives this model the correct lineage.
160 if not isinstance(constraints, ConstraintSet):
161 constraints = ConstraintSet(constraints)
162 substitutions = KeyDict(constraints.substitutions)
163 constants, _, linked = parse_subs(constraints.varkeys, substitutions)
164 if linked:
165 kdc = KeyDict(constants)
166 constrained_varkeys = constraints.constrained_varkeys()
167 constants.update({k: f(kdc) for k, f in linked.items()
168 if k in constrained_varkeys})
170 self._derelax_map = {}
171 relaxvars, self.freedvars, relaxation_constraints = [], [], {}
172 for const, val in sorted(constants.items(), key=lambda i: i[0].eqstr):
173 if val == 0:
174 substitutions[const] = 0
175 continue
176 if include_only and const.name not in include_only:
177 continue
178 if const.name in exclude:
179 continue
180 # set up the lineage
181 const.descr.pop("gradients", None) # nothing wants an old gradient
182 newconstd = const.descr.copy()
183 newconstd.pop("veckey", None) # only const wants an old veckey
184 # everything but const wants a new lineage, to distinguish them
185 newconstd["lineage"] = (newconstd.pop("lineage", ())
186 + (self.lineage[-1],))
187 # make the relaxation variable, free to get as large as it needs
188 relaxedd = newconstd.copy()
189 relaxedd["unitrepr"] = "-" # and unitless, importantly
190 relaxvar = Variable(**relaxedd)
191 relaxvars.append(relaxvar)
192 # the newly freed const can acquire a new value
193 del substitutions[const]
194 freed = Variable(**const.descr)
195 self.freedvars.append(freed)
196 # becuase the make the newconst will take its old value
197 newconstd["lineage"] += (("OriginalValues", 0),)
198 newconst = Variable(**newconstd)
199 substitutions[newconst] = val
200 self._derelax_map[newconst.key] = const
201 # add constraints so the newly freed's wiggle room
202 # is proportional to the value relaxvar, and it can't antirelax
203 relaxation_constraints[str(const)] = [relaxvar >= 1,
204 newconst/relaxvar <= freed,
205 freed <= newconst*relaxvar]
206 ConstraintSet.__init__(self, {
207 "original constraints": constraints,
208 "relaxation constraints": relaxation_constraints})
209 self.relaxvars = NomialArray(relaxvars) # so they can be .prod()'d
210 self.substitutions = substitutions
211 self.constants = constants
213 def process_result(self, result):
214 "Transfers constant sensitivities back to the original constants"
215 super().process_result(result)
216 constant_senss = result["sensitivities"]["variables"]
217 for new_constant, former_constant in self._derelax_map.items():
218 constant_senss[former_constant] = constant_senss[new_constant]
219 del constant_senss[new_constant]
220 self.check_relaxed(result)
223 def check_relaxed(self, result):
224 "Adds relaxation warnings to the result"
225 relaxed = get_relaxed([result["freevariables"][r]
226 for r in self.relaxvars], self.freedvars)
227 for (_, freed) in relaxed:
228 msg = (" %s: relaxed from %-.4g to %-.4g"
229 % (freed,
230 mag(self.constants[freed.key]),
231 mag(result["freevariables"][freed])))
232 appendsolwarning(msg, freed, result, "Relaxed Constants")
235def get_relaxed(relaxvals, mapped_list):
236 "Returns 'relaxed' vals (those above an arbitrary threshold of 1.01)."
237 sortrelaxed = sorted(zip(relaxvals, mapped_list), key=lambda x: -x[0])
238 mostrelaxed = max(sortrelaxed[0][0], 1.01)
239 for i, (val, _) in enumerate(sortrelaxed):
240 if val <= 1.01 and (val-1) <= (mostrelaxed-1)/10:
241 return sortrelaxed[:i]
242 return sortrelaxed