From 626969271e14449adf79c8fb3b553f4129d2ee3b Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Thu, 22 Oct 2026 14:56:43 +0000 Subject: [PATCH 1/2] Search for a root of a small or a large number at the size of one FindGoodRoot looks for an exact root. It rounded each candidate with the zero range set to 1e-6 and accepted a rational one whose power was within 1e-6 of the base, both absolutely. A root below 1e-6 became the integer 0, and 0 ^ q is within 1e-6 of a base below it, so sqrt(2/10^20), (3/10^30)^(1/3) and sqrt(i/10^20) were 0. And the rounded candidate was what it returned as the real root when no rational one held: the root of 10^12 + 1/2 was 10^6, and (2 + i/10^8)^(1/2) lost its imaginary part. The base is scaled to the size of one by an exact power of ten and the root scaled back, so sqrt(1/10^20) is 1/10^10 exactly. A candidate is rounded only to look for a rational root, which is accepted where its power is the base at the working precision; a real root is the candidate as computed. The real cube root of a negative base stays the one given: (-2/10^21)^(1/3) is -1.26e-7, as (-8)^(1/3) is -2. Fixes #2065. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 19 +++++ .../Entity/Continuous/Number/Operators.cs | 38 ++++++++-- .../UnitTests/Core/ARootOfASmallNumberTest.cs | 69 +++++++++++++++++++ 3 files changed, 120 insertions(+), 6 deletions(-) create mode 100644 Sources/Tests/UnitTests/Core/ARootOfASmallNumberTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 9f1a36a05..6bfdeacd4 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -58,6 +58,25 @@ as `a - b = 0`, as a statement's own solve reads it: `"x + 1 = 3".Solve("x")` wa | `MathS.Equations("x + y = 3", "x - y = 1").Solve("x", "y")` | `null` | `[[2, 1]]` | | `MathS.Equations("x + y = 3", "x - y = 1").ToString()` | `x + y = 3 = 0` and `x - y = 1 = 0` | `x + y - 3 = 0` and `x - y - 1 = 0` | +### A root of a small or a large number is searched for at the size of one + +**Silent: wrong answers.** The search for an exact root of a number rounded each candidate to six digits, absolutely. +A root below `1e-6` became the integer `0`, which was accepted because `0 ^ q` is within `1e-6` of a base below it; and +a rounded candidate was returned as the real root even where its power was not the base. It now searches with the +base scaled to the size of one by an exact power of ten. It accepts a rational root only where its power is the base +at the working precision, and returns a real root as it was computed +([#2065](https://github.com/asc-community/AngouriMath/issues/2065)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `"sqrt(2/10^20)".EvalNumerical()` | `0` | `1.4142135623730950…E-10` | +| `"sqrt(1/10^20)".EvalNumerical()` | `0` | `1/10000000000` | +| `"(-2/10^21)^(1/3)".EvalNumerical()` | `0` | `-1.2599210498948731…E-7` | +| `"sqrt(i/10^20)".EvalNumerical()` | `0` | `7.0710678118654752…E-11 + 7.0710678118654752…E-11i` | +| `"(10^12 + 1/2)^(1/2)".EvalNumerical()` | `1000000` | `1000000.00000024999999999996875…` | +| `"(2 + i/10^8)^(1/2)".EvalNumerical()` | `1.4142135623730950…` | `1.4142135623730950… + 3.5355339059327376…E-9i` | +| `"(-8)^(1/3)".EvalNumerical()`, `"sqrt(10^30)".EvalNumerical()` | `-2`, `1000000000000000` | `-2`, `1000000000000000` | + --- ## 2.6.0 — since 2.5.0 diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs b/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs index da379c455..72e65339c 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs @@ -330,17 +330,43 @@ private static bool CanHaveARealRoot(Complex @base, EInteger rootPower) => @base is not Real { IsNegative: true } || !rootPower.IsEven; internal static Real? FindGoodRoot(Complex @base, Integer power) + { + // Searched at the size of one, by an exact power of ten: the candidates are rounded with + // the zero range at 1e-6, so a root below it was the integer 0, and 0 ^ q was within 1e-6 + // of the base, so sqrt(1/10^20) was 0. Scaled, it is sqrt(1) / 10^10. + // https://github.com/asc-community/AngouriMath/issues/2065 + var size = EDecimal.Max(@base.RealPart.EDecimal.Abs(), @base.ImaginaryPart.EDecimal.Abs()); + if (!size.IsZero && size.IsFinite) + { + var shift = size.Exponent.Add(size.Precision()).Subtract(1).Divide(power.EInteger); + if (!shift.IsZero) + { + Real factor = Integer.Create(EInteger.FromInt32(10).Pow(shift.Abs())); + Complex power10 = Integer.Create(EInteger.FromInt32(10).Pow(shift.Abs().Multiply(power.EInteger))); + var scaled = shift.Sign < 0 ? @base * power10 : @base / power10; + return FindGoodRootNearOne(scaled, power) is { } root ? (shift.Sign < 0 ? root / factor : root * factor) : null; + } + } + return FindGoodRootNearOne(@base, power); + } + + private static Real? FindGoodRootNearOne(Complex @base, Integer power) { Real? positive = null, real = null; foreach (var root in GetAllRoots(@base, power.EInteger)) { - using var _ = MathS.Settings.FloatToRationalIterCount.Set(15); - using var __ = MathS.Settings.PrecisionErrorZeroRange.Set(1e-6m); - var toCheck = Complex.Create(root.RealPart, root.ImaginaryPart); - switch (toCheck) + // Rounded to six digits only to look for a rational root, which is that root only if its + // power is the base at the working precision: within 1e-6, sqrt(10^12 + 1/2) was 10^6. + // A real root is the candidate as computed, never the rounded one. + // https://github.com/asc-community/AngouriMath/issues/2065 + Complex rounded; + using (MathS.Settings.FloatToRationalIterCount.Set(15)) + using (MathS.Settings.PrecisionErrorZeroRange.Set(1e-6m)) + rounded = Complex.Create(root.RealPart, root.ImaginaryPart); + if (rounded is Rational rational && IsZero(Pow(rational, power) - @base)) // To keep user's desired precision + return rational; + switch (Complex.Create(root.RealPart, root.ImaginaryPart)) { - case Rational rational when IsZero(Pow(rational, power) - @base): // To keep user's desired precision - return rational; case Real r when r > 0: positive ??= r; break; diff --git a/Sources/Tests/UnitTests/Core/ARootOfASmallNumberTest.cs b/Sources/Tests/UnitTests/Core/ARootOfASmallNumberTest.cs new file mode 100644 index 000000000..5f2031733 --- /dev/null +++ b/Sources/Tests/UnitTests/Core/ARootOfASmallNumberTest.cs @@ -0,0 +1,69 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using AngouriMath.Extensions; +using PeterO.Numbers; +using Xunit; +using static AngouriMath.Entity.Number; + +namespace AngouriMath.Tests.Core +{ + /// + /// The search for an exact root rounds its candidates to six digits. A root below 1e-6 was the integer 0, which was + /// accepted because 0 ^ q is within 1e-6 of a base below it; and a rounded candidate was returned as the real root + /// even where its power was not the base. It searches at the size of one, by an exact power of ten, accepts a + /// rational root only where its power is the base at the working precision, and returns a real root as computed. + /// References from mpmath. + /// #2065 + /// + [Trait("Area", "Core")] + public sealed class ARootOfASmallNumberTest + { + private static void AssertClose(string reference, EDecimal actual, string relativeTolerance = "1E-40") + { + var expected = EDecimal.FromString(reference); + var error = actual.Subtract(expected, EContext.ForPrecision(200)).Abs().Divide(expected.Abs(), EContext.ForPrecision(10)); + Assert.True(error.LessThan(EDecimal.FromString(relativeTolerance)), $"{actual} where it is {expected}"); + } + + [Theory] + [InlineData("sqrt(2/10^20)", "1.41421356237309504880168872420969807856967187537694807317668E-10")] + [InlineData("(3/10^30)^(1/3)", "1.44224957030740838232163831078010958839186925349935057754642E-10")] + // The real root, as (-8)^(1/3) is -2. + [InlineData("(-2/10^21)^(1/3)", "-1.25992104989487316476721060727822835057025146470150798008198E-7")] + [InlineData("(10^12 + 1/2)^(1/2)", "1000000.00000024999999999996875000000000781249999999755859375")] + public void IsItsValue(string expression, string reference) + => AssertClose(reference, Assert.IsType(expression.EvalNumerical()).EDecimal); + + [Theory] + [InlineData("sqrt(1/10^20)", "1/10000000000")] + [InlineData("(-1/10^30)^(1/3)", "-1/10000000000")] + [InlineData("(16/10^40)^(1/4)", "1/5000000000")] + [InlineData("sqrt(10^30)", "1000000000000000")] + [InlineData("(-8)^(1/3)", "-2")] + [InlineData("(27/8)^(2/3)", "9/4")] + public void AnExactRootIsExact(string expression, string root) + => Assert.Equal(root.ToEntity(), expression.EvalNumerical()); + + [Fact] + public void TheRootOfASmallImaginaryIsComplex() + { + var root = Assert.IsType("sqrt(i/10^20)".EvalNumerical()); + AssertClose("7.07106781186547524400844362104849039284835937688474036588340E-11", root.RealPart.EDecimal); + AssertClose("7.07106781186547524400844362104849039284835937688474036588340E-11", root.ImaginaryPart.EDecimal); + } + + [Fact] + public void ARootOffTheRealLineKeepsItsImaginaryPart() + { + // It was 1: the candidate rounded to six digits lost 5e-9 i. To twelve digits, because the modulus, + // 1 + 5e-17, is read as 1 within 1e-16 (https://github.com/asc-community/AngouriMath/issues/1944). + var root = Assert.IsType("(1 + i/10^8)^(1/2)".EvalNumerical()); + AssertClose("4.9999999999999999375000000000000027343749999999998388671875E-9", root.ImaginaryPart.EDecimal, "1E-12"); + } + } +} From bb0fe65201dff3a0dcf015cf16be0c1b2e159c8d Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Thu, 22 Oct 2026 16:07:27 +0000 Subject: [PATCH 2/2] Take the real root as the root search made it The real root is the candidate as GetAllRoots computed it at the working precision, where it was made a second time from its parts for every candidate: (-27)^(1/3) allocated 8.0% more than master, sqrt(2) 1.4%. Now both are within 0.1% of master, measured per call. Part of #2065. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs b/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs index 72e65339c..5d8445eb9 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs @@ -365,7 +365,8 @@ private static bool CanHaveARealRoot(Complex @base, EInteger rootPower) => rounded = Complex.Create(root.RealPart, root.ImaginaryPart); if (rounded is Rational rational && IsZero(Pow(rational, power) - @base)) // To keep user's desired precision return rational; - switch (Complex.Create(root.RealPart, root.ImaginaryPart)) + // The root as GetAllRoots made it, at the working precision. + switch (root) { case Real r when r > 0: positive ??= r;