diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index a70ca6bd5..12d497e91 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -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
diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/ExponentialSolver.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/ExponentialSolver.cs
index 0b0fb73ae..32edf38da 100644
--- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/ExponentialSolver.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/ExponentialSolver.cs
@@ -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
diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/SolveStatement.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/SolveStatement.cs
index b867e52be..c8691bb9e 100644
--- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/SolveStatement.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/SolveStatement.cs
@@ -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
/// pi + 2 * pi * n_1, 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 .
///
private static bool IsSpurious(Entity equation, Variable x, Entity root)
{
@@ -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);
+ }
+
+ ///
+ /// Whether , 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. x^i + x at 0 is one, since
+ /// 0^i is undefined, so 0 is not a root of x^i + x = 0 however small the
+ /// compiled function Newton's method iterated on is there.
+ ///
+ ///
+ /// 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
+ ///
+ 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;
}
///
diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/NumericalSolving/NewtonSolver.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/NumericalSolving/NewtonSolver.cs
index 3719a1555..009d9a153 100644
--- a/Sources/AngouriMath/Functions/Continuous/Solvers/NumericalSolving/NewtonSolver.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Solvers/NumericalSolving/NewtonSolver.cs
@@ -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++)
@@ -341,11 +349,16 @@ private static Entity WithoutConditions(Entity expr)
///
///
/// 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 0^i to be 0, and x^i + x was answered with the root 0.
///
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);
}
diff --git a/Sources/Tests/UnitTests/Common/ANegativeBaseExponentialSolveTerminatesTest.cs b/Sources/Tests/UnitTests/Common/ANegativeBaseExponentialSolveTerminatesTest.cs
new file mode 100644
index 000000000..da3425545
--- /dev/null
+++ b/Sources/Tests/UnitTests/Common/ANegativeBaseExponentialSolveTerminatesTest.cs
@@ -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
+{
+ ///
+ /// 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:
+ /// (-2)^x + 4^x = 0 reaches that as t^ln(-2) + t^ln(4).
+ /// #2424
+ ///
+ [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(root.EvalNumerical()).ToNumerics();
+
+ ///
+ /// a^x + s b^x = 0 with s = 1 or -1. With principal powers it is
+ /// e^(x (ln a - ln b)) = -s, so the roots are
+ /// x = (ln(-s) + 2 pi i k) / (ln a - ln b) for every whole k:
+ /// (-2)^x + 4^x = 0 has x = (2k + 1) pi i / (pi i - ln 2), the first of which is
+ /// 0.95358 - 0.21039i, and (-2)^x + 2^x = 0 has the odd integers. Each root
+ /// the solver answers is to be one of them; declining is honest, an empty set is not.
+ ///
+ [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}");
+ }
+ }
+
+ ///
+ /// The same substitution without a negative base. x^(1 + i) + x is
+ /// x (x^i + 1), whose roots are 0, where 0^(1 + i) is 0, and e^((2k + 1) pi).
+ /// x^i + x and x^i + x^(2i) have no root at 0, where 0^i is undefined,
+ /// though the function Newton's method compiles is 0 there.
+ ///
+ [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(residual);
+ Assert.True(value.IsFinite && NumericsComplex.Abs(value.ToNumerics()) <= 1e-9 * Math.Max(1, NumericsComplex.Abs(Value(root))),
+ $"{root} leaves {residual} in {expression} = 0");
+ }
+ }
+ }
+}