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 @@ -31,8 +31,16 @@ internal static Set Solve(Entity equation, Variable x)

if (solutions is FiniteSet finiteSet)
{
return finiteSet.Select(simplifier)
var finite = finiteSet.Select(simplifier)
.Where(elem => elem.IsFinite && factorizer(equation.Substitute(x, elem)).IsFinite).ToSet();
// An equation written out as expr = 0 has had its answers checked against it
// since the extraneous root of ln(x) + ln(x+1) = 0 was fixed, but the same
// equation written as the bare expr came through here instead and was only
// checked for being finite. The two disagreed:
// (2^x + 2^(2x) - 6 = 0).Solve(x) gave { 1 } while
// "2^x + 2^(2x) - 6".SolveEquation(x) gave { 1, ln((-3) ^ (1 / ln(2))) },
// and the second of those is not a root of anything.
return StatementSolver.WithoutSpuriousRoots(finite, equation, x);
}
else
return solutions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

using AngouriMath.Extensions;
using PeterO.Numbers;
using AngouriMath.Functions.Continuous.Solvers.SetSolver;
using static AngouriMath.Entity;
using static AngouriMath.Entity.Set;
Expand Down Expand Up @@ -36,11 +37,35 @@ private static Entity Minus(Entity left, Entity right)
/// substitutions that follows, so the answers are checked once, here, against the
/// equation as the caller wrote it.
/// </remarks>
private static Set WithoutSpuriousRoots(Set roots, Entity equation, Variable x)
internal static Set WithoutSpuriousRoots(Set roots, Entity equation, Variable x)
=> roots is FiniteSet finite && finite.Any(root => IsSpurious(equation, x, root))
? finite.Where(root => !IsSpurious(equation, x, root)).ToSet()
: roots;

/// <summary>
/// How large the residual has to be next to the terms it is the sum of before it
/// counts as evidence against a root.
/// </summary>
/// <remarks>
/// A residual that is merely non-zero is not evidence. A root found numerically is
/// only as accurate as the search that found it, and the terms of the equation at
/// such a root cancel to within that accuracy rather than exactly. Judging the
/// residual on its own size threw away every root of
/// <c>1/210 - 17x/210 + 101x^2/210 - 247x^3/210 + x^4</c>, whose four roots come
/// back as decimals and leave 5.2e-6 against terms of about 1.2e-2, and answered
/// that quartic with the empty set.
///
/// The two cases are far apart, which is what makes a threshold possible at all.
/// A spurious root is not a near miss: it comes from a rewrite that widened the
/// domain, so the residual is a whole quantity in its own right -- 2*pi*i for the
/// logarithm sum, or the difference between -3 and 9 for an exponential. Over the
/// equations measured, every genuine root sits at 4.5e-4 or below and most at
/// 1e-15, while every spurious one sits between 1.5 and 2. This is a hundredth:
/// twenty times above the worst genuine root and a hundred and fifty times below
/// the mildest spurious one.
/// </remarks>
[ConstantField] private static readonly EDecimal RelativeResidualTolerance = EDecimal.FromString("0.01");

/// <summary>
/// Whether a root demonstrably fails the equation. Anything that cannot be
/// evaluated to a number -- a root still carrying a parameter, say
Expand All @@ -53,14 +78,33 @@ private static bool IsSpurious(Entity equation, Variable x, Entity root)
return false;
try
{
return equation.Substitute(x, root).Evaled is Number.Complex residual
&& residual.IsFinite
&& !residual.Abs().EDecimal.LessThan(MathS.Settings.PrecisionErrorCommon);
var substituted = equation.Substitute(x, root);
if (substituted.Evaled is not Number.Complex residual || !residual.IsFinite)
return false;
var size = residual.Abs().EDecimal;
if (size.LessThan(MathS.Settings.PrecisionErrorCommon))
return false;
return size.GreaterThan(LargestTerm(substituted).Multiply(RelativeResidualTolerance));
}
catch (Core.Exceptions.AngouriBugException) { throw; }
catch (System.Exception) { return false; }
}

/// <summary>
/// The largest of the terms the equation adds up to at the root, which is the scale
/// the residual has to be read against. A single term is its own largest, so an
/// equation that is not a sum is judged on the size of its residual alone -- there
/// is nothing there for a root to have cancelled inexactly.
/// </summary>
private static EDecimal LargestTerm(Entity substituted)
{
var largest = EDecimal.Zero;
foreach (var term in Sumf.LinearChildren(substituted))
if (term.Evaled is Number.Complex value && value.IsFinite)
largest = EDecimal.Max(largest, value.Abs().EDecimal);
return largest;
}

internal static Set Solve(Entity expr, Variable x)
=> expr switch
{
Expand Down
126 changes: 126 additions & 0 deletions Sources/Tests/UnitTests/Algebra/SolveTest/SpuriousRootTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//
// 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 System.Collections.Generic;
using System.Linq;
using AngouriMath;
using AngouriMath.Extensions;
using Xunit;

namespace AngouriMath.Tests.Algebra.SolveTest
{
/// <summary>
/// An equation and the same equation written out as <c>= 0</c> have to be answered
/// alike, and neither answer may contain something that is not a root. No issue is
/// filed for this; it was found while checking whether
/// https://github.com/asc-community/AngouriMath/issues/214 could be closed.
/// </summary>
public sealed class SpuriousRootTest
{
/// <summary>
/// Whether every root returned satisfies the equation it came from. Checked
/// numerically, because the point is what the answers are worth as numbers and not
/// what form they are written in.
/// </summary>
private static void AssertEveryRootSatisfies(string expression, params string[] variables)
{
var expr = expression.ToEntity();
foreach (var variable in variables)
{
var roots = expr.SolveEquation(variable);
var finite = Assert.IsType<Entity.Set.FiniteSet>(roots);
foreach (var root in finite)
{
if (root.Vars.Any())
continue;
var residual = expr.Substitute(variable, root).EvalNumerical();
Assert.True(
residual.Abs().EDecimal.ToDouble() < 1e-6,
$"{root.Stringize()} is not a root of {expression}; it leaves {residual.Stringize()}");
}
}
}

/// <summary>
/// The exponential solver substitutes t = e^x and inverts what it finds with
/// t = v^(1/p). Composing that with x = ln(t) is only the same number while the
/// imaginary part stays inside the principal branch, and for a negative v it does
/// not: ln((-3) ^ (1 / ln(2))) is 1.58 - 1.75i, a full turn away from the root
/// ln(-3) / ln(2) = 1.58 + 4.53i, and satisfies nothing.
/// </summary>
[Theory]
[InlineData("2 ^ x + 2 ^ (2 * x) - 6")]
[InlineData("2 ^ (x ^ 2 + 1) + 2 ^ (2 * x ^ 2 - 1) - 3")]
[InlineData("3 ^ x + 3 ^ (2 * x) - 12")]
[InlineData("e ^ x + e ^ (2 * x) - 2")]
[InlineData("2 ^ (x + 1) + 2 ^ (2 * x ^ 2 - 1) + 1")]
public void AnExponentialEquationReturnsOnlyRoots(string expression) =>
AssertEveryRootSatisfies(expression, "x");

// The answer must not depend on whether the caller wrote the equation out.
[Theory]
[InlineData("2 ^ x + 2 ^ (2 * x) - 6")]
[InlineData("ln(x) + ln(x + 1)")]
[InlineData("x ^ 2 - 4")]
[InlineData("sin(x)")]
[InlineData("x ^ 3 - 1")]
public void WritingOutTheEqualsSignChangesNothing(string expression)
{
var bare = Assert.IsType<Entity.Set.FiniteSet>(expression.ToEntity().SolveEquation("x"));
var written = Assert.IsType<Entity.Set.FiniteSet>($"{expression} = 0".ToEntity().Solve("x"));
// Compared as numbers rather than as trees: the two arrive at the same roots by
// different routes and write them differently, sqrt(3/4) * (-1) against
// (-1/2) * sqrt(3) and -1/2 against -0.5, while the claim here is only about
// which numbers are answered.
Assert.Equal(AsNumbers(written), AsNumbers(bare));
}

private static List<string> AsNumbers(Entity.Set.FiniteSet roots)
{
var numbers = roots
.Where(root => !root.Vars.Any())
.Select(root => root.EvalNumerical())
.Select(value => $"{value.RealPart.EDecimal.ToDouble():G8} {value.ImaginaryPart.EDecimal.ToDouble():G8}")
.ToList();
numbers.Sort(System.StringComparer.Ordinal);
return numbers;
}

/// <summary>
/// A root found numerically is only as accurate as the search that found it, so the
/// terms of the equation cancel at it to within that accuracy rather than exactly.
/// This quartic's four roots come back as decimals and leave 5.2e-6 against terms of
/// about 1.2e-2, and judging that residual on its own size answered the whole
/// equation with the empty set.
/// </summary>
[Theory]
[InlineData("1/210 - (17*x)/210 + (101*x^2)/210 - (247*x^3)/210 + x^4", 4)]
[InlineData("x^4 - 10*x^3 + 35*x^2 - 50*x + 24", 4)]
public void RootsThatOnlyCancelToTheAccuracyTheyWereFoundWithAreKept(string expression, int count)
{
var bare = Assert.IsType<Entity.Set.FiniteSet>(expression.ToEntity().SolveEquation("x"));
var written = Assert.IsType<Entity.Set.FiniteSet>($"{expression} = 0".ToEntity().Solve("x"));
Assert.Equal(count, bare.Count);
Assert.Equal(count, written.Count);
}

// Nothing is dropped from the equations that were already answered correctly, and
// a root carrying a parameter is kept rather than judged.
[Theory]
[InlineData("x ^ 2 - 4", 2)]
[InlineData("x ^ 2 + 1", 2)]
[InlineData("x ^ 3 - 1", 3)]
[InlineData("2 ^ x - 8", 1)]
[InlineData("ln(x) ^ 2 - 3 * ln(x) + 2", 2)]
[InlineData("sin(x)", 2)]
public void TheRootsThatWereRightAreStillThere(string expression, int count)
{
var roots = Assert.IsType<Entity.Set.FiniteSet>(expression.ToEntity().SolveEquation("x"));
Assert.Equal(count, roots.Count);
}
}
}
Loading