logarithmic functions implementation
.NET Test / .NET tests (push) Successful in 2m14s

This commit is contained in:
2026-09-24 01:23:09 +04:00
parent 720fd1e21f
commit cb06df8865
11 changed files with 1944 additions and 22 deletions
@@ -7,6 +7,141 @@ 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()
{
@@ -93,6 +228,40 @@ public class PreciseMathLogTests
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);