added sanity checks
.NET Test / .NET tests (push) Successful in 3m55s

This commit is contained in:
2026-09-18 14:10:24 +04:00
parent b5a8bd2580
commit 5797bf4884
44 changed files with 419 additions and 146 deletions
@@ -1,7 +1,3 @@
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests;
public class DoubleDoubleArithmeticTests
@@ -332,6 +328,53 @@ public class DoubleDoubleArithmeticTests
Check(6.0 / new DoubleDouble(2.0), 3.0, 0.0);
}
[Theory]
[InlineData(2.0)]
[InlineData(3.0)]
public void ScalarAdditionAndSubtractionRetainTheMiddleSumResidual(double magnitude)
{
// Exact Fraction arithmetic on the stored E components confirms that
// all signed E +/- 2 and E +/- 3 results fit exactly in two components.
// Compare exact dyadic sums, not another overload or a rounded tolerance.
foreach (double valueSign in new[] { -1.0, 1.0 })
{
foreach (double scalarSign in new[] { -1.0, 1.0 })
{
DoubleDouble value = valueSign * DoubleDouble.E;
double scalar = scalarSign * magnitude;
BigInteger expectedSum = Units(value) + Units(scalar);
BigInteger expectedDifference = Units(value) - Units(scalar);
Units(value + scalar).ShouldBe(expectedSum);
Units(scalar + value).ShouldBe(expectedSum);
Units(value - scalar).ShouldBe(expectedDifference);
Units(scalar - value).ShouldBe(-expectedDifference);
}
}
}
[Theory]
[InlineData("+")]
[InlineData("-")]
public void ScalarSumsAndDifferencesMatchExpansionBitsAcrossRanges(string operation)
{
// Bitwise compatibility; independent exact-rational accuracy checks are separate.
(double[] scalars, DoubleDouble[] values) = ScalarArithmeticCompatibilityCases();
foreach (DoubleDouble value in values)
{
foreach (double scalar in scalars)
{
DoubleDouble expanded = new(scalar);
DoubleDouble expected = operation == "+" ? value + expanded : value - expanded;
DoubleDouble actual = operation == "+" ? value + scalar : value - scalar;
CheckBoundary(actual, expected.High, expected.Low);
DoubleDouble expectedReverse = operation == "+" ? expanded + value : expanded - value;
DoubleDouble actualReverse = operation == "+" ? scalar + value : scalar - value;
CheckBoundary(actualReverse, expectedReverse.High, expectedReverse.Low);
}
}
}
[Fact]
public void ExpansionCancellationRetainsExactComponentsAcrossTheAdditionGuard()
{
@@ -579,6 +622,25 @@ public class DoubleDoubleArithmeticTests
}
}
[Fact]
public void DivisionOverloadsPreserveComponentBitsAcrossRangesAndSpecialValues()
{
// Compatibility, not an accuracy oracle. GeneralArithmeticMeetsExactRationalBound
// separately checks complete results against exact rational bounds.
(double[] scalars, DoubleDouble[] values) = ScalarArithmeticCompatibilityCases();
foreach (DoubleDouble value in values)
{
foreach (double scalar in scalars)
{
DoubleDouble expectedQuotient = value / new DoubleDouble(scalar);
CheckBoundary(value / scalar, expectedQuotient.High, expectedQuotient.Low);
DoubleDouble expectedReverseQuotient = new DoubleDouble(scalar) / value;
CheckBoundary(scalar / value, expectedReverseQuotient.High, expectedReverseQuotient.Low);
}
}
}
[Fact]
public void DivisionRetainsSubnormalCorrectionsWithOrdinaryHighComponents()
{
@@ -597,6 +659,38 @@ public class DoubleDoubleArithmeticTests
}
}
[Theory]
[InlineData(-1000)]
[InlineData(-1021)]
[InlineData(-1022)]
[InlineData(-1023)]
public void ScalarDivisionPreservesSparseCorrectionProductsNearUnderflow(int lowExponent)
{
// At -1000, the second product is normal but its exact residual is
// (962132647665515 / 1073741824) * epsilon, which rounds to 896056 * epsilon.
// Derived from exact binary64 rational products; the other rows bracket
// the correction product's normal/subnormal transition.
foreach (double numeratorSign in new[] { -1.0, 1.0 })
{
foreach (double denominatorSign in new[] { -1.0, 1.0 })
{
foreach (double lowSign in new[] { -1.0, 1.0 })
{
double low = Math.ScaleB(lowSign, lowExponent);
double denominator = denominatorSign * 1.1;
DoubleDouble numerator = DoubleDouble.FromComponents(numeratorSign * 1.1, low);
DoubleDouble actual = numerator / denominator;
DoubleDouble expanded = numerator / new DoubleDouble(denominator);
CheckBoundary(actual, expanded.High, expanded.Low);
// Exact quotient = +/-1 + low/denominator. Its high cannot
// change here; binary64 division independently rounds the low.
CheckBoundary(actual, numeratorSign / denominatorSign, low / denominator);
AssertRelative(actual, Units(numerator) << 1074, Units(denominator));
}
}
}
}
[Fact]
public void SubnormalProductsWithLargeNormalsRetainExactResultsInBothOrders()
{
@@ -884,6 +978,39 @@ public class DoubleDoubleArithmeticTests
}
}
private static (double[] Scalars, DoubleDouble[] Values) ScalarArithmeticCompatibilityCases()
{
List<double> scalars = [0.0, -0.0, 1.0, -1.0, 2.0, -2.0, 3.0, -3.0, 100.0, -100.0,
double.MaxValue, double.MinValue, double.PositiveInfinity,
double.NegativeInfinity, double.NaN];
List<DoubleDouble> values = [DoubleDouble.E, -DoubleDouble.E];
// Include addition/division dispatch edges, exponent extremes, dense/sparse
// lows of either sign, and significands on either side of a binade.
int[] exponents = [-1074, -1022, -451, -450, -1, 0, 1, 450, 451, 1020, 1021, 1023];
double[] significands = [1.0, Math.BitIncrement(1.0), 1.5, Math.BitDecrement(2.0)];
foreach (int exponent in exponents)
{
foreach (double significand in significands)
{
foreach (double sign in new[] { -1.0, 1.0 })
{
double high = Math.ScaleB(sign * significand, exponent);
scalars.Add(high);
double denseLow = Math.ScaleB(1.0, exponent - 54);
foreach (double low in new[] { 0.0, denseLow, -denseLow, double.Epsilon, -double.Epsilon })
{
values.Add(DoubleDouble.FromComponents(high, low));
}
}
}
}
foreach (double scalar in scalars)
{
values.Add(new DoubleDouble(scalar));
}
return (scalars.ToArray(), values.ToArray());
}
private static void AssertDivisionMatchesPrevious(DoubleDouble left, DoubleDouble right)
{
// Freeze the pre-specialization expression. The unchanged public operators