added sanity checks
.NET Test / .NET tests (push) Successful in 3m55s

This commit is contained in:
2026-09-18 14:10:24 +04:00
parent b5a8bd2580
commit 5797bf4884
44 changed files with 419 additions and 146 deletions
+1 -10
View File
@@ -36,16 +36,7 @@ public static partial class DDMath
return PreciseMathHelper.DivideBoundary(one, value); return PreciseMathHelper.DivideBoundary(one, value);
} }
double quotient = one / value._high; return PreciseMathHelper.DivideScalarFinite(one, value);
double remainder = Math.FusedMultiplyAdd(-quotient, value._high, one);
remainder = Math.FusedMultiplyAdd(-quotient, value._low, remainder);
double correction = remainder / value._high;
// For normalized input, using the high denominator in the correction
// adds only O(u^2) error, with u=2^-53. The denominator exponent guard
// keeps the quotient normal and finite; it dominates the correction,
// so QuickTwoSum is ordered and its sum remains finite.
(double high, double low) = PreciseMathHelper.TwoQuickAdd(quotient, correction);
return new DoubleDouble(high, low == 0.0 ? 0.0 : low);
} }
/// <inheritdoc cref="DoubleDouble.Cbrt"/> /// <inheritdoc cref="DoubleDouble.Cbrt"/>
@@ -103,24 +103,28 @@ public readonly partial struct DoubleDouble :
} }
/// <summary>Applies the expansion operation without discarding the low component.</summary> /// <summary>Applies the expansion operation without discarding the low component.</summary>
/// <remarks>Preserves the component bits of addition with the equivalent zero-low scalar expansion.</remarks>
public static DoubleDouble operator +(DoubleDouble left, double right) public static DoubleDouble operator +(DoubleDouble left, double right)
{ {
return PreciseMathHelper.AddScalar(left._high, left._low, right); return PreciseMathHelper.AddScalar(left._high, left._low, right);
} }
/// <summary>Applies the expansion operation without discarding the low component.</summary> /// <summary>Applies the expansion operation without discarding the low component.</summary>
/// <remarks>Preserves the component bits of addition with the equivalent zero-low scalar expansion.</remarks>
public static DoubleDouble operator +(double left, DoubleDouble right) public static DoubleDouble operator +(double left, DoubleDouble right)
{ {
return right + left; return right + left;
} }
/// <summary>Applies the expansion operation without discarding the low component.</summary> /// <summary>Applies the expansion operation without discarding the low component.</summary>
/// <remarks>Preserves the component bits of subtraction with the equivalent zero-low scalar expansion.</remarks>
public static DoubleDouble operator -(DoubleDouble left, double right) public static DoubleDouble operator -(DoubleDouble left, double right)
{ {
return PreciseMathHelper.AddScalar(left._high, left._low, -right); return PreciseMathHelper.AddScalar(left._high, left._low, -right);
} }
/// <summary>Applies the expansion operation without discarding the low component.</summary> /// <summary>Applies the expansion operation without discarding the low component.</summary>
/// <remarks>Preserves the component bits of subtraction with the equivalent zero-low scalar expansion.</remarks>
public static DoubleDouble operator -(double left, DoubleDouble right) public static DoubleDouble operator -(double left, DoubleDouble right)
{ {
// Negate the components, not the result: exact cancellation must yield +0. // Negate the components, not the result: exact cancellation must yield +0.
@@ -153,7 +157,8 @@ public readonly partial struct DoubleDouble :
return right * left; return right * left;
} }
/// <summary>Applies the expansion operation without discarding the low component.</summary> /// <summary>Divides by a scalar using a quotient estimate and two residual corrections.</summary>
/// <remarks>Preserves the component bits of division by the equivalent zero-low expansion.</remarks>
public static DoubleDouble operator /(DoubleDouble left, double right) public static DoubleDouble operator /(DoubleDouble left, double right)
{ {
if (!IsFinite(left) || !double.IsFinite(right) || left._high == 0.0 || right == 0.0) if (!IsFinite(left) || !double.IsFinite(right) || left._high == 0.0 || right == 0.0)
@@ -166,15 +171,24 @@ public readonly partial struct DoubleDouble :
} }
double quotient = left._high / right; double quotient = left._high / right;
double remainder = Math.FusedMultiplyAdd(-quotient, right, left._high); // The initial product is near left.High, so the exponent guard makes
double correction = (remainder + left._low) / right; // TwoMultiply exact. Keep the complete remainder, including its low.
// The exponent guard keeps the quotient normal. The correction is (double product, double productError) = PreciseMathHelper.TwoMultiply(right, quotient);
// O(u * quotient), so QuickTwoSum is ordered; one correction gives O(u^2) error. DoubleDouble remainder = PreciseMathHelper.AddFinite(left._high, left._low, -product, -productError);
(double high, double low) = PreciseMathHelper.TwoQuickAdd(quotient, correction); double correction = remainder._high / right;
return new DoubleDouble(high, low == 0.0 ? 0.0 : low); (double correctionProduct, double correctionError) = PreciseMathHelper.TwoMultiply(right, correction);
// This product can be subnormal. FMA rounds its residual just as the
// exact boundary product does; normalize and canonicalize its zero low.
DoubleDouble correctedProduct = PreciseMathHelper.NormalizeFinite(correctionProduct, correctionError);
double finalRemainder = PreciseMathHelper.SubtractDivisionCorrectionHigh(remainder, correctedProduct);
double finalCorrection = finalRemainder / right;
// The same finite bounds as DD/DD apply. Normalize the first correction
// before adding the last, preserving the DD/DD evaluation order.
return PreciseMathHelper.NormalizeFinite(quotient, correction) + finalCorrection;
} }
/// <summary>Applies the expansion operation without discarding the low component.</summary> /// <summary>Divides a scalar using a quotient estimate and two residual corrections.</summary>
/// <remarks>Preserves the component bits of division with the equivalent zero-low numerator.</remarks>
public static DoubleDouble operator /(double left, DoubleDouble right) public static DoubleDouble operator /(double left, DoubleDouble right)
{ {
if (!double.IsFinite(left) || !IsFinite(right) || left == 0.0 || right._high == 0.0) if (!double.IsFinite(left) || !IsFinite(right) || left == 0.0 || right._high == 0.0)
@@ -186,13 +200,6 @@ public readonly partial struct DoubleDouble :
return PreciseMathHelper.DivideBoundary(left, right); return PreciseMathHelper.DivideBoundary(left, right);
} }
double quotient = left / right._high; return PreciseMathHelper.DivideScalarFinite(left, right);
double remainder = Math.FusedMultiplyAdd(-quotient, right._high, left);
remainder = Math.FusedMultiplyAdd(-quotient, right._low, remainder);
double correction = remainder / right._high;
// Using the high denominator in the correction adds only O(u^2) error.
// As above, the guarded quotient dominates its correction in magnitude.
(double high, double low) = PreciseMathHelper.TwoQuickAdd(quotient, correction);
return new DoubleDouble(high, low == 0.0 ? 0.0 : low);
} }
} }
+27 -6
View File
@@ -104,7 +104,25 @@ internal static class PreciseMathHelper
return NormalizeFinite(sum, sumError + (middleError + lowError)); return NormalizeFinite(sum, sumError + (middleError + lowError));
} }
// Only for DD division's second remainder: normalized finite inputs with highs // Finite, nonzero numerator and normalized denominator, both with high
// exponents in [-450, 450]. Shared with Reciprocal after its constant-numerator
// checks. Do not promote left or dispatch to the DD/DD division operator.
internal static DoubleDouble DivideScalarFinite(double left, DoubleDouble right)
{
double quotient = left / right._high;
DoubleDouble product = right * quotient;
// Preserve DD/DD's complete first remainder. A chained scalar FMA drops
// residual bits needed by the second correction; retain the expansion
// subtraction before computing the next quotient correction.
DoubleDouble remainder = AddFinite(left, 0.0, -product._high, -product._low);
double correction = remainder._high / right._high;
double finalRemainder = SubtractDivisionCorrectionHigh(remainder, right * correction);
double finalCorrection = finalRemainder / right._high;
// DD/DD's guard-derived finite bounds and normalization order apply.
return NormalizeFinite(quotient, correction) + finalCorrection;
}
// Only for division's second remainder: normalized finite inputs with highs
// bounded by 2^462, including the correction product's exact fallback results. // bounded by 2^462, including the correction product's exact fallback results.
// Same sign and normal binade imply Sterbenz-exact high subtraction. Its error // Same sign and normal binade imply Sterbenz-exact high subtraction. Its error
// is +0; with canonical input lows, TwoAdd(+0, lowSum) is (lowSum, +0). // is +0; with canonical input lows, TwoAdd(+0, lowSum) is (lowSum, +0).
@@ -164,11 +182,14 @@ internal static class PreciseMathHelper
} }
(double sum, double error) = TwoAdd(high, value); (double sum, double error) = TwoAdd(high, value);
// Near high-component cancellation, Sterbenz makes the first sum exact, // Specialize AddFinite for a zero-low scalar: adding that zero to low
// so error is zero and this retains low exactly. Otherwise its rounding // has no residual. Retain the rounding error when combining the high
// contributes only O(u^2) relative error. The final TwoSum normalizes. // sum's error with low, then fold it into the final normalization.
(double result, double residual) = TwoAdd(sum, error + low); // TwoSum also absorbs a negated zero low from scalar-left subtraction.
return new DoubleDouble(result, residual == 0.0 ? 0.0 : residual); (double middle, double middleError) = TwoAdd(error, low);
(double result, double residual) = TwoAdd(sum, middle);
// The unchanged addition guard gives AddFinite's finite-sum bounds.
return NormalizeFinite(result, residual + middleError);
} }
// Keep the complete BigInteger expressions out of ordinary arithmetic bodies, // Keep the complete BigInteger expressions out of ordinary arithmetic bodies,
+5
View File
@@ -18,4 +18,9 @@
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Using Include="System.Numerics"/>
<Using Include="Shouldly"/>
<Using Include="Xunit"/>
</ItemGroup>
</Project> </Project>
@@ -1,6 +1,3 @@
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
public class ArithmeticRangeTests public class ArithmeticRangeTests
@@ -1,7 +1,3 @@
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
public class DoubleDoubleArithmeticTests public class DoubleDoubleArithmeticTests
@@ -332,6 +328,53 @@ public class DoubleDoubleArithmeticTests
Check(6.0 / new DoubleDouble(2.0), 3.0, 0.0); Check(6.0 / new DoubleDouble(2.0), 3.0, 0.0);
} }
[Theory]
[InlineData(2.0)]
[InlineData(3.0)]
public void ScalarAdditionAndSubtractionRetainTheMiddleSumResidual(double magnitude)
{
// Exact Fraction arithmetic on the stored E components confirms that
// all signed E +/- 2 and E +/- 3 results fit exactly in two components.
// Compare exact dyadic sums, not another overload or a rounded tolerance.
foreach (double valueSign in new[] { -1.0, 1.0 })
{
foreach (double scalarSign in new[] { -1.0, 1.0 })
{
DoubleDouble value = valueSign * DoubleDouble.E;
double scalar = scalarSign * magnitude;
BigInteger expectedSum = Units(value) + Units(scalar);
BigInteger expectedDifference = Units(value) - Units(scalar);
Units(value + scalar).ShouldBe(expectedSum);
Units(scalar + value).ShouldBe(expectedSum);
Units(value - scalar).ShouldBe(expectedDifference);
Units(scalar - value).ShouldBe(-expectedDifference);
}
}
}
[Theory]
[InlineData("+")]
[InlineData("-")]
public void ScalarSumsAndDifferencesMatchExpansionBitsAcrossRanges(string operation)
{
// Bitwise compatibility; independent exact-rational accuracy checks are separate.
(double[] scalars, DoubleDouble[] values) = ScalarArithmeticCompatibilityCases();
foreach (DoubleDouble value in values)
{
foreach (double scalar in scalars)
{
DoubleDouble expanded = new(scalar);
DoubleDouble expected = operation == "+" ? value + expanded : value - expanded;
DoubleDouble actual = operation == "+" ? value + scalar : value - scalar;
CheckBoundary(actual, expected.High, expected.Low);
DoubleDouble expectedReverse = operation == "+" ? expanded + value : expanded - value;
DoubleDouble actualReverse = operation == "+" ? scalar + value : scalar - value;
CheckBoundary(actualReverse, expectedReverse.High, expectedReverse.Low);
}
}
}
[Fact] [Fact]
public void ExpansionCancellationRetainsExactComponentsAcrossTheAdditionGuard() public void ExpansionCancellationRetainsExactComponentsAcrossTheAdditionGuard()
{ {
@@ -579,6 +622,25 @@ public class DoubleDoubleArithmeticTests
} }
} }
[Fact]
public void DivisionOverloadsPreserveComponentBitsAcrossRangesAndSpecialValues()
{
// Compatibility, not an accuracy oracle. GeneralArithmeticMeetsExactRationalBound
// separately checks complete results against exact rational bounds.
(double[] scalars, DoubleDouble[] values) = ScalarArithmeticCompatibilityCases();
foreach (DoubleDouble value in values)
{
foreach (double scalar in scalars)
{
DoubleDouble expectedQuotient = value / new DoubleDouble(scalar);
CheckBoundary(value / scalar, expectedQuotient.High, expectedQuotient.Low);
DoubleDouble expectedReverseQuotient = new DoubleDouble(scalar) / value;
CheckBoundary(scalar / value, expectedReverseQuotient.High, expectedReverseQuotient.Low);
}
}
}
[Fact] [Fact]
public void DivisionRetainsSubnormalCorrectionsWithOrdinaryHighComponents() public void DivisionRetainsSubnormalCorrectionsWithOrdinaryHighComponents()
{ {
@@ -597,6 +659,38 @@ public class DoubleDoubleArithmeticTests
} }
} }
[Theory]
[InlineData(-1000)]
[InlineData(-1021)]
[InlineData(-1022)]
[InlineData(-1023)]
public void ScalarDivisionPreservesSparseCorrectionProductsNearUnderflow(int lowExponent)
{
// At -1000, the second product is normal but its exact residual is
// (962132647665515 / 1073741824) * epsilon, which rounds to 896056 * epsilon.
// Derived from exact binary64 rational products; the other rows bracket
// the correction product's normal/subnormal transition.
foreach (double numeratorSign in new[] { -1.0, 1.0 })
{
foreach (double denominatorSign in new[] { -1.0, 1.0 })
{
foreach (double lowSign in new[] { -1.0, 1.0 })
{
double low = Math.ScaleB(lowSign, lowExponent);
double denominator = denominatorSign * 1.1;
DoubleDouble numerator = DoubleDouble.FromComponents(numeratorSign * 1.1, low);
DoubleDouble actual = numerator / denominator;
DoubleDouble expanded = numerator / new DoubleDouble(denominator);
CheckBoundary(actual, expanded.High, expanded.Low);
// Exact quotient = +/-1 + low/denominator. Its high cannot
// change here; binary64 division independently rounds the low.
CheckBoundary(actual, numeratorSign / denominatorSign, low / denominator);
AssertRelative(actual, Units(numerator) << 1074, Units(denominator));
}
}
}
}
[Fact] [Fact]
public void SubnormalProductsWithLargeNormalsRetainExactResultsInBothOrders() public void SubnormalProductsWithLargeNormalsRetainExactResultsInBothOrders()
{ {
@@ -884,6 +978,39 @@ public class DoubleDoubleArithmeticTests
} }
} }
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,
double.MaxValue, double.MinValue, double.PositiveInfinity,
double.NegativeInfinity, double.NaN];
List<DoubleDouble> values = [DoubleDouble.E, -DoubleDouble.E];
// Include addition/division dispatch edges, exponent extremes, dense/sparse
// lows of either sign, and significands on either side of a binade.
int[] exponents = [-1074, -1022, -451, -450, -1, 0, 1, 450, 451, 1020, 1021, 1023];
double[] significands = [1.0, Math.BitIncrement(1.0), 1.5, Math.BitDecrement(2.0)];
foreach (int exponent in exponents)
{
foreach (double significand in significands)
{
foreach (double sign in new[] { -1.0, 1.0 })
{
double high = Math.ScaleB(sign * significand, exponent);
scalars.Add(high);
double denseLow = Math.ScaleB(1.0, exponent - 54);
foreach (double low in new[] { 0.0, denseLow, -denseLow, double.Epsilon, -double.Epsilon })
{
values.Add(DoubleDouble.FromComponents(high, low));
}
}
}
}
foreach (double scalar in scalars)
{
values.Add(new DoubleDouble(scalar));
}
return (scalars.ToArray(), values.ToArray());
}
private static void AssertDivisionMatchesPrevious(DoubleDouble left, DoubleDouble right) private static void AssertDivisionMatchesPrevious(DoubleDouble left, DoubleDouble right)
{ {
// Freeze the pre-specialization expression. The unchanged public operators // Freeze the pre-specialization expression. The unchanged public operators
@@ -1,7 +1,3 @@
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
public class DoubleDoubleBoundaryTests public class DoubleDoubleBoundaryTests
@@ -1,7 +1,3 @@
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
public class DoubleDoubleCbrtTests public class DoubleDoubleCbrtTests
@@ -1,6 +1,3 @@
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
public class DoubleDoubleComparisonTests public class DoubleDoubleComparisonTests
@@ -1,7 +1,3 @@
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
public class DoubleDoubleConversionTests public class DoubleDoubleConversionTests
@@ -1,8 +1,5 @@
using System.Globalization; using System.Globalization;
using System.Numerics;
using Just.PreciseMath.Tests.ReferenceData; using Just.PreciseMath.Tests.ReferenceData;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
@@ -1,6 +1,4 @@
using System.Globalization; using System.Globalization;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
@@ -1,7 +1,3 @@
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
public class DoubleDoubleHypotTests public class DoubleDoubleHypotTests
@@ -1,7 +1,4 @@
using System.Globalization; using System.Globalization;
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
@@ -1,7 +1,4 @@
using System.Globalization; using System.Globalization;
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
@@ -1,6 +1,3 @@
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
public class DoubleDoubleRepresentationTests public class DoubleDoubleRepresentationTests
@@ -1,6 +1,3 @@
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
public class DoubleDoubleRootFunctionsTests public class DoubleDoubleRootFunctionsTests
@@ -1,8 +1,5 @@
using System.Globalization; using System.Globalization;
using System.Numerics;
using Just.PreciseMath.Tests.ReferenceData; using Just.PreciseMath.Tests.ReferenceData;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
@@ -1,7 +1,3 @@
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
public class DoubleDoubleSignedNumberTests public class DoubleDoubleSignedNumberTests
@@ -1,6 +1,4 @@
using System.Globalization; using System.Globalization;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
@@ -1,7 +1,4 @@
using System.Globalization; using System.Globalization;
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
@@ -1,7 +1,3 @@
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
public class DoubleDoubleTests public class DoubleDoubleTests
@@ -1,7 +1,4 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
@@ -1,8 +1,5 @@
using System.Globalization; using System.Globalization;
using System.Numerics;
using Just.PreciseMath.Tests.ReferenceData; using Just.PreciseMath.Tests.ReferenceData;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
@@ -1,7 +1,3 @@
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
public class PreciseMathInvSqrtTests public class PreciseMathInvSqrtTests
@@ -1,8 +1,5 @@
using System.Globalization; using System.Globalization;
using System.Numerics;
using Just.PreciseMath.Tests.ReferenceData; using Just.PreciseMath.Tests.ReferenceData;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
@@ -1,7 +1,3 @@
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
public class PreciseMathPowTests public class PreciseMathPowTests
@@ -1,6 +1,3 @@
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
public class PreciseMathRealPowSpecialTests public class PreciseMathRealPowSpecialTests
@@ -1,8 +1,5 @@
using System.Globalization; using System.Globalization;
using System.Numerics;
using Just.PreciseMath.Tests.ReferenceData; using Just.PreciseMath.Tests.ReferenceData;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
@@ -1,7 +1,3 @@
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
public class PreciseMathReciprocalTests public class PreciseMathReciprocalTests
@@ -1,7 +1,3 @@
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
public class PreciseMathSqrtTests public class PreciseMathSqrtTests
@@ -1,6 +1,3 @@
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests; namespace Just.PreciseMath.Tests;
public class PreciseMathTests public class PreciseMathTests
@@ -1,5 +1,3 @@
using Xunit;
namespace Just.PreciseMath.Tests.ReferenceData; namespace Just.PreciseMath.Tests.ReferenceData;
// Generated by generate_exp.py; do not derive expected values from DD arithmetic. // Generated by generate_exp.py; do not derive expected values from DD arithmetic.
@@ -9,8 +9,6 @@
// absolute; reference rounding uncertainty is bounded by 2^-350 relative. // absolute; reference rounding uncertainty is bounded by 2^-350 relative.
// Tiny corrections to 1/-1 below 120 digits are not component-retention oracles. // Tiny corrections to 1/-1 below 120 digits are not component-retention oracles.
// Signed zeros, infinities, NaNs, and enormous arguments are tested separately. // Signed zeros, infinities, NaNs, and enormous arguments are tested separately.
using Xunit;
namespace Just.PreciseMath.Tests.ReferenceData; namespace Just.PreciseMath.Tests.ReferenceData;
internal static class ExponentialFunctionsReferenceData internal static class ExponentialFunctionsReferenceData
@@ -1,7 +1,5 @@
// Generated by generate_log.py; do not hand-edit reference literals. // Generated by generate_log.py; do not hand-edit reference literals.
// Exact binary64 sums; Decimal.ln at 450/650 digits, rounded to 120 digits. // Exact binary64 sums; Decimal.ln at 450/650 digits, rounded to 120 digits.
using Xunit;
namespace Just.PreciseMath.Tests.ReferenceData; namespace Just.PreciseMath.Tests.ReferenceData;
internal static class LogReferenceData internal static class LogReferenceData
@@ -1,8 +1,6 @@
// Generated by generate_real_pow.py; do not hand-edit reference literals. // Generated by generate_real_pow.py; do not hand-edit reference literals.
// Exact binary64 sums; Decimal ln/exp at 450/650 digits, rounded to 120 digits. // Exact binary64 sums; Decimal ln/exp at 450/650 digits, rounded to 120 digits.
// Reference uncertainty is explicitly allowed as 2^-350 relative in the tests. // Reference uncertainty is explicitly allowed as 2^-350 relative in the tests.
using Xunit;
namespace Just.PreciseMath.Tests.ReferenceData; namespace Just.PreciseMath.Tests.ReferenceData;
internal static class RealPowReferenceData internal static class RealPowReferenceData
@@ -1,7 +1,5 @@
// Generated by generate_rootn.py; do not hand-edit reference literals. // Generated by generate_rootn.py; do not hand-edit reference literals.
// Exact component sums; Decimal ln/exp at 450/650 digits, rounded to 120 digits. // Exact component sums; Decimal ln/exp at 450/650 digits, rounded to 120 digits.
using Xunit;
namespace Just.PreciseMath.Tests.ReferenceData; namespace Just.PreciseMath.Tests.ReferenceData;
internal static class RootNReferenceData internal static class RootNReferenceData
@@ -0,0 +1,27 @@
namespace Just.PreciseMath.Tests.SanityChecks;
public class DoubleDoubleAdditionChecks
{
public static IEnumerable<TheoryDataRow<DoubleDouble, DoubleDouble, DoubleDouble>> ReferenceCases =>
[
(new DoubleDouble(1.0), new DoubleDouble(1.0), new DoubleDouble(2.0)),
(new DoubleDouble(1.0), new DoubleDouble(2.0), new DoubleDouble(3.0)),
(new DoubleDouble(1.25), new DoubleDouble(2.5), new DoubleDouble(3.75)),
(new DoubleDouble(65535.0), new DoubleDouble(1.0), new DoubleDouble(65536.0)),
(DoubleDouble.Parse("812563124576179134546", System.Globalization.CultureInfo.InvariantCulture), DoubleDouble.Parse("-812563124576179134504", System.Globalization.CultureInfo.InvariantCulture), new DoubleDouble(42.0)),
(DoubleDouble.Parse("812563124576179134546", System.Globalization.CultureInfo.InvariantCulture), new DoubleDouble(127.0), DoubleDouble.Parse("812563124576179134673", System.Globalization.CultureInfo.InvariantCulture)),
(DoubleDouble.Parse("812563124576179134546", System.Globalization.CultureInfo.InvariantCulture), new DoubleDouble(27.0), DoubleDouble.Parse("812563124576179134573", System.Globalization.CultureInfo.InvariantCulture)),
(DoubleDouble.Parse("812563124576179134546", System.Globalization.CultureInfo.InvariantCulture), new DoubleDouble(2.70), DoubleDouble.Parse("812563124576179134548.7", System.Globalization.CultureInfo.InvariantCulture)),
];
[Theory]
[MemberData(nameof(ReferenceCases))]
public void AdditionShouldProduceExpectedResults(DoubleDouble left, DoubleDouble right, DoubleDouble expected)
{
DoubleDouble result = left + right;
result.High.ShouldBe(expected.High);
result.Low.ShouldBe(expected.Low);
result.ShouldBe(expected);
}
}
@@ -0,0 +1,108 @@
namespace Just.PreciseMath.Tests.SanityChecks;
public class DoubleDoubleArithmeticEqualityChecks
{
public static IEnumerable<TheoryDataRow<double>> ReferenceCases => [1.0, 2.0, 3.0, 100.0, 0.1, 0.5, 0.333333333333333333, -1.0, 1553.0, 1.0e+10];
[Theory]
[MemberData(nameof(ReferenceCases))]
public void AdditionShouldProduceIdenticalResults(double reference)
{
DoubleDouble testValue = DoubleDouble.E; // any value with full precision range used
DoubleDouble ddxdd_Result = testValue + (new DoubleDouble(reference));
DoubleDouble ddxd_Result = testValue + reference;
DoubleDouble dxdd_Result = reference + testValue;
ddxdd_Result.High.ShouldBe(ddxd_Result.High);
ddxdd_Result.Low.ShouldBe(ddxd_Result.Low);
ddxdd_Result.ShouldBe(ddxd_Result);
ddxdd_Result.High.ShouldBe(dxdd_Result.High);
ddxdd_Result.Low.ShouldBe(dxdd_Result.Low);
ddxdd_Result.ShouldBe(dxdd_Result);
ddxd_Result.High.ShouldBe(dxdd_Result.High);
ddxd_Result.Low.ShouldBe(dxdd_Result.Low);
ddxd_Result.ShouldBe(dxdd_Result);
}
[Theory]
[MemberData(nameof(ReferenceCases))]
public void SubtractionFromShouldProduceIdenticalResults(double reference)
{
DoubleDouble testValue = DoubleDouble.E; // any value with full precision range used
DoubleDouble ddxdd_Result = testValue - (new DoubleDouble(reference));
DoubleDouble ddxd_Result = testValue - reference;
ddxdd_Result.High.ShouldBe(ddxd_Result.High);
ddxdd_Result.Low.ShouldBe(ddxd_Result.Low);
ddxdd_Result.ShouldBe(ddxd_Result);
}
[Theory]
[MemberData(nameof(ReferenceCases))]
public void SubtractionShouldProduceIdenticalResults(double reference)
{
DoubleDouble testValue = DoubleDouble.E; // any value with full precision range used
DoubleDouble ddxdd_Result = (new DoubleDouble(reference)) - testValue;
DoubleDouble dxdd_Result = reference - testValue;
ddxdd_Result.High.ShouldBe(dxdd_Result.High);
ddxdd_Result.Low.ShouldBe(dxdd_Result.Low);
ddxdd_Result.ShouldBe(dxdd_Result);
}
[Theory]
[MemberData(nameof(ReferenceCases))]
public void MultiplicationShouldProduceIdenticalResults(double reference)
{
DoubleDouble testValue = DoubleDouble.E; // any value with full precision range used
DoubleDouble ddxdd_Result = testValue * (new DoubleDouble(reference));
DoubleDouble ddxd_Result = testValue * reference;
DoubleDouble dxdd_Result = reference * testValue;
ddxdd_Result.High.ShouldBe(ddxd_Result.High);
ddxdd_Result.Low.ShouldBe(ddxd_Result.Low);
ddxdd_Result.ShouldBe(ddxd_Result);
ddxdd_Result.High.ShouldBe(dxdd_Result.High);
ddxdd_Result.Low.ShouldBe(dxdd_Result.Low);
ddxdd_Result.ShouldBe(dxdd_Result);
ddxd_Result.High.ShouldBe(dxdd_Result.High);
ddxd_Result.Low.ShouldBe(dxdd_Result.Low);
ddxd_Result.ShouldBe(dxdd_Result);
}
[Theory]
[MemberData(nameof(ReferenceCases))]
public void DivisionShouldProduceIdenticalResults(double reference)
{
DoubleDouble testValue = DoubleDouble.E; // any value with full precision range used
DoubleDouble ddxdd_Result = testValue / (new DoubleDouble(reference));
DoubleDouble ddxd_Result = testValue / reference;
ddxdd_Result.High.ShouldBe(ddxd_Result.High);
ddxdd_Result.Low.ShouldBe(ddxd_Result.Low);
ddxdd_Result.ShouldBe(ddxd_Result);
}
[Theory]
[MemberData(nameof(ReferenceCases))]
public void DivisionByShouldProduceIdenticalResults(double reference)
{
DoubleDouble testValue = DoubleDouble.E; // any value with full precision range used
DoubleDouble ddxdd_Result = (new DoubleDouble(reference)) / testValue;
DoubleDouble dxdd_Result = reference / testValue;
ddxdd_Result.High.ShouldBe(dxdd_Result.High);
ddxdd_Result.Low.ShouldBe(dxdd_Result.Low);
ddxdd_Result.ShouldBe(dxdd_Result);
}
}
@@ -0,0 +1,29 @@
namespace Just.PreciseMath.Tests.SanityChecks;
public class DoubleDoubleDivisionChecks
{
public static IEnumerable<TheoryDataRow<DoubleDouble, DoubleDouble, DoubleDouble>> ReferenceCases =>
[
(new DoubleDouble(6.0), new DoubleDouble(2.0), new DoubleDouble(3.0)),
(new DoubleDouble(6.0), new DoubleDouble(3.0), new DoubleDouble(2.0)),
(new DoubleDouble(5.0), new DoubleDouble(2.0), new DoubleDouble(2.5)),
(new DoubleDouble(1.0), new DoubleDouble(4.0), new DoubleDouble(0.25)),
(new DoubleDouble(3.75), new DoubleDouble(1.25), new DoubleDouble(3.0)),
(new DoubleDouble(65536.0), new DoubleDouble(1024.0), new DoubleDouble(64.0)),
(DoubleDouble.Parse("812563124576179134546", System.Globalization.CultureInfo.InvariantCulture), new DoubleDouble(2.0), DoubleDouble.Parse("406281562288089567273", System.Globalization.CultureInfo.InvariantCulture)),
(DoubleDouble.Parse("812563124576179134546", System.Globalization.CultureInfo.InvariantCulture), new DoubleDouble(4.0), DoubleDouble.Parse("203140781144044783636.5", System.Globalization.CultureInfo.InvariantCulture)),
(DoubleDouble.Parse("812563124576179134546", System.Globalization.CultureInfo.InvariantCulture), new DoubleDouble(8.0), DoubleDouble.Parse("101570390572022391818.25", System.Globalization.CultureInfo.InvariantCulture)),
(DoubleDouble.Parse("812563124576179134546", System.Globalization.CultureInfo.InvariantCulture), DoubleDouble.Parse("812563124576179134546", System.Globalization.CultureInfo.InvariantCulture), new DoubleDouble(1.0)),
];
[Theory]
[MemberData(nameof(ReferenceCases))]
public void DivisionShouldProduceExpectedResults(DoubleDouble left, DoubleDouble right, DoubleDouble expected)
{
DoubleDouble result = left / right;
result.High.ShouldBe(expected.High);
result.Low.ShouldBe(expected.Low);
result.ShouldBe(expected);
}
}
@@ -0,0 +1,29 @@
namespace Just.PreciseMath.Tests.SanityChecks;
public class DoubleDoubleMultiplicationChecks
{
public static IEnumerable<TheoryDataRow<DoubleDouble, DoubleDouble, DoubleDouble>> ReferenceCases =>
[
(new DoubleDouble(1.0), new DoubleDouble(1.0), new DoubleDouble(1.0)),
(new DoubleDouble(2.0), new DoubleDouble(3.0), new DoubleDouble(6.0)),
(new DoubleDouble(1.25), new DoubleDouble(2.5), new DoubleDouble(3.125)),
(new DoubleDouble(-1.25), new DoubleDouble(2.5), new DoubleDouble(-3.125)),
(new DoubleDouble(65535.0), new DoubleDouble(65536.0), new DoubleDouble(4294901760.0)),
(new DoubleDouble(65536.0), new DoubleDouble(65536.0), new DoubleDouble(4294967296.0)),
(DoubleDouble.Parse("812563124576179134546", System.Globalization.CultureInfo.InvariantCulture), new DoubleDouble(2.0), DoubleDouble.Parse("1625126249152358269092", System.Globalization.CultureInfo.InvariantCulture)),
(DoubleDouble.Parse("812563124576179134546", System.Globalization.CultureInfo.InvariantCulture), new DoubleDouble(0.5), DoubleDouble.Parse("406281562288089567273", System.Globalization.CultureInfo.InvariantCulture)),
(DoubleDouble.Parse("812563124576179134546", System.Globalization.CultureInfo.InvariantCulture), new DoubleDouble(27.0), DoubleDouble.Parse("21939204363556836632742", System.Globalization.CultureInfo.InvariantCulture)),
(DoubleDouble.Parse("812563124576179134546", System.Globalization.CultureInfo.InvariantCulture), new DoubleDouble(127.0), DoubleDouble.Parse("103195516821174750087342", System.Globalization.CultureInfo.InvariantCulture)),
];
[Theory]
[MemberData(nameof(ReferenceCases))]
public void MultiplicationShouldProduceExpectedResults(DoubleDouble left, DoubleDouble right, DoubleDouble expected)
{
DoubleDouble result = left * right;
result.High.ShouldBe(expected.High);
result.Low.ShouldBe(expected.Low);
result.ShouldBe(expected);
}
}
@@ -0,0 +1,30 @@
namespace Just.PreciseMath.Tests.SanityChecks;
public class DoubleDoubleSubtractionChecks
{
public static IEnumerable<TheoryDataRow<DoubleDouble, DoubleDouble, DoubleDouble>> ReferenceCases =>
[
(new DoubleDouble(2.0), new DoubleDouble(1.0), new DoubleDouble(1.0)),
(new DoubleDouble(1.0), new DoubleDouble(2.0), new DoubleDouble(-1.0)),
(new DoubleDouble(3.75), new DoubleDouble(2.5), new DoubleDouble(1.25)),
(new DoubleDouble(2.5), new DoubleDouble(3.75), new DoubleDouble(-1.25)),
(new DoubleDouble(65536.0), new DoubleDouble(1.0), new DoubleDouble(65535.0)),
(new DoubleDouble(65536.0), new DoubleDouble(65535.0), new DoubleDouble(1.0)),
(DoubleDouble.Parse("812563124576179134673", System.Globalization.CultureInfo.InvariantCulture), new DoubleDouble(127.0), DoubleDouble.Parse("812563124576179134546", System.Globalization.CultureInfo.InvariantCulture)),
(DoubleDouble.Parse("812563124576179134546", System.Globalization.CultureInfo.InvariantCulture), new DoubleDouble(127.0), DoubleDouble.Parse("812563124576179134419", System.Globalization.CultureInfo.InvariantCulture)),
(DoubleDouble.Parse("812563124576179134546", System.Globalization.CultureInfo.InvariantCulture), DoubleDouble.Parse("812563124576179134504", System.Globalization.CultureInfo.InvariantCulture), new DoubleDouble(42.0)),
(DoubleDouble.Parse("812563124576179134546", System.Globalization.CultureInfo.InvariantCulture), new DoubleDouble(0.5), DoubleDouble.Parse("812563124576179134545.5", System.Globalization.CultureInfo.InvariantCulture)),
(DoubleDouble.Parse("812563124576179134546", System.Globalization.CultureInfo.InvariantCulture), DoubleDouble.Parse("812563124576179134546", System.Globalization.CultureInfo.InvariantCulture), new DoubleDouble(0.0)),
];
[Theory]
[MemberData(nameof(ReferenceCases))]
public void SubtractionShouldProduceExpectedResults(DoubleDouble left, DoubleDouble right, DoubleDouble expected)
{
DoubleDouble result = left - right;
result.High.ShouldBe(expected.High);
result.Low.ShouldBe(expected.Low);
result.ShouldBe(expected);
}
}
-6
View File
@@ -16,12 +16,6 @@ Do not claim API completeness or accuracy beyond tested contracts.
comparisons; use the Dry job for smoke validation, not performance conclusions. comparisons; use the Dry job for smoke validation, not performance conclusions.
- Root `Directory.Build.props` holds shared settings. Each numbered directory's - Root `Directory.Build.props` holds shared settings. Each numbered directory's
props explicitly imports it; preserve this import chain. props explicitly imports it; preserve this import chain.
- `review-legacy/`: optional local reference material, excluded by `.gitignore`.
Start with `review-legacy/Review-Revisited-DoubleDouble.md` when present. The code
and proposed fixes contain known defects: reproduce findings against current
code rather than treating them as correctness oracles. Do not compile, copy
wholesale, or force-add these files. If absent, proceed without them; do not
invent their contents or make validation depend on them.
- Use the ignored, repository-local `.hermes/` directory for plans, checklists, - Use the ignored, repository-local `.hermes/` directory for plans, checklists,
investigation notes, and handoff context. Create or update notes as useful; investigation notes, and handoff context. Create or update notes as useful;
keep them concise and revalidate them against current files. Optional keep them concise and revalidate them against current files. Optional
+9 -5
View File
@@ -24,11 +24,15 @@ not perform double-double arithmetic or allocate on the heap.
a `double`. Addition retains residuals under cancellation; multiplication uses a `double`. Addition retains residuals under cancellation; multiplication uses
fused multiply-add; division uses residual corrections. Mixed `double` operators fused multiply-add; division uses residual corrections. Mixed `double` operators
use specialized scalar paths rather than promoting the scalar to `DoubleDouble`. use specialized scalar paths rather than promoting the scalar to `DoubleDouble`.
Their finite fast paths normalize once with a final sum transform; scalar Division retains the complete first remainder and applies two quotient corrections,
division uses one compensated quotient correction within the error contract below. normalizing before the final correction. Mixed addition and subtraction also retain
intermediate sum residuals through final normalization. Mixed addition, subtraction,
and division produce the same component bits as their `DoubleDouble` operations
with the scalar represented as a zero-low pair.
- Exponent boundaries: bounded `BigInteger` calculations avoid intermediate - Exponent boundaries: bounded `BigInteger` calculations avoid intermediate
overflow and underflow on the exceptional finite path. Ordinary arithmetic uses overflow and underflow on the exceptional finite path. Ordinary arithmetic uses
floating-point transforms without allocations. The stored value remains two floating-point transforms without allocations, although sparse division correction
products can also reach a boundary path. The stored value remains two
doubles; this is not an arbitrary-precision API. doubles; this is not an arbitrary-precision API.
- Comparisons use both components. `Equals` treats NaNs as equal and signed zeros - Comparisons use both components. `Equals` treats NaNs as equal and signed zeros
as equal for collections. `CompareTo` orders NaN before other values. Numerical as equal for collections. `CompareTo` orders NaN before other values. Numerical
@@ -107,8 +111,8 @@ The `DDMath` static class provides:
It shares the existing `DoubleDouble.Abs` implementation. It shares the existing `DoubleDouble.Abs` implementation.
- `Reciprocal(DoubleDouble)`: returns exactly the same high and low component bits - `Reciprocal(DoubleDouble)`: returns exactly the same high and low component bits
as `1.0 / value`. It specializes scalar/DD division for a numerator of one, as `1.0 / value`. It specializes scalar/DD division for a numerator of one,
omitting only redundant numerator checks while preserving both divisions, omitting redundant numerator checks and sharing the finite scalar-numerator kernel,
the FMA sequence, and normalization. Signed zeros map to signed infinities, including both residual corrections and normalization. Signed zeros map to signed infinities,
signed infinities to signed zeros, and NaN to canonical NaN. The allocating exact boundary signed infinities to signed zeros, and NaN to canonical NaN. The allocating exact boundary
path handles extreme exponents; finite overflow produces signed infinity. path handles extreme exponents; finite overflow produces signed infinity.
No speedup over scalar/DD division has been measured. No speedup over scalar/DD division has been measured.