using Shouldly; using Xunit; namespace Just.PreciseMath.Tests; public class ArithmeticRangeTests { [Fact] public void AdditionRangeMatchesTheOriginalExponentGuard() { // Independent BCL predicate: include both signed zeros, subnormals, and // the first/last significands of every finite exponent field. foreach (double value in FiniteExponentSamples()) { bool expected = Math.ILogB(value) <= 1020; PreciseMathHelper.IsAdditionWithinFastRange(value).ShouldBe(expected, $"{value:R}"); } } [Fact] public void DivisionRangeMatchesTheOriginalExponentGuard() { // Public operators handle zero before this guard; ILogB(0) is the // int.MinValue sentinel, whose absolute value is not representable. foreach (double value in FiniteExponentSamples()) { if (value != 0.0) { bool expected = Math.Abs(Math.ILogB(value)) <= 450; PreciseMathHelper.IsDivisionWithinFastRange(value).ShouldBe(expected, $"{value:R}"); } } } [Fact] public void MultiplicationRangeMatchesEveryNormalExponentPair() { // Exhaust all normal exponent combinations. The original guard depends // only on these exponents, never on the sign or fractional significand. double[] values = new double[2046]; for (int i = 0; i < values.Length; ++i) { values[i] = Math.ScaleB(1.0, i - 1022); } foreach (double left in values) { foreach (double right in values) { AssertMultiplicationRange(left, right); } } } [Fact] public void MultiplicationRangePreservesSubnormalAndSignedOperandPaths() { // Include all subnormal binades and both significand edges, paired // with large normals that can bring the exponent sum into range. double[] partners = [double.Epsilon, Math.BitDecrement(Math.ScaleB(1.0, -1022)), Math.ScaleB(1.0, -1022), Math.ScaleB(1.0, -901), Math.ScaleB(1.0, -900), Math.ScaleB(1.0, -1), 1.0, 2.0, Math.ScaleB(1.0, 900), Math.ScaleB(1.0, 901), Math.ScaleB(1.0, 1023), double.MaxValue]; foreach (double value in FiniteExponentSamples()) { if (value == 0.0) { continue; } foreach (double partner in partners) { AssertMultiplicationRange(value, partner); AssertMultiplicationRange(partner, value); AssertMultiplicationRange(value, -partner); AssertMultiplicationRange(-partner, value); } } } [Theory] [InlineData("+")] [InlineData("-")] [InlineData("*")] [InlineData("/")] public void FiniteKernelsPreservePreviousComponentBits(string operation) { // Differential characterization, not an independent accuracy oracle. // Freeze the previous four-TwoSum/FMA expressions and public normalization; // independent exact/rational accuracy cases remain in the arithmetic suites. List values = [new(0.0), new(-0.0)]; int[] exponents = [-1074, -1022, -901, -900, -899, -451, -450, -1, 0, 1, 450, 451, 899, 900, 901, 1020, 1021, 1023]; foreach (int exponent in exponents) { foreach (double significand in new[] { 1.0, Math.BitIncrement(1.0), 1.5, Math.BitDecrement(2.0) }) { foreach (double sign in new[] { -1.0, 1.0 }) { double high = sign * Math.ScaleB(significand, exponent); foreach (double low in new[] { 0.0, Math.ScaleB(high, -53), -Math.ScaleB(high, -53), double.Epsilon, -double.Epsilon }) { DoubleDouble value = DoubleDouble.FromComponents(high, low); if (DoubleDouble.IsFinite(value)) { values.Add(value); } } } } } foreach (DoubleDouble left in values) { foreach (DoubleDouble right in values) { DoubleDouble expected; DoubleDouble actual; if (operation == "/") { if (left.High == 0.0 || right.High == 0.0 || Math.Abs(Math.ILogB(left.High)) > 450 || Math.Abs(Math.ILogB(right.High)) > 450) { continue; } // Retain the original division expression, including its public // normalization. Sparse corrections may use MultiplyBoundary. double quotient = left.High / right.High; DoubleDouble remainder = left - (right * quotient); double correction = remainder.High / right.High; double.IsFinite(quotient).ShouldBeTrue(); double.IsFinite(correction).ShouldBeTrue(); double.IsFinite(quotient + correction).ShouldBeTrue(); // Conservative bounds from the entry domain and the first // scalar product/four-TwoSum remainder, not an accuracy claim. (Math.Abs(quotient) <= Math.ScaleB(1.0, 901)).ShouldBeTrue(); (Math.Abs(remainder.High) <= Math.ScaleB(1.0, 457)).ShouldBeTrue(); (Math.Abs(correction) <= Math.ScaleB(1.0, 908)).ShouldBeTrue(); (Math.Abs(quotient + correction) < Math.ScaleB(1.0, 909)).ShouldBeTrue(); DoubleDouble correctionProduct = right * correction; AssertDivisionResidualCancellation(remainder, correctionProduct); remainder -= correctionProduct; double finalCorrection = remainder.High / right.High; DoubleDouble normalized = DoubleDouble.FromComponents(quotient, correction); 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; actual = left / right; } else if (operation == "*") { if (left.High == 0.0 || right.High == 0.0) { continue; } int exponent = Math.ILogB(left.High) + Math.ILogB(right.High); if (exponent < -900 || exponent > 900) { continue; } (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); Math.Abs(error).ShouldBeLessThan(Math.Abs(product)); expected = DoubleDouble.FromComponents(product, error); actual = left * right; } else { if (Math.ILogB(left.High) > 1020 || Math.ILogB(right.High) > 1020) { continue; } DoubleDouble operand = operation == "-" ? -right : right; if (left.High == 0.0 && operand.High == 0.0) { expected = new DoubleDouble(left.High + operand.High); } else { (double high, double highError) = PreciseMathHelper.TwoAdd(left.High, operand.High); (double low, double lowError) = PreciseMathHelper.TwoAdd(left.Low, operand.Low); (double middle, double middleError) = PreciseMathHelper.TwoAdd(highError, low); (double sum, double sumError) = PreciseMathHelper.TwoAdd(high, middle); double correction = sumError + (middleError + lowError); double.IsFinite(sum + correction).ShouldBeTrue(); expected = DoubleDouble.FromComponents(sum, correction); } actual = operation == "-" ? left - right : left + right; } BitConverter.DoubleToInt64Bits(actual.High).ShouldBe(BitConverter.DoubleToInt64Bits(expected.High)); BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(BitConverter.DoubleToInt64Bits(expected.Low)); } } } [Fact] public void DivisionResidualCancellationPreservesHighBitsAtBinadeEdges() { // Include both sides of normal/subnormal and binade transitions, with // canonical zero, dense and sparse lows. Cross-binade/sign/zero cases // are intentionally ineligible and must retain the general subtraction. List values = [new(0.0), new(-0.0)]; foreach (int exponent in new[] { -1074, -1022, -900, -54, 0, 1, 457, 461 }) { foreach (double significand in new[] { 1.0, Math.BitIncrement(1.0), Math.BitDecrement(2.0) }) { foreach (double sign in new[] { -1.0, 1.0 }) { double high = sign * Math.ScaleB(significand, exponent); foreach (double low in new[] { 0.0, Math.ScaleB(high, -54), -Math.ScaleB(high, -54), double.Epsilon, -double.Epsilon }) { values.Add(DoubleDouble.FromComponents(high, low)); } } } } foreach (DoubleDouble remainder in values) { foreach (DoubleDouble product in values) { AssertDivisionResidualCancellation(remainder, product); } } } private static void AssertDivisionResidualCancellation(DoubleDouble remainder, DoubleDouble product) { DoubleDouble expected = remainder - product; double actual = PreciseMathHelper.SubtractDivisionCorrectionHigh(remainder, product); BitConverter.DoubleToInt64Bits(actual).ShouldBe(BitConverter.DoubleToInt64Bits(expected.High)); // Independent BCL classification, rather than the proposed exponent-bit // guard. Within one normal binade, same-sign subtraction is exact. if (!double.IsNormal(remainder.High) || !double.IsNormal(product.High) || Math.Sign(remainder.High) != Math.Sign(product.High) || Math.ILogB(remainder.High) != Math.ILogB(product.High)) { return; } (double high, double highError) = PreciseMathHelper.TwoAdd(remainder.High, -product.High); BitConverter.DoubleToInt64Bits(highError).ShouldBe(0L); (double low, double lowError) = PreciseMathHelper.TwoAdd(remainder.Low, -product.Low); (double middle, double middleError) = PreciseMathHelper.TwoAdd(highError, low); BitConverter.DoubleToInt64Bits(middle).ShouldBe(BitConverter.DoubleToInt64Bits(low)); BitConverter.DoubleToInt64Bits(middleError).ShouldBe(0L); (double sum, double sumError) = PreciseMathHelper.TwoAdd(high, low); double error = sumError + lowError; double simplifiedHigh = error == 0.0 ? sum : sum + error; BitConverter.DoubleToInt64Bits(simplifiedHigh).ShouldBe(BitConverter.DoubleToInt64Bits(expected.High)); } private static void AssertMultiplicationRange(double left, double right) { int exponent = Math.ILogB(left) + Math.ILogB(right); bool expected = exponent >= -900 && exponent <= 900; bool actual = PreciseMathHelper.IsMultiplicationWithinFastRange(left, right); // Only format diagnostics on failure in this exhaustive matrix. if (actual != expected) { actual.ShouldBe(expected, $"({left:R}, {right:R})"); } } private static IEnumerable FiniteExponentSamples() { ulong[] fractions = [0, 1, 0x0008_0000_0000_0000, 0x000f_ffff_ffff_ffff]; foreach (ulong sign in new[] { 0UL, 0x8000_0000_0000_0000UL }) { for (int bit = 0; bit < 52; ++bit) { yield return BitConverter.UInt64BitsToDouble(sign | (1UL << bit)); yield return BitConverter.UInt64BitsToDouble(sign | ((1UL << (bit + 1)) - 1)); } for (ulong exponent = 0; exponent < 0x7ff; ++exponent) { foreach (ulong fraction in fractions) { yield return BitConverter.UInt64BitsToDouble(sign | (exponent << 52) | fraction); } } } } }