add precomputed mathematical constants and specialize the second division residual
.NET Test / .NET tests (push) Successful in 2m23s

This commit is contained in:
2026-09-14 21:21:48 +04:00
parent 08036a31d5
commit 4efecbb1bc
11 changed files with 875 additions and 24 deletions
@@ -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}")