This commit is contained in:
@@ -141,7 +141,21 @@ public class ArithmeticRangeTests
|
||||
DoubleDouble finiteNormalized = PreciseMathHelper.NormalizeFinite(quotient, correction);
|
||||
BitConverter.DoubleToInt64Bits(finiteNormalized.High).ShouldBe(BitConverter.DoubleToInt64Bits(normalized.High));
|
||||
BitConverter.DoubleToInt64Bits(finiteNormalized.Low).ShouldBe(BitConverter.DoubleToInt64Bits(normalized.Low));
|
||||
expected = normalized + finalCorrection;
|
||||
// Pin the final-add caller invariants before removing public
|
||||
// dispatch. Sparse correction products above can use the exact
|
||||
// fallback; these bounds must cover their results too.
|
||||
normalized.High.ShouldNotBe(0.0);
|
||||
(Math.Abs(correction) < Math.Abs(quotient) * 0.5).ShouldBeTrue();
|
||||
(Math.Abs(normalized.High) < Math.ScaleB(1.0, 909)).ShouldBeTrue();
|
||||
(Math.Abs(remainder.High) < Math.ScaleB(1.0, 470)).ShouldBeTrue();
|
||||
(Math.Abs(finalCorrection) < Math.ScaleB(1.0, 921)).ShouldBeTrue();
|
||||
PreciseMathHelper.IsAdditionWithinFastRange(normalized.High).ShouldBeTrue();
|
||||
PreciseMathHelper.IsAdditionWithinFastRange(finalCorrection).ShouldBeTrue();
|
||||
(Math.ILogB(normalized.High) <= 1020).ShouldBeTrue();
|
||||
(Math.ILogB(finalCorrection) <= 1020).ShouldBeTrue();
|
||||
expected = PreviousFiniteScalarAdd(normalized.High, normalized.Low, finalCorrection);
|
||||
AssertComponentBits(normalized + finalCorrection, expected);
|
||||
AssertComponentBits(PreciseMathHelper.AddScalarFinite(normalized.High, normalized.Low, finalCorrection), expected);
|
||||
actual = left / right;
|
||||
}
|
||||
else if (operation == "*")
|
||||
@@ -231,6 +245,58 @@ public class ArithmeticRangeTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScalarAdditionPreservesThePreviousFiniteSequence()
|
||||
{
|
||||
// Characterization, not an accuracy oracle. Include cancellation, sparse
|
||||
// lows, signed high zeros, and the negated zero low used by subtraction.
|
||||
double[] highs = [0.0, -0.0, double.Epsilon, -double.Epsilon,
|
||||
Math.ScaleB(1.0, -1022), 1.0, -1.0, Math.BitDecrement(2.0),
|
||||
Math.BitDecrement(Math.ScaleB(1.0, 909)),
|
||||
-Math.BitDecrement(Math.ScaleB(1.0, 909)),
|
||||
Math.BitDecrement(Math.ScaleB(1.0, 921)),
|
||||
-Math.BitDecrement(Math.ScaleB(1.0, 921)),
|
||||
Math.BitDecrement(Math.ScaleB(1.0, 1021))];
|
||||
foreach (double high in highs)
|
||||
{
|
||||
foreach (double low in new[] { 0.0, -0.0, double.Epsilon, -double.Epsilon,
|
||||
Math.ScaleB(high, -54), -Math.ScaleB(high, -54) })
|
||||
{
|
||||
DoubleDouble value = DoubleDouble.FromComponents(high, low);
|
||||
foreach (double scalar in highs.Concat([-high, Math.BitIncrement(high), Math.BitDecrement(high)]))
|
||||
{
|
||||
if ((value.High == 0.0 && scalar == 0.0)
|
||||
|| Math.ILogB(value.High) > 1020 || Math.ILogB(scalar) > 1020)
|
||||
{
|
||||
continue; // Public signed-zero and boundary dispatch remain.
|
||||
}
|
||||
foreach (double signedLow in value.Low == 0.0 ? new[] { 0.0, -0.0 } : new[] { value.Low })
|
||||
{
|
||||
DoubleDouble expected = PreviousFiniteScalarAdd(value.High, signedLow, scalar);
|
||||
AssertComponentBits(PreciseMathHelper.AddScalar(value.High, signedLow, scalar), expected);
|
||||
AssertComponentBits(PreciseMathHelper.AddScalarFinite(value.High, signedLow, scalar), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static DoubleDouble PreviousFiniteScalarAdd(double high, double low, double value)
|
||||
{
|
||||
// Freeze the pre-extraction scalar sequence and public normalization.
|
||||
// Do not redirect this reference to the extracted production helper.
|
||||
(double sum, double error) = PreciseMathHelper.TwoAdd(high, value);
|
||||
(double middle, double middleError) = PreciseMathHelper.TwoAdd(error, low);
|
||||
(double result, double residual) = PreciseMathHelper.TwoAdd(sum, middle);
|
||||
return DoubleDouble.FromComponents(result, residual + middleError);
|
||||
}
|
||||
|
||||
private static void AssertComponentBits(DoubleDouble actual, DoubleDouble expected)
|
||||
{
|
||||
BitConverter.DoubleToInt64Bits(actual.High).ShouldBe(BitConverter.DoubleToInt64Bits(expected.High));
|
||||
BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(BitConverter.DoubleToInt64Bits(expected.Low));
|
||||
}
|
||||
|
||||
private static void AssertDivisionResidualCancellation(DoubleDouble remainder, DoubleDouble product)
|
||||
{
|
||||
DoubleDouble expected = remainder - product;
|
||||
|
||||
@@ -261,6 +261,91 @@ public class PreciseMathSqrtTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FiniteNormalizationMatchesBothPreviousSqrtSitesAcrossTheRange()
|
||||
{
|
||||
// Replay the unchanged scaled Newton sequence with public factories.
|
||||
// This pins normalization compatibility, not root accuracy: the exact
|
||||
// squared-inequality tests above remain the independent accuracy oracle.
|
||||
bool sawSeparateLow = false;
|
||||
bool sawZeroCorrection = false;
|
||||
bool sawNonzeroCorrection = false;
|
||||
foreach (DoubleDouble input in NormalizationInputs())
|
||||
{
|
||||
int exponent = Math.ILogB(input.High) & ~1;
|
||||
double high = Math.ScaleB(input.High, -exponent);
|
||||
double low = Math.ScaleB(input.Low, -exponent);
|
||||
bool separateLow = exponent > 0 && input.Low != 0.0 && Math.Abs(low) < Math.ScaleB(1.0, -1020);
|
||||
sawSeparateLow |= separateLow;
|
||||
if (separateLow)
|
||||
{
|
||||
low = 0.0;
|
||||
}
|
||||
double estimate = Math.Sqrt(high);
|
||||
double residual = Math.FusedMultiplyAdd(-estimate, estimate, high) + low;
|
||||
double correction = residual / (2.0 * estimate);
|
||||
sawZeroCorrection |= correction == 0.0;
|
||||
sawNonzeroCorrection |= correction != 0.0;
|
||||
(estimate >= 1.0 && estimate <= 2.0).ShouldBeTrue();
|
||||
(Math.Abs(correction) < Math.ScaleB(1.0, -48)).ShouldBeTrue();
|
||||
(estimate + correction < 4.0).ShouldBeTrue();
|
||||
DoubleDouble root = AssertFiniteNormalization(estimate, correction);
|
||||
|
||||
double scaledHigh = Math.ScaleB(root.High, exponent / 2);
|
||||
double scaledLow = Math.ScaleB(root.Low, exponent / 2);
|
||||
(Math.Abs(scaledHigh) < Math.ScaleB(1.0, 514)).ShouldBeTrue();
|
||||
(Math.Abs(scaledLow) < Math.ScaleB(1.0, 461)).ShouldBeTrue();
|
||||
(scaledHigh + scaledLow < Math.ScaleB(1.0, 514)).ShouldBeTrue();
|
||||
DoubleDouble expected = AssertFiniteNormalization(scaledHigh, scaledLow);
|
||||
if (!separateLow)
|
||||
{
|
||||
DoubleDouble actual = DoubleDouble.Sqrt(input);
|
||||
BitConverter.DoubleToInt64Bits(actual.High).ShouldBe(BitConverter.DoubleToInt64Bits(expected.High));
|
||||
BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(BitConverter.DoubleToInt64Bits(expected.Low));
|
||||
}
|
||||
// The sparse branch also passed both site checks. Its additional
|
||||
// correction is pinned independently by AssertSparseRoot above.
|
||||
}
|
||||
sawSeparateLow.ShouldBeTrue();
|
||||
sawZeroCorrection.ShouldBeTrue();
|
||||
sawNonzeroCorrection.ShouldBeTrue();
|
||||
}
|
||||
|
||||
private static IEnumerable<DoubleDouble> NormalizationInputs()
|
||||
{
|
||||
for (int exponent = -1074; exponent <= 1023; ++exponent)
|
||||
{
|
||||
foreach (double significand in new[] { 1.0, Math.BitIncrement(1.0), 1.5, Math.BitDecrement(2.0) })
|
||||
{
|
||||
double high = Math.ScaleB(significand, exponent);
|
||||
double dense = Math.ScaleB(1.0, exponent - 54);
|
||||
double threshold = Math.ScaleB(1.0, (exponent & ~1) - 1020);
|
||||
foreach (double low in new[] { 0.0, dense, -dense, double.Epsilon, -double.Epsilon,
|
||||
Math.BitDecrement(threshold), threshold, Math.BitIncrement(threshold), -threshold })
|
||||
{
|
||||
DoubleDouble input = DoubleDouble.FromComponents(high, low);
|
||||
if (input.High > 0.0 && DoubleDouble.IsFinite(input))
|
||||
{
|
||||
yield return input;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
yield return DoubleDouble.FromComponents(double.MaxValue, Math.BitDecrement(Math.ScaleB(1.0, 970)));
|
||||
}
|
||||
|
||||
private static DoubleDouble AssertFiniteNormalization(double high, double low)
|
||||
{
|
||||
double.IsFinite(high).ShouldBeTrue();
|
||||
double.IsFinite(low).ShouldBeTrue();
|
||||
double.IsFinite(high + low).ShouldBeTrue();
|
||||
DoubleDouble expected = DoubleDouble.FromComponents(high, low);
|
||||
DoubleDouble actual = PreciseMathHelper.NormalizeFinite(high, low);
|
||||
BitConverter.DoubleToInt64Bits(actual.High).ShouldBe(BitConverter.DoubleToInt64Bits(expected.High));
|
||||
BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(BitConverter.DoubleToInt64Bits(expected.Low));
|
||||
return expected;
|
||||
}
|
||||
|
||||
private static void AssertSparseRoot(DoubleDouble input, double high, double low)
|
||||
{
|
||||
BigInteger x = (Units(input.High) + Units(input.Low)) << 1074;
|
||||
|
||||
Reference in New Issue
Block a user