@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user