Skip to content

Sin, Atan, Tanh, Asinh, LogP1, Log and ExpM1 round the wrong way on midpoint arguments: LogP1(1.5e-15, 1) returns 2e-15 (correct: 1e-15) #154

Description

@matt-edmondson

What's wrong

Near zero, several functions are the identity minus a tiny correction (sin x = x − x³/6, atan x = x − x³/3, tanh x = x − x³/3, asinh x = x − x³/6, ln(1+x) = x − x²/2, expm1(−x) = −x + x²/2). Each computes at working = significantDigits + 10 (ExponentialGuardDigits / TrigonometricGuardDigits) and then rounds with ReduceSignificance(significantDigits) (half away from zero):

  • PreciseNumber/PreciseNumber.Exponentials.cs:166 (Log), :325 (LogP1), :489-493 (ExpM1)
  • PreciseNumber/PreciseNumber.Trigonometry.cs:764 (Sin/Cos), :386 (Atan)
  • PreciseNumber/PreciseNumber.Hyperbolics.cs:212 (Tanh), :259 (Asinh, via LogP1)

When the argument has exactly significantDigits + 1 digits ending in 5, it sits on a rounding midpoint, and the true result is just below it by a relative amount ≈ x². For |x| < 10^-(guard/2) that gap is below what the guard digits resolve, so the working value is exactly …5000000000 and the final half-away rounding goes the wrong way. No fixed guard width fixes this, since the gap shrinks with x.

This is distinct from #127/#142 (too few guard digits); random sweeps rarely land on a midpoint, which is why it slipped through.

Reproduction (main 09efd30, net10.0)

Call Returned True value Correct
LogP1(1.5e-15, 1) 2e-15 1.4999999999999988…e-15 1e-15
LogP1(1.25e-30, 2) 1.3e-30 1.2499…e-30 1.2e-30
Log(1.0000000000000000000000000000015, 1) 2e-30 1.4999…e-30 1e-30
Sin(2.5e-20, 1) 3e-20 2.4999…e-20 2e-20
Atan / Tanh / Asinh(2.5e-20, 1) 3e-20 2.4999…e-20 2e-20
ExpM1(-2.5e-20, 1) -3e-20 −2.4999…e-20 -2e-20
Sin(1.00000000000000000000000000000000000000000000000005e-30, 50) 1.0000000000000000000000000000000000000000000000001e-30 x − 1.7e-91 1e-30
Asinh(5.653464549377765e-25, 15) (found by random sweep vs Python decimal) 5.65346454937777e-25 5.65346454937776e-25

Controls: LogP1(1.5e-8, 1) correctly returns 1e-8 (gap still within guard digits); Sinh and Tan are correct here because their correction is positive.

Why it matters

The type promises correctly rounded results at the requested significant digits, and #121/#123/#127/#142 were treated as bugs on that basis. Midpoint arguments are ordinary input and the error also appears at the default 50 digits. It also affects #149: short-circuiting tiny arguments to "x rounded" would reproduce exactly this error for midpoint x.

Suggested fix / acceptance criteria

  • Before the final ReduceSignificance, detect when the discarded guard digits are within one working ulp of a half (…4999… / …5000…) and don't round blindly — either:
    • recompute at wider precision (Ziv-style; for a series in x the needed extra width is bounded by ≈ 2·|log10 x| digits), or
    • for the near-identity family, when the working value is exactly a midpoint, round in the direction of the known sign of the first correction term (toward zero for sin, atan, tanh, asinh, log1p with x > 0, expm1 with x < 0; away from zero for sinh, tan, asin, atanh, expm1 with x > 0).
  • Any Sin(1e-1000000, 10) takes 9 s and Exp(1e-2000000000, 10) runs out of memory, though the answers are x and 1: tiny series arguments (and Atan of huge ones) cost time proportional to the exponent #149 tiny-argument short-circuit applies the same directed rounding.
  • Pinning tests for the rows above, plus a differential test over midpoint inputs (d + 1 digits ending in 5, exponents −5…−60) for Sin, Atan, Tanh, Asinh, LogP1, Log(1 + x) and ExpM1 against a high-precision reference.

Activity

  1. matt-edmondson commented on Oct 6, 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

Assignees

No one assigned

    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