pow, log and exp
.NET Test / .NET tests (push) Successful in 1m31s

This commit is contained in:
2026-09-15 00:33:31 +04:00
parent beb9a084d0
commit b54a0a2d42
18 changed files with 2890 additions and 12 deletions
+64
View File
@@ -0,0 +1,64 @@
namespace Just.PreciseMath;
public static partial class DDMath
{
/// <summary>Returns e raised to the specified double-double value.</summary>
/// <remarks>
/// Uses binary range reduction and a [12/12] Padé approximation. Results are
/// approximate, not guaranteed correctly rounded; tests check 2^-100 relative
/// error plus one minimum binary64 subnormal against high-precision references.
/// Precision decreases near underflow. NaN returns canonical NaN; positive
/// infinity returns positive infinity, negative infinity returns positive zero,
/// and either zero returns one. Both input components affect range boundaries.
/// </remarks>
[Pure]
public static DoubleDouble Exp(DoubleDouble value)
{
if (double.IsNaN(value.High))
{
return DoubleDouble.NaN;
}
// These deliberately loose bounds only reject inputs safely outside the
// result range, even with a normalized low of either sign. They also keep
// infinities and huge finite inputs out of the floating-to-int conversion.
if (value.High > 710.0)
{
return new DoubleDouble(double.PositiveInfinity);
}
if (value.High < -746.0)
{
return DoubleDouble.Zero;
}
if (value.High == 0.0)
{
return DoubleDouble.One;
}
// The binary64 estimate need not choose the nearest k on a tie: either
// neighbor leaves |r| < 0.347. k is bounded by [-1076, 1024]. Subtract
// separate products to retain cancellation residuals. A third ln(2)
// component prevents k times the DD constant error from dominating r.
// Generated independently at 180/260 decimal digits by generate_exp.py.
const double ln2Tail = 5.707708438416212e-34;
int exponent = (int)Math.Round(value.High / DoubleDouble.Ln2.High);
DoubleDouble reduced = value - (new DoubleDouble(DoubleDouble.Ln2.High) * exponent);
reduced -= new DoubleDouble(DoubleDouble.Ln2.Low) * exponent;
reduced -= new DoubleDouble(ln2Tail) * exponent;
// P(x)/P(-x), with exact binary64 integer coefficients. For n=12,
// c_k = (24-k)!*12! / (24!*k!*(12-k)!), scaled by 1/c_12.
// This retains the legacy approximation but not its unsafe E^k scaling.
// Horner accumulators remain normal and finite on the reduced interval.
ReadOnlySpan<double> coefficients = [156.0, 12012.0, 600600.0, 21621600.0,
588107520.0, 12350257920.0, 201132771840.0, 2514159648000.0,
23465490048000.0, 154872234316800.0, 647647525324800.0, 1295295050649600.0];
DoubleDouble numerator = DoubleDouble.One;
DoubleDouble denominator = DoubleDouble.One;
foreach (double coefficient in coefficients)
{
numerator = (numerator * reduced) + coefficient;
denominator = (denominator * -reduced) + coefficient;
}
return ScalePowerOfTwo(numerator / denominator, exponent);
}
}
+77
View File
@@ -0,0 +1,77 @@
namespace Just.PreciseMath;
public static partial class DDMath
{
/// <summary>Returns the natural logarithm of a double-double value.</summary>
/// <remarks>
/// Either signed zero returns negative infinity; one returns positive zero.
/// Positive infinity is preserved. Negative nonzero values and NaN return
/// canonical NaN. Both input components contribute to the finite result.
/// Uses binary range reduction and an atanh series, with a separate near-one
/// path that preserves sparse low components. Results are approximate, not
/// guaranteed correctly rounded: tests check 2^-100 relative error plus one
/// minimum binary64 subnormal against independent high-precision references.
/// </remarks>
[Pure]
public static DoubleDouble Log(DoubleDouble value)
{
if (double.IsNaN(value.High) || value.High < 0.0)
{
return DoubleDouble.NaN;
}
if (value.High == 0.0)
{
return new DoubleDouble(double.NegativeInfinity);
}
if (double.IsPositiveInfinity(value.High))
{
return value;
}
if (value == DoubleDouble.One)
{
return DoubleDouble.Zero;
}
return LogPositiveFinite(value);
}
// Shared with Pow. Requires a positive finite normalized value other than one;
// callers handle domain errors and special values before any range reduction.
private static DoubleDouble LogPositiveFinite(DoubleDouble value)
{
if (value.High == 1.0 && Math.Abs(value.Low) <= Math.ScaleB(1.0, -54))
{
// log(1+d) = d - d^2/2 + O(d^3). The omitted relative term is
// below 2^-108. Keep d itself, rather than forming d/(2+d), which
// could round a minimum-subnormal low to zero before a huge power
// amplifies it. Underflow of d^2 is immaterial to the relative bound.
DoubleDouble delta = new(value.Low);
return delta - ((delta * delta) * 0.5);
}
int exponent = Math.ILogB(value.High);
DoubleDouble mantissa = DoubleDouble.FromComponents(Math.ScaleB(value.High, -exponent),
Math.ScaleB(value.Low, -exponent));
if (mantissa.High > DoubleDouble.Sqrt2.High)
{
mantissa *= 0.5;
++exponent;
}
// The mantissa is approximately in [1/sqrt(2), sqrt(2)], so
// |t| = |(m-1)/(m+1)| < 0.172. Centering at one avoids cancellation
// between log(m) and exponent*ln(2) for inputs immediately below one.
// All unscaled sums are bounded: m+1 cannot overflow at large inputs.
DoubleDouble t = (mantissa - 1.0) / (mantissa + 1.0);
DoubleDouble squared = t * t;
DoubleDouble term = t;
DoubleDouble sum = t;
for (int denominator = 3; denominator <= 49; denominator += 2)
{
term *= squared;
sum += term / denominator;
}
// log(m) = 2*atanh(t). The omitted series tail is bounded by
// 2*|t|^51/(51*(1-t^2)); rounding, not truncation, dominates.
return (sum * 2.0) + (DoubleDouble.Ln2 * exponent);
}
}
+164
View File
@@ -0,0 +1,164 @@
namespace Just.PreciseMath;
public static partial class DDMath
{
/// <summary>Raises a double-double value to an integer power.</summary>
/// <remarks>
/// Uses exponentiation by squaring with a separately tracked binary exponent,
/// so negative powers do not overflow or underflow before reciprocation. All
/// integer exponents, including <see cref="int.MinValue"/>, are supported.
/// Any value to power zero is one, including NaN and either zero. Otherwise
/// NaN propagates; zero and infinity follow binary64 integer-power sign rules.
/// Results are approximate, not guaranteed correctly rounded. Error can grow
/// with the exponent magnitude; the tested absolute error bound for finite
/// results is |exact result| * (|exponent| + 1) * 2^-100 + 2^-1074.
/// This is not a uniform 2^-100 relative accuracy claim or a proof for every
/// input. Precision decreases near underflow, and results extremely close to
/// overflow or underflow rounding boundaries can be affected by approximation.
/// </remarks>
[Pure]
public static DoubleDouble Pow(DoubleDouble value, int exponent)
{
if (exponent == 0)
{
return DoubleDouble.One;
}
if (double.IsNaN(value.High))
{
return DoubleDouble.NaN;
}
if (exponent == 1)
{
// Even a low thousands of bits below high must survive the identity.
return value;
}
bool negative = double.IsNegative(value.High) && (exponent & 1) != 0;
if (value.High == 0.0 || double.IsInfinity(value.High))
{
bool infinite = (value.High == 0.0) == (exponent < 0);
double magnitude = infinite ? double.PositiveInfinity : 0.0;
return new DoubleDouble(negative ? -magnitude : magnitude);
}
// Widen BEFORE negation: abs(int.MinValue) is not an int.
long remaining = exponent < 0 ? -(long)exponent : exponent;
long factorExponent = Math.ILogB(value.High);
DoubleDouble factor = DoubleDouble.FromComponents(
Math.ScaleB(Math.Abs(value.High), -(int)factorExponent),
Math.ScaleB(value.High < 0.0 ? -value.Low : value.Low, -(int)factorExponent));
DoubleDouble result = DoubleDouble.One;
long resultExponent = 0;
while (remaining != 0)
{
if ((remaining & 1) != 0)
{
result *= factor;
resultExponent += factorExponent;
result = NormalizePowerMantissa(result, ref resultExponent);
}
remaining >>= 1;
if (remaining != 0)
{
factor *= factor;
factorExponent *= 2;
factor = NormalizePowerMantissa(factor, ref factorExponent);
}
}
if (exponent < 0)
{
// Reciprocate the bounded significand, NOT the range-limited result.
result = DoubleDouble.One / result;
resultExponent = -resultExponent;
}
return ScalePowerOfTwo(negative ? -result : result, resultExponent);
}
/// <summary>Raises a double-double value to a binary64 power.</summary>
/// <remarks>
/// Promotes the exponent exactly to a zero-low expansion and uses the same
/// domain, special-value, and accuracy rules as <see cref="Pow(DoubleDouble, DoubleDouble)"/>.
/// The base retains both components.
/// </remarks>
[Pure]
public static DoubleDouble Pow(DoubleDouble value, double exponent)
{
return Pow(value, new DoubleDouble(exponent));
}
/// <summary>Raises a double-double value to a double-double power.</summary>
/// <remarks>
/// Both exponent components determine integrality and parity. For finite
/// exponents, negative finite bases require an integer exponent; otherwise
/// the result is canonical NaN.
/// Any base to either zero power is one, and positive one to any power is one,
/// including NaN. Other NaNs propagate. Infinite exponents compare the complete
/// base magnitude with one; either unit magnitude to infinite power is one.
/// Zero and infinite bases have a negative result only for odd integer powers;
/// negative powers exchange zero and infinity. Integral exponents within the
/// int range reuse <see cref="Pow(DoubleDouble, int)"/> and its accuracy contract.
/// Other finite cases use a scaled logarithm and <see cref="Exp"/>, tested
/// against 2^-90 relative error plus one minimum binary64 subnormal with
/// independent high-precision references. This is a tested bound, not a
/// universal proof or correct-rounding guarantee. Precision decreases near
/// underflow, and approximation can affect exceptionally close range boundaries.
/// </remarks>
[Pure]
public static DoubleDouble Pow(DoubleDouble value, DoubleDouble exponent)
{
if (DoubleDouble.IsZero(exponent) || value == DoubleDouble.One)
{
return DoubleDouble.One;
}
if (DoubleDouble.IsNaN(value) || DoubleDouble.IsNaN(exponent))
{
return DoubleDouble.NaN;
}
DoubleDouble magnitude = DoubleDouble.Abs(value);
if (DoubleDouble.IsInfinity(exponent))
{
if (magnitude == DoubleDouble.One)
{
return DoubleDouble.One;
}
bool grows = (magnitude > DoubleDouble.One) == (exponent.High > 0.0);
return grows ? new DoubleDouble(double.PositiveInfinity) : DoubleDouble.Zero;
}
bool integer = DoubleDouble.IsInteger(exponent);
if (integer && exponent.High >= int.MinValue && exponent.High <= int.MaxValue)
{
// A normalized integer in this range has a zero low component.
// Reuse the integer algorithm, including exact identities and ties.
return Pow(value, (int)exponent.High);
}
bool negative = double.IsNegative(value.High) && DoubleDouble.IsOddInteger(exponent);
if (value.High == 0.0 || DoubleDouble.IsInfinity(value))
{
bool infinite = (value.High == 0.0) == (exponent.High < 0.0);
double result = infinite ? double.PositiveInfinity : 0.0;
return new DoubleDouble(negative ? -result : result);
}
if (value.High < 0.0 && !integer)
{
return DoubleDouble.NaN;
}
if (magnitude == DoubleDouble.One)
{
return negative ? DoubleDouble.NegativeOne : DoubleDouble.One;
}
DoubleDouble resultMagnitude = Exp(exponent * LogPositiveFinite(magnitude));
return negative ? -resultMagnitude : resultMagnitude;
}
private static DoubleDouble NormalizePowerMantissa(DoubleDouble value, ref long exponent)
{
// High is positive and in [1, 4], including a rounded binade endpoint.
// Both multiply operands stay modest; a long safely tracks even
// int.MinValue times any finite input's binary64 exponent.
int adjustment = Math.ILogB(value.High);
exponent += adjustment;
return DoubleDouble.FromComponents(Math.ScaleB(value.High, -adjustment),
Math.ScaleB(value.Low, -adjustment));
}
}
+26 -1
View File
@@ -2,7 +2,7 @@ namespace Just.PreciseMath;
/// <summary>Provides mathematical functions for normalized double-double values.</summary>
/// <remarks>Results use fixed-size double-double precision, not arbitrary precision.</remarks>
public static class DDMath
public static partial class DDMath
{
/// <summary>Returns the absolute value without discarding the low component.</summary>
/// <remarks>Both signs of zero become positive zero; NaN is canonicalized and either infinity becomes positive infinity.</remarks>
@@ -54,4 +54,29 @@ public static class DDMath
int rootExponent = exponent / 2;
return DoubleDouble.FromComponents(Math.ScaleB(root.High, rootExponent), Math.ScaleB(root.Low, rootExponent));
}
// For finite, nonzero normalized significands and exponents bounded by the
// integer-power domain (|exponent| < 2^42). Keep the exponent separate until
// the final result so an intermediate cannot overflow before reciprocation.
private static DoubleDouble ScalePowerOfTwo(DoubleDouble value, long exponent)
{
long resultExponent = Math.ILogB(value.High) + exponent;
if (resultExponent > 1024)
{
return new DoubleDouble(Math.CopySign(double.PositiveInfinity, value.High));
}
if (resultExponent < -1075)
{
return new DoubleDouble(Math.CopySign(0.0, value.High));
}
if (resultExponent >= -969 && resultExponent <= 1022)
{
// Normal high and room for a dense low; a sparse low may underflow
// by at most half a minimum subnormal. Normalize its signed zero.
return DoubleDouble.FromComponents(Math.ScaleB(value.High, (int)exponent),
Math.ScaleB(value.Low, (int)exponent));
}
return PreciseMathHelper.ScalePowerOfTwoBoundary(value, exponent);
}
}
+30 -7
View File
@@ -182,13 +182,6 @@ internal static class PreciseMathHelper
return ArithmeticFromRatio(ArithmeticUnits(left) + ArithmeticUnits(right), BigInteger.One << 1074);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static DoubleDouble AddScalarBoundary(double high, double low, double value)
{
// As in AddScalar, high/low are normalized but a negated zero low is allowed.
return ArithmeticFromRatio(ArithmeticUnits(high) + ArithmeticUnits(low) + ArithmeticUnits(value), BigInteger.One << 1074);
}
[MethodImpl(MethodImplOptions.NoInlining)]
internal static DoubleDouble MultiplyBoundary(DoubleDouble left, DoubleDouble right)
{
@@ -219,6 +212,29 @@ internal static class PreciseMathHelper
return ArithmeticFromRatio(ArithmeticUnits(left), ArithmeticUnits(right));
}
// The caller supplies a finite, nonzero normalized value and has bounded
// ILogB(value.High) + exponent to [-1075, 1024], keeping both shifts small.
// Isolate all allocating setup from ordinary scaling, including conversion
// to integer units and construction of the denominator.
[MethodImpl(MethodImplOptions.NoInlining)]
internal static DoubleDouble ScalePowerOfTwoBoundary(DoubleDouble value, long exponent)
{
// Scale the complete expansion exactly at either exponent boundary.
// Scaling the high first could overflow despite a negative low, or round
// a subnormal tie the wrong way before the low is taken into account.
BigInteger numerator = ArithmeticUnits(value);
BigInteger denominator = BigInteger.One << 1074;
if (exponent >= 0)
{
numerator <<= (int)exponent;
}
else
{
denominator <<= (int)-exponent;
}
return ArithmeticFromRatio(numerator, denominator);
}
// The boundary path uses bounded binary integers (at most about 4200 bits), not
// arbitrary-precision storage. It avoids overflow and double rounding in EFTs
// at the binary64 exponent limits. The common path remains allocation-free.
@@ -266,6 +282,13 @@ internal static class PreciseMathHelper
return DoubleDouble.FromComponents(high, low);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static DoubleDouble AddScalarBoundary(double high, double low, double value)
{
// As in AddScalar, high/low are normalized but a negated zero low is allowed.
return ArithmeticFromRatio(ArithmeticUnits(high) + ArithmeticUnits(low) + ArithmeticUnits(value), BigInteger.One << 1074);
}
// Round an exact rational to binary64, ties-to-even, including subnormal and
// overflow boundaries. Denominator is positive; sign is retained on underflow.
private static double ArithmeticRound(BigInteger numerator, BigInteger denominator)