Hide keyboard shortcuts

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 

17 

18 

19class Signomial(Nomial): 

20 """A representation of a Signomial. 

21 

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 

30 

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 

38 

39 __hash__ = Nomial.__hash__ 

40 

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 

66 

67 def diff(self, var): 

68 """Derivative of this with respect to a Variable 

69 

70 Arguments 

71 --------- 

72 var : Variable key 

73 Variable to take derivative with respect to 

74 

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) 

90 

91 def posy_negy(self): 

92 """Get the positive and negative parts, both as Posynomials 

93 

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 

107 

108 def mono_approximation(self, x0): 

109 """Monomial approximation about a point x0 

110 

111 Arguments 

112 --------- 

113 x0 (dict): 

114 point to monomialize about 

115 

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) 

147 

148 def sub(self, substitutions, require_positive=True): 

149 """Returns a nomial with substitued values. 

150 

151 Usage 

152 ----- 

153 3 == (x**2 + y).sub({'x': 1, y: 2}) 

154 3 == (x).gp.sub(x, 3) 

155 

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. 

165 

166 Returns 

167 ------- 

168 Returns substituted nomial. 

169 """ 

170 return Signomial(self.hmap.sub(substitutions, self.varkeys), 

171 require_positive=require_positive) 

172 

173 def __le__(self, other): 

174 if isinstance(other, (Numbers, Signomial)): 

175 return SignomialInequality(self, "<=", other) 

176 return NotImplemented 

177 

178 def __ge__(self, other): 

179 if isinstance(other, (Numbers, Signomial)): 

180 return SignomialInequality(self, ">=", other) 

181 return NotImplemented 

182 

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 

198 

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 

230 

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 

240 

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 

250 

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 

257 

258 def __sub__(self, other): 

259 return self + -other if SignomialsEnabled else NotImplemented # pylint: disable=using-constant-test 

260 

261 def __rsub__(self, other): 

262 return other + -self if SignomialsEnabled else NotImplemented # pylint: disable=using-constant-test 

263 

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] 

270 

271class Posynomial(Signomial): 

272 "A Signomial with strictly positive cs" 

273 

274 __hash__ = Signomial.__hash__ 

275 

276 def __le__(self, other): 

277 if isinstance(other, Numbers + (Monomial,)): 

278 return PosynomialInequality(self, "<=", other) 

279 return NotImplemented 

280 

281 # Posynomial.__ge__ falls back on Signomial.__ge__ 

282 

283 def mono_lower_bound(self, x0): 

284 """Monomial lower bound at a point x0 

285 

286 Arguments 

287 --------- 

288 x0 (dict): 

289 point to make lower bound exact 

290 

291 Returns 

292 ------- 

293 Monomial 

294 """ 

295 return self.mono_approximation(x0) 

296 

297 

298class Monomial(Posynomial): 

299 "A Posynomial with only one term" 

300 

301 __hash__ = Posynomial.__hash__ 

302 

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 

309 

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 

316 

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 

324 

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 

338 

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) 

347 

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 

354 

355 # Monomial.__le__ falls back on Posynomial.__le__ 

356 

357 def mono_approximation(self, x0): 

358 return self 

359 

360 

361MONS = Numbers + (Monomial,) 

362 

363 

364####################################################### 

365####### CONSTRAINTS ################################### 

366####################################################### 

367 

368 

369class ScalarSingleEquationConstraint(SingleEquationConstraint): 

370 "A SingleEquationConstraint with scalar left and right sides." 

371 generated_by = v_ss = parent = None 

372 bounded = meq_bounded = {} 

373 

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]) 

385 

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) 

397 

398 

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... 

407 

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) 

416 

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")) 

423 

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 

444 

445 def _gen_unsubbed(self, p_lt, m_gt): 

446 """Returns the unsubstituted posys <= 1. 

447 

448 Parameters 

449 ---------- 

450 p_lt : posynomial 

451 the left-hand side of (posynomial < monomial) 

452 

453 m_gt : monomial 

454 the right-hand side of (posynomial < monomial) 

455 

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 [] 

468 

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 

484 

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 

507 

508 

509class MonomialEquality(PosynomialInequality): 

510 "A Constraint of the form Monomial == Monomial." 

511 oper = "=" 

512 

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]) 

530 

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 

537 

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 

544 

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) 

549 

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 

560 

561 

562class SignomialInequality(ScalarSingleEquationConstraint): 

563 """A constraint of the general form posynomial >= posynomial 

564 

565 Stored at .unsubbed[0] as a single Signomial (0 >= self)""" 

566 

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 

580 

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) 

620 

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 

635 

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 

644 

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 

660 

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 

670 

671 

672class SingleSignomialEquality(SignomialInequality): 

673 "A constraint of the general form posynomial == posynomial" 

674 

675 def __init__(self, left, right): 

676 SignomialInequality.__init__(self, left, "<=", right) 

677 self.oper = "=" 

678 self.meq_bounded = self.as_gpconstr({}).meq_bounded 

679 

680 def as_hmapslt1(self, substitutions): 

681 "SignomialEquality is never considered GP-compatible" 

682 raise InvalidGPConstraint(self) 

683 

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