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
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,17 @@ internal static Set Solve(Entity expr, Variable x, bool compensateSolving = fals
if (expr == x)
return new Entity[] { 0 }.ToSet();

// Whether a candidate root really is one. The loose tolerance that guesses the
// rational must not also be what decides this: at 1e-7, x^41 + 6x + 1 accepted
// -1/6, whose residual is -1.25e-32 -- small, but the difference between an
// answer and a decoration (https://github.com/asc-community/AngouriMath/issues/235).
// Where the residual comes out as an exact ratio, which is the case whenever the
// equation and the candidate are both rational, it is required to be exactly zero.
// Where it does not -- an equation carrying pi, say -- there is nothing to be exact
// about and the ordinary tolerance still decides.
static bool IsGenuineRoot(Complex residual)
=> residual is Rational ratio ? ratio.ERational.IsZero : IsZero(residual);

// Applies an attempt to downcast roots
static Entity TryDowncast(Entity equation, Variable x, Entity root)
{
Expand All @@ -102,7 +113,7 @@ static Entity TryDowncast(Entity equation, Variable x, Entity root)
var downcasted = Complex.Create(preciseValue.RealPart, preciseValue.ImaginaryPart);
if (equation.Substitute(x, downcasted).Evaled is not Complex error)
return root;
return IsZero(error) && downcasted.RealPart is Rational && downcasted.ImaginaryPart is Rational
return IsGenuineRoot(error) && downcasted.RealPart is Rational && downcasted.ImaginaryPart is Rational
? downcasted : root.InnerSimplified;
}

Expand Down
64 changes: 64 additions & 0 deletions Sources/Tests/UnitTests/Algebra/SolveTest/RootDowncastTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// Copyright (c) 2019-2022 Angouri.
// AngouriMath is licensed under MIT.
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
// Website: https://am.angouri.org.
//

using AngouriMath;
using AngouriMath.Extensions;
using Xunit;

namespace AngouriMath.Tests.Algebra
{
/// <summary>
/// A numeric root that lands near a simple ratio is rewritten as that ratio. These
/// pin when that is allowed to happen, since a ratio is read as an exact answer.
/// </summary>
public sealed class RootDowncastTest
{
private static Entity SingleRoot(string equation) =>
Assert.Single((Entity.Set.FiniteSet)equation.ToEntity().Solve("x"));

// https://github.com/asc-community/AngouriMath/issues/235
// x^41 + 6x + 1 = 0 answered -1/6, which is not a root: it leaves
// -1/80204967233062404407033075859456. The tolerance that guesses the ratio was
// also deciding whether the guess was right, and at 1e-7 a residual of 1.25e-32
// passes for zero.
[Fact]
public void Issue235_NearRationalRootIsNotPresentedAsExact()
{
var root = SingleRoot("x + x + x + x + x + x + x41 + 1 = 0");
Assert.False(root is Entity.Number.Rational,
$"root came back as the exact {root.Stringize()}, which is not a root");
// It is still the right root, to the accuracy the numeric solver has.
Assert.Equal(-1.0 / 6, root.EvalNumerical().RealPart.EDecimal.ToDouble(), 15);
}

[Fact]
public void Issue235_TheRejectedRatioReallyIsNotARoot() =>
Assert.NotEqual(Entity.Number.Integer.Create(0),
"x + x + x + x + x + x + x41 + 1".ToEntity().Substitute("x", "-1/6".ToEntity()).Evaled);

// Roots that genuinely are ratios must still come back as ratios, or the fix has
// traded one wrong answer for uglier right ones.
[Theory]
[InlineData("2 * x - 1 = 0", "1/2")]
[InlineData("x ^ 2 - 1/4 = 0", "1/2")]
[InlineData("3 * x + 2 = 0", "-2/3")]
[InlineData("x ^ 2 + 2 * x + 1 = 0", "-1")]
public void GenuineRationalRootsStayExact(string equation, string expected)
{
var roots = (Entity.Set.FiniteSet)equation.ToEntity().Solve("x");
Assert.Contains(expected.ToEntity().InnerSimplified, roots);
}

// And irrational ones must not be rounded into a ratio either.
[Fact]
public void IrrationalRootsStayIrrational()
{
var roots = (Entity.Set.FiniteSet)"x ^ 2 - 2 = 0".ToEntity().Solve("x");
Assert.Contains("sqrt(2)".ToEntity().InnerSimplified, roots);
}
}
}
Loading