From ab78cbaa6909dde34a9dac9b18708ce391eb89eb Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Mon, 3 Aug 2026 11:57:09 +0000 Subject: [PATCH 1/2] Stop presenting a near-rational root as an exact one (#235) `x^41 + 6x + 1 = 0` answered { -1/6 }. That is not a root: it leaves -1/80204967233062404407033075859456. A numeric root that lands near a simple ratio is rewritten as that ratio, which is worth doing -- Newton returns 0.4999999999999999 where 1/2 is meant. But the loose 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. So the guess was accepted, and a value that is merely close to a root was returned as though it were exact. Where the residual comes out as an exact ratio -- which it does whenever the equation and the candidate are both rational, as here -- it now has to be exactly zero. Where it does not, an equation carrying pi for instance, there is nothing to be exact about and the ordinary tolerance still decides. The equation now answers its numeric root instead, which is honest about what is known. Roots that genuinely are ratios still come back as ratios, and irrational ones are still not rounded into ratios; both are in the tests. Co-Authored-By: Claude Opus 5 --- .../AnalyticalEquationSolver.cs | 13 +++- .../Algebra/SolveTest/RootDowncastTest.cs | 64 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 Sources/Tests/UnitTests/Algebra/SolveTest/RootDowncastTest.cs diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs index 7f23da6ce..88e216352 100644 --- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs @@ -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 (#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) { @@ -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; } diff --git a/Sources/Tests/UnitTests/Algebra/SolveTest/RootDowncastTest.cs b/Sources/Tests/UnitTests/Algebra/SolveTest/RootDowncastTest.cs new file mode 100644 index 000000000..d1d2e9023 --- /dev/null +++ b/Sources/Tests/UnitTests/Algebra/SolveTest/RootDowncastTest.cs @@ -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 +{ + /// + /// 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. + /// + 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); + } + } +} From 38185104fa5a0eed1d1af3017f1ab991cae39c49 Mon Sep 17 00:00:00 2001 From: Hadrian Tang Date: Mon, 3 Aug 2026 18:15:03 +0000 Subject: [PATCH 2/2] Fix comment --- .../Solvers/EquationSolver/AnalyticalEquationSolver.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs index 88e216352..9a20f8ead 100644 --- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/AnalyticalEquationSolver.cs @@ -95,11 +95,11 @@ internal static Set Solve(Entity expr, Variable x, bool compensateSolving = fals // 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 (#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. + // 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);