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
@@ -16,7 +16,7 @@ public readonly partial struct DoubleDouble : IExponentialFunctions<DoubleDouble
{
if (double.IsNaN(value.High))
{
return DoubleDouble.NaN;
return NaN;
}
// These deliberately loose bounds only reject inputs safely outside the
// result range, even with a normalized low of either sign. They also keep
@@ -27,11 +27,11 @@ public readonly partial struct DoubleDouble : IExponentialFunctions<DoubleDouble
}
if (value.High < -746.0)
{
return DoubleDouble.Zero;
return Zero;
}
if (value.High == 0.0)
{
return DoubleDouble.One;
return One;
}
// The binary64 estimate need not choose the nearest k on a tie: either
@@ -40,9 +40,9 @@ public readonly partial struct DoubleDouble : IExponentialFunctions<DoubleDouble
// component prevents k times the DD constant error from dominating r.
// Generated independently at 180/260 decimal digits by generate_exp.py.
const double ln2Tail = 5.707708438416212e-34;
int exponent = (int)Math.Round(value.High / DoubleDouble.Ln2.High);
DoubleDouble reduced = value - (new DoubleDouble(DoubleDouble.Ln2.High) * exponent);
reduced -= new DoubleDouble(DoubleDouble.Ln2.Low) * exponent;
int exponent = (int)Math.Round(value.High / Ln2.High);
DoubleDouble reduced = value - (new DoubleDouble(Ln2.High) * exponent);
reduced -= new DoubleDouble(Ln2.Low) * exponent;
reduced -= new DoubleDouble(ln2Tail) * exponent;
return PreciseMathHelper.ScalePowerOfTwo(ExpReduced(reduced), exponent);
@@ -225,8 +225,8 @@ public readonly partial struct DoubleDouble : IExponentialFunctions<DoubleDouble
ReadOnlySpan<double> coefficients = [156.0, 12012.0, 600600.0, 21621600.0,
588107520.0, 12350257920.0, 201132771840.0, 2514159648000.0,
23465490048000.0, 154872234316800.0, 647647525324800.0, 1295295050649600.0];
DoubleDouble numerator = DoubleDouble.One;
DoubleDouble denominator = DoubleDouble.One;
DoubleDouble numerator = One;
DoubleDouble denominator = One;
foreach (double coefficient in coefficients)
{
numerator = (numerator * reduced) + coefficient;
+105 -1
View File
@@ -1,6 +1,6 @@
namespace Just.PreciseMath;
public readonly partial struct DoubleDouble
public readonly partial struct DoubleDouble : ILogarithmicFunctions<DoubleDouble>
{
/// <summary>Returns the natural logarithm of a double-double value.</summary>
/// <remarks>
@@ -34,6 +34,110 @@ public readonly partial struct DoubleDouble
return LogPositiveFinite(value);
}
/// <summary>Returns the logarithm of a value in the specified base.</summary>
/// <remarks>
/// Uses the quotient of natural logarithms, retaining both components of each
/// operand. Negative inputs or bases, NaN, and base one return canonical NaN.
/// A zero or positive-infinite base is allowed only for value one, returning
/// negative or positive zero respectively. For other positive bases, zero and
/// infinity follow the signs of log(value)/log(newBase), including signed zero
/// for value one. Finite results are approximate, not guaranteed correctly rounded;
/// tests check 2^-100 relative error plus one minimum binary64 subnormal.
/// </remarks>
[Pure]
public static DoubleDouble Log(DoubleDouble value, DoubleDouble newBase)
{
if (IsNaN(value) || IsNaN(newBase) || newBase == One
|| (value != One && (IsZero(newBase) || IsPositiveInfinity(newBase))))
{
return NaN;
}
return Log(value) / Log(newBase);
}
/// <summary>Returns the base-two logarithm of a double-double value.</summary>
/// <remarks>
/// Special values and approximate accuracy follow <see cref="Log(DoubleDouble)"/>.
/// Representable powers of two return their exact integer exponent.
/// </remarks>
[Pure]
public static DoubleDouble Log2(DoubleDouble value)
{
if (value.High > 0.0 && double.IsFinite(value.High) && value.Low == 0.0)
{
int exponent = Math.ILogB(value.High);
if (value.High == Math.ScaleB(1.0, exponent))
{
return new DoubleDouble(exponent);
}
}
return Log(value) / Ln2;
}
/// <summary>Returns the base-ten logarithm of a double-double value.</summary>
/// <remarks>
/// Special values and approximate accuracy follow <see cref="Log(DoubleDouble)"/>.
/// Uses the natural logarithm divided by ln(10), without collapsing components.
/// </remarks>
[Pure]
public static DoubleDouble Log10(DoubleDouble value) => Log(value) / Ln10;
/// <summary>Returns the natural logarithm of one plus a double-double value.</summary>
/// <remarks>
/// Evaluates a direct atanh series near zero, without rounding away either
/// input component by adding one first. Signed zeros are preserved; minus one
/// returns negative infinity; values below minus one and NaN return canonical
/// NaN; positive infinity is preserved. Approximate accuracy follows
/// <see cref="Log(DoubleDouble)"/>, relative to log(1+value) itself.
/// </remarks>
[Pure]
public static DoubleDouble LogP1(DoubleDouble value)
{
if (IsZero(value))
{
return value;
}
if (Math.Abs(value.High) <= Math.ScaleB(1.0, -54))
{
// log(1+x) = x-x²/2+O(x³), with omitted relative error < 2^-109.
// Keep x unscaled so even minimum-subnormal inputs survive.
return value - ((value * value) * 0.5);
}
if (Math.Abs(value.High) <= 0.5)
{
// |t| <= 1/3 plus endpoint rounding. Unlike Log(1+x), the numerator
// is the original expansion, retaining both components near zero.
DoubleDouble t = value / (value + 2.0);
DoubleDouble squared = t * t;
DoubleDouble term = t;
DoubleDouble sum = t;
for (int denominator = 3; denominator <= 69; denominator += 2)
{
term *= squared;
sum += term / denominator;
}
// The omitted tail is <= 2*|t|^71/(71*(1-t²)); rounding dominates.
return sum * 2.0;
}
return Log(value + 1.0);
}
/// <summary>Returns the base-two logarithm of one plus a double-double value.</summary>
/// <remarks>
/// Uses the cancellation-safe natural logarithm divided by ln(2). Domain,
/// signed-zero, and approximate accuracy rules follow <see cref="LogP1"/>.
/// </remarks>
[Pure]
public static DoubleDouble Log2P1(DoubleDouble value) => LogP1(value) / Ln2;
/// <summary>Returns the base-ten logarithm of one plus a double-double value.</summary>
/// <remarks>
/// Uses the cancellation-safe natural logarithm divided by ln(10). Domain,
/// signed-zero, and approximate accuracy rules follow <see cref="LogP1"/>.
/// </remarks>
[Pure]
public static DoubleDouble Log10P1(DoubleDouble value) => LogP1(value) / Ln10;
// Shared with Pow. Requires a positive finite normalized value other than one;
// callers handle domain errors and special values before any range reduction.
private static DoubleDouble LogPositiveFinite(DoubleDouble value)
@@ -20,7 +20,7 @@ public readonly partial struct DoubleDouble
public static DoubleDouble Reciprocal(DoubleDouble value)
{
const double one = 1.0;
if (!DoubleDouble.IsFinite(value) || value._high == 0.0)
if (!IsFinite(value) || value._high == 0.0)
{
return new DoubleDouble(one / value._high);
}
@@ -1,6 +1,6 @@
namespace Just.PreciseMath;
public readonly partial struct DoubleDouble
public readonly partial struct DoubleDouble : IPowerFunctions<DoubleDouble>
{
/// <summary>Raises a double-double value to an integer power.</summary>
/// <remarks>
@@ -256,7 +256,7 @@ public readonly partial struct DoubleDouble : IRootFunctions<DoubleDouble>
{
if (double.IsNaN(value.High) || value.High < 0.0)
{
return DoubleDouble.NaN;
return NaN;
}
if (value.High == 0.0 || double.IsPositiveInfinity(value.High))
{
@@ -316,7 +316,7 @@ public readonly partial struct DoubleDouble : IRootFunctions<DoubleDouble>
{
if (double.IsNaN(value.High) || value.High < 0.0)
{
return DoubleDouble.NaN;
return NaN;
}
if (value.High == 0.0)
{
@@ -324,7 +324,7 @@ public readonly partial struct DoubleDouble : IRootFunctions<DoubleDouble>
}
if (double.IsPositiveInfinity(value.High))
{
return DoubleDouble.Zero;
return Zero;
}
// As in Sqrt, the even exponent is in [-1074, 1022], high is in [1, 4),
@@ -345,13 +345,13 @@ public readonly partial struct DoubleDouble : IRootFunctions<DoubleDouble>
residual = Math.FusedMultiplyAdd(-high, squareError, residual);
residual = Math.FusedMultiplyAdd(-low, square, residual);
double correction = (0.5 * estimate) * residual;
DoubleDouble inverseRoot = DoubleDouble.FromComponents(estimate, correction);
DoubleDouble inverseRoot = FromComponents(estimate, correction);
// The result high is normal and finite over the entire positive input
// range. Rescale in the opposite direction to Sqrt and canonicalize any
// zero low, including underflow of an exceptionally sparse correction.
int inverseExponent = -(exponent / 2);
return DoubleDouble.FromComponents(Math.ScaleB(inverseRoot.High, inverseExponent),
return FromComponents(Math.ScaleB(inverseRoot.High, inverseExponent),
Math.ScaleB(inverseRoot.Low, inverseExponent));
}
@@ -59,6 +59,30 @@ public static class DoubleDoubleExtensions
[Pure]
public static DoubleDouble Log(this DoubleDouble value) => DoubleDouble.Log(value);
/// <inheritdoc cref="DoubleDouble.Log(DoubleDouble, DoubleDouble)"/>
[Pure]
public static DoubleDouble Log(this DoubleDouble value, DoubleDouble newBase) => DoubleDouble.Log(value, newBase);
/// <inheritdoc cref="DoubleDouble.Log2(DoubleDouble)"/>
[Pure]
public static DoubleDouble Log2(this DoubleDouble value) => DoubleDouble.Log2(value);
/// <inheritdoc cref="DoubleDouble.Log10(DoubleDouble)"/>
[Pure]
public static DoubleDouble Log10(this DoubleDouble value) => DoubleDouble.Log10(value);
/// <inheritdoc cref="DoubleDouble.LogP1(DoubleDouble)"/>
[Pure]
public static DoubleDouble LogP1(this DoubleDouble value) => DoubleDouble.LogP1(value);
/// <inheritdoc cref="DoubleDouble.Log2P1(DoubleDouble)"/>
[Pure]
public static DoubleDouble Log2P1(this DoubleDouble value) => DoubleDouble.Log2P1(value);
/// <inheritdoc cref="DoubleDouble.Log10P1(DoubleDouble)"/>
[Pure]
public static DoubleDouble Log10P1(this DoubleDouble value) => DoubleDouble.Log10P1(value);
/// <inheritdoc cref="DoubleDouble.RootN(DoubleDouble, int)"/>
[Pure]
public static DoubleDouble RootN(this DoubleDouble value, int n) => DoubleDouble.RootN(value, n);
@@ -16,12 +16,18 @@ public class DoubleDoubleExtensionsTests
[InlineData("Exp2M1")]
[InlineData("Exp10M1")]
[InlineData("Log")]
[InlineData("Log2")]
[InlineData("Log10")]
[InlineData("LogP1")]
[InlineData("Log2P1")]
[InlineData("Log10P1")]
public void UnaryExtensionsPreserveStaticFunctionComponentBits(string operation)
{
// These are forwarding contracts, not accuracy oracles. The static
// kernels have separate independent numerical-reference tests.
foreach (DoubleDouble value in Values())
{
DoubleDouble original = value;
(DoubleDouble expected, DoubleDouble actual) = operation switch
{
"Abs" => (DoubleDouble.Abs(value), value.Abs()),
@@ -37,12 +43,49 @@ public class DoubleDoubleExtensionsTests
"Exp2M1" => (DoubleDouble.Exp2M1(value), value.Exp2M1()),
"Exp10M1" => (DoubleDouble.Exp10M1(value), value.Exp10M1()),
"Log" => (DoubleDouble.Log(value), value.Log()),
"Log2" => (DoubleDouble.Log2(value), value.Log2()),
"Log10" => (DoubleDouble.Log10(value), value.Log10()),
"LogP1" => (DoubleDouble.LogP1(value), value.LogP1()),
"Log2P1" => (DoubleDouble.Log2P1(value), value.Log2P1()),
"Log10P1" => (DoubleDouble.Log10P1(value), value.Log10P1()),
_ => throw new ArgumentOutOfRangeException(nameof(operation))
};
AssertBits(expected, actual);
AssertBits(original, value);
}
}
[Fact]
public void BaseLogarithmExtensionPreservesBothOperands()
{
DoubleDouble[] bases = [DoubleDouble.Zero, DoubleDouble.NegativeZero,
DoubleDouble.One, DoubleDouble.NegativeOne, DoubleDouble.NaN,
DoubleDouble.PositiveInfinity, DoubleDouble.NegativeInfinity,
new(0.5), new(2.0), new(10.0), new(double.Epsilon), new(double.MaxValue),
DoubleDouble.FromComponents(1.0, double.Epsilon),
DoubleDouble.FromComponents(1.0, -double.Epsilon),
DoubleDouble.FromComponents(2.0, Math.ScaleB(1.0, -54))];
foreach (DoubleDouble value in Values())
{
foreach (DoubleDouble newBase in bases)
{
DoubleDouble originalValue = value;
DoubleDouble originalBase = newBase;
AssertBits(DoubleDouble.Log(value, newBase), value.Log(newBase));
AssertBits(originalValue, value);
AssertBits(originalBase, newBase);
}
}
}
[Fact]
public void LogarithmExtensionsCanBeChainedWithoutChangingTheReceiver()
{
DoubleDouble value = new(8.0);
AssertBits(new DoubleDouble(8.0), value.Log2().Exp2());
AssertBits(new DoubleDouble(8.0), value);
}
[Fact]
public void ExtensionsCanBeChainedWithoutChangingTheReceiver()
{
@@ -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);
@@ -0,0 +1,137 @@
"""Generate exact-input logarithmic-family references using only the standard library.
Run this file with --check to verify the checked-in C# fixtures. Component sums
and 1+x are formed exactly at 2200 digits and checked with Fraction. Decimal.ln
and base conversion are evaluated independently at 450 and 650 digits, requiring
identical 120-digit output and range flags. Tests do not require Python.
"""
from decimal import Decimal, localcontext
from fractions import Fraction
from pathlib import Path
import argparse
import math
import random
import sys
from generate_log import exact_sum, inputs
OUTPUT = Path(__file__).with_name('LogarithmicFunctionsReferenceData.cs')
def plus_one_inputs():
epsilon = math.ulp(0.0)
yield -1.0, epsilon
yield -1.0, 2.0 ** -1000
yield -1.0, 2.0 ** -54
for high in [-0.999, -0.75, -0.5, -0.125, 0.125, 0.5, 1.0, 3.0, 9.0,
sys.float_info.min, 1e100, sys.float_info.max]:
for low in [0.0, math.ulp(high) / 4, -math.ulp(high) / 4]:
yield high, low
for exponent in [-1074, -1073, -1022, -1000, -500, -100, -55, -54, -53]:
for sign in [-1, 1]:
high = sign * 2.0 ** exponent
for low in [0.0, math.ulp(high) / 4, -math.ulp(high) / 4]:
yield high, low
# Sparse lows and both sides of the actual high-component dispatch boundaries.
for high in [-2.0 ** -100, 2.0 ** -100]:
for low in [-2.0 ** -180, 2.0 ** -180]:
yield high, low
for high in [-0.5, 0.5, -2.0 ** -54, 2.0 ** -54]:
for neighbor in [math.nextafter(high, -math.inf), high, math.nextafter(high, math.inf)]:
for low in [-math.ulp(neighbor) / 4, 0.0, math.ulp(neighbor) / 4]:
yield neighbor, low
rng = random.Random(271828)
for _ in range(96):
high = rng.uniform(-0.99, 1.0)
yield high, rng.choice([-1, 1]) * math.ulp(high) / 4
def base_inputs():
values = [(0.5, 0.0), (3.0, 2.0 ** -53), (10.0, -2.0 ** -51),
(math.ulp(0.0), 0.0), (sys.float_info.max, 0.0),
(1.0, 2.0 ** -54), (1.0, -2.0 ** -54),
(1.0, math.ulp(0.0)), (1.0, -math.ulp(0.0)),
(1.0, 2.0 ** -500), (1.0, -2.0 ** -500)]
for high, low in values:
for base_high, base_low in values:
yield high, low, base_high, base_low
def cases():
for _, high, low in inputs():
for operation in ['Log2', 'Log10']:
yield operation, high, low, 0.0, 0.0
for high, low in plus_one_inputs():
for operation in ['LogP1', 'Log2P1', 'Log10P1']:
yield operation, high, low, 0.0, 0.0
for high, low, base_high, base_low in base_inputs():
yield 'LogBase', high, low, base_high, base_low
def reference(operation, high, low, base_high, base_low, precision):
value = exact_sum(high, low)
if operation.endswith('P1'):
with localcontext() as context:
context.prec = 2200
value += 1
assert Fraction(value) == Fraction(high) + Fraction(low) + 1
assert value > 0
base = exact_sum(base_high, base_low) if operation == 'LogBase' else None
with localcontext() as context:
context.prec = precision
result = value.ln()
if base is not None:
result /= base.ln()
elif operation.startswith('Log2'):
result /= Decimal(2).ln()
elif operation.startswith('Log10'):
result /= Decimal(10).ln()
with localcontext() as output_context:
output_context.prec = 120
rounded = +result
assert abs(result - rounded) <= abs(rounded) * Decimal(2) ** -350
# Independently classify against binary64's exact range midpoints.
magnitude = abs(Fraction(result))
overflow = magnitude >= Fraction((2 ** 54 - 1) * 2 ** 970)
underflow = magnitude != 0 and magnitude <= Fraction(1, 2 ** 1075)
return format(rounded, 'e') if rounded else '0e+0', overflow, underflow
def generate():
rows = []
for case in dict.fromkeys(cases()):
result = reference(*case, 450)
assert result == reference(*case, 650), case
operation, high, low, base_high, base_low = case
text, overflow, underflow = result
rows.append(f' yield return new("{operation}", {high!r}, {low!r}, '
f'{base_high!r}, {base_low!r}, "{text}", '
f'{str(overflow).lower()}, {str(underflow).lower()});')
content = '''// Generated by generate_logarithmic_functions.py; do not hand-edit reference literals.
// Exact component sums and 1+x; Decimal.ln at 450/650 digits, rounded to 120 digits.
namespace Just.PreciseMath.Tests.ReferenceData;
internal static class LogarithmicFunctionsReferenceData
{
internal static IEnumerable<TheoryDataRow<string, double, double, double, double, string, bool, bool>> Cases()
{
''' + '\n'.join(rows) + '\n }\n}\n'
return content, len(rows)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--check', action='store_true')
args = parser.parse_args()
content, count = generate()
if args.check:
assert OUTPUT.read_text() == content, 'Fixture is stale; regenerate it'
print(f'Verified {count} logarithmic-family references at 450/650 digits.')
else:
OUTPUT.write_text(content)
print(f'Generated {count} references in {OUTPUT.name}.')
if __name__ == '__main__':
main()
+34 -6
View File
@@ -105,8 +105,10 @@ The additional `InvSqrt` helper and three-argument `Hypot` overload live alongsi
them. Exponential functions live in `DoubleDouble.ExponentialFunctions.cs`:
`Exp`, `Exp2`, `Exp10`, `ExpM1`, `Exp2M1`, and `Exp10M1` complete
`IExponentialFunctions<DoubleDouble>`, including cancellation-safe overrides of
the interface's default minus-one methods. Natural logarithm and power kernels
live in `DoubleDouble.Log.cs` and `DoubleDouble.Pow.cs`; reciprocal lives in
the interface's default minus-one methods. Logarithms live in `DoubleDouble.Log.cs`:
`Log` (natural and arbitrary base), `Log2`, `Log10`, `LogP1`, `Log2P1`, and `Log10P1`
complete `ILogarithmicFunctions<DoubleDouble>`, including cancellation-safe plus-one
overrides. Power kernels live in `DoubleDouble.Pow.cs`; reciprocal lives in
`DoubleDouble.NumberBase.cs`. Arithmetic implementations live in
`DoubleDouble.Arithmetic.cs`, with forwarding operators in `DoubleDouble.Operators.cs`.
@@ -117,7 +119,8 @@ facade is provided.
`DoubleDoubleExtensions`, in the same `Just.PreciseMath` namespace, also provides
instance-style calls for `Abs`, `Square`, `Reciprocal`, `Sqrt`, `InvSqrt`, `Cbrt`,
all six exponential functions, `Log`, `RootN(int)`, and `Pow` with `int`, `double`,
all six exponential functions, all logarithms (including `Log(newBase)` and the
three `P1` variants), `RootN(int)`, and `Pow` with `int`, `double`,
or `DoubleDouble` exponents. These extensions return a result without modifying
the receiver and forward directly to the corresponding static method, preserving
its numerical contract and component bits. Arithmetic remains available through
@@ -232,6 +235,22 @@ DoubleDouble root = magnitude.RootN(2);
large inputs; a separate near-one path retains even minimum-subnormal low
components. Either zero maps to negative infinity, one to positive zero,
positive infinity to itself, and negative nonzero inputs or NaN to canonical NaN.
- `Log2(DoubleDouble)` and `Log10(DoubleDouble)`: base-two and base-ten logarithms,
using natural-log base conversion with the same domain and special-value rules.
`Log2` returns the exact integer exponent for every representable power of two.
- `Log(DoubleDouble, DoubleDouble)`: arbitrary-base logarithm retaining both
components of both operands. Negative arguments or bases, NaNs, and base one
return canonical NaN. Zero or positive-infinite bases are accepted only for
argument one, returning negative or positive zero respectively. Otherwise the
signs of zero and infinity follow `log(value)/log(newBase)`; bases between zero
and one reverse the sign. Finite results can overflow or underflow when the
base is extremely close to one or the argument is extremely close to one.
- `LogP1(DoubleDouble)`, `Log2P1(DoubleDouble)`, and `Log10P1(DoubleDouble)`:
logarithms of one plus the argument. A direct near-zero series avoids losing
input components by adding one first. Signed zeros are preserved, minus one
returns negative infinity, values below minus one and NaN return canonical NaN,
and positive infinity is preserved. Accuracy is measured relative to the final
logarithm, not to the argument shifted by one.
```csharp
using Just.PreciseMath;
@@ -251,6 +270,8 @@ DoubleDouble binaryPower = DoubleDouble.Exp2(new DoubleDouble(-1000.0));
DoubleDouble decimalPower = DoubleDouble.Exp10(new DoubleDouble(0.5));
DoubleDouble tinyChange = DoubleDouble.ExpM1(new DoubleDouble(1e-30));
DoubleDouble logarithm = DoubleDouble.Log(new DoubleDouble(10.0));
DoubleDouble binaryLogarithm = DoubleDouble.Log2(new DoubleDouble(8.0));
DoubleDouble smallLogarithm = DoubleDouble.LogP1(new DoubleDouble(1e-30));
DoubleDouble fractionalPower = DoubleDouble.Pow(new DoubleDouble(2.0), 0.5);
DoubleDouble preciseExponent = DoubleDouble.FromComponents(0.5, 1e-30);
DoubleDouble precisePower = DoubleDouble.Pow(new DoubleDouble(2.0), preciseExponent);
@@ -295,6 +316,13 @@ reference-rounding allowance. Powers of two cover every finite binary64 exponent
additional tests cover extreme magnitudes, near-one cancellation, sparse lows of
either sign, and range-reduction transitions. This is sampled approximate accuracy,
not a universal error proof or a correct-rounding guarantee.
The other bases and plus-one variants use the same tested error bound, with
independent references for exact component sums and exact `1+x`. Tests cover
near-zero series transitions, bases on either side of one (including sparse
subnormal differences), domain/signed-zero behavior, and direct/generic dispatch.
Selected sparse plus-one cases separately pin retained low components. Precision
decreases near underflow; no universal correct-rounding or range-boundary guarantee
is implied by the sampled arbitrary-base checks.
Exponential-family tests use independent high-precision decimal references evaluated at
two precisions, with exact integer comparisons of the stored component sum. They
@@ -429,10 +457,10 @@ bool success = DoubleDouble.TryParse("1.25e-2".AsSpan(), CultureInfo.InvariantCu
## Deferred scope
Natural `DoubleDouble.Log`, the complete exponential family on `DoubleDouble`, and all
The complete logarithmic and exponential families on `DoubleDouble`, and all
three `DoubleDouble.Pow` overloads are implemented.
Logarithms in other bases, generic-math interfaces beyond `ISignedNumber`,
`IFloatingPointConstants`, `IRootFunctions`, and `IExponentialFunctions`, additional text formats/shortest-
Generic-math interfaces beyond `ISignedNumber`, `IFloatingPointConstants`,
`IRootFunctions`, `IExponentialFunctions`, `ILogarithmicFunctions`, and `IPowerFunctions`, additional text formats/shortest-
round-trip formatting, and non-arithmetic performance benchmarks remain deferred.
Replacing allocating arithmetic boundary fallbacks is also deferred; the current
`BigInteger` paths remain in place. That optimization does not require removing