Files
Just.PreciseMath/1-tests/Just.PreciseMath.Tests/DoubleDoubleHypotTests.cs
T
just 5797bf4884
.NET Test / .NET tests (push) Successful in 3m55s
added sanity checks
2026-09-18 14:10:24 +04:00

346 lines
16 KiB
C#

namespace Just.PreciseMath.Tests;
public class DoubleDoubleHypotTests
{
[Fact]
public void SpecialValuesAndZeroOperandsFollowTheContract()
{
double[] values = [0.0, -0.0, 1.0, -1.0, double.PositiveInfinity, double.NegativeInfinity, double.NaN];
foreach (double x in values)
{
foreach (double y in values)
{
if (!double.IsFinite(x) || !double.IsFinite(y) || x == 0.0 || y == 0.0)
{
AssertBits(new DoubleDouble(double.Hypot(x, y)), DoubleDouble.Hypot(new DoubleDouble(x), new DoubleDouble(y)));
}
}
}
DoubleDouble sparse = DoubleDouble.FromComponents(double.MaxValue, double.Epsilon);
AssertBits(sparse, DoubleDouble.Hypot(-sparse, DoubleDouble.NegativeZero));
AssertBits(sparse, DoubleDouble.Hypot(DoubleDouble.Zero, -sparse));
AssertBits(DoubleDouble.PositiveInfinity, DoubleDouble.Hypot(new DoubleDouble(double.MaxValue), new DoubleDouble(double.MaxValue)));
}
[Fact]
public void ScaledPythagoreanTriplesAreExact()
{
for (int exponent = -1074; exponent <= 1021; ++exponent)
{
DoubleDouble x = new(Math.ScaleB(3.0, exponent));
DoubleDouble y = new(Math.ScaleB(4.0, exponent));
DoubleDouble expected = new(Math.ScaleB(5.0, exponent));
AssertBits(expected, DoubleDouble.Hypot(x, y));
AssertBits(expected, DoubleDouble.Hypot(-y, -x));
}
}
[Fact]
public void FiniteResultsMeetExactSquaredBoundAcrossTheRange()
{
Random random = new(123456);
for (int exponent = -1074; exponent <= 1023; ++exponent)
{
double high = Math.ScaleB(1.0 + (random.NextDouble() * 0.1), exponent);
foreach (int gap in new[] { 0, 1, 26, 54, 55, 500, 1100 })
{
double second = Math.ScaleB(1.0, exponent - gap);
foreach (double sign in new[] { -1.0, 1.0 })
{
DoubleDouble x = DoubleDouble.FromComponents(high, Math.ScaleB(sign, exponent - 54));
DoubleDouble y = DoubleDouble.FromComponents(second, Math.ScaleB(-sign, exponent - gap - 54));
DoubleDouble result = DoubleDouble.Hypot(x, y);
AssertBound(x, y, result);
AssertBits(result, DoubleDouble.Hypot(-y, x));
AssertBits(result, DDMath.Hypot(x, y));
}
}
}
DoubleDouble maximum = new(double.MaxValue);
AssertBound(maximum, new DoubleDouble(Math.ScaleB(1.0, 995)), DoubleDouble.Hypot(maximum, new DoubleDouble(Math.ScaleB(1.0, 995))));
}
[Fact]
public void WidelySeparatedOperandsRetainRepresentableCorrections()
{
// sqrt(a^2+b^2) = a+b^2/(2a)+O(b^4/a^3). These exact dyadics
// put the omitted term below half an ulp of the expected low.
foreach ((int large, int small) in new[] { (0, -100), (1000, 450), (-500, -600) })
{
DoubleDouble result = DoubleDouble.Hypot(new DoubleDouble(Math.ScaleB(1.0, large)), new DoubleDouble(Math.ScaleB(1.0, small)));
AssertBits(DoubleDouble.FromComponents(Math.ScaleB(1.0, large), Math.ScaleB(1.0, (2 * small) - large - 1)), result);
}
}
[Fact]
public void OverflowClassificationUsesTheExactInputSquares()
{
DoubleDouble x = DoubleDouble.FromComponents(double.MaxValue, Math.BitDecrement(Math.ScaleB(1.0, 970)));
double boundaryY = Math.ScaleB(1.0, 971);
// T=2^1024-2^970 is the binary64 overflow midpoint. For the lower y,
// T²-x²-y² = 3*2^1888-5*2^1834 > 0, though the scaled root can round
// up to T before rescaling. The adjacent boundaryY is above the threshold.
foreach (double y in new[] { Math.BitDecrement(boundaryY), boundaryY, Math.BitIncrement(boundaryY) })
{
AssertOverflowBoundary(x, new DoubleDouble(y));
}
}
[Fact]
public void ExactOverflowMidpointAndAdjacentLowsAreClassified()
{
// 55²+48²=73² and 73 divides 2^54-1, so these scaled integer legs
// have hypotenuse exactly T=(2^54-1)*2^970. Split integer components
// before scaling; no DD arithmetic supplies the expected threshold.
const long factor = ((1L << 54) - 1) / 73;
const long first = 55 * factor;
const long second = 48 * factor;
DoubleDouble x = DoubleDouble.FromComponents(Math.ScaleB((double)first, 970), Math.ScaleB(first - (long)(double)first, 970));
DoubleDouble y = DoubleDouble.FromComponents(Math.ScaleB((double)second, 970), Math.ScaleB(second - (long)(double)second, 970));
BigInteger threshold = Units(double.MaxValue) + Units(Math.ScaleB(1.0, 970));
BigInteger a = Units(x.High) + Units(x.Low);
BigInteger b = Units(y.High) + Units(y.Low);
((a * a) + (b * b)).ShouldBe(threshold * threshold);
foreach (double low in new[] { Math.BitDecrement(x.Low), x.Low, Math.BitIncrement(x.Low) })
{
AssertOverflowBoundary(DoubleDouble.FromComponents(x.High, low), y);
}
}
[Fact]
public void ThreeDimensionalSpecialValuesAndZeroReductionFollowTheContract()
{
double[] values = [0.0, -0.0, 1.0, -1.0, double.PositiveInfinity, double.NegativeInfinity, double.NaN];
foreach (double x in values)
{
foreach (double y in values)
{
foreach (double z in values)
{
DoubleDouble result = DoubleDouble.Hypot(new DoubleDouble(x), new DoubleDouble(y), new DoubleDouble(z));
if (double.IsInfinity(x) || double.IsInfinity(y) || double.IsInfinity(z))
{
AssertBits(DoubleDouble.PositiveInfinity, result);
}
else if (double.IsNaN(x) || double.IsNaN(y) || double.IsNaN(z))
{
AssertBits(DoubleDouble.NaN, result);
}
else if (x == 0.0 && y == 0.0 && z == 0.0)
{
AssertBits(DoubleDouble.Zero, result);
}
else
{
AssertBound(new DoubleDouble(x), new DoubleDouble(y), new DoubleDouble(z), result);
}
AssertBits(result, DDMath.Hypot(new DoubleDouble(x), new DoubleDouble(y), new DoubleDouble(z)));
}
}
}
DoubleDouble[] finite = [new(double.Epsilon), new(double.MaxValue), new(2.0),
DoubleDouble.FromComponents(1.0, double.Epsilon)];
foreach (DoubleDouble x in finite)
{
foreach (DoubleDouble y in finite)
{
DoubleDouble expected = DoubleDouble.Hypot(x, y);
AssertBits(expected, DoubleDouble.Hypot(x, y, DoubleDouble.NegativeZero));
AssertBits(expected, DoubleDouble.Hypot(x, DoubleDouble.Zero, y));
AssertBits(expected, DoubleDouble.Hypot(DoubleDouble.NegativeZero, x, y));
}
}
}
[Fact]
public void ThreeDimensionalExactBinaryNormsCoverTheExponentRange()
{
// 1²+2²+2²=3²; all scaled inputs and outputs here are exact dyadics.
for (int exponent = -1074; exponent <= 1021; ++exponent)
{
DoubleDouble x = new(Math.ScaleB(1.0, exponent));
DoubleDouble y = new(Math.ScaleB(2.0, exponent));
DoubleDouble expected = new(Math.ScaleB(3.0, exponent));
AssertBits(expected, DoubleDouble.Hypot(x, y, y));
AssertBits(expected, DoubleDouble.Hypot(-y, x, -y));
}
AssertBits(new DoubleDouble(7.0), DoubleDouble.Hypot(new DoubleDouble(2.0), new DoubleDouble(3.0), new DoubleDouble(6.0)));
AssertBits(new DoubleDouble(13.0), DoubleDouble.Hypot(new DoubleDouble(3.0), new DoubleDouble(4.0), new DoubleDouble(12.0)));
}
[Fact]
public void ThreeDimensionalFiniteResultsMeetExactSquaredBound()
{
Random random = new(314265);
for (int exponent = -1074; exponent <= 1023; ++exponent)
{
double high = Math.ScaleB(1.0 + (random.NextDouble() * 0.1), exponent);
foreach (int gap in new[] { 0, 1, 26, 54, 55, 500, 1100 })
{
foreach (double sign in new[] { -1.0, 1.0 })
{
DoubleDouble x = DoubleDouble.FromComponents(high, Math.ScaleB(sign, exponent - 54));
DoubleDouble y = DoubleDouble.FromComponents(Math.ScaleB(1.0, exponent - gap), Math.ScaleB(-sign, exponent - gap - 54));
DoubleDouble z = DoubleDouble.FromComponents(Math.ScaleB(0.75, exponent - gap), Math.ScaleB(sign, exponent - gap - 55));
DoubleDouble result = DoubleDouble.Hypot(x, y, z);
AssertBound(x, y, z, result);
AssertBits(result, DoubleDouble.Hypot(-z, x, -y));
AssertBits(result, DoubleDouble.Hypot(y, -z, x));
AssertBits(result, DDMath.Hypot(x, y, z));
}
}
}
}
[Fact]
public void ThreeDimensionalSparseCorrectionsAreCombinedBeforeFinalRounding()
{
// sqrt(a²+2b²) = a+b²/a+O(b^4/a^3). The omitted term is below
// half an ulp of the expected low in these exact dyadic cases.
foreach ((int large, int small) in new[] { (0, -100), (1000, 450), (-500, -600), (0, -537), (-500, -787) })
{
DoubleDouble x = new(Math.ScaleB(1.0, large));
DoubleDouble y = new(Math.ScaleB(1.0, small));
DoubleDouble expected = DoubleDouble.FromComponents(x.High, Math.ScaleB(1.0, (2 * small) - large));
AssertThreeDimensionalPermutations(x, y, y, expected);
}
DoubleDouble sparse = DoubleDouble.FromComponents(Math.ScaleB(1.0, 1000), double.Epsilon);
AssertThreeDimensionalPermutations(sparse, DoubleDouble.One, DoubleDouble.One,
DoubleDouble.FromComponents(sparse.High, Math.ScaleB(1.0, -1000)));
}
[Fact]
public void ThreeDimensionalOverflowUsesAllExactInputSquares()
{
// The third coordinate changes a finite two-coordinate norm to overflow.
DoubleDouble value = new(Math.ScaleB(1.25, 1023));
DoubleDouble.IsFinite(DoubleDouble.Hypot(value, value)).ShouldBeTrue();
AssertThreeDimensionalOverflowBoundary(value, value, value);
AssertThreeDimensionalOverflowBoundary(new DoubleDouble(double.MaxValue), value, DoubleDouble.One);
DoubleDouble x = DoubleDouble.FromComponents(double.MaxValue, Math.BitDecrement(Math.ScaleB(1.0, 970)));
double boundaryY = Math.ScaleB(1.0, 971);
foreach (double y in new[] { Math.BitDecrement(boundaryY), boundaryY, Math.BitIncrement(boundaryY) })
{
AssertThreeDimensionalOverflowBoundary(x, new DoubleDouble(y), DoubleDouble.One);
}
// 1²+2²+2²=3², and 3 divides 2^54-1. These exact scaled integers
// give norm T=(2^54-1)*2^970. Even an epsilon low on the first
// coordinate decides which side of the exact overflow midpoint we are on.
const long factor = ((1L << 54) - 1) / 3;
DoubleDouble first = new(Math.ScaleB((double)factor, 970));
DoubleDouble second = new(Math.ScaleB((double)(2 * factor), 970));
BigInteger a = Units(first.High);
BigInteger b = Units(second.High);
BigInteger threshold = Units(double.MaxValue) + Units(Math.ScaleB(1.0, 970));
((a * a) + (2 * b * b)).ShouldBe(threshold * threshold);
foreach (double low in new[] { -double.Epsilon, 0.0, double.Epsilon })
{
AssertThreeDimensionalOverflowBoundary(DoubleDouble.FromComponents(first.High, low), second, second);
}
}
private static void AssertThreeDimensionalOverflowBoundary(DoubleDouble x, DoubleDouble y, DoubleDouble z)
{
BigInteger a = Units(x.High) + Units(x.Low);
BigInteger b = Units(y.High) + Units(y.Low);
BigInteger c = Units(z.High) + Units(z.Low);
BigInteger threshold = Units(double.MaxValue) + Units(Math.ScaleB(1.0, 970));
bool overflow = ((a * a) + (b * b) + (c * c)) >= threshold * threshold;
DoubleDouble result = DoubleDouble.Hypot(x, y, z);
DoubleDouble.IsPositiveInfinity(result).ShouldBe(overflow);
if (!overflow)
{
AssertBound(x, y, z, result);
}
AssertThreeDimensionalPermutations(x, y, z, result);
}
private static void AssertThreeDimensionalPermutations(DoubleDouble x, DoubleDouble y, DoubleDouble z, DoubleDouble expected)
{
foreach (DoubleDouble a in new[] { x, -x })
{
foreach (DoubleDouble b in new[] { y, -y })
{
foreach (DoubleDouble c in new[] { z, -z })
{
AssertBits(expected, DoubleDouble.Hypot(a, b, c));
AssertBits(expected, DoubleDouble.Hypot(a, c, b));
AssertBits(expected, DoubleDouble.Hypot(b, a, c));
AssertBits(expected, DoubleDouble.Hypot(b, c, a));
AssertBits(expected, DoubleDouble.Hypot(c, a, b));
AssertBits(expected, DoubleDouble.Hypot(c, b, a));
AssertBits(expected, DDMath.Hypot(a, b, c));
}
}
}
}
private static void AssertOverflowBoundary(DoubleDouble x, DoubleDouble y)
{
BigInteger a = Units(x.High) + Units(x.Low);
BigInteger b = Units(y.High) + Units(y.Low);
BigInteger threshold = Units(double.MaxValue) + Units(Math.ScaleB(1.0, 970));
bool overflow = ((a * a) + (b * b)) >= threshold * threshold;
foreach (DoubleDouble first in new[] { x, -x })
{
foreach (DoubleDouble second in new[] { y, -y })
{
DoubleDouble result = DoubleDouble.Hypot(first, second);
DoubleDouble.IsPositiveInfinity(result).ShouldBe(overflow);
if (!overflow)
{
AssertBound(first, second, result);
}
AssertBits(result, DoubleDouble.Hypot(second, first));
AssertBits(result, DDMath.Hypot(first, second));
}
}
}
private static void AssertBound(DoubleDouble x, DoubleDouble y, DoubleDouble actual)
{
AssertBound(x, y, DoubleDouble.Zero, actual);
}
private static void AssertBound(DoubleDouble x, DoubleDouble y, DoubleDouble z, DoubleDouble actual)
{
DoubleDouble.IsFinite(actual).ShouldBeTrue();
DoubleDouble.IsCanonical(actual).ShouldBeTrue();
double.IsNegative(actual.High).ShouldBeFalse();
// Exact integer units of 2^-1074. The squared inequalities are equivalent
// to |actual/sqrt(x*x+y*y+z*z)-1| <= 2^-100, allowing one subnormal unit.
BigInteger a = Units(x.High) + Units(x.Low);
BigInteger b = Units(y.High) + Units(y.Low);
BigInteger c = Units(z.High) + Units(z.Low);
BigInteger r = Units(actual.High) + Units(actual.Low);
BigInteger sum = (a * a) + (b * b) + (c * c);
BigInteger scale = BigInteger.One << 100;
BigInteger lower = BigInteger.Max(BigInteger.Zero, r - 1) * scale;
BigInteger upper = (r + 1) * scale;
((lower * lower <= sum * (scale + 1) * (scale + 1))
&& (upper * upper >= sum * (scale - 1) * (scale - 1))).ShouldBeTrue(
$"Hypot bound failed for ({x.High:R}, {x.Low:R}), ({y.High:R}, {y.Low:R}), ({z.High:R}, {z.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;
}
private static void AssertBits(DoubleDouble expected, DoubleDouble actual)
{
BitConverter.DoubleToInt64Bits(actual.High).ShouldBe(BitConverter.DoubleToInt64Bits(expected.High));
BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(BitConverter.DoubleToInt64Bits(expected.Low));
}
}