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