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#TODO: add depth-culling to simplify
2# clean up factor generation
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 for vk in gt.vks:
132 if vk.name == "podturnaroundtime":
133 print(constraint, senss, len(pos_gtvks))
134 if len(pos_gtvks) == 1:
135 chosenvk, = pos_gtvks
136 breakdowns[chosenvk].append((lt, gt, constraint))
137 for constraint, senss in sorted(solution["sensitivities"]["constraints"].items(), key=lambda kv: (-abs(kv[1]), str(kv[0]))):
138 if abs(senss) <= 1e-5: # only tight-ish ones
139 continue
140 if constraint.oper == ">=":
141 gt, lt = (constraint.left, constraint.right)
142 elif constraint.oper == "<=":
143 lt, gt = (constraint.left, constraint.right)
144 elif constraint.oper == "=":
145 if senss > 0: # l_over_r is more sensitive - see nomials/math.py
146 lt, gt = (constraint.left, constraint.right)
147 else: # r_over_l is more sensitive - see nomials/math.py
148 gt, lt = (constraint.left, constraint.right)
149 if lt.any_nonpositive_cs or len(gt.hmap) > 1:
150 continue # no signomials # TODO: approximate signomials at sol
151 pos_gtvks = {vk for vk, pow in gt.exp.items() if pow > 0}
152 if len(pos_gtvks) > 1:
153 pos_gtvks &= get_free_vks(gt, solution) # remove constants
154 if len(pos_gtvks) != 1: # we'll choose our favorite vk
155 for vk, pow in gt.exp.items():
156 if pow < 0: # remove all non-positive
157 lt, gt = divide_out_vk(vk, pow, lt, gt)
158 # bring over common factors from lt
159 lt_pows = defaultdict(set)
160 for exp in lt.hmap:
161 for vk, pow in exp.items():
162 lt_pows[vk].add(pow)
163 for vk, pows in lt_pows.items():
164 if len(pows) == 1:
165 pow, = pows
166 if pow < 0: # ...but only if they're positive
167 lt, gt = divide_out_vk(vk, pow, lt, gt)
168 # don't choose something that's already been broken down
169 candidatevks = {vk for vk in gt.vks if vk not in breakdowns}
170 if candidatevks:
171 chosenvk, *_ = sorted(candidatevks, key=lambda vk: (-gt.exp[vk]*solution["sensitivities"]["variablerisk"].get(vk, 0), str(vk)))
172 for vk in gt.vks:
173 if vk is not chosenvk:
174 lt, gt = divide_out_vk(vk, pow, lt, gt)
175 breakdowns[chosenvk].append((lt, gt, constraint))
176 breakdowns = dict(breakdowns) # remove the defaultdict-ness
178 prevlen = None
179 while len(BASICALLY_FIXED_VARIABLES) != prevlen:
180 prevlen = len(BASICALLY_FIXED_VARIABLES)
181 for key in breakdowns:
182 if key not in BASICALLY_FIXED_VARIABLES:
183 get_fixity(key, breakdowns, solution, BASICALLY_FIXED_VARIABLES)
184 return breakdowns
186BASICALLY_FIXED_VARIABLES = set()
189def get_fixity(key, bd, solution, basically_fixed=set(), visited=set()):
190 lt, gt, _ = bd[key][0]
191 free_vks = get_free_vks(lt, solution).union(get_free_vks(gt, solution))
192 for vk in free_vks:
193 if vk is key or vk in BASICALLY_FIXED_VARIABLES:
194 continue # currently checking or already checked
195 if vk not in bd:
196 return # a very free variable, can't even be broken down
197 if vk in visited:
198 return # tried it before, implicitly it didn't work out
199 # maybe it's basically fixed?
200 visited.add(key)
201 get_fixity(vk, bd, solution, basically_fixed, visited)
202 if vk not in BASICALLY_FIXED_VARIABLES:
203 return # ...well, we tried
204 basically_fixed.add(key)
206SOLCACHE = {}
207def solcache(solution, key): # replaces solution(key)
208 if key not in SOLCACHE:
209 SOLCACHE[key] = solution(key)
210 return SOLCACHE[key]
212# @profile # ~84% of total last check # TODO: remove
213def crawl(key, bd, solution, basescale=1, permissivity=2, verbosity=0,
214 visited_bdkeys=None, gone_negative=False):
215 "Returns the tree of breakdowns of key in bd, sorting by solution's values"
216 if key in bd:
217 # TODO: do multiple if sensitivities are quite close?
218 composition, keymon, constraint = bd[key][0]
219 elif isinstance(key, Posynomial):
220 composition = key
221 keymon = None
222 else:
223 raise TypeError("the `key` argument must be a VarKey or Posynomial.")
225 if visited_bdkeys is None:
226 visited_bdkeys = set()
227 if verbosity == 1:
228 solution.set_necessarylineage()
229 if verbosity:
230 indent = verbosity-1 # HACK: a bit of overloading, here
231 keyvalstr = "%s (%s)" % (key.str_without(["unnecessary lineage", "units"]),
232 get_valstr(key, solution))
233 print(" "*indent + keyvalstr + ", which breaks down further")
234 indent += 1
235 orig_subtree = subtree = []
236 tree = Tree(key, basescale, subtree)
237 visited_bdkeys.add(key)
238 if keymon is None:
239 scale = solution(key)/basescale
240 else:
241 interesting_vks = {key}
242 subkey, = interesting_vks
243 power = keymon.exp[subkey]
244 boring_vks = set(keymon.vks) - interesting_vks
245 scale = solution(key)**power/basescale
246 # TODO: make method that can handle both kinds of transforms
247 if power != 1 or boring_vks or mag(keymon.c) != 1 or keymon.units != key.units:
248 units = 1
249 exp = HashVector()
250 for vk in interesting_vks:
251 exp[vk] = keymon.exp[vk]
252 if vk.units:
253 units *= vk.units**keymon.exp[vk]
254 subhmap = NomialMap({exp: 1})
255 try:
256 subhmap.units = None if units == 1 else units
257 except DimensionalityError:
258 # pints was unable to divide a unit by itself bc
259 # it has terrible floating-point errors.
260 # so let's assume it isn't dimensionless
261 # even though it probably is
262 subhmap.units = units
263 freemon = Monomial(subhmap)
264 factor = Monomial(keymon/freemon)
265 scale = scale * solution(factor)
266 if factor != 1:
267 factor = factor**(-1/power) # invert the transform
268 factor.ast = None
269 if verbosity:
270 keyvalstr = "%s (%s)" % (factor.str_without(["unnecessary lineage", "units"]),
271 get_valstr(factor, solution))
272 print(" "*indent + "(with a factor of " + keyvalstr + " )")
273 subsubtree = []
274 transform = Transform(factor, 1, keymon)
275 orig_subtree.append(Tree(transform, basescale, subsubtree))
276 orig_subtree = subsubtree
277 if power != 1:
278 if verbosity:
279 print(" "*indent + "(with a power of %.2g )" % power)
280 subsubtree = []
281 transform = Transform(1, 1/power, keymon) # inverted bc it's on the gt side
282 orig_subtree.append(Tree(transform, basescale, subsubtree))
283 orig_subtree = subsubtree
284 if verbosity:
285 if keymon is not None:
286 print(" "*indent + "in: " + constraint.str_without(["units", "lineage"]))
287 print(" "*indent + "by:")
288 indent += 1
290 # TODO: use ast_parsing instead of chop?
291 monsols = [solcache(solution, mon) for mon in composition.chop()] # ~20% of total last check # TODO: remove
292 parsed_monsols = [getattr(mon, "value", mon) for mon in monsols]
293 monvals = [float(mon/scale) for mon in parsed_monsols] # ~10% of total last check # TODO: remove
294 # sort by value, preserving order in case of value tie
295 sortedmonvals = sorted(zip(monvals, range(len(monvals)),
296 composition.chop()), reverse=True)
297 for scaledmonval, _, mon in sortedmonvals:
298 if not scaledmonval:
299 continue
300 subtree = orig_subtree # return to the original subtree
302 # time for some filtering
303 interesting_vks = mon.vks
304 potential_filters = [
305 {vk for vk in interesting_vks if vk not in bd},
306 mon.vks - get_free_vks(mon, solution),
307 {vk for vk in interesting_vks if vk in BASICALLY_FIXED_VARIABLES}
308 ]
309 if scaledmonval < 1 - permissivity: # skip breakdown filter
310 potential_filters = potential_filters[1:]
311 for filter in potential_filters:
312 if interesting_vks - filter: # don't remove the last one
313 interesting_vks = interesting_vks - filter
314 # if filters weren't enough and permissivity is high enough, sort!
315 if len(interesting_vks) > 1 and permissivity > 1:
316 best_vks = sorted((vk for vk in interesting_vks if vk in bd),
317 key=lambda vk:
318 # TODO: without exp: "most strongly broken-down component"
319 # but it could use nus (or v_ss) to say
320 # "breakdown which the solution is most sensitive to"
321 # ...right now it's in-between
322 (-mon.exp[vk]*abs(solution["sensitivities"]["constraints"][bd[vk][0][2]]),
323 str(bd[vk][0][0]))) # ~5% of total last check # TODO: remove
324 # changing to str(vk) above does some odd stuff
325 if best_vks:
326 interesting_vks = set([best_vks[0]])
327 boring_vks = mon.vks - interesting_vks
329 subkey = None
330 if len(interesting_vks) == 1:
331 subkey, = interesting_vks
332 if subkey in visited_bdkeys and len(sortedmonvals) == 1:
333 continue # don't even go there
334 if subkey not in bd:
335 power = 1 # no need for a transform
336 else:
337 power = mon.exp[subkey]
338 if power < 0 and gone_negative:
339 subkey = None # don't breakdown another negative
341 if subkey is None:
342 power = 1
343 if scaledmonval > 1 - permissivity and not boring_vks:
344 boring_vks = interesting_vks
345 interesting_vks = set()
346 if not interesting_vks:
347 # prioritize showing some boring_vks as if they were "free"
348 if len(boring_vks) == 1:
349 interesting_vks = boring_vks
350 boring_vks = set()
351 else:
352 for vk in list(boring_vks):
353 if vk.units and not vk.units.dimensionless:
354 interesting_vks.add(vk)
355 boring_vks.remove(vk)
357 if interesting_vks and (boring_vks or mag(mon.c) != 1):
358 units = 1
359 exp = HashVector()
360 for vk in interesting_vks:
361 exp[vk] = mon.exp[vk]
362 if vk.units:
363 units *= vk.units**mon.exp[vk]
364 subhmap = NomialMap({exp: 1})
365 subhmap.units = None if units is 1 else units
366 freemon = Monomial(subhmap)
367 factor = mon/freemon # autoconvert...
368 if (factor.units is None and isinstance(factor, FixedScalar)
369 and abs(factor.value - 1) <= 1e-4):
370 factor = 1 # minor fudge to clear numerical inaccuracies
371 if factor != 1 :
372 factor.ast = None
373 if verbosity:
374 keyvalstr = "%s (%s)" % (factor.str_without(["unnecessary lineage", "units"]),
375 get_valstr(factor, solution))
376 print(" "*indent + "(with a factor of %s )" % keyvalstr)
377 subsubtree = []
378 transform = Transform(factor, 1, mon)
379 subtree.append(Tree(transform, scaledmonval, subsubtree))
380 subtree = subsubtree
381 mon = freemon # simplifies units
382 if power != 1:
383 if verbosity:
384 print(" "*indent + "(with a power of %.2g )" % power)
385 subsubtree = []
386 transform = Transform(1, power, mon)
387 subtree.append(Tree(transform, scaledmonval, subsubtree))
388 subtree = subsubtree
389 mon = mon**(1/power)
390 mon.ast = None
391 # TODO: make minscale an argument - currently an arbitrary 0.01
392 if (subkey is not None and subkey not in visited_bdkeys
393 and subkey in bd and scaledmonval > 0.05):
394 if verbosity:
395 verbosity = indent + 1 # slight hack
396 subsubtree = crawl(subkey, bd, solution, scaledmonval,
397 permissivity, verbosity, set(visited_bdkeys),
398 gone_negative)
399 subtree.append(subsubtree)
400 else:
401 if verbosity:
402 keyvalstr = "%s (%s)" % (mon.str_without(["unnecessary lineage", "units"]),
403 get_valstr(mon, solution))
404 print(" "*indent + keyvalstr)
405 subtree.append(Tree(mon, scaledmonval, []))
406 if verbosity == 1:
407 solution.set_necessarylineage(clear=True)
408 return tree
410SYMBOLS = string.ascii_uppercase + string.ascii_lowercase
411for ambiguous_symbol in "lILT":
412 SYMBOLS = SYMBOLS.replace(ambiguous_symbol, "")
414def get_spanstr(legend, length, label, leftwards, solution):
415 "Returns span visualization, collapsing labels to symbols"
416 if label is None:
417 return " "*length
418 spacer, lend, rend = "│", "┯", "┷"
419 if isinstance(label, Transform):
420 spacer, lend, rend = "╎", "╤", "╧"
421 if label.power != 1:
422 spacer = " "
423 lend = rend = "^" if label.power > 0 else "/"
424 # remove origkeys so they collide in the legends dictionary
425 label = Transform(label.factor, label.power, None)
426 if label.power == 1 and len(str(label.factor)) == 1:
427 legend[label] = str(label.factor)
429 if label not in legend:
430 legend[label] = SYMBOLS[len(legend)]
431 shortname = legend[label]
433 if length <= 1:
434 return shortname
435 shortside = int(max(0, length - 2)/2)
436 longside = int(max(0, length - 3)/2)
437 if leftwards:
438 if length == 2:
439 return lend + shortname
440 return lend + spacer*shortside + shortname + spacer*longside + rend
441 else:
442 if length == 2:
443 return shortname + rend
444 # HACK: no corners on long rightwards - only used for depth 0
445 return "┃"*(longside+1) + shortname + "┃"*(shortside+1)
447def simplify(tree, extent, solution, collapse, depth=0, justsplit=False):
448 # TODO: add vertical simplification?
449 key, val, branches = tree
450 if collapse: # collapse Transforms with power 1
451 while any(isinstance(branch.key, Transform) and branch.key.power > 0 for branch in branches):
452 newbranches = []
453 for branch in branches:
454 # isinstance(branch.key, Transform) and branch.key.power > 0
455 if isinstance(branch.key, Transform) and branch.key.power > 0:
456 newbranches.extend(branch.branches)
457 else:
458 newbranches.append(branch)
459 branches = newbranches
461 scale = extent/val
462 values = [b.value for b in branches]
463 bkey_indexs = {}
464 for i, b in enumerate(branches):
465 k = get_keystr(b.key, solution)
466 if isinstance(b.key, Transform):
467 if len(b.branches) == 1:
468 k = get_keystr(b.branches[0].key, solution)
469 if k in bkey_indexs:
470 values[bkey_indexs[k]] += values[i]
471 values[i] = None
472 else:
473 bkey_indexs[k] = i
474 if any(v is None for v in values):
475 branches, values = zip(*((b, v) for b, v in zip(branches, values) if v is not None))
476 branches = list(branches)
477 values = list(values)
478 extents = [int(round(scale*v)) for v in values]
479 surplus = extent - sum(extents)
480 for i, b in enumerate(branches):
481 if isinstance(b.key, Transform):
482 subscale = extents[i]/b.value
483 if not any(round(subscale*subv) for _, subv, _ in b.branches):
484 extents[i] = 0 # transform with no worthy heirs: misc it
485 if not any(extents):
486 return Tree(key, extent, [])
487 if not all(extents): # create a catch-all
488 branches = branches.copy()
489 miscvkeys, miscval = [], 0
490 for subextent in reversed(extents):
491 if not subextent or (branches[-1].value < miscval and surplus < 0):
492 extents.pop()
493 k, v, _ = branches.pop()
494 if isinstance(k, Transform):
495 k = k.origkey # TODO: this is the only use of origkey - remove it
496 if isinstance(k, tuple):
497 vkeys = [(-kv[1], str(kv[0]), kv[0]) for kv in k]
498 if not isinstance(k, tuple):
499 vkeys = [(-v, str(k), k)]
500 miscvkeys += vkeys
501 surplus -= (round(scale*(miscval + v))
502 - round(scale*miscval) - subextent)
503 miscval += v
504 misckeys = tuple(k for _, _, k in sorted(miscvkeys))
505 branches.append(Tree(misckeys, miscval, []))
506 extents.append(int(round(scale*miscval)))
507 if surplus:
508 sign = int(np.sign(surplus))
509 bump_priority = sorted((ext, sign*b.value, i) for i, (b, ext)
510 in enumerate(zip(branches, extents)))
511 while surplus:
512 extents[bump_priority.pop()[-1]] += sign
513 surplus -= sign
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(key, extent, [])
521 if collapse and not branchfactor and not justsplit:
522 # if we didn't just split and aren't about to, collapse
523 return simplify(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 tree = Tree(key, extent, [])
531 for branch, subextent in zip(branches, extents):
532 if subextent:
533 branch = simplify(branch, subextent, solution, collapse,
534 depth=depth+1, justsplit=justsplit)
535 if (collapse and is_power(branch.key)
536 and all(is_power(b.key) for b in branch.branches)):
537 # collapse stacked powers
538 power = branch.key.power
539 for b in branch.branches:
540 key = Transform(1, power*b.key.power, None)
541 if key.power == 1: # powers canceled, collapse both
542 tree.branches.extend(b.branches)
543 else: # collapse this level
544 tree.branches.append(Tree(key, b.value, b.branches))
545 else:
546 tree.branches.append(branch)
547 return tree
549def layer(map, tree, maxdepth, depth=0):
550 "Turns the tree into a 2D-array"
551 if depth <= maxdepth:
552 if len(map) <= depth:
553 map.append([])
554 key, extent, branches = tree
555 map[depth].append((key, extent))
556 if not branches:
557 branches = [Tree(None, extent, [])] # pad it out
558 for branch in branches:
559 layer(map, branch, maxdepth, depth+1)
560 return map
562def plumb(tree, depth=0):
563 "Finds maximum depth of a tree"
564 maxdepth = depth
565 for branch in tree.branches:
566 maxdepth = max(maxdepth, plumb(branch, depth+1))
567 return maxdepth
569# @profile # ~16% of total last check # TODO: remove
570def graph(tree, solution, extent=None, maxdepth=None, showlegend=False, maxwidth=110):
571 "Prints breakdown"
572 if maxdepth is None:
573 maxdepth = plumb(tree)
574 if extent is None: # auto-zoom-in from 20
575 prev_extent = None
576 extent = 20
577 while prev_extent != extent:
578 subtree = simplify(tree, extent, solution, collapse=not showlegend)
579 mt = layer([], subtree, maxdepth)
580 prev_extent = extent
581 extent = min(extent, max(*(4*len(at_depth) for at_depth in mt)))
582 else:
583 subtree = simplify(tree, extent, solution, collapse=not showlegend)
584 mt = layer([], subtree, maxdepth)
585 legend = {}
586 chararray = np.full((len(mt), extent), "", "object")
587 for depth, elements_at_depth in enumerate(mt):
588 row = ""
589 for i, (element, length) in enumerate(elements_at_depth):
590 leftwards = depth > 0 and length > 2
591 row += get_spanstr(legend, length, element, leftwards, solution)
592 chararray[depth, :] = list(row)
594 solution.set_necessarylineage()
595 # Format depth=0
596 A_key, = [key for key, value in legend.items() if value == "A"]
597 A_str = get_keystr(A_key, solution)
598 prefix = ""
599 if isinstance(A_key, VarKey) and A_key.necessarylineage:
600 prefix = A_key.lineagestr()
601 A_valstr = get_valstr(A_key, solution, into="(%s)")
602 fmt = "{0:>%s}" % (max(len(A_str), len(A_valstr)) + 3)
603 for j, entry in enumerate(chararray[0,:]):
604 if entry == "A":
605 chararray[0,j] = fmt.format(A_str + "╺┫")
606 chararray[0,j+1] = fmt.format(A_valstr + " ┃")
607 else:
608 chararray[0,j] = fmt.format(entry)
609 # Format depths 1+
610 labeled = set()
611 new_legend = {}
612 for pos in range(extent):
613 for depth in reversed(range(1,len(mt))):
614 value = chararray[depth, pos]
615 if value not in SYMBOLS:
616 continue
617 key, = [k for k, val in legend.items() if val == value]
618 if getattr(key, "vks", None) and len(key.vks) == 1 and all(vk in new_legend for vk in key.vks):
619 key, = key.vks
620 if key not in new_legend and (isinstance(key, tuple) or (depth != len(mt) - 1 and chararray[depth+1, pos] != " ")):
621 new_legend[key] = SYMBOLS[len(new_legend)]
622 if key in new_legend:
623 chararray[depth, pos] = new_legend[key]
624 if isinstance(key, tuple) and not isinstance(key, Transform):
625 chararray[depth, pos] = "*" + chararray[depth, pos]
626 if showlegend:
627 continue
628 tryup, trydn = True, True
629 span = 0
630 if not showlegend and is_power(key):
631 chararray[depth, pos] = "^" if key.power > 0 else "/"
632 continue
634 keystr = get_keystr(key, solution, prefix)
635 valuestr = get_valstr(key, solution, into=" (%s)")
636 if keystr in labeled:
637 valuestr = ""
638 if not showlegend:
639 fmt = "{0:<%s}" % max(len(keystr) + 3, len(valuestr) + 2)
640 else:
641 fmt = "{0:<1}"
642 while tryup or trydn:
643 span += 1
644 if tryup:
645 if pos - span < 0:
646 tryup = False
647 else:
648 upchar = chararray[depth, pos-span]
649 if upchar == "│":
650 chararray[depth, pos-span] = fmt.format("┃")
651 elif upchar == "┯":
652 chararray[depth, pos-span] = fmt.format("┓")
653 else:
654 tryup = False
655 if trydn:
656 if pos + span >= extent:
657 trydn = False
658 else:
659 dnchar = chararray[depth, pos+span]
660 if dnchar == "│":
661 chararray[depth, pos+span] = fmt.format("┃")
662 elif dnchar == "┷":
663 chararray[depth, pos+span] = fmt.format("┛")
664 else:
665 trydn = False
666 #TODO: make submodels show up with this; bd should be an argument
667 if showlegend and (key in bd or (hasattr(key, "vks") and key.vks and any(vk in bd for vk in key.vks))):
668 linkstr = "┣┉"
669 else:
670 linkstr = "┣╸"
671 if not isinstance(key, FixedScalar):
672 labeled.add(keystr)
673 if span > 1 and (not showlegend or pos + 2 >= extent or chararray[depth, pos+1] == "┃"):
674 chararray[depth, pos+1] = fmt.format(chararray[depth, pos+1].rstrip() + valuestr)
675 elif showlegend:
676 keystr += valuestr
677 chararray[depth, pos] = fmt.format(linkstr + keystr)
678 # Rotate and print
679 toowiderows = []
680 rows = chararray.T.tolist()
681 if not showlegend: # remove according to maxwidth
682 for i, orig_row in enumerate(rows):
683 depth_occluded = -1
684 width = None
685 row = orig_row.copy()
686 row.append("")
687 while width is None or width > maxwidth:
688 row = row[:-1]
689 rowstr = " " + "".join(row).rstrip()
690 width = len(rowstr)
691 depth_occluded += 1
692 if depth_occluded:
693 previous_is_pow = orig_row[-depth_occluded-1] in "^/"
694 if abs(depth_occluded) + 1 + previous_is_pow < len(orig_row):
695 strdepth = len(" " + "".join(orig_row[:-depth_occluded]))
696 toowiderows.append((strdepth, i))
697 rowstrs = [" " + "".join(row).rstrip() for row in rows]
698 for depth_occluded, i in sorted(toowiderows, reverse=True):
699 if len(rowstrs[i]) <= depth_occluded:
700 continue # already occluded
701 if "┣" == rowstrs[i][depth_occluded]:
702 pow = 0
703 while rowstrs[i][depth_occluded-pow-1] in "^/":
704 pow += 1
705 rowstrs[i] = rowstrs[i][:depth_occluded-pow]
706 connected = "^┃┓┛┣╸"
707 for dir in [-1, 1]:
708 idx = i + dir
709 while (0 <= idx < len(rowstrs)
710 and len(rowstrs[idx]) > depth_occluded
711 and rowstrs[idx][depth_occluded]
712 and rowstrs[idx][depth_occluded] in connected):
713 while rowstrs[idx][depth_occluded-pow-1] in "^/":
714 pow += 1
715 rowstrs[idx] = rowstrs[idx][:depth_occluded-pow]
716 idx += dir
717 vertstr = "\n".join(rowstr.rstrip() for rowstr in rowstrs)
718 print()
719 print(vertstr)
720 print()
721 legend = new_legend
723 if showlegend: # create and print legend
724 legend_lines = []
725 for key, shortname in sorted(legend.items(), key=lambda kv: kv[1]):
726 legend_lines.append(legend_entry(key, shortname, solution))
727 maxlens = [max(len(el) for el in col) for col in zip(*legend_lines)]
728 fmts = ["{0:<%s}" % L for L in maxlens]
729 for line in legend_lines:
730 line = "".join(fmt.format(cell)
731 for fmt, cell in zip(fmts, line) if cell).rstrip()
732 print(" " + line)
734 solution.set_necessarylineage(clear=True)
736def legend_entry(key, shortname, solution):
737 "Returns list of legend elements"
738 operator = note = ""
739 keystr = valuestr = " "
740 operator = "= " if shortname else " + "
741 if is_factor(key):
742 operator = " ×"
743 key = key.factor
744 free, quasifixed = False, False
745 if any(vk not in BASICALLY_FIXED_VARIABLES
746 for vk in get_free_vks(key, solution)):
747 note = " [free factor]"
748 if is_power(key):
749 valuestr = " ^%.3g" % key.power
750 else:
751 valuestr = get_valstr(key, solution, into=" "+operator+"%s")
752 if not isinstance(key, FixedScalar):
753 keystr = get_keystr(key, solution)
754 return ["%-4s" % shortname, keystr, valuestr, note]
756def get_keystr(key, solution, prefix=""):
757 if key is solution.costposy:
758 out = "Cost"
759 elif hasattr(key, "str_without"):
760 out = key.str_without({"unnecessary lineage", "units", ":MAGIC:"+prefix})
761 elif isinstance(key, tuple):
762 out = "[%i terms]" % len(key)
763 else:
764 out = str(key)
765 return out if len(out) <= 67 else out[:66]+"…"
767def get_valstr(key, solution, into="%s"):
768 "Returns formatted string of the value of key in solution."
769 # get valuestr
770 try:
771 value = solution(key)
772 except (ValueError, TypeError):
773 try:
774 value = sum(solution(subkey) for subkey in key)
775 except (ValueError, TypeError):
776 return " "
777 if isinstance(value, FixedScalar):
778 value = value.value
779 if 1e3 <= mag(value) < 1e6:
780 valuestr = "{:,.0f}".format(mag(value))
781 else:
782 valuestr = "%-.3g" % mag(value)
783 # get unitstr
784 if hasattr(key, "unitstr"):
785 unitstr = key.unitstr()
786 else:
787 try:
788 if hasattr(value, "units"):
789 value.ito_reduced_units()
790 except DimensionalityError:
791 pass
792 unitstr = get_unitstr(value)
793 if unitstr[:2] == "1/":
794 unitstr = "/" + unitstr[2:]
795 if key in solution["constants"] or (hasattr(key, "vks") and key.vks and all(vk in solution["constants"] for vk in key.vks)):
796 unitstr += ", fixed"
797 return into % (valuestr + unitstr)
800import pickle
801from gpkit import ureg
802ureg.define("pax = 1")
803ureg.define("paxkm = km")
804ureg.define("trip = 1")
806print("STARTING...")
807from gpkit.tests.helpers import StdoutCaptured
809import difflib
811permissivity = 2
813# sol = pickle.load(open("solar.p", "rb"))
814# bd = get_breakdowns(sol)
815# mbd = get_model_breakdown(sol)
816# mtree = crawl_modelbd(mbd)
817# graph(mtree, sol, showlegend=True, extent=20)
818# graph(mtree.branches[0].branches[0].branches[0], sol, showlegend=False, extent=20)
819#
820# tree = crawl(sol.costposy, bd, sol, permissivity=2, verbosity=0)
821# graph(tree, sol)
822#
823#
824# # key, = [vk for vk in bd if "Wing.Planform.b" in str(vk)]
825# # tree = crawl(key, bd, sol, permissivity=2, verbosity=1)
826# # graph(tree, sol)
827# # key, = [vk for vk in bd if "Aircraft.Fuselage.R[0,0]" in str(vk)]
828# # tree = crawl(key, bd, sol, permissivity=2, verbosity=1)
829# # graph(tree, sol)
830# # key, = [vk for vk in bd if "Mission.Climb.AircraftDrag.CD[0]" in str(vk)]
831# # tree = crawl(key, bd, sol, permissivity=2, verbosity=1)
832# # graph(tree, sol)
833#
834#
835# keys = sorted(bd.keys(), key=str)
836#
837# # with StdoutCaptured("solarbreakdowns.log"):
838# # graph(mtree, sol, showlegend=False)
839# # graph(mtree, sol, showlegend=True)
840# # tree = crawl(sol.costposy, bd, sol, permissivity=permissivity)
841# # graph(tree, sol)
842# # for key in keys:
843# # tree = crawl(key, bd, sol, permissivity=permissivity)
844# # graph(tree, sol)
845#
846# with StdoutCaptured("solarbreakdowns.log.new"):
847# graph(mtree, sol, showlegend=False)
848# graph(mtree, sol, showlegend=True)
849# tree = crawl(sol.costposy, bd, sol, permissivity=permissivity)
850# graph(tree, sol)
851# for key in keys:
852# tree = crawl(key, bd, sol, permissivity=permissivity)
853# try:
854# graph(tree, sol)
855# except:
856# raise ValueError(key)
857#
858# with open("solarbreakdowns.log", "r") as original:
859# with open("solarbreakdowns.log.new", "r") as new:
860# diff = difflib.unified_diff(
861# original.readlines(),
862# new.readlines(),
863# fromfile="original",
864# tofile="new",
865# )
866# for line in diff:
867# print(line[:-1])
868#
869# print("SOLAR DONE")
872sol = pickle.load(open("bd.p", "rb"))
873bd = get_breakdowns(sol)
874mbd = get_model_breakdown(sol)
875mtree = crawl_modelbd(mbd)
877# graph(mtree, sol, showlegend=False)
879key, = [vk for vk in bd if "podtripenergy[1,0]" in str(vk)]
880tree = crawl(key, bd, sol, permissivity=2, verbosity=1)
881graph(tree, sol, extent=40)
882tree = crawl(sol.costposy, bd, sol, permissivity=2, verbosity=1)
883graph(tree, sol)
884# key, = [vk for vk in bd if "statorlaminationmassperpolepair" in str(vk)]
885# tree = crawl(key, bd, sol, permissivity=2, verbosity=1)
886# graph(tree, sol)
887# key, = [vk for vk in bd if "numberofcellsperstring" in str(vk)]
888# tree = crawl(key, bd, sol, permissivity=2, verbosity=1)
889# graph(tree, sol)
891keys = sorted((key for key in bd.keys() if not key.idx or len(key.shape) == 1),
892 key=lambda k: k.str_without(excluded={}))
894# with StdoutCaptured("breakdowns.log"):
895# graph(mtree, sol, showlegend=False)
896# graph(mtree.branches[0].branches[1], sol, showlegend=False)
897# graph(mtree, sol, showlegend=True)
898# tree = crawl(sol.costposy, bd, sol, permissivity=permissivity)
899# graph(tree, sol)
900# for key in keys:
901# tree = crawl(key, bd, sol, permissivity=permissivity)
902# graph(tree, sol)
904with StdoutCaptured("breakdowns.log.new"):
905 graph(mtree, sol, showlegend=False)
906 graph(mtree.branches[0].branches[1], sol, showlegend=False)
907 graph(mtree, sol, showlegend=True)
908 tree = crawl(sol.costposy, bd, sol, permissivity=permissivity)
909 graph(tree, sol)
910 for key in keys:
911 tree = crawl(key, bd, sol, permissivity=permissivity)
912 try:
913 graph(tree, sol)
914 except:
915 raise ValueError(key)
917with open("breakdowns.log", "r") as original:
918 with open("breakdowns.log.new", "r") as new:
919 diff = difflib.unified_diff(
920 original.readlines(),
921 new.readlines(),
922 fromfile="original",
923 tofile="new",
924 )
925 for line in diff:
926 print(line[:-1])
928print("DONE")