From 18c8afa3692914ad8caee9a81a2eb1d0a0830184 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 27 Oct 2026 21:34:19 +0000 Subject: [PATCH] Over the reals, a modulus of a polynomial is split at the polynomial's real roots With the real codomain, ModulusSolver.SolveOverTheReals solved an equation in moduli by cases between the kinks, but only where every modulus was of a linear function. A modulus of anything else fell through to Newton's method. x^2/|1 - x^2| = x was answered { 0, 0.618..., 1.618..., 1.618... + 1.7e-9 i, 1.618... + 9.9e-8 i }, and |x^2 - 4| = 3x as { 1, 4, 4.000...0062 + 1.6e-7 i }: over the reals, with entries that are neither real nor roots. A polynomial keeps one sign between neighbouring real roots, so a modulus of a polynomial with real numeric coefficients has its real roots for kinks, found by the analytical solver and kept exact so that a piece that holds throughout is an exact interval. A polynomial with no real root has none, and the whole line is one piece. Anything else still declines, so nothing that answered changes. On 2.6.0 and on this branch, with the real codomain: x^2/abs(1-x^2) = x decimals + 2 non-roots -> { 0, (-1 + sqrt(5)) / 2, (1 + sqrt(5)) / 2 } abs(x^2 - 4) = 3*x { 1, 4, 4.0...0062 + 1.6e-7 i } -> { 1, 4 } Unit suite 17,164 passed, 0 failed (before the six new rows); RootCheck 596/596 clean; CasBench 120/121 unchanged; BoundCheck 0 disagreements. The complex codomain, the issue's own setting, is unchanged: a split there can miss roots at which the argument is not real, as x^2 + 1 = |x| has at +-0.618 i. Part of https://github.com/asc-community/AngouriMath/issues/2139 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 14 +++++ .../Solvers/EquationSolver/ModulusSolver.cs | 60 ++++++++++++++++--- .../Algebra/SolveTest/ModulusEquationTest.cs | 19 ++++++ 3 files changed, 84 insertions(+), 9 deletions(-) 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")); + } } }