Discuss Scratch

JediChickenD
Scratcher
8 posts

Numeric precision issues with floor() and log()

I'm trying to understand why the simple equation “floor(log(1000))” fails to produce the correct answer of 3.0.

The test goes something like this:

set [logOneThousand] to ([log v] of (1000)) // ok, this much appears to work, shows 3
set [floorOfThree] to ([floor v] of (3)) // ok, this much appears to work, shows 3
set [floorOfLogOneThousand] to ([floor v] of ([log v] of (1000))) // WRONG!, shows 2

The implication is that log(1000) returns just less than the correct answer of 3…

This can be experimentally “proven” by multiplying the residual (ie, the difference between correct answer of 3.0 and wrong answer given by log(1000)) by some large power of 10, until it is large enough to be represented in Scratch's variable display. (because if the residual were correctly zero, then no multiplier would be sufficient to reveal any residual “precision noise”) Demonstrated in this sketch

This specific simple example doesn't really matter that much, but it causes great concern that Scratch's number handling may not be as rigorous as I require. Does Scratch use IEEE754 double-precision numbers? If so, this shouldn't be an issue. (it isn't an issue in other languages that are IEEE754-compliant such as Javascript, Lua, C/C++, etc. for example, the equivalent Javascript of “Math.floor(Math.log10(1000))” evaluates to 3.0 as expected)

Any insight or ideas? Thanks.
D-ScratchNinja
Scratcher
1000+ posts

Numeric precision issues with floor() and log()

This project might show something similar to the precision error here.
JediChickenD
Scratcher
8 posts

Numeric precision issues with floor() and log()

Thanks. That other project demonstrates a problem with representation - a result of finite precision, but isn't quite the same problem that I'm attempting to describe…

There are some numbers (for example 1/3) that cannot be exactly represented as a finite expanded series of binary fractions, thus “off by least-significant-bit” representation errors will occur if you attempt to print those numbers to their full precision (52 bit mantissa gives about 16 decimal digits, for IEEE754).

The problem I'm having is with calculation - all of the numbers (ie: 3, 1000) involved in my equation can be exactly represented, and you would expect an IEEE754-compliant environment to properly evaluate the correct answer. (that is: finite precision shouldn't be a concern for the calculation of that equation, the answer should be 3.0 and exactly represented)

The evidence suggests that Scratch's log() implementation is flawed / non-compliant. (but, is it even claimed to be compliant? can't find a reference)

Last edited by JediChickenD (April 17, 2019 18:39:11)

game_pr0grammer
Scratcher
500+ posts

Numeric precision issues with floor() and log()

JediChickenD wrote:

The evidence suggests that Scratch's log() implementation is flawed / non-compliant. (but, is it even claimed to be compliant? can't find a reference)

Since Scratch 3.0 is programmed entirely used JavaScript, it most likely uses the built-in Math.log function.

wait… hold on

Okay so I tested log10(1000) on both Scratch and a JS editor
Scratch gave me 2.9999999999996 (probably a floating point error)
JS gave me 3

I noticed the floor of 2.999999996 is 2, and when you floor “logOneThousand” you get two. What is probably happening here is that Scratch displays the right number, but in reality it probably says something like 2.99999999999…
JediChickenD
Scratcher
8 posts

Numeric precision issues with floor() and log()

game_pr0grammer wrote:

Okay so I tested log10(1000) on both Scratch and a JS editor
Scratch gave me 2.9999999999996 (probably a floating point error)
JS gave me 3
.

Right. (and is essentially what I said in my first post)

The correct answer is 3.0, and that is not open for discussion. Javascript, and nearly every other modern language that implements double-precision floating point numbers, get it right. Scratch does not.

My intent isn't to bash Scratch, just to ask “why”, and if that sort of weird math result should be expected. If all of Scratch is written in Javascript then it's hard to understand why they'd return different values for such a simple expression.
ihgfedcba
Scratcher
100+ posts

Numeric precision issues with floor() and log()

JediChickenD wrote:

set [logOneThousand] to ([log v] of (1000)) // ok, this much appears to work, shows 3
set [floorOfThree] to ([floor v] of (3)) // ok, this much appears to work, shows 3
set [floorOfLogOneThousand] to ([floor v] of ([log v] of (1000))) // WRONG!, shows 2

Use this script to show the full precision:
set [logOneThousand] to (join ([log v] of (1000))())
set [floorOfThree] to (join ([floor v] of (3))())
set [floorOfLogOneThousand] to (join ([floor v] of ([log v] of (1000)))())

Last edited by ihgfedcba (April 18, 2019 04:24:27)

JediChickenD
Scratcher
8 posts

Numeric precision issues with floor() and log()

ihgfedcba wrote:

Use this script to show the full precision:

Ah, cool tip, didn't know, thanks!
That greatly simplifies explaining the problem, this is all you need to know:

set [logOneThousand] to (join ([log v] of (1000))()) // incorrectly gives 2.9999999999999996

By the way, the problem is “real”, and I've tracked down the culprit (thanks to game_pr0grammer for the javascript clue, went hunting for source).

The trouble is on line 146 of scratch3_operators.js where they implement it as a base conversion from e to 10, rather than directly calling Javascript's Math.log10().

I understand why they'd do that, as Math.log10() isn't supported everywhere (IE ruins things yet again!). But the two-step equation does introduce rounding error that a one-step native log10() wouldn't. I suppose it'd be nice if it checked for the existence of a natively implemented log10() and used it when available.

Failing that, an alternate formulation that might be less prone to rounding error is Math.log(n) * Math.LOG10E – it's still a base conversion, but it's the “reciprocal” of the one they're using because multiplying a pre-divided constant can be a bit more stable than dividing a pre-multiplied constant (and does correctly return 3.0 when n=1000.0)

Well, that answers the original question for me. Unfortunately, Scratch isn't suitable as-is for the task I had in mind. I don't know if the authors/maintainers read these forums, but I hope they'll someday give some thought to the log() function's implementation.

Cheers all, thanks for letting me bounce around ideas with you.
minor-edit
Scratcher
500+ posts

Numeric precision issues with floor() and log()

This bug is still there.

Powered by DjangoBB