diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index fa097778d..a3ed4d206 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -42,6 +42,24 @@ A not-equal written `<>` is read as `not (a = b)`, and printed that way. ## Unreleased — since 2.6.0 +### A system with a product equation is solved branch by branch + +**Silent: solutions that were missing are answered.** `EquationSystem.Solve` eliminated `x * y = 0` for `y` as `0`, +which is the answer only where `x` is not zero, and lost the solutions where it is, in a system the triangularisation +does not take. A system with a product in more than one of its unknowns is now solved once for each factor, and its +solutions are theirs together, each put back into the system as written. A row is dropped where an equation, with the +row put in, is not a finite number, as `x ln(y)` is at `(0, 0)`. A square system with an unknown that no equation +mentions answers it as a parameter, as a short system does, where it was `null` +([#2128](https://github.com/asc-community/AngouriMath/issues/2128)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `MathS.Equations("x*y", "sin(x) + y - 1").Solve("x", "y")` | `[[pi / 2 + 2 * pi * n_1, 0], [pi - pi / 2 + 2 * pi * n_1, 0]]` | `[[0, 1], [pi / 2 + 2 * pi * n_1, 0], [pi - pi / 2 + 2 * pi * n_1, 0]]` | +| `MathS.Equations("(x - 1)*(y - 2)", "e^x - y").Solve("x", "y")` | `[[ln(2), 2]]` | `[[1, e], [ln(2), 2]]` | +| `MathS.Equations("x*e^y", "x^2*y").Solve("x", "y")` | `[[0, 0]]` | `[[0, t_1], [0, 0]]` | +| `MathS.Equations("x - 1", "x^2 - 1").Solve("x", "y")` | `null` | `[[1, t_1]]` | +| `MathS.Equations("a - 1", "b * 0").Solve("a", "b")` | `null` | `[[1, t_1]]` | + ### A polynomial system is not answered with some of its solutions missing **Silent: a polynomial system that lost solutions has all of them.** `EquationSystem.Solve` triangularises a diff --git a/Sources/.editorconfig b/Sources/.editorconfig index d220dbbc3..b9d119287 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -264,6 +264,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Algebra/SolveTest/ParametricSystemTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n +[Tests/UnitTests/Algebra/SolveTest/ProductSystemTest.cs] +file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n + [Tests/UnitTests/Algebra/SolveTest/SpuriousSystemRowTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver.cs index 168cd83f4..820c750ff 100644 --- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver.cs @@ -140,11 +140,21 @@ internal static Set Solve(Entity equation, Variable x) equations, variables, out var family)) return family; + // A product is zero where any one of its factors is, and the elimination solves + // `x * y = 0` for `y` as `0`, which is the answer only where `x` is not zero. So a + // system with a product in more than one of its unknowns is solved once for each + // factor, and its solutions are theirs together. + // https://github.com/asc-community/AngouriMath/issues/2128 + if (Branches(equations, variables) is { } branches) + return Together(branches, written, vars, variables); + // Nor is any other count: `x^2 + y^2 = 1` has a family of solutions in a // parameter, and `sin(x) = 0` with `x = 0` has one solution. Those are eliminated, // and a system whose answer cannot be shown to hold is declined, as `Solve` // documents. https://github.com/asc-community/AngouriMath/issues/212 - if (equations.Count != vars.Length) + // An unknown that no equation mentions is free, as one is in a short system, and + // the eliminator has no equation to take it from. + if (equations.Count != vars.Length || variables.Any(variable => !equations.Any(equation => equation.ContainsNode(variable)))) return NonSquareSystemSolver.Solve(equations, variables, ledger) is { } families ? new MatrixBuilder(families, vars.Length).ToMatrix() : null; @@ -171,6 +181,75 @@ internal static Set Solve(Entity equation, Variable x) } } + /// The most systems a system is split into by its products. + private const int MostBranches = 64; + + /// + /// One system for each way of choosing a factor of each product in more than one of + /// , with the product replaced by the factor, or + /// where there is no such product or the choices are too many. + /// + /// + /// A product whose factors all mention the same unknowns is left alone: solving it for + /// any of them reads every factor, as x (x - 1) is solved for x as + /// 0 and 1. + /// + private static List>? Branches(List equations, Variable[] variables) + { + var branches = new List> { new(equations) }; + var split = false; + for (var i = 0; i < equations.Count; i++) + { + if (equations[i] is not Mulf) + continue; + var factors = Mulf.LinearChildren(equations[i]).Where(factor => variables.Any(factor.ContainsNode)).ToList(); + if (factors.Count < 2) + continue; + var first = variables.Where(factors[0].ContainsNode).ToList(); + if (factors.All(factor => variables.Where(factor.ContainsNode).SequenceEqual(first))) + continue; + if (branches.Count * factors.Count > MostBranches) + return null; + split = true; + branches = branches.SelectMany(branch => factors.Select(factor => + { + var chosen = new List(branch); + chosen[i] = factor; + return chosen; + })).ToList(); + } + return split ? branches : null; + } + + /// + /// The solutions of each of together, each kept only where + /// it satisfies the system as written: a factor can be zero where another is not + /// defined, as x ln(y) is at x = 0 and y = 0. + /// + /// + /// A branch answered by is taken to have no solutions, which is + /// what says of it: x e^y = 0 has none where + /// e^y is zero. + /// + private static Matrix? Together(List> branches, List written, ReadOnlySpan vars, Variable[] variables) + { + var rows = new List>(); + foreach (var branch in branches) + { + if (SolveSystem(branch, vars) is not { } solved) + continue; + for (var row = 0; row < solved.RowCount; row++) + { + var values = Enumerable.Range(0, solved.ColumnCount).Select(column => solved[row, column]).ToList(); + if (!rows.Any(other => other.SequenceEqual(values))) + rows.Add(values); + } + } + if (rows.Count == 0 || RowsThatHold(written, variables, rows) is not { Count: > 0 } kept) + return null; + return new MatrixBuilder(kept, vars.Length).ToMatrix(); + } + /// /// The rows of that satisfy each of , /// or where a row is shown not to for some value of the symbols @@ -180,8 +259,9 @@ internal static Set Solve(Entity equation, Variable x) /// /// A row is dropped on the evidence a root of one equation is dropped on, /// : an equation that, with the row put - /// in, has no symbol left and is a number away from zero. Such a row is no solution for - /// any value of anything, so dropping it loses none. + /// in, has no symbol left and is a number away from zero, or is no finite number at all, as + /// x ln(y) is at (0, 0). Such a row is no solution for any value of anything, + /// so dropping it loses none. /// /// /// A row with symbols left in it is tried at one value of them, and one that fails there @@ -207,7 +287,8 @@ internal static Set Solve(Entity equation, Variable x) for (var index = 0; index < variables.Length; index++) replacements[variables[index]] = row[index]; var residuals = equations.Select(equation => equation.Substitute(replacements)).ToList(); - if (residuals.Any(residual => !residual.Vars.Any() && StatementSolver.IsEvidenceAgainst(residual))) + if (residuals.Any(residual => !residual.Vars.Any() + && (residual.Evaled is Complex { IsFinite: false } || StatementSolver.IsEvidenceAgainst(residual)))) continue; if (residuals.Any(residual => residual.Vars.Any() && StatementSolver.IsEvidenceAgainst(AtASamplePoint(residual, own)))) return null; diff --git a/Sources/Tests/UnitTests/Algebra/SolveTest/ProductSystemTest.cs b/Sources/Tests/UnitTests/Algebra/SolveTest/ProductSystemTest.cs new file mode 100644 index 000000000..8c7be1697 --- /dev/null +++ b/Sources/Tests/UnitTests/Algebra/SolveTest/ProductSystemTest.cs @@ -0,0 +1,113 @@ +// +// 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 AngouriMath; +using AngouriMath.Extensions; +using System.Linq; +using Xunit; + +namespace AngouriMath.Tests.Algebra +{ + /// + /// Systems with an equation that is a product in more than one unknown, which the + /// elimination solved for one unknown as if the other factors were not zero: x y = 0 + /// for y is 0, and the branch x = 0 was lost. Each is now solved once + /// for each factor. + /// #2128 + /// + [Trait("Area", "Algebra")] + public sealed class ProductSystemTest + { + private static Entity.Matrix? Solve(params string[] equations) + => MathS.Equations(equations.Select(e => e.ToEntity()).ToArray()).Solve("x", "y"); + + /// + /// Each row satisfies each equation, at several values of what is left in it: an + /// integer parameter at whole numbers, anything else at values of both signs. + /// + private static void EveryRowHolds(Entity.Matrix solutions, params string[] equations) + { + for (var row = 0; row < solutions.RowCount; row++) + foreach (var equation in equations) + { + var residual = equation.ToEntity().Substitute("x", solutions[row, 0]).Substitute("y", solutions[row, 1]); + foreach (var at in new[] { -1, 0, 2 }) + { + var pinned = residual; + foreach (var symbol in residual.Vars.ToList()) + pinned = pinned.Substitute(symbol, symbol.Name.StartsWith("n_") ? at : at + 0.3); + var value = Assert.IsAssignableFrom(pinned.Evaled); + Assert.True(((Entity.Number.Real)value.Abs()).EDecimal.ToDouble() < 1e-9, $"{equation} at row {row}, {at}: {value}"); + } + } + } + + private static bool HasRow(Entity.Matrix solutions, string x, string y) + => Enumerable.Range(0, solutions.RowCount).Any(row => + (solutions[row, 0] - x).Simplify() == 0 && (solutions[row, 1] - y).Simplify() == 0); + + /// + /// The branch the elimination lost: (0, 1) for the first two, where x is + /// zero, and (1, e) for the third, where x - 1 is. + /// + [Theory] + [InlineData("x * y", "sin(x) + y - 1", 3, "0", "1")] + [InlineData("x * y", "y - cos(x)", 3, "0", "1")] + [InlineData("(x - 1) * (y - 2)", "e^x - y", 2, "1", "e")] + [InlineData("(x - 1) * (y - 2)", "e^x - y", 2, "ln(2)", "2")] + public void EachFactorIsABranch(string first, string second, int rows, string x, string y) + { + var solutions = Solve(first, second); + Assert.NotNull(solutions); + Assert.Equal(rows, solutions!.RowCount); + Assert.True(HasRow(solutions, x, y), $"({x}, {y}) is not among {solutions}"); + EveryRowHolds(solutions, first, second); + } + + /// + /// x e^y = 0 and x^2 y = 0 hold wherever x is zero, which was + /// answered as the one point (0, 0). + /// + [Fact] + public void ABranchThatLeavesAnUnknownFreeIsAFamily() + { + var solutions = Solve("x * e^y", "x^2 * y"); + Assert.NotNull(solutions); + Assert.Contains(Enumerable.Range(0, solutions!.RowCount), + row => solutions[row, 0] == 0 && solutions[row, 1] is Entity.Variable); + EveryRowHolds(solutions, "x * e^y", "x^2 * y"); + } + + /// + /// The branch x = 0 of x ln(y) = 0 with x = y is (0, 0), + /// where ln(0) is not a number and the product is not zero. + /// + [Fact] + public void AFactorZeroWhereAnotherIsUndefinedIsNoSolution() + { + var solutions = Solve("x * ln(y)", "x - y"); + Assert.NotNull(solutions); + Assert.Equal(1, solutions!.RowCount); + Assert.True(HasRow(solutions, "1", "1")); + } + + /// + /// A branch can leave an unknown that no equation mentions, and so can a caller. It is + /// a parameter, as in a system with fewer equations than unknowns; the elimination had + /// no equation to take it from and answered nothing. + /// + [Fact] + public void AnUnknownNoEquationMentionsIsAParameter() + { + var solutions = Solve("x - 1", "x^2 - 1"); + Assert.NotNull(solutions); + Assert.Equal(1, solutions!.RowCount); + Assert.Equal(Entity.Number.Integer.Create(1), solutions[0, 0]); + Assert.IsType(solutions[0, 1]); + } + } +} diff --git a/Sources/Tests/UnitTests/Algebra/SolveTest/SolveSystem.cs b/Sources/Tests/UnitTests/Algebra/SolveTest/SolveSystem.cs index a3a63717b..1338b6325 100644 --- a/Sources/Tests/UnitTests/Algebra/SolveTest/SolveSystem.cs +++ b/Sources/Tests/UnitTests/Algebra/SolveTest/SolveSystem.cs @@ -79,11 +79,15 @@ public void TestTrigSystem3() => AssertSystemSolvable(new Entity[] { "3a+4b-4c" }, new Entity.Variable[] { "a", "b", "c" }, 1); + // b * 0 is zero for every b, so b is free and the solutions are (1, t) for every t. This + // was answered null, because no equation was left to eliminate b from; an unknown that + // no equation mentions is a parameter now, as it is in a short system. + // https://github.com/asc-community/AngouriMath/issues/2128 [Fact] public void TestTrigSystem4() => AssertSystemSolvable(new Entity[] { "a - 1", "b * 0" - }, new Entity.Variable[] { "a", "b" }, 0); + }, new Entity.Variable[] { "a", "b" }, 1); // https://www.youtube.com/watch?v=dVs26SSUJSA [Fact]