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