Files
Just.PreciseMath/1-tests/Just.PreciseMath.Tests/PreciseMathSqrtTests.cs
T
just beb9a084d0
.NET Test / .NET tests (push) Successful in 1m35s
initial DDMath implementation
2026-09-14 22:25:50 +04:00

162 lines
6.5 KiB
C#

using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests;
public class PreciseMathSqrtTests
{
[Theory]
[InlineData(1.0, 1.0)]
[InlineData(4.0, 2.0)]
[InlineData(9.0, 3.0)]
[InlineData(0.25, 0.5)]
[InlineData(2.25, 1.5)]
public void ExactBinarySquaresHaveExactRoots(double square, double root)
{
DoubleDouble actual = DDMath.Sqrt(new DoubleDouble(square));
actual.High.ShouldBe(root);
BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(0L);
}
[Fact]
public void PowersOfFourHaveExactRootsAcrossTheFiniteRange()
{
// sqrt(2^(2k)) = 2^k exactly, including the minimum subnormal input.
for (int exponent = -1074; exponent <= 1022; exponent += 2)
{
DoubleDouble actual = DDMath.Sqrt(new DoubleDouble(Math.ScaleB(1.0, exponent)));
actual.High.ShouldBe(Math.ScaleB(1.0, exponent / 2));
BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(0L);
}
}
[Theory]
[InlineData(2.0)]
[InlineData(3.0)]
[InlineData(5.0)]
[InlineData(1e-308)]
[InlineData(1e308)]
public void IrrationalRootsRetainMoreThanBinary64Precision(double value)
{
DoubleDouble input = new(value);
DoubleDouble actual = DDMath.Sqrt(input);
actual.Low.ShouldNotBe(0.0);
AssertSqrtBound(input, actual);
}
[Fact]
public void LowComponentsOnEitherSideOfAnExactSquareAffectTheRoot()
{
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.Sqrt(input);
actual.High.ShouldBe(1.0);
(Math.Sign(actual.Low) == Math.Sign(low)).ShouldBeTrue();
AssertSqrtBound(input, actual);
}
}
[Fact]
public void SpecialValuesMatchBinary64AndRemainCanonical()
{
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.Sqrt(new DoubleDouble(value));
double expected = Math.Sqrt(value);
// Construction canonicalizes all NaNs to double.NaN, including
// domain errors; zeros must instead be compared by their sign bits.
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.Sqrt(negative)).ShouldBeTrue();
}
[Fact]
public void PositiveFiniteInputsMeetExactRelativeErrorBound()
{
// Every binary64 exponent, including subnormal input exponents. Test zero,
// dense, and sparse lows of either sign, without relying on DD arithmetic
// to compute the expected square root or its square.
Random random = new(314159);
for (int exponent = -1074; exponent <= 1023; ++exponent)
{
double high = Math.ScaleB(1.0 + random.NextDouble(), exponent);
double low = Math.ScaleB(random.NextDouble(), exponent - 54);
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);
AssertSqrtBound(input, DDMath.Sqrt(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)
{
AssertSqrtBound(input, DDMath.Sqrt(input));
}
}
}
// Pin the tiny-input precision loss described in the optional legacy review,
// and guard against intermediate squaring overflow near the finite limit.
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.ScaleB(1.0, 969)),
DoubleDouble.FromComponents(double.MaxValue, -Math.ScaleB(1.0, 969)),
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))];
foreach (DoubleDouble input in boundaries)
{
AssertSqrtBound(input, DDMath.Sqrt(input));
}
}
private static void AssertSqrtBound(DoubleDouble input, DoubleDouble actual)
{
DoubleDouble.IsFinite(actual).ShouldBeTrue();
DoubleDouble.IsCanonical(actual).ShouldBeTrue();
(actual.High > 0.0).ShouldBeTrue();
// Independent exact rational oracle: x = X*2^-1074, y = Y*2^-1074.
// For positive x and y, |y/sqrt(x) - 1| <= t iff
// x*(1-t)^2 <= y^2 <= x*(1+t)^2. With t=2^-100 we
// cross-multiply to integers: no rounded sqrt, product, or DD conversion.
BigInteger x = (Units(input.High) + Units(input.Low)) << 1074;
BigInteger y = Units(actual.High) + Units(actual.Low);
BigInteger scale = BigInteger.One << 100;
BigInteger squared = (y * y) << 200;
BigInteger lower = x * (scale - 1) * (scale - 1);
BigInteger upper = x * (scale + 1) * (scale + 1);
(squared >= lower && squared <= upper).ShouldBeTrue(
$"Sqrt 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;
}
}