using System.Globalization; using Just.PreciseMath.Tests.ReferenceData; namespace Just.PreciseMath.Tests; public class PreciseMathLogTests { public static IEnumerable> ReferenceCases => LogReferenceData.Cases(); public static IEnumerable> FamilyReferenceCases => LogarithmicFunctionsReferenceData.Cases(); [Theory] [MemberData(nameof(FamilyReferenceCases))] public void LogarithmicFamilyMeetsIndependentReferenceBound(string operation, double high, double low, double baseHigh, double baseLow, string reference, bool overflow, bool underflow) { // Decimal.ln at 450/650 digits; sums, including 1+x, are exact at 2200 // digits and checked with Fraction before taking logs. No DD-derived oracle. DoubleDouble input = DoubleDouble.FromComponents(high, low); DoubleDouble newBase = DoubleDouble.FromComponents(baseHigh, baseLow); (Units(input.High) + Units(input.Low)).ShouldBe(Units(high) + Units(low)); (Units(newBase.High) + Units(newBase.Low)).ShouldBe(Units(baseHigh) + Units(baseLow)); DoubleDouble actual = operation == "LogBase" ? DoubleDouble.Log(input, newBase) : Evaluate(operation, input); DoubleDouble generic = operation == "LogBase" ? LogGeneric(input, newBase) : EvaluateGeneric(operation, input); AssertBits(generic, actual.High, actual.Low); string context = $"{operation}({high:R}, {low:R}), base=({baseHigh:R}, {baseLow:R})"; if (overflow) { AssertBits(actual, reference[0] == '-' ? double.NegativeInfinity : double.PositiveInfinity, 0.0); } else if (underflow) { AssertBits(actual, reference[0] == '-' ? -0.0 : 0.0, 0.0); } else { AssertReferenceBound(actual, reference, 1, context); if (input != DoubleDouble.One && !DoubleDouble.IsZero(input)) { DoubleDouble.IsZero(actual).ShouldBeFalse(context); } } } [Theory] [InlineData("Log")] [InlineData("Log2")] [InlineData("Log10")] [InlineData("LogP1")] [InlineData("Log2P1")] [InlineData("Log10P1")] public void FamilyDomainAndSpecialValuesHaveGenericDispatch(string operation) { bool plusOne = operation.EndsWith("P1", StringComparison.Ordinal); DoubleDouble[] inputs = [DoubleDouble.Zero, DoubleDouble.NegativeZero, DoubleDouble.NaN, DoubleDouble.PositiveInfinity, DoubleDouble.NegativeInfinity, new(-2.0), new(-1.0), new(1.0), DoubleDouble.FromComponents(-1.0, -double.Epsilon)]; foreach (DoubleDouble input in inputs) { DoubleDouble actual = Evaluate(operation, input); AssertBits(EvaluateGeneric(operation, input), actual.High, actual.Low); if (DoubleDouble.IsNaN(input) || input < (plusOne ? DoubleDouble.NegativeOne : DoubleDouble.Zero)) { AssertBits(actual, double.NaN, 0.0); } else if (DoubleDouble.IsPositiveInfinity(input)) { AssertBits(actual, double.PositiveInfinity, 0.0); } else if (plusOne && input == DoubleDouble.NegativeOne || !plusOne && DoubleDouble.IsZero(input)) { AssertBits(actual, double.NegativeInfinity, 0.0); } else if (DoubleDouble.IsZero(input)) { AssertBits(actual, input.High, 0.0); } else if (!plusOne) { AssertBits(actual, 0.0, 0.0); } } } [Fact] public void BinaryLogarithmOfEveryRepresentablePowerOfTwoIsExact() { for (int exponent = -1074; exponent <= 1023; ++exponent) { DoubleDouble input = new(Math.ScaleB(1.0, exponent)); AssertBits(Log2Generic(input), exponent, 0.0); } } [Fact] public void DecimalLogarithmAndArbitraryBaseHaveElementaryValues() { AssertReferenceBound(Log10Generic(new DoubleDouble(10.0)), "1e+0", 1, "log10(10)"); AssertReferenceBound(LogGeneric(new DoubleDouble(8.0), new DoubleDouble(2.0)), "3e+0", 1, "log2(8)"); AssertReferenceBound(LogGeneric(new DoubleDouble(8.0), new DoubleDouble(0.5)), "-3e+0", 1, "log0.5(8)"); } [Fact] public void ArbitraryBaseSpecialValuesMatchRuntimeWithCanonicalNaNs() { double[] values = [-1.0, -0.0, 0.0, 0.5, 1.0, 2.0, double.PositiveInfinity, double.NegativeInfinity, double.NaN]; foreach (double value in values) { foreach (double newBase in values) { DoubleDouble expected = new(Math.Log(value, newBase)); AssertBits(LogGeneric(new DoubleDouble(value), new DoubleDouble(newBase)), expected.High, 0.0); } } } [Theory] [InlineData("LogP1")] [InlineData("Log2P1")] [InlineData("Log10P1")] public void PlusOneFunctionsPreserveSignedZero(string operation) { AssertBits(EvaluateGeneric(operation, DoubleDouble.NegativeZero), -0.0, 0.0); AssertBits(EvaluateGeneric(operation, DoubleDouble.Zero), 0.0, 0.0); } [Fact] public void NaturalPlusOneLogarithmRetainsTinyQuadraticCorrection() { foreach (double sign in new[] { -1.0, 1.0 }) { double tiny = sign * Math.ScaleB(1.0, -100); // ln(1+x) = x-x²/2+O(x³). The cubic is far below half an ulp // of this low. Adding one first loses the sparse input's second component. AssertBits(EvaluateGeneric("LogP1", new DoubleDouble(tiny)), tiny, -Math.ScaleB(1.0, -201)); DoubleDouble sparse = DoubleDouble.FromComponents(tiny, Math.ScaleB(1.0, -180)); AssertBits(EvaluateGeneric("LogP1", sparse), tiny, Math.ScaleB(1.0, -180) - Math.ScaleB(1.0, -201)); AssertBits(EvaluateGeneric("LogP1", new DoubleDouble(sign * double.Epsilon)), sign * double.Epsilon, 0.0); } } [Fact] public void SpecialValuesHaveExplicitCanonicalResults() { // Legacy Log(0) reached Exp(-Infinity) and threw. Domain handling must // precede finite range reduction, with either zero mapping to -Infinity. double[] values = [0.0, -0.0, 1.0, -1.0, -double.Epsilon, double.MinValue, double.PositiveInfinity, double.NegativeInfinity, double.NaN]; foreach (double value in values) { DoubleDouble expected = new(Math.Log(value)); AssertBits(DoubleDouble.Log(new DoubleDouble(value)), expected.High, 0.0); } DoubleDouble negative = DoubleDouble.FromComponents(-1.0, Math.ScaleB(1.0, -54)); AssertBits(DoubleDouble.Log(negative), double.NaN, 0.0); } [Theory] [MemberData(nameof(ReferenceCases))] public void FiniteInputsMeetIndependentReferenceBound(string label, double high, double low, string reference) { // Decimal.ln at 450/650 digits, from exact component sums formed at // 2200 digits and checked with Fraction. No binary64 log supplies the oracle. DoubleDouble input = DoubleDouble.FromComponents(high, low); (Units(input.High) + Units(input.Low)).ShouldBe(Units(high) + Units(low)); DoubleDouble actual = DoubleDouble.Log(input); AssertReferenceBound(actual, reference, 1, $"{label}: ({high:R}, {low:R})"); if (input == DoubleDouble.One) { AssertBits(actual, 0.0, 0.0); } else { Math.Sign(actual.High).ShouldBe(input > DoubleDouble.One ? 1 : -1); } } [Fact] public void PowersOfTwoCoverEveryFiniteBinaryInputExponent() { // ln(2^k) = k*ln(2). Multiply the independent decimal reference by k // with exact integers, never using the library constant or DD arithmetic. for (int exponent = -1074; exponent <= 1023; ++exponent) { DoubleDouble input = new(Math.ScaleB(1.0, exponent)); AssertReferenceBound(DoubleDouble.Log(input), LogReferenceData.Ln2, exponent, $"2^{exponent}"); } } [Theory] [InlineData(1e308)] [InlineData(double.MaxValue)] [InlineData(1e-308)] public void ExtremeFiniteInputsRetainExtendedPrecisionWithoutDenominatorOverflow(double input) { // The legacy Log(1e308) formed u+value and overflowed. The finite // kernel must reduce the mantissa before constructing its denominator. DoubleDouble actual = DoubleDouble.Log(new DoubleDouble(input)); DoubleDouble.IsFinite(actual).ShouldBeTrue(); actual.Low.ShouldNotBe(0.0); DoubleDouble.IsCanonical(actual).ShouldBeTrue(); // ReferenceCases independently pins the complete result for these inputs. } [Fact] public void NearOneRetainsSparseCorrectionsOfEitherSign() { foreach (double sign in new[] { -1.0, 1.0 }) { AssertBits(DoubleDouble.Log(DoubleDouble.FromComponents(1.0, sign * double.Epsilon)), sign * double.Epsilon, 0.0); double delta = sign * Math.ScaleB(1.0, -500); // ln(1+d) = d-d^2/2+O(d^3). Here the cubic tail is below half // the minimum subnormal, so these two binary components are exact. AssertBits(DoubleDouble.Log(DoubleDouble.FromComponents(1.0, delta)), delta, -Math.ScaleB(1.0, -1001)); } } [Fact] public void ArbitraryPairsAreNormalizedByThePublicFactoryBeforeLog() { DoubleDouble positive = DoubleDouble.FromComponents(0.0, 2.0); AssertReferenceBound(DoubleDouble.Log(positive), LogReferenceData.Ln2, 1, "zero-high public construction"); AssertBits(DoubleDouble.Log(DoubleDouble.FromComponents(0.0, -2.0)), double.NaN, 0.0); } private static DoubleDouble Evaluate(string operation, DoubleDouble value) { return operation switch { "Log" => DoubleDouble.Log(value), "Log2" => DoubleDouble.Log2(value), "Log10" => DoubleDouble.Log10(value), "LogP1" => DoubleDouble.LogP1(value), "Log2P1" => DoubleDouble.Log2P1(value), "Log10P1" => DoubleDouble.Log10P1(value), _ => throw new ArgumentOutOfRangeException(nameof(operation)) }; } private static T EvaluateGeneric(string operation, T value) where T : ILogarithmicFunctions { return operation switch { "Log" => T.Log(value), "Log2" => T.Log2(value), "Log10" => T.Log10(value), "LogP1" => T.LogP1(value), "Log2P1" => T.Log2P1(value), "Log10P1" => T.Log10P1(value), _ => throw new ArgumentOutOfRangeException(nameof(operation)) }; } private static T LogGeneric(T value, T newBase) where T : ILogarithmicFunctions => T.Log(value, newBase); private static T Log2Generic(T value) where T : ILogarithmicFunctions => T.Log2(value); private static T Log10Generic(T value) where T : ILogarithmicFunctions => T.Log10(value); private static void AssertReferenceBound(DoubleDouble actual, string reference, int factor, string context) { DoubleDouble.IsFinite(actual).ShouldBeTrue(context); DoubleDouble.IsCanonical(actual).ShouldBeTrue(context); string[] parts = reference.Split('e'); int point = parts[0].IndexOf('.', StringComparison.Ordinal); int decimals = point < 0 ? 0 : parts[0].Length - point - 1; BigInteger numerator = factor * 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)); BigInteger magnitude = BigInteger.Abs(numerator); // 2^-100 relative + one minimum subnormal, with independent reference // uncertainty <2^-350 relative explicitly included. No component collapse. BigInteger bound = (magnitude << 1324) + (denominator << 350) + (magnitude << 1074); ((error << 350) <= bound).ShouldBeTrue( $"Log bound failed for {context}: ({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 actual, double high, double low) { BitConverter.DoubleToInt64Bits(actual.High).ShouldBe(BitConverter.DoubleToInt64Bits(high)); BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(BitConverter.DoubleToInt64Bits(low)); DoubleDouble.IsCanonical(actual).ShouldBeTrue(); } }