add precomputed mathematical constants and specialize the second division residual
.NET Test / .NET tests (push) Successful in 2m23s
.NET Test / .NET tests (push) Successful in 2m23s
This commit is contained in:
@@ -101,9 +101,13 @@ public readonly partial struct DoubleDouble :
|
||||
double quotient = left._high / right._high;
|
||||
DoubleDouble remainder = left - (right * quotient);
|
||||
double correction = remainder._high / right._high;
|
||||
remainder -= right * correction;
|
||||
double finalCorrection = remainder._high / right._high;
|
||||
return FromComponents(quotient, correction) + finalCorrection;
|
||||
double finalRemainder = PreciseMathHelper.SubtractDivisionCorrectionHigh(remainder, right * correction);
|
||||
double finalCorrection = finalRemainder / right._high;
|
||||
// The entry guard gives |quotient| <= 2^901. Conservatively bounding
|
||||
// the first product and subtraction gives |remainder.High| <= 2^457
|
||||
// at the first correction, hence |correction| <= 2^908 and a finite sum
|
||||
// below 2^909 in magnitude. Retain normalization before the final add.
|
||||
return PreciseMathHelper.NormalizeFinite(quotient, correction) + finalCorrection;
|
||||
}
|
||||
|
||||
/// <summary>Applies the expansion operation without discarding the low component.</summary>
|
||||
|
||||
@@ -98,10 +98,15 @@ public readonly partial struct DoubleDouble :
|
||||
/// Represents a zero value.
|
||||
/// </summary>
|
||||
public static DoubleDouble Zero => new();
|
||||
|
||||
// Mathematical constants store normalized, precomputed binary64 pairs: the
|
||||
// nearest high and then the nearest residual of the high-precision value.
|
||||
// No DD arithmetic, parsing, normalization or heap allocation occurs on access.
|
||||
// Reproduce with the tests' ReferenceData/generate_constants.py.
|
||||
/// <summary>
|
||||
/// Represents the ratio of the circumference of a circle to its diameter, specified by the constant, π.
|
||||
/// </summary>
|
||||
public static DoubleDouble PI => new(3.141592653589793, 1.2246467991473532e-16);
|
||||
public static DoubleDouble Pi => new(3.141592653589793, 1.2246467991473532e-16);
|
||||
/// <summary>
|
||||
/// Represents the natural logarithmic base, specified by the constant, e.
|
||||
/// </summary>
|
||||
@@ -109,7 +114,85 @@ public readonly partial struct DoubleDouble :
|
||||
/// <summary>
|
||||
/// Represents the natural logarithm of value 2.
|
||||
/// </summary>
|
||||
public static DoubleDouble LN2 => new(0.6931471805599453, 2.3190468138462996e-17);
|
||||
public static DoubleDouble Ln2 => new(0.6931471805599453, 2.3190468138462996e-17);
|
||||
|
||||
/// <summary>Gets τ = 2π, the angle of one full turn in radians.</summary>
|
||||
public static DoubleDouble Tau => new(6.283185307179586, 2.4492935982947064e-16);
|
||||
|
||||
/// <summary>Gets π/2, the angle of 90 degrees in radians.</summary>
|
||||
public static DoubleDouble PiOver2 => new(1.5707963267948966, 6.123233995736766e-17);
|
||||
|
||||
/// <summary>Gets π/3, the angle of 60 degrees in radians.</summary>
|
||||
public static DoubleDouble PiOver3 => new(1.0471975511965979, -1.072081766451091e-16);
|
||||
|
||||
/// <summary>Gets π/4, the angle of 45 degrees in radians.</summary>
|
||||
public static DoubleDouble PiOver4 => new(0.7853981633974483, 3.061616997868383e-17);
|
||||
|
||||
/// <summary>Gets π/6, the angle of 30 degrees in radians.</summary>
|
||||
public static DoubleDouble PiOver6 => new(0.5235987755982989, -5.360408832255455e-17);
|
||||
|
||||
/// <summary>Gets 1/π.</summary>
|
||||
public static DoubleDouble InvPi => new(0.3183098861837907, -1.9678676675182486e-17);
|
||||
|
||||
/// <summary>Gets 1/(2π), the factor for converting radians to turns.</summary>
|
||||
public static DoubleDouble InvTau => new(0.15915494309189535, -9.839338337591243e-18);
|
||||
|
||||
/// <summary>Gets π/180. Multiply an angle in degrees by this value to obtain radians.</summary>
|
||||
public static DoubleDouble DegToRad => new(0.017453292519943295, 2.9486522708701687e-19);
|
||||
|
||||
/// <summary>Gets 180/π. Multiply an angle in radians by this value to obtain degrees.</summary>
|
||||
public static DoubleDouble RadToDeg => new(57.29577951308232, -1.9878495670576283e-15);
|
||||
|
||||
/// <summary>Gets 1/e = exp(-1).</summary>
|
||||
public static DoubleDouble InvE => new(0.36787944117144233, -1.2428753672788363e-17);
|
||||
|
||||
/// <summary>Gets ln(10), the factor for converting base-10 logarithms to natural logarithms.</summary>
|
||||
public static DoubleDouble Ln10 => new(2.302585092994046, -2.1707562233822494e-16);
|
||||
|
||||
/// <summary>Gets log₂(e) = 1/ln(2), the factor for converting natural logarithms to base 2.</summary>
|
||||
public static DoubleDouble Log2E => new(1.4426950408889634, 2.0355273740931033e-17);
|
||||
|
||||
/// <summary>Gets log₁₀(e) = 1/ln(10), the factor for converting natural logarithms to base 10.</summary>
|
||||
public static DoubleDouble Log10E => new(0.4342944819032518, 1.098319650216765e-17);
|
||||
|
||||
/// <summary>Gets log₂(10), the factor for converting base-10 logarithms to base 2.</summary>
|
||||
public static DoubleDouble Log2Of10 => new(3.321928094887362, 1.661617516973592e-16);
|
||||
|
||||
/// <summary>Gets log₁₀(2), the factor for converting base-2 logarithms to base 10.</summary>
|
||||
public static DoubleDouble Log10Of2 => new(0.3010299956639812, -2.8037281277851704e-18);
|
||||
|
||||
/// <summary>Gets √2.</summary>
|
||||
public static DoubleDouble Sqrt2 => new(1.4142135623730951, -9.667293313452913e-17);
|
||||
|
||||
/// <summary>Gets √3.</summary>
|
||||
public static DoubleDouble Sqrt3 => new(1.7320508075688772, 1.0035084221806903e-16);
|
||||
|
||||
/// <summary>Gets √5.</summary>
|
||||
public static DoubleDouble Sqrt5 => new(2.23606797749979, -1.0864230407365012e-16);
|
||||
|
||||
/// <summary>Gets 1/√2, also the sine and cosine of π/4.</summary>
|
||||
public static DoubleDouble InvSqrt2 => new(0.7071067811865476, -4.833646656726457e-17);
|
||||
|
||||
/// <summary>Gets 1/√3, also the tangent of π/6.</summary>
|
||||
public static DoubleDouble InvSqrt3 => new(0.5773502691896257, 3.3450280739356345e-17);
|
||||
|
||||
/// <summary>Gets √π, the Gaussian integral over the real line for exp(-x²).</summary>
|
||||
public static DoubleDouble SqrtPi => new(1.772453850905516, -7.666586499825799e-17);
|
||||
|
||||
/// <summary>Gets 1/√π, a Gaussian normalization factor.</summary>
|
||||
public static DoubleDouble InvSqrtPi => new(0.5641895835477563, 7.66772980658294e-18);
|
||||
|
||||
/// <summary>Gets 2/√π, the normalization factor in the error-function integral.</summary>
|
||||
public static DoubleDouble TwoInvSqrtPi => new(1.1283791670955126, 1.533545961316588e-17);
|
||||
|
||||
/// <summary>Gets √(2π), used in Gaussian integrals and Stirling's approximation.</summary>
|
||||
public static DoubleDouble SqrtTau => new(2.5066282746310007, -1.8328579980459167e-16);
|
||||
|
||||
/// <summary>Gets 1/√(2π), the standard normal probability density's normalization factor.</summary>
|
||||
public static DoubleDouble InvSqrtTau => new(0.3989422804014327, -2.49232720227773e-17);
|
||||
|
||||
/// <summary>Gets the golden ratio φ = (1 + √5)/2.</summary>
|
||||
public static DoubleDouble GoldenRatio => new(1.618033988749895, -5.432115203682506e-17);
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -104,10 +104,35 @@ internal static class PreciseMathHelper
|
||||
return NormalizeFinite(sum, sumError + (middleError + lowError));
|
||||
}
|
||||
|
||||
// Only for DD division's second remainder: normalized finite inputs with highs
|
||||
// bounded by 2^462, including the correction product's exact fallback results.
|
||||
// Same sign and normal binade imply Sterbenz-exact high subtraction. Its error
|
||||
// is +0; with canonical input lows, TwoAdd(+0, lowSum) is (lowSum, +0).
|
||||
// Eliminate those two transforms, not the low-sum error or final normalization.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static double SubtractDivisionCorrectionHigh(DoubleDouble remainder, DoubleDouble product)
|
||||
{
|
||||
ulong remainderBits = BitConverter.DoubleToUInt64Bits(remainder._high);
|
||||
ulong productBits = BitConverter.DoubleToUInt64Bits(product._high);
|
||||
if ((remainderBits & 0x7ff0_0000_0000_0000UL) == 0
|
||||
|| ((remainderBits ^ productBits) & 0xfff0_0000_0000_0000UL) != 0)
|
||||
{
|
||||
return (remainder - product)._high;
|
||||
}
|
||||
|
||||
double high = remainder._high - product._high;
|
||||
(double low, double lowError) = TwoAdd(remainder._low, -product._low);
|
||||
(double sum, double sumError) = TwoAdd(high, low);
|
||||
double error = sumError + lowError;
|
||||
// Preserve NormalizeFinite's zero-low shortcut, including the high zero's
|
||||
// sign. The normalized low output is unused by the final quotient correction.
|
||||
return error == 0.0 ? sum : sum + error;
|
||||
}
|
||||
|
||||
// Both components and their rounded sum must be finite. Unlike QuickTwoSum,
|
||||
// this entry point permits either magnitude order, including cancellation.
|
||||
// AddFinite establishes these bounds; this helper does not validate them or
|
||||
// canonicalize NaN/infinity. Use DoubleDouble.FromComponents for arbitrary pairs.
|
||||
// AddFinite and DD division establish these bounds; this helper does not validate
|
||||
// them or canonicalize NaN/infinity. Use DoubleDouble.FromComponents for arbitrary pairs.
|
||||
// Retain the high zero's sign when low is zero, as FromComponents does.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static DoubleDouble NormalizeFinite(double high, double low)
|
||||
|
||||
@@ -80,6 +80,7 @@ public class ArithmeticRangeTests
|
||||
[InlineData("+")]
|
||||
[InlineData("-")]
|
||||
[InlineData("*")]
|
||||
[InlineData("/")]
|
||||
public void FiniteKernelsPreservePreviousComponentBits(string operation)
|
||||
{
|
||||
// Differential characterization, not an independent accuracy oracle.
|
||||
@@ -113,7 +114,39 @@ public class ArithmeticRangeTests
|
||||
{
|
||||
DoubleDouble expected;
|
||||
DoubleDouble actual;
|
||||
if (operation == "*")
|
||||
if (operation == "/")
|
||||
{
|
||||
if (left.High == 0.0 || right.High == 0.0
|
||||
|| Math.Abs(Math.ILogB(left.High)) > 450 || Math.Abs(Math.ILogB(right.High)) > 450)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Retain the original division expression, including its public
|
||||
// normalization. Sparse corrections may use MultiplyBoundary.
|
||||
double quotient = left.High / right.High;
|
||||
DoubleDouble remainder = left - (right * quotient);
|
||||
double correction = remainder.High / right.High;
|
||||
double.IsFinite(quotient).ShouldBeTrue();
|
||||
double.IsFinite(correction).ShouldBeTrue();
|
||||
double.IsFinite(quotient + correction).ShouldBeTrue();
|
||||
// Conservative bounds from the entry domain and the first
|
||||
// scalar product/four-TwoSum remainder, not an accuracy claim.
|
||||
(Math.Abs(quotient) <= Math.ScaleB(1.0, 901)).ShouldBeTrue();
|
||||
(Math.Abs(remainder.High) <= Math.ScaleB(1.0, 457)).ShouldBeTrue();
|
||||
(Math.Abs(correction) <= Math.ScaleB(1.0, 908)).ShouldBeTrue();
|
||||
(Math.Abs(quotient + correction) < Math.ScaleB(1.0, 909)).ShouldBeTrue();
|
||||
DoubleDouble correctionProduct = right * correction;
|
||||
AssertDivisionResidualCancellation(remainder, correctionProduct);
|
||||
remainder -= correctionProduct;
|
||||
double finalCorrection = remainder.High / right.High;
|
||||
DoubleDouble normalized = DoubleDouble.FromComponents(quotient, correction);
|
||||
DoubleDouble finiteNormalized = PreciseMathHelper.NormalizeFinite(quotient, correction);
|
||||
BitConverter.DoubleToInt64Bits(finiteNormalized.High).ShouldBe(BitConverter.DoubleToInt64Bits(normalized.High));
|
||||
BitConverter.DoubleToInt64Bits(finiteNormalized.Low).ShouldBe(BitConverter.DoubleToInt64Bits(normalized.Low));
|
||||
expected = normalized + finalCorrection;
|
||||
actual = left / right;
|
||||
}
|
||||
else if (operation == "*")
|
||||
{
|
||||
if (left.High == 0.0 || right.High == 0.0)
|
||||
{
|
||||
@@ -161,6 +194,62 @@ public class ArithmeticRangeTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DivisionResidualCancellationPreservesHighBitsAtBinadeEdges()
|
||||
{
|
||||
// Include both sides of normal/subnormal and binade transitions, with
|
||||
// canonical zero, dense and sparse lows. Cross-binade/sign/zero cases
|
||||
// are intentionally ineligible and must retain the general subtraction.
|
||||
List<DoubleDouble> values = [new(0.0), new(-0.0)];
|
||||
foreach (int exponent in new[] { -1074, -1022, -900, -54, 0, 1, 457, 461 })
|
||||
{
|
||||
foreach (double significand in new[] { 1.0, Math.BitIncrement(1.0), Math.BitDecrement(2.0) })
|
||||
{
|
||||
foreach (double sign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
double high = sign * Math.ScaleB(significand, exponent);
|
||||
foreach (double low in new[] { 0.0, Math.ScaleB(high, -54), -Math.ScaleB(high, -54),
|
||||
double.Epsilon, -double.Epsilon })
|
||||
{
|
||||
values.Add(DoubleDouble.FromComponents(high, low));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (DoubleDouble remainder in values)
|
||||
{
|
||||
foreach (DoubleDouble product in values)
|
||||
{
|
||||
AssertDivisionResidualCancellation(remainder, product);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertDivisionResidualCancellation(DoubleDouble remainder, DoubleDouble product)
|
||||
{
|
||||
DoubleDouble expected = remainder - product;
|
||||
double actual = PreciseMathHelper.SubtractDivisionCorrectionHigh(remainder, product);
|
||||
BitConverter.DoubleToInt64Bits(actual).ShouldBe(BitConverter.DoubleToInt64Bits(expected.High));
|
||||
// Independent BCL classification, rather than the proposed exponent-bit
|
||||
// guard. Within one normal binade, same-sign subtraction is exact.
|
||||
if (!double.IsNormal(remainder.High) || !double.IsNormal(product.High)
|
||||
|| Math.Sign(remainder.High) != Math.Sign(product.High)
|
||||
|| Math.ILogB(remainder.High) != Math.ILogB(product.High))
|
||||
{
|
||||
return;
|
||||
}
|
||||
(double high, double highError) = PreciseMathHelper.TwoAdd(remainder.High, -product.High);
|
||||
BitConverter.DoubleToInt64Bits(highError).ShouldBe(0L);
|
||||
(double low, double lowError) = PreciseMathHelper.TwoAdd(remainder.Low, -product.Low);
|
||||
(double middle, double middleError) = PreciseMathHelper.TwoAdd(highError, low);
|
||||
BitConverter.DoubleToInt64Bits(middle).ShouldBe(BitConverter.DoubleToInt64Bits(low));
|
||||
BitConverter.DoubleToInt64Bits(middleError).ShouldBe(0L);
|
||||
(double sum, double sumError) = PreciseMathHelper.TwoAdd(high, low);
|
||||
double error = sumError + lowError;
|
||||
double simplifiedHigh = error == 0.0 ? sum : sum + error;
|
||||
BitConverter.DoubleToInt64Bits(simplifiedHigh).ShouldBe(BitConverter.DoubleToInt64Bits(expected.High));
|
||||
}
|
||||
|
||||
private static void AssertMultiplicationRange(double left, double right)
|
||||
{
|
||||
int exponent = Math.ILogB(left) + Math.ILogB(right);
|
||||
|
||||
@@ -6,6 +6,303 @@ namespace Just.PreciseMath.Tests;
|
||||
|
||||
public class DoubleDoubleArithmeticTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(6.0, 2.0, 8.0, 4.0, 12.0, 3.0)]
|
||||
[InlineData(-6.0, 2.0, -4.0, -8.0, -12.0, -3.0)]
|
||||
[InlineData(6.0, -2.0, 4.0, 8.0, -12.0, -3.0)]
|
||||
[InlineData(-6.0, -2.0, -8.0, -4.0, 12.0, 3.0)]
|
||||
[InlineData(1.5, 0.5, 2.0, 1.0, 0.75, 3.0)]
|
||||
[InlineData(-1.5, 0.5, -1.0, -2.0, -0.75, -3.0)]
|
||||
[InlineData(0.75, 1.5, 2.25, -0.75, 1.125, 0.5)]
|
||||
[InlineData(0.0, 2.0, 2.0, -2.0, 0.0, 0.0)]
|
||||
[InlineData(2.0, 2.0, 4.0, 0.0, 4.0, 1.0)]
|
||||
public void BasicArithmeticHasExactDyadicResults(double left, double right,
|
||||
double sum, double difference, double product, double quotient)
|
||||
{
|
||||
// Small integers and binary fractions: each expected result is an exact
|
||||
// rational representable in binary64, so no accuracy tolerance is needed.
|
||||
DoubleDouble a = new(left);
|
||||
DoubleDouble b = new(right);
|
||||
CheckBoundary(a + b, sum, 0.0);
|
||||
CheckBoundary(a + right, sum, 0.0);
|
||||
CheckBoundary(left + b, sum, 0.0);
|
||||
CheckBoundary(a - b, difference, 0.0);
|
||||
CheckBoundary(a - right, difference, 0.0);
|
||||
CheckBoundary(left - b, difference, 0.0);
|
||||
CheckBoundary(a * b, product, 0.0);
|
||||
CheckBoundary(a * right, product, 0.0);
|
||||
CheckBoundary(left * b, product, 0.0);
|
||||
CheckBoundary(a / b, quotient, 0.0);
|
||||
CheckBoundary(a / right, quotient, 0.0);
|
||||
CheckBoundary(left / b, quotient, 0.0);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(30.0, 0.5235987755982989, -5.360408832255455e-17)]
|
||||
[InlineData(45.0, 0.7853981633974483, 3.061616997868383e-17)]
|
||||
[InlineData(60.0, 1.0471975511965979, -1.072081766451091e-16)]
|
||||
[InlineData(90.0, 1.5707963267948966, 6.123233995736766e-17)]
|
||||
[InlineData(180.0, 3.141592653589793, 1.2246467991473532e-16)]
|
||||
[InlineData(360.0, 6.283185307179586, 2.4492935982947064e-16)]
|
||||
public void AngleConversionConstantsRetainExtendedPrecision(double degrees, double radiansHigh, double radiansLow)
|
||||
{
|
||||
// Independent precomputed pi fractions from ReferenceData/generate_constants.py.
|
||||
// Check each direction against its own reference, not a computed round trip.
|
||||
foreach (double sign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
double signedDegrees = sign * degrees;
|
||||
BigInteger radiansUnits = Units(sign * radiansHigh) + Units(sign * radiansLow);
|
||||
AssertRelative(signedDegrees * DoubleDouble.DegToRad, radiansUnits, BigInteger.One);
|
||||
AssertRelative(DoubleDouble.DegToRad * signedDegrees, radiansUnits, BigInteger.One);
|
||||
AssertRelative(new DoubleDouble(signedDegrees) * DoubleDouble.DegToRad, radiansUnits, BigInteger.One);
|
||||
DoubleDouble radians = DoubleDouble.FromComponents(sign * radiansHigh, sign * radiansLow);
|
||||
AssertRelative(radians * DoubleDouble.RadToDeg, Units(signedDegrees), BigInteger.One);
|
||||
AssertRelative(DoubleDouble.RadToDeg * radians, Units(signedDegrees), BigInteger.One);
|
||||
}
|
||||
}
|
||||
|
||||
// Generated by: python3 1-tests/Just.PreciseMath.Tests/ReferenceData/generate_irrational_arithmetic.py
|
||||
// Constants: Python Decimal at 160 digits, checked again at 240 digits;
|
||||
// pi uses Machin's formula, e/roots/ln use Decimal's exp/sqrt/ln, phi=(1+sqrt(5))/2.
|
||||
// Expected operations use exact Fraction sums of the stored input components,
|
||||
// then round high and residual separately to binary64. A scalar input has no low.
|
||||
// Column pairs: left, right, sum, difference, product, quotient.
|
||||
[Theory]
|
||||
// pi, e: DD/DD
|
||||
[InlineData("DD/DD",
|
||||
3.141592653589793, 1.2246467991473532e-16,
|
||||
2.718281828459045, 1.4456468917292502e-16,
|
||||
5.859874482048839, -1.7705984076240228e-16,
|
||||
0.423310825130748, -2.2100009258189695e-17,
|
||||
8.539734222673568, -6.773815290502424e-16,
|
||||
1.1557273497909217, -1.3998972600526045e-17)]
|
||||
// pi, e: DD/double
|
||||
[InlineData("DD/double",
|
||||
3.141592653589793, 1.2246467991473532e-16,
|
||||
2.718281828459045, 0.0,
|
||||
5.859874482048839, -3.2162452993532727e-16,
|
||||
0.42331082513074814, 1.1442377452219667e-17,
|
||||
8.539734222673566, 6.44811944875855e-16,
|
||||
1.1557273497909217, 4.746535510161172e-17)]
|
||||
// pi, e: double/DD
|
||||
[InlineData("double/DD",
|
||||
3.141592653589793, 0.0,
|
||||
2.718281828459045, 1.4456468917292502e-16,
|
||||
5.859874482048839, -2.995245206771376e-16,
|
||||
0.42331082513074786, 2.1968764520848465e-17,
|
||||
8.539734222673566, 7.660817963097297e-16,
|
||||
1.1557273497909217, -5.905121061079843e-17)]
|
||||
// sqrt2, sqrt3: DD/DD
|
||||
[InlineData("DD/DD",
|
||||
1.4142135623730951, -9.667293313452913e-17,
|
||||
1.7320508075688772, 1.0035084221806903e-16,
|
||||
3.1462643699419726, -2.1836669584149143e-16,
|
||||
-0.31783724519578227, 2.5020829572433146e-17,
|
||||
2.449489742783178, 2.168616518103246e-16,
|
||||
0.816496580927726, -1.7276510382355668e-18)]
|
||||
// sqrt2, sqrt3: DD/double
|
||||
[InlineData("DD/double",
|
||||
1.4142135623730951, -9.667293313452913e-17,
|
||||
1.7320508075688772, 0.0,
|
||||
3.146264369941972, 1.2537167179050217e-16,
|
||||
-0.31783724519578216, 1.434936932798652e-17,
|
||||
2.449489742783178, 7.494412974996883e-17,
|
||||
0.816496580927726, 4.5578189648549696e-17)]
|
||||
// sqrt2, sqrt3: double/DD
|
||||
[InlineData("double/DD",
|
||||
1.4142135623730951, 0.0,
|
||||
1.7320508075688772, 1.0035084221806903e-16,
|
||||
3.1462643699419726, -1.2169376270696227e-16,
|
||||
-0.31783724519578216, 1.0671460244446626e-17,
|
||||
2.4494897427831783, -5.978512613402474e-17,
|
||||
0.816496580927726, 5.408649293033552e-17)]
|
||||
// ln2, phi: DD/DD
|
||||
[InlineData("DD/DD",
|
||||
0.6931471805599453, 2.3190468138462996e-17,
|
||||
1.618033988749895, -5.432115203682506e-17,
|
||||
2.3111811693098403, -1.421529863608777e-16,
|
||||
-0.9248868081899495, -3.35106822872276e-17,
|
||||
1.121535697352152, -9.053143373999594e-17,
|
||||
0.4283885167922066, -2.699599415943276e-18)]
|
||||
// ln2, phi: DD/double
|
||||
[InlineData("DD/double",
|
||||
0.6931471805599453, 2.3190468138462996e-17,
|
||||
1.618033988749895, 0.0,
|
||||
2.3111811693098403, -8.783183432405266e-17,
|
||||
-0.9248868081899496, 2.3190468138462996e-17,
|
||||
1.121535697352152, -5.2878880360902515e-17,
|
||||
0.4283885167922066, -1.708159504353726e-17)]
|
||||
// ln2, phi: double/DD
|
||||
[InlineData("double/DD",
|
||||
0.6931471805599453, 0.0,
|
||||
1.618033988749895, -5.432115203682506e-17,
|
||||
2.3111811693098403, -1.653434544993407e-16,
|
||||
-0.9248868081899496, 5.432115203682506e-17,
|
||||
1.1215356973521518, 9.399020552198074e-17,
|
||||
0.4283885167922066, -1.703209694053491e-17)]
|
||||
public void BasicArithmeticMatchesPrecomputedIrrationalResults(string overload,
|
||||
double leftHigh, double leftLow, double rightHigh, double rightLow,
|
||||
double sumHigh, double sumLow, double differenceHigh, double differenceLow,
|
||||
double productHigh, double productLow, double quotientHigh, double quotientLow)
|
||||
{
|
||||
foreach (double sign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
DoubleDouble left = DoubleDouble.FromComponents(sign * leftHigh, sign * leftLow);
|
||||
DoubleDouble right = DoubleDouble.FromComponents(sign * rightHigh, sign * rightLow);
|
||||
(DoubleDouble sum, DoubleDouble difference, DoubleDouble product, DoubleDouble quotient) = overload switch
|
||||
{
|
||||
"DD/DD" => (left + right, left - right, left * right, left / right),
|
||||
"DD/double" => (left + right.High, left - right.High, left * right.High, left / right.High),
|
||||
"double/DD" => (left.High + right, left.High - right, left.High * right, left.High / right),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(overload))
|
||||
};
|
||||
// Negating both inputs negates sum/difference but not product/quotient.
|
||||
// Compare complete expansions at the established 2^-100 relative + epsilon bound,
|
||||
// not individual component equality: correct rounding is not guaranteed.
|
||||
// The generator checks reference-rounding error is below 2^-105 relative.
|
||||
AssertRelative(sum, Units(sign * sumHigh) + Units(sign * sumLow), BigInteger.One);
|
||||
AssertRelative(difference, Units(sign * differenceHigh) + Units(sign * differenceLow), BigInteger.One);
|
||||
AssertRelative(product, Units(productHigh) + Units(productLow), BigInteger.One);
|
||||
AssertRelative(quotient, Units(quotientHigh) + Units(quotientLow), BigInteger.One);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("+", "DD/DD")]
|
||||
[InlineData("-", "DD/DD")]
|
||||
[InlineData("*", "DD/DD")]
|
||||
[InlineData("/", "DD/DD")]
|
||||
[InlineData("+", "DD/double")]
|
||||
[InlineData("-", "DD/double")]
|
||||
[InlineData("*", "DD/double")]
|
||||
[InlineData("/", "DD/double")]
|
||||
[InlineData("+", "double/DD")]
|
||||
[InlineData("-", "double/DD")]
|
||||
[InlineData("*", "double/DD")]
|
||||
[InlineData("/", "double/DD")]
|
||||
public void GeneralArithmeticMeetsExactRationalBound(string operation, string overload)
|
||||
{
|
||||
// Fixed seed, with dense, sparse and zero residuals independent of the
|
||||
// high sign. Include ordinary exponents as well as dispatch/range edges.
|
||||
// Every pair is checked in both orders; overflow cases are asserted, not skipped.
|
||||
Random random = new(65537);
|
||||
int[] exponents = [-1074, -1022, -969, -451, -450, -1, 0, 1, 450, 451, 900, 1020, 1021, 1023];
|
||||
foreach (int leftExponent in exponents)
|
||||
{
|
||||
foreach (int rightExponent in exponents)
|
||||
{
|
||||
for (int sample = 0; sample < 4; ++sample)
|
||||
{
|
||||
DoubleDouble left = GeneralArithmeticSample(random, leftExponent, sample);
|
||||
DoubleDouble right = GeneralArithmeticSample(random, rightExponent, (sample + 1) % 4);
|
||||
AssertGeneralArithmetic(left, right, operation, overload);
|
||||
AssertGeneralArithmetic(right, left, operation, overload);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int sample = 0; sample < 512; ++sample)
|
||||
{
|
||||
int exponent = random.Next(-450, 451);
|
||||
DoubleDouble left = GeneralArithmeticSample(random, exponent, sample % 4);
|
||||
DoubleDouble right = GeneralArithmeticSample(random, random.Next(-450, 451), (sample + 1) % 4);
|
||||
AssertGeneralArithmetic(left, right, operation, overload);
|
||||
AssertGeneralArithmetic(right, left, operation, overload);
|
||||
// Correlate highs to force cancellation rather than hoping random
|
||||
// independent values happen to exercise it. Keep different low terms.
|
||||
DoubleDouble neighbor = DoubleDouble.FromComponents(left.High, -left.Low);
|
||||
AssertGeneralArithmetic(left, neighbor, operation, overload);
|
||||
AssertGeneralArithmetic(left, -neighbor, operation, overload);
|
||||
}
|
||||
}
|
||||
|
||||
private static DoubleDouble GeneralArithmeticSample(Random random, int exponent, int residualKind)
|
||||
{
|
||||
double sign = random.Next(2) == 0 ? -1.0 : 1.0;
|
||||
double high = Math.ScaleB(sign * (1.0 + (0.75 * random.NextDouble())), exponent);
|
||||
double low = residualKind switch
|
||||
{
|
||||
0 => 0.0,
|
||||
1 => Math.ScaleB(random.NextDouble() - 0.5, exponent - 53),
|
||||
2 => Math.ScaleB(random.NextDouble() - 0.5, exponent - 106),
|
||||
3 => random.Next(2) == 0 ? -double.Epsilon : double.Epsilon,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(residualKind))
|
||||
};
|
||||
// At the subnormal floor a residual can cancel the high completely.
|
||||
// Use a scalar there so this finite-input matrix never divides by zero;
|
||||
// the separate special-value matrix covers zero denominators and NaNs.
|
||||
if (exponent < -1022)
|
||||
{
|
||||
low = 0.0;
|
||||
}
|
||||
DoubleDouble result = DoubleDouble.FromComponents(high, low);
|
||||
double.IsFinite(result.High).ShouldBeTrue();
|
||||
result.High.ShouldNotBe(0.0);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void AssertGeneralArithmetic(DoubleDouble left, DoubleDouble right, string operation, string overload)
|
||||
{
|
||||
DoubleDouble actual = (operation, overload) switch
|
||||
{
|
||||
("+", "DD/DD") => left + right,
|
||||
("-", "DD/DD") => left - right,
|
||||
("*", "DD/DD") => left * right,
|
||||
("/", "DD/DD") => left / right,
|
||||
("+", "DD/double") => left + right.High,
|
||||
("-", "DD/double") => left - right.High,
|
||||
("*", "DD/double") => left * right.High,
|
||||
("/", "DD/double") => left / right.High,
|
||||
("+", "double/DD") => left.High + right,
|
||||
("-", "double/DD") => left.High - right,
|
||||
("*", "double/DD") => left.High * right,
|
||||
("/", "double/DD") => left.High / right,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(operation))
|
||||
};
|
||||
// Units decodes both IEEE-754 components independently of library arithmetic.
|
||||
// Expected numerator/denominator is measured in units of epsilon, not doubles.
|
||||
BigInteger x = overload == "double/DD" ? Units(left.High) : Units(left);
|
||||
BigInteger y = overload == "DD/double" ? Units(right.High) : Units(right);
|
||||
(BigInteger numerator, BigInteger denominator) = operation switch
|
||||
{
|
||||
"+" => (x + y, BigInteger.One),
|
||||
"-" => (x - y, BigInteger.One),
|
||||
"*" => (x * y, BigInteger.One << 1074),
|
||||
"/" => (x << 1074, y),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(operation))
|
||||
};
|
||||
if (denominator.Sign < 0)
|
||||
{
|
||||
numerator = -numerator;
|
||||
denominator = -denominator;
|
||||
}
|
||||
string context = $"{overload}: ({left.High:R}, {left.Low:R}) {operation} ({right.High:R}, {right.Low:R}); "
|
||||
+ $"actual ({actual.High:R}, {actual.Low:R})";
|
||||
// Nearest-even binary64 overflow begins at 2^1024 - 2^970.
|
||||
BigInteger overflowUnits = Units(double.MaxValue) + Units(Math.ScaleB(1.0, 970));
|
||||
if (BigInteger.Abs(numerator) >= overflowUnits * denominator)
|
||||
{
|
||||
actual.High.ShouldBe(numerator.Sign < 0 ? double.NegativeInfinity : double.PositiveInfinity, context);
|
||||
BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(0L, context);
|
||||
return;
|
||||
}
|
||||
double.IsFinite(actual.High).ShouldBeTrue(context);
|
||||
double.IsFinite(actual.Low).ShouldBeTrue(context);
|
||||
BigInteger error = BigInteger.Abs((Units(actual) * denominator) - numerator);
|
||||
// Same conservative 2^-100 relative + epsilon contract as the existing
|
||||
// suite, cross-multiplied exactly to retain all low-component information.
|
||||
(error <= (BigInteger.Abs(numerator) >> 100) + denominator).ShouldBeTrue(context);
|
||||
(actual.High + actual.Low).ShouldBe(actual.High, context);
|
||||
if (actual.Low == 0.0)
|
||||
{
|
||||
BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(0L, context);
|
||||
}
|
||||
if (actual.High == 0.0)
|
||||
{
|
||||
// Exact cancellation is +0; a nonzero underflow keeps its sign.
|
||||
BitConverter.DoubleToInt64Bits(actual.High).ShouldBe(numerator.Sign < 0 ? long.MinValue : 0L, context);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CancellationRetainsBothLowSumTerms()
|
||||
{
|
||||
@@ -232,6 +529,56 @@ public class DoubleDoubleArithmeticTests
|
||||
AssertRelative(-1.0 / value, -numerator, Units(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DivisionNormalizesTheFirstCorrectionBeforeAddingTheLast()
|
||||
{
|
||||
// Exact rational: (1 + 2^-54) / (17/16 - 2^-54).
|
||||
// Independently round that rational to binary64, then round its exact
|
||||
// residual: high bits 3FEE1E1E1E1E1E1F, low bits 3C4FE3A76B2EF2C4.
|
||||
// Adding the last correction to the unnormalized pair instead loses
|
||||
// four low-component ULPs. Common power-of-two scaling preserves the ratio.
|
||||
double expectedHigh = BitConverter.UInt64BitsToDouble(0x3fee_1e1e_1e1e_1e1f);
|
||||
double expectedLow = BitConverter.UInt64BitsToDouble(0x3c4f_e3a7_6b2e_f2c4);
|
||||
foreach (int exponent in new[] { -450, 0, 450 })
|
||||
{
|
||||
foreach (double leftSign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
foreach (double rightSign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
DoubleDouble left = DoubleDouble.FromComponents(leftSign * Math.ScaleB(1.0, exponent),
|
||||
leftSign * Math.ScaleB(1.0, exponent - 54));
|
||||
DoubleDouble right = DoubleDouble.FromComponents(rightSign * Math.ScaleB(1.0625, exponent),
|
||||
-rightSign * Math.ScaleB(1.0, exponent - 54));
|
||||
double sign = leftSign * rightSign;
|
||||
DoubleDouble actual = left / right;
|
||||
CheckBoundary(actual, sign * expectedHigh, sign * expectedLow);
|
||||
AssertRelative(actual, Units(left) << 1074, Units(right));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DivisionSecondResidualPreservesBitsAndRationalAccuracy()
|
||||
{
|
||||
Random random = new(104729);
|
||||
for (int i = 0; i < 4096; ++i)
|
||||
{
|
||||
int leftExponent = random.Next(-450, 451);
|
||||
int rightExponent = random.Next(-450, 451);
|
||||
double leftHigh = Math.ScaleB(1.0 + (0.75 * random.NextDouble()), leftExponent);
|
||||
double rightHigh = Math.ScaleB(1.0 + (0.75 * random.NextDouble()), rightExponent);
|
||||
double leftLow = i % 3 == 0 ? 0.0 : i % 3 == 1 ? double.Epsilon
|
||||
: Math.ScaleB(random.NextDouble() - 0.5, leftExponent - 53);
|
||||
double rightLow = i % 3 == 1 ? 0.0 : i % 3 == 2 ? -double.Epsilon
|
||||
: Math.ScaleB(random.NextDouble() - 0.5, rightExponent - 53);
|
||||
DoubleDouble left = DoubleDouble.FromComponents(i % 2 == 0 ? leftHigh : -leftHigh, leftLow);
|
||||
DoubleDouble right = DoubleDouble.FromComponents(i % 4 < 2 ? rightHigh : -rightHigh, rightLow);
|
||||
AssertDivisionMatchesPrevious(left, right);
|
||||
AssertDivisionMatchesPrevious(right, left);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DivisionRetainsSubnormalCorrectionsWithOrdinaryHighComponents()
|
||||
{
|
||||
@@ -537,6 +884,21 @@ public class DoubleDoubleArithmeticTests
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertDivisionMatchesPrevious(DoubleDouble left, DoubleDouble right)
|
||||
{
|
||||
// Freeze the pre-specialization expression. The unchanged public operators
|
||||
// form the bitwise reference; Units supplies the independent rational oracle.
|
||||
double quotient = left.High / right.High;
|
||||
DoubleDouble remainder = left - (right * quotient);
|
||||
double correction = remainder.High / right.High;
|
||||
remainder -= right * correction;
|
||||
double finalCorrection = remainder.High / right.High;
|
||||
DoubleDouble expected = DoubleDouble.FromComponents(quotient, correction) + finalCorrection;
|
||||
DoubleDouble actual = left / right;
|
||||
CheckBoundary(actual, expected.High, expected.Low);
|
||||
AssertRelative(actual, Units(left) << 1074, Units(right));
|
||||
}
|
||||
|
||||
private static void Check(DoubleDouble value, double high, double low)
|
||||
{
|
||||
value.High.ShouldBe(high);
|
||||
|
||||
@@ -58,12 +58,12 @@ public class DoubleDoubleFormattingTests
|
||||
public void FormattingDoesNotRouteThroughDecimalOrRestrictExponentRange()
|
||||
{
|
||||
// Review-1 §6 verified the former decimal-based formatter threw for 1e100,
|
||||
// printed "0" for 1e-100, threw for NaN/infinity, and emitted for PI digits
|
||||
// printed "0" for 1e-100, threw for NaN/infinity, and emitted for pi digits
|
||||
// already wrong at binary64 precision. Expected digits are exact references:
|
||||
// the PI pair rounded to 32 significant digits, ties to even
|
||||
// the Pi pair rounded to 32 significant digits, ties to even
|
||||
// (Python Fraction/Decimal at precision 120), and the exact binary64 values of
|
||||
// the powers of ten.
|
||||
DoubleDouble.PI.ToString("G32", CultureInfo.InvariantCulture).ShouldBe("3.1415926535897932384626433832795");
|
||||
DoubleDouble.Pi.ToString("G32", CultureInfo.InvariantCulture).ShouldBe("3.1415926535897932384626433832795");
|
||||
new DoubleDouble(1e100).ToString("G", CultureInfo.InvariantCulture).ShouldBe("1.0000000000000000159028911097599E+100");
|
||||
new DoubleDouble(1e-100).ToString("G", CultureInfo.InvariantCulture).ShouldBe("1.0000000000000000199918998026029E-100");
|
||||
// The binary64 values are exactly representable as sums with a zero residual,
|
||||
|
||||
@@ -38,7 +38,7 @@ public class DoubleDoubleSpanFormattingTests
|
||||
public void ShortDestinationIsUnchangedAndReportsZero()
|
||||
{
|
||||
char[] buffer = ['!', '!'];
|
||||
DoubleDouble.PI.TryFormat(buffer, out int written, "G32", CultureInfo.InvariantCulture).ShouldBeFalse();
|
||||
DoubleDouble.Pi.TryFormat(buffer, out int written, "G32", CultureInfo.InvariantCulture).ShouldBeFalse();
|
||||
written.ShouldBe(0);
|
||||
new string(buffer).ShouldBe("!!");
|
||||
DoubleDouble.One.TryFormat(Span<char>.Empty, out written, provider: CultureInfo.InvariantCulture).ShouldBeFalse();
|
||||
|
||||
@@ -6,25 +6,81 @@ namespace Just.PreciseMath.Tests;
|
||||
public class DoubleDoubleTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("PI", 3.141592653589793, 1.2246467991473532e-16)]
|
||||
[InlineData("Pi", 3.141592653589793, 1.2246467991473532e-16)]
|
||||
[InlineData("E", 2.718281828459045, 1.4456468917292502e-16)]
|
||||
[InlineData("LN2", 0.6931471805599453, 2.3190468138462996e-17)]
|
||||
[InlineData("Ln2", 0.6931471805599453, 2.3190468138462996e-17)]
|
||||
[InlineData("Tau", 6.283185307179586, 2.4492935982947064e-16)]
|
||||
[InlineData("PiOver2", 1.5707963267948966, 6.123233995736766e-17)]
|
||||
[InlineData("PiOver3", 1.0471975511965979, -1.072081766451091e-16)]
|
||||
[InlineData("PiOver4", 0.7853981633974483, 3.061616997868383e-17)]
|
||||
[InlineData("PiOver6", 0.5235987755982989, -5.360408832255455e-17)]
|
||||
[InlineData("InvPi", 0.3183098861837907, -1.9678676675182486e-17)]
|
||||
[InlineData("InvTau", 0.15915494309189535, -9.839338337591243e-18)]
|
||||
[InlineData("DegToRad", 0.017453292519943295, 2.9486522708701687e-19)]
|
||||
[InlineData("RadToDeg", 57.29577951308232, -1.9878495670576283e-15)]
|
||||
[InlineData("InvE", 0.36787944117144233, -1.2428753672788363e-17)]
|
||||
[InlineData("Ln10", 2.302585092994046, -2.1707562233822494e-16)]
|
||||
[InlineData("Log2E", 1.4426950408889634, 2.0355273740931033e-17)]
|
||||
[InlineData("Log10E", 0.4342944819032518, 1.098319650216765e-17)]
|
||||
[InlineData("Log2Of10", 3.321928094887362, 1.661617516973592e-16)]
|
||||
[InlineData("Log10Of2", 0.3010299956639812, -2.8037281277851704e-18)]
|
||||
[InlineData("Sqrt2", 1.4142135623730951, -9.667293313452913e-17)]
|
||||
[InlineData("Sqrt3", 1.7320508075688772, 1.0035084221806903e-16)]
|
||||
[InlineData("Sqrt5", 2.23606797749979, -1.0864230407365012e-16)]
|
||||
[InlineData("InvSqrt2", 0.7071067811865476, -4.833646656726457e-17)]
|
||||
[InlineData("InvSqrt3", 0.5773502691896257, 3.3450280739356345e-17)]
|
||||
[InlineData("SqrtPi", 1.772453850905516, -7.666586499825799e-17)]
|
||||
[InlineData("InvSqrtPi", 0.5641895835477563, 7.66772980658294e-18)]
|
||||
[InlineData("TwoInvSqrtPi", 1.1283791670955126, 1.533545961316588e-17)]
|
||||
[InlineData("SqrtTau", 2.5066282746310007, -1.8328579980459167e-16)]
|
||||
[InlineData("InvSqrtTau", 0.3989422804014327, -2.49232720227773e-17)]
|
||||
[InlineData("GoldenRatio", 1.618033988749895, -5.432115203682506e-17)]
|
||||
public void ConstantsHaveNearestBinary64Residuals(string name, double high, double low)
|
||||
{
|
||||
// Each residual is round_binary64(constant - exact_binary64(high)).
|
||||
// Reproduced with Python decimal at precision 90: e = Decimal(1).exp(),
|
||||
// ln(2) = Decimal(2).ln(), pi = 16*atan(1/5) - 4*atan(1/239), using
|
||||
// atan(x) = sum((-1)^k*x^(2k+1)/(2k+1)) until |term| < 1e-95.
|
||||
// low = float(reference - Decimal.from_float(float(reference))).
|
||||
// Reproduce with ReferenceData/generate_constants.py: Python Decimal at
|
||||
// 160 and 240 digits, Machin's pi, exp/ln/sqrt and exact Fraction splitting.
|
||||
// The generator records every formula and checks normalized components.
|
||||
DoubleDouble value = name switch
|
||||
{
|
||||
"PI" => DoubleDouble.PI,
|
||||
"Pi" => DoubleDouble.Pi,
|
||||
"E" => DoubleDouble.E,
|
||||
"LN2" => DoubleDouble.LN2,
|
||||
"Ln2" => DoubleDouble.Ln2,
|
||||
"Tau" => DoubleDouble.Tau,
|
||||
"PiOver2" => DoubleDouble.PiOver2,
|
||||
"PiOver3" => DoubleDouble.PiOver3,
|
||||
"PiOver4" => DoubleDouble.PiOver4,
|
||||
"PiOver6" => DoubleDouble.PiOver6,
|
||||
"InvPi" => DoubleDouble.InvPi,
|
||||
"InvTau" => DoubleDouble.InvTau,
|
||||
"DegToRad" => DoubleDouble.DegToRad,
|
||||
"RadToDeg" => DoubleDouble.RadToDeg,
|
||||
"InvE" => DoubleDouble.InvE,
|
||||
"Ln10" => DoubleDouble.Ln10,
|
||||
"Log2E" => DoubleDouble.Log2E,
|
||||
"Log10E" => DoubleDouble.Log10E,
|
||||
"Log2Of10" => DoubleDouble.Log2Of10,
|
||||
"Log10Of2" => DoubleDouble.Log10Of2,
|
||||
"Sqrt2" => DoubleDouble.Sqrt2,
|
||||
"Sqrt3" => DoubleDouble.Sqrt3,
|
||||
"Sqrt5" => DoubleDouble.Sqrt5,
|
||||
"InvSqrt2" => DoubleDouble.InvSqrt2,
|
||||
"InvSqrt3" => DoubleDouble.InvSqrt3,
|
||||
"SqrtPi" => DoubleDouble.SqrtPi,
|
||||
"InvSqrtPi" => DoubleDouble.InvSqrtPi,
|
||||
"TwoInvSqrtPi" => DoubleDouble.TwoInvSqrtPi,
|
||||
"SqrtTau" => DoubleDouble.SqrtTau,
|
||||
"InvSqrtTau" => DoubleDouble.InvSqrtTau,
|
||||
"GoldenRatio" => DoubleDouble.GoldenRatio,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(name)),
|
||||
};
|
||||
value.High.ShouldBe(high);
|
||||
value.Low.ShouldBe(low);
|
||||
BitConverter.DoubleToInt64Bits(value.High).ShouldBe(BitConverter.DoubleToInt64Bits(high));
|
||||
BitConverter.DoubleToInt64Bits(value.Low).ShouldBe(BitConverter.DoubleToInt64Bits(low));
|
||||
DoubleDouble.IsCanonical(value).ShouldBeTrue();
|
||||
DoubleDouble.IsFinite(value).ShouldBeTrue();
|
||||
value.Low.ShouldNotBe(0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -46,7 +102,7 @@ public class DoubleDoubleTests
|
||||
[InlineData(3.141592653589793, 1.2246467991473532e-16)]
|
||||
[InlineData(2.718281828459045, 1.4456468917292502e-16)]
|
||||
[InlineData(0.6931471805599453, 2.3190468138462996e-17)]
|
||||
public void AdditiveIdentity(double high, double low)
|
||||
public void AdditiveIdentityAdd(double high, double low)
|
||||
{
|
||||
DoubleDouble value = new(high, low);
|
||||
|
||||
@@ -70,7 +126,32 @@ public class DoubleDoubleTests
|
||||
[InlineData(3.141592653589793, 1.2246467991473532e-16)]
|
||||
[InlineData(2.718281828459045, 1.4456468917292502e-16)]
|
||||
[InlineData(0.6931471805599453, 2.3190468138462996e-17)]
|
||||
public void MultiplicativeIdentity(double high, double low)
|
||||
public void AdditiveIdentitySubtract(double high, double low)
|
||||
{
|
||||
DoubleDouble value = new(high, low);
|
||||
DoubleDouble negativeValue = -value;
|
||||
|
||||
DoubleDouble result = value - DoubleDouble.AdditiveIdentity;
|
||||
DoubleDouble resultInversedOrder = DoubleDouble.AdditiveIdentity - value;
|
||||
|
||||
result.High.ShouldBe(high);
|
||||
result.Low.ShouldBe(low);
|
||||
|
||||
resultInversedOrder.High.ShouldBe(negativeValue.High);
|
||||
resultInversedOrder.Low.ShouldBe(negativeValue.Low);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1.0, 0.0)]
|
||||
[InlineData(10.0, 0.0)]
|
||||
[InlineData(100.0, 0.0)]
|
||||
[InlineData(-1.0, 0.0)]
|
||||
[InlineData(-10.0, 0.0)]
|
||||
[InlineData(-100.0, 0.0)]
|
||||
[InlineData(3.141592653589793, 1.2246467991473532e-16)]
|
||||
[InlineData(2.718281828459045, 1.4456468917292502e-16)]
|
||||
[InlineData(0.6931471805599453, 2.3190468138462996e-17)]
|
||||
public void MultiplicativeIdentityMultiply(double high, double low)
|
||||
{
|
||||
DoubleDouble value = new(high, low);
|
||||
|
||||
@@ -83,4 +164,24 @@ public class DoubleDoubleTests
|
||||
resultInversedOrder.High.ShouldBe(high);
|
||||
resultInversedOrder.Low.ShouldBe(low);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1.0, 0.0)]
|
||||
[InlineData(10.0, 0.0)]
|
||||
[InlineData(100.0, 0.0)]
|
||||
[InlineData(-1.0, 0.0)]
|
||||
[InlineData(-10.0, 0.0)]
|
||||
[InlineData(-100.0, 0.0)]
|
||||
[InlineData(3.141592653589793, 1.2246467991473532e-16)]
|
||||
[InlineData(2.718281828459045, 1.4456468917292502e-16)]
|
||||
[InlineData(0.6931471805599453, 2.3190468138462996e-17)]
|
||||
public void MultiplicativeIdentityDivide(double high, double low)
|
||||
{
|
||||
DoubleDouble value = new(high, low);
|
||||
|
||||
DoubleDouble result = value / DoubleDouble.MultiplicativeIdentity;
|
||||
|
||||
result.High.ShouldBe(high);
|
||||
result.Low.ShouldBe(low);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""Reproduce DoubleDouble's precomputed mathematical constants.
|
||||
|
||||
Run: python3 -B 1-tests/Just.PreciseMath.Tests/ReferenceData/generate_constants.py
|
||||
Standard library only; no dependency on the implementation under test.
|
||||
Decimal evaluates formulas at 160 and 240 digits; Fraction-based splitting
|
||||
rounds the high and then the exact residual to nearest-even binary64.
|
||||
"""
|
||||
|
||||
from decimal import Decimal, localcontext
|
||||
from fractions import Fraction
|
||||
|
||||
from generate_irrational_arithmetic import arctan_inverse, split
|
||||
|
||||
|
||||
def constants(precision: int) -> dict[str, tuple[float, float]]:
|
||||
with localcontext() as context:
|
||||
context.prec = precision
|
||||
one = Decimal(1)
|
||||
pi = 16 * arctan_inverse(5) - 4 * arctan_inverse(239)
|
||||
e = one.exp()
|
||||
ln2 = Decimal(2).ln()
|
||||
ln10 = Decimal(10).ln()
|
||||
sqrt2 = Decimal(2).sqrt()
|
||||
sqrt3 = Decimal(3).sqrt()
|
||||
sqrt5 = Decimal(5).sqrt()
|
||||
sqrt_pi = pi.sqrt()
|
||||
sqrt_tau = (2 * pi).sqrt()
|
||||
values = {
|
||||
"Pi": pi,
|
||||
"E": e,
|
||||
"Ln2": ln2,
|
||||
"Tau": 2 * pi,
|
||||
"PiOver2": pi / 2,
|
||||
"PiOver3": pi / 3,
|
||||
"PiOver4": pi / 4,
|
||||
"PiOver6": pi / 6,
|
||||
"InvPi": one / pi,
|
||||
"InvTau": one / (2 * pi),
|
||||
"DegToRad": pi / 180,
|
||||
"RadToDeg": 180 / pi,
|
||||
"InvE": one / e,
|
||||
"Ln10": ln10,
|
||||
"Log2E": one / ln2,
|
||||
"Log10E": one / ln10,
|
||||
"Log2Of10": ln10 / ln2,
|
||||
"Log10Of2": ln2 / ln10,
|
||||
"Sqrt2": sqrt2,
|
||||
"Sqrt3": sqrt3,
|
||||
"Sqrt5": sqrt5,
|
||||
"InvSqrt2": one / sqrt2,
|
||||
"InvSqrt3": one / sqrt3,
|
||||
"SqrtPi": sqrt_pi,
|
||||
"InvSqrtPi": one / sqrt_pi,
|
||||
"TwoInvSqrtPi": 2 / sqrt_pi,
|
||||
"SqrtTau": sqrt_tau,
|
||||
"InvSqrtTau": one / sqrt_tau,
|
||||
"GoldenRatio": (one + sqrt5) / 2,
|
||||
}
|
||||
pairs = {name: split(Fraction(value)) for name, value in values.items()}
|
||||
for name, value in values.items():
|
||||
high, low = pairs[name]
|
||||
assert high + low == high, f"Unnormalized constant: {name}"
|
||||
assert low != 0.0, f"Lost residual: {name}"
|
||||
assert abs(Fraction(high) + Fraction(low) - Fraction(value)) <= abs(Fraction(value)) / (1 << 105)
|
||||
return pairs
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pairs = constants(160)
|
||||
assert pairs == constants(240), "Increase precision: binary64 constants did not stabilize"
|
||||
print("// Expected test components")
|
||||
for name, (high, low) in pairs.items():
|
||||
print(f' [InlineData("{name}", {high!r}, {low!r})]')
|
||||
print("\n// Precomputed properties")
|
||||
for name, (high, low) in pairs.items():
|
||||
print(f" public static DoubleDouble {name} => new({high!r}, {low!r});")
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Print BasicArithmeticMatchesPrecomputedIrrationalResults InlineData.
|
||||
|
||||
Run with Python 3.11+; standard library only. Decimal supplies irrational
|
||||
constants; Fraction supplies exact arithmetic on their stored binary64 pairs.
|
||||
The test suite consumes the literals, not this script or Python at runtime.
|
||||
"""
|
||||
|
||||
from decimal import Decimal, localcontext
|
||||
from fractions import Fraction
|
||||
|
||||
|
||||
def arctan_inverse(inverse: int) -> Decimal:
|
||||
"""Evaluate atan(1/inverse) using its alternating power series."""
|
||||
x = Decimal(1) / inverse
|
||||
power = x
|
||||
total = x
|
||||
index = 1
|
||||
while True:
|
||||
power *= -(x * x)
|
||||
updated = total + power / (2 * index + 1)
|
||||
if updated == total:
|
||||
return total
|
||||
total = updated
|
||||
index += 1
|
||||
|
||||
|
||||
def split(value: Fraction) -> tuple[float, float]:
|
||||
"""Round high, then the exact residual, to nearest-even binary64."""
|
||||
high = float(value)
|
||||
return high, float(value - Fraction(high))
|
||||
|
||||
|
||||
def fixtures(precision: int) -> list[tuple[str, list[float]]]:
|
||||
with localcontext() as context:
|
||||
context.prec = precision
|
||||
constants = {
|
||||
"pi": 16 * arctan_inverse(5) - 4 * arctan_inverse(239),
|
||||
"e": Decimal(1).exp(),
|
||||
"sqrt2": Decimal(2).sqrt(),
|
||||
"sqrt3": Decimal(3).sqrt(),
|
||||
"ln2": Decimal(2).ln(),
|
||||
"phi": (1 + Decimal(5).sqrt()) / 2,
|
||||
}
|
||||
pairs = {name: split(Fraction(value)) for name, value in constants.items()}
|
||||
|
||||
rows = []
|
||||
for left_name, right_name in [("pi", "e"), ("sqrt2", "sqrt3"), ("ln2", "phi")]:
|
||||
for overload in ["DD/DD", "DD/double", "double/DD"]:
|
||||
left_high, left_low = pairs[left_name]
|
||||
right_high, right_low = pairs[right_name]
|
||||
if overload == "DD/double":
|
||||
right_low = 0.0
|
||||
if overload == "double/DD":
|
||||
left_low = 0.0
|
||||
left = Fraction(left_high) + Fraction(left_low)
|
||||
right = Fraction(right_high) + Fraction(right_low)
|
||||
values = [left_high, left_low, right_high, right_low]
|
||||
for expected in [left + right, left - right, left * right, left / right]:
|
||||
high, low = split(expected)
|
||||
# These precomputed references are much closer than the test's
|
||||
# 2^-100 bound; this does not assert library correct rounding.
|
||||
assert abs(Fraction(high) + Fraction(low) - expected) <= abs(expected) / (1 << 105)
|
||||
values.extend([high, low])
|
||||
rows.append((f"{left_name}, {right_name}: {overload}", values))
|
||||
return rows
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
rows = fixtures(160)
|
||||
assert rows == fixtures(240), "Increase precision: binary64 fixtures did not stabilize"
|
||||
for label, values in rows:
|
||||
print(f" // {label}")
|
||||
overload = label.split(": ")[1]
|
||||
print(f' [InlineData("{overload}",')
|
||||
for index in range(0, len(values), 2):
|
||||
suffix = ")]" if index == len(values) - 2 else ","
|
||||
print(f" {values[index]!r}, {values[index + 1]!r}{suffix}")
|
||||
@@ -16,8 +16,9 @@ for a single `double`, or `DoubleDouble.FromComponents(high, low)` for arbitrary
|
||||
components. The factory normalizes finite sums and canonicalizes NaN/infinity
|
||||
with a positive-zero low component. The two-component constructor is internal
|
||||
and performs no normalization or validation; it is reserved for trusted,
|
||||
already-normalized results. The constants `PI`, `E`, and `LN2` include binary64
|
||||
residuals checked against independently computed high-precision values.
|
||||
already-normalized results. Mathematical constants use precomputed high/low pairs
|
||||
checked against independently computed high-precision values; accessing them does
|
||||
not perform double-double arithmetic or allocate on the heap.
|
||||
|
||||
- Arithmetic: unary `+`/`-`, binary `+`, `-`, `*`, `/`, and both operand orders with
|
||||
a `double`. Addition retains residuals under cancellation; multiplication uses
|
||||
@@ -44,6 +45,39 @@ with exact component checks for selected representable cases. Near underflow,
|
||||
extended precision necessarily decreases; overflow produces infinity. Performance
|
||||
of the allocating exponent-boundary path is not covered by the basic benchmarks.
|
||||
|
||||
## Predefined mathematical constants
|
||||
|
||||
All constants below are static `DoubleDouble` properties. Each stores the nearest
|
||||
binary64 high component followed by the nearest binary64 residual, rather than
|
||||
calculating a ratio, root, or logarithm on access. Names use PascalCase, including
|
||||
`Pi`, `E`, and `Ln2`.
|
||||
|
||||
| Group | Properties and values |
|
||||
|---|---|
|
||||
| Circle and common angles | `Pi` (π), `Tau` (2π), `PiOver2`, `PiOver3`, `PiOver4`, `PiOver6` |
|
||||
| Angular conversion | `DegToRad` (π/180), `RadToDeg` (180/π), `InvPi` (1/π), `InvTau` (1/(2π), radians to turns) |
|
||||
| Exponential and logarithmic | `E`, `InvE` (1/e), `Ln2` (ln 2), `Ln10` (ln 10) |
|
||||
| Log-base conversion | `Log2E` (1/ln 2), `Log10E` (1/ln 10), `Log2Of10` (ln 10/ln 2), `Log10Of2` (ln 2/ln 10) |
|
||||
| Roots and geometry | `Sqrt2`, `Sqrt3`, `Sqrt5`, `InvSqrt2`, `InvSqrt3`, `GoldenRatio` ((1+√5)/2) |
|
||||
| Gaussian and error-function factors | `SqrtPi`, `InvSqrtPi`, `TwoInvSqrtPi` (2/√π), `SqrtTau` (√(2π)), `InvSqrtTau` (1/√(2π)) |
|
||||
|
||||
Multiply by conversion factors instead of recomputing them:
|
||||
|
||||
```csharp
|
||||
using Just.PreciseMath;
|
||||
|
||||
DoubleDouble degrees = new(180.0);
|
||||
DoubleDouble radians = degrees * DoubleDouble.DegToRad;
|
||||
DoubleDouble convertedDegrees = radians * DoubleDouble.RadToDeg;
|
||||
DoubleDouble quarterTurn = DoubleDouble.PiOver2;
|
||||
```
|
||||
|
||||
The factors avoid deriving constants at runtime; the multiplication itself remains
|
||||
approximate DD arithmetic, so conversions are not guaranteed exact round trips.
|
||||
The list is mathematical and dimensionless, not a table of unit-dependent physical
|
||||
constants. Precomputed roots/logarithms do not imply general `Sqrt`/`Log` functions
|
||||
are implemented.
|
||||
|
||||
## Conversions and formatting
|
||||
|
||||
- Explicit conversions support `double`, `float`, `int`, `long`, and `decimal`
|
||||
|
||||
Reference in New Issue
Block a user