using System.Globalization; using System.Numerics; using Just.PreciseMath.Tests.ReferenceData; using Shouldly; using Xunit; namespace Just.PreciseMath.Tests; public class PreciseMathExpTests { public static IEnumerable> ReferenceCases => ExpReferenceData.Cases(); [Theory] [MemberData(nameof(ReferenceCases))] public void FiniteInputsMeetIndependentReferenceBound(double high, double low, string reference, bool overflow, bool underflow) { // Decimal.exp at 180/260 digits, rounded to 120 digits by generate_exp.py. // Inputs are exact stored binary64 sums, not rounded irrational constants. DoubleDouble input = DoubleDouble.FromComponents(high, low); (Units(input.High) + Units(input.Low)).ShouldBe(Units(high) + Units(low)); DoubleDouble actual = DDMath.Exp(input); DoubleDouble.IsCanonical(actual).ShouldBeTrue(); if (overflow) { actual.High.ShouldBe(double.PositiveInfinity); return; } if (underflow) { BitConverter.DoubleToInt64Bits(actual.High).ShouldBe(0L); return; } DoubleDouble.IsFinite(actual).ShouldBeTrue(); (actual.High > 0.0).ShouldBeTrue(); 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)); // Contract: 2^-100 relative + one minimum subnormal. The independent // decimal reference error is < 2^-350 relative; include it explicitly. BigInteger bound = (numerator << 1324) + (denominator << 350) + (numerator << 1074); ((error << 350) <= bound).ShouldBeTrue( $"Exp bound failed for ({high:R}, {low:R}): ({actual.High:R}, {actual.Low:R})"); } [Theory] [InlineData(-740.0, 85L)] [InlineData(-745.131, 1L)] public void LegacySubnormalResultsRoundToTheExpectedUnits(double input, long expectedBits) { // Independently round Decimal.exp(exact binary64 input)/2^-1074 to an // integer at 180 digits. Pin more than the general one-subnormal bound. DoubleDouble actual = DDMath.Exp(new DoubleDouble(input)); BitConverter.DoubleToInt64Bits(actual.High).ShouldBe(expectedBits); BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(0L); } [Theory] [InlineData(709.5)] [InlineData(709.781)] public void LegacyFiniteResultsDoNotOverflowPrematurely(double input) { DoubleDouble actual = DDMath.Exp(new DoubleDouble(input)); DoubleDouble.IsFinite(actual).ShouldBeTrue(); actual.Low.ShouldNotBe(0.0); // Component-aware accuracy for these exact inputs is pinned by ReferenceCases. } [Fact] public void SpecialValuesAreHandledBeforeRangeReduction() { double[] inputs = [0.0, -0.0, double.NaN, double.PositiveInfinity, double.NegativeInfinity, double.MaxValue, double.MinValue]; foreach (double input in inputs) { DoubleDouble actual = DDMath.Exp(new DoubleDouble(input)); double expected = Math.Exp(input); BitConverter.DoubleToInt64Bits(actual.High).ShouldBe( BitConverter.DoubleToInt64Bits(double.IsNaN(expected) ? double.NaN : expected)); BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(0L); DoubleDouble.IsCanonical(actual).ShouldBeTrue(); } } 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; } }