What's wrong
The negative-degree path in RootN (PreciseNumber/PreciseNumber.Roots.cs:158-161) is:
DivideApproximation(One, RootN(x, -n, significantDigits + RootGuardDigits), significantDigits + RootGuardDigits, significantDigits)
RootGuardDigits is 2 (:27), and the value is rounded three times:
- The inner root is rounded to d+2 digits.
- The reciprocal is rounded to d+2 digits.
ReduceSignificance(d) rounds again.
Two guard digits are not enough to absorb two roundings before the final one. When the true value lies close to a rounding boundary at d digits, the result rounds the wrong way.
Failure scenario
The true value of 3^(-1/3) is 0.69336127435063…, which rounds to 0.6933612744 at 10 digits.
RootN(3, 3, 12) gives 1.44224957031, rounded up.
- Its reciprocal at 12 digits is 0.693361274349.
- Rounding that to 10 digits gives 0.6933612743, which is wrong.
A differential sweep against HEAD compared RootN(k, n, d) with RootN(k, n, d + 40).ReduceSignificance(d), for k = 2..300, n ∈ {-2, -3, -5} and d ∈ {5, 10, 20, 50}. It found 27 of 3,588 cases misrounded, including some at the default 50 digits:
| Call |
Returned |
Correct |
RootN(3, -3, 10) |
0.6933612743 |
0.6933612744 |
RootN(20, -2, 10) |
0.2236067978 |
0.2236067977 (true 0.22360679774997…) |
RootN(47, -2, 5) |
0.14587 |
0.14586 |
RootN(41, -3, 5) |
0.29001 |
0.29 |
RootN(0.5, -3, 8) |
1.2599211 |
1.2599210 |
RootN(1.5, -3, 6) |
0.873581 |
0.873580 |
RootN(7, -5, 50) |
…70498 |
…70499 |
Positive degrees are unaffected. That path floors an integer root and then rounds once.
This is a sibling of #121. Its fix (310faf9) rewrote this line to handle quotients that terminate, but it kept the 2 guard digits, so ordinary misrounding remains. #127 (double rounding in FractionalPow) is scoped to Pow and doesn't cover this path.
Suggested fix
- Preferred (exact): take the root of the reciprocal directly in integers, as the positive path does.
- For significand
s, compute q = floor(10^K / s), with K chosen so that q has about n·(d+2) digits and the resulting exponent is divisible by n.
- Take
IntegerRootN(q, n). Since floor((floor y)^(1/n)) = floor(y^(1/n)), the truncated digits are exact, and a single ReduceSignificance(d) rounds correctly.
- Treat the result as exact only when
10^K % s == 0 and root^n == q.
- Cheap mitigation: widen the guard on this path to about 10 digits, like
ExponentialGuardDigits and TrigonometricGuardDigits. This makes misrounding much rarer but does not eliminate it.
Acceptance criteria
- Pinning tests:
RootN(3, -3, 10) == 0.6933612744 and RootN(20, -2, 10) == 0.2236067977.
- A differential test over a range of k, n < 0 and d finds no mismatch against a high-precision reference.
What's wrong
The negative-degree path in
RootN(PreciseNumber/PreciseNumber.Roots.cs:158-161) is:RootGuardDigitsis 2 (:27), and the value is rounded three times:ReduceSignificance(d)rounds again.Two guard digits are not enough to absorb two roundings before the final one. When the true value lies close to a rounding boundary at d digits, the result rounds the wrong way.
Failure scenario
The true value of 3^(-1/3) is 0.69336127435063…, which rounds to 0.6933612744 at 10 digits.
RootN(3, 3, 12)gives 1.44224957031, rounded up.A differential sweep against HEAD compared
RootN(k, n, d)withRootN(k, n, d + 40).ReduceSignificance(d), for k = 2..300, n ∈ {-2, -3, -5} and d ∈ {5, 10, 20, 50}. It found 27 of 3,588 cases misrounded, including some at the default 50 digits:RootN(3, -3, 10)RootN(20, -2, 10)RootN(47, -2, 5)RootN(41, -3, 5)RootN(0.5, -3, 8)RootN(1.5, -3, 6)RootN(7, -5, 50)Positive degrees are unaffected. That path floors an integer root and then rounds once.
This is a sibling of #121. Its fix (310faf9) rewrote this line to handle quotients that terminate, but it kept the 2 guard digits, so ordinary misrounding remains. #127 (double rounding in
FractionalPow) is scoped toPowand doesn't cover this path.Suggested fix
s, computeq = floor(10^K / s), with K chosen so thatqhas about n·(d+2) digits and the resulting exponent is divisible by n.IntegerRootN(q, n). Since floor((floor y)^(1/n)) = floor(y^(1/n)), the truncated digits are exact, and a singleReduceSignificance(d)rounds correctly.10^K % s == 0androot^n == q.ExponentialGuardDigitsandTrigonometricGuardDigits. This makes misrounding much rarer but does not eliminate it.Acceptance criteria
RootN(3, -3, 10) == 0.6933612744andRootN(20, -2, 10) == 0.2236067977.