Coverage for gpkit/repr_conventions.py: 93%
128 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-03 15:33 -0500
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-03 15:33 -0500
1"Repository for representation standards"
2import sys
3import re
4import numpy as np
5from .small_classes import Quantity, Numbers
6from .small_scripts import try_str_without
8INSIDE_PARENS = re.compile(r"\(.*\)")
10if sys.platform[:3] == "win": # pragma: no cover
11 MUL = "*"
12 PI_STR = "PI"
13 UNICODE_EXPONENTS = False
14 UNIT_FORMATTING = ":~"
15else: # pragma: no cover
16 MUL = "·"
17 PI_STR = "π"
18 UNICODE_EXPONENTS = True
19 UNIT_FORMATTING = ":P~"
21def lineagestr(lineage, modelnums=True):
22 "Returns properly formatted lineage string"
23 if not isinstance(lineage, tuple):
24 lineage = getattr(lineage, "lineage", None)
25 return ".".join(["%s%i" % (name, num) if (num and modelnums) else name
26 for name, num in lineage]) if lineage else ""
29def unitstr(units, into="%s", options=UNIT_FORMATTING, dimless=""):
30 "Returns the string corresponding to an object's units."
31 if hasattr(units, "units") and isinstance(units.units, Quantity):
32 units = units.units
33 if not isinstance(units, Quantity):
34 return dimless
35 if options == ":~" and "ohm" in str(units.units): # pragma: no cover
36 rawstr = str(units.units) # otherwise it'll be a capital Omega
37 else:
38 rawstr = ("{%s}" % options).format(units.units)
39 units = rawstr.replace(" ", "").replace("dimensionless", dimless)
40 return into % units or dimless
42def latex_unitstr(units):
43 "Returns latex unitstr"
44 us = unitstr(units, r"~\mathrm{%s}", ":L~")
45 utf = us.replace("frac", "tfrac").replace(r"\cdot", r"\cdot ")
46 return utf if utf != r"~\mathrm{-}" else ""
49def strify(val, excluded):
50 "Turns a value into as pretty a string as possible."
51 if isinstance(val, Numbers):
52 isqty = hasattr(val, "magnitude")
53 if isqty:
54 units = val
55 val = val.magnitude
56 if np.pi/12 < val < 100*np.pi and abs(12*val/np.pi % 1) <= 1e-2:
57 # val is in bounds and a clean multiple of PI!
58 if val > 3.1: # product of PI
59 val = "%.3g%s" % (val/np.pi, PI_STR)
60 if val == "1%s" % PI_STR:
61 val = PI_STR
62 else: # division of PI
63 val = "(%s/%.3g)" % (PI_STR, np.pi/val)
64 else:
65 val = "%.3g" % val
66 if isqty:
67 val += unitstr(units, " [%s]")
68 else:
69 val = try_str_without(val, excluded)
70 return val
73def parenthesize(string, addi=True, mult=True):
74 "Parenthesizes a string if it needs it and isn't already."
75 parensless = string if "(" not in string else INSIDE_PARENS.sub("", string)
76 bare_addi = (" + " in parensless or " - " in parensless)
77 bare_mult = ("·" in parensless or "/" in parensless)
78 if parensless and (addi and bare_addi) or (mult and bare_mult):
79 return "(%s)" % string
80 return string
83class ReprMixin:
84 "This class combines various printing methods for easier adoption."
85 lineagestr = lineagestr
86 unitstr = unitstr
87 latex_unitstr = latex_unitstr
89 cached_strs = None
90 ast = None
91 # pylint: disable=too-many-branches, too-many-statements
92 def parse_ast(self, excluded=()):
93 "Turns the AST of this object's construction into a faithful string"
94 excluded = frozenset({"units"}.union(excluded))
95 if self.cached_strs is None:
96 self.cached_strs = {}
97 elif excluded in self.cached_strs:
98 return self.cached_strs[excluded]
99 oper, values = self.ast # pylint: disable=unpacking-non-sequence
101 if oper == "add":
102 left = strify(values[0], excluded)
103 right = strify(values[1], excluded)
104 if right[0] == "-":
105 aststr = "%s - %s" % (left, right[1:])
106 else:
107 aststr = "%s + %s" % (left, right)
108 elif oper == "mul":
109 left = parenthesize(strify(values[0], excluded), mult=False)
110 right = parenthesize(strify(values[1], excluded), mult=False)
111 if left == "1":
112 aststr = right
113 elif right == "1":
114 aststr = left
115 else:
116 aststr = "%s%s%s" % (left, MUL, right)
117 elif oper == "div":
118 left = parenthesize(strify(values[0], excluded), mult=False)
119 right = parenthesize(strify(values[1], excluded))
120 if right == "1":
121 aststr = left
122 else:
123 aststr = "%s/%s" % (left, right)
124 elif oper == "neg":
125 aststr = "-%s" % parenthesize(strify(values, excluded), mult=False)
126 elif oper == "pow":
127 left = parenthesize(strify(values[0], excluded))
128 x = values[1]
129 if left == "1":
130 aststr = "1"
131 elif (UNICODE_EXPONENTS and not getattr(x, "shape", None)
132 and int(x) == x and 2 <= x <= 9):
133 x = int(x)
134 if x in (2, 3):
135 aststr = "%s%s" % (left, chr(176+x))
136 elif x in (4, 5, 6, 7, 8, 9):
137 aststr = "%s%s" % (left, chr(8304+x))
138 else:
139 aststr = "%s^%s" % (left, x)
140 elif oper == "prod": # TODO: only do if it makes a shorter string
141 aststr = "%s.prod()" % parenthesize(strify(values[0], excluded))
142 elif oper == "sum": # TODO: only do if it makes a shorter string
143 aststr = "%s.sum()" % parenthesize(strify(values[0], excluded))
144 elif oper == "index": # TODO: label vectorization idxs
145 left = parenthesize(strify(values[0], excluded))
146 idx = values[1]
147 if left[-3:] == "[:]": # pure variable access
148 left = left[:-3]
149 if isinstance(idx, tuple):
150 elstrs = []
151 for el in idx:
152 if isinstance(el, slice):
153 start = el.start or ""
154 stop = (el.stop if el.stop and el.stop < sys.maxsize
155 else "")
156 step = ":%s" % el.step if el.step is not None else ""
157 elstrs.append("%s:%s%s" % (start, stop, step))
158 elif isinstance(el, Numbers):
159 elstrs.append("%s" % el)
160 idx = ",".join(elstrs)
161 elif isinstance(idx, slice):
162 start = idx.start or ""
163 stop = idx.stop if idx.stop and idx.stop < 1e6 else ""
164 step = ":%s" % idx.step if idx.step is not None else ""
165 idx = "%s:%s%s" % (start, stop, step)
166 aststr = "%s[%s]" % (left, idx)
167 else:
168 raise ValueError(oper)
169 self.cached_strs[excluded] = aststr
170 return aststr
172 def __repr__(self):
173 "Returns namespaced string."
174 return "gpkit.%s(%s)" % (self.__class__.__name__, self)
176 def __str__(self):
177 "Returns default string."
178 return self.str_without() # pylint: disable=no-member
180 def _repr_latex_(self):
181 "Returns default latex for automatic iPython Notebook rendering."
182 return "$$"+self.latex()+"$$" # pylint: disable=no-member