Coverage for gpkit/breakdown.py : 0%
![Show keyboard shortcuts](keybd_closed.png)
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# pylint: skip-file
2import string
3from collections import defaultdict, namedtuple
4from gpkit.nomials import Monomial, Posynomial
5from gpkit.nomials.map import NomialMap
6from gpkit.small_scripts import mag
7from gpkit.small_classes import FixedScalar, HashVector
8from gpkit.exceptions import DimensionalityError
9from gpkit.repr_conventions import unitstr as get_unitstr
10import numpy as np
13Transform = namedtuple("Transform", ["factor", "power", "origkey"])
14Tree = namedtuple("Tree", ["key", "value", "branches"])
16def get_free_vks(posy, solution):
17 "Returns all free vks of a given posynomial for a given solution"
18 return set(vk for vk in posy.vks if vk not in solution["constants"])
20def get_model_breakdown(solution):
21 breakdowns = {"|sensitivity|": 0}
22 for modelname, senss in solution["sensitivities"]["models"].items():
23 senss = abs(senss) # for those monomial equalities
24 *namespace, name = modelname.split(".")
25 subbd = breakdowns
26 subbd["|sensitivity|"] += senss
27 for parent in namespace:
28 if parent not in subbd:
29 subbd[parent] = {parent: {}}
30 subbd = subbd[parent]
31 if "|sensitivity|" not in subbd:
32 subbd["|sensitivity|"] = 0
33 subbd["|sensitivity|"] += senss
34 subbd[name] = {"|sensitivity|": senss}
35 print(breakdowns["HyperloopSystem"]["|sensitivity|"])
36 breakdowns = {"|sensitivity|": 0}
37 for constraint, senss in solution["sensitivities"]["constraints"].items():
38 senss = abs(senss) # for those monomial
39 if senss <= 1e-5:
40 continue
41 subbd = breakdowns
42 subbd["|sensitivity|"] += senss
43 for parent in constraint.lineagestr().split("."):
44 if parent not in subbd:
45 subbd[parent] = {}
46 subbd = subbd[parent]
47 if "|sensitivity|" not in subbd:
48 subbd["|sensitivity|"] = 0
49 subbd["|sensitivity|"] += senss
50 # treat vectors as namespace
51 subbd[constraint] = {"|sensitivity|": senss}
52 for vk, senss in solution["sensitivities"]["variables"].items():
53 senss = abs(senss)
54 if hasattr(senss, "shape"):
55 senss = np.nansum(senss)
56 if senss <= 1e-5:
57 continue
58 subbd = breakdowns
59 subbd["|sensitivity|"] += senss
60 for parent in vk.lineagestr().split("."):
61 if parent not in subbd:
62 subbd[parent] = {}
63 subbd = subbd[parent]
64 if "|sensitivity|" not in subbd:
65 subbd["|sensitivity|"] = 0
66 subbd["|sensitivity|"] += senss
67 # treat vectors as namespace (indexing vectors above)
68 subbd[vk] = {"|sensitivity|": senss}
69 print(breakdowns["|sensitivity|"])
70 return breakdowns
72def crawl_modelbd(bd, tree=None, name="Model"):
73 if tree is None:
74 tree = []
75 subtree = []
76 tree.append(Tree(name, bd.pop("|sensitivity|"), subtree))
77 for key, _ in sorted(bd.items(), key=lambda kv: kv[1]["|sensitivity|"], reverse=True):
78 crawl_modelbd(bd[key], subtree, key)
79 return tree[0]
81def get_breakdowns(solution):
82 """Returns {key: (lt, gt, constraint)} for breakdown constrain in solution.
84 A breakdown constraint is any whose "gt" contains a single free variable.
86 (At present, monomial constraints check both sides as "gt")
87 """
88 breakdowns = defaultdict(list)
89 for constraint, senss in solution["sensitivities"]["constraints"].items():
90 if abs(senss) <= 1e-5: # only tight-ish ones
91 continue
92 if constraint.oper == ">=":
93 gt, lt = (constraint.left, constraint.right)
94 elif constraint.oper == "<=":
95 lt, gt = (constraint.left, constraint.right)
96 elif constraint.oper == "=":
97 if senss > 0: # l_over_r is more sensitive - see nomials/math.py
98 lt, gt = [constraint.left, constraint.right]
99 else: # r_over_l is more sensitive - see nomials/math.py
100 gt, lt = [constraint.left, constraint.right]
101 if lt.any_nonpositive_cs:
102 continue # no signomials
103 freegt_vks = get_free_vks(gt, solution)
104 if len(freegt_vks) != 1:
105 continue # not a breakdown constraint
106 brokendownvk, = freegt_vks
107 if gt.exp[brokendownvk] < 0:
108 if constraint.oper == "=" or len(lt.hmap) != 1:
109 continue
110 # We can try flipping gt/lt to make a breakdown.
111 freelt_vks = get_free_vks(lt, solution)
112 if len(lt.hmap) != 1 or len(freelt_vks) != 1:
113 continue
114 brokendownvk, = freelt_vks
115 if lt.exp[brokendownvk] > 0:
116 continue # not a breakdown constraint after transformation
117 gt, lt = 1/lt, 1/gt
118 breakdowns[brokendownvk].append((lt, gt, constraint))
119 breakdowns = dict(breakdowns) # remove the defaultdict-ness
120 for key, bds in breakdowns.items():
121 # TODO: do multiple if sensitivities are quite close? right now we have to break ties!
122 if len(bds) > 1:
123 bds.sort(key=lambda lgc: (abs(solution["sensitivities"]["constraints"][lgc[2]]), str(lgc[0])), reverse=True)
125 prevlen = None
126 while len(BASICALLY_FIXED_VARIABLES) != prevlen:
127 prevlen = len(BASICALLY_FIXED_VARIABLES)
128 for key in breakdowns:
129 if key not in BASICALLY_FIXED_VARIABLES:
130 get_fixity(key, breakdowns, solution, BASICALLY_FIXED_VARIABLES)
131 return breakdowns
133BASICALLY_FIXED_VARIABLES = set()
136def get_fixity(key, bd, solution, basically_fixed=set(), visited=set()):
137 lt, gt, _ = bd[key][0]
138 free_vks = get_free_vks(lt, solution).union(get_free_vks(gt, solution))
139 for vk in free_vks:
140 if vk not in bd:
141 return
142 if vk in BASICALLY_FIXED_VARIABLES:
143 continue
144 if vk is key:
145 continue
146 if vk in visited: # been here before and it's not me
147 return
148 visited.add(key)
149 get_fixity(vk, bd, solution, basically_fixed, visited)
150 if vk not in BASICALLY_FIXED_VARIABLES:
151 return
152 basically_fixed.add(key)
154def crawl(key, bd, solution, basescale=1, permissivity=2, verbosity=0,
155 visited_bdkeys=None):
156 "Returns the tree of breakdowns of key in bd, sorting by solution's values"
157 if key in bd:
158 # TODO: do multiple if sensitivities are quite close?
159 composition, keymon, constraint = bd[key][0]
160 elif isinstance(key, Posynomial):
161 composition = key
162 keymon = None
163 else:
164 raise TypeError("the `key` argument must be a VarKey or Posynomial.")
166 if visited_bdkeys is None:
167 visited_bdkeys = set()
168 if verbosity:
169 indent = verbosity-1 # HACK: a bit of overloading, here
170 keyvalstr = "%s (%s)" % (key.str_without(["lineage", "units"]),
171 get_valstr(key, solution))
172 print(" "*indent + keyvalstr + ", which breaks down further")
173 indent += 1
174 orig_subtree = subtree = []
175 tree = Tree(key, basescale, subtree)
176 visited_bdkeys.add(key)
177 if keymon is None:
178 scale = solution(key)/basescale
179 else:
180 free_vks = get_free_vks(keymon, solution)
181 subkey, = free_vks
182 power = keymon.exp[subkey]
183 fixed_vks = set(keymon.vks) - free_vks
184 scale = solution(key)**power/basescale
185 # TODO: make method that can handle both kinds of transforms
186 if power != 1 or fixed_vks or mag(keymon.c) != 1 or keymon.units != key.units:
187 units = 1
188 exp = HashVector()
189 for vk in free_vks:
190 exp[vk] = keymon.exp[vk]
191 if vk.units:
192 units *= vk.units**keymon.exp[vk]
193 subhmap = NomialMap({exp: 1})
194 subhmap.units = None if units == 1 else units
195 freemon = Monomial(subhmap)
196 factor = Monomial(keymon/freemon)
197 scale = scale * solution(factor)
198 if factor != 1:
199 factor = factor**(-1/power) # invert the transform
200 factor.ast = None
201 if verbosity:
202 keyvalstr = "%s (%s)" % (factor.str_without(["lineage", "units"]),
203 get_valstr(factor, solution))
204 print(" "*indent + "(with a factor of " + keyvalstr + " )")
205 subsubtree = []
206 transform = Transform(factor, 1, keymon)
207 orig_subtree.append(Tree(transform, basescale, subsubtree))
208 orig_subtree = subsubtree
209 if power != 1:
210 if verbosity:
211 print(" "*indent + "(with a power of %.2g )" % power)
212 subsubtree = []
213 transform = Transform(1, 1/power, keymon) # inverted bc it's on the gt side
214 orig_subtree.append(Tree(transform, basescale, subsubtree))
215 orig_subtree = subsubtree
216 if verbosity:
217 if keymon is not None:
218 print(" "*indent + "in: " + constraint.str_without(["units", "lineage"]))
219 print(" "*indent + "by:")
220 indent += 1
222 # TODO: use ast_parsing instead of chop?
223 monsols = [solution(mon) for mon in composition.chop()]
224 parsed_monsols = [getattr(mon, "value", mon) for mon in monsols]
225 monvals = [float(mon/scale) for mon in parsed_monsols]
226 # sort by value, preserving order in case of value tie
227 sortedmonvals = sorted(zip(monvals, range(len(monvals)),
228 composition.chop()), reverse=True)
229 for scaledmonval, _, mon in sortedmonvals:
230 if not scaledmonval:
231 continue
232 scaledmonval = min(1, scaledmonval) # clip it
233 subtree = orig_subtree # revert back to the original subtree
235 # time for some filtering
236 free_vks = get_free_vks(mon, solution)
237 basically_fixed_vks = {vk for vk in free_vks
238 if vk in BASICALLY_FIXED_VARIABLES}
239 if free_vks - basically_fixed_vks: # don't remove the last one
240 free_vks = free_vks - basically_fixed_vks
242 if scaledmonval > 1 - permissivity:
243 unbreakdownable_vks = {vk for vk in free_vks if vk not in bd}
244 if free_vks - unbreakdownable_vks: # don't remove the last one
245 free_vks = free_vks - unbreakdownable_vks
247 if len(free_vks) > 1 and permissivity > 1:
248 best_vks = sorted((vk for vk in free_vks if vk in bd),
249 key=lambda vk:
250 (abs(solution["sensitivities"]["constraints"][bd[vk][0][2]]),
251 str(bd[vk][0][0])), reverse=True)
252 if best_vks:
253 free_vks = set([best_vks[0]])
254 fixed_vks = mon.vks - free_vks
256 if len(free_vks) == 1:
257 subkey, = free_vks
258 power = mon.exp[subkey]
259 if power != 1 and subkey not in bd:
260 power = 1 # no need for a transform
261 else:
262 subkey = None
263 power = 1
264 if not free_vks:
265 # prioritize showing some fixed_vks as if they were "free"
266 if len(fixed_vks) == 1:
267 free_vks = fixed_vks
268 fixed_vks = set()
269 else:
270 for vk in list(fixed_vks):
271 if vk.units and not vk.units.dimensionless:
272 free_vks.add(vk)
273 fixed_vks.remove(vk)
275 if free_vks and (fixed_vks or mag(mon.c) != 1):
276 if subkey:
277 kindafree_vks = set(vk for vk in fixed_vks
278 if vk not in solution["constants"])
279 if kindafree_vks == fixed_vks:
280 kindafree_vks = set() # don't remove ALL of them
281 else:
282 free_vks.update(kindafree_vks)
283 units = 1
284 exp = HashVector()
285 for vk in free_vks:
286 exp[vk] = mon.exp[vk]
287 if vk.units:
288 units *= vk.units**mon.exp[vk]
289 subhmap = NomialMap({exp: 1})
290 subhmap.units = None if units is 1 else units
291 freemon = Monomial(subhmap)
292 factor = mon/freemon # autoconvert...
293 if (factor.units is None and isinstance(factor, FixedScalar)
294 and abs(factor.value - 1) <= 1e-4):
295 factor = 1 # minor fudge to clear numerical inaccuracies
296 if factor != 1 :
297 factor.ast = None
298 if verbosity:
299 keyvalstr = "%s (%s)" % (factor.str_without(["lineage", "units"]),
300 get_valstr(factor, solution))
301 print(" "*indent + "(with a factor of %s )" % keyvalstr)
302 subsubtree = []
303 transform = Transform(factor, 1, mon)
304 subtree.append(Tree(transform, scaledmonval, subsubtree))
305 subtree = subsubtree
306 mon = freemon # simplifies units
307 if subkey and fixed_vks and kindafree_vks:
308 units = 1
309 exp = HashVector()
310 for vk in kindafree_vks:
311 exp[vk] = mon.exp[vk]
312 if vk.units:
313 units *= vk.units**mon.exp[vk]
314 subhmap = NomialMap({exp: 1})
315 subhmap.units = None if units == 1 else units
316 factor = Monomial(subhmap)
317 if factor != 1:
318 factor.ast = None
319 if verbosity:
320 keyvalstr = "%s (%s)" % (factor.str_without(["lineage", "units"]),
321 get_valstr(factor, solution))
322 print(" "*indent + "(with a factor of " + keyvalstr + " )")
323 subsubtree = []
324 transform = Transform(factor, 1, mon)
325 subtree.append(Tree(transform, scaledmonval, subsubtree))
326 subtree = subsubtree
327 mon = mon/factor
328 mon.ast = None
329 if power != 1:
330 if verbosity:
331 print(" "*indent + "(with a power of %.2g )" % power)
332 subsubtree = []
333 transform = Transform(1, power, mon)
334 subtree.append(Tree(transform, scaledmonval, subsubtree))
335 subtree = subsubtree
336 mon = mon**(1/power)
337 mon.ast = None
338 # TODO: make minscale an argument - currently an arbitrary 0.01
339 if (subkey is not None and subkey not in visited_bdkeys
340 and subkey in bd and scaledmonval > 0.01):
341 if verbosity:
342 verbosity = indent + 1 # slight hack
343 try:
344 subsubtree = crawl(subkey, bd, solution, scaledmonval,
345 permissivity, verbosity, visited_bdkeys)
346 subtree.append(subsubtree)
347 continue
348 except Exception as e:
349 print(repr(e))
351 if verbosity:
352 keyvalstr = "%s (%s)" % (mon.str_without(["lineage", "units"]),
353 get_valstr(mon, solution))
354 print(" "*indent + keyvalstr)
355 subtree.append(Tree(mon, scaledmonval, []))
356 # clean up node combination - use a dictionary at the subtree level?
357 ftidxs = defaultdict(list)
358 for i, node in enumerate(orig_subtree):
359 if isinstance(node.key, Transform):
360 ftidxs[(node.key.factor, node.key.power)].append(i)
361 to_delete = []
362 to_insert = []
363 for idxs in ftidxs.values():
364 if len(idxs) > 1:
365 valsum = 0
366 newbranches = []
367 new_origkeys = []
368 for idx in idxs:
369 key, val, subbranches = orig_subtree[idx]
370 valsum += val
371 new_origkeys.append((key.origkey, val))
372 newbranches.extend(subbranches)
373 to_delete.extend(idxs)
374 newkey = Transform(key.factor, key.power, tuple(new_origkeys))
375 to_insert.append(Tree(newkey, valsum, newbranches))
376 for idx in sorted(to_delete, reverse=True): # high to low
377 orig_subtree.pop(idx)
378 if to_insert:
379 orig_subtree.extend(to_insert)
380 orig_subtree.sort(reverse=True, key=lambda branch: branch.value)
381 return tree
383SYMBOLS = string.ascii_uppercase + string.ascii_lowercase
384for ambiguous_symbol in "lILT":
385 SYMBOLS = SYMBOLS.replace(ambiguous_symbol, "")
386SYMBOLS += "⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵"
388def get_spanstr(legend, length, label, leftwards, solution):
389 "Returns span visualization, collapsing labels to symbols"
390 spacer, lend, rend = "│", "┯", "┷"
391 if isinstance(label, Transform):
392 spacer, lend, rend = "╎", "╤", "╧"
393 if label.power != 1:
394 spacer, lend, rend = "^", "^", "^"
395 # remove origkey so they collide in the legends dictionary
396 label = Transform(label.factor, label.power, None)
397 # TODO: catch PI (or again could that come from AST parsing?)
398 if label.power == 1 and len(str(label.factor)) == 1:
399 legend[label] = str(label.factor)
401 if label not in legend:
402 shortname = SYMBOLS[len(legend)]
403 legend[label] = shortname
404 else:
405 shortname = legend[label]
407 if length <= 1:
408 return shortname
410 shortside = int(max(0, length - 2)/2)
411 longside = int(max(0, length - 3)/2)
412 if leftwards:
413 if length == 2:
414 return lend + shortname
415 return lend + spacer*shortside + shortname + spacer*longside + rend
416 else:
417 if length == 2:
418 return shortname + rend
419 # HACK: no corners on rightwards - only used for depth 0
420 return "┃"*(longside+1) + shortname + "┃"*(shortside+1)
422def layer(map, tree, extent, maxdepth, depth=0):
423 "Turns the tree into a 2D-array"
424 key, val, branches = tree
425 if len(map) <= depth:
426 map.append([])
427 map[depth].append((key, extent))
428 if depth > maxdepth or not val:
429 return map
430 scale = extent/val
432 extents = [round(scale*node.value) for node in branches]
433 for i, branch in enumerate(branches):
434 k, v, bs = branch
435 if isinstance(k, Transform):
436 subscale = extents[i]/v
437 if not any(round(subscale*subv) for _, subv, _ in bs):
438 extents[i] = 0 # transform with no worthy heirs: misc it
440 if not any(extents) or (extent == 1 and not isinstance(key, Transform)):
441 branches = [Tree(None, val, [])] # pad it out
442 extents = [extent]
443 elif not all(extents): # create a catch-all
444 branches = branches.copy()
445 surplus = extent - sum(extents)
446 miscvkeys, miscval = [], 0
447 for subextent in reversed(extents):
448 if not subextent or (branches[-1].value < miscval and surplus < 0):
449 k, v, _ = branches.pop()
450 if isinstance(k, Transform):
451 k = k.origkey # TODO: this is the only use of origkey - remove it
452 if isinstance(k, tuple):
453 vkeys = [(-kv[1], str(kv[0]), kv[0]) for kv in k]
454 if not isinstance(k, tuple):
455 vkeys = [(-v, str(k), k)]
456 miscvkeys += vkeys
457 surplus -= (round(scale*(miscval + v))
458 - round(scale*miscval) - subextent)
459 miscval += v
460 misckeys = tuple(k for _, _, k in sorted(miscvkeys))
461 branches.append(Tree(misckeys, miscval, []))
463 extents = [int(round(scale*node.value)) for node in branches]
464 surplus = extent - sum(extents)
465 if surplus:
466 sign = int(np.sign(surplus))
467 bump_priority = sorted((extent, sign*node.value, i)
468 for i, (node, extent)
469 in enumerate(zip(branches, extents)))
470 while surplus:
471 extents[bump_priority.pop()[-1]] += sign
472 surplus -= sign
473 if not isinstance(key, Transform):
474 if len([ext for ext in extents if ext]) >= max(extent-1, 2):
475 # if we'd branch a lot (all ones but one, or at all if extent <= 3)
476 branches = [Tree(None, val, [])]
477 extents = [extent]
478 for branch, subextent in zip(branches, extents):
479 if subextent:
480 layer(map, branch, subextent, maxdepth, depth+1)
481 return map
483def plumb(tree, depth=0):
484 "Finds maximum depth of a tree"
485 maxdepth = depth
486 for branch in tree.branches:
487 maxdepth = max(maxdepth, plumb(branch, depth+1))
488 return maxdepth
490def graph(tree, solution, extent=None, maxdepth=None, collapse=True):
491 "Prints breakdown"
492 if maxdepth is None:
493 maxdepth = plumb(tree) - 1
494 if extent is None: # autozoom in from 20
495 prev_extent = None
496 extent = 20
497 while prev_extent != extent:
498 mt = layer([], tree, extent, maxdepth)
499 prev_extent = extent
500 extent = min(extent, 4*max(len(at_depth) for at_depth in mt))
501 else:
502 mt = layer([], tree, extent, maxdepth)
503 scale = 1/extent
504 legend = {}
505 chararray = np.full((len(mt), extent), "", "object")
506 for depth, elements_at_depth in enumerate(mt):
507 row = ""
508 for i, (element, length) in enumerate(elements_at_depth):
509 if element is None:
510 row += " "*length
511 continue
512 leftwards = depth > 0 and length > 2
513 row += get_spanstr(legend, length, element, leftwards, solution)
514 if row.strip():
515 chararray[depth, :] = list(row)
517 # Format depth=0
518 A_key, = [key for key, value in legend.items() if value == "A"]
519 A_str = get_keystr(A_key, solution)
520 A_valstr = get_valstr(A_key, solution, into="(%s)")
521 fmt = "{0:>%s}" % (max(len(A_str), len(A_valstr)) + 3)
522 for j, entry in enumerate(chararray[0,:]):
523 if entry == "A":
524 chararray[0,j] = fmt.format(A_str + "╺┫")
525 chararray[0,j+1] = fmt.format(A_valstr + " ┃")
526 else:
527 chararray[0,j] = fmt.format(entry)
529 # Format depths 1+
530 labeled = set()
531 new_legend = {}
532 for pos in range(extent):
533 for depth in reversed(range(1,len(mt))):
534 value = chararray[depth, pos]
535 if value == " ": # spacer character
536 chararray[depth, pos] = ""
537 continue
538 elif not value or value not in SYMBOLS:
539 continue
540 key, = [k for k, val in legend.items() if val == value]
541 if getattr(key, "vks", None) and len(key.vks) == 1 and all(vk in new_legend for vk in key.vks):
542 key, = key.vks
543 if key not in new_legend and (isinstance(key, tuple) or (depth != len(mt) - 1 and chararray[depth+1, pos] != "")):
544 new_legend[key] = SYMBOLS[len(new_legend)]
545 if key in new_legend:
546 chararray[depth, pos] = new_legend[key]
547 if isinstance(key, tuple) and not isinstance(key, Transform):
548 chararray[depth, pos] = "*" + chararray[depth, pos]
549 if collapse:
550 continue
551 tryup, trydn = True, True
552 span = 0
553 keystr = get_keystr(key, solution)
554 if not collapse:
555 fmt = "{0:<%s}" % (len(keystr) + 3)
556 else:
557 fmt = "{0:<1}"
558 while tryup or trydn:
559 span += 1
560 if tryup:
561 if pos - span < 0:
562 tryup = False
563 else:
564 upchar = chararray[depth, pos-span]
565 if upchar == "│":
566 chararray[depth, pos-span] = fmt.format("┃")
567 elif upchar == "┯":
568 chararray[depth, pos-span] = fmt.format("┓")
569 else:
570 tryup = False
571 if trydn:
572 if pos + span >= extent:
573 trydn = False
574 else:
575 dnchar = chararray[depth, pos+span]
576 if dnchar == "│":
577 chararray[depth, pos+span] = fmt.format("┃")
578 elif dnchar == "┷":
579 chararray[depth, pos+span] = fmt.format("┛")
580 else:
581 trydn = False
582 #TODO: make submodels show up with this; bd should be an argument
583 if key in bd or (hasattr(key, "vks") and key.vks and any(vk in bd for vk in key.vks)):
584 linkstr = "┣┉"
585 else:
586 linkstr = "┣╸"
587 if not (isinstance(key, FixedScalar) or keystr in labeled):
588 labeled.add(keystr)
589 valuestr = get_valstr(key, solution, into=" (%s)")
590 if span > 1 and (pos + 2 >= extent or chararray[depth, pos+1] == "┃"):
591 chararray[depth, pos+1] += valuestr
592 else:
593 keystr += valuestr
594 chararray[depth, pos] = linkstr + keystr
596 # Rotate and print
597 vertstr = "\n".join(" " + "".join(row) for row in chararray.T.tolist())
598 print()
599 print(vertstr)
600 print()
601 legend = new_legend
603 # Create and print legend
604 if collapse:
605 legend_lines = []
606 for key, shortname in sorted(legend.items(), key=lambda kv: kv[1]):
607 legend_lines.append(legend_entry(key, shortname, solution))
608 maxlens = [max(len(el) for el in col) for col in zip(*legend_lines)]
609 fmts = ["{0:<%s}" % L for L in maxlens]
610 for line in legend_lines:
611 line = "".join(fmt.format(cell)
612 for fmt, cell in zip(fmts, line) if cell).rstrip()
613 print(" " + line)
615def legend_entry(key, shortname, solution):
616 "Returns list of legend elements"
617 operator = note = ""
618 keystr = valuestr = " "
619 operator = "= " if shortname else " + "
620 if isinstance(key, Transform):
621 if key.power == 1:
622 operator = " ×"
623 key = key.factor
624 free, quasifixed = False, False
625 if any(vk not in BASICALLY_FIXED_VARIABLES
626 for vk in get_free_vks(key, solution)):
627 note = " [free factor]"
628 else:
629 valuestr = " ^%.3g" % key.power
630 key = None
631 if key is not None:
632 if not isinstance(key, FixedScalar):
633 keystr = get_keystr(key, solution)
634 valuestr = get_valstr(key, solution, into=" "+operator+"%s")
635 return ["%-4s" % shortname, keystr, valuestr, note]
637def get_keystr(key, solution):
638 if key is None:
639 out = " "
640 elif key is solution.costposy:
641 out = "Cost"
642 elif hasattr(key, "str_without"):
643 out = key.str_without(["lineage", "units"])
644 elif isinstance(key, tuple):
645 return "(%i entries)" % len(key)
646 else:
647 out = str(key)
648 # TODO: use fixedfactors to drop below 50
649 if len(out) > 120:
650 out = out[:120]+"…"
651 return out
653def get_valstr(key, solution, into="%s"):
654 "Returns formatted string of the value of key in solution."
655 try:
656 value = solution(key)
657 except (ValueError, TypeError):
658 try:
659 value = sum(solution(subkey) for subkey in key)
660 except (ValueError, TypeError):
661 return " "
662 if isinstance(value, FixedScalar):
663 value = value.value
664 if hasattr(key, "unitstr"):
665 unitstr = key.unitstr()
666 else:
667 if hasattr(value, "units"):
668 value.ito_reduced_units()
669 unitstr = get_unitstr(value)
670 if unitstr[:2] == "1/":
671 unitstr = "/" + unitstr[2:]
672 value = mag(value)
673 if 1e3 <= value < 1e6:
674 valuestr = "{:,.0f}".format(value)
675 else:
676 valuestr = "%-.3g" % value
677 return into % (valuestr + unitstr)
680import pickle
681from gpkit import ureg
682ureg.define("pax = 1")
683ureg.define("paxkm = km")
684ureg.define("trip = 1")
686print("STARTING...")
688sol = pickle.load(open("bd.p", "rb"))
690bd = get_breakdowns(sol)
692mbd = get_model_breakdown(sol)
693tree = crawl_modelbd(mbd)
694graph(tree, sol, maxdepth=2, extent=20, collapse=False)
696graph(tree.branches[0].branches[1],
697 sol, maxdepth=2, extent=20, collapse=False)
699graph(tree, sol, maxdepth=2, extent=20)
701from gpkit.tests.helpers import StdoutCaptured
703import difflib
705# key, = [vk for vk in bd if "cruisevelocity[0]" in str(vk)]
706# tree = crawl(key, bd, sol, permissivity=2, verbosity=1)
707# graph(tree, sol)
708#
709# print("\n\nPERMISSIVITY = 0")
710# tree = crawl(sol.costposy, bd, sol, permissivity=0)
711# graph(tree, sol)
712#
713# print("\n\nPERMISSIVITY = 0.6")
714# tree = crawl(sol.costposy, bd, sol, permissivity=0.6)
715# graph(tree, sol)
716#
717# print("\n\nPERMISSIVITY = 1")
718# tree = crawl(sol.costposy, bd, sol, permissivity=1)
719# graph(tree, sol)
720#
721print("\n\nPERMISSIVITY = 2")
722tree = crawl(sol.costposy, bd, sol, permissivity=2)
723graph(tree, sol)
725keys = sorted((key for key in bd.keys() if not key.idx or len(key.shape) == 1),
726 key=lambda k: str(k))
728permissivity = 2
730# with StdoutCaptured("breakdowns.log"):
731# for key in keys:
732# tree = crawl(key, bd, sol, permissivity=permissivity)
733# graph(tree, sol)
735# with StdoutCaptured("breakdowns.log.new"):
736# for key in keys:
737# tree = crawl(key, bd, sol, permissivity=permissivity)
738# graph(tree, sol)
739#
740# with open("breakdowns.log", "r") as original:
741# with open("breakdowns.log.new", "r") as new:
742# diff = difflib.unified_diff(
743# original.readlines(),
744# new.readlines(),
745# fromfile="original",
746# tofile="new",
747# )
748# for line in diff:
749# print(line[:-1])
751print("DONE")