You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
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
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.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.
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).
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.
Priority: Medium. It violates the correctly-rounded guarantee, including at the default 50 digits, but only for midpoint arguments (d+1 digits ending in 5) with small magnitude. The fix needs care: Ziv-style recompute, or directed rounding by the sign of the first correction term.
Area / suggested assignment: Transcendental functions (PreciseNumber.Exponentials.cs, .Trigonometry.cs, .Hyperbolics.cs), rounding.
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 atworking = significantDigits + 10(ExponentialGuardDigits/TrigonometricGuardDigits) and then rounds withReduceSignificance(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 + 1digits 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…5000000000and the final half-away rounding goes the wrong way. No fixed guard width fixes this, since the gap shrinks withx.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)
LogP1(1.5e-15, 1)2e-151e-15LogP1(1.25e-30, 2)1.3e-301.2e-30Log(1.0000000000000000000000000000015, 1)2e-301e-30Sin(2.5e-20, 1)3e-202e-20Atan/Tanh/Asinh(2.5e-20, 1)3e-202e-20ExpM1(-2.5e-20, 1)-3e-20-2e-20Sin(1.00000000000000000000000000000000000000000000000005e-30, 50)1.0000000000000000000000000000000000000000000000001e-301e-30Asinh(5.653464549377765e-25, 15)(found by random sweep vs Pythondecimal)5.65346454937777e-255.65346454937776e-25Controls:
LogP1(1.5e-8, 1)correctly returns1e-8(gap still within guard digits);SinhandTanare 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 "
xrounded" would reproduce exactly this error for midpointx.Suggested fix / acceptance criteria
ReduceSignificance, detect when the discarded guard digits are within one working ulp of a half (…4999…/…5000…) and don't round blindly — either:xthe needed extra width is bounded by ≈2·|log10 x|digits), ord + 1digits ending in 5, exponents −5…−60) for Sin, Atan, Tanh, Asinh, LogP1, Log(1 + x) and ExpM1 against a high-precision reference.