diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index e7fb9c055..ec97ca21a 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -62,6 +62,25 @@ not seen, and a symmetric divergent one comes to its principal value
| `"ln(x)".ToEntity().DefiniteIntegral("x", 0, 1)` | `NaN` | `-1.00000` |
| `"1/x".ToEntity().DefiniteIntegral("x", 0, 1)` | `NaN` | `NaN` |
+### 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` |
+
### An equality in an equation or a system is read as a difference equal to zero
**Silent: wrong answers.** `MathS.SolveEquation`, `Entity.SolveEquation` and `MathS.Equations` read each equation as an
diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs b/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs
index da379c455..5d8445eb9 100644
--- a/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs
+++ b/Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs
@@ -330,17 +330,44 @@ 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;
+ // The root as GetAllRoots made it, at the working precision.
+ switch (root)
{
- 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");
+ }
+ }
+}