diff options
Diffstat (limited to 'silx/utils/number.py')
-rw-r--r-- | silx/utils/number.py | 143 |
1 files changed, 143 insertions, 0 deletions
diff --git a/silx/utils/number.py b/silx/utils/number.py new file mode 100644 index 0000000..e6a87e7 --- /dev/null +++ b/silx/utils/number.py @@ -0,0 +1,143 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016-2018 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ###########################################################################*/ +"""Utilitary functions dealing with numbers. +""" + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "05/06/2018" + +import numpy +import re +import logging + + +_logger = logging.getLogger(__name__) + + +_biggest_float = None + +if hasattr(numpy, "longdouble"): + finfo = numpy.finfo(numpy.longdouble) + # The bit for sign is missing here + bits = finfo.nexp + finfo.nmant + if bits > 64: + _biggest_float = numpy.longdouble + # From bigger to smaller + _float_types = (numpy.longdouble, numpy.float64, numpy.float32, numpy.float16) +if _biggest_float is None: + _biggest_float = numpy.float64 + # From bigger to smaller + _float_types = (numpy.float64, numpy.float32, numpy.float16) + + +_parse_numeric_value = re.compile("^\s*[-+]?0*(\d+?)?(?:\.(\d+))?(?:[eE]([-+]?\d+))?\s*$") + + +def is_longdouble_64bits(): + """Returns true if the system uses floating-point 64bits for it's + long double type. + + .. note:: Comparing `numpy.longdouble` and `numpy.float64` on Windows is not + possible (or at least not will all the numpy version) + """ + return _biggest_float == numpy.float64 + + +def min_numerical_convertible_type(string, check_accuracy=True): + """ + Parse the string and return the smallest numerical type to use for a safe + conversion. + + :param str string: Representation of a float/integer with text + :param bool check_accuracy: If true, a warning is pushed on the logger + in case there is a loss of accuracy. + :raise ValueError: When the string is not a numerical value + :retrun: A numpy numerical type + """ + if string == "": + raise ValueError("Not a numerical value") + match = _parse_numeric_value.match(string) + if match is None: + raise ValueError("Not a numerical value") + number, decimal, exponent = match.groups() + + if decimal is None and exponent is None: + # It's an integer + # TODO: We could find the int type without converting the number + value = int(string) + return numpy.min_scalar_type(value).type + + # Try floating-point + try: + value = _biggest_float(string) + except ValueError: + raise ValueError("Not a numerical value") + + if number is None: + number = "" + if decimal is None: + decimal = "" + if exponent is None: + exponent = "0" + + nb_precision_digits = int(exponent) - len(decimal) - 1 + precision = _biggest_float(10) ** nb_precision_digits * 2.5 + previous_type = _biggest_float + for numpy_type in _float_types: + if numpy_type == _biggest_float: + # value was already casted using the bigger type + continue + reduced_value = numpy_type(value) + if not numpy.isfinite(reduced_value): + break + # numpy isclose(atol=is not accurate enough) + diff = value - reduced_value + # numpy 1.8.2 looks to do the substraction using float64... + # we lose precision here + diff = numpy.abs(diff) + if diff > precision: + break + previous_type = numpy_type + + # It's the smaller float type which fit with enougth precision + numpy_type = previous_type + + if check_accuracy and numpy_type == _biggest_float: + # Check the precision using the original string + expected = number + decimal + # This format the number without python convertion + try: + result = numpy.array2string(value, precision=len(number) + len(decimal), floatmode="fixed") + except TypeError: + # numpy 1.8.2 do not have floatmode argument + _logger.warning("Not able to check accuracy of the conversion of '%s' using %s", string, _biggest_float) + return numpy_type + + result = result.replace(".", "").replace("-", "") + if not result.startswith(expected): + _logger.warning("Not able to convert '%s' using %s without losing precision", string, _biggest_float) + + return numpy_type |