2a36bb33ff9dc94b9f7971075ecd3a3770deb51f
[hooke.git] / hooke / util / si.py
1 # Copyright (C) 2010-2012 W. Trevor King <wking@tremily.us>
2 #
3 # This file is part of Hooke.
4 #
5 # Hooke is free software: you can redistribute it and/or modify it under the
6 # terms of the GNU Lesser General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option) any
8 # later version.
9 #
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
13 # details.
14 #
15 # You should have received a copy of the GNU Lesser General Public License
16 # along with Hooke.  If not, see <http://www.gnu.org/licenses/>.
17
18 """Define functions for handling numbers in SI notation.
19
20 Notes
21 -----
22 To output a scale, choose any value on the axis and find the
23 multiplier and prefix for it.  Use those to format the rest of the
24 scale.  As values can span several orders of magnitude, you have
25 to decide what units to use.
26
27 >>> xs = (985e-12, 1e-9, 112358e-12)
28
29 Get the power from the first (or last, or middle, ...) value
30
31 >>> p = get_power(xs[0])
32 >>> for x in xs:
33 ...     print ppSI(x, decimals=2, power=p)
34 985.00 p
35 1000.00 p
36 112358.00 p
37 >>> print prefix_from_value(xs[0]) + 'N'
38 pN
39 """
40
41 import math
42 from numpy import isnan
43 import re
44
45
46 PREFIX = {
47     24: 'Y',
48     21: 'Z',
49     18: 'E',
50     15: 'P',
51     12: 'T',
52     9: 'G',
53     6: 'M',
54     3: 'k',
55     0: '',
56     -3: 'm',
57     -6: u'\u00B5',
58     -9: 'n',
59     -12: 'p',
60     -15: 'f',
61     -18: 'a',
62     -21: 'z',
63     -24: 'y',
64     }
65 """A dictionary of SI prefixes from 10**24 to 10**-24.
66
67 Examples
68 --------
69 >>> PREFIX[0]
70 ''
71 >>> PREFIX[6]
72 'M'
73 >>> PREFIX[-9]
74 'n'
75 """
76
77 _DATA_LABEL_REGEXP = re.compile('^([^(]*[^ ]) ?\(([^)]*)\)$')
78 """Used by :func:`data_label_unit`.
79 """
80
81
82 def ppSI(value, unit='', decimals=None, power=None, pad=False):
83     """Pretty-print `value` in SI notation.
84
85     The current implementation ignores `pad` if `decimals` is `None`.
86
87     Examples
88     --------
89     >>> x = math.pi * 1e-8
90     >>> print ppSI(x, 'N')
91     31.415927 nN
92     >>> print ppSI(x, 'N', 3)
93     31.416 nN
94     >>> print ppSI(x, 'N', 4, power=-12)
95     31415.9265 pN
96     >>> print ppSI(x, 'N', 5, pad=True)
97        31.41593 nN
98
99     If you want the decimal indented by six spaces with `decimal=2`,
100     `pad` should be the sum of
101
102     * 6 (places before the decimal point)
103     * 1 (length of the decimal point)
104     * 2 (places after the decimal point)
105
106     >>> print ppSI(-x, 'N', 2, pad=(6+1+2))
107        -31.42 nN
108     """
109     if value == 0:
110         return '0'
111     if value == None or isnan(value):
112         return 'NaN'
113
114     if power == None:  # auto-detect power
115         power = get_power(value)
116
117     if decimals == None:
118         format = lambda n: '%f' % n
119     else:
120         if pad == False:  # no padding
121             format = lambda n: '%.*f' % (decimals, n)            
122         else:
123             if pad == True:  # auto-generate pad
124                 # 1 for ' ', 1 for '-', 3 for number, 1 for '.', and decimals.
125                 pad = 6 + decimals
126             format = lambda n: '%*.*f' % (pad, decimals, n)
127     try:
128         prefix = ' '+PREFIX[power]
129     except KeyError:
130         prefix = 'e%d ' % power
131     return '%s%s%s' % (format(value / pow(10,power)), prefix, unit)
132
133
134 def get_power(value):
135     """Return the SI power for which `0 <= |value|/10**pow < 1000`. 
136     
137     Exampes
138     -------
139     >>> get_power(0)
140     0
141     >>> get_power(123)
142     0
143     >>> get_power(-123)
144     0
145     >>> get_power(1e8)
146     6
147     >>> get_power(1e-16)
148     -18
149     """
150     if value != 0 and not isnan(value):
151         # get log10(|value|)
152         value_temp = math.floor(math.log10(math.fabs(value)))
153         # reduce the log10 to a multiple of 3
154         return int(value_temp - (value_temp % 3))
155     else:
156         return 0
157
158 def prefix_from_value(value):
159     """Determine the SI power of `value` and return its prefix.
160
161     Examples
162     --------
163     >>> prefix_from_value(0)
164     ''
165     >>> prefix_from_value(1e10)
166     'G'
167     """
168     return PREFIX[get_power(value)]
169
170 def join_data_label(name, unit):
171     """Create laels for `curve.data[i].info['columns']`.
172
173     See Also
174     --------
175     split_data_label
176
177     Examples
178     --------
179     >>> join_data_label('z piezo', 'm')
180     'z piezo (m)'
181     >>> join_data_label('deflection', 'N')
182     'deflection (N)'
183     """
184     return '%s (%s)' % (name, unit)
185
186 def split_data_label(label):
187     """Split `curve.data[i].info['columns']` labels into `(name, unit)`.
188
189     See Also
190     --------
191     join_data_label
192
193     Examples
194     --------
195     >>> split_data_label('z piezo (m)')
196     ('z piezo', 'm')
197     >>> split_data_label('deflection (N)')
198     ('deflection', 'N')
199     """
200     m = _DATA_LABEL_REGEXP.match(label)
201     assert m != None, label
202     return m.groups()