MicroPython round() Function

General discussions and questions abound development of code with MicroPython that is not hardware specific.
Target audience: MicroPython Users.
User avatar
on4aa
Posts: 70
Joined: Sat Nov 11, 2017 8:41 pm
Location: Europe
Contact:

Re: MicroPython round() Function

Post by on4aa » Mon Dec 25, 2017 3:05 pm

You have all been very naughty!
Look at the following Christmas bug Santa has brought…

The problem concerns the builtin function round().
In its current implementation, rounding is done to the nearest float that can be represented by the machine.
This is wrong, rounding should be done to the nearest float with the correct least significant decimal that can be represented by the machine.

Consider the following little main.py script:

Code: Select all

    f = 1011.640071
    print('\n%f = float' % f)
    print('%f = float rounded to one decimal' % round(f, 1))
    print('%6.1f = formatted rounded float\n' % round(f, 1))

    delta = 0.000040
    print('%f = float correctly rounded to least significant first decimal' % (round(f, 1) + delta))
    print('%6.1f = formatted correctly rounded float\n' % (round(f, 1) + delta))

    epsilon = abs(7./3 - 4./3 - 1)
    print('%f = 1000 × machine epsilon' % (1000 * epsilon))
    print(epsilon)
    print()
Here is the output on a PyBoard:

Code: Select all

    MicroPython v1.9.2-87-gda8c4c26 on 2017-09-13; PYBv1.1 with STM32F405RG

    1011.639952 = float
    1011.599898 = float rounded to one decimal
    1011.5 = formatted rounded float

    1011.600017 = float correctly rounded to least significant first decimal
    1011.6 = formatted correctly rounded float

    0.000119 = 1000 × machine epsilon
    1.192093e-07
Things are not any different on a Pycom LoPy ESP32:

Code: Select all

    MicroPython 2ac6da2 on 2017-12-18; LoPy with ESP32

    1011.639952 = float
    1011.599898 = float rounded to one decimal
    1011.5 = formatted rounded float

    1011.600017 = float correctly rounded to least significant first decimal
    1011.6 = formatted correctly rounded float

    0.000119 = 1000 × machine epsilon
    1.192093e-07
Note that the difference between correct and erroneous rounding is about a 1000 times the machine epsilon, which makes sense.

For certain applications, such poor rounding can have very serious consequences down the line…

This finding may account for the multitude of other reports complaining about round() behaviour:
- viewtopic.php?t=802
- https://github.com/bbcmicrobit/micropython/issues/367
- https://github.com/micropython/micropython/issues/2616
- https://support.microbit.org/support/so ... ding-error
Serge

User avatar
on4aa
Posts: 70
Joined: Sat Nov 11, 2017 8:41 pm
Location: Europe
Contact:

Re: MicroPython round() Function

Post by on4aa » Thu Dec 28, 2017 7:03 pm

Here is a follow up for the forum, taken from the GitHub bug report discussion:

Float formatting by itself is not involved, but I would not be surprised that many of those bug reports stem from the problem reported here.
What seems to be reported here is that round(1011.640071, 1) with 32-bit float is rounded "too low" and that there exists a closer machine value (representable in a 32-bit float) to the true 1011.6 value.
That is not entirely correct. round(1011.640071, 1) is being rounded to the nearest representable 32-bit float, which happens to be 1011.599898.

However, the second nearest representable 32-bit float 1011.600017 —which is actually still within the machine epsilon range— would be a better float representation simply because its least significant decimal .6 is correct. When rounding, the least significant decimal of the float representation is what needs to be correct. This is the essential thing one is after when rounding. What follows this decimal does not matter.

I consider this to be a very serious bug, worthy of a new MicroPython release when resolved. In scientific experiments, this bug may cause formatted floats to suddenly jump from 1011.7 to 1011.5, when the underlying float for the latter is actually closer to 1011.6. This is how I noticed this. Errors of this magnitude are actually capable of sparking global warming climate discussions, to say the least.

I am willing to have a look at the C code, even though I am not experienced with C.
Could you provide a link to the actual routine?
I have been browsing this GitHub repository, but was unable to find it.

By the way, the MicroPython code for testing the round() builtin function would also better include less naive example values.
Serge

User avatar
on4aa
Posts: 70
Joined: Sat Nov 11, 2017 8:41 pm
Location: Europe
Contact:

Re: MicroPython round() Function

Post by on4aa » Thu Dec 28, 2017 7:52 pm

Serge

User avatar
dhylands
Posts: 3821
Joined: Mon Jan 06, 2014 6:08 pm
Location: Peachland, BC, Canada
Contact:

Re: MicroPython round() Function

Post by dhylands » Thu Dec 28, 2017 11:41 pm

The number 1011.599898 is closer to 1011.6 than the other one, so using the closer one will cause less error for further mathematical calculations.

The number 1011.599898 should be printed as 1011.6 if you're just printing 1 digit after the decimal, and the fact that it isn't is a bug in the formatting routine.

User avatar
on4aa
Posts: 70
Joined: Sat Nov 11, 2017 8:41 pm
Location: Europe
Contact:

