1 | # This program is public domain |
---|
2 | # Author Paul Kienzle |
---|
3 | """ |
---|
4 | Format numbers nicely for printing. |
---|
5 | |
---|
6 | Usage:: |
---|
7 | |
---|
8 | >> from danse.common.util.formatnum import * |
---|
9 | >> v,dv = 757.2356,0.01032 |
---|
10 | >> print format_uncertainty_pm(v,dv) |
---|
11 | 757.235 +/- 0.010 |
---|
12 | >> format_uncertainty_compact(v,dv) |
---|
13 | 757.235(10) |
---|
14 | >> format_uncertainty(v,dv) |
---|
15 | 757.235(10) |
---|
16 | |
---|
17 | Set format_uncertainty.compact to False to use the +/- |
---|
18 | format by default, otherwise leave it at True for compact |
---|
19 | value(##) format. |
---|
20 | """ |
---|
21 | from __future__ import division |
---|
22 | |
---|
23 | import math |
---|
24 | import numpy |
---|
25 | __all__ = ['format_uncertainty', 'format_uncertainty_pm', |
---|
26 | 'format_uncertainty_compact'] |
---|
27 | |
---|
28 | # These routines need work for +/- formatting:: |
---|
29 | # - pm formats are not rigorously tested |
---|
30 | # - pm formats do not try to align the scale to multiples of 1000 |
---|
31 | # - pm formats do not try to align the value scale to uncertainty scale |
---|
32 | |
---|
33 | # Coordinating scales across a set of numbers is not supported. For easy |
---|
34 | # comparison a set of numbers should be shown in the same scale. One could |
---|
35 | # force this from the outside by adding scale parameter (either 10**n, n, or |
---|
36 | # a string representing the desired SI prefix) and having a separate routine |
---|
37 | # which computes the scale given a set of values. |
---|
38 | |
---|
39 | # Coordinating scales with units offers its own problems. Again, the user |
---|
40 | # may want to force particular units. This can be done by outside of the |
---|
41 | # formatting routines by scaling the numbers to the appropriate units then |
---|
42 | # forcing them to print with scale 10**0. If this is a common operation, |
---|
43 | # however, it may want to happen inside. |
---|
44 | |
---|
45 | # The value e<n> is currently formatted into the number. Alternatively this |
---|
46 | # scale factor could be returned so that the user can choose the appriate |
---|
47 | # SI prefix when printing the units. This gets tricky when talking about |
---|
48 | # composite units such as 2.3e-3 m**2 -> 2300 mm**2, and with volumes |
---|
49 | # such as 1 g/cm**3 -> 1 kg/L. |
---|
50 | |
---|
51 | def format_uncertainty_pm(value, uncertainty=None): |
---|
52 | """ |
---|
53 | Given *value* v and *uncertainty* dv, return a string v +/- dv. |
---|
54 | |
---|
55 | The returned string uses only the number of digits warranted by |
---|
56 | the uncertainty in the measurement. |
---|
57 | |
---|
58 | If the uncertainty is 0 or not otherwise provided, the simple |
---|
59 | %g floating point format option is used. |
---|
60 | |
---|
61 | Infinite and indefinite numbers are represented as inf and NaN. |
---|
62 | """ |
---|
63 | return _format_uncertainty(value, uncertainty, compact=False) |
---|
64 | |
---|
65 | def format_uncertainty_compact(value, uncertainty=None): |
---|
66 | """ |
---|
67 | Given *value* v and *uncertainty* dv, return the compact |
---|
68 | representation v(##), where ## are the first two digits of |
---|
69 | the uncertainty. |
---|
70 | |
---|
71 | The returned string uses only the number of digits warranted by |
---|
72 | the uncertainty in the measurement. |
---|
73 | |
---|
74 | If the uncertainty is 0 or not otherwise provided, the simple |
---|
75 | %g floating point format option is used. |
---|
76 | |
---|
77 | Infinite and indefinite numbers are represented as inf and NaN. |
---|
78 | """ |
---|
79 | return _format_uncertainty(value, uncertainty, compact=True) |
---|
80 | |
---|
81 | class FormatUncertainty: |
---|
82 | """ |
---|
83 | Given *value* and *uncertainty*, return a concise string representation. |
---|
84 | |
---|
85 | This will either by the +/- form of :func:`format_uncertainty_pm` or |
---|
86 | the compact form of :func:`format_uncertainty_compact` depending on |
---|
87 | whether *compact* is specified or whether *format_uncertainty.compact* |
---|
88 | is True or False. |
---|
89 | """ |
---|
90 | compact = True |
---|
91 | def __call__(self, value, uncertainty): |
---|
92 | return _format_uncertainty(value, uncertainty, self.compact) |
---|
93 | format_uncertainty = FormatUncertainty() |
---|
94 | |
---|
95 | def _format_uncertainty(value,uncertainty,compact): |
---|
96 | """ |
---|
97 | Implementation of both the compact and the +/- formats. |
---|
98 | """ |
---|
99 | if numpy.isinf(value): |
---|
100 | return "inf" if value > 0 else "-inf" |
---|
101 | |
---|
102 | if numpy.isnan(value): |
---|
103 | return "NaN" |
---|
104 | |
---|
105 | # Uncertainty check must come after indefinite check since the %g |
---|
106 | # format string doesn't handle indefinite numbers consistently |
---|
107 | # across platforms. |
---|
108 | if uncertainty == None or uncertainty <= 0 or numpy.isnan(uncertainty): |
---|
109 | return "%g"%value |
---|
110 | if numpy.isinf(uncertainty): |
---|
111 | if compact: |
---|
112 | return "%g(inf)"%value |
---|
113 | else: |
---|
114 | return "%g +/- inf"%value |
---|
115 | |
---|
116 | # Process sign |
---|
117 | sign = "-" if value < 0 else "" |
---|
118 | value = abs(value) |
---|
119 | |
---|
120 | # Determine the number of digits in the value and the error |
---|
121 | # Note that uncertainty <= 0 is handled above |
---|
122 | err_place = int(math.floor(math.log10(uncertainty))) |
---|
123 | if value == 0: |
---|
124 | val_place = err_place-1 |
---|
125 | else: |
---|
126 | val_place = int(math.floor(math.log10(value))) |
---|
127 | |
---|
128 | # If pm, return a simple v +/- dv |
---|
129 | if not compact: |
---|
130 | scale = 10**(err_place-1) |
---|
131 | val_digits = val_place-err_place+2 |
---|
132 | return "%s%.*g +/- %.2g"%(sign,val_digits,value,uncertainty) |
---|
133 | |
---|
134 | # The remainder is for the v(dv) case |
---|
135 | err_str = "(%2d)"%int(uncertainty/10.**(err_place-1)+0.5) |
---|
136 | if err_place > val_place: |
---|
137 | # Degenerate case: error bigger than value |
---|
138 | # The mantissa is 0.#(##)e#, 0.0#(##)e# or 0.00#(##)e# |
---|
139 | if err_place - val_place > 2: value = 0 |
---|
140 | val_place = int(math.floor((err_place+2)/3.))*3 |
---|
141 | digits_after_decimal = val_place - err_place + 1 |
---|
142 | val_str = "%.*f%s"%(digits_after_decimal,value/10.**val_place,err_str) |
---|
143 | if val_place != 0: val_str += "e%d"%val_place |
---|
144 | elif err_place == val_place: |
---|
145 | # Degenerate case: error and value the same order of magnitude |
---|
146 | # The value is ##(##)e#, #.#(##)e# or 0.##(##)e# |
---|
147 | val_place = int(math.floor((err_place+1)/3.))*3 |
---|
148 | digits_after_decimal = val_place - err_place + 1 |
---|
149 | val_str = "%.*f%s"%(digits_after_decimal,value/10.**val_place,err_str) |
---|
150 | if val_place != 0: val_str += "e%d"%val_place |
---|
151 | elif err_place <= 1 and val_place >= -3: |
---|
152 | # Normal case: nice numbers and errors |
---|
153 | # The value is ###.###(##) |
---|
154 | digits_after_decimal = abs(err_place-1) |
---|
155 | val_str = "%.*f%s"%(digits_after_decimal,value,err_str) |
---|
156 | else: |
---|
157 | # Extreme cases: zeros before value or after error |
---|
158 | # The value is ###.###(##)e#, ##.####(##)e# or #.#####(##)e# |
---|
159 | total_digits = val_place - err_place + 2 |
---|
160 | val_place = int(math.floor(val_place/3.))*3 |
---|
161 | val_str = "%.*g%se%d"%(total_digits, |
---|
162 | value/10.**val_place, |
---|
163 | err_str,val_place) |
---|
164 | |
---|
165 | return sign+val_str |
---|
166 | |
---|
167 | |
---|
168 | |
---|
169 | def test(): |
---|
170 | # Oops... renamed function after writing tests |
---|
171 | value_str = format_uncertainty_compact |
---|
172 | |
---|
173 | # val_place > err_place |
---|
174 | assert value_str(1235670,766000) == "1.24(77)e6" |
---|
175 | assert value_str(123567.,76600) == "124(77)e3" |
---|
176 | assert value_str(12356.7,7660) == "12.4(77)e3" |
---|
177 | assert value_str(1235.67,766) == "1.24(77)e3" |
---|
178 | assert value_str(123.567,76.6) == "124(77)" |
---|
179 | assert value_str(12.3567,7.66) == "12.4(77)" |
---|
180 | assert value_str(1.23567,.766) == "1.24(77)" |
---|
181 | assert value_str(.123567,.0766) == "0.124(77)" |
---|
182 | assert value_str(.0123567,.00766) == "0.0124(77)" |
---|
183 | assert value_str(.00123567,.000766) == "0.00124(77)" |
---|
184 | assert value_str(.000123567,.0000766) == "124(77)e-6" |
---|
185 | assert value_str(.0000123567,.00000766) == "12.4(77)e-6" |
---|
186 | assert value_str(.00000123567,.000000766) == "1.24(77)e-6" |
---|
187 | assert value_str(.000000123567,.0000000766) == "124(77)e-9" |
---|
188 | assert value_str(.00000123567,.0000000766) == "1.236(77)e-6" |
---|
189 | assert value_str(.0000123567,.0000000766) == "12.357(77)e-6" |
---|
190 | assert value_str(.000123567,.0000000766) == "123.567(77)e-6" |
---|
191 | assert value_str(.00123567,.000000766) == "0.00123567(77)" |
---|
192 | assert value_str(.0123567,.00000766) == "0.0123567(77)" |
---|
193 | assert value_str(.123567,.0000766) == "0.123567(77)" |
---|
194 | assert value_str(1.23567,.000766) == "1.23567(77)" |
---|
195 | assert value_str(12.3567,.00766) == "12.3567(77)" |
---|
196 | assert value_str(123.567,.0764) == "123.567(76)" |
---|
197 | assert value_str(1235.67,.764) == "1235.67(76)" |
---|
198 | assert value_str(12356.7,7.64) == "12356.7(76)" |
---|
199 | assert value_str(123567,76.4) == "123567(76)" |
---|
200 | assert value_str(1235670,764) == "1.23567(76)e6" |
---|
201 | assert value_str(12356700,764) == "12.3567(76)e6" |
---|
202 | assert value_str(123567000,7640) == "123.567(76)e6" |
---|
203 | assert value_str(1235670000,76400) == "1.23567(76)e9" |
---|
204 | |
---|
205 | # val_place == err_place |
---|
206 | assert value_str(123567,764000) == "0.12(76)e6" |
---|
207 | assert value_str(12356.7,76400) == "12(76)e3" |
---|
208 | assert value_str(1235.67,7640) == "1.2(76)e3" |
---|
209 | assert value_str(123.567,764) == "0.12(76)e3" |
---|
210 | assert value_str(12.3567,76.4) == "12(76)" |
---|
211 | assert value_str(1.23567,7.64) == "1.2(76)" |
---|
212 | assert value_str(.123567,.764) == "0.12(76)" |
---|
213 | assert value_str(.0123567,.0764) == "12(76)e-3" |
---|
214 | assert value_str(.00123567,.00764) == "1.2(76)e-3" |
---|
215 | assert value_str(.000123567,.000764) == "0.12(76)e-3" |
---|
216 | |
---|
217 | # val_place == err_place-1 |
---|
218 | assert value_str(123567,7640000) == "0.1(76)e6" |
---|
219 | assert value_str(12356.7,764000) == "0.01(76)e6" |
---|
220 | assert value_str(1235.67,76400) == "0.001(76)e6" |
---|
221 | assert value_str(123.567,7640) == "0.1(76)e3" |
---|
222 | assert value_str(12.3567,764) == "0.01(76)e3" |
---|
223 | assert value_str(1.23567,76.4) == "0.001(76)e3" |
---|
224 | assert value_str(.123567,7.64) == "0.1(76)" |
---|
225 | assert value_str(.0123567,.764) == "0.01(76)" |
---|
226 | assert value_str(.00123567,.0764) == "0.001(76)" |
---|
227 | assert value_str(.000123567,.00764) == "0.1(76)e-3" |
---|
228 | |
---|
229 | # val_place == err_place-2 |
---|
230 | assert value_str(12356700,7640000000) == "0.0(76)e9" |
---|
231 | assert value_str(1235670,764000000) == "0.00(76)e9" |
---|
232 | assert value_str(123567,76400000) == "0.000(76)e9" |
---|
233 | assert value_str(12356,7640000) == "0.0(76)e6" |
---|
234 | assert value_str(1235,764000) == "0.00(76)e6" |
---|
235 | assert value_str(123,76400) == "0.000(76)e6" |
---|
236 | assert value_str(12,7640) == "0.0(76)e3" |
---|
237 | assert value_str(1,764) == "0.00(76)e3" |
---|
238 | assert value_str(0.1,76.4) == "0.000(76)e3" |
---|
239 | assert value_str(0.01,7.64) == "0.0(76)" |
---|
240 | assert value_str(0.001,0.764) == "0.00(76)" |
---|
241 | assert value_str(0.0001,0.0764) == "0.000(76)" |
---|
242 | assert value_str(0.00001,0.00764) == "0.0(76)e-3" |
---|
243 | |
---|
244 | # val_place == err_place-3 |
---|
245 | assert value_str(12356700,76400000000) == "0.000(76)e12" |
---|
246 | assert value_str(1235670,7640000000) == "0.0(76)e9" |
---|
247 | assert value_str(123567,764000000) == "0.00(76)e9" |
---|
248 | assert value_str(12356,76400000) == "0.000(76)e9" |
---|
249 | assert value_str(1235,7640000) == "0.0(76)e6" |
---|
250 | assert value_str(123,764000) == "0.00(76)e6" |
---|
251 | assert value_str(12,76400) == "0.000(76)e6" |
---|
252 | assert value_str(1,7640) == "0.0(76)e3" |
---|
253 | assert value_str(0.1,764) == "0.00(76)e3" |
---|
254 | assert value_str(0.01,76.4) == "0.000(76)e3" |
---|
255 | assert value_str(0.001,7.64) == "0.0(76)" |
---|
256 | assert value_str(0.0001,0.764) == "0.00(76)" |
---|
257 | assert value_str(0.00001,0.0764) == "0.000(76)" |
---|
258 | assert value_str(0.000001,0.00764) == "0.0(76)e-3" |
---|
259 | |
---|
260 | # negative values |
---|
261 | assert value_str(-1235670,765000) == "-1.24(77)e6" |
---|
262 | assert value_str(-1.23567,.765) == "-1.24(77)" |
---|
263 | assert value_str(-.00000123567,.0000000765) == "-1.236(77)e-6" |
---|
264 | assert value_str(-12356.7,7.64) == "-12356.7(76)" |
---|
265 | assert value_str(-123.567,764) == "-0.12(76)e3" |
---|
266 | assert value_str(-1235.67,76400) == "-0.001(76)e6" |
---|
267 | assert value_str(-.000123567,.00764) == "-0.1(76)e-3" |
---|
268 | assert value_str(-12356,7640000) == "-0.0(76)e6" |
---|
269 | assert value_str(-12,76400) == "-0.000(76)e6" |
---|
270 | assert value_str(-0.0001,0.764) == "-0.00(76)" |
---|
271 | |
---|
272 | # zero values |
---|
273 | assert value_str(0,0) == "0" |
---|
274 | assert value_str(-1.23567,0) == "-1.23567" |
---|
275 | assert value_str(0,765000) == "0.00(77)e6" |
---|
276 | assert value_str(0,.765) == "0.00(77)" |
---|
277 | assert value_str(0,.000000765) == "0.00(77)e-6" |
---|
278 | assert value_str(0,76400) == "0.000(76)e6" |
---|
279 | assert value_str(0,7640) == "0.0(76)e3" |
---|
280 | |
---|
281 | # marked values |
---|
282 | assert value_str(-numpy.inf,None) == "-inf" |
---|
283 | assert value_str(numpy.inf,None) == "inf" |
---|
284 | assert value_str(numpy.NaN,None) == "NaN" |
---|
285 | |
---|
286 | # plus/minus form |
---|
287 | assert format_uncertainty_pm(-1.23567,0.765) == "-1.24 +/- 0.77" |
---|
288 | assert format_uncertainty_compact(-1.23567,0.765) == "-1.24(77)" |
---|
289 | assert format_uncertainty_pm(752.3567,0.01) == "752.357 +/- 0.01" |
---|
290 | assert format_uncertainty(-1.23567,0.765) == "-1.24(77)" |
---|
291 | |
---|
292 | # bad uncertainty |
---|
293 | assert format_uncertainty_pm(-1.23567,numpy.NaN) == "-1.23567" |
---|
294 | assert format_uncertainty_pm(-1.23567,-numpy.inf) == "-1.23567" |
---|
295 | assert format_uncertainty_pm(-1.23567,-0.1) == "-1.23567" |
---|
296 | assert format_uncertainty_compact(-1.23567,numpy.NaN) == "-1.23567" |
---|
297 | assert format_uncertainty_compact(-1.23567,-numpy.inf) == "-1.23567" |
---|
298 | assert format_uncertainty_compact(-1.23567,-0.1) == "-1.23567" |
---|
299 | |
---|
300 | # no uncertainty |
---|
301 | assert format_uncertainty_pm(-1.23567,0) == "-1.23567" |
---|
302 | assert format_uncertainty_compact(-1.23567,0) == "-1.23567" |
---|
303 | assert format_uncertainty_pm(-1.23567,None) == "-1.23567" |
---|
304 | assert format_uncertainty_compact(-1.23567,None) == "-1.23567" |
---|
305 | |
---|
306 | # inf uncertainty |
---|
307 | assert format_uncertainty_pm(-1.23567,numpy.inf) == "-1.23567 +/- inf" |
---|
308 | assert format_uncertainty_compact(-1.23567,numpy.inf) == "-1.23567(inf)" |
---|
309 | |
---|
310 | |
---|
311 | if __name__ == "__main__": test() |
---|
312 | |
---|