This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user