Re: MicroPython round() Function

Post by on4aa » Fri Dec 29, 2017 1:06 am

Yep, you are right, Dave.
The same effect can be reproduced with much larger numbers on CPython.
I have also been reading about the IEEE 754 standard.
A summary of my findings can be found in the now somewhat redundant bug report.

Below graph explains very well the difference in floating point arithmetic limitations of 64-bit CPython versus 32-bit MicroPython.
Image

It even seems that IEEE 754-2008 is up for revision in 2018 about this very issue. Perhaps, we as the MicroPython community, should chime in on this open revision process. The trade-off between a tiny loss in precision (still within one ULP) versus gain in rounding convenience appears to be open for debate.

The following workaround resolves this issue on all platforms, but it may look a bit counter-intuitive to the casual programmer:

Code: Select all

>>> f = 1011.639952
>>> print('%f' % (round(f,1)))
1011.599898
>>> print('%s' % (repr(round(f,1))))
1011.6
The question is now: Who is to blame for this human-unfriendly behaviour; string formatting or ultimately IEEE 754-2008 through the round() function?
One thing is sure: A lot of people are falling into this rounding trap!
Last edited by on4aa on Fri Dec 29, 2017 6:40 pm, edited 1 time in total.
Serge

User avatar
on4aa
Posts: 70
Joined: Sat Nov 11, 2017 8:41 pm
Location: Europe
Contact:

Re: MicroPython round() Function

Post by on4aa » Fri Dec 29, 2017 1:51 am

Still, there seems to be something wrong…

Code: Select all

>>> f = 1011.60004
>>> print('%f' % f)
1011.600017
OK, but this should have the same internal representation:

Code: Select all

>>> f = 1011.60003
>>> print('%f' % f)
1011.599898
However, it has an internal representation that is further off.
Serge

User avatar
dhylands
Posts: 3821
Joined: Mon Jan 06, 2014 6:08 pm
Location: Peachland, BC, Canada
Contact:

Re: MicroPython round() Function

Post by dhylands » Fri Dec 29, 2017 3:08 pm

A 32-bit float can only store 6 significant digits. Anything beyond that will typically be noise produced by the formatting routine, and isn't necessarily an accurate representation of the number.

To analyze things properly, you need to get at the actual 32-bit value that's being stored.

User avatar
on4aa
Posts: 70
Joined: Sat Nov 11, 2017 8:41 pm
Location: Europe
Contact:

Re: MicroPython round() Function

Post by on4aa » Fri Dec 29, 2017 6:47 pm

@Dave You are right about the noise. That is the IEEE 754 graph above.

Anyhow, for me the lesson learned is that round() is not sufficient for string formatting; repr(round()) is required.

Code: Select all

>>> f = 1011.639952
>>> print('%f' % (round(f,1)))
1011.599898
>>> print('%s' % (repr(round(f,1))))
1011.6
I will raise this rounding matter with the IEEE 754 revision group. Just to see what they have to say about this (and probably to get myself ridiculed in the process :lol: )
Serge

Iyassou
Posts: 42
Joined: Sun Jun 26, 2016 9:15 am

Re: MicroPython round() Function

Post by Iyassou » Fri Feb 09, 2018 8:27 am

Since this thread already has the appropriate title, has anyone tried overloading the __round__ builtin inside a class? I'm currently writing a matrix module and when I run it on Python 3.6.4 I can call round on a matrix and it returns a matrix with rounded coefficients as expected.

Code: Select all

>>> a = matrix([1.1, 2.1, 3.1], [4.1, 5.1, 6.1], [7.1, 8.1, 9.1])
>>> a
matrix( [1.1, 2.1, 3.1],
        [4.1, 5.1, 6.1],
        [7.1, 8.1, 9.1] )
>>> round(a)
matrix( [1.0, 2.0, 3.0],
        [4.0, 5.0, 6.0],
        [7.0, 8.0, 9.0] )
When I try it on Micropython 1.9.3 I get:

Code: Select all

>>> a = matrix([1.1,2.1,3.1],[4.1,5.1,6.1],[7.1,8.1,9.1])
>>> a
matrix( [1.1, 2.1, 3.1],
        [4.1, 5.1, 6.1],
        [7.1, 8.1, 9.1] )
>>> round(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't convert matrix to float
Here's my overloaded round function:

Code: Select all

def __round__(self, _10_exponent=0):
		result = [[0 for i in range(self.size[0])] for j in range(self.size[1])]
		for i in range(self.size[0]):
			for j in range(self.size[1]):
				result[i][j] = round(self[i][j], _10_exponent)
		return matrix(*result)
Subscripting a matrix gets the coefficient, int or float, at [row][column]. Any suggestions?

User avatar
pythoncoder
Posts: 5956
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: MicroPython round() Function

Post by pythoncoder » Fri Feb 09, 2018 9:48 am

It looks like the __round__() special method is not supported. There is no reference to it in the tests (tests/basics/special_methods*) even as an unsupported method. You could raise an issue.
Peter Hinch
Index to my micropython libraries.

Post Reply