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"""Signomial, Posynomial, Monomial, Constraint, & MonoEQCOnstraint classes"""
2from collections import defaultdict
3import numpy as np
4from .core import Nomial
5from .array import NomialArray
6from .. import units
7from ..constraints import SingleEquationConstraint
8from ..globals import SignomialsEnabled
9from ..small_classes import Numbers
10from ..small_classes import HashVector, EMPTY_HV
11from ..varkey import VarKey
12from ..small_scripts import mag
13from ..exceptions import (InvalidGPConstraint, InvalidPosynomial,
14 PrimalInfeasible, DimensionalityError)
15from .map import NomialMap
16from .substitution import parse_subs
19class Signomial(Nomial):
20 """A representation of a Signomial.
22 Arguments
23 ---------
24 exps: tuple of dicts
25 Exponent dicts for each monomial term
26 cs: tuple
27 Coefficient values for each monomial term
28 require_positive: bool
29 If True and Signomials not enabled, c <= 0 will raise ValueError
31 Returns
32 -------
33 Signomial
34 Posynomial (if the input has only positive cs)
35 Monomial (if the input has one term and only positive cs)
36 """
37 _c = _exp = None # pylint: disable=invalid-name
39 __hash__ = Nomial.__hash__
41 def __init__(self, hmap=None, cs=1, require_positive=True): # pylint: disable=too-many-statements,too-many-branches
42 if not isinstance(hmap, NomialMap):
43 if hasattr(hmap, "hmap"):
44 hmap = hmap.hmap
45 elif isinstance(hmap, Numbers):
46 hmap_ = NomialMap([(EMPTY_HV, mag(hmap))])
47 hmap_.units_of_product(hmap)
48 hmap = hmap_
49 elif isinstance(hmap, dict):
50 exp = HashVector({VarKey(k): v for k, v in hmap.items() if v})
51 hmap = NomialMap({exp: mag(cs)})
52 hmap.units_of_product(cs)
53 else:
54 raise ValueError("Nomial construction accepts only NomialMaps,"
55 " objects with an .hmap attribute, numbers,"
56 " or *(exp dict of strings, number).")
57 super().__init__(hmap)
58 if self.any_nonpositive_cs:
59 if require_positive and not SignomialsEnabled:
60 raise InvalidPosynomial("each c must be positive.")
61 self.__class__ = Signomial
62 elif len(self.hmap) == 1:
63 self.__class__ = Monomial
64 else:
65 self.__class__ = Posynomial
67 def diff(self, var):
68 """Derivative of this with respect to a Variable
70 Arguments
71 ---------
72 var : Variable key
73 Variable to take derivative with respect to
75 Returns
76 -------
77 Signomial (or Posynomial or Monomial)
78 """
79 varset = self.varkeys[var]
80 if len(varset) > 1:
81 raise ValueError("multiple variables %s found for key %s"
82 % (list(varset), var))
83 if not varset:
84 diff = NomialMap({EMPTY_HV: 0.0})
85 diff.units = None
86 else:
87 var, = varset
88 diff = self.hmap.diff(var)
89 return Signomial(diff, require_positive=False)
91 def posy_negy(self):
92 """Get the positive and negative parts, both as Posynomials
94 Returns
95 -------
96 Posynomial, Posynomial:
97 p_pos and p_neg in (self = p_pos - p_neg) decomposition,
98 """
99 py, ny = NomialMap(), NomialMap()
100 py.units, ny.units = self.units, self.units
101 for exp, c in self.hmap.items():
102 if c > 0:
103 py[exp] = c
104 elif c < 0:
105 ny[exp] = -c # -c to keep it a posynomial
106 return Posynomial(py) if py else 0, Posynomial(ny) if ny else 0
108 def mono_approximation(self, x0):
109 """Monomial approximation about a point x0
111 Arguments
112 ---------
113 x0 (dict):
114 point to monomialize about
116 Returns
117 -------
118 Monomial (unless self(x0) < 0, in which case a Signomial is returned)
119 """
120 x0, _, _ = parse_subs(self.varkeys, x0) # use only varkey keys
121 psub = self.hmap.sub(x0, self.varkeys, parsedsubs=True)
122 if EMPTY_HV not in psub or len(psub) > 1:
123 raise ValueError("Variables %s remained after substituting x0=%s"
124 " into %s" % (psub, x0, self))
125 c0, = psub.values()
126 c, exp = c0, HashVector()
127 for vk in self.vks:
128 val = float(x0[vk])
129 diff, = self.hmap.diff(vk).sub(x0, self.varkeys,
130 parsedsubs=True).values()
131 e = val*diff/c0
132 if e:
133 exp[vk] = e
134 try:
135 c /= val**e
136 except OverflowError:
137 raise OverflowError(
138 "While approximating the variable %s with a local value of"
139 " %s, %s/(%s**%s) overflowed. Try reducing the variable's"
140 " value by changing its unit prefix, or specify x0 values"
141 " for any free variables it's multiplied or divided by in"
142 " the posynomial %s whose expected value is far from 1."
143 % (vk, val, c, val, e, self))
144 hmap = NomialMap({exp: c})
145 hmap.units = self.units
146 return Monomial(hmap)
148 def sub(self, substitutions, require_positive=True):
149 """Returns a nomial with substitued values.
151 Usage
152 -----
153 3 == (x**2 + y).sub({'x': 1, y: 2})
154 3 == (x).gp.sub(x, 3)
156 Arguments
157 ---------
158 substitutions : dict or key
159 Either a dictionary whose keys are strings, Variables, or VarKeys,
160 and whose values are numbers, or a string, Variable or Varkey.
161 val : number (optional)
162 If the substitutions entry is a single key, val holds the value
163 require_positive : boolean (optional, default is True)
164 Controls whether the returned value can be a Signomial.
166 Returns
167 -------
168 Returns substituted nomial.
169 """
170 return Signomial(self.hmap.sub(substitutions, self.varkeys),
171 require_positive=require_positive)
173 def __le__(self, other):
174 if isinstance(other, (Numbers, Signomial)):
175 return SignomialInequality(self, "<=", other)
176 return NotImplemented
178 def __ge__(self, other):
179 if isinstance(other, (Numbers, Signomial)):
180 return SignomialInequality(self, ">=", other)
181 return NotImplemented
183 def __add__(self, other, rev=False):
184 other_hmap = getattr(other, "hmap", None)
185 if isinstance(other, Numbers):
186 if not other: # other is zero
187 return Signomial(self.hmap)
188 other_hmap = NomialMap({EMPTY_HV: mag(other)})
189 other_hmap.units_of_product(other)
190 if other_hmap:
191 astorder = (self, other)
192 if rev:
193 astorder = tuple(reversed(astorder))
194 out = Signomial(self.hmap + other_hmap)
195 out.ast = ("add", astorder)
196 return out
197 return NotImplemented
199 def __mul__(self, other, rev=False):
200 astorder = (self, other)
201 if rev:
202 astorder = tuple(reversed(astorder))
203 if isinstance(other, np.ndarray):
204 s = NomialArray(self)
205 s.ast = self.ast
206 return s*other
207 if isinstance(other, Numbers):
208 if not other: # other is zero
209 return other
210 hmap = mag(other)*self.hmap
211 hmap.units_of_product(self.hmap.units, other)
212 out = Signomial(hmap)
213 out.ast = ("mul", astorder)
214 return out
215 if isinstance(other, Signomial):
216 hmap = NomialMap()
217 for exp_s, c_s in self.hmap.items():
218 for exp_o, c_o in other.hmap.items():
219 exp = exp_s + exp_o
220 new, accumulated = c_s*c_o, hmap.get(exp, 0)
221 if new != -accumulated:
222 hmap[exp] = accumulated + new
223 elif accumulated:
224 del hmap[exp]
225 hmap.units_of_product(self.hmap.units, other.hmap.units)
226 out = Signomial(hmap)
227 out.ast = ("mul", astorder)
228 return out
229 return NotImplemented
231 def __truediv__(self, other):
232 "Support the / operator in Python 2.x"
233 if isinstance(other, Numbers):
234 out = self*other**-1
235 out.ast = ("div", (self, other))
236 return out
237 if isinstance(other, Monomial):
238 return other.__rtruediv__(self)
239 return NotImplemented
241 def __pow__(self, expo):
242 if isinstance(expo, int) and expo >= 0:
243 p = 1
244 while expo > 0:
245 p *= self
246 expo -= 1
247 p.ast = ("pow", (self, expo))
248 return p
249 return NotImplemented
251 def __neg__(self):
252 if SignomialsEnabled: # pylint: disable=using-constant-test
253 out = -1*self
254 out.ast = ("neg", self)
255 return out
256 return NotImplemented
258 def __sub__(self, other):
259 return self + -other if SignomialsEnabled else NotImplemented # pylint: disable=using-constant-test
261 def __rsub__(self, other):
262 return other + -self if SignomialsEnabled else NotImplemented # pylint: disable=using-constant-test
264 def chop(self):
265 "Returns a list of monomials in the signomial."
266 monmaps = [NomialMap({exp: c}) for exp, c in self.hmap.items()]
267 for monmap in monmaps:
268 monmap.units = self.hmap.units
269 return [Monomial(monmap) for monmap in monmaps]
271class Posynomial(Signomial):
272 "A Signomial with strictly positive cs"
274 __hash__ = Signomial.__hash__
276 def __le__(self, other):
277 if isinstance(other, Numbers + (Monomial,)):
278 return PosynomialInequality(self, "<=", other)
279 return NotImplemented
281 # Posynomial.__ge__ falls back on Signomial.__ge__
283 def mono_lower_bound(self, x0):
284 """Monomial lower bound at a point x0
286 Arguments
287 ---------
288 x0 (dict):
289 point to make lower bound exact
291 Returns
292 -------
293 Monomial
294 """
295 return self.mono_approximation(x0)
298class Monomial(Posynomial):
299 "A Posynomial with only one term"
301 __hash__ = Posynomial.__hash__
303 @property
304 def exp(self):
305 "Creates exp or returns a cached exp"
306 if not self._exp:
307 self._exp, = self.hmap.keys() # pylint: disable=attribute-defined-outside-init
308 return self._exp
310 @property
311 def c(self): # pylint: disable=invalid-name
312 "Creates c or returns a cached c"
313 if not self._c:
314 self._c, = self.cs # pylint: disable=attribute-defined-outside-init, invalid-name
315 return self._c
317 def __rtruediv__(self, other):
318 "Divide other by this Monomial"
319 if isinstance(other, Numbers + (Signomial,)):
320 out = other * self**-1
321 out.ast = ("div", (other, self))
322 return out
323 return NotImplemented
325 def __pow__(self, expo):
326 if isinstance(expo, Numbers):
327 (exp, c), = self.hmap.items()
328 exp = exp*expo if expo else EMPTY_HV
329 hmap = NomialMap({exp: c**expo})
330 if expo and self.hmap.units:
331 hmap.units = self.hmap.units**expo
332 else:
333 hmap.units = None
334 out = Monomial(hmap)
335 out.ast = ("pow", (self, expo))
336 return out
337 return NotImplemented
339 def __eq__(self, other):
340 if isinstance(other, MONS):
341 try: # if both are monomials, return a constraint
342 return MonomialEquality(self, other)
343 except (DimensionalityError, ValueError) as e:
344 print("Infeasible monomial equality: %s" % e)
345 return False
346 return super().__eq__(other)
348 def __ge__(self, other):
349 if isinstance(other, Numbers + (Posynomial,)):
350 return PosynomialInequality(self, ">=", other)
351 # elif isinstance(other, np.ndarray):
352 # return other.__le__(self, rev=True)
353 return NotImplemented
355 # Monomial.__le__ falls back on Posynomial.__le__
357 def mono_approximation(self, x0):
358 return self
361MONS = Numbers + (Monomial,)
364#######################################################
365####### CONSTRAINTS ###################################
366#######################################################
369class ScalarSingleEquationConstraint(SingleEquationConstraint):
370 "A SingleEquationConstraint with scalar left and right sides."
371 generated_by = v_ss = parent = None
372 bounded = meq_bounded = {}
374 def __init__(self, left, oper, right):
375 lr = [left, right]
376 self.varkeys = set()
377 for i, sig in enumerate(lr):
378 if isinstance(sig, Signomial):
379 self.varkeys.update(sig.vks)
380 else:
381 lr[i] = Signomial(sig)
382 from .. import NamedVariables
383 self.lineage = tuple(NamedVariables.lineage)
384 super().__init__(lr[0], oper, lr[1])
386 def relaxed(self, relaxvar):
387 "Returns the relaxation of the constraint in a list."
388 if self.oper == ">=":
389 return [relaxvar*self.left >= self.right]
390 if self.oper == "<=":
391 return [self.left <= relaxvar*self.right]
392 if self.oper == "=":
393 return [self.left <= relaxvar*self.right,
394 relaxvar*self.left >= self.right]
395 raise ValueError(
396 "Constraint %s had unknown operator %s." % self.oper, self)
399# pylint: disable=too-many-instance-attributes, invalid-unary-operand-type
400class PosynomialInequality(ScalarSingleEquationConstraint):
401 """A constraint of the general form monomial >= posynomial
402 Stored in the posylt1_rep attribute as a single Posynomial (self <= 1)
403 Usually initialized via operator overloading, e.g. cc = (y**2 >= 1 + x)
404 """
405 feastol = 1e-3
406 # NOTE: follows .check_result's max default, but 1e-3 seems a bit lax...
408 def __init__(self, left, oper, right):
409 ScalarSingleEquationConstraint.__init__(self, left, oper, right)
410 if self.oper == "<=":
411 p_lt, m_gt = self.left, self.right
412 elif self.oper == ">=":
413 m_gt, p_lt = self.left, self.right
414 else:
415 raise ValueError("operator %s is not supported." % self.oper)
417 self.unsubbed = self._gen_unsubbed(p_lt, m_gt)
418 self.bounded = set()
419 for p in self.unsubbed:
420 for exp in p.hmap:
421 for vk, x in exp.items():
422 self.bounded.add((vk, "upper" if x > 0 else "lower"))
424 def _simplify_posy_ineq(self, hmap, pmap=None, fixed=None):
425 "Simplify a posy <= 1 by moving constants to the right side."
426 if EMPTY_HV not in hmap:
427 return hmap
428 coeff = 1 - hmap[EMPTY_HV]
429 if pmap is not None: # note constant term's mmap
430 const_idx = list(hmap.keys()).index(EMPTY_HV)
431 self.const_mmap = self.pmap.pop(const_idx) # pylint: disable=attribute-defined-outside-init
432 self.const_coeff = coeff # pylint: disable=attribute-defined-outside-init
433 if coeff >= -self.feastol and len(hmap) == 1:
434 return None # a tautological monomial!
435 if coeff < -self.feastol:
436 msg = "'%s' is infeasible by %.2g%%" % (self, -coeff*100)
437 if fixed:
438 msg += " after substituting %s." % fixed
439 raise PrimalInfeasible(msg)
440 scaled = hmap/coeff
441 scaled.units = hmap.units
442 del scaled[EMPTY_HV]
443 return scaled
445 def _gen_unsubbed(self, p_lt, m_gt):
446 """Returns the unsubstituted posys <= 1.
448 Parameters
449 ----------
450 p_lt : posynomial
451 the left-hand side of (posynomial < monomial)
453 m_gt : monomial
454 the right-hand side of (posynomial < monomial)
456 """
457 try:
458 m_exp, = m_gt.hmap.keys()
459 m_c, = m_gt.hmap.values()
460 except ValueError:
461 raise TypeError("greater-than side '%s' is not monomial." % m_gt)
462 m_c *= units.of_division(m_gt, p_lt)
463 hmap = p_lt.hmap.copy()
464 for exp in list(hmap):
465 hmap[exp-m_exp] = hmap.pop(exp)/m_c
466 hmap = self._simplify_posy_ineq(hmap)
467 return [Posynomial(hmap)] if hmap else []
469 def as_hmapslt1(self, substitutions):
470 "Returns the posys <= 1 representation of this constraint."
471 out = []
472 for posy in self.unsubbed:
473 fixed, _, _ = parse_subs(posy.varkeys, substitutions, clean=True)
474 hmap = posy.hmap.sub(fixed, posy.varkeys, parsedsubs=True)
475 self.pmap, self.mfm = hmap.mmap(posy.hmap) # pylint: disable=attribute-defined-outside-init
476 hmap = self._simplify_posy_ineq(hmap, self.pmap, fixed)
477 if hmap is not None:
478 if any(c <= 0 for c in hmap.values()):
479 raise InvalidGPConstraint("'%s' became Signomial after sub"
480 "stituting %s" % (self, fixed))
481 hmap.parent = self
482 out.append(hmap)
483 return out
485 def sens_from_dual(self, la, nu, _):
486 "Returns the variable/constraint sensitivities from lambda/nu"
487 presub, = self.unsubbed
488 if hasattr(self, "pmap"):
489 nu_ = np.zeros(len(presub.hmap))
490 for i, mmap in enumerate(self.pmap):
491 for idx, percentage in mmap.items():
492 nu_[idx] += percentage*nu[i]
493 if hasattr(self, "const_mmap"):
494 scale = (1-self.const_coeff)/self.const_coeff
495 for idx, percentage in self.const_mmap.items():
496 nu_[idx] += percentage * la*scale
497 nu = nu_
498 self.v_ss = HashVector()
499 if self.parent:
500 self.parent.v_ss = self.v_ss
501 if self.generated_by:
502 self.generated_by.v_ss = self.v_ss
503 for nu_i, exp in zip(nu, presub.hmap):
504 for vk, x in exp.items():
505 self.v_ss[vk] = nu_i*x + self.v_ss.get(vk, 0)
506 return self.v_ss, la
509class MonomialEquality(PosynomialInequality):
510 "A Constraint of the form Monomial == Monomial."
511 oper = "="
513 def __init__(self, left, right):
514 # pylint: disable=super-init-not-called,non-parent-init-called
515 ScalarSingleEquationConstraint.__init__(self, left, self.oper, right)
516 self.unsubbed = self._gen_unsubbed(self.left, self.right)
517 self.bounded = set()
518 self.meq_bounded = {}
519 self._las = []
520 if self.unsubbed and len(self.varkeys) > 1:
521 exp, = self.unsubbed[0].hmap
522 for key, e in exp.items():
523 s_e = np.sign(e)
524 ubs = frozenset((k, "upper" if np.sign(e) != s_e else "lower")
525 for k, e in exp.items() if k != key)
526 lbs = frozenset((k, "lower" if np.sign(e) != s_e else "upper")
527 for k, e in exp.items() if k != key)
528 self.meq_bounded[(key, "upper")] = frozenset([ubs])
529 self.meq_bounded[(key, "lower")] = frozenset([lbs])
531 def _gen_unsubbed(self, left, right): # pylint: disable=arguments-differ
532 "Returns the unsubstituted posys <= 1."
533 unsubbed = PosynomialInequality._gen_unsubbed
534 l_over_r = unsubbed(self, left, right)
535 r_over_l = unsubbed(self, right, left)
536 return l_over_r + r_over_l
538 def as_hmapslt1(self, substitutions):
539 "Tags posynomials for dual feasibility checking"
540 out = super().as_hmapslt1(substitutions)
541 for h in out:
542 h.from_meq = True # pylint: disable=attribute-defined-outside-init
543 return out
545 def __bool__(self):
546 'A constraint not guaranteed to be satisfied evaluates as "False".'
547 return bool(self.left.c == self.right.c
548 and self.left.exp == self.right.exp)
550 def sens_from_dual(self, la, nu, _):
551 "Returns the variable/constraint sensitivities from lambda/nu"
552 self._las.append(la)
553 if len(self._las) < 2:
554 return {}, 0
555 la = self._las[0] - self._las[1]
556 self._las = []
557 exp, = self.unsubbed[0].hmap
558 self.v_ss = exp*la
559 return self.v_ss, la
562class SignomialInequality(ScalarSingleEquationConstraint):
563 """A constraint of the general form posynomial >= posynomial
565 Stored at .unsubbed[0] as a single Signomial (0 >= self)"""
567 def __init__(self, left, oper, right):
568 ScalarSingleEquationConstraint.__init__(self, left, oper, right)
569 if not SignomialsEnabled:
570 raise TypeError("Cannot initialize SignomialInequality"
571 " outside of a SignomialsEnabled environment.")
572 if self.oper == "<=":
573 plt, pgt = self.left, self.right
574 elif self.oper == ">=":
575 pgt, plt = self.left, self.right
576 else:
577 raise ValueError("operator %s is not supported." % self.oper)
578 self.unsubbed = [plt - pgt]
579 self.bounded = self.as_gpconstr({}).bounded
581 def as_hmapslt1(self, substitutions):
582 "Returns the posys <= 1 representation of this constraint."
583 siglt0, = self.unsubbed
584 siglt0 = siglt0.sub(substitutions, require_positive=False)
585 posy, negy = siglt0.posy_negy()
586 if posy is 0: # pylint: disable=literal-comparison
587 print("Warning: SignomialConstraint %s became the tautological"
588 " constraint 0 <= %s after substitution." % (self, negy))
589 return []
590 if negy is 0: # pylint: disable=literal-comparison
591 raise ValueError("%s became the infeasible constraint %s <= 0"
592 " after substitution." % (self, posy))
593 if hasattr(negy, "cs") and len(negy.cs) > 1:
594 raise InvalidGPConstraint(
595 "%s did not simplify to a PosynomialInequality; try calling"
596 " `.localsolve` instead of `.solve` to form your Model as a"
597 " SequentialGeometricProgram." % self)
598 # all but one of the negy terms becomes compatible with the posy
599 p_ineq = PosynomialInequality(posy, "<=", negy)
600 p_ineq.parent = self
601 siglt0_us, = self.unsubbed
602 siglt0_hmap = siglt0_us.hmap.sub(substitutions, siglt0_us.varkeys)
603 negy_hmap = NomialMap()
604 posy_hmaps = defaultdict(NomialMap)
605 for o_exp, exp in siglt0_hmap.expmap.items():
606 if exp == negy.exp:
607 negy_hmap[o_exp] = -siglt0_us.hmap[o_exp]
608 else:
609 posy_hmaps[exp-negy.exp][o_exp] = siglt0_us.hmap[o_exp]
610 # pylint: disable=attribute-defined-outside-init
611 self._mons = [Monomial(NomialMap({k: v}))
612 for k, v in (posy/negy).hmap.items()]
613 self._negysig = Signomial(negy_hmap, require_positive=False)
614 self._coeffsigs = {exp: Signomial(hmap, require_positive=False)
615 for exp, hmap in posy_hmaps.items()}
616 self._sigvars = {exp: (list(self._negysig.varkeys)
617 + list(sig.varkeys))
618 for exp, sig in self._coeffsigs.items()}
619 return p_ineq.as_hmapslt1(substitutions)
621 def sens_from_dual(self, la, nu, result):
622 """ We want to do the following chain:
623 dlog(Obj)/dlog(monomial[i]) = nu[i]
624 * dlog(monomial)/d(monomial) = 1/(monomial value)
625 * d(monomial)/d(var) = see below
626 * d(var)/dlog(var) = var
627 = dlog(Obj)/dlog(var)
628 each final monomial is really
629 (coeff signomial)/(negy signomial)
630 and by the chain rule d(monomial)/d(var) =
631 d(coeff)/d(var)*1/negy + d(1/negy)/d(var)*coeff
632 = d(coeff)/d(var)*1/negy - d(negy)/d(var)*coeff*1/negy**2
633 """
634 # pylint: disable=too-many-locals, attribute-defined-outside-init
636 # pylint: disable=no-member
637 def subval(posy):
638 "Substitute solution into a posynomial and return the result"
639 hmap = posy.sub(result["variables"],
640 require_positive=False).hmap
641 (key, value), = hmap.items()
642 assert not key # constant
643 return value
645 self.v_ss = {}
646 invnegy_val = 1/subval(self._negysig)
647 for i, nu_i in enumerate(nu):
648 mon = self._mons[i]
649 inv_mon_val = 1/subval(mon)
650 coeff = self._coeffsigs[mon.exp]
651 for var in self._sigvars[mon.exp]:
652 d_mon_d_var = (subval(coeff.diff(var))*invnegy_val
653 - (subval(self._negysig.diff(var))
654 * subval(coeff) * invnegy_val**2))
655 var_val = result["variables"][var]
656 sens = (nu_i*inv_mon_val*d_mon_d_var*var_val)
657 assert isinstance(sens, float)
658 self.v_ss[var] = sens + self.v_ss.get(var, 0)
659 return self.v_ss, la
661 def as_gpconstr(self, x0):
662 "Returns GP-compatible approximation at x0"
663 siglt0, = self.unsubbed
664 posy, negy = siglt0.posy_negy()
665 # default guess of 1.0 for unspecified negy variables
666 x0 = {vk: x0.get(vk, 1) for vk in negy.vks}
667 pconstr = PosynomialInequality(posy, "<=", negy.mono_lower_bound(x0))
668 pconstr.generated_by = self
669 return pconstr
672class SingleSignomialEquality(SignomialInequality):
673 "A constraint of the general form posynomial == posynomial"
675 def __init__(self, left, right):
676 SignomialInequality.__init__(self, left, "<=", right)
677 self.oper = "="
678 self.meq_bounded = self.as_gpconstr({}).meq_bounded
680 def as_hmapslt1(self, substitutions):
681 "SignomialEquality is never considered GP-compatible"
682 raise InvalidGPConstraint(self)
684 def as_gpconstr(self, x0):
685 "Returns GP-compatible approximation at x0"
686 siglt0, = self.unsubbed
687 posy, negy = siglt0.posy_negy()
688 # default guess of 1.0 for unspecified negy variables
689 x0 = {vk: x0.get(vk, 1) for vk in siglt0.vks}
690 mec = (posy.mono_lower_bound(x0) == negy.mono_lower_bound(x0))
691 mec.generated_by = self
692 return mec