This commit is contained in:
@@ -978,6 +978,114 @@ public class DoubleDoubleArithmeticTests
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-450)]
|
||||
[InlineData(0)]
|
||||
[InlineData(450)]
|
||||
public void SquareRetainsExactCrossAndLowSquaredTerms(int exponent)
|
||||
{
|
||||
// (h - h*2^-54)^2 = h^2 - h^2*2^-53 + h^2*2^-108.
|
||||
// (h + h*2^-53)^2 = h^2 + h^2*2^-52 + h^2*2^-106.
|
||||
// Both sums fit exactly in two components, including fast-range endpoints.
|
||||
double high = Math.ScaleB(1.0, exponent);
|
||||
double highSquared = Math.ScaleB(1.0, 2 * exponent);
|
||||
DoubleDouble below = DoubleDouble.FromComponents(high, -Math.ScaleB(1.0, exponent - 54));
|
||||
DoubleDouble above = DoubleDouble.FromComponents(high, Math.ScaleB(1.0, exponent - 53));
|
||||
foreach (double sign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
CheckBoundary(DoubleDouble.Square(sign * below), Math.BitDecrement(highSquared), Math.ScaleB(1.0, (2 * exponent) - 108));
|
||||
CheckBoundary(DDMath.Square(sign * above), Math.BitIncrement(highSquared), Math.ScaleB(1.0, (2 * exponent) - 106));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SquareHandlesSpecialValuesAndExactBinaryPowers()
|
||||
{
|
||||
foreach (double value in new[] { 0.0, -0.0, double.NaN, double.PositiveInfinity, double.NegativeInfinity })
|
||||
{
|
||||
CheckBits(DoubleDouble.Square(new DoubleDouble(value)), value * value);
|
||||
CheckBits(DDMath.Square(new DoubleDouble(value)), value * value);
|
||||
}
|
||||
for (int exponent = -1074; exponent <= 1023; exponent++)
|
||||
{
|
||||
// A binary power squared needs only exponent arithmetic, not a DD oracle.
|
||||
double high = Math.ScaleB(1.0, exponent);
|
||||
double expected = Math.ScaleB(1.0, 2 * exponent);
|
||||
CheckBits(DoubleDouble.Square(new DoubleDouble(high)), expected);
|
||||
CheckBits(DoubleDouble.Square(new DoubleDouble(-high)), expected);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SquareMeetsExactRationalBoundsAcrossRangesAndDispatchTransitions()
|
||||
{
|
||||
Random random = new(57721);
|
||||
for (int exponent = -1074; exponent <= 1023; exponent++)
|
||||
{
|
||||
double high = Math.ScaleB(1.0 + random.NextDouble(), exponent);
|
||||
double low = Math.ScaleB(random.NextDouble(), exponent - 53);
|
||||
foreach (double residual in new[] { 0.0, low, -low, double.Epsilon, -double.Epsilon })
|
||||
{
|
||||
AssertSquare(DoubleDouble.FromComponents(high, residual));
|
||||
}
|
||||
double power = Math.ScaleB(1.0, exponent);
|
||||
AssertSquare(new DoubleDouble(Math.BitDecrement(power)));
|
||||
AssertSquare(new DoubleDouble(power));
|
||||
AssertSquare(new DoubleDouble(Math.BitIncrement(power)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SquareUsesTheCompleteInputForOverflowAndUnderflowClassification()
|
||||
{
|
||||
// Neighborhoods of sqrt(overflow midpoint) and sqrt(epsilon/2).
|
||||
// The exact integer oracle, not Math.Sqrt or another DD operation,
|
||||
// decides which side of each threshold the actual stored input lies on.
|
||||
double overflowHigh = Math.ScaleB(1.0, 512);
|
||||
// At low=-2^457, the square exceeds the midpoint by low^2;
|
||||
// the next more-negative low puts it strictly below the midpoint.
|
||||
double overflowLow = -Math.ScaleB(1.0, 457);
|
||||
double underflowHigh = Math.ScaleB(Math.Sqrt(0.5), -537);
|
||||
double underflowLow = Math.ScaleB(1.0, -591);
|
||||
foreach (double high in new[] { Math.BitDecrement(overflowHigh), overflowHigh, Math.BitIncrement(overflowHigh) })
|
||||
{
|
||||
foreach (double low in new[] { Math.BitDecrement(overflowLow), overflowLow, Math.BitIncrement(overflowLow), 0.0 })
|
||||
{
|
||||
AssertSquare(DoubleDouble.FromComponents(high, low));
|
||||
}
|
||||
}
|
||||
foreach (double high in new[] { Math.BitDecrement(underflowHigh), underflowHigh, Math.BitIncrement(underflowHigh) })
|
||||
{
|
||||
foreach (double low in new[] { -underflowLow, 0.0, underflowLow })
|
||||
{
|
||||
AssertSquare(DoubleDouble.FromComponents(high, low));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertSquare(DoubleDouble value)
|
||||
{
|
||||
BigInteger input = Units(value);
|
||||
BigInteger numerator = input * input;
|
||||
BigInteger denominator = BigInteger.One << 1074;
|
||||
BigInteger overflow = Units(double.MaxValue) + Units(Math.ScaleB(1.0, 970));
|
||||
DoubleDouble actual = DoubleDouble.Square(value);
|
||||
DoubleDouble.IsCanonical(actual).ShouldBeTrue();
|
||||
DoubleDouble.IsNegative(actual).ShouldBeFalse();
|
||||
if (numerator >= overflow * denominator)
|
||||
{
|
||||
CheckBits(actual, double.PositiveInfinity);
|
||||
}
|
||||
else
|
||||
{
|
||||
AssertRelative(actual, numerator, denominator);
|
||||
// The absolute error floor must not hide a wrong zero/nonzero result.
|
||||
DoubleDouble.IsZero(actual).ShouldBe((numerator << 1) <= denominator);
|
||||
}
|
||||
CheckBoundary(DoubleDouble.Square(-value), actual.High, actual.Low);
|
||||
CheckBoundary(DDMath.Square(value), actual.High, actual.Low);
|
||||
}
|
||||
|
||||
private static (double[] Scalars, DoubleDouble[] Values) ScalarArithmeticCompatibilityCases()
|
||||
{
|
||||
List<double> scalars = [0.0, -0.0, 1.0, -1.0, 2.0, -2.0, 3.0, -3.0, 100.0, -100.0,
|
||||
|
||||
@@ -4,6 +4,79 @@ namespace Just.PreciseMath.Tests;
|
||||
|
||||
public class DoubleDoubleFormattingTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(1.25, 0.0, "1.25")]
|
||||
[InlineData(-1.25, 0.0, "-1.25")]
|
||||
[InlineData(1000.0, 0.0, "1000")]
|
||||
[InlineData(1.0, 5.551115123125783e-17, "1.000000000000000055511151231257827021181583404541015625")]
|
||||
[InlineData(9007199254740992.0, 1.0, "9007199254740993")]
|
||||
public void ExactFormattingPreservesTheCompleteDyadicValue(double high, double low, string expected)
|
||||
{
|
||||
// Exact binary fractions: the nonzero fractional low is 2^-54.
|
||||
DoubleDouble value = DoubleDouble.FromComponents(high, low);
|
||||
value.ToStringExact().ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExactFormattingUsesInvariantSpecialSymbolsAndPreservesZeroSigns()
|
||||
{
|
||||
CultureInfo original = CultureInfo.CurrentCulture;
|
||||
try
|
||||
{
|
||||
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("fr-FR");
|
||||
new DoubleDouble(-1.25).ToStringExact().ShouldBe("-1.25");
|
||||
DoubleDouble[] values = [DoubleDouble.Zero, DoubleDouble.NegativeZero,
|
||||
DoubleDouble.NaN, DoubleDouble.PositiveInfinity, DoubleDouble.NegativeInfinity];
|
||||
string[] expected = ["0", "-0", "NaN", "Infinity", "-Infinity"];
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
string text = values[i].ToStringExact();
|
||||
text.ShouldBe(expected[i]);
|
||||
DoubleDouble parsed = DoubleDouble.Parse(text, CultureInfo.InvariantCulture);
|
||||
BitConverter.DoubleToInt64Bits(parsed.High).ShouldBe(BitConverter.DoubleToInt64Bits(values[i].High));
|
||||
BitConverter.DoubleToInt64Bits(parsed.Low).ShouldBe(0L);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
CultureInfo.CurrentCulture = original;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExactFormattingPreservesSparsePairsBeyondTheStandardPrecisionLimit()
|
||||
{
|
||||
DoubleDouble[] values = [new(double.Epsilon), new(double.MaxValue), DoubleDouble.Pi,
|
||||
DoubleDouble.FromComponents(1.0, double.Epsilon),
|
||||
DoubleDouble.FromComponents(1.0, -double.Epsilon),
|
||||
DoubleDouble.FromComponents(double.MaxValue, double.Epsilon),
|
||||
DoubleDouble.FromComponents(double.MaxValue, -double.Epsilon),
|
||||
DoubleDouble.FromComponents(double.MaxValue, Math.BitDecrement(Math.ScaleB(1.0, 970)))];
|
||||
foreach (DoubleDouble value in values)
|
||||
{
|
||||
AssertExactText(value);
|
||||
AssertExactText(-value);
|
||||
}
|
||||
DoubleDouble.FromComponents(1.0, double.Epsilon).ToStringExact().Length.ShouldBeGreaterThan(999);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExactFormattingMatchesIndependentIntegerValuesAcrossEveryExponent()
|
||||
{
|
||||
Random random = new(271828);
|
||||
for (int exponent = -1074; exponent <= 1023; exponent++)
|
||||
{
|
||||
double high = Math.ScaleB(1.0 + random.NextDouble(), exponent);
|
||||
double denseLow = Math.ScaleB(random.NextDouble(), exponent - 53);
|
||||
foreach (double low in new[] { 0.0, denseLow, -denseLow, double.Epsilon, -double.Epsilon })
|
||||
{
|
||||
DoubleDouble value = DoubleDouble.FromComponents(high, low);
|
||||
AssertExactText(value);
|
||||
AssertExactText(-value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("G0")]
|
||||
[InlineData("g0")]
|
||||
@@ -138,4 +211,40 @@ public class DoubleDoubleFormattingTests
|
||||
{
|
||||
Should.Throw<FormatException>(() => DoubleDouble.One.ToString(format, CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
private static void AssertExactText(DoubleDouble value)
|
||||
{
|
||||
string text = value.ToStringExact();
|
||||
text.Length.ShouldBeLessThanOrEqualTo(2048);
|
||||
text.ShouldNotContain("E");
|
||||
text.ShouldNotContain("e");
|
||||
int point = text.IndexOf('.', StringComparison.Ordinal);
|
||||
int places = point < 0 ? 0 : text.Length - point - 1;
|
||||
if (point >= 0)
|
||||
{
|
||||
text.ShouldNotEndWith("0");
|
||||
text.ShouldNotEndWith(".");
|
||||
}
|
||||
string digits = point < 0 ? text : text.Remove(point, 1);
|
||||
BigInteger coefficient = BigInteger.Parse(digits, CultureInfo.InvariantCulture);
|
||||
// Independently decode the input as integer multiples of 2^-1074 and
|
||||
// compare with the printed integer coefficient / 10^places exactly.
|
||||
BigInteger units = ExactFormattingUnits(value.High) + ExactFormattingUnits(value.Low);
|
||||
(coefficient << 1074).ShouldBe(units * BigInteger.Pow(10, places));
|
||||
DoubleDouble parsed = DoubleDouble.Parse(text, CultureInfo.InvariantCulture);
|
||||
BitConverter.DoubleToInt64Bits(parsed.High).ShouldBe(BitConverter.DoubleToInt64Bits(value.High));
|
||||
BitConverter.DoubleToInt64Bits(parsed.Low).ShouldBe(BitConverter.DoubleToInt64Bits(value.Low));
|
||||
}
|
||||
|
||||
private static BigInteger ExactFormattingUnits(double value)
|
||||
{
|
||||
long bits = BitConverter.DoubleToInt64Bits(value);
|
||||
int exponent = (int)((bits >> 52) & 0x7ff);
|
||||
BigInteger significand = bits & 0xfffffffffffffL;
|
||||
if (exponent != 0)
|
||||
{
|
||||
significand = (significand + (BigInteger.One << 52)) << (exponent - 1);
|
||||
}
|
||||
return bits < 0 ? -significand : significand;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,6 +202,83 @@ public class PreciseMathSqrtTests
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-1.0)]
|
||||
[InlineData(1.0)]
|
||||
public void ScalingPreservesRepresentableSparseRootCorrections(double sign)
|
||||
{
|
||||
// sqrt(2^1000 + d) = 2^500 + d/2^501 + O(d^2/2^1500).
|
||||
// Scaling d=+/-2^-174 down by 2^1000 erases it, but the root's
|
||||
// +/-2^-675 low is representable. Prove its rounding interval exactly.
|
||||
DoubleDouble input = DoubleDouble.FromComponents(Math.ScaleB(1.0, 1000), Math.ScaleB(sign, -174));
|
||||
AssertSparseRoot(input, Math.ScaleB(1.0, 500), Math.ScaleB(sign, -675));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SparseRootCorrectionsSurviveScalingAndCorrectionDivisionBoundaries()
|
||||
{
|
||||
// Exact squares with both even and odd high exponents. Include scaled
|
||||
// lows on both sides of the separate-correction dispatch, subnormal
|
||||
// scaled lows, fully erased scaled lows, and subnormal output lows.
|
||||
foreach (int exponent in new[] { 0, 2, 100, 1000, 1022 })
|
||||
{
|
||||
foreach (double rootMantissa in new[] { 1.0, 1.5 })
|
||||
{
|
||||
double high = Math.ScaleB(rootMantissa * rootMantissa, exponent);
|
||||
double rootHigh = Math.ScaleB(rootMantissa, exponent / 2);
|
||||
foreach (int gap in new[] { -1019, -1020, -1021, -1074, -1075, -1174 })
|
||||
{
|
||||
double low = Math.ScaleB(1.0, exponent + gap);
|
||||
double rootLow = Math.ScaleB(low, -(exponent / 2)) / (2.0 * rootMantissa);
|
||||
if (rootLow == 0.0)
|
||||
{
|
||||
continue; // This matrix targets representable nonzero corrections.
|
||||
}
|
||||
foreach (double sign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
// AssertSparseRoot independently proves this candidate's
|
||||
// rounding interval; the first-order formula is not an oracle.
|
||||
AssertSparseRoot(DoubleDouble.FromComponents(high, sign * low), rootHigh, sign * rootLow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SparseRootDispatchNeighborsRetainBothSignsWithoutDoubleCounting()
|
||||
{
|
||||
// With high=2^1000, lows around 2^-20 scale to the dispatch at 2^-1020.
|
||||
// Keep the predicted output normal so candidate scaling is exact.
|
||||
double threshold = Math.ScaleB(1.0, -20);
|
||||
foreach (double low in new[] { Math.BitDecrement(threshold), threshold, Math.BitIncrement(threshold) })
|
||||
{
|
||||
foreach (double sign in new[] { -1.0, 1.0 })
|
||||
{
|
||||
DoubleDouble input = DoubleDouble.FromComponents(Math.ScaleB(1.0, 1000), sign * low);
|
||||
AssertSparseRoot(input, Math.ScaleB(1.0, 500), Math.ScaleB(sign * low, -501));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertSparseRoot(DoubleDouble input, double high, double low)
|
||||
{
|
||||
BigInteger x = (Units(input.High) + Units(input.Low)) << 1074;
|
||||
BigInteger center = Units(high) + Units(low);
|
||||
BigInteger previous = Units(high) + Units(Math.BitDecrement(low));
|
||||
BigInteger following = Units(high) + Units(Math.BitIncrement(low));
|
||||
// Strict bounds avoid relying on a tie rule or a rounded sqrt oracle.
|
||||
((previous + center) * (previous + center) < (x << 2)).ShouldBeTrue();
|
||||
((center + following) * (center + following) > (x << 2)).ShouldBeTrue();
|
||||
DoubleDouble actual = DoubleDouble.Sqrt(input);
|
||||
actual.High.ShouldBe(high);
|
||||
BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(BitConverter.DoubleToInt64Bits(low));
|
||||
DoubleDouble facade = DDMath.Sqrt(input);
|
||||
BitConverter.DoubleToInt64Bits(facade.High).ShouldBe(BitConverter.DoubleToInt64Bits(actual.High));
|
||||
BitConverter.DoubleToInt64Bits(facade.Low).ShouldBe(BitConverter.DoubleToInt64Bits(actual.Low));
|
||||
AssertSqrtBound(input, actual);
|
||||
}
|
||||
|
||||
private static void AssertSqrtBound(DoubleDouble input, DoubleDouble actual)
|
||||
{
|
||||
DoubleDouble.IsFinite(actual).ShouldBeTrue();
|
||||
|
||||
Reference in New Issue
Block a user