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"); + } + } +}