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
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
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)
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"])
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
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
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
104# @profile
105def get_breakdowns(solution):
106 """Returns {key: (lt, gt, constraint)} for breakdown constrain in solution.
108 A breakdown constraint is any whose "gt" contains a single free variable.
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
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
187BASICALLY_FIXED_VARIABLES = set()
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)
207SOLCACHE = {}
208def solcache(solution, key): # replaces solution(key)
209 if key not in SOLCACHE:
210 SOLCACHE[key] = solution(key)
211 return SOLCACHE[key]
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.")
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
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
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
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
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)
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
409SYMBOLS = string.ascii_uppercase + string.ascii_lowercase
410for ambiguous_symbol in "lILT":
411 SYMBOLS = SYMBOLS.replace(ambiguous_symbol, "")
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)
428 if label not in legend:
429 legend[label] = SYMBOLS[len(legend)]
430 shortname = legend[label]
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)
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
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
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
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
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
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
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])
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)
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)))
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)
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
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")
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)
716 solution.set_necessarylineage(clear=True)
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]
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]+"…"
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)
785import pickle
786from gpkit import ureg
787ureg.define("pax = 1")
788ureg.define("paxkm = km")
789ureg.define("trip = 1")
791print("STARTING...")
792from gpkit.tests.helpers import StdoutCaptured
794import difflib
796permissivity = 2
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)
805tree = crawl(sol.costposy, bd, sol, permissivity=2, verbosity=0)
806graph(tree, sol)
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)
820keys = sorted(bd.keys(), key=str)
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)
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)
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])
854print("SOLAR DONE")
857sol = pickle.load(open("bd.p", "rb"))
858bd = get_breakdowns(sol)
859mbd = get_model_breakdown(sol)
860mtree = crawl_modelbd(mbd)
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={}))
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)
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)
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])
913print("DONE")