313 lines
13 KiB
C#
313 lines
13 KiB
C#
using System.Globalization;
|
|
using Just.PreciseMath.Tests.ReferenceData;
|
|
|
|
namespace Just.PreciseMath.Tests;
|
|
|
|
public class PreciseMathLogTests
|
|
{
|
|
public static IEnumerable<TheoryDataRow<string, double, double, string>> ReferenceCases => LogReferenceData.Cases();
|
|
|
|
public static IEnumerable<TheoryDataRow<string, double, double, double, double, string, bool, bool>> 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<T>(string operation, T value) where T : ILogarithmicFunctions<T>
|
|
{
|
|
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>(T value, T newBase) where T : ILogarithmicFunctions<T> => T.Log(value, newBase);
|
|
|
|
private static T Log2Generic<T>(T value) where T : ILogarithmicFunctions<T> => T.Log2(value);
|
|
|
|
private static T Log10Generic<T>(T value) where T : ILogarithmicFunctions<T> => 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();
|
|
}
|
|
}
|