…tions [minor]
Exp and non-integer Pow computed Math.Exp(Math.Log(x.To<double>()) * y),
so a fractional power of a fifty-digit value came back with about fifteen
correct digits wearing a fifty-digit type, with nothing in the signature,
the return value or the documentation to say so.
Both are now computed on the significand, following the precision rule
Divide and the roots already follow: the significant digits of the
argument, never fewer than MinimumDivisionPrecision, with an overload to
choose that count.
The base-10 representation does most of the work. ln(m . 10^k) splits into
ln m + k . ln 10 against the stored Ln10, with the mantissa centred on
[1/root10, root10) and fed to the atanh series. exp(v) factors out
10^round(v / ln 10) as an exponent shift, then halves what is left until
it is under 1/64 and squares the sum back afterwards.
Two constraints in the design are load-bearing rather than stylistic, and
each has a test that fails on the naive implementation:
- Exp10 and Log10 do not route through the natural log. Exp10 of an
integer is an exponent and no series at all; Log10 of a power of ten
returns that exponent exactly.
- The M1 and P1 variants are computed directly, not as Exp(x) - 1 and
Log(1 + x), which would cancel away the precision near zero they exist
to keep. ExpM1(1e-30) is 1e-30, not 0.
The integer Pow path is untouched and stays exact; the tests pin that with
equality against Squared() and Cubed() rather than with a tolerance. A
fractional power of a negative value now throws ArgumentOutOfRangeException
instead of returning a NaN round trip.
Four existing tests pinned the old double-precision answers, one of them
documenting the 17th digit as coming from binary rounding. They now carry
the published digits.
Fixes #81
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JVVMyN8uWMrqzKf3iCd84T
Fixes #81.
The bug half
PowandExpcomputedMath.Exp(Math.Log(x.To<double>()) * y), so a fractional power of a fifty-digit value came back with about fifteen correct digits wearing a fifty-digit type — with nothing in the signature, the return value or the documentation to say so.Both are now computed on the significand, following the precision rule
Divideand the roots already follow: the significant digits of the argument, never fewer thanMinimumDivisionPrecision, with an overload to choose that count.Design
Implemented in a new
PreciseNumber/PreciseNumber.Exponentials.cs, alongsidePreciseNumber.Roots.csand following its precedent for guard digits, termination and thesignificantDigitsoverload shape.The base-10 representation does most of the work:
Log—ln(m · 10^k) = ln m + k · ln 10, withln 10read from the storedLn10rather than computed per call. The mantissa is centred on[1/√10, √10)— the test for it is exact, sincem > √10exactly whenm² > 10and squaring is exact — then fed to2 · atanh((m-1)/(m+1)), whose argument is never larger than about0.52.Exp—exp(v) = 10^k · exp(r)withk = round(v / ln 10), so the whole power of ten is anExponentshift and the series only sees|r| ≤ ln(10)/2. That is halved until it is under1/64and the sum squared back afterwards, which trades most of the terms for a handful of multiplications.Pow— the integer fast path is untouched. A fractional exponent isexp(y · ln x), with the logarithm carried wider by the integer digits ofy · ln x, becauseExp's range reduction cancels exactly those digits.Ln2andLn10were already stored atConstantPrecision, so no new constants were needed.The two "must not" constraints
Both are load-bearing rather than stylistic, and each has a test that fails on the naive implementation:
Exp10/Log10do not route through the natural log.Exp10of an integer is an exponent shift and no series at all;Log10of a power of ten returns that exponent exactly.TestExp10OfAnIntegerIsAnExponentAndNothingElseasserts the significand is1, not merely that the value is right.…M1/…P1variants are computed directly, not asExp(x) - 1andLog(1 + x), which would cancel away precisely the precision near zero they exist to keep.ExpM1(1e-30)is1e-30, not0.Behaviour changes
ArgumentOutOfRangeExceptionrather than returning a NaN round trip. There are no complex results here, and this matchesSqrtof a negative value.intthrowsOverflowException. There is no infinity to saturate to.Exp(One)still returns the full storedE; thesignificantDigitsoverload returnsETo(n).Tests
PreciseNumber.Test/PreciseNumberExponentialTests.cs, in the style ofPreciseNumberRootTests.cs— published digits rather than values this library produced, so a change that makes the series agree with themselves but not with mathematics still fails. It covers every item the issue lists:ln 2,ln 10,e²,2^0.5,10^(1/3)against published fifty-digit values;Exp(Log(x))round-tripping across a sweep;Pow(x, 2) == x.Squared()exactly;ExpM1(1e-30)andLogP1(1e-30)not collapsing to zero; andExp10(50)allocating one significand rather than running a series.Proven to fail without the fix. With the
doubleroute temporarily restored and everything else unchanged, 13 tests fail — 9 of the new ones plus the 4 existing ones updated below. With the implementation in place, all 330 pass, and the solution builds clean across net7.0, net8.0, net9.0 and net10.0.Four existing tests pinned the old
double-precision answers — one of them documenting its 17th digit as coming from binary rounding — and now carry the published digits instead:TestExpWithNegativePower0.367879441171442330.36787944117144232159…TestExpWithLargePositivePower148.4131591025766148.41315910257660342…TestExpWithLargeNegativePower0.0067379469990854670.00673794699908546709…PowShouldReturnCorrectValue5.6568542494923815.65685424949238019520…Benchmarks
ExponentialBenchmarks, with thedoublecalls these replaced as an explicit baseline, so the cost of correctness is on the record rather than discovered later (issue #76 flagsPowamong the regressed benchmarks). Short-run, 8/30/200 digits:LogExpPowFractionalPowIntegerExp10OfAnIntegerLog10OfAPowerOfTenDoubleLogBaselineThe two free paths are flat across the digit axis and allocate nothing, which is the property worth regressing against: a series being run where none is needed would show up there first.
Docs
README.md's Limitations section no longer claimsExpand non-integerPowroute throughdouble, andCLAUDE.mdrecords the design and the two "must not" constraints so they are not lost to a later simplification.🤖 Generated with Claude Code
https://claude.ai/code/session_01JVVMyN8uWMrqzKf3iCd84T
Generated by Claude Code