# This program is public domain # Author Paul Kienzle """ Format numbers nicely for printing. Usage:: >> from danse.common.util.formatnum import * >> v,dv = 757.2356,0.01032 >> print format_uncertainty_pm(v,dv) 757.235 +/- 0.010 >> format_uncertainty_compact(v,dv) 757.235(10) >> format_uncertainty(v,dv) 757.235(10) Set format_uncertainty.compact to False to use the +/- format by default, otherwise leave it at True for compact value(##) format. """ from __future__ import division import math import numpy __all__ = ['format_uncertainty', 'format_uncertainty_pm', 'format_uncertainty_compact'] # These routines need work for +/- formatting:: # - pm formats are not rigorously tested # - pm formats do not try to align the scale to multiples of 1000 # - pm formats do not try to align the value scale to uncertainty scale # Coordinating scales across a set of numbers is not supported. For easy # comparison a set of numbers should be shown in the same scale. One could # force this from the outside by adding scale parameter (either 10**n, n, or # a string representing the desired SI prefix) and having a separate routine # which computes the scale given a set of values. # Coordinating scales with units offers its own problems. Again, the user # may want to force particular units. This can be done by outside of the # formatting routines by scaling the numbers to the appropriate units then # forcing them to print with scale 10**0. If this is a common operation, # however, it may want to happen inside. # The value e is currently formatted into the number. Alternatively this # scale factor could be returned so that the user can choose the appriate # SI prefix when printing the units. This gets tricky when talking about # composite units such as 2.3e-3 m**2 -> 2300 mm**2, and with volumes # such as 1 g/cm**3 -> 1 kg/L. def format_uncertainty_pm(value, uncertainty=None): """ Given *value* v and *uncertainty* dv, return a string v +/- dv. The returned string uses only the number of digits warranted by the uncertainty in the measurement. If the uncertainty is 0 or not otherwise provided, the simple %g floating point format option is used. Infinite and indefinite numbers are represented as inf and NaN. """ return _format_uncertainty(value, uncertainty, compact=False) def format_uncertainty_compact(value, uncertainty=None): """ Given *value* v and *uncertainty* dv, return the compact representation v(##), where ## are the first two digits of the uncertainty. The returned string uses only the number of digits warranted by the uncertainty in the measurement. If the uncertainty is 0 or not otherwise provided, the simple %g floating point format option is used. Infinite and indefinite numbers are represented as inf and NaN. """ return _format_uncertainty(value, uncertainty, compact=True) class FormatUncertainty: """ Given *value* and *uncertainty*, return a concise string representation. This will either by the +/- form of :func:`format_uncertainty_pm` or the compact form of :func:`format_uncertainty_compact` depending on whether *compact* is specified or whether *format_uncertainty.compact* is True or False. """ compact = True def __call__(self, value, uncertainty): return _format_uncertainty(value, uncertainty, self.compact) format_uncertainty = FormatUncertainty() def _format_uncertainty(value,uncertainty,compact): """ Implementation of both the compact and the +/- formats. """ if numpy.isinf(value): return "inf" if value > 0 else "-inf" if numpy.isnan(value): return "NaN" # Uncertainty check must come after indefinite check since the %g # format string doesn't handle indefinite numbers consistently # across platforms. if uncertainty == None or uncertainty <= 0 or numpy.isnan(uncertainty): return "%g"%value if numpy.isinf(uncertainty): if compact: return "%g(inf)"%value else: return "%g +/- inf"%value # Process sign sign = "-" if value < 0 else "" value = abs(value) # Determine the number of digits in the value and the error # Note that uncertainty <= 0 is handled above err_place = int(math.floor(math.log10(uncertainty))) if value == 0: val_place = err_place-1 else: val_place = int(math.floor(math.log10(value))) # If pm, return a simple v +/- dv if not compact: scale = 10**(err_place-1) val_digits = val_place-err_place+2 return "%s%.*g +/- %.2g"%(sign,val_digits,value,uncertainty) # The remainder is for the v(dv) case err_str = "(%2d)"%int(uncertainty/10.**(err_place-1)+0.5) if err_place > val_place: # Degenerate case: error bigger than value # The mantissa is 0.#(##)e#, 0.0#(##)e# or 0.00#(##)e# if err_place - val_place > 2: value = 0 val_place = int(math.floor((err_place+2)/3.))*3 digits_after_decimal = val_place - err_place + 1 val_str = "%.*f%s"%(digits_after_decimal,value/10.**val_place,err_str) if val_place != 0: val_str += "e%d"%val_place elif err_place == val_place: # Degenerate case: error and value the same order of magnitude # The value is ##(##)e#, #.#(##)e# or 0.##(##)e# val_place = int(math.floor((err_place+1)/3.))*3 digits_after_decimal = val_place - err_place + 1 val_str = "%.*f%s"%(digits_after_decimal,value/10.**val_place,err_str) if val_place != 0: val_str += "e%d"%val_place elif err_place <= 1 and val_place >= -3: # Normal case: nice numbers and errors # The value is ###.###(##) digits_after_decimal = abs(err_place-1) val_str = "%.*f%s"%(digits_after_decimal,value,err_str) else: # Extreme cases: zeros before value or after error # The value is ###.###(##)e#, ##.####(##)e# or #.#####(##)e# total_digits = val_place - err_place + 2 val_place = int(math.floor(val_place/3.))*3 val_str = "%.*g%se%d"%(total_digits, value/10.**val_place, err_str,val_place) return sign+val_str def test(): # Oops... renamed function after writing tests value_str = format_uncertainty_compact # val_place > err_place assert value_str(1235670,766000) == "1.24(77)e6" assert value_str(123567.,76600) == "124(77)e3" assert value_str(12356.7,7660) == "12.4(77)e3" assert value_str(1235.67,766) == "1.24(77)e3" assert value_str(123.567,76.6) == "124(77)" assert value_str(12.3567,7.66) == "12.4(77)" assert value_str(1.23567,.766) == "1.24(77)" assert value_str(.123567,.0766) == "0.124(77)" assert value_str(.0123567,.00766) == "0.0124(77)" assert value_str(.00123567,.000766) == "0.00124(77)" assert value_str(.000123567,.0000766) == "124(77)e-6" assert value_str(.0000123567,.00000766) == "12.4(77)e-6" assert value_str(.00000123567,.000000766) == "1.24(77)e-6" assert value_str(.000000123567,.0000000766) == "124(77)e-9" assert value_str(.00000123567,.0000000766) == "1.236(77)e-6" assert value_str(.0000123567,.0000000766) == "12.357(77)e-6" assert value_str(.000123567,.0000000766) == "123.567(77)e-6" assert value_str(.00123567,.000000766) == "0.00123567(77)" assert value_str(.0123567,.00000766) == "0.0123567(77)" assert value_str(.123567,.0000766) == "0.123567(77)" assert value_str(1.23567,.000766) == "1.23567(77)" assert value_str(12.3567,.00766) == "12.3567(77)" assert value_str(123.567,.0764) == "123.567(76)" assert value_str(1235.67,.764) == "1235.67(76)" assert value_str(12356.7,7.64) == "12356.7(76)" assert value_str(123567,76.4) == "123567(76)" assert value_str(1235670,764) == "1.23567(76)e6" assert value_str(12356700,764) == "12.3567(76)e6" assert value_str(123567000,7640) == "123.567(76)e6" assert value_str(1235670000,76400) == "1.23567(76)e9" # val_place == err_place assert value_str(123567,764000) == "0.12(76)e6" assert value_str(12356.7,76400) == "12(76)e3" assert value_str(1235.67,7640) == "1.2(76)e3" assert value_str(123.567,764) == "0.12(76)e3" assert value_str(12.3567,76.4) == "12(76)" assert value_str(1.23567,7.64) == "1.2(76)" assert value_str(.123567,.764) == "0.12(76)" assert value_str(.0123567,.0764) == "12(76)e-3" assert value_str(.00123567,.00764) == "1.2(76)e-3" assert value_str(.000123567,.000764) == "0.12(76)e-3" # val_place == err_place-1 assert value_str(123567,7640000) == "0.1(76)e6" assert value_str(12356.7,764000) == "0.01(76)e6" assert value_str(1235.67,76400) == "0.001(76)e6" assert value_str(123.567,7640) == "0.1(76)e3" assert value_str(12.3567,764) == "0.01(76)e3" assert value_str(1.23567,76.4) == "0.001(76)e3" assert value_str(.123567,7.64) == "0.1(76)" assert value_str(.0123567,.764) == "0.01(76)" assert value_str(.00123567,.0764) == "0.001(76)" assert value_str(.000123567,.00764) == "0.1(76)e-3" # val_place == err_place-2 assert value_str(12356700,7640000000) == "0.0(76)e9" assert value_str(1235670,764000000) == "0.00(76)e9" assert value_str(123567,76400000) == "0.000(76)e9" assert value_str(12356,7640000) == "0.0(76)e6" assert value_str(1235,764000) == "0.00(76)e6" assert value_str(123,76400) == "0.000(76)e6" assert value_str(12,7640) == "0.0(76)e3" assert value_str(1,764) == "0.00(76)e3" assert value_str(0.1,76.4) == "0.000(76)e3" assert value_str(0.01,7.64) == "0.0(76)" assert value_str(0.001,0.764) == "0.00(76)" assert value_str(0.0001,0.0764) == "0.000(76)" assert value_str(0.00001,0.00764) == "0.0(76)e-3" # val_place == err_place-3 assert value_str(12356700,76400000000) == "0.000(76)e12" assert value_str(1235670,7640000000) == "0.0(76)e9" assert value_str(123567,764000000) == "0.00(76)e9" assert value_str(12356,76400000) == "0.000(76)e9" assert value_str(1235,7640000) == "0.0(76)e6" assert value_str(123,764000) == "0.00(76)e6" assert value_str(12,76400) == "0.000(76)e6" assert value_str(1,7640) == "0.0(76)e3" assert value_str(0.1,764) == "0.00(76)e3" assert value_str(0.01,76.4) == "0.000(76)e3" assert value_str(0.001,7.64) == "0.0(76)" assert value_str(0.0001,0.764) == "0.00(76)" assert value_str(0.00001,0.0764) == "0.000(76)" assert value_str(0.000001,0.00764) == "0.0(76)e-3" # negative values assert value_str(-1235670,765000) == "-1.24(77)e6" assert value_str(-1.23567,.765) == "-1.24(77)" assert value_str(-.00000123567,.0000000765) == "-1.236(77)e-6" assert value_str(-12356.7,7.64) == "-12356.7(76)" assert value_str(-123.567,764) == "-0.12(76)e3" assert value_str(-1235.67,76400) == "-0.001(76)e6" assert value_str(-.000123567,.00764) == "-0.1(76)e-3" assert value_str(-12356,7640000) == "-0.0(76)e6" assert value_str(-12,76400) == "-0.000(76)e6" assert value_str(-0.0001,0.764) == "-0.00(76)" # zero values assert value_str(0,0) == "0" assert value_str(-1.23567,0) == "-1.23567" assert value_str(0,765000) == "0.00(77)e6" assert value_str(0,.765) == "0.00(77)" assert value_str(0,.000000765) == "0.00(77)e-6" assert value_str(0,76400) == "0.000(76)e6" assert value_str(0,7640) == "0.0(76)e3" # marked values assert value_str(-numpy.inf,None) == "-inf" assert value_str(numpy.inf,None) == "inf" assert value_str(numpy.NaN,None) == "NaN" # plus/minus form assert format_uncertainty_pm(-1.23567,0.765) == "-1.24 +/- 0.77" assert format_uncertainty_compact(-1.23567,0.765) == "-1.24(77)" assert format_uncertainty_pm(752.3567,0.01) == "752.357 +/- 0.01" assert format_uncertainty(-1.23567,0.765) == "-1.24(77)" # bad uncertainty assert format_uncertainty_pm(-1.23567,numpy.NaN) == "-1.23567" assert format_uncertainty_pm(-1.23567,-numpy.inf) == "-1.23567" assert format_uncertainty_pm(-1.23567,-0.1) == "-1.23567" assert format_uncertainty_compact(-1.23567,numpy.NaN) == "-1.23567" assert format_uncertainty_compact(-1.23567,-numpy.inf) == "-1.23567" assert format_uncertainty_compact(-1.23567,-0.1) == "-1.23567" # no uncertainty assert format_uncertainty_pm(-1.23567,0) == "-1.23567" assert format_uncertainty_compact(-1.23567,0) == "-1.23567" assert format_uncertainty_pm(-1.23567,None) == "-1.23567" assert format_uncertainty_compact(-1.23567,None) == "-1.23567" # inf uncertainty assert format_uncertainty_pm(-1.23567,numpy.inf) == "-1.23567 +/- inf" assert format_uncertainty_compact(-1.23567,numpy.inf) == "-1.23567(inf)" if __name__ == "__main__": test()