pow, log and exp
.NET Test / .NET tests (push) Successful in 1m31s

This commit is contained in:
2026-09-15 00:33:31 +04:00
parent beb9a084d0
commit b54a0a2d42
18 changed files with 2890 additions and 12 deletions
@@ -0,0 +1,283 @@
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests;
public class PreciseMathPowTests
{
[Fact]
public void NegativePowerDoesNotOverflowBeforeReciprocation()
{
// Legacy review: Pow(2, -1024) overflowed before taking the reciprocal.
DoubleDouble actual = DDMath.Pow(new DoubleDouble(2.0), -1024);
AssertBits(actual, Math.ScaleB(1.0, -1024), 0.0);
}
[Fact]
public void MinimumIntegerExponentDoesNotOverflowItsMagnitude()
{
AssertBits(DDMath.Pow(DoubleDouble.One, int.MinValue), 1.0, 0.0);
AssertBits(DDMath.Pow(DoubleDouble.NegativeOne, int.MinValue), 1.0, 0.0);
AssertBits(DDMath.Pow(new DoubleDouble(2.0), int.MinValue), 0.0, 0.0);
AssertBits(DDMath.Pow(new DoubleDouble(0.5), int.MinValue), double.PositiveInfinity, 0.0);
}
[Fact]
public void PowerOnePreservesEvenAnExtremelySparseLow()
{
foreach (double sign in new[] { -1.0, 1.0 })
{
DoubleDouble input = DoubleDouble.FromComponents(sign * double.MaxValue, sign * double.Epsilon);
AssertBits(DDMath.Pow(input, 1), input.High, input.Low);
}
}
[Fact]
public void SpecialValuesFollowIntegerPowerRulesWithCanonicalBits()
{
double[] inputs = [0.0, -0.0, double.PositiveInfinity, double.NegativeInfinity,
double.NaN, 1.0, -1.0];
int[] exponents = [int.MinValue, int.MinValue + 1, -1025, -2, -1, 0, 1, 2, 1025, int.MaxValue];
foreach (double input in inputs)
{
foreach (int exponent in exponents)
{
// Every int converts exactly to double. BCL is an independent
// special-value/sign oracle, not the finite precision oracle.
double expected = Math.Pow(input, exponent);
AssertBits(DDMath.Pow(new DoubleDouble(input), exponent),
double.IsNaN(expected) ? double.NaN : expected, 0.0);
}
}
}
[Fact]
public void BinaryPowersAreExactAcrossInputAndOutputExponentRanges()
{
int[] powers = [int.MinValue, int.MinValue + 1, -1075, -1024, -3, -2, -1,
0, 1, 2, 3, 1024, 1075, int.MaxValue];
for (int inputExponent = -1074; inputExponent <= 1023; ++inputExponent)
{
foreach (int power in powers)
{
long outputExponent = (long)inputExponent * power;
double magnitude = outputExponent > 1023 ? double.PositiveInfinity
: outputExponent < -1074 ? 0.0 : Math.ScaleB(1.0, (int)outputExponent);
foreach (double sign in new[] { -1.0, 1.0 })
{
double expected = sign < 0.0 && (power & 1) != 0 ? -magnitude : magnitude;
AssertBits(DDMath.Pow(new DoubleDouble(sign * Math.ScaleB(1.0, inputExponent)), power),
expected, 0.0);
}
}
}
}
[Fact]
public void FinitePowersMeetAnIndependentExactRationalErrorBound()
{
// Exact binary input sums, raised with BigInteger arithmetic; no DD
// multiplication/division, decimal, or Math.Pow supplies the oracle.
Random random = new(270718);
for (int sample = 0; sample < 100; ++sample)
{
int scale = random.Next(-12, 13);
double high = Math.ScaleB(1.0 + random.NextDouble(), scale);
double low = Math.ScaleB(random.NextDouble(), scale - 54);
foreach (double residual in new[] { 0.0, low, -low })
{
DoubleDouble input = DoubleDouble.FromComponents(sample % 2 == 0 ? high : -high, residual);
foreach (int exponent in new[] { -63, -7, -2, -1, 1, 2, 7, 63 })
{
AssertExactPowerBound(input, exponent);
}
}
}
foreach (DoubleDouble input in new[] {
DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -53)),
DoubleDouble.FromComponents(1.0, -Math.ScaleB(1.0, -54)),
DoubleDouble.FromComponents(0.5, double.Epsilon) })
{
foreach (int exponent in new[] { -1024, -257, 257, 1024 })
{
if (input.High == 0.5 && exponent == -1024)
{
continue; // Overflow is pinned by the binary-power matrix.
}
AssertExactPowerBound(input, exponent);
}
}
}
[Fact]
public void NegativePowersRetainRepresentableLowsAtExtremeInputMagnitudes()
{
foreach (int scale in new[] { -1023, -1000, -500, 500, 1000, 1023 })
{
foreach (double sign in new[] { -1.0, 1.0 })
{
DoubleDouble input = DoubleDouble.FromComponents(Math.ScaleB(1.5, scale),
sign * Math.ScaleB(1.0, scale - 54));
AssertExactPowerBound(input, -1);
if (scale == -500 || scale == 500)
{
DoubleDouble actual = DDMath.Pow(input, -2);
actual.Low.ShouldNotBe(0.0);
AssertExactPowerBound(input, -2);
}
if (scale < 0)
{
DDMath.Pow(input, -1).Low.ShouldNotBe(0.0);
}
}
}
AssertExactPowerBound(new DoubleDouble(double.MaxValue), -1);
AssertExactPowerBound(new DoubleDouble(1e-308), -1);
AssertExactPowerBound(new DoubleDouble(1e308), -1);
}
[Fact]
public void SparseReciprocalCorrectionsSurviveBothRangeDirections()
{
// 1/(h+l) = 1/h - l/h^2 + O(l^2/h^3). In these dyadic
// cases the remainder is strictly below half an ulp of the stated low.
foreach (double sign in new[] { -1.0, 1.0 })
{
DoubleDouble tiny = DoubleDouble.FromComponents(Math.ScaleB(1.0, -500), sign * double.Epsilon);
AssertBits(DDMath.Pow(tiny, -1), Math.ScaleB(1.0, 500), -sign * Math.ScaleB(1.0, -74));
DoubleDouble huge = DoubleDouble.FromComponents(Math.ScaleB(1.0, 500), sign * Math.ScaleB(1.0, -74));
AssertBits(DDMath.Pow(huge, -1), Math.ScaleB(1.0, -500), -sign * double.Epsilon);
DoubleDouble tinySquare = DoubleDouble.FromComponents(Math.ScaleB(1.0, -250), sign * double.Epsilon);
AssertBits(DDMath.Pow(tinySquare, -2), Math.ScaleB(1.0, 500), -sign * Math.ScaleB(1.0, -323));
}
}
[Fact]
public void UnderflowMidpointAndAdjacentLowComponentsRespectBothSigns()
{
// (2^-215)^5 = 2^-1075, exactly the tie between zero and epsilon.
// The low perturbations strictly bracket the tie; reciprocals of
// the corresponding large inputs reverse the perturbation direction.
foreach (int exponent in new[] { -5, 5 })
{
int scale = exponent < 0 ? 215 : -215;
foreach (double lowSign in new[] { -1.0, 0.0, 1.0 })
{
bool aboveTie = lowSign * exponent > 0.0;
foreach (double sign in new[] { -1.0, 1.0 })
{
DoubleDouble input = DoubleDouble.FromComponents(sign * Math.ScaleB(1.0, scale),
sign * lowSign * Math.ScaleB(1.0, scale - 54));
AssertBits(DDMath.Pow(input, exponent), sign * (aboveTie ? double.Epsilon : 0.0), 0.0);
}
}
}
}
[Fact]
public void OverflowBoundaryUsesTheLowComponentBeforeFinalScaling()
{
foreach (int exponent in new[] { -4, 4 })
{
int scale = exponent < 0 ? -256 : 256;
double lowSign = exponent < 0 ? 1.0 : -1.0;
DoubleDouble finite = DoubleDouble.FromComponents(Math.ScaleB(1.0, scale),
lowSign * Math.ScaleB(1.0, scale - 55));
AssertExactPowerBound(finite, exponent);
DoubleDouble overflowing = DoubleDouble.FromComponents(Math.ScaleB(1.0, scale),
lowSign * Math.ScaleB(1.0, scale - 57));
AssertBits(DDMath.Pow(overflowing, exponent), double.PositiveInfinity, 0.0);
}
}
[Theory]
// ReferenceData/generate_pow_reference.py: exact stored binary inputs,
// Decimal integer powers at 160/240 digits, stable floor(value * 10^100).
[InlineData(1.0, 5.551115123125783e-17, -2147483648, "9999998807907175546458285654165775755995358529592462953428551373115894598504056800747655101172369356")]
[InlineData(1.0, 5.551115123125783e-17, 2147483647, "10000001192092966006977287879000612242425055148009778363959373202290565261791488930669591094667385585")]
[InlineData(1.0, -5.551115123125783e-17, -2147483648, "10000001192092966562088932540488693037989493813706046557380379233423355139596032355926918800247828081")]
[InlineData(1.0, -5.551115123125783e-17, 2147483647, "9999998807907176101569665617857901211208198096053270077732977033266503983279402161561236649189617033")]
[InlineData(1.0000000009313226, 5.169878828456423e-26, -2147483648, "1353352833626534812694377803744507919692475898909062344519276068330639808800815294171180036409012889")]
[InlineData(1.0000000009313226, 5.169878828456423e-26, 2147483647, "73890560851674615678069694043352842890214309016451211229831428411840370567020651340427147012473005377")]
[InlineData(0.9999999990686774, -5.169878828456423e-26, -2147483648, "73890561058122458050918958513594285934973647528626440535855911314733978878721028315311791064891371241")]
[InlineData(0.9999999990686774, -5.169878828456423e-26, 2147483647, "1353352832366126768492006308656122646859542550999041350022637326283256718420076811410419433559508500")]
[InlineData(1.00000003, 2.7755575615628914e-17, -2147483648, "1049038452726052018566777293807106599923174085199532650373208349817256939")]
[InlineData(1.00000003, 2.7755575615628914e-17, 2147483647, "95325387491886611267179809406372425766774105569877060927978408348321633401745440609674655439897287007094693160193846818457460816")]
public void ExtremeIntegerExponentsMeetHighPrecisionReferenceIntervals(
double high, double low, int exponent, string lowerNumerator)
{
BigInteger numerator = BigInteger.Parse(lowerNumerator, System.Globalization.CultureInfo.InvariantCulture);
BigInteger denominator = BigInteger.Pow(10, 100);
foreach (double sign in new[] { -1.0, 1.0 })
{
DoubleDouble input = DoubleDouble.FromComponents(sign * high, sign * low);
DoubleDouble actual = DDMath.Pow(input, exponent);
actual.Low.ShouldNotBe(0.0);
int resultSign = sign < 0.0 && (exponent & 1) != 0 ? -1 : 1;
// Check both interval endpoints so the reference uncertainty is not
// silently omitted from the claimed exponent-dependent tolerance.
AssertRationalBound(actual, resultSign * numerator, denominator, exponent, lowerNumerator);
AssertRationalBound(actual, resultSign * (numerator + 1), denominator, exponent, lowerNumerator);
}
}
private static void AssertExactPowerBound(DoubleDouble input, int exponent)
{
BigInteger numerator = Units(input.High) + Units(input.Low);
int trailing = (int)BigInteger.TrailingZeroCount(BigInteger.Abs(numerator));
numerator >>= trailing;
BigInteger denominator = BigInteger.One;
if (trailing <= 1074)
{
denominator <<= 1074 - trailing;
}
else
{
numerator <<= trailing - 1074;
}
int magnitude = Math.Abs(exponent); // This oracle is used only for small exponents.
numerator = BigInteger.Pow(numerator, magnitude);
denominator = BigInteger.Pow(denominator, magnitude);
if (exponent < 0)
{
(numerator, denominator) = (denominator * numerator.Sign, BigInteger.Abs(numerator));
}
AssertRationalBound(DDMath.Pow(input, exponent), numerator, denominator, exponent,
$"({input.High:R}, {input.Low:R})^{exponent}");
}
private static void AssertRationalBound(DoubleDouble actual, BigInteger numerator,
BigInteger denominator, int exponent, string context)
{
DoubleDouble.IsFinite(actual).ShouldBeTrue(context);
DoubleDouble.IsCanonical(actual).ShouldBeTrue(context);
BigInteger actualUnits = Units(actual.High) + Units(actual.Low);
BigInteger error = BigInteger.Abs(actualUnits * denominator - (numerator << 1074));
// |actual-exact| <= |exact|*(|n|+1)*2^-100 + 2^-1074,
// cross-multiplied exactly in units of the smallest subnormal.
BigInteger tolerance = (BigInteger.Abs(numerator) * (Math.Abs((long)exponent) + 1) << 974)
+ denominator;
(error <= tolerance).ShouldBeTrue($"Power 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();
}
}