187 lines
7.7 KiB
C#
187 lines
7.7 KiB
C#
using System.Numerics;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace Just.PreciseMath.Tests;
|
|
|
|
public class PreciseMathInvSqrtTests
|
|
{
|
|
[Theory]
|
|
[InlineData(2.0)]
|
|
[InlineData(3.0)]
|
|
[InlineData(5.0)]
|
|
[InlineData(1e-308)]
|
|
[InlineData(1e308)]
|
|
public void IrrationalInverseRootsRetainMoreThanBinary64Precision(double value)
|
|
{
|
|
DoubleDouble input = new(value);
|
|
DoubleDouble actual = DDMath.InvSqrt(input);
|
|
actual.Low.ShouldNotBe(0.0);
|
|
AssertInvSqrtBound(input, actual);
|
|
}
|
|
|
|
[Fact]
|
|
public void SpecialValuesMatchReciprocalSquareRootAndRemainCanonical()
|
|
{
|
|
double[] values = [0.0, -0.0, double.PositiveInfinity, double.NegativeInfinity,
|
|
double.NaN, -1.0, -double.Epsilon, double.MinValue];
|
|
foreach (double value in values)
|
|
{
|
|
DoubleDouble actual = DDMath.InvSqrt(new DoubleDouble(value));
|
|
// Binary64 is an independent oracle only for these special values.
|
|
double expected = 1.0 / Math.Sqrt(value);
|
|
BitConverter.DoubleToInt64Bits(actual.High).ShouldBe(
|
|
BitConverter.DoubleToInt64Bits(double.IsNaN(expected) ? double.NaN : expected));
|
|
BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(0L);
|
|
DoubleDouble.IsCanonical(actual).ShouldBeTrue();
|
|
}
|
|
|
|
DoubleDouble negative = DoubleDouble.FromComponents(-1.0, Math.ScaleB(1.0, -54));
|
|
DoubleDouble.IsNaN(DDMath.InvSqrt(negative)).ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void PowersOfFourHaveExactInverseRootsAcrossTheFiniteRange()
|
|
{
|
|
// 1/sqrt(2^(2k)) = 2^-k exactly, including the minimum subnormal input.
|
|
for (int exponent = -1074; exponent <= 1022; exponent += 2)
|
|
{
|
|
DoubleDouble actual = DDMath.InvSqrt(new DoubleDouble(Math.ScaleB(1.0, exponent)));
|
|
actual.High.ShouldBe(Math.ScaleB(1.0, -(exponent / 2)));
|
|
BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(0L);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void LowComponentsOnEitherSideOfOneAffectTheInverseRoot()
|
|
{
|
|
foreach (double low in new[] { Math.ScaleB(1.0, -54), -Math.ScaleB(1.0, -54) })
|
|
{
|
|
DoubleDouble input = DoubleDouble.FromComponents(1.0, low);
|
|
DoubleDouble actual = DDMath.InvSqrt(input);
|
|
actual.High.ShouldBe(1.0);
|
|
Math.Sign(actual.Low).ShouldBe(-Math.Sign(low));
|
|
AssertInvSqrtBound(input, actual);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void PositiveFiniteInputsMeetExactRelativeErrorBound()
|
|
{
|
|
// Every input exponent, both parities, dense/sparse lows of either sign,
|
|
// and neighbors of binade transitions. The oracle uses the exact stored
|
|
// input sum, including any rounding during public input construction.
|
|
Random random = new(271828);
|
|
for (int exponent = -1074; exponent <= 1023; ++exponent)
|
|
{
|
|
double high = Math.ScaleB(1.0 + random.NextDouble(), exponent);
|
|
double low = Math.ScaleB(random.NextDouble(), exponent - 53);
|
|
double power = Math.ScaleB(1.0, exponent);
|
|
foreach (double boundary in new[] { Math.BitDecrement(power), power, Math.BitIncrement(power) })
|
|
{
|
|
if (boundary > 0.0)
|
|
{
|
|
DoubleDouble input = new(boundary);
|
|
AssertInvSqrtBound(input, DDMath.InvSqrt(input));
|
|
}
|
|
}
|
|
foreach (double residual in new[] { 0.0, low, -low, double.Epsilon, -double.Epsilon })
|
|
{
|
|
DoubleDouble input = DoubleDouble.FromComponents(high, residual);
|
|
if (input.High > 0.0)
|
|
{
|
|
AssertInvSqrtBound(input, DDMath.InvSqrt(input));
|
|
}
|
|
}
|
|
}
|
|
|
|
DoubleDouble[] boundaries = [new(1e-308), new(double.Epsilon),
|
|
new(Math.BitDecrement(Math.ScaleB(1.0, -1022))), new(Math.ScaleB(1.0, -1022)),
|
|
new(Math.BitIncrement(Math.ScaleB(1.0, -1022))), new(double.MaxValue),
|
|
DoubleDouble.FromComponents(double.MaxValue, Math.BitDecrement(Math.ScaleB(1.0, 970))),
|
|
DoubleDouble.FromComponents(double.MaxValue, -Math.ScaleB(1.0, 970)),
|
|
DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -53)),
|
|
DoubleDouble.FromComponents(1.0, -Math.ScaleB(1.0, -54)),
|
|
DoubleDouble.FromComponents(4.0, Math.ScaleB(1.0, -51)),
|
|
DoubleDouble.FromComponents(4.0, -Math.ScaleB(1.0, -52)),
|
|
DoubleDouble.FromComponents(0.0, double.Epsilon)];
|
|
foreach (DoubleDouble input in boundaries)
|
|
{
|
|
AssertInvSqrtBound(input, DDMath.InvSqrt(input));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void DenseLowNormalizationBoundariesMeetTheBound()
|
|
{
|
|
// Exercise adjacent lows at half an ulp, both signs, for each exponent.
|
|
Random random = new(161803);
|
|
for (int exponent = -1074; exponent <= 1023; ++exponent)
|
|
{
|
|
double high = Math.ScaleB(1.0 + random.NextDouble(), exponent);
|
|
double halfUlp = Math.ScaleB(1.0, exponent - 53);
|
|
foreach (double magnitude in new[] { Math.BitDecrement(halfUlp), halfUlp, Math.BitIncrement(halfUlp) })
|
|
{
|
|
foreach (double low in new[] { magnitude, -magnitude })
|
|
{
|
|
DoubleDouble input = DoubleDouble.FromComponents(high, low);
|
|
if (DoubleDouble.IsFinite(input) && input.High > 0.0)
|
|
{
|
|
AssertInvSqrtBound(input, DDMath.InvSqrt(input));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void RepresentableSparseCorrectionsAreNotDiscarded()
|
|
{
|
|
// (1+d)^(-1/2) = 1-d/2+O(d^2). For these dyadic d, the quadratic
|
|
// term is below half an ulp of d/2, even at the minimum subnormal.
|
|
foreach (int exponent in new[] { -100, -500, -1000, -1073 })
|
|
{
|
|
foreach (double sign in new[] { -1.0, 1.0 })
|
|
{
|
|
double low = Math.ScaleB(sign, exponent);
|
|
DoubleDouble actual = DDMath.InvSqrt(DoubleDouble.FromComponents(1.0, low));
|
|
actual.High.ShouldBe(1.0);
|
|
actual.Low.ShouldBe(Math.ScaleB(-sign, exponent - 1));
|
|
DoubleDouble.IsCanonical(actual).ShouldBeTrue();
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void AssertInvSqrtBound(DoubleDouble input, DoubleDouble actual)
|
|
{
|
|
DoubleDouble.IsFinite(actual).ShouldBeTrue();
|
|
DoubleDouble.IsCanonical(actual).ShouldBeTrue();
|
|
(actual.High > 0.0).ShouldBeTrue();
|
|
|
|
// Exact dyadic oracle: x = X*2^-1074, y = Y*2^-1074. For positive
|
|
// x and y, |y/(1/sqrt(x)) - 1| <= t iff (1-t)^2 <= x*y^2 <= (1+t)^2.
|
|
// Cross-multiply with t=2^-100; no DD product, division or rounded root.
|
|
BigInteger x = Units(input.High) + Units(input.Low);
|
|
BigInteger y = Units(actual.High) + Units(actual.Low);
|
|
BigInteger scale = BigInteger.One << 100;
|
|
BigInteger product = (x * y * y) << 200;
|
|
BigInteger lower = ((scale - 1) * (scale - 1)) << 3222;
|
|
BigInteger upper = ((scale + 1) * (scale + 1)) << 3222;
|
|
(product >= lower && product <= upper).ShouldBeTrue(
|
|
$"InvSqrt bound failed for ({input.High:R}, {input.Low:R}): ({actual.High:R}, {actual.Low:R})");
|
|
}
|
|
|
|
private static BigInteger Units(double value)
|
|
{
|
|
long bits = BitConverter.DoubleToInt64Bits(value);
|
|
int exponent = (int)((bits >> 52) & 0x7ff);
|
|
BigInteger significand = bits & 0xfffffffffffffL;
|
|
if (exponent != 0)
|
|
{
|
|
significand += BigInteger.One << 52;
|
|
significand <<= exponent - 1;
|
|
}
|
|
return bits < 0 ? -significand : significand;
|
|
}
|
|
}
|