This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace Just.PreciseMath.Tests;
|
||||
|
||||
public class ArithmeticRangeTests
|
||||
{
|
||||
[Fact]
|
||||
public void AdditionRangeMatchesTheOriginalExponentGuard()
|
||||
{
|
||||
// Independent BCL predicate: include both signed zeros, subnormals, and
|
||||
// the first/last significands of every finite exponent field.
|
||||
foreach (double value in FiniteExponentSamples())
|
||||
{
|
||||
bool expected = Math.ILogB(value) <= 1020;
|
||||
PreciseMathHelper.IsAdditionWithinFastRange(value).ShouldBe(expected, $"{value:R}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DivisionRangeMatchesTheOriginalExponentGuard()
|
||||
{
|
||||
// Public operators handle zero before this guard; ILogB(0) is the
|
||||
// int.MinValue sentinel, whose absolute value is not representable.
|
||||
foreach (double value in FiniteExponentSamples())
|
||||
{
|
||||
if (value != 0.0)
|
||||
{
|
||||
bool expected = Math.Abs(Math.ILogB(value)) <= 450;
|
||||
PreciseMathHelper.IsDivisionWithinFastRange(value).ShouldBe(expected, $"{value:R}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultiplicationRangeMatchesEveryNormalExponentPair()
|
||||
{
|
||||
// Exhaust all normal exponent combinations. The original guard depends
|
||||
// only on these exponents, never on the sign or fractional significand.
|
||||
double[] values = new double[2046];
|
||||
for (int i = 0; i < values.Length; ++i)
|
||||
{
|
||||
values[i] = Math.ScaleB(1.0, i - 1022);
|
||||
}
|
||||
foreach (double left in values)
|
||||
{
|
||||
foreach (double right in values)
|
||||
{
|
||||
AssertMultiplicationRange(left, right);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultiplicationRangePreservesSubnormalAndSignedOperandPaths()
|
||||
{
|
||||
// Include all subnormal binades and both significand edges, paired
|
||||
// with large normals that can bring the exponent sum into range.
|
||||
double[] partners = [double.Epsilon, Math.BitDecrement(Math.ScaleB(1.0, -1022)),
|
||||
Math.ScaleB(1.0, -1022), Math.ScaleB(1.0, -901), Math.ScaleB(1.0, -900),
|
||||
Math.ScaleB(1.0, -1), 1.0, 2.0, Math.ScaleB(1.0, 900), Math.ScaleB(1.0, 901),
|
||||
Math.ScaleB(1.0, 1023), double.MaxValue];
|
||||
foreach (double value in FiniteExponentSamples())
|
||||
{
|
||||
if (value == 0.0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
foreach (double partner in partners)
|
||||
{
|
||||
AssertMultiplicationRange(value, partner);
|
||||
AssertMultiplicationRange(partner, value);
|
||||
AssertMultiplicationRange(value, -partner);
|
||||
AssertMultiplicationRange(-partner, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("+")]
|
||||
[InlineData("-")]
|
||||
[InlineData("*")]
|
||||
public void FiniteKernelsPreservePreviousComponentBits(string operation)
|
||||
{
|
||||
// Differential characterization, not an independent accuracy oracle.
|
||||
// Freeze the previous four-TwoSum/FMA expressions and public normalization;
|
||||
// independent exact/rational accuracy cases remain in the arithmetic suites.
|
||||
List<DoubleDouble> values = [new(0.0), new(-0.0)];
|
||||
int[] exponents = [-1074, -1022, -901, -900, -899, -451, -450, -1, 0, 1,
|
||||
450, 451, 899, 900, 901, 1020, 1021, 1023];
|
||||
foreach (int exponent in exponents)
|
||||
{
|
||||
foreach (double significand in new[] { 1.0, Math.BitIncrement(1.0), 1.5, 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, -53), -Math.ScaleB(high, -53),
|
||||
double.Epsilon, -double.Epsilon })
|
||||
{
|
||||
DoubleDouble value = DoubleDouble.FromComponents(high, low);
|
||||
if (DoubleDouble.IsFinite(value))
|
||||
{
|
||||
values.Add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (DoubleDouble left in values)
|
||||
{
|
||||
foreach (DoubleDouble right in values)
|
||||
{
|
||||
DoubleDouble expected;
|
||||
DoubleDouble actual;
|
||||
if (operation == "*")
|
||||
{
|
||||
if (left.High == 0.0 || right.High == 0.0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
int exponent = Math.ILogB(left.High) + Math.ILogB(right.High);
|
||||
if (exponent < -900 || exponent > 900)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
(double product, double error) = PreciseMathHelper.TwoMultiply(left.High, right.High);
|
||||
error = Math.FusedMultiplyAdd(left.High, right.Low, error);
|
||||
error = Math.FusedMultiplyAdd(left.Low, right.High, error);
|
||||
error = Math.FusedMultiplyAdd(left.Low, right.Low, error);
|
||||
Math.Abs(error).ShouldBeLessThan(Math.Abs(product));
|
||||
expected = DoubleDouble.FromComponents(product, error);
|
||||
actual = left * right;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Math.ILogB(left.High) > 1020 || Math.ILogB(right.High) > 1020)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
DoubleDouble operand = operation == "-" ? -right : right;
|
||||
if (left.High == 0.0 && operand.High == 0.0)
|
||||
{
|
||||
expected = new DoubleDouble(left.High + operand.High);
|
||||
}
|
||||
else
|
||||
{
|
||||
(double high, double highError) = PreciseMathHelper.TwoAdd(left.High, operand.High);
|
||||
(double low, double lowError) = PreciseMathHelper.TwoAdd(left.Low, operand.Low);
|
||||
(double middle, double middleError) = PreciseMathHelper.TwoAdd(highError, low);
|
||||
(double sum, double sumError) = PreciseMathHelper.TwoAdd(high, middle);
|
||||
double correction = sumError + (middleError + lowError);
|
||||
double.IsFinite(sum + correction).ShouldBeTrue();
|
||||
expected = DoubleDouble.FromComponents(sum, correction);
|
||||
}
|
||||
actual = operation == "-" ? left - right : left + right;
|
||||
}
|
||||
BitConverter.DoubleToInt64Bits(actual.High).ShouldBe(BitConverter.DoubleToInt64Bits(expected.High));
|
||||
BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(BitConverter.DoubleToInt64Bits(expected.Low));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertMultiplicationRange(double left, double right)
|
||||
{
|
||||
int exponent = Math.ILogB(left) + Math.ILogB(right);
|
||||
bool expected = exponent >= -900 && exponent <= 900;
|
||||
bool actual = PreciseMathHelper.IsMultiplicationWithinFastRange(left, right);
|
||||
// Only format diagnostics on failure in this exhaustive matrix.
|
||||
if (actual != expected)
|
||||
{
|
||||
actual.ShouldBe(expected, $"({left:R}, {right:R})");
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<double> FiniteExponentSamples()
|
||||
{
|
||||
ulong[] fractions = [0, 1, 0x0008_0000_0000_0000, 0x000f_ffff_ffff_ffff];
|
||||
foreach (ulong sign in new[] { 0UL, 0x8000_0000_0000_0000UL })
|
||||
{
|
||||
for (int bit = 0; bit < 52; ++bit)
|
||||
{
|
||||
yield return BitConverter.UInt64BitsToDouble(sign | (1UL << bit));
|
||||
yield return BitConverter.UInt64BitsToDouble(sign | ((1UL << (bit + 1)) - 1));
|
||||
}
|
||||
for (ulong exponent = 0; exponent < 0x7ff; ++exponent)
|
||||
{
|
||||
foreach (ulong fraction in fractions)
|
||||
{
|
||||
yield return BitConverter.UInt64BitsToDouble(sign | (exponent << 52) | fraction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,53 @@ public class DoubleDoubleArithmeticTests
|
||||
Check(6.0 / new DoubleDouble(2.0), 3.0, 0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExpansionCancellationRetainsExactComponentsAcrossTheAdditionGuard()
|
||||
{
|
||||
// (2^e + 2^(e-54)) - (2^e - 2^(e-108)) is exactly the
|
||||
// normalized pair (2^(e-54), 2^(e-108)). The smallest residual
|
||||
// is epsilon; the largest case exercises the boundary fallback.
|
||||
foreach (int exponent in new[] { -966, -450, 0, 450, 1020, 1021 })
|
||||
{
|
||||
foreach (double sign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
double high = sign * Math.ScaleB(1.0, exponent);
|
||||
double small = sign * Math.ScaleB(1.0, exponent - 54);
|
||||
double tiny = sign * Math.ScaleB(1.0, exponent - 108);
|
||||
DoubleDouble left = DoubleDouble.FromComponents(high, small);
|
||||
DoubleDouble right = DoubleDouble.FromComponents(high, -tiny);
|
||||
CheckBoundary(left - right, small, tiny);
|
||||
CheckBoundary(right - left, -small, -tiny);
|
||||
CheckBoundary(left + (-right), small, tiny);
|
||||
CheckBoundary((-right) + left, small, tiny);
|
||||
CheckBoundary(left - left, 0.0, 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExpansionSubtractionHandlesSpecialValuesAlongsideNonzeroResiduals()
|
||||
{
|
||||
foreach (double sign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
DoubleDouble value = DoubleDouble.FromComponents(sign, sign * double.Epsilon);
|
||||
foreach (double special in new[] { 0.0, -0.0, double.NegativeInfinity, double.PositiveInfinity, double.NaN })
|
||||
{
|
||||
DoubleDouble other = new(special);
|
||||
if (special == 0.0)
|
||||
{
|
||||
CheckBoundary(value - other, value.High, value.Low);
|
||||
CheckBoundary(other - value, -value.High, -value.Low);
|
||||
}
|
||||
else
|
||||
{
|
||||
CheckBits(value - other, sign - special);
|
||||
CheckBits(other - value, special - sign);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScalarLeftSubtractionAppliesTheRequestedOperandOrder()
|
||||
{
|
||||
@@ -135,6 +182,41 @@ public class DoubleDoubleArithmeticTests
|
||||
Check(scalar * value, high, low);
|
||||
Check(value * (-scalar), -high, -low);
|
||||
Check((-scalar) * value, -high, -low);
|
||||
CheckBoundary(value * new DoubleDouble(scalar), high, low);
|
||||
CheckBoundary(new DoubleDouble(scalar) * value, high, low);
|
||||
CheckBoundary(value * new DoubleDouble(-scalar), -high, -low);
|
||||
CheckBoundary(new DoubleDouble(-scalar) * value, -high, -low);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExpansionProductRetainsTheLowLowTermAtFastRangeEndpoints()
|
||||
{
|
||||
// (1 + 2^-53)(1 - 2^-54) = 1 + 2^-54 - 2^-107 exactly.
|
||||
// Its residual is BitDecrement(2^-54); omitting low*low loses that bit.
|
||||
// Power-of-two scaling keeps both expected components representable,
|
||||
// including exponent sums at each inclusive fast-path endpoint.
|
||||
foreach (int leftExponent in new[] { -450, 0, 450 })
|
||||
{
|
||||
foreach (int rightExponent 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, leftExponent),
|
||||
leftSign * Math.ScaleB(1.0, leftExponent - 53));
|
||||
DoubleDouble right = DoubleDouble.FromComponents(rightSign * Math.ScaleB(1.0, rightExponent),
|
||||
-rightSign * Math.ScaleB(1.0, rightExponent - 54));
|
||||
int exponent = leftExponent + rightExponent;
|
||||
double sign = leftSign * rightSign;
|
||||
double high = sign * Math.ScaleB(1.0, exponent);
|
||||
double low = sign * Math.ScaleB(Math.BitDecrement(Math.ScaleB(1.0, -54)), exponent);
|
||||
CheckBoundary(left * right, high, low);
|
||||
CheckBoundary(right * left, high, low);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -150,6 +232,47 @@ public class DoubleDoubleArithmeticTests
|
||||
AssertRelative(-1.0 / value, -numerator, Units(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DivisionRetainsSubnormalCorrectionsWithOrdinaryHighComponents()
|
||||
{
|
||||
// Dividing (1 + epsilon) by +/-1 is exact. The outer division is
|
||||
// ordinary, but its second residual product can use the boundary path.
|
||||
foreach (double sign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
DoubleDouble numerator = DoubleDouble.FromComponents(sign, sign * double.Epsilon);
|
||||
foreach (double denominator in new[] { -1.0, 1.0 })
|
||||
{
|
||||
double high = sign / denominator;
|
||||
double low = high * double.Epsilon;
|
||||
CheckBoundary(numerator / new DoubleDouble(denominator), high, low);
|
||||
CheckBoundary(numerator / denominator, high, low);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubnormalProductsWithLargeNormalsRetainExactResultsInBothOrders()
|
||||
{
|
||||
// 2^-1074 * 2^1023 = 2^-51 exactly, despite the subnormal input.
|
||||
foreach (double leftSign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
foreach (double rightSign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
double tiny = leftSign * double.Epsilon;
|
||||
double large = rightSign * Math.ScaleB(1.0, 1023);
|
||||
double expected = (leftSign * rightSign) * Math.ScaleB(1.0, -51);
|
||||
DoubleDouble left = new(tiny);
|
||||
DoubleDouble right = new(large);
|
||||
CheckBoundary(left * right, expected, 0.0);
|
||||
CheckBoundary(right * left, expected, 0.0);
|
||||
CheckBoundary(left * large, expected, 0.0);
|
||||
CheckBoundary(large * left, expected, 0.0);
|
||||
CheckBoundary(tiny * right, expected, 0.0);
|
||||
CheckBoundary(right * tiny, expected, 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExtremeFiniteOperationsDoNotOverflowIntermediates()
|
||||
{
|
||||
@@ -170,6 +293,29 @@ public class DoubleDoubleArithmeticTests
|
||||
Check(below + Math.ScaleB(1.0, 969), double.MaxValue, 0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubnormalHighProductsRetainANormalPartnersResidual()
|
||||
{
|
||||
// (2^-1074, 0) * (2^1023, 2^969) = (2^-51, 2^-105), exactly.
|
||||
// The high product is ordinary despite the subnormal input high.
|
||||
foreach (double leftSign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
foreach (double rightSign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
double scalar = leftSign * double.Epsilon;
|
||||
DoubleDouble tiny = new(scalar);
|
||||
DoubleDouble large = DoubleDouble.FromComponents(rightSign * Math.ScaleB(1.0, 1023),
|
||||
rightSign * Math.ScaleB(1.0, 969));
|
||||
double high = (leftSign * rightSign) * Math.ScaleB(1.0, -51);
|
||||
double low = (leftSign * rightSign) * Math.ScaleB(1.0, -105);
|
||||
CheckBoundary(tiny * large, high, low);
|
||||
CheckBoundary(large * tiny, high, low);
|
||||
CheckBoundary(scalar * large, high, low);
|
||||
CheckBoundary(large * scalar, high, low);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1.0)]
|
||||
[InlineData(-1.0)]
|
||||
|
||||
@@ -6,6 +6,60 @@ namespace Just.PreciseMath.Tests;
|
||||
|
||||
public class DoubleDoubleBoundaryTests
|
||||
{
|
||||
[Fact]
|
||||
public void BoundarySubtractionPreservesComponentsOfTheOriginalExpression()
|
||||
{
|
||||
DoubleDouble[] magnitudes =
|
||||
[
|
||||
new(0.0), new(double.Epsilon), new(1.0),
|
||||
new(Math.BitDecrement(Math.ScaleB(1.0, 1021))),
|
||||
new(Math.ScaleB(1.0, 1021)),
|
||||
DoubleDouble.FromComponents(Math.ScaleB(1.0, 1021), double.Epsilon),
|
||||
DoubleDouble.FromComponents(Math.ScaleB(1.0, 1021), Math.ScaleB(1.0, 967)),
|
||||
new(double.MaxValue),
|
||||
DoubleDouble.FromComponents(double.MaxValue, Math.ScaleB(1.0, 969)),
|
||||
DoubleDouble.FromComponents(double.MaxValue, -Math.ScaleB(1.0, 969))
|
||||
];
|
||||
foreach (DoubleDouble leftMagnitude in magnitudes)
|
||||
{
|
||||
foreach (DoubleDouble rightMagnitude in magnitudes)
|
||||
{
|
||||
// Use the original exponent-domain rule, not the production predicate.
|
||||
if (Math.ILogB(leftMagnitude.High) <= 1020 && Math.ILogB(rightMagnitude.High) <= 1020)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
foreach (double leftSign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
foreach (double rightSign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
DoubleDouble left = leftSign < 0.0 ? -leftMagnitude : leftMagnitude;
|
||||
DoubleDouble right = rightSign < 0.0 ? -rightMagnitude : rightMagnitude;
|
||||
DoubleDouble actual = left - right;
|
||||
// Bitwise characterization of the expression being extracted,
|
||||
// supplemented below by the independent exact-rational oracle.
|
||||
DoubleDouble original = PreciseMathHelper.AddBoundary(left, -right);
|
||||
string context = Describe(left, right, "-");
|
||||
BitConverter.DoubleToInt64Bits(actual.High).ShouldBe(BitConverter.DoubleToInt64Bits(original.High), context);
|
||||
BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(BitConverter.DoubleToInt64Bits(original.Low), context);
|
||||
Rational expected = Exact(left) - Exact(right);
|
||||
if (BelowOverflowMidpoint(expected))
|
||||
{
|
||||
AssertAccurate(actual, expected, context);
|
||||
AssertNormalized(actual);
|
||||
}
|
||||
else
|
||||
{
|
||||
actual.High.ShouldBe(expected.CompareTo(Exact(0.0)) < 0
|
||||
? double.NegativeInfinity : double.PositiveInfinity, context);
|
||||
BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(0L, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1.0, "+")]
|
||||
[InlineData(-1.0, "+")]
|
||||
|
||||
@@ -89,6 +89,38 @@ public class DoubleDoubleRepresentationTests
|
||||
(value != DoubleDouble.NaN).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FiniteNormalizationPreservesOrderingResidualsAndZeroSigns()
|
||||
{
|
||||
// Exact dyadic sums, independent of the public factory. Include both
|
||||
// magnitude orders, cancellation, subnormals, and signed zero lows.
|
||||
double unit = Math.ScaleB(1.0, 970);
|
||||
(double High, double Low, double ExpectedHigh, double ExpectedLow)[] cases =
|
||||
[
|
||||
(-0.0, 0.0, -0.0, 0.0), (-0.0, -0.0, -0.0, 0.0),
|
||||
(0.0, -0.0, 0.0, 0.0), (1.0, -1.0, 0.0, 0.0),
|
||||
(-1.0, 1.0, 0.0, 0.0), (double.Epsilon, -double.Epsilon, 0.0, 0.0),
|
||||
(double.Epsilon, double.Epsilon, 2 * double.Epsilon, 0.0),
|
||||
(0.0, double.Epsilon, double.Epsilon, 0.0),
|
||||
(double.Epsilon, -1.0, -1.0, double.Epsilon),
|
||||
(1.0, double.Epsilon, 1.0, double.Epsilon),
|
||||
(double.Epsilon, 1.0, 1.0, double.Epsilon),
|
||||
(-1.0, -double.Epsilon, -1.0, -double.Epsilon),
|
||||
(-double.Epsilon, -1.0, -1.0, -double.Epsilon),
|
||||
(double.MaxValue, 0.0, double.MaxValue, 0.0),
|
||||
// MaxValue - 3*2^970 = (MaxValue - 2^971) - 2^970.
|
||||
// An unordered TwoSum would overflow an intermediate in the reversed case.
|
||||
(double.MaxValue, -3.0 * unit, Math.BitDecrement(double.MaxValue), -unit),
|
||||
(-3.0 * unit, double.MaxValue, Math.BitDecrement(double.MaxValue), -unit)
|
||||
];
|
||||
foreach ((double high, double low, double expectedHigh, double expectedLow) in cases)
|
||||
{
|
||||
DoubleDouble actual = PreciseMathHelper.NormalizeFinite(high, low);
|
||||
BitConverter.DoubleToInt64Bits(actual.High).ShouldBe(BitConverter.DoubleToInt64Bits(expectedHigh));
|
||||
BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(BitConverter.DoubleToInt64Bits(expectedLow));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZeroSignsArePreservedButEqual()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user