111 lines
4.5 KiB
C#
111 lines
4.5 KiB
C#
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<TheoryDataRow<double, double, string, bool, bool>> 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;
|
|
}
|
|
}
|