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#TODO: cleanup weird conditionals 

2# add conversions to plotly/sankey 

3 

4# pylint: skip-file 

5import string 

6from collections import defaultdict, namedtuple, Counter 

7from gpkit.nomials import Monomial, Posynomial, Variable 

8from gpkit.nomials.map import NomialMap 

9from gpkit.small_scripts import mag 

10from gpkit.small_classes import FixedScalar, HashVector 

11from gpkit.exceptions import DimensionalityError 

12from gpkit.repr_conventions import unitstr as get_unitstr 

13from gpkit.varkey import VarKey 

14import numpy as np 

15 

16 

17Tree = namedtuple("Tree", ["key", "value", "branches"]) 

18Transform = namedtuple("Transform", ["factor", "power", "origkey"]) 

19def is_factor(key): 

20 return (isinstance(key, Transform) and key.power == 1) 

21def is_power(key): 

22 return (isinstance(key, Transform) and key.power != 1) 

23 

24def get_free_vks(posy, solution): 

25 "Returns all free vks of a given posynomial for a given solution" 

26 return set(vk for vk in posy.vks if vk not in solution["constants"]) 

27 

28def get_model_breakdown(solution): 

29 breakdowns = {"|sensitivity|": 0} 

30 for modelname, senss in solution["sensitivities"]["models"].items(): 

31 senss = abs(senss) # for those monomial equalities 

32 *namespace, name = modelname.split(".") 

33 subbd = breakdowns 

34 subbd["|sensitivity|"] += senss 

35 for parent in namespace: 

36 if parent not in subbd: 

37 subbd[parent] = {parent: {}} 

38 subbd = subbd[parent] 

39 if "|sensitivity|" not in subbd: 

40 subbd["|sensitivity|"] = 0 

41 subbd["|sensitivity|"] += senss 

42 subbd[name] = {"|sensitivity|": senss} 

43 # print(breakdowns["HyperloopSystem"]["|sensitivity|"]) 

44 breakdowns = {"|sensitivity|": 0} 

45 for constraint, senss in solution["sensitivities"]["constraints"].items(): 

46 senss = abs(senss) # for those monomial 

47 if senss <= 1e-5: 

48 continue 

49 subbd = breakdowns 

50 subbd["|sensitivity|"] += senss 

51 for parent in constraint.lineagestr().split("."): 

52 if parent == "": 

53 continue 

54 if parent not in subbd: 

55 subbd[parent] = {} 

56 subbd = subbd[parent] 

57 if "|sensitivity|" not in subbd: 

58 subbd["|sensitivity|"] = 0 

59 subbd["|sensitivity|"] += senss 

60 # treat vectors as namespace 

61 constraint = constraint.str_without({"unnecessary lineage", "units", ":MAGIC:"+constraint.lineagestr()}) 

62 subbd[constraint] = {"|sensitivity|": senss} 

63 for vk in solution.vks: 

64 if vk not in solution["sensitivities"]["variables"]: 

65 continue 

66 senss = abs(solution["sensitivities"]["variables"][vk]) 

67 if hasattr(senss, "shape"): 

68 senss = np.nansum(senss) 

69 if senss <= 1e-5: 

70 continue 

71 subbd = breakdowns 

72 subbd["|sensitivity|"] += senss 

73 for parent in vk.lineagestr().split("."): 

74 if parent == "": 

75 continue 

76 if parent not in subbd: 

77 subbd[parent] = {} 

78 subbd = subbd[parent] 

79 if "|sensitivity|" not in subbd: 

80 subbd["|sensitivity|"] = 0 

81 subbd["|sensitivity|"] += senss 

82 # treat vectors as namespace (indexing vectors above) 

83 vk = vk.str_without({"lineage"}) + get_valstr(vk, solution, " = %s").replace(", fixed", "") 

84 subbd[vk] = {"|sensitivity|": senss} 

85 # TODO: track down in a live-solve environment why this isn't the same 

86 # print(breakdowns["HyperloopSystem"]["|sensitivity|"]) 

87 return breakdowns 

88 

89def crawl_modelbd(bd, name="Model"): 

90 tree = Tree(name, bd.pop("|sensitivity|"), []) 

91 for subname, subtree in sorted(bd.items(), 

92 key=lambda kv: -kv[1]["|sensitivity|"]): 

93 tree.branches.append(crawl_modelbd(subtree, subname)) 

94 return tree 

95 

96def divide_out_vk(vk, pow, lt, gt): 

97 hmap = NomialMap({HashVector({vk: 1}): 1.0}) 

98 hmap.units = vk.units 

99 var = Monomial(hmap)**pow 

100 lt, gt = lt/var, gt/var 

101 lt.ast = gt.ast = None 

102 return lt, gt 

103 

104# @profile 

105def get_breakdowns(solution): 

106 """Returns {key: (lt, gt, constraint)} for breakdown constrain in solution. 

107 

108 A breakdown constraint is any whose "gt" contains a single free variable. 

109 

110 (At present, monomial constraints check both sides as "gt") 

111 """ 

112 breakdowns = defaultdict(list) 

113 beatout = defaultdict(set) 

