Skip to content

RootN with a negative degree misrounds the last digit: RootN(3, -3, 10) returns 0.6933612743, correct is 0.6933612744 #142

Description

@matt-edmondson

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:

  1. The inner root is rounded to d+2 digits.
  2. The reciprocal is rounded to d+2 digits.
  3. 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.

Activity

  1. matt-edmondson commented on Sep 30, 2026

    @matt-edmondson
    ContributorAuthor

    Triage


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions