Files
Just.PreciseMath/0-source/Just.PreciseMath/DoubleDouble.Arithmetic.cs
T
just 620faca7eb
.NET Test / .NET tests (push) Successful in 1m35s
square function and some sanity checks
2026-09-18 16:59:28 +04:00

243 lines
12 KiB
C#

namespace Just.PreciseMath;
public readonly partial struct DoubleDouble :
IAdditionOperators<DoubleDouble, double, DoubleDouble>,
ISubtractionOperators<DoubleDouble, double, DoubleDouble>,
IMultiplyOperators<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>
public static DoubleDouble operator +(DoubleDouble value)
{
return value;
}
/// <summary>Negates the value, including the high zero's sign, retaining canonical NaN and zero residuals.</summary>
public static DoubleDouble operator -(DoubleDouble value)
{
// Negation preserves normalization; only NaN and zero residuals need canonicalization.
return new DoubleDouble(double.IsNaN(value._high) ? double.NaN : -value._high,
value._low == 0.0 ? 0.0 : -value._low);
}
/// <summary>Adds normalized expansions, retaining low-sum residuals under cancellation.</summary>
public static DoubleDouble operator +(DoubleDouble left, DoubleDouble right)
{
if (!IsFinite(left) || !IsFinite(right) || (left._high == 0.0 && right._high == 0.0))
{
return new DoubleDouble(left._high + right._high);
}
if (!PreciseMathHelper.IsAdditionWithinFastRange(left._high) || !PreciseMathHelper.IsAdditionWithinFastRange(right._high))
{
return PreciseMathHelper.AddBoundary(left, right);
}
return PreciseMathHelper.AddFinite(left._high, left._low, right._high, right._low);
}
/// <summary>Subtracts normalized expansions.</summary>
public static DoubleDouble operator -(DoubleDouble left, DoubleDouble right)
{
if (!IsFinite(left) || !IsFinite(right) || (left._high == 0.0 && right._high == 0.0))
{
return new DoubleDouble(left._high - right._high);
}
if (!PreciseMathHelper.IsAdditionWithinFastRange(left._high) || !PreciseMathHelper.IsAdditionWithinFastRange(right._high))
{
return PreciseMathHelper.AddBoundary(left, -right);
}
// With a canonical left low (never -0), the low TwoSum absorbs the
// negated right zero low without changing either output component's bits.
// Avoid unary negation's intermediate canonicalization on this finite path.
return PreciseMathHelper.AddFinite(left._high, left._low, -right._high, -right._low);
}
/// <summary>Multiplies expansions using an FMA product residual and cross terms.</summary>
/// <remarks>Results are approximate double-double values, not universally correctly rounded.</remarks>
public static DoubleDouble operator *(DoubleDouble left, DoubleDouble right)
{
if (!IsFinite(left) || !IsFinite(right) || left._high == 0.0 || right._high == 0.0)
{
return new DoubleDouble(left._high * right._high);
}
if (!PreciseMathHelper.IsMultiplicationWithinFastRange(left._high, right._high))
{
return PreciseMathHelper.MultiplyBoundary(left, right);
}
(double product, double error) = PreciseMathHelper.TwoMultiply(left._high, right._high);
error = Math.FusedMultiplyAdd(left._high, right._low, error);
error = Math.FusedMultiplyAdd(left._low, right._high, error);
error = Math.FusedMultiplyAdd(left._low, right._low, error);
// With u = 2^-53, normalized inputs and the exponent-sum guard give
// |error| < 4u*|product|, including rounding at the subnormal floor.
// The product is normal and nonzero; its corrected sum remains finite.
(double high, double low) = PreciseMathHelper.TwoQuickAdd(product, error);
return new DoubleDouble(high, low == 0.0 ? 0.0 : low);
}
/// <summary>Divides expansions using a quotient estimate and two residual corrections.</summary>
/// <remarks>Zero and nonfinite operands follow binary64 rules; precision decreases near underflow.</remarks>
public static DoubleDouble operator /(DoubleDouble left, DoubleDouble right)
{
if (!IsFinite(left) || !IsFinite(right) || left._high == 0.0 || right._high == 0.0)
{
return new DoubleDouble(left._high / right._high);
}
if (!PreciseMathHelper.IsDivisionWithinFastRange(left._high) || !PreciseMathHelper.IsDivisionWithinFastRange(right._high))
{
return PreciseMathHelper.DivideBoundary(left, right);
}
double quotient = left._high / right._high;
DoubleDouble remainder = left - (right * quotient);
double correction = remainder._high / right._high;
double finalRemainder = PreciseMathHelper.SubtractDivisionCorrectionHigh(remainder, right * correction);
double finalCorrection = finalRemainder / right._high;
// The entry guard gives |quotient| <= 2^901. Conservatively bounding
// the first product and subtraction gives |remainder.High| <= 2^457
// at the first correction, hence |correction| <= 2^908 and a finite sum
// below 2^909 in magnitude. Retain normalization before the final add.
return PreciseMathHelper.NormalizeFinite(quotient, correction) + finalCorrection;
}
/// <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)
{
return PreciseMathHelper.AddScalar(left._high, left._low, right);
}
/// <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)
{
return right + left;
}
/// <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)
{
return PreciseMathHelper.AddScalar(left._high, left._low, -right);
}
/// <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)
{
// Negate the components, not the result: exact cancellation must yield +0.
return PreciseMathHelper.AddScalar(-right._high, -right._low, left);
}
/// <summary>Applies the expansion operation without discarding the low component.</summary>
public static DoubleDouble operator *(DoubleDouble left, double right)
{
if (!IsFinite(left) || !double.IsFinite(right) || left._high == 0.0 || right == 0.0)
{
return new DoubleDouble(left._high * right);
}
if (!PreciseMathHelper.IsMultiplicationWithinFastRange(left._high, right))
{
return PreciseMathHelper.MultiplyBoundary(left, right);
}
(double product, double error) = PreciseMathHelper.TwoMultiply(left._high, right);
error = Math.FusedMultiplyAdd(left._low, right, error);
// Normalized input bounds the correction by O(u * product). The exponent
// guard keeps the high product normal and its product residual representable.
(double high, double low) = PreciseMathHelper.TwoQuickAdd(product, error);
return new DoubleDouble(high, low == 0.0 ? 0.0 : low);
}
/// <summary>Applies the expansion operation without discarding the low component.</summary>
public static DoubleDouble operator *(double left, DoubleDouble right)
{
return right * left;
}
/// <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)
{
if (!IsFinite(left) || !double.IsFinite(right) || left._high == 0.0 || right == 0.0)
{
return new DoubleDouble(left._high / right);
}
if (!PreciseMathHelper.IsDivisionWithinFastRange(left._high) || !PreciseMathHelper.IsDivisionWithinFastRange(right))
{
return PreciseMathHelper.DivideBoundary(left, right);
}
double quotient = left._high / right;
// The initial product is near left.High, so the exponent guard makes
// TwoMultiply exact. Keep the complete remainder, including its low.
(double product, double productError) = PreciseMathHelper.TwoMultiply(right, quotient);
DoubleDouble remainder = PreciseMathHelper.AddFinite(left._high, left._low, -product, -productError);
double correction = remainder._high / right;
(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>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)
{
if (!double.IsFinite(left) || !IsFinite(right) || left == 0.0 || right._high == 0.0)
{
return new DoubleDouble(left / right._high);
}
if (!PreciseMathHelper.IsDivisionWithinFastRange(left) || !PreciseMathHelper.IsDivisionWithinFastRange(right._high))
{
return PreciseMathHelper.DivideBoundary(left, right);
}
return PreciseMathHelper.DivideScalarFinite(left, right);
}
}