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
PreciseNumber.Pow(power) uses exponentiation by squaring for integer exponents — exact, fast, correct — and then for everything else:
// Use logarithm and exponential to support decimal powersdoublelogValue=Math.Log(To<double>());returnMath.Exp(logValue*power.To<double>()).ToPreciseNumber();
So x.Pow(0.5) on a 50-digit x returns about 15 correct digits in a type that claims 50, with nothing in the signature, the return value or the documentation to say so. Exp(power) has the same shape. A drag model using an exponential atmosphere, or anything taking a fractional power, silently drops to double precision.
Design
Log
The base-10 representation does half the work for free. v = s · 10^e with s ∈ [1, 10), so:
ln v = ln s + e · ln 10
Reduce s further into [1/√10, √10) so the argument is centred, then use the atanh series, which converges quickly there:
ln m = 2 · atanh(z), z = (m - 1) / (m + 1)
atanh z = z + z³/3 + z⁵/5 + …
ln 10 must be a stored constant at the same precision as π (#79), not computed per call — otherwise every logarithm pays for it.
Exp
Reduce by ln 10: exp(v) = 10^k · exp(r) with k = round(v / ln 10), so the 10^k is a free exponent shift and |r| ≤ ln10/2. Then Taylor exp r = Σ rⁿ/n!, truncating each term with ReduceSignificance. If |r| is still large enough to make the series long, halve it m times and square the result back m times.
Pow
Keep the integer fast path exactly as it is — it is exact and must not regress. For non-integer exponents: x^y = Exp(y · Log(x)), with the working precision raised by the number of integer digits in y · ln x, because Exp's reduction consumes them. Negative base with a non-integer exponent throws ArgumentOutOfRangeException; there are no complex results here.
The rest of the interface surface
Exp2, Exp10, Log2, Log10 and the …M1 / …P1 variants.
Exp10 and Log10 must not route through the natural log. In this representation they are nearly free: Log10(s · 10^e) = e + Log10(s), and Exp10(n) for integer n is an exponent shift and nothing else. Going via ln would be both slower and less accurate.
The M1 / P1 variants must be implemented directly, not as Exp(x) - 1. They exist precisely to keep precision near zero, and computing them the naive way throws away everything they are for. ExpM1 is the series with its first term omitted; LogP1 is atanh on z = v / (v + 2).
New stored constants
Ln10 and Ln2, correctly rounded, at the precision agreed in #79. Neither derived from the other.
Tests
ln 2, ln 10, e, 2^0.5, 10^(1/3) against published 50-digit values
Exp(Log(x)) == x to 49 of 50 digits across a sweep
Pow(x, 2) == x.Squared()exactly — the integer path must not regress
ExpM1(1e-30) returns ~1e-30, not 0; LogP1(1e-30) likewise
Exp10(50) is exact and allocates one significand, not a series
Part of #78.
The bug half of this
PreciseNumber.Pow(power)uses exponentiation by squaring for integer exponents — exact, fast, correct — and then for everything else:So
x.Pow(0.5)on a 50-digitxreturns about 15 correct digits in a type that claims 50, with nothing in the signature, the return value or the documentation to say so.Exp(power)has the same shape. A drag model using an exponential atmosphere, or anything taking a fractional power, silently drops todoubleprecision.Design
LogThe base-10 representation does half the work for free.
v = s · 10^ewiths ∈ [1, 10), so:Reduce
sfurther into[1/√10, √10)so the argument is centred, then use the atanh series, which converges quickly there:ln 10must be a stored constant at the same precision as π (#79), not computed per call — otherwise every logarithm pays for it.ExpReduce by
ln 10:exp(v) = 10^k · exp(r)withk = round(v / ln 10), so the10^kis a free exponent shift and|r| ≤ ln10/2. Then Taylorexp r = Σ rⁿ/n!, truncating each term withReduceSignificance. If|r|is still large enough to make the series long, halve itmtimes and square the result backmtimes.PowKeep the integer fast path exactly as it is — it is exact and must not regress. For non-integer exponents:
x^y = Exp(y · Log(x)), with the working precision raised by the number of integer digits iny · ln x, becauseExp's reduction consumes them. Negative base with a non-integer exponent throwsArgumentOutOfRangeException; there are no complex results here.The rest of the interface surface
Exp2,Exp10,Log2,Log10and the…M1/…P1variants.Exp10andLog10must not route through the natural log. In this representation they are nearly free:Log10(s · 10^e) = e + Log10(s), andExp10(n)for integernis an exponent shift and nothing else. Going vialnwould be both slower and less accurate.The
M1/P1variants must be implemented directly, not asExp(x) - 1. They exist precisely to keep precision near zero, and computing them the naive way throws away everything they are for.ExpM1is the series with its first term omitted;LogP1is atanh onz = v / (v + 2).New stored constants
Ln10andLn2, correctly rounded, at the precision agreed in #79. Neither derived from the other.Tests
ln 2,ln 10,e,2^0.5,10^(1/3)against published 50-digit valuesExp(Log(x)) == xto 49 of 50 digits across a sweepPow(x, 2) == x.Squared()exactly — the integer path must not regressExpM1(1e-30)returns ~1e-30, not 0;LogP1(1e-30)likewiseExp10(50)is exact and allocates one significand, not a seriesdoublefallback, so the cost of correctness is on the record rather than discovered later. Related: Investigate benchmark regressions in Max, CompareTo, ReduceSignificance, Pow, and Divide since 2.0 #76, which already flagsPowamong the regressed benchmarks.