114 for constraint, senss in sorted(solution["sensitivities"]["constraints"].items(), key=lambda kv: (-abs(kv[1]), str(kv[0]))): 

115 if abs(senss) <= 1e-5: # only tight-ish ones 

116 continue 

117 if constraint.oper == ">=": 

118 gt, lt = (constraint.left, constraint.right) 

119 elif constraint.oper == "<=": 

120 lt, gt = (constraint.left, constraint.right) 

121 elif constraint.oper == "=": 

122 if senss > 0: # l_over_r is more sensitive - see nomials/math.py 

123 lt, gt = (constraint.left, constraint.right) 

124 else: # r_over_l is more sensitive - see nomials/math.py 

125 gt, lt = (constraint.left, constraint.right) 

126 if lt.any_nonpositive_cs or len(gt.hmap) > 1: 

127 continue # no signomials # TODO: approximate signomials at sol 

128 pos_gtvks = {vk for vk, pow in gt.exp.items() if pow > 0} 

129 if len(pos_gtvks) > 1: 

130 pos_gtvks &= get_free_vks(gt, solution) # remove constants 

131 if len(pos_gtvks) == 1: 

132 chosenvk, = pos_gtvks 

133 breakdowns[chosenvk].append((lt, gt, constraint)) 

134 for constraint, senss in sorted(solution["sensitivities"]["constraints"].items(), key=lambda kv: (-abs(kv[1]), str(kv[0]))): 

135 if abs(senss) <= 1e-5: # only tight-ish ones 

136 continue 

137 if constraint.oper == ">=": 

138 gt, lt = (constraint.left, constraint.right) 

139 elif constraint.oper == "<=": 

140 lt, gt = (constraint.left, constraint.right) 

141 elif constraint.oper == "=": 

142 if senss > 0: # l_over_r is more sensitive - see nomials/math.py 

143 lt, gt = (constraint.left, constraint.right) 

144 else: # r_over_l is more sensitive - see nomials/math.py 

145 gt, lt = (constraint.left, constraint.right) 

146 if lt.any_nonpositive_cs or len(gt.hmap) > 1: 

147 continue # no signomials # TODO: approximate signomials at sol 

148 pos_gtvks = {vk for vk, pow in gt.exp.items() if pow > 0} 

149 if len(pos_gtvks) > 1: 

150 pos_gtvks &= get_free_vks(gt, solution) # remove constants 

151 if len(pos_gtvks) != 1: # we'll choose our favorite vk 

152 for vk, pow in gt.exp.items(): 

153 if pow < 0: # remove all non-positive 

154 lt, gt = divide_out_vk(vk, pow, lt, gt) 

155 # bring over common factors from lt 

156 lt_pows = defaultdict(set) 

157 for exp in lt.hmap: 

158 for vk, pow in exp.items(): 

159 lt_pows[vk].add(pow) 

160 for vk, pows in lt_pows.items(): 

161 if len(pows) == 1: 

162 pow, = pows 

163 if pow < 0: # ...but only if they're positive 

164 lt, gt = divide_out_vk(vk, pow, lt, gt) 

165 # don't choose something that's already been broken down 

166 candidatevks = {vk for vk in gt.vks if vk not in breakdowns} 

167 if candidatevks: 

168 vrisk = solution["sensitivities"]["variablerisk"] 

169 chosenvk, *_ = sorted( 

170 candidatevks, 

171 key=lambda vk: (-gt.exp[vk]*vrisk.get(vk, 0), str(vk)) 

172 ) 

173 for vk in gt.vks: 

174 if vk is not chosenvk: 

175 lt, gt = divide_out_vk(vk, pow, lt, gt) 

176 breakdowns[chosenvk].append((lt, gt, constraint)) 

177 breakdowns = dict(breakdowns) # remove the defaultdict-ness 

178 

179 prevlen = None 

180 while len(BASICALLY_FIXED_VARIABLES) != prevlen: 

181 prevlen = len(BASICALLY_FIXED_VARIABLES) 

182 for key in breakdowns: 

183 if key not in BASICALLY_FIXED_VARIABLES: 

184 get_fixity(key, breakdowns, solution, BASICALLY_FIXED_VARIABLES) 

185 return breakdowns 

186 

187BASICALLY_FIXED_VARIABLES = set() 

188 

189 

190def get_fixity(key, bd, solution, basically_fixed=set(), visited=set()): 

191 lt, gt, _ = bd[key][0] 

192 free_vks = get_free_vks(lt, solution).union(get_free_vks(gt, solution)) 

193 for vk in free_vks: 

194 if vk is key or vk in BASICALLY_FIXED_VARIABLES: 

195 continue # currently checking or already checked 

196 if vk not in bd: 

197 return # a very free variable, can't even be broken down 

198 if vk in visited: 

199 return # tried it before, implicitly it didn't work out 

200 # maybe it's basically fixed? 

201 visited.add(key) 

202 get_fixity(vk, bd, solution, basically_fixed, visited) 

203 if vk not in BASICALLY_FIXED_VARIABLES: 

204 return # ...well, we tried 

205 basically_fixed.add(key) 

206 

207SOLCACHE = {} 

208def solcache(solution, key): # replaces solution(key) 

