Automatic conversion of units to preferred unit causes loss of precision/errors
- Dominant language
- JavaScript
- Stars
- 15.1k
- Forks
- 1.3k
- PR merge metrics
- No merged PRs in 30d
Description
**Describe the bug**
Someone can introduce a Unit quantity with an exact or highly precise value, and further computation with it, although apparently exact, will produce highly inaccurate results or throw errors.
**To Reproduce**
```
let b = math.evaluate('72 deg')
while (true) {
b = math.add(b, b)
// Mod out by 360 degrees so b does not blow up
b = math.unit((b.toNumber('deg') % 360), 'deg') // mod doesn't work with Unit denominator ;-/
if (b.toNumber('deg') % 72 !== 0) {
throw new Error('Oops, I guess 72 deg is not one-fifth of a circle')
}
}
```
The above should run forever, since every multiple of 72 degrees is 0 deg, 72 deg, 144 deg, 216 deg, or 288 deg, But it throws after a few dozen iterations.
Or, much more simply and really the same problem:
```
let b = math.evaluate('fraction(3) cycle')
```
This throws `TypeError: Cannot implicitly convert a number to a Fraction when there will be a loss of precision (value: 6.283185307179586). Use function fraction(x) to convert to Fraction.`
Huh? I didn't ask for any conversions of numbers to fractions. I just wanted the exact angle of three cycles.
** Discussion **
There is nothing fundamentally wrong with doing exact or high-precision computations in degrees (or equivalently cycles). The built in number type is capable of taking 72, doubling it, and modding out by 360, exactly, forever. The fraction class can exactly represent the number 3. The operations in the example should not become impossible/forbidden simply by trying to perform them in terms of degrees or cycles.
The difficulty is that Unit eagerly converts its values into the preferred unit for the dimension(s) the unit happens to lie in. This behavior leads to (a) a certain redundancy in the representation of Unit entities, and (b) a loss of precision in expressing units. For an example of (a), consider the unit value `3 cycle`. Its representation is, paraphrased, `18.85 in cycles = 6.2832 radians`. So the only way to get the 3 back is to divide the 18.85 by the 6.2832 to get (approximately) 3. In other words, the redundancy is in the fact that the conversion factor from cycles to radians is recorded both in the value field (by having multiplied 3 by tau) and in the "cycle" component of the units field, which knows that each cycle is tau radians. For an example of (b) that I am not too sympathetic with, but it illustrates the problems that arise with other units including cycles/radians, consider `1.2345678901234 in`. An inch is exactly .0254 m, and `number` has 15 decimal digits of precision, so Unit should be able to record this value with that much precision. But by premultiplying the value of the measurement in inches by the conversion factor to meters, Unit is using up some of `number`s supply of precision to re-record the conversion factor .0254, losing roughly two of the digits of precision in the supplied value.
For these reasons, I would strongly recommend _lazily_ rather than _eagerly_ internally converting unit values to common units. It only needs to be done upon arithmetic operations involving more that one unit value. In other words, the square root of 117649 mi^2 ought to be 343 mi, not `(sqrt(117649*1.609344*1.609344)/1.609344)` = 343.00000006 mi, since the JavaScript number class is perfectly well able to compute the exact square root of 117649 = 343^2.
I also want to emphasize that this issue can become a practical one; it is not just an esoteric/theoretical one. We are in part implementing a turtle graphics system, and we would like people using it to be able to work with angles or radians at their preference. However, if they use the turtle's current bearing in the formula for computing its new bearing (a perfectly natural thing to do), these errors compound, leading to completely different qualitative behavior degrading into meaningless chaos even for angle computations that if done with pure JavaScript `number` type would always maintain exact results. So I think this issue does need attention.
Contributor guide
Assessment
This issue has not been assessed yet.