Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 33 additions & 6 deletions Sources/AngouriMath/Core/Entity/Continuous/Number/Operators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
69 changes: 69 additions & 0 deletions Sources/Tests/UnitTests/Core/ARootOfASmallNumberTest.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// <a href="https://github.com/asc-community/AngouriMath/issues/2065">#2065</a>
/// </summary>
[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<Real>(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<Complex>("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<Complex>("(1 + i/10^8)^(1/2)".EvalNumerical());
AssertClose("4.9999999999999999375000000000000027343749999999998388671875E-9", root.ImaginaryPart.EDecimal, "1E-12");
}
}
}
Loading