From 3ec711b47de36bad0cc0f4a6b6a75c183f07c14b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 18:27:12 +0000 Subject: [PATCH] Cap Pow and Exp digits by the precision of the double exponent [patch] Pow and Exp compute e^y in a double, whose relative error grows with |y|, so for |y| in the hundreds they reported 15-17 significant digits when only 12-13 were correct. Limit the reported digits to 15 - ceil(log10(|y|)) in one helper used by Exp and by every logarithm path of Pow, including the out-of-range fallback. Fixes ktsu-dev/SignificantNumber#113 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017ft9gJVQx6JFNQ4VRoAwd9 --- .../SignificantNumberPowExpPrecisionTests.cs | 52 +++++++++++++++++++ SignificantNumber/SignificantNumber.cs | 23 ++++++++ 2 files changed, 75 insertions(+) create mode 100644 SignificantNumber.Test/SignificantNumberPowExpPrecisionTests.cs diff --git a/SignificantNumber.Test/SignificantNumberPowExpPrecisionTests.cs b/SignificantNumber.Test/SignificantNumberPowExpPrecisionTests.cs new file mode 100644 index 0000000..d3b5777 --- /dev/null +++ b/SignificantNumber.Test/SignificantNumberPowExpPrecisionTests.cs @@ -0,0 +1,52 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace SignificantNumber.Test; + +using System.Globalization; +using ktsu.PreciseNumber; +using ktsu.SignificantNumber; + +/// +/// Covers the significant digits and report +/// when the result is computed as e^y in a and |y| is large enough to cost digits. +/// +[TestClass] +public class SignificantNumberPowExpPrecisionTests +{ + private static SignificantNumber Parse(string text) => + SignificantNumber.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture); + + private static PreciseNumber Precise(string text) => + PreciseNumber.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture); + + /// + /// Asserts that every digit reports as significant matches . + /// + private static void AssertReportedDigitsAreCorrect(string trueValue, SignificantNumber result) => + Assert.AreEqual( + Precise(trueValue).ToSignificantNumber(result.SignificantDigits), + result, + $"{result} reports {result.SignificantDigits} significant digits of {trueValue}"); + + // Reference values from Python decimal at 50 digits. + [TestMethod] + [DataRow("700.12345678901234567890", "1.1475032771153835242760515951099859734230596995731E+304")] + [DataRow("-700.12345678901234567890", "8.7145720621715330952462542732455202987551213258088E-305")] + public void Exp_LargeNonIntegerPower_ReportsOnlyCorrectDigits(string power, string trueValue) => + AssertReportedDigitsAreCorrect(trueValue, SignificantNumber.Exp(Precise(power))); + + [TestMethod] + [DataRow("3.0000000000000000001", "200.50000000000000000001", "4.6005692393404968725867994056284596535772404109326E+95")] + [DataRow("3.0000000000000000001", "2.5000000000000000001", "15.588457268119895644758622250842932011355302787297")] + [DataRow("1.2345678901234567890", "2000", "1.0714068665496473862896950588742712767980475529713E+183")] + public void Pow_LargeLogarithmOfResult_ReportsOnlyCorrectDigits(string baseNumber, string power, string trueValue) => + AssertReportedDigitsAreCorrect(trueValue, Parse(baseNumber).Pow(Precise(power))); + + [TestMethod] + public void Exp_PowerNearSevenHundred_ReportsTwelveSignificantDigits() => + Assert.AreEqual(12, SignificantNumber.Exp(Precise("700.12345678901234567890")).SignificantDigits); + + [TestMethod] + public void Pow_SmallLogarithmOfResult_KeepsTheDoublesPrecision() => + Assert.AreEqual(15, Parse("1.2345678901234567890").Pow(Precise("0.5000000000000000001")).SignificantDigits); +} diff --git a/SignificantNumber/SignificantNumber.cs b/SignificantNumber/SignificantNumber.cs index c90ff9e..f879e51 100644 --- a/SignificantNumber/SignificantNumber.cs +++ b/SignificantNumber/SignificantNumber.cs @@ -644,6 +644,9 @@ public SignificantNumber Pow(PreciseNumber power) /// ||^, negated when is set. private static SignificantNumber PowByLogarithm(PreciseNumber value, PreciseNumber power, bool isNegative, int significantDigits) { + // |x|^p = e^(p × ln|x|), so the digits are bounded by how precisely e^(p × ln|x|) is known. + significantDigits = CapToExponentPrecision(significantDigits, Log10OfMagnitude(value) * Math.Log(10) * power.To()); + double logValue = Math.Log(Math.Abs(value.To())); double magnitude = Math.Exp(logValue * power.To()); if (double.IsNormal(magnitude)) @@ -680,6 +683,7 @@ public static SignificantNumber Exp(PreciseNumber power) int significantDigits = PreciseNumber.IsInteger(power) ? int.Min(E.SignificantDigits, DoubleSignificantDigits) : LowestSignificantDigits(E, power); + significantDigits = CapToExponentPrecision(significantDigits, power.To()); double result = Math.Exp(power.To()); if (double.IsNormal(result)) @@ -691,6 +695,25 @@ public static SignificantNumber Exp(PreciseNumber power) return FromLog10OfMagnitude(power.To() * Math.Log10(Math.E), isNegative: false, significantDigits); } + /// + /// Limits a significant digit count to the digits that e^ computed in a + /// can be trusted to. The exponent carries a relative error of about 2⁻⁵³, which + /// becomes an absolute error of |exponent| × 2⁻⁵³ in it, and so a relative error of that size in the result: + /// every power of ten in |exponent| costs one of the double's significant digits. + /// + /// The significant digit count to limit. + /// The natural logarithm of the magnitude of the result. + /// The lower of and the digits the double result determines, and at least 1. + private static int CapToExponentPrecision(int significantDigits, double exponent) + { + double magnitude = Math.Abs(exponent); + int lostDigits = !double.IsFinite(magnitude) + ? DoubleSignificantDigits + : magnitude > 1 ? (int)Math.Ceiling(Math.Log10(magnitude)) : 0; + + return int.Max(1, int.Min(significantDigits, DoubleSignificantDigits - lostDigits)); + } + /// /// Computes the base-10 logarithm of the magnitude of a nonzero number from its significand and exponent, so /// that it works for magnitudes outside the range of a .