285 lines
13 KiB
C#
285 lines
13 KiB
C#
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("*")]
|
|
[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
|
|
|| 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)
|
|
{
|
|
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));
|
|
}
|
|
}
|
|
}
|
|
|
|
[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);
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|