This commit is contained in:
@@ -12,31 +12,18 @@ public static partial class DDMath
|
|||||||
return DoubleDouble.Abs(value);
|
return DoubleDouble.Abs(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Returns the reciprocal with double-double precision.</summary>
|
/// <inheritdoc cref="DoubleDouble.Square"/>
|
||||||
/// <remarks>
|
[Pure]
|
||||||
/// Specializes scalar/DD division for a numerator of one, omitting only its
|
public static DoubleDouble Square(DoubleDouble value)
|
||||||
/// redundant numerator checks. The arithmetic sequence and boundary handling
|
{
|
||||||
/// are unchanged, preserving the high and low component bits of <c>1.0 / value</c>.
|
return DoubleDouble.Square(value);
|
||||||
/// Results are approximate, not guaranteed correctly rounded;
|
}
|
||||||
/// finite results are tested against a relative error bound of 2^-100 plus one
|
|
||||||
/// minimum binary64 subnormal. Precision decreases near underflow.
|
/// <inheritdoc cref="DoubleDouble.Reciprocal"/>
|
||||||
/// Signed zeros map to signed infinities, signed infinities to signed zeros,
|
|
||||||
/// and NaN to canonical NaN. Finite overflow produces signed infinity.
|
|
||||||
/// </remarks>
|
|
||||||
[Pure]
|
[Pure]
|
||||||
public static DoubleDouble Reciprocal(DoubleDouble value)
|
public static DoubleDouble Reciprocal(DoubleDouble value)
|
||||||
{
|
{
|
||||||
const double one = 1.0;
|
return DoubleDouble.Reciprocal(value);
|
||||||
if (!DoubleDouble.IsFinite(value) || value._high == 0.0)
|
|
||||||
{
|
|
||||||
return new DoubleDouble(one / value._high);
|
|
||||||
}
|
|
||||||
if (!PreciseMathHelper.IsDivisionWithinFastRange(value._high))
|
|
||||||
{
|
|
||||||
return PreciseMathHelper.DivideBoundary(one, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return PreciseMathHelper.DivideScalarFinite(one, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="DoubleDouble.Cbrt"/>
|
/// <inheritdoc cref="DoubleDouble.Cbrt"/>
|
||||||
|
|||||||
@@ -6,6 +6,43 @@ public readonly partial struct DoubleDouble :
|
|||||||
IMultiplyOperators<DoubleDouble, double, DoubleDouble>,
|
IMultiplyOperators<DoubleDouble, double, DoubleDouble>,
|
||||||
IDivisionOperators<DoubleDouble, double, DoubleDouble>
|
IDivisionOperators<DoubleDouble, double, DoubleDouble>
|
||||||
{
|
{
|
||||||
|
/// <summary>Returns the square using a specialized double-double product.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Combines the equal cross terms before FMA accumulation, retaining low squared.
|
||||||
|
/// Results are approximate and may differ in low bits from value * value;
|
||||||
|
/// finite results are tested against 2^-100 relative error plus one minimum
|
||||||
|
/// binary64 subnormal, not a guarantee of correctly rounded components.
|
||||||
|
/// Either zero maps to positive zero, either infinity to positive infinity,
|
||||||
|
/// and NaN to canonical NaN. Uses the allocating multiplication fallback at
|
||||||
|
/// exponent boundaries. No performance improvement is guaranteed.
|
||||||
|
/// </remarks>
|
||||||
|
[Pure]
|
||||||
|
public static DoubleDouble Square(DoubleDouble value)
|
||||||
|
{
|
||||||
|
if (!IsFinite(value) || value._high == 0.0)
|
||||||
|
{
|
||||||
|
return new DoubleDouble(value._high * value._high);
|
||||||
|
}
|
||||||
|
if (!PreciseMathHelper.IsMultiplicationWithinFastRange(value._high, value._high))
|
||||||
|
{
|
||||||
|
return PreciseMathHelper.MultiplyBoundary(value, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
(double product, double error) = PreciseMathHelper.TwoSquare(value._high);
|
||||||
|
// The guard bounds the high exponent to [-450, 450], so doubling high
|
||||||
|
// is exact and finite. FMA avoids rounding the cross product separately.
|
||||||
|
error = Math.FusedMultiplyAdd(value._high + value._high, value._low, error);
|
||||||
|
// As for multiplication, normalized input makes |error| < 4u*product;
|
||||||
|
// the positive high product is normal and the corrected sum is finite.
|
||||||
|
(double high, double low) = PreciseMathHelper.TwoQuickAdd(product, error);
|
||||||
|
// Incorporate low squared AFTER the cross term has been absorbed into
|
||||||
|
// high. Otherwise an exactly representable tail can round away before
|
||||||
|
// cancellation, e.g. (1 - 2^-54)^2 loses its 2^-108 residual.
|
||||||
|
low = Math.FusedMultiplyAdd(value._low, value._low, low);
|
||||||
|
(high, low) = PreciseMathHelper.TwoQuickAdd(high, low);
|
||||||
|
return new DoubleDouble(high, low == 0.0 ? 0.0 : low);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Returns the operand unchanged.</summary>
|
/// <summary>Returns the operand unchanged.</summary>
|
||||||
public static DoubleDouble operator +(DoubleDouble value)
|
public static DoubleDouble operator +(DoubleDouble value)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,6 +22,37 @@ public readonly partial struct DoubleDouble : IFormattable
|
|||||||
return ToString(null, provider);
|
return ToString(null, provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns the exact decimal expansion of the stored value using invariant culture.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Uses allocating integer arithmetic, without rounding or scientific notation.
|
||||||
|
/// Omits unnecessary fractional zeros and preserves negative zero; nonfinite
|
||||||
|
/// values use NaN, Infinity, and -Infinity. Sparse pairs can require over a
|
||||||
|
/// thousand characters. Parse with invariant culture to recover the components;
|
||||||
|
/// this is an exact-value format, not a shortest-round-trip format.
|
||||||
|
/// </remarks>
|
||||||
|
public string ToStringExact()
|
||||||
|
{
|
||||||
|
if (!double.IsFinite(_high) || _high == 0.0)
|
||||||
|
{
|
||||||
|
return _high.ToString(CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
(BigInteger numerator, BigInteger denominator) = ConversionFraction();
|
||||||
|
// A sum of binary64 values has a power-of-two denominator. Multiplying
|
||||||
|
// by the matching power of five makes an integer coefficient over 10^n.
|
||||||
|
int decimalPlaces = (int)(denominator.GetBitLength() - 1);
|
||||||
|
BigInteger coefficient = BigInteger.Abs(numerator) * BigInteger.Pow(5, decimalPlaces);
|
||||||
|
string digits = coefficient.ToString(CultureInfo.InvariantCulture);
|
||||||
|
int end = digits.Length;
|
||||||
|
while (decimalPlaces > 0 && digits[end - 1] == '0')
|
||||||
|
{
|
||||||
|
end--;
|
||||||
|
decimalPlaces--;
|
||||||
|
}
|
||||||
|
string sign = numerator.Sign < 0 ? "-" : string.Empty;
|
||||||
|
return sign + FormattingFixed(digits[..end], decimalPlaces, ".");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Formats the exact component sum, rounding to nearest with ties to even.
|
/// Formats the exact component sum, rounding to nearest with ties to even.
|
||||||
/// Supports G/g (significant digits, default 32; G0 also means 32), E/e
|
/// Supports G/g (significant digits, default 32; G0 also means 32), E/e
|
||||||
|
|||||||
@@ -5,6 +5,33 @@ public readonly partial struct DoubleDouble
|
|||||||
/// <summary>Gets the binary radix of the components.</summary>
|
/// <summary>Gets the binary radix of the components.</summary>
|
||||||
public static int Radix => 2;
|
public static int Radix => 2;
|
||||||
|
|
||||||
|
/// <summary>Returns the reciprocal with double-double precision.</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Specializes scalar/DD division for a numerator of one, omitting only its
|
||||||
|
/// redundant numerator checks. The arithmetic sequence and boundary handling
|
||||||
|
/// are unchanged, preserving the high and low component bits of <c>1.0 / value</c>.
|
||||||
|
/// Results are approximate, not guaranteed correctly rounded;
|
||||||
|
/// finite results are tested against a relative error bound of 2^-100 plus one
|
||||||
|
/// minimum binary64 subnormal. Precision decreases near underflow.
|
||||||
|
/// Signed zeros map to signed infinities, signed infinities to signed zeros,
|
||||||
|
/// and NaN to canonical NaN. Finite overflow produces signed infinity.
|
||||||
|
/// </remarks>
|
||||||
|
[Pure]
|
||||||
|
public static DoubleDouble Reciprocal(DoubleDouble value)
|
||||||
|
{
|
||||||
|
const double one = 1.0;
|
||||||
|
if (!DoubleDouble.IsFinite(value) || value._high == 0.0)
|
||||||
|
{
|
||||||
|
return new DoubleDouble(one / value._high);
|
||||||
|
}
|
||||||
|
if (!PreciseMathHelper.IsDivisionWithinFastRange(value._high))
|
||||||
|
{
|
||||||
|
return PreciseMathHelper.DivideBoundary(one, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PreciseMathHelper.DivideScalarFinite(one, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Returns the absolute value, preserving both components and canonicalizing NaN.</summary>
|
/// <summary>Returns the absolute value, preserving both components and canonicalizing NaN.</summary>
|
||||||
public static DoubleDouble Abs(DoubleDouble value)
|
public static DoubleDouble Abs(DoubleDouble value)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ public readonly partial struct DoubleDouble : IRootFunctions<DoubleDouble>
|
|||||||
|
|
||||||
/// <summary>Returns the nonnegative square root with double-double precision.</summary>
|
/// <summary>Returns the nonnegative square root with double-double precision.</summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Uses power-of-two scaling and an FMA-based Newton correction. Results are
|
/// Uses power-of-two scaling and an FMA-based Newton correction. An exponent-
|
||||||
|
/// tracked correction retains sparse lows endangered by downscaling. Results are
|
||||||
/// approximate, not guaranteed correctly rounded; finite positive inputs are
|
/// approximate, not guaranteed correctly rounded; finite positive inputs are
|
||||||
/// tested against a relative error bound of 2^-100 across the binary64 range.
|
/// tested against a relative error bound of 2^-100 across the binary64 range.
|
||||||
/// Signed zero and positive infinity are preserved. Negative nonzero values
|
/// Signed zero and positive infinity are preserved. Negative nonzero values
|
||||||
@@ -264,11 +265,19 @@ public readonly partial struct DoubleDouble : IRootFunctions<DoubleDouble>
|
|||||||
|
|
||||||
// Choose an even exponent, rounding negative odd exponents down too.
|
// Choose an even exponent, rounding negative odd exponents down too.
|
||||||
// For positive finite normalized inputs, exponent is in [-1074, 1022]
|
// For positive finite normalized inputs, exponent is in [-1074, 1022]
|
||||||
// and the scaled high is in [1, 4). Scaling the high is exact; any
|
// and the scaled high is in [1, 4). Scaling the high is exact.
|
||||||
// underflow in a very sparse low is far below the relative error bound.
|
|
||||||
int exponent = Math.ILogB(value.High) & ~1;
|
int exponent = Math.ILogB(value.High) & ~1;
|
||||||
double high = Math.ScaleB(value.High, -exponent);
|
double high = Math.ScaleB(value.High, -exponent);
|
||||||
double low = Math.ScaleB(value.Low, -exponent);
|
double low = Math.ScaleB(value.Low, -exponent);
|
||||||
|
// Dividing the scaled low by 2*estimate (in [2, 4]) can round at the
|
||||||
|
// subnormal floor before output rescaling restores its magnitude. In
|
||||||
|
// that sparse domain, refine high alone and apply the original low with
|
||||||
|
// separate exponents afterward. Never add both the scaled and original low.
|
||||||
|
bool separateLow = exponent > 0 && value.Low != 0.0 && Math.Abs(low) < Math.ScaleB(1.0, -1020);
|
||||||
|
if (separateLow)
|
||||||
|
{
|
||||||
|
low = 0.0;
|
||||||
|
}
|
||||||
double estimate = Math.Sqrt(high);
|
double estimate = Math.Sqrt(high);
|
||||||
|
|
||||||
// Estimate is in [1, 2]. FMA avoids rounding its square before
|
// Estimate is in [1, 2]. FMA avoids rounding its square before
|
||||||
@@ -282,7 +291,10 @@ public readonly partial struct DoubleDouble : IRootFunctions<DoubleDouble>
|
|||||||
// Normalize again to canonicalize a zero low, including any underflow
|
// Normalize again to canonicalize a zero low, including any underflow
|
||||||
// of an exceptionally sparse correction during rescaling.
|
// of an exceptionally sparse correction during rescaling.
|
||||||
int rootExponent = exponent / 2;
|
int rootExponent = exponent / 2;
|
||||||
return DoubleDouble.FromComponents(Math.ScaleB(root.High, rootExponent), Math.ScaleB(root.Low, rootExponent));
|
root = DoubleDouble.FromComponents(Math.ScaleB(root.High, rootExponent), Math.ScaleB(root.Low, rootExponent));
|
||||||
|
// In this branch |originalLow/originalHigh| < 2^-1020. The omitted
|
||||||
|
// quadratic term is negligible even relative to the low correction.
|
||||||
|
return separateLow ? root + RootLowCorrection(value, root, 2) : root;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Returns the reciprocal square root with double-double precision.</summary>
|
/// <summary>Returns the reciprocal square root with double-double precision.</summary>
|
||||||
|
|||||||
@@ -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()
|
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,
|
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
|
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]
|
[Theory]
|
||||||
[InlineData("G0")]
|
[InlineData("G0")]
|
||||||
[InlineData("g0")]
|
[InlineData("g0")]
|
||||||
@@ -138,4 +211,40 @@ public class DoubleDoubleFormattingTests
|
|||||||
{
|
{
|
||||||
Should.Throw<FormatException>(() => DoubleDouble.One.ToString(format, CultureInfo.InvariantCulture));
|
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)
|
private static void AssertSqrtBound(DoubleDouble input, DoubleDouble actual)
|
||||||
{
|
{
|
||||||
DoubleDouble.IsFinite(actual).ShouldBeTrue();
|
DoubleDouble.IsFinite(actual).ShouldBeTrue();
|
||||||
|
|||||||
@@ -87,6 +87,18 @@ public class ArithmeticBenchmarks
|
|||||||
return _doubleDoubleLeft * _doubleDoubleRight;
|
return _doubleDoubleLeft * _doubleDoubleRight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Benchmark(Baseline = true), BenchmarkCategory("Squaring")]
|
||||||
|
public DoubleDouble DoubleDoubleSquareViaMultiply()
|
||||||
|
{
|
||||||
|
return _doubleDoubleLeft * _doubleDoubleLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark, BenchmarkCategory("Squaring")]
|
||||||
|
public DoubleDouble DoubleDoubleSquare()
|
||||||
|
{
|
||||||
|
return DoubleDouble.Square(_doubleDoubleLeft);
|
||||||
|
}
|
||||||
|
|
||||||
[Benchmark(Baseline = true), BenchmarkCategory("Division")]
|
[Benchmark(Baseline = true), BenchmarkCategory("Division")]
|
||||||
public double DoubleDivide()
|
public double DoubleDivide()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -109,6 +109,15 @@ The `DDMath` static class provides:
|
|||||||
- `Abs(DoubleDouble)`: preserves both components, maps either signed zero to
|
- `Abs(DoubleDouble)`: preserves both components, maps either signed zero to
|
||||||
positive zero and either infinity to positive infinity, and returns canonical NaN.
|
positive zero and either infinity to positive infinity, and returns canonical NaN.
|
||||||
It shares the existing `DoubleDouble.Abs` implementation.
|
It shares the existing `DoubleDouble.Abs` implementation.
|
||||||
|
- `Square(DoubleDouble)`: forwards to the specialized `DoubleDouble.Square` kernel.
|
||||||
|
It combines the equal cross terms with FMA, normalizes, then incorporates the
|
||||||
|
low-component square without losing selected exactly representable tails.
|
||||||
|
Either signed zero maps to positive zero, either infinity to positive infinity,
|
||||||
|
and NaN to canonical NaN. It shares multiplication's allocating boundary fallback
|
||||||
|
and tested `2^-100` relative error plus one minimum binary64 subnormal bound.
|
||||||
|
Results can differ in low bits from `value * value`; neither route is universally
|
||||||
|
correctly rounded. No speedup has been measured, and existing kernels have not
|
||||||
|
been rewritten to use it.
|
||||||
- `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 redundant numerator checks and sharing the finite scalar-numerator kernel,
|
omitting redundant numerator checks and sharing the finite scalar-numerator kernel,
|
||||||
@@ -118,7 +127,11 @@ The `DDMath` static class provides:
|
|||||||
No speedup over scalar/DD division has been measured.
|
No speedup over scalar/DD division has been measured.
|
||||||
- `Sqrt(DoubleDouble)`: uses power-of-two scaling and an FMA-based Newton correction
|
- `Sqrt(DoubleDouble)`: uses power-of-two scaling and an FMA-based Newton correction
|
||||||
to retain extended precision, including for subnormal inputs, without squaring
|
to retain extended precision, including for subnormal inputs, without squaring
|
||||||
an unscaled estimate near the exponent limits. Signed zero and positive infinity
|
an unscaled estimate near the exponent limits. Sparse lows endangered by input
|
||||||
|
scaling or correction division are applied separately with tracked exponents:
|
||||||
|
for example, `sqrt((2^1000, 2^-174))` retains the low component `2^-675`.
|
||||||
|
This exceptional correction can reach the allocating boundary machinery;
|
||||||
|
ordinary inputs retain the original scaled correction path. Signed zero and positive infinity
|
||||||
are preserved; negative nonzero inputs and NaN return canonical NaN.
|
are preserved; negative nonzero inputs and NaN return canonical NaN.
|
||||||
- `InvSqrt(DoubleDouble)`: computes the reciprocal square root with power-of-two
|
- `InvSqrt(DoubleDouble)`: computes the reciprocal square root with power-of-two
|
||||||
scaling and a compensated Newton step, avoiding double-double division and its
|
scaling and a compensated Newton step, avoiding double-double division and its
|
||||||
@@ -195,6 +208,7 @@ The `DDMath` static class provides:
|
|||||||
using Just.PreciseMath;
|
using Just.PreciseMath;
|
||||||
|
|
||||||
DoubleDouble root = DoubleDouble.Sqrt(new DoubleDouble(2.0));
|
DoubleDouble root = DoubleDouble.Sqrt(new DoubleDouble(2.0));
|
||||||
|
DoubleDouble square = DoubleDouble.Square(root);
|
||||||
DoubleDouble inverseRoot = DoubleDouble.InvSqrt(new DoubleDouble(2.0));
|
DoubleDouble inverseRoot = DoubleDouble.InvSqrt(new DoubleDouble(2.0));
|
||||||
DoubleDouble cubeRoot = DoubleDouble.Cbrt(new DoubleDouble(-8.0));
|
DoubleDouble cubeRoot = DoubleDouble.Cbrt(new DoubleDouble(-8.0));
|
||||||
DoubleDouble distance = DoubleDouble.Hypot(new DoubleDouble(3.0), new DoubleDouble(4.0));
|
DoubleDouble distance = DoubleDouble.Hypot(new DoubleDouble(3.0), new DoubleDouble(4.0));
|
||||||
@@ -227,6 +241,9 @@ squares/powers of four. Square-root tests also bracket exact root-rounding midpo
|
|||||||
both suites exercise half-ulp low-component normalization boundaries.
|
both suites exercise half-ulp low-component normalization boundaries.
|
||||||
This is a tested approximate-accuracy contract, not exhaustive coverage
|
This is a tested approximate-accuracy contract, not exhaustive coverage
|
||||||
of all component pairs or a guarantee of correctly rounded results.
|
of all component pairs or a guarantee of correctly rounded results.
|
||||||
|
Square-root tests separately verify selected sparse output residuals with exact
|
||||||
|
squared midpoint inequalities, including either low sign, dispatch neighbors,
|
||||||
|
and corrections that become representable only after rescaling.
|
||||||
|
|
||||||
Cube-root and nth-root tests check `2^-100` relative error for finite nonzero
|
Cube-root and nth-root tests check `2^-100` relative error for finite nonzero
|
||||||
inputs with degree magnitude at least two. Small degrees use exact integer-power
|
inputs with degree magnitude at least two. Small degrees use exact integer-power
|
||||||
@@ -306,6 +323,14 @@ sampled error bounds does not guarantee correct range decisions for every input.
|
|||||||
- `TryFormat(Span<char>, ...)` implements `ISpanFormattable` with the same formats.
|
- `TryFormat(Span<char>, ...)` implements `ISpanFormattable` with the same formats.
|
||||||
It currently allocates via `ToString`; insufficient space returns `false`, writes
|
It currently allocates via `ToString`; insufficient space returns `false`, writes
|
||||||
zero characters, and leaves the destination unchanged.
|
zero characters, and leaves the destination unchanged.
|
||||||
|
- `ToStringExact()` emits the complete decimal expansion of the stored component
|
||||||
|
sum, without rounding, scientific notation, or unnecessary fractional zeros.
|
||||||
|
Unlike ordinary `ToString`, it always uses **invariant culture**. It preserves
|
||||||
|
negative zero and emits `NaN`, `Infinity`, or `-Infinity` for special values.
|
||||||
|
It allocates through exact integer arithmetic; sparse pairs can require over
|
||||||
|
a thousand characters. Use `DoubleDouble.Parse(text, CultureInfo.InvariantCulture)`
|
||||||
|
to recover the components. This is not a shortest-round-trip format and does not
|
||||||
|
change the standard formats' 999-digit precision limit or the parser's length limit.
|
||||||
|
|
||||||
`DoubleDouble` implements `ISignedNumber<DoubleDouble>`, including the inherited
|
`DoubleDouble` implements `ISignedNumber<DoubleDouble>`, including the inherited
|
||||||
`INumberBase` contracts: binary radix, classification, absolute value, magnitude
|
`INumberBase` contracts: binary radix, classification, absolute value, magnitude
|
||||||
@@ -374,7 +399,7 @@ bool success = DoubleDouble.TryParse("1.25e-2".AsSpan(), CultureInfo.InvariantCu
|
|||||||
Natural `DDMath.Log`, the complete exponential family on `DoubleDouble`, and all
|
Natural `DDMath.Log`, the complete exponential family on `DoubleDouble`, and all
|
||||||
three `DDMath.Pow` overloads are implemented.
|
three `DDMath.Pow` overloads are implemented.
|
||||||
Logarithms in other bases, generic-math interfaces beyond `ISignedNumber`,
|
Logarithms in other bases, generic-math interfaces beyond `ISignedNumber`,
|
||||||
`IFloatingPointConstants`, `IRootFunctions`, and `IExponentialFunctions`, additional text formats/general
|
`IFloatingPointConstants`, `IRootFunctions`, and `IExponentialFunctions`, additional text formats/shortest-
|
||||||
round-trip formatting, and non-arithmetic performance benchmarks remain deferred.
|
round-trip formatting, and non-arithmetic performance benchmarks remain deferred.
|
||||||
Replacing allocating arithmetic boundary fallbacks is also deferred; the current
|
Replacing allocating arithmetic boundary fallbacks is also deferred; the current
|
||||||
`BigInteger` paths remain in place. That optimization does not require removing
|
`BigInteger` paths remain in place. That optimization does not require removing
|
||||||
@@ -400,6 +425,8 @@ on CI workflow runs.
|
|||||||
BenchmarkDotNet measures arithmetic throughput, dependent-chain latency, and allocations,
|
BenchmarkDotNet measures arithmetic throughput, dependent-chain latency, and allocations,
|
||||||
including comparisons of `DoubleDouble`, `decimal`, and `double`, mixed scalar operations,
|
including comparisons of `DoubleDouble`, `decimal`, and `double`, mixed scalar operations,
|
||||||
and exponent-boundary paths. These are performance measurements, not accuracy tests.
|
and exponent-boundary paths. These are performance measurements, not accuracy tests.
|
||||||
|
Squaring cases compare `DoubleDouble.Square(value)` with `value * value`; their
|
||||||
|
presence is not evidence of a speedup.
|
||||||
|
|
||||||
After the Release build above, run from the repository root:
|
After the Release build above, run from the repository root:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user