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")); + } } }