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