209 if key not in SOLCACHE: 

210 SOLCACHE[key] = solution(key) 

211 return SOLCACHE[key] 

212 

213# @profile # ~84% of total last check # TODO: remove 

214def crawl(key, bd, solution, basescale=1, permissivity=2, verbosity=0, 

215 visited_bdkeys=None, gone_negative=False): 

216 "Returns the tree of breakdowns of key in bd, sorting by solution's values" 

217 if key in bd: 

218 # TODO: do multiple if sensitivities are quite close? 

219 composition, keymon, constraint = bd[key][0] 

220 elif isinstance(key, Posynomial): 

221 composition = key 

222 keymon = None 

223 else: 

224 raise TypeError("the `key` argument must be a VarKey or Posynomial.") 

225 

226 if visited_bdkeys is None: 

227 visited_bdkeys = set() 

228 if verbosity == 1: 

229 solution.set_necessarylineage() 

230 if verbosity: 

231 indent = verbosity-1 # HACK: a bit of overloading, here 

232 kvstr = "%s (%s)" % (key.str_without(["unnecessary lineage", "units"]), 

233 get_valstr(key, solution)) 

234 print(" "*indent + kvstr + ", which breaks down further") 

235 indent += 1 

236 orig_subtree = subtree = [] 

237 tree = Tree(key, basescale, subtree) 

238 visited_bdkeys.add(key) 

239 if keymon is None: 

240 scale = solution(key)/basescale 

241 else: 

242 interesting_vks = {key} 

243 subkey, = interesting_vks 

244 power = keymon.exp[subkey] 

245 boring_vks = set(keymon.vks) - interesting_vks 

246 scale = solution(key)**power/basescale 

247 # TODO: make method that can handle both kinds of transforms 

248 if (power != 1 or boring_vks or mag(keymon.c) != 1 

249 or keymon.units != key.units): # some kind of transform here 

250 units = 1 

251 exp = HashVector() 

252 for vk in interesting_vks: 

253 exp[vk] = keymon.exp[vk] 

254 if vk.units: 

255 units *= vk.units**keymon.exp[vk] 

256 subhmap = NomialMap({exp: 1}) 

257 try: 

258 subhmap.units = None if units == 1 else units 

259 except DimensionalityError: 

260 # pints was unable to divide a unit by itself bc 

261 # it has terrible floating-point errors. 

262 # so let's assume it isn't dimensionless 

263 # even though it probably is 

264 subhmap.units = units 

265 freemon = Monomial(subhmap) 

266 factor = Monomial(keymon/freemon) 

267 scale = scale * solution(factor) 

268 if factor != 1: 

269 factor = factor**(-1/power) # invert the transform 

270 factor.ast = None 

271 if verbosity: 

272 print(" "*indent + "(with a factor of %s (%s))" % 

273 (factor.str_without(["unnecessary lineage", "units"]), 

274 get_valstr(factor, solution))) 

275 subsubtree = [] 

276 transform = Transform(factor, 1, keymon) 

277 orig_subtree.append(Tree(transform, basescale, subsubtree)) 

278 orig_subtree = subsubtree 

279 if power != 1: 

280 if verbosity: 

281 print(" "*indent + "(with a power of %.2g )" % power) 

282 subsubtree = [] 

283 transform = Transform(1, 1/power, keymon) # inverted bc it's on the gt side 

284 orig_subtree.append(Tree(transform, basescale, subsubtree)) 

285 orig_subtree = subsubtree 

286 if verbosity: 

287 if keymon is not None: 

288 print(" "*indent + "in: " 

289 + constraint.str_without(["units", "lineage"])) 

290 print(" "*indent + "by:") 

291 indent += 1 

292 

293 # TODO: use ast_parsing instead of chop? 

294 monsols = [solcache(solution, mon) for mon in composition.chop()] # ~20% of total last check # TODO: remove 

295 parsed_monsols = [getattr(mon, "value", mon) for mon in monsols] 

296 monvals = [float(mon/scale) for mon in parsed_monsols] # ~10% of total last check # TODO: remove 

297 # sort by value, preserving order in case of value tie 

298 sortedmonvals = sorted(zip(monvals, range(len(monvals)), 

299 composition.chop()), reverse=True) 

300 for scaledmonval, _, mon in sortedmonvals: 

301 if not scaledmonval: 

302 continue 

303 subtree = orig_subtree # return to the original subtree 

304 

305 # time for some filtering 

306 interesting_vks = mon.vks 

307 potential_filters = [ 

308 {vk for vk in interesting_vks if vk not in bd}, 

309 mon.vks - get_free_vks(mon, solution), 

310 {vk for vk in interesting_vks if vk in BASICALLY_FIXED_VARIABLES} 

311 ] 

312 if scaledmonval < 1 - permissivity: # skip breakdown filter 

313 potential_filters = potential_filters[1:] 

314 for filter in potential_filters: 

315 if interesting_vks - filter: # don't remove the last one 

316 interesting_vks = interesting_vks - filter 

317 # if filters weren't enough and permissivity is high enough, sort! 

318 if len(interesting_vks) > 1 and permissivity > 1: 

319 csenss = solution["sensitivities"]["constraints"] 

320 best_vks = sorted((vk for vk in interesting_vks if vk in bd), 

321 key=lambda vk: (-mon.exp[vk]*abs(csenss[bd[vk][0][2]]), 

322 str(bd[vk][0][0]))) # ~5% of total last check # TODO: remove 

323 # TODO: changing to str(vk) above does some odd stuff, why? 

324 if best_vks: 

325 interesting_vks = set([best_vks[0]]) 

326 boring_vks = mon.vks - interesting_vks 

327 

328 subkey = None 

329 if len(interesting_vks) == 1: 

330 subkey, = interesting_vks 

331 if subkey in visited_bdkeys and len(sortedmonvals) == 1: 

332 continue # don't even go there 

333 if subkey not in bd: 

334 power = 1 # no need for a transform 

335 else: 

336 power = mon.exp[subkey] 

337 if power < 0 and gone_negative: 

338 subkey = None # don't breakdown another negative 

339 

340 if subkey is None: 

341 power = 1 

342 if scaledmonval > 1 - permissivity and not boring_vks: 

343 boring_vks = interesting_vks 

344 interesting_vks = set() 

345 if not interesting_vks: 

346 # prioritize showing some boring_vks as if they were "free" 

347 if len(boring_vks) == 1: 

348 interesting_vks = boring_vks 

349 boring_vks = set() 

350 else: 

351 for vk in list(boring_vks): 

352 if vk.units and not vk.units.dimensionless: 

353 interesting_vks.add(vk) 

354 boring_vks.remove(vk) 

355 

356 if interesting_vks and (boring_vks or mag(mon.c) != 1): 

357 units = 1 

358 exp = HashVector() 

359 for vk in interesting_vks: 

360 exp[vk] = mon.exp[vk] 

361 if vk.units: 

362 units *= vk.units**mon.exp[vk] 

363 subhmap = NomialMap({exp: 1}) 

364 subhmap.units = None if units is 1 else units 

365 freemon = Monomial(subhmap) 

366 factor = mon/freemon # autoconvert... 

367 if (factor.units is None and isinstance(factor, FixedScalar) 

368 and abs(factor.value - 1) <= 1e-4): 

369 factor = 1 # minor fudge to clear numerical inaccuracies 

370 if factor != 1 : 

371 factor.ast = None 

372 if verbosity: 

373 keyvalstr = "%s (%s)" % (factor.str_without(["unnecessary lineage", "units"]), 

374 get_valstr(factor, solution)) 

375 print(" "*indent + "(with a factor of %s )" % keyvalstr) 

376 subsubtree = [] 

377 transform = Transform(factor, 1, mon) 

378 subtree.append(Tree(transform, scaledmonval, subsubtree)) 

379 subtree = subsubtree 

380 mon = freemon # simplifies units 

381 if power != 1: 

382 if verbosity: 

383 print(" "*indent + "(with a power of %.2g )" % power) 

384 subsubtree = [] 

385 transform = Transform(1, power, mon) 

386 subtree.append(Tree(transform, scaledmonval, subsubtree)) 

387 subtree = subsubtree 

388 mon = mon**(1/power) 

389 mon.ast = None 

390 # TODO: make minscale an argument - currently an arbitrary 0.01 

391 if (subkey is not None and subkey not in visited_bdkeys 

392 and subkey in bd and scaledmonval > 0.05): 

393 if verbosity: 

394 verbosity = indent + 1 # slight hack 

395 subsubtree = crawl(subkey, bd, solution, scaledmonval, 

396 permissivity, verbosity, set(visited_bdkeys), 

397 gone_negative) 

398 subtree.append(subsubtree) 

399 else: 

400 if verbosity: 

401 keyvalstr = "%s (%s)" % (mon.str_without(["unnecessary lineage", "units"]), 

402 get_valstr(mon, solution)) 

403 print(" "*indent + keyvalstr) 

404 subtree.append(Tree(mon, scaledmonval, [])) 

405 if verbosity == 1: 

406 solution.set_necessarylineage(clear=True) 

407 return tree 

408 

409SYMBOLS = string.ascii_uppercase + string.ascii_lowercase 

410for ambiguous_symbol in "lILT": 

411 SYMBOLS = SYMBOLS.replace(ambiguous_symbol, "") 

412 

413def get_spanstr(legend, length, label, leftwards, solution): 

414 "Returns span visualization, collapsing labels to symbols" 

415 if label is None: 

416 return " "*length 

417 spacer, lend, rend = "│", "┯", "┷" 

418 if isinstance(label, Transform): 

419 spacer, lend, rend = "╎", "╤", "╧" 

420 if label.power != 1: 

421 spacer = " " 

422 lend = rend = "^" if label.power > 0 else "/" 

423 # remove origkeys so they collide in the legends dictionary 

424 label = Transform(label.factor, label.power, None) 

425 if label.power == 1 and len(str(label.factor)) == 1: 

426 legend[label] = str(label.factor) 

427 

428 if label not in legend: 

429 legend[label] = SYMBOLS[len(legend)] 

430 shortname = legend[label] 

431 

432 if length <= 1: 

433 return shortname 

434 shortside = int(max(0, length - 2)/2) 

