Closed arve0 closed 7 years ago
Both calculations are done with floating point arithmetic and actually result in the same value:
1cm + 1mm to cm // = 1.0999999999999999 cm
1mm + 1cm to cm // = 1.0999999999999999 cm
I would suggest formatting the result to about 12 to 14 decimal places, depending on what works best for you. I believe this is what Math Notepad does. Another possibility is using BigNumber, if you need even greater precision.
Thanks! Setting precision below 17 fixes this.
Just curious, why does this only happen to cm?
> n = m.eval("1cm + 1mm in meter").format(20)
'0.011 meter'
> n = m.eval("1cm + 1mm in cm").format(20)
'1.0999999999999999 cm'
> n = m.eval("1cm + 1mm in mm").format(20)
'11 mm'
> n = m.eval("1cm + 1mm in micrometer").format(20)
'11000 micrometer'
I don't know. Floating point arithmetic is mysterious to me. I think it is something many people have just learned to live with.
Here is a far more information on the subject: https://en.wikipedia.org/wiki/IEEE_floating_point And yet even more information: https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
I think (and hope), that I have a good conceptual understanding of floats. I made this contrived example, which I assume is similar in the JS-engine implementation when converting numbers to string. Note that I do calculations with type number
, the correct implementation would be with type string
and placing the numbers in correct places.
function numberStored (number) {
let binaryRepresentation = number.toString(2) // 0.000011010110110
let [integer, decimal] = binaryRepresentation.split('.')
let intValues = integer.split('').reverse()
.map((b, i) => b == 1 ? Math.pow(2, i) : 0)
.filter(v => v !== 0) // remove zeroes
let floatValues = decimal.split('')
.map((b, i) => b == 1 ? Math.pow(2, -1 - i) : 0)
.filter(v => v !== 0) // remove zeroes
return { intValues, floatValues }
}
let res = numberStored(0.011)
res == { intValues: [],
floatValues:
[ 0.0078125,
0.001953125,
0.0009765625,
0.000244140625,
0.00000762939453125,
0.000003814697265625,
0.0000019073486328125,
2.384185791015625e-7,
5.960464477539063e-8,
1.4901161193847656e-8,
7.450580596923828e-9,
5.820766091346741e-11,
9.094947017729282e-13,
4.547473508864641e-13,
2.842170943040401e-14,
3.552713678800501e-15,
4.440892098500626e-16,
2.220446049250313e-16,
5.551115123125783e-17,
2.7755575615628914e-17,
1.3877787807814457e-17,
3.469446951953614e-18 ] }
res.reduce((sum, v) => sum + v, 0) // 0.011
So with the assumption that mathjs uses the binary representation of 0.011 as the value when creating string, result should be deterministic and same for meter, cm, mm, and so on.
I've debugged a bit and found this interesting:
const m = require('mathjs')
let v = m.parse('1cm + 1mm') // not evaluated yet, no signs of 1.099999...
v.compile().eval.toString // still no signs of 1.09999...
v.compile().eval() // 1.09999.. emerges
Another example:
> m.format(0.011, 100)
'0.011'
> m.format(0.11, 100)
'0.11'
> m.format(1.1, 100)
'1.1' // why not 1.09999999 ?
> m.format(11, 100)
'11'
Maybe with plus instead?
> m.format(0.01 + 0.001, 100)
'0.011'
> m.format(0.1 + 0.01, 100)
'0.11'
> m.format(1 + 0.1, 100)
'1.1' // same!
> m.format(10 + 1, 100)
'11'
So I still feel there is something funny going on, reopening.
Btw, code for add in mathjs is here: https://github.com/josdejong/mathjs/blob/master/lib/function/arithmetic/addScalar.js (used some time to find it)
Can't see that it does anything special.
If I understood it, the magic (converting 1 cm to value) happens at these places:
https://github.com/josdejong/mathjs/blob/master/lib/function/arithmetic/multiplyScalar.js#L36 https://github.com/josdejong/mathjs/blob/e105f60ab795dedcd294a66d7fcf0c3d0b24fea5/lib/type/unit/Unit.js#L457
Given by eval:
m.parse('1cm + 1mm').compile().eval
function eval (scope) {
if (scope) _validateScope(scope)
scope = scope || {}
return math["add"](
math["multiply"](1, ("cm" in scope ? getSafeProperty(scope, "cm") : new Unit(null, "cm"))),
math["multiply"](1, ("mm" in scope ? getSafeProperty(scope, "mm") : new Unit(null, "mm")))
)
}
When adding two units or converting a unit, you can get round-off errors because the Units use regular numbers (floats).
The function math.format
rounds to a specific precision, which is a great way to get rid of round-off errors (see docs), and apparently it does it's job :).
I'm not entirely sure what question you try to get answered exactly?
I'm sorry for rambling, TL;DR:
Why does math.eval('1cm + 1mm')
get a rounding error when a = 0.01; b = 0.001; c = a + b
does not. Both are represented in float, why are the results different?
you can get round-off errors because the Units use regular numbers (floats).
Are you saying that other numbers do not use floats?
The function math.format rounds to a specific precision, which is a great way to get rid of round-off errors (see docs), and apparently it does it's job :).
What about when I specify above the maximum precision, why does it not give the same rounding error? Assuming JS has 64 bit float, precision is 15.96 digits according to wikipedia. Then math.format(c, 20)
should give the same rounding error, right?
The Units of mathjs store the value they hold in SI units (like in meter), so round-off errors can occur when multiplying and dividing to normalize/unnormalize values there. That is really because we try to store values from a 10 digit system into a binary representation with 2 digits. It's how these floats work. Why does the operation 0.5 - 0.2
result in 0.3
, whilst 0.4 - 0.1
results in 0.30000000000000004
in the IEEE 754 number system?... Because most numbers cannot be represented exactly and there are round-off errors introduced. Sometimes they pop up, sometimes they don't.
Are you saying that other numbers do not use floats?
Math.js currently supports three numeric types: numbers
, BigNumbers
, and Fractions
. You also have integer number representations (though not natively in JavaScript). etc.
And yes, the JavaScript numbers are 64 bit floats, can hold almost 16 digits. So rounding to 20 digits doesn't make sense. You can try it for yourself though in your developer console :D
Yes, rounding error do occur, we agree on that. What I'm arguing is that 1cm + 1mm
is the same calculation as 1*0.01 + 1*0.001
, and it should give the same rounding error.
Sometimes they pop up, sometimes they don't.
I'm trying to find out why, for the enlightenment. As I see it, there is two possibilities here:
toString()
method.Ok here we go :)
math.eval('1 cm') // a unit created holding 0.01 m
math.eval('1 mm') // a unit created holding 0.001 m
math.eval('1 cm + 1 mm') // a unit created holding 0.011 m
So far no rounding errors.
When calling .toString()
, the unit is transformed back to either mm
or cm
. The heuristics of the current implementation in mathjs will select the prefix of the first unit of the two added units.
math.eval('1 cm + 1 mm').toString()
// convert 0.011 m to cm -> 0.011 * 100 = '1.0999999999999999 cm'
math.eval('1 mm + 1 cm').toString()
// convert 0.011 m to mm -> 0.011 * 1000 = '11 mm'
So in this case 0.011 * 100
introduces a floating point rounding error, whilst 0.011 * 1000
doesn't.
Ahh! Of course, it's (0.01 + 0.001) * 100
! I'm sorry for the noise.
Last note:
math.eval('(0.01 + 0.001) * 100').toString().length - 1 // 17, above precision
Minus 1 is the .
dot. Thanks again!
You're welcome.
And yes... the number of digits of a number can float a bit. If you drive deeper into the mystical world of floating point numbers you will surely discover why :)
Is there any best practice of how to catch those rounding errors?
My examples:
1 um in km // 0.0000000009999999999999999 km (9.999999999999999e-10 km)
45m^2 in dm^2 // 4499.999999999999 dm^2
1 liter in dm^3 // 0.9999999999999998 dm^3
I tried deal with the first error 1 um in km
by:
answer = math.eval(mathline, scope);
answer_out = math.format( answer, {notation: "fixed", lowerExp: -10, upperExp: 10} );
if(answer.toNumber() < 1e-9)
{
answer_out = math.format( answer, {notation: "fixed", precision: 10, lowerExp: -10, upperExp: 10} );
}
But that is only working for this specific case and will cut the result of 1 nm in km
for instance.
I have no idea how to fix the other rounding errors like 45m^2 in dm^2
or the uncommon 1 liter in dm^3
error above.
Maybe round()
can help somehow?
You can play around with the various options that math.format
offers. Using a limited precision
is what you probably want. The format
function uses rounding internally, I don't think using round
instead would make it easier.
Thanks for your reply. I played around already but as I wrote in the post before, those hacks are no solution. That's why I asked for a "best practice".
I thought using precision:10
would be an option, but (1.) it always prints multiply 000
in the end and (2.) it "destroys" handling with values that have more digits. E.g. 1 nm in km
is "1e-12 km"
- but with the precision hack we get 0.0000000000 km
.
PS:
If I write 1 um in km
in your http://mathnotepad.com/ it gives me the correct 1e-9km
.
If I write math.eval("1 um in km").toString()
in the developer console on mathjs.com it gives "9.999999999999999e-10 km"
?!
If I remember well mathnotepad doesn't do much magic, it does something like:
math.format(math.eval("1 um in km"), {precision: 10}) // "1e-9 km"
I thought using precision:10 would be an option, but (1.) it always prints multiply 000 in the end and (2.) it "destroys" handling with values that have more digits. E.g. 1 nm in km is "1e-12 km" - but with the precision hack we get 0.0000000000 km.
Can you give examples of this?
I used now:
math.format( answer, {precision: 10, lowerExp: -10, upperExp: 10} );
and it seems to work. Weird. The notation: "fixed"
without having precision
seems to be the culprit.
Result: https://www.matheretter.de/notiz?n=a113z (1 um in km works now!)
Thanks.
Good to hear you have a working solution now!
I have been happy to early :-(
See for example here: https://www.matheretter.de/notiz?n=88374
You can enter 12*0.50+3*1.99
which leads to 11.969999999999999
Damn it :(
All good, my bad. Did not aply the fix in all my code. Works now!
When using
eval
with units, first unit in string is set as the "ground" unit for the calculation. For example:The result is a funny difference in the output format. Of course one could force output format:
I guess this is because first calculation is done with decimal (1 mm as 0.1 cm, which is not precise in binary) and second calculation in integers. As I see it, a sensible heuristic could be:
Does this heuristic make sense for mathjs, or should the user of mathjs implement similar heuristics himself?
BTW, see that Math Notepad does what I expected: