Repository navigation
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
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
xthe 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).
- recompute at wider precision (Ziv-style; for a series in
- 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 + 1digits ending in 5, exponents −5…−60) for Sin, Atan, Tanh, Asinh, LogP1, Log(1 + x) and ExpM1 against a high-precision reference.
Activity
matt-edmondson commentedon Oct 6, 2026
Triage
d+1digits ending in 5) with small magnitude. The fix needs care: Ziv-style recompute, or directed rounding by the sign of the first correction term.PreciseNumber.Exponentials.cs,.Trigonometry.cs,.Hyperbolics.cs), rounding.-1raised to a huge integer) is unrelated. Coordinate with 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 so any tiny-argument short-circuit uses the same directed rounding.Generated by Claude Code