435 longside = int(max(0, length - 3)/2) 

436 if leftwards: 

437 if length == 2: 

438 return lend + shortname 

439 return lend + spacer*shortside + shortname + spacer*longside + rend 

440 else: 

441 if length == 2: 

442 return shortname + rend 

443 # HACK: no corners on long rightwards - only used for depth 0 

444 return "┃"*(longside+1) + shortname + "┃"*(shortside+1) 

445 

446def discretize(tree, extent, solution, collapse, depth=0, justsplit=False): 

447 # TODO: add vertical simplification? 

448 key, val, branches = tree 

449 if collapse: # collapse Transforms with power 1 

450 while any(isinstance(branch.key, Transform) and branch.key.power > 0 for branch in branches): 

451 newbranches = [] 

452 for branch in branches: 

453 # isinstance(branch.key, Transform) and branch.key.power > 0 

454 if isinstance(branch.key, Transform) and branch.key.power > 0: 

455 newbranches.extend(branch.branches) 

456 else: 

457 newbranches.append(branch) 

458 branches = newbranches 

459 

460 scale = extent/val 

461 values = [b.value for b in branches] 

462 bkey_indexs = {} 

463 for i, b in enumerate(branches): 

464 k = get_keystr(b.key, solution) 

465 if isinstance(b.key, Transform): 

466 if len(b.branches) == 1: 

467 k = get_keystr(b.branches[0].key, solution) 

468 if k in bkey_indexs: 

469 values[bkey_indexs[k]] += values[i] 

470 values[i] = None 

471 else: 

472 bkey_indexs[k] = i 

473 if any(v is None for v in values): 

474 branches, values = zip(*((b, v) for b, v in zip(branches, values) if v is not None)) 

475 branches = list(branches) 

476 values = list(values) 

477 extents = [int(round(scale*v)) for v in values] 

478 surplus = extent - sum(extents) 

479 for i, b in enumerate(branches): 

480 if isinstance(b.key, Transform): 

481 subscale = extents[i]/b.value 

482 if not any(round(subscale*subv) for _, subv, _ in b.branches): 

483 extents[i] = 0 # transform with no worthy heirs: misc it 

484 if not any(extents): 

485 return Tree(key, extent, []) 

486 if not all(extents): # create a catch-all 

487 branches = branches.copy() 

488 miscvkeys, miscval = [], 0 

489 for subextent in reversed(extents): 

490 if not subextent or (branches[-1].value < miscval and surplus < 0): 

491 extents.pop() 

492 k, v, _ = branches.pop() 

493 if isinstance(k, Transform): 

494 k = k.origkey # TODO: this is the only use of origkey - remove it 

495 if isinstance(k, tuple): 

496 vkeys = [(-kv[1], str(kv[0]), kv[0]) for kv in k] 

497 if not isinstance(k, tuple): 

498 vkeys = [(-v, str(k), k)] 

499 miscvkeys += vkeys 

500 surplus -= (round(scale*(miscval + v)) 

501 - round(scale*miscval) - subextent) 

502 miscval += v 

503 misckeys = tuple(k for _, _, k in sorted(miscvkeys)) 

504 branches.append(Tree(misckeys, miscval, [])) 

505 extents.append(int(round(scale*miscval))) 

506 if surplus: 

507 sign = int(np.sign(surplus)) 

508 bump_priority = sorted((ext, sign*b.value, i) for i, (b, ext) 

509 in enumerate(zip(branches, extents))) 

510 while surplus: 

511 extents[bump_priority.pop()[-1]] += sign 

512 surplus -= sign 

513 

514 tree = Tree(key, extent, []) 

515 # simplify based on how we're branching 

516 branchfactor = len([ext for ext in extents if ext]) - 1 

517 if depth and not isinstance(key, Transform): 

518 if extent == 1 or branchfactor >= max(extent-2, 1): 

519 # if we'd branch too much, stop 

520 return tree 

521 if collapse and not branchfactor and not justsplit: 

522 # if we didn't just split and aren't about to, skip through 

523 return discretize(branches[0], extent, solution, collapse, 

524 depth=depth+1, justsplit=False) 

525 if branchfactor: 

526 justsplit = True 

527 elif not isinstance(key, Transform): # justsplit passes through transforms 

528 justsplit = False 

529 

530 for branch, subextent in zip(branches, extents): 

531 if subextent: 

532 branch = discretize(branch, subextent, solution, collapse, 

533 depth=depth+1, justsplit=justsplit) 

534 if (collapse and is_power(branch.key) 

535 and all(is_power(b.key) for b in branch.branches)): 

536 # combine stacked powers 

537 power = branch.key.power 

538 for b in branch.branches: 

539 key = Transform(1, power*b.key.power, None) 

540 if key.power == 1: # powers canceled, collapse both 

541 tree.branches.extend(b.branches) 

542 else: # collapse this level 

543 tree.branches.append(Tree(key, b.value, b.branches)) 

544 else: 

545 tree.branches.append(branch) 

546 return tree 

547 

548def layer(map, tree, maxdepth, depth=0): 

549 "Turns the tree into a 2D-array" 

550 key, extent, branches = tree 

551 if depth <= maxdepth: 

552 if len(map) <= depth: 

553 map.append([]) 

554 map[depth].append((key, extent)) 

555 if not branches: 

556 branches = [Tree(None, extent, [])] # pad it out 

557 for branch in branches: 

558 layer(map, branch, maxdepth, depth+1) 

559 return map 

560 

561def plumb(tree, depth=0): 

562 "Finds maximum depth of a tree" 

563 maxdepth = depth 

564 for branch in tree.branches: 

565 maxdepth = max(maxdepth, plumb(branch, depth+1)) 

566 return maxdepth 

567 

568def prune(tree, solution, maxlength, length=4, prefix=""): 

569 "Prune branches that are longer than a certain number of characters" 

570 key, extent, branches = tree 

571 if length == 4 and isinstance(key, VarKey) and key.necessarylineage: 

572 prefix = key.lineagestr() 

573 keylength = max(len(get_valstr(key, solution, into="(%s)")), 

574 len(get_keystr(key, solution, prefix))) 

575 length += keylength + 3 

576 stop_here = False 

577 for branch in branches: 

578 keylength = max(len(get_valstr(branch.key, solution, into="(%s)")), 

579 len(get_keystr(branch.key, solution, prefix))) 

580 branchlength = length + keylength 

581 if branchlength > maxlength: 

582 return Tree(key, extent, []) 

583 return Tree(key, extent, [prune(b, solution, maxlength, length, prefix) 

584 for b in branches]) 

585 

586def simplify(tree, solution, extent, maxdepth, maxlength, collapse): 

587 "Discretize, prune, and layer a tree to prepare for printing" 

588 subtree = discretize(tree, extent, solution, collapse) 

589 if collapse and maxlength: 

590 subtree = prune(subtree, solution, maxlength) 

591 return layer([], subtree, maxdepth) 

592 

593# @profile # ~16% of total last check # TODO: remove 

594def graph(tree, solution, height=None, maxdepth=None, maxwidth=110, 

595 showlegend=False): 

596 "Prints breakdown" 

597 solution.set_necessarylineage() 

598 collapse = (not showlegend) 

599 if maxdepth is None: 

600 maxdepth = plumb(tree) 

601 if height is not None: 

602 mt = simplify(tree, solution, height, maxdepth, maxwidth, collapse) 

603 else: # zoom in from a default height of 20 to a height of 4 per branch 

604 prev_height = None 

605 height = 20 

606 while prev_height != height: 

607 mt = simplify(tree, solution, height, maxdepth, maxwidth, collapse) 

608 prev_height = height 

609 height = min(height, max(*(4*len(at_depth) for at_depth in mt))) 

610 

611 legend = {} 

612 chararray = np.full((len(mt), height), "", "object") 

613 for depth, elements_at_depth in enumerate(mt): 

614 row = "" 

615 for i, (element, length) in enumerate(elements_at_depth): 

616 leftwards = depth > 0 and length > 2 

617 row += get_spanstr(legend, length, element, leftwards, solution) 

618 chararray[depth, :] = list(row) 

619 

620 # Format depth=0 

621 A_key, = [key for key, value in legend.items() if value == "A"] 

622 A_str = get_keystr(A_key, solution) 

623 prefix = "" 

624 if isinstance(A_key, VarKey) and A_key.necessarylineage: 

625 prefix = A_key.lineagestr() 

626 A_valstr = get_valstr(A_key, solution, into="(%s)") 

627 fmt = "{0:>%s}" % (max(len(A_str), len(A_valstr)) + 3) 

628 for j, entry in enumerate(chararray[0,:]): 

629 if entry == "A": 

630 chararray[0,j] = fmt.format(A_str + "╺┫") 

631 chararray[0,j+1] = fmt.format(A_valstr + " ┃") 

632 else: 

633 chararray[0,j] = fmt.format(entry) 

634 # Format depths 1+ 

635 labeled = set() 

636 reverse_legend = {v: k for k, v in legend.items()} 

637 legend = {} 

638 for pos in range(height): 

639 for depth in reversed(range(1,len(mt))): 

640 char = chararray[depth, pos] 

641 if char not in reverse_legend: 

642 continue 

643 key = reverse_legend[char] 

644 if key not in legend and (isinstance(key, tuple) or (depth != len(mt) - 1 and chararray[depth+1, pos] != " ")): 

645 legend[key] = SYMBOLS[len(legend)] 

646 if key in legend: 

647 chararray[depth, pos] = legend[key] 

648 if isinstance(key, tuple) and not isinstance(key, Transform): 

649 chararray[depth, pos] = "*" 

650 if showlegend: 

651 continue 

652 if collapse and is_power(key): 

653 chararray[depth, pos] = "^" if key.power > 0 else "/" 

654 continue 

655 

656 keystr = get_keystr(key, solution, prefix) 

657 if keystr in labeled: 

658 valuestr = "" 

659 else: 

660 valuestr = get_valstr(key, solution, into=" (%s)") 

661 if collapse: 

662 fmt = "{0:<%s}" % max(len(keystr) + 3, len(valuestr) + 2) 

663 else: 

664 fmt = "{0:<1}" 

665 span = 0 

666 tryup, trydn = True, True 

667 while tryup or trydn: 

668 span += 1 

669 if tryup: 

670 if pos - span < 0: 

671 tryup = False 

672 else: 

673 upchar = chararray[depth, pos-span] 

674 if upchar == "│": 

675 chararray[depth, pos-span] = fmt.format("┃") 

676 elif upchar == "┯": 

677 chararray[depth, pos-span] = fmt.format("┓") 

678 else: 

679 tryup = False 

680 if trydn: 

681 if pos + span >= height: 

682 trydn = False 

683 else: 

684 dnchar = chararray[depth, pos+span] 

685 if dnchar == "│": 

686 chararray[depth, pos+span] = fmt.format("┃") 

687 elif dnchar == "┷": 

688 chararray[depth, pos+span] = fmt.format("┛") 

689 else: 

690 trydn = False 

691 linkstr = "┣╸" 

692 if not isinstance(key, FixedScalar): 

693 labeled.add(keystr) 

694 if span > 1 and (collapse or pos + 2 >= height 

695 or chararray[depth, pos+1] == "┃"): 

696 vallabel = chararray[depth, pos+1].rstrip() + valuestr 

697 chararray[depth, pos+1] = fmt.format(vallabel) 

698 elif showlegend: 

699 keystr += valuestr 

700 chararray[depth, pos] = fmt.format(linkstr + keystr) 

701 # Rotate and print 

702 rowstrs = [" " + "".join(row).rstrip() for row in chararray.T.tolist()] 

703 print("\n" + "\n".join(rowstrs) + "\n") 

704 

705 if showlegend: # create and print legend 

706 legend_lines = [] 

707 for key, shortname in sorted(legend.items(), key=lambda kv: kv[1]): 

708 legend_lines.append(legend_entry(key, shortname, solution)) 

709 maxlens = [max(len(el) for el in col) for col in zip(*legend_lines)] 

710 fmts = ["{0:<%s}" % L for L in maxlens] 

711 for line in legend_lines: 

712 line = "".join(fmt.format(cell) 

713 for fmt, cell in zip(fmts, line) if cell).rstrip() 

714 print(" " + line) 

715 

716 solution.set_necessarylineage(clear=True) 

717 

718def legend_entry(key, shortname, solution): 

719 "Returns list of legend elements" 

720 operator = note = "" 

721 keystr = valuestr = " " 

722 operator = "= " if shortname else " + " 

723 if is_factor(key): 

724 operator = " ×" 

725 key = key.factor 

726 free, quasifixed = False, False 

727 if any(vk not in BASICALLY_FIXED_VARIABLES 

728 for vk in get_free_vks(key, solution)): 

729 note = " [free factor]" 

730 if is_power(key): 

731 valuestr = " ^%.3g" % key.power 

732 else: 

733 valuestr = get_valstr(key, solution, into=" "+operator+"%s") 

734 if not isinstance(key, FixedScalar): 

735 keystr = get_keystr(key, solution) 

736 return ["%-4s" % shortname, keystr, valuestr, note] 

737 

738def get_keystr(key, solution, prefix=""): 

739 if key is solution.costposy: 

740 out = "Cost" 

741 elif hasattr(key, "str_without"): 

742 out = key.str_without({"unnecessary lineage", 

743 "units", ":MAGIC:"+prefix}) 

744 elif isinstance(key, tuple): 

745 out = "[%i terms]" % len(key) 

746 else: 

747 out = str(key) 

748 return out if len(out) <= 67 else out[:66]+"…" 

749 

750def get_valstr(key, solution, into="%s"): 

751 "Returns formatted string of the value of key in solution." 

752 # get valuestr 

753 try: 

754 value = solution(key) 

755 except (ValueError, TypeError): 

756 try: 

757 value = sum(solution(subkey) for subkey in key) 

758 except (ValueError, TypeError): 

759 return " " 

760 if isinstance(value, FixedScalar): 

761 value = value.value 

762 if 1e3 <= mag(value) < 1e6: 

763 valuestr = "{:,.0f}".format(mag(value)) 

764 else: 

765 valuestr = "%-.3g" % mag(value) 

766 # get unitstr 

767 if hasattr(key, "unitstr"): 

768 unitstr = key.unitstr() 

769 else: 

770 try: 

771 if hasattr(value, "units"): 

772 value.ito_reduced_units() 

773 except DimensionalityError: 

774 pass 

775 unitstr = get_unitstr(value) 

776 if unitstr[:2] == "1/": 

777 unitstr = "/" + unitstr[2:] 

778 if key in solution["constants"] or ( 

779 hasattr(key, "vks") and key.vks 

780 and all(vk in solution["constants"] for vk in key.vks)): 

781 unitstr += ", fixed" 

782 return into % (valuestr + unitstr) 

783 

784 

785import pickle 

786from gpkit import ureg 

787ureg.define("pax = 1") 

788ureg.define("paxkm = km") 

789ureg.define("trip = 1") 

790 

791print("STARTING...") 

792from gpkit.tests.helpers import StdoutCaptured 

793 

794import difflib 

795 

796permissivity = 2 

797 

798sol = pickle.load(open("solar.p", "rb")) 

799bd = get_breakdowns(sol) 

800mbd = get_model_breakdown(sol) 

801mtree = crawl_modelbd(mbd) 

802# graph(mtree, sol, showlegend=True, height=20) 

803# graph(mtree.branches[0].branches[0].branches[0], sol, showlegend=False, height=20) 

804 

805tree = crawl(sol.costposy, bd, sol, permissivity=2, verbosity=0) 

806graph(tree, sol) 

807 

808 

809# key, = [vk for vk in bd if "Wing.Planform.b" in str(vk)] 

810# tree = crawl(key, bd, sol, permissivity=2, verbosity=1) 

811# graph(tree, sol) 

812# key, = [vk for vk in bd if "Aircraft.Fuselage.R[0,0]" in str(vk)] 

813# tree = crawl(key, bd, sol, permissivity=2, verbosity=1) 

814# graph(tree, sol) 

815# key, = [vk for vk in bd if "Mission.Climb.AircraftDrag.CD[0]" in str(vk)] 

816# tree = crawl(key, bd, sol, permissivity=2, verbosity=1) 

817# graph(tree, sol) 

818 

819 

820keys = sorted(bd.keys(), key=str) 

821 

822# with StdoutCaptured("solarbreakdowns.log"): 

823# graph(mtree, sol, showlegend=False) 

824# graph(mtree, sol, showlegend=True) 

825# tree = crawl(sol.costposy, bd, sol, permissivity=permissivity) 

826# graph(tree, sol) 

827# for key in keys: 

828# tree = crawl(key, bd, sol, permissivity=permissivity) 

829# graph(tree, sol) 

830 

831with StdoutCaptured("solarbreakdowns.log.new"): 

832 graph(mtree, sol, showlegend=False) 

833 graph(mtree, sol, showlegend=True) 

834 tree = crawl(sol.costposy, bd, sol, permissivity=permissivity) 

835 graph(tree, sol) 

836 for key in keys: 

837 tree = crawl(key, bd, sol, permissivity=permissivity) 

838 try: 

839 graph(tree, sol) 

840 except: 

841 raise ValueError(key) 

842 

843with open("solarbreakdowns.log", "r") as original: 

844 with open("solarbreakdowns.log.new", "r") as new: 

845 diff = difflib.unified_diff( 

846 original.readlines(), 

847 new.readlines(), 

848 fromfile="original", 

849 tofile="new", 

850 ) 

851 for line in diff: 

852 print(line[:-1]) 

853 

854print("SOLAR DONE") 

855 

856 

857sol = pickle.load(open("bd.p", "rb")) 

858bd = get_breakdowns(sol) 

859mbd = get_model_breakdown(sol) 

860mtree = crawl_modelbd(mbd) 

861 

862# graph(mtree, sol, showlegend=False) 

863# 

864# # key, = [vk for vk in bd if "podtripenergy[1,0]" in str(vk)] 

865# # tree = crawl(key, bd, sol, permissivity=2, verbosity=1) 

866# # graph(tree, sol, height=40) 

867# tree = crawl(sol.costposy, bd, sol, permissivity=2, verbosity=1) 

868# graph(tree, sol) 

869# # key, = [vk for vk in bd if "statorlaminationmassperpolepair" in str(vk)] 

870# # tree = crawl(key, bd, sol, permissivity=2, verbosity=1) 

871# # graph(tree, sol) 

872# # key, = [vk for vk in bd if "numberofcellsperstring" in str(vk)] 

873# # tree = crawl(key, bd, sol, permissivity=2, verbosity=1) 

874# # graph(tree, sol) 

875# 

876keys = sorted((key for key in bd.keys() if not key.idx or len(key.shape) == 1), 

877 key=lambda k: k.str_without(excluded={})) 

878 

879# with StdoutCaptured("breakdowns.log"): 

880# graph(mtree, sol, showlegend=False) 

881# graph(mtree.branches[0].branches[1], sol, showlegend=False) 

882# graph(mtree, sol, showlegend=True) 

883# tree = crawl(sol.costposy, bd, sol, permissivity=permissivity) 

884# graph(tree, sol) 

885# for key in keys: 

886# tree = crawl(key, bd, sol, permissivity=permissivity) 

887# graph(tree, sol) 

888 

889with StdoutCaptured("breakdowns.log.new"): 

890 graph(mtree, sol, showlegend=False) 

891 graph(mtree.branches[0].branches[1], sol, showlegend=False) 

892 graph(mtree, sol, showlegend=True) 

893 tree = crawl(sol.costposy, bd, sol, permissivity=permissivity) 

894 graph(tree, sol) 

895 for key in keys: 

896 tree = crawl(key, bd, sol, permissivity=permissivity) 

897 try: 

898 graph(tree, sol) 

899 except: 

900 raise ValueError(key) 

901 

902with open("breakdowns.log", "r") as original: 

903 with open("breakdowns.log.new", "r") as new: 

904 diff = difflib.unified_diff( 

905 original.readlines(), 

906 new.readlines(), 

907 fromfile="original", 

908 tofile="new", 

909 ) 

910 for line in diff: 

911 print(line[:-1]) 

912 

913print("DONE")