diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index a838613d5..1bd0d427a 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -246,6 +246,20 @@ variable's name ([#2136](https://github.com/asc-community/AngouriMath/issues/213
| `"conj(z)".ToEntity()` | `conj * z` | throws `UnrecognizedFunctionParseException` |
| `"det([[1, 2], [3, 4]])".ToEntity()` | `det * [[1, 2], [3, 4]]` | throws `UnrecognizedFunctionParseException` |
+### Over the reals, a modulus of a polynomial is split at the polynomial's real roots
+
+**Silent: an answer with entries that solve nothing is exact.** With the real codomain an equation in moduli was
+solved by cases only where every modulus was of a linear function. A modulus of a polynomial fell through to a
+numerical search, which answered in decimals and kept non-real numbers near a root that solve nothing: an answer
+over the reals with entries that are neither real nor roots. A polynomial keeps one sign between its neighbouring
+real roots, so those are now the kinks, and each piece is solved exactly
+([#2139](https://github.com/asc-community/AngouriMath/issues/2139)).
+
+| Input, with `MathS.Settings.Codomain` set to `Domain.Real` | Was (2.6.0) | Now |
+|---|---|---|
+| `"x^2/abs(1-x^2) = x".Solve("x")` | `{ 0, 0.618…, 1.618…, 1.618… + 1.7E-9i, … }` | `{ 0, (-1 + sqrt(5)) / 2, (1 + sqrt(5)) / 2 }` |
+| `"abs(x^2 - 4) = 3*x".Solve("x")` | `{ 1, 4, 4.0000000000000062… + 1.6E-7i }` | `{ 1, 4 }` |
+
### A system with more or fewer equations than unknowns is solved or declined
**Not silent: what was an exception is an answer or `null`.** `EquationSystem.Solve` threw
diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/ModulusSolver.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/ModulusSolver.cs
index b6113e2ee..6de43fa8f 100644
--- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/ModulusSolver.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/ModulusSolver.cs
@@ -52,27 +52,35 @@ private static bool RealWhereverItIs(Entity expr, Variable x)
///
/// The real at which is zero, where every
- /// modulus in it with inside is of a linear function with real
- /// coefficients; otherwise, or where a piece is not solved.
+ /// modulus in it with inside is of a polynomial with real
+ /// coefficients whose real roots are found; otherwise, or where a
+ /// piece is not solved.
///
+ ///
+ /// A polynomial keeps one sign between two neighbouring real roots, so its real roots are
+ /// where a modulus of it can change from the argument to its negation, as a linear one's
+ /// single root is. x^2/|1 - x^2| = x has kinks at -1 and 1, and on each piece an
+ /// equation with no modulus, solved exactly; it was answered by a numerical search, in
+ /// decimals and with two non-real numbers that solve nothing.
+ /// https://github.com/asc-community/AngouriMath/issues/2139
+ ///
internal static Set? SolveOverTheReals(Entity expr, Variable x)
{
var moduli = new List();
- var kinks = new List();
+ var kinks = new List<(Entity Exact, Real Value)>();
foreach (var node in expr.Nodes)
{
if (node is not Absf(var argument) || !argument.ContainsNode(x))
continue;
- if (!TreeAnalyzer.TryGetPolyLinear(argument, x, out var slope, out var intercept)
- || intercept.ContainsNode(x) || slope.Evaled is not Real { IsZero: false }
- || (-intercept / slope).InnerSimplified is not Real kink)
+ if (RealRootsOf(argument, x) is not { } roots)
return null;
moduli.Add(argument);
- kinks.Add(kink);
+ kinks.AddRange(roots);
}
if (moduli.Count == 0)
return null;
- var ends = kinks.Distinct().OrderBy(kink => kink.EDecimal).ToList();
+ var ends = kinks.GroupBy(kink => kink.Value).Select(same => same.First())
+ .OrderBy(kink => kink.Value.EDecimal).Select(kink => kink.Exact).ToList();
// The pieces between neighbouring kinks, closed where they end at one, and on each the
// sign every modulus takes, read at a point inside it.
@@ -81,7 +89,8 @@ private static bool RealWhereverItIs(Entity expr, Variable x)
{
Entity left = i == 0 ? Real.NegativeInfinity : ends[i - 1];
Entity right = i == ends.Count ? Real.PositiveInfinity : ends[i];
- Entity inside = i == 0 ? (Entity)ends[0] - 1 : i == ends.Count ? (Entity)ends[^1] + 1 : ((Entity)ends[i - 1] + ends[i]) / 2;
+ Entity inside = ends.Count == 0 ? Integer.Zero
+ : i == 0 ? ends[0] - 1 : i == ends.Count ? ends[^1] + 1 : (ends[i - 1] + ends[i]) / 2;
var signs = new Dictionary();
foreach (var argument in moduli)
if (!signs.ContainsKey(argument))
@@ -118,6 +127,39 @@ private static bool RealWhereverItIs(Entity expr, Variable x)
return (Set)answer.InnerSimplified;
}
+ ///
+ /// The real roots of , exact and with their values, where it is a
+ /// polynomial in with real numeric coefficients whose roots the solver
+ /// finds; otherwise.
+ ///
+ private static List<(Entity Exact, Real Value)>? RealRootsOf(Entity argument, Variable x)
+ {
+ if (TreeAnalyzer.TryGetPolyLinear(argument, x, out var slope, out var intercept))
+ return !intercept.ContainsNode(x) && slope.Evaled is Real { IsZero: false }
+ && (-intercept / slope).InnerSimplified is Real kink
+ ? new() { (kink, kink) }
+ : null;
+ if (!TreeAnalyzer.TryGetPolynomial(argument, x, out var monomials)
+ || monomials.Count == 0
+ || monomials.Keys.Any(power => power.Sign < 0)
+ || monomials.Values.Any(coefficient => coefficient.ContainsNode(x) || coefficient.Evaled is not Real))
+ return null;
+ if (AnalyticalEquationSolver.Solve(argument, x) is not FiniteSet roots)
+ return null;
+ var real = new List<(Entity, Real)>();
+ foreach (var root in roots)
+ switch (root.Evaled)
+ {
+ case Real value:
+ real.Add((root, value));
+ break;
+ case Complex { ImaginaryPart.IsZero: true } value:
+ real.Add((root, value.RealPart));
+ break;
+ }
+ return real;
+ }
+
private static bool Within(Real value, Entity left, Entity right)
=> left.Evaled is Real low && right.Evaled is Real high
&& low.EDecimal.CompareTo(value.EDecimal) <= 0 && value.EDecimal.CompareTo(high.EDecimal) <= 0;
diff --git a/Sources/Tests/UnitTests/Algebra/SolveTest/ModulusEquationTest.cs b/Sources/Tests/UnitTests/Algebra/SolveTest/ModulusEquationTest.cs
index 8a8539861..830c5b0f9 100644
--- a/Sources/Tests/UnitTests/Algebra/SolveTest/ModulusEquationTest.cs
+++ b/Sources/Tests/UnitTests/Algebra/SolveTest/ModulusEquationTest.cs
@@ -51,5 +51,24 @@ public void OverTheRealsTheKinksCutItIntoCases(string equation, string expected)
using var _ = MathS.Settings.Codomain.Set(Domain.Real);
Assert.Equal(expected.ToEntity(), equation.ToEntity().Solve("x"));
}
+
+ ///
+ /// A modulus of a polynomial keeps one sign between the polynomial's neighbouring real
+ /// roots, so those are its kinks. x^2/|1 - x^2| = x was answered by a numerical
+ /// search, in decimals and with two non-real numbers that solve nothing.
+ /// #2139
+ ///
+ [Theory]
+ [InlineData("x^2/abs(1 - x^2) = x", "{ 0, (-1 + sqrt(5)) / 2, (1 + sqrt(5)) / 2 }")]
+ [InlineData("abs(x^2 - 4) = 3 x", "{ 1, 4 }")]
+ [InlineData("abs(x^2 - 1) + abs(x - 2) = 3", "{ -1, 0, 2 }")]
+ [InlineData("abs(x^2 - 2) = 1", "{ -sqrt(3), 1, -1, sqrt(3) }")]
+ [InlineData("abs(x^2 + 1) = 2", "{ 1, -1 }")]
+ [InlineData("x^2 + 1 = abs(x)", "{ }")]
+ public void OverTheRealsAPolynomialIsCutAtItsRealRoots(string equation, string expected)
+ {
+ using var _ = MathS.Settings.Codomain.Set(Domain.Real);
+ Assert.Equal(expected.ToEntity(), equation.ToEntity().Solve("x"));
+ }
}
}