346 lines
16 KiB
C#
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));
|
|
}
|
|
}
|