using System.Globalization; using System.Numerics; using Just.PreciseMath.Tests.ReferenceData; using Shouldly; using Xunit; namespace Just.PreciseMath.Tests; public class DoubleDoubleRootNTests { public static IEnumerable> ReferenceCases => RootNReferenceData.Cases(); [Theory] [MemberData(nameof(ReferenceCases))] public void LargeDegreesMeetIndependentHighPrecisionReferences(double high, double low, int degree, string reference) { DoubleDouble input = DoubleDouble.FromComponents(high, low); (Units(input.High) + Units(input.Low)).ShouldBe(Units(high) + Units(low)); DoubleDouble actual = DoubleDouble.RootN(input, degree); DoubleDouble.IsFinite(actual).ShouldBeTrue(); DoubleDouble.IsCanonical(actual).ShouldBeTrue(); Math.Sign(actual.High).ShouldBe(Math.Sign(high)); string[] parts = reference.Split('e'); int point = parts[0].IndexOf('.', StringComparison.Ordinal); int decimals = point < 0 ? 0 : parts[0].Length - point - 1; BigInteger numerator = BigInteger.Parse(parts[0].Replace(".", "", StringComparison.Ordinal), CultureInfo.InvariantCulture); int exponent = int.Parse(parts[1], CultureInfo.InvariantCulture) - decimals; BigInteger denominator = BigInteger.One; if (exponent >= 0) { numerator *= BigInteger.Pow(10, exponent); } else { denominator = BigInteger.Pow(10, -exponent); } BigInteger actualUnits = Units(actual.High) + Units(actual.Low); BigInteger error = BigInteger.Abs((actualUnits * denominator) - (numerator << 1074)); // 2^-100 relative plus the separately bounded 2^-350 relative reference // rounding allowance. For |degree|>=2 every nonzero root is normal. BigInteger magnitude = BigInteger.Abs(numerator); BigInteger bound = (magnitude << 1324) + (magnitude << 1074); ((error << 350) <= bound).ShouldBeTrue( $"RootN reference failed for ({high:R}, {low:R}), {degree}: ({actual.High:R}, {actual.Low:R})"); } [Fact] public void SpecialValuesMatchRuntimeRootNSemantics() { double[] values = [0.0, -0.0, 1.0, -1.0, double.PositiveInfinity, double.NegativeInfinity, double.NaN]; int[] degrees = [int.MinValue, int.MinValue + 1, -1000, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 1000, int.MaxValue]; foreach (double value in values) { foreach (int degree in degrees) { AssertBits(new DoubleDouble(double.RootN(value, degree)), DoubleDouble.RootN(new DoubleDouble(value), degree)); } } foreach (int degree in new[] { int.MinValue, -4, -2, 0, 2, 4, int.MaxValue - 1 }) { DoubleDouble.IsNaN(DoubleDouble.RootN(new DoubleDouble(-2.0), degree)).ShouldBeTrue(); } } [Fact] public void IdentitiesAndExistingKernelsKeepTheirComponentBits() { DoubleDouble[] values = [new(double.Epsilon), new(double.MaxValue), new(2.0), DoubleDouble.FromComponents(1.0, double.Epsilon), DoubleDouble.FromComponents(1.0, -Math.ScaleB(1.0, -54))]; foreach (DoubleDouble value in values) { AssertBits(value, DoubleDouble.RootN(value, 1)); AssertBits(-value, DoubleDouble.RootN(-value, 1)); AssertBits(DDMath.Reciprocal(value), DoubleDouble.RootN(value, -1)); AssertBits(DoubleDouble.Sqrt(value), DoubleDouble.RootN(value, 2)); AssertBits(DoubleDouble.InvSqrt(value), DoubleDouble.RootN(value, -2)); AssertBits(DoubleDouble.Cbrt(value), DoubleDouble.RootN(value, 3)); AssertBits(1.0 / DoubleDouble.Cbrt(value), DoubleDouble.RootN(value, -3)); } } [Fact] public void SmallDegreesMeetIndependentExactPowerInequalities() { Random random = new(8675309); for (int exponent = -1074; exponent <= 1023; ++exponent) { double high = Math.ScaleB(1.0 + random.NextDouble(), exponent); foreach (int degree in new[] { -5, -4, -3, -2, 2, 3, 4, 5 }) { DoubleDouble input = DoubleDouble.FromComponents(high, Math.ScaleB((degree < 0 ? -1.0 : 1.0), exponent - 54)); AssertBound(input, DoubleDouble.RootN(input, degree), degree); if ((degree & 1) != 0) { AssertBits(-DoubleDouble.RootN(input, degree), DoubleDouble.RootN(-input, degree)); } } } foreach (double high in new[] { double.Epsilon, 1e-308, 0.5, 1.0, 2.0, 81.0, 1e308, double.MaxValue }) { foreach (int degree in new[] { -31, -17, -7, -6, 6, 7, 17, 31 }) { DoubleDouble input = new(high); AssertBound(input, DoubleDouble.RootN(input, degree), degree); } } } [Fact] public void ExactPowerOfTwoRootsAndSparseCorrectionsSurvive() { foreach (int degree in new[] { -31, -7, -5, -4, 4, 5, 7, 31 }) { for (int exponent = -1074; exponent <= 1023; ++exponent) { if (exponent % degree == 0) { AssertBits(new DoubleDouble(Math.ScaleB(1.0, exponent / degree)), DoubleDouble.RootN(new DoubleDouble(Math.ScaleB(1.0, exponent)), degree)); } } } foreach (int degree in new[] { int.MinValue, int.MinValue + 1, -7, -4, 4, 7, int.MaxValue }) { foreach (int exponent in new[] { -100, -500, -1000, -1070 }) { foreach (double sign in new[] { -1.0, 1.0 }) { double low = Math.ScaleB(sign, exponent); // (1+d)^(1/n) = 1+d/n+O(d²); the quadratic term is // below half an ulp of this rounded correction. AssertBits(DoubleDouble.FromComponents(1.0, low / degree), DoubleDouble.RootN(DoubleDouble.FromComponents(1.0, low), degree)); } } } foreach (double sign in new[] { -1.0, 1.0 }) { DoubleDouble input = DoubleDouble.FromComponents(Math.ScaleB(1.0, 900), Math.ScaleB(sign, -174)); // d*r/(n*high), with exact r=2^225 and n=4, is sign*2^-851. AssertBits(DoubleDouble.FromComponents(Math.ScaleB(1.0, 225), Math.ScaleB(sign, -851)), DoubleDouble.RootN(input, 4)); } } [Fact] public void NegativeDegreeRetainsSparseCorrectionAfterLargeRescaling() { // (2^-900+d)^(-1/4) = 2^225 - d*2^225/(4*2^-900) + O(d²). // For d=±2^-1074, the correction is exactly ∓2^49 at binary64 // residual precision; the quadratic contribution is below half an ulp. foreach (double sign in new[] { -1.0, 1.0 }) { DoubleDouble input = DoubleDouble.FromComponents(Math.ScaleB(1.0, -900), sign * double.Epsilon); AssertBits(DoubleDouble.FromComponents(Math.ScaleB(1.0, 225), Math.ScaleB(-sign, 49)), DoubleDouble.RootN(input, -4)); } } [Fact] public void GenericContractAndFacadeDispatchToTypeMembers() { DoubleDouble[] values = [DoubleDouble.Zero, DoubleDouble.NegativeZero, DoubleDouble.NaN, DoubleDouble.PositiveInfinity, DoubleDouble.NegativeInfinity, new(-2.0), new(double.Epsilon), new(double.MaxValue), DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -54))]; foreach (DoubleDouble value in values) { AssertBits(DoubleDouble.Sqrt(value), GenericSqrt(value)); AssertBits(DoubleDouble.Cbrt(value), GenericCbrt(value)); AssertBits(DoubleDouble.Hypot(value, DoubleDouble.One), GenericHypot(value, DoubleDouble.One)); foreach (int degree in new[] { int.MinValue, -7, -3, -2, -1, 0, 1, 2, 3, 4, 7, int.MaxValue }) { DoubleDouble result = DoubleDouble.RootN(value, degree); AssertBits(result, GenericRootN(value, degree)); AssertBits(result, DDMath.RootN(value, degree)); } } } private static T GenericSqrt(T value) where T : IRootFunctions { return T.Sqrt(value); } private static T GenericCbrt(T value) where T : IRootFunctions { return T.Cbrt(value); } private static T GenericHypot(T x, T y) where T : IRootFunctions { return T.Hypot(x, y); } private static T GenericRootN(T value, int n) where T : IRootFunctions { return T.RootN(value, n); } private static void AssertBound(DoubleDouble input, DoubleDouble actual, int degree) { DoubleDouble.IsFinite(actual).ShouldBeTrue(); DoubleDouble.IsCanonical(actual).ShouldBeTrue(); (actual.High > 0.0).ShouldBeTrue(); int n = Math.Abs(degree); BigInteger x = Units(input.High) + Units(input.Low); BigInteger y = Units(actual.High) + Units(actual.Low); BigInteger scale = BigInteger.One << 100; BigInteger power = BigInteger.Pow(y, n) << (100 * n); BigInteger target; if (degree > 0) { target = x << (1074 * (n - 1)); } else { power *= x; target = BigInteger.One << (1074 * (n + 1)); } // Positive degree: x*(1-t)^n <= y^n <= x*(1+t)^n. // Negative degree: (1-t)^n <= x*y^n <= (1+t)^n. (power >= target * BigInteger.Pow(scale - 1, n) && power <= target * BigInteger.Pow(scale + 1, n)).ShouldBeTrue( $"RootN bound failed for ({input.High:R}, {input.Low:R}), {degree}: ({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)); } }