asteca / ASteCA

Code for the ASteCA package.
http://asteca.github.io/
MIT License
18 stars 6 forks source link

Bad parameter rounding #248

Closed Gabriel-p closed 7 years ago

Gabriel-p commented 8 years ago

Sometimes parameter values like for example an age of 7.85 will be rounded to 7.8 instead of 7.9.

This is a known issue with Python and floats. I sort of fixed it here. <-- did not.

Related questions in SO:

For some clusters I've seen a value shown in screen, another printed in the output image, and another one to the output file. E.g. NGC294 (20th run): 0.000481 (screen), 0.0005 (image), 0.001 (output file). This is an issue with the rounding done for both the parameter value and its error.

The general problem is much more complicated and requires a lot of heuristics to get all of the cases right. The errors_round function needs a re-write to correctly handle them. Here's a start <-- Check new code below instead.


And easier way to go about this:

  1. Keep the first significant figure for the error
  2. Keep a number of fixed figures for each parameter (ie: 5 for the metallicity, 3 for extinction, 2 for age and distance modulus, 0 for mass, etc)

Also, check uncertainties package.

Gabriel-p commented 8 years ago

And easier way to go about this:

  1. Keep the first significant figure for the error

from decimal import Decimal
import numpy as np

def format_5(f):
    """
    Round floats that end with a '5' correctly, bypassing Python's issue
    with the round() function.

    See: http://codereview.stackexchange.com/q/122243/35351
    """
    # If the last digit is 5 or larger, round up.
    if int(f[-1]) >= 5:
        r_up_down = 'ROUND_UP'
    else:
        r_up_down = 'ROUND_DOWN'

    return Decimal.quantize(Decimal(f), Decimal(f[:-1]), rounding=r_up_down)

def count_zeros(sf):
    """
    Count the number of leading zeros until the first non-zero digit.
    Only works on float < 1.
    """
    nd = 0
    for d in sf[2:]:  # Remove trailing '0.'
        if d == '0':
            nd += 1
        else:
            # Stop at the first non-zero digit found.
            break
    return nd

def round1(f):
    """
    Round *positive* float to 1 significant figure.
    """
    # Float as string.
    sf = str(f)

    if f == 0.:
        f_r = 0.
    elif 0. < f < 1.:
        # Number of leading zeros.
        nd = count_zeros(sf)
        # If the float already has a single non-zero digit.
        if len(sf) == (2 + nd + 1):
            f_r = f
        else:
            # Isolate first two non-zero digits.
            dn = sf[2+nd:nd+4]
            # Create new float using the first two digits to round
            # to a single digit.
            sf_r = '0.' + dn
            # Round digits.
            sf_r5 = format_5(sf_r)
            # Generate final properly rounded float.
            # Catch special case.
            if sf_r5 == Decimal('1.0'):
                if nd > 0:
                    f_r = '0.' + '0'*nd + '1'
                else:
                    f_r = 1.
            else:
                # General case.
                f_r = '0.' + '0'*nd + str(sf_r5)[2:]
    elif 1. <= f < 10.:
        # Create float with first two digits, without the floating point.
        sf_r = '0.' + sf[0] + sf[2]
        # Round to one digit.
        sf_r5 = format_5(sf_r)
        # Catch special case.
        if sf_r5 == Decimal('1.0'):
            f_r = 10.
        else:
            # General case.
            f_r = str(sf_r5)[-1]
    else:
        # Number of digits before the decimal point.
        nb = len(sf.split('.')[0])
        # Create float with first two digits.
        sf_r = '0.' + sf[:2]
        # Round to one digit.
        sf_r5 = format_5(sf_r)
        # Catch special case.
        if sf_r5 == Decimal('1.0'):
            f_r = '1' + str(sf_r5)[-1] + '0'*(nb-1) + '.'
        else:
            # General case.
            f_r = str(sf_r5)[-1] + '0'*(nb-1) + '.'

    return float(f_r)

f_lst = [0.01, 0.1, 0.99, 0.099, 0.95, 0.094, 0.00012, 0.055, 0.0075, 0.23]
f_lst = [1.0025, 5.57, 6.99, 9.49, 9.55, 9.99]
f_lst = [10., 10.05, 500.5, 556, 9956.2]
f_lst = np.random.uniform(10., 100000., 100)
for f in f_lst:
    print 'orig:', f
    f_r = round1(f)
    print 'round:', f_r, '\n'
  1. Keep a number of fixed figures for each parameter (ie: 5 for the metallicity, 3 for extinction, 2 for age and distance modulus, 0 for mass, etc)
Gabriel-p commented 7 years ago

This issue raises unnecessary complications.

It's easier to simply output values with a fixed number of decimals, and let the user decide how to round them.