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
25 changes: 25 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,31 @@ one inside another was answered with the empty set, a claim that it has no solut
| `"1/(2 + p^(-3)) = 1/3".Solve("p")` | `{ }` | `{ 1, -1/2 + i * 1/2 * sqrt(3), -1/2 + i * -1/2 * sqrt(3) }` |
| `"1/(1 + x^(-2)) = y".Solve("x")` | `{ }` | `{ ((-1) / (-y) - 1) ^ (-1/2), -((-1) / (-y) - 1) ^ (-1/2) }` |

### An equation with a power of x to a number that is not real is solved rather than overflowing the stack

**A crash becomes an answer.** A power of x to an exponent that is a number but not a real one, such as
`t ^ ln(-2)` (which is how `(-2)^x` reaches the solver), was substituted by x itself. That renamed the
variable, the same equation was asked again, and the stack overflowed and took the process down. The
substitution is declined now, and the equation goes on to the other routes. Where Newton's method answers it, a
point is a root only if the last step was small relative to the point. Without that, points crawling towards
`t = 0`, where every term vanishes, were kept: `x = -12.536 + 2.550i` was answered for `(-2)^x + 4^x = 0`, a
quarter of the way from the nearest root. A point is also not a root where a function in the equation is undefined
at exact arguments: Newton's method compiles `0^i` to 0, so `x^i + x = 0` had 0 among its roots, where `0^i` has no
value. The answers are the roots that lie within the search, not the whole
family `x = (ln(-s) + 2 pi i k) / (ln(a) - ln(b))`
([#2319](https://github.com/asc-community/AngouriMath/issues/2319)).
[#2424](https://github.com/asc-community/AngouriMath/issues/2424)

| Input | Was (2.6.0) | Now |
|---|---|---|
| `"(-2)^x + 4^x = 0".Solve("x")` | stack overflow | nine roots `(2k + 1) pi i / (pi i - ln(2))`, `k` from -7 to 1, in decimals |
| `"(-3)^x + 9^x = 0".Solve("x")` | stack overflow | seven roots `(2k + 1) pi i / (pi i - ln(3))`, `k` from -5 to 1, in decimals |
| `"(-2)^x - 4^x = 0".Solve("x")` | stack overflow | eight roots `2k pi i / (pi i - ln(2))`, `k` from -6 to 1, in decimals |
| `"(-2)^x + 2^x = 0".Solve("x")` | stack overflow | `{ -5, -3, -1, 1, 3 }` |
| `"x^(1 + i) + x = 0".Solve("x")` | stack overflow | `0` and `e^((2k + 1) pi)` for `k` from -3 to 0, in decimals |
| `"x^i + x = 0".Solve("x")` | stack overflow | `{ -e^(pi/2) i, e^(-pi/2) i }`, in decimals |
| `"x^i + x^(2*i) = 0".Solve("x")` | stack overflow | `{ e^pi }`, in decimals |

### A vanishing factor times an infinity is asked as a whole when read as a part

**Silent: a limit with, somewhere inside it, a product of a factor tending to 0 and one tending to an infinity is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,13 @@ Entity ApplyPowerTransform(Entity @base, Entity arg)

MultithreadingFunctional.ExitIfCancelled();
}
// A substitution of x by itself only renames the variable, and solving the renamed
// equation asks this solver the same question again, without end. It is what is left
// when every exponent is numeric but one is not real, so the case above cannot choose
// a power of x: (-2)^x + 4^x = 0 reaches here as t^ln(-2) + t^ln(4), and so does
// x^(1 + i) + x = 0. https://github.com/asc-community/AngouriMath/issues/2424
if (substitution == x)
return null;
expr = expr.Substitute(substitution, replacement);
if (expr.ContainsNode(x)) return null; // cannot be solved, not a multiplicative exponenial equation

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ internal static Set UnsolvedWhereIndependenceIsDenied(Set roots, Entity conditio
/// Whether a root demonstrably fails the equation. Anything that cannot be
/// evaluated to a number -- a root still carrying a parameter, say
/// <c>pi + 2 * pi * n_1</c>, or one whose residual is not finite -- is kept, so
/// that a root is only ever dropped on positive evidence against it.
/// that a root is only ever dropped on positive evidence against it. A function
/// undefined at exact arguments is such evidence; see <see cref="IsUndefinedExactly"/>.
/// </summary>
private static bool IsSpurious(Entity equation, Variable x, Entity root)
{
Expand All @@ -183,7 +184,32 @@ private static bool IsSpurious(Entity equation, Variable x, Entity root)
return true;
if (root.Vars.Any())
return false;
return IsEvidenceAgainst(equation.Substitute(x, root));
var substituted = equation.Substitute(x, root);
return IsUndefinedExactly(substituted) || IsEvidenceAgainst(substituted);
}

/// <summary>
/// Whether <paramref name="substituted"/>, with no symbol left in it, has no value because
/// some function in it is undefined at its arguments: a node whose arguments are all exact
/// numbers and whose own value is not a finite number. <c>x^i + x</c> at 0 is one, since
/// <c>0^i</c> is undefined, so 0 is not a root of <c>x^i + x = 0</c> however small the
/// compiled function Newton's method iterated on is there.
/// </summary>
/// <remarks>
/// A residual that is not finite is otherwise no evidence against a root: a root found in
/// decimals carries rounding, and a function taken at it next to the edge of its domain, or
/// a term that overflows, may have no value where the root itself gives one. Exact arguments
/// have neither to come from. https://github.com/asc-community/AngouriMath/issues/2424
/// </remarks>
internal static bool IsUndefinedExactly(Entity substituted)
{
static bool IsExact(Entity argument)
=> argument.Evaled is Complex value && value.RealPart is Rational && value.ImaginaryPart is Rational;
foreach (var node in substituted.Nodes)
if (node.DirectChildren.Count > 0 && node.DirectChildren.All(IsExact)
&& node.Evaled is Complex { IsFinite: false })
return true;
return false;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,18 @@ static Complex NewtonIter(FastExpression f, FastExpression df, NumericsComplex v
{
var prev = value;

NumericsComplex ChooseGood() =>
NumericsComplex.Abs(prev - value) > (double)MathS.Settings.PrecisionErrorCommon.Value
? double.NaN
: value;
// Converged means the last step was small beside the point, not only small: near a
// zero that every term shares, t ^ ln(-2) + t ^ ln(4) at t = 0, the iteration crawls
// and its steps fall under any fixed tolerance while it is still a fifth of the way
// from where it stops, and those points came back as roots. So below 1 the step is
// measured against the point; from 1 up the tolerance is what it always was.
// https://github.com/asc-community/AngouriMath/issues/2424
NumericsComplex ChooseGood()
{
var tolerance = (double)MathS.Settings.PrecisionErrorCommon.Value;
var scale = Math.Min(1, Math.Max(NumericsComplex.Abs(value), tolerance));
return NumericsComplex.Abs(prev - value) > tolerance * scale ? double.NaN : value;
}

int minCheckIters = (int)Math.Sqrt(precision);
for (int i = 0; i < precision; i++)
Expand Down Expand Up @@ -341,11 +349,16 @@ private static Entity WithoutConditions(Entity expr)
/// </summary>
/// <returns>
/// <see langword="true"/> for anything that cannot be checked, so that a root is
/// only ever dropped on positive evidence that it is spurious.
/// only ever dropped on positive evidence that it is spurious. A point where the
/// expression is undefined at exact arguments is such evidence: the compiled form
/// takes <c>0^i</c> to be 0, and <c>x^i + x</c> was answered with the root 0.
/// </returns>
private static bool Satisfies(Entity expr, Entity.Variable v, Complex root)
{
return expr.Substitute(v, root).Evaled is not Complex residual
var substituted = expr.Substitute(v, root);
if (AnalyticalSolving.StatementSolver.IsUndefinedExactly(substituted))
return false;
return substituted.Evaled is not Complex residual
|| !residual.IsFinite
|| residual.Abs().EDecimal.LessThan(MathS.Settings.PrecisionErrorCommon);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//
// 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 System;
using System.Linq;
using System.Threading.Tasks;
using AngouriMath;
using AngouriMath.Extensions;
using Xunit;
using NumericsComplex = System.Numerics.Complex;

namespace AngouriMath.Tests.Common
{
/// <summary>
/// A power of x to an exponent that is a number but not a real one was substituted by x itself,
/// which renamed the variable and asked the same equation again until the stack overflowed:
/// <c>(-2)^x + 4^x = 0</c> reaches that as <c>t^ln(-2) + t^ln(4)</c>.
/// <a href="https://github.com/asc-community/AngouriMath/issues/2424">#2424</a>
/// </summary>
[Trait("Area", "Common")]
public sealed class ANegativeBaseExponentialSolveTerminatesTest
{
private static readonly TimeSpan Budget = LimitTermination.Guard;

private static Entity.Set Solved(string equation)
{
var task = Task.Run(() => equation.ToEntity().Solve("x"));
Assert.True(task.Wait(Budget), $"{equation} did not finish within {Budget.TotalSeconds}s");
return task.Result;
}

private static NumericsComplex Value(Entity root)
=> Assert.IsAssignableFrom<Entity.Number.Complex>(root.EvalNumerical()).ToNumerics();

/// <summary>
/// <c>a^x + s b^x = 0</c> with <c>s = 1</c> or <c>-1</c>. With principal powers it is
/// <c>e^(x (ln a - ln b)) = -s</c>, so the roots are
/// <c>x = (ln(-s) + 2 pi i k) / (ln a - ln b)</c> for every whole <c>k</c>:
/// <c>(-2)^x + 4^x = 0</c> has <c>x = (2k + 1) pi i / (pi i - ln 2)</c>, the first of which is
/// <c>0.95358 - 0.21039i</c>, and <c>(-2)^x + 2^x = 0</c> has the odd integers. Each root
/// the solver answers is to be one of them; declining is honest, an empty set is not.
/// </summary>
[Theory]
[InlineData("(-2)^x + 4^x = 0", -2, 4, 1)]
[InlineData("(-3)^x + 9^x = 0", -3, 9, 1)]
[InlineData("(-2)^x - 4^x = 0", -2, 4, -1)]
[InlineData("(-2)^x + 2^x = 0", -2, 2, 1)]
public void TheRootsAreOnTheFamily(string equation, int a, int b, int sign)
{
var solved = Solved(equation);
if (solved is not Entity.Set.FiniteSet roots)
return;
Assert.NotEmpty(roots);
var rate = NumericsComplex.Log(a) - NumericsComplex.Log(b);
var shift = NumericsComplex.Log(-sign);
foreach (var root in roots)
{
var x = Value(root);
var left = NumericsComplex.Exp(x * NumericsComplex.Log(a));
var right = sign * NumericsComplex.Exp(x * NumericsComplex.Log(b));
Assert.True(NumericsComplex.Abs(left + right) <= 1e-9 * (NumericsComplex.Abs(left) + NumericsComplex.Abs(right)),
$"{root} leaves {left + right} in {equation}");
var k = (x * rate - shift) / (2 * Math.PI * NumericsComplex.ImaginaryOne);
Assert.True(Math.Abs(k.Real - Math.Round(k.Real)) < 1e-9 && Math.Abs(k.Imaginary) < 1e-9,
$"{root} is not on the family of {equation}: k = {k}");
}
}

/// <summary>
/// The same substitution without a negative base. <c>x^(1 + i) + x</c> is
/// <c>x (x^i + 1)</c>, whose roots are 0, where <c>0^(1 + i)</c> is 0, and <c>e^((2k + 1) pi)</c>.
/// <c>x^i + x</c> and <c>x^i + x^(2i)</c> have no root at 0, where <c>0^i</c> is undefined,
/// though the function Newton's method compiles is 0 there.
/// </summary>
[Theory]
[InlineData("x^(1 + i) + x")]
[InlineData("x^i + x")]
[InlineData("x^i + x^(2*i)")]
public void AComplexExponentOfXTerminates(string expression)
{
if (Solved(expression + " = 0") is not Entity.Set.FiniteSet roots)
return;
Assert.NotEmpty(roots);
foreach (var root in roots)
{
var residual = expression.ToEntity().Substitute("x", root).EvalNumerical();
var value = Assert.IsAssignableFrom<Entity.Number.Complex>(residual);
Assert.True(value.IsFinite && NumericsComplex.Abs(value.ToNumerics()) <= 1e-9 * Math.Max(1, NumericsComplex.Abs(Value(root))),
$"{root} leaves {residual} in {expression} = 0");
}
}
}
}
Loading