284 lines
13 KiB
C#
284 lines
13 KiB
C#
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();
|
|
}
|
|
}
|