prettyformat -> hooke.util.si, and 'plot SI format' option to GUI
[hooke.git] / hooke / util / si.py
1 # Copyright\r
2 \r
3 """Define functions for handling numbers in SI notation.\r
4 \r
5 Notes\r
6 -----\r
7 To output a scale, choose any value on the axis and find the\r
8 multiplier and prefix for it.  Use those to format the rest of the\r
9 scale.  As values can span several orders of magnitude, you have\r
10 to decide what units to use.\r
11 \r
12 >>> xs = (985e-12, 1e-9, 112358e-12)\r
13 \r
14 Get the power from the first (or last, or middle, ...) value\r
15 \r
16 >>> p = get_power(xs[0])\r
17 >>> for x in xs:\r
18 ...     print ppSI(x, decimals=2, power=p)\r
19 985.00 p\r
20 1000.00 p\r
21 112358.00 p\r
22 >>> print prefix_from_value(xs[0]) + 'N'\r
23 pN\r
24 """\r
25 \r
26 import math\r
27 from numpy import isnan\r
28 import re\r
29 \r
30 \r
31 PREFIX = {\r
32     24: 'Y',\r
33     21: 'Z',\r
34     18: 'E',\r
35     15: 'P',\r
36     12: 'T',\r
37     9: 'G',\r
38     6: 'M',\r
39     3: 'k',\r
40     0: '',\r
41     -3: 'm',\r
42     -6: u'\u00B5',\r
43     -9: 'n',\r
44     -12: 'p',\r
45     -15: 'f',\r
46     -18: 'a',\r
47     -21: 'z',\r
48     -24: 'y',\r
49     }\r
50 """A dictionary of SI prefixes from 10**24 to 10**-24.\r
51 \r
52 Examples\r
53 --------\r
54 >>> PREFIX[0]\r
55 ''\r
56 >>> PREFIX[6]\r
57 'M'\r
58 >>> PREFIX[-9]\r
59 'n'\r
60 """\r
61 \r
62 _DATA_LABEL_REGEXP = re.compile('^([^(]*[^ ]) ?\(([^)]*)\)$')\r
63 """Used by :func:`data_label_unit`.\r
64 """\r
65 \r
66 \r
67 def ppSI(value, unit='', decimals=None, power=None, pad=False):\r
68     """Pretty-print `value` in SI notation.\r
69 \r
70     The current implementation ignores `pad` if `decimals` is `None`.\r
71 \r
72     Examples\r
73     --------\r
74     >>> x = math.pi * 1e-8\r
75     >>> print ppSI(x, 'N')\r
76     31.415927 nN\r
77     >>> print ppSI(x, 'N', 3)\r
78     31.416 nN\r
79     >>> print ppSI(x, 'N', 4, power=-12)\r
80     31415.9265 pN\r
81     >>> print ppSI(x, 'N', 5, pad=True)\r
82        31.41593 nN\r
83 \r
84     If you want the decimal indented by six spaces with `decimal=2`,\r
85     `pad` should be the sum of\r
86 \r
87     * 6 (places before the decimal point)\r
88     * 1 (length of the decimal point)\r
89     * 2 (places after the decimal point)\r
90 \r
91     >>> print ppSI(-x, 'N', 2, pad=(6+1+2))\r
92        -31.42 nN\r
93     """\r
94     if value == 0:\r
95         return '0'\r
96     if isnan(value):\r
97         return 'NaN'\r
98 \r
99     if power == None:  # auto-detect power\r
100         power = get_power(value)\r
101 \r
102     if decimals == None:\r
103         format = lambda n: '%f' % n\r
104     else:\r
105         if pad == False:  # no padding\r
106             format = lambda n: '%.*f' % (decimals, n)            \r
107         else:\r
108             if pad == True:  # auto-generate pad\r
109                 # 1 for ' ', 1 for '-', 3 for number, 1 for '.', and decimals.\r
110                 pad = 6 + decimals\r
111             format = lambda n: '%*.*f' % (pad, decimals, n)\r
112     return '%s %s%s' % (format(value / pow(10,power)), PREFIX[power], unit)\r
113 \r
114 \r
115 def get_power(value):\r
116     """Return the SI power for which `0 <= |value|/10**pow < 1000`. \r
117     \r
118     Exampes\r
119     -------\r
120     >>> get_power(0)\r
121     0\r
122     >>> get_power(123)\r
123     0\r
124     >>> get_power(-123)\r
125     0\r
126     >>> get_power(1e8)\r
127     6\r
128     >>> get_power(1e-16)\r
129     -18\r
130     """\r
131     if value != 0 and not isnan(value):\r
132         # get log10(|value|)\r
133         value_temp = math.floor(math.log10(math.fabs(value)))\r
134         # reduce the log10 to a multiple of 3\r
135         return int(value_temp - (value_temp % 3))\r
136     else:\r
137         return 0\r
138 \r
139 def prefix_from_value(value):\r
140     """Determine the SI power of `value` and return its prefix.\r
141 \r
142     Examples\r
143     --------\r
144     >>> prefix_from_value(0)\r
145     ''\r
146     >>> prefix_from_value(1e10)\r
147     'G'\r
148     """\r
149     return PREFIX[get_power(value)]\r
150 \r
151 def split_data_label(label):\r
152     """Split `curve.data[i].info['name']` labels into `(name, unit)`.\r
153 \r
154     Examples\r
155     --------\r
156     >>> split_data_label('z piezo (m)')\r
157     ('z piezo', 'm')\r
158     >>> split_data_label('deflection (N)')\r
159     ('deflection', 'N')\r
160     """\r
161     m = _DATA_LABEL_REGEXP.match(label)\r
162     assert m != None, label\r
163     return m.groups()\r