From ceaee37db4a26f201fd2822fb4d7c464e1bf8047 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Mon, 26 Oct 2026 16:54:48 +0000 Subject: [PATCH 1/2] A system with a product equation is solved branch by branch The eliminator solved x * y = 0 for y as 0, which is the answer only where x is not zero, and in a system the triangularisation does not take nothing followed the branch where x is. Each row that came back satisfied the system, so the answer read as all of the solutions: x*y; sin(x) + y - 1 missing (0, 1) (x - 1)*(y - 2); e^x - y missing (1, e) x*e^y; x^2*y [[0, 0]], where every (0, t) is a solution A system with a product in more than one of its unknowns is now solved once for each way of choosing a factor of each such product, up to 64 systems, and its solutions are theirs together. A product whose factors all mention the same unknowns is left alone, since solving it reads every factor. A branch that answers null is taken to have no solutions, which is what Solve says of it. Every row of the union is put back into the system as written, because a factor can be zero where another is not defined: the branch x = 0 of x ln(y), x - y is (0, 0), where ln(0) is no number. A row whose residual has no symbol left and is not a finite number is now dropped, on the square path too, as a root of one equation is. A branch can leave an unknown that no equation mentions, and the eliminator had no equation to take it from and answered nothing. Such a system goes to the non-square solver, which makes the unknown a parameter: x - 1, x^2 - 1 in x, y was null and is [[1, t_1]]. SolveSystem.TestTrigSystem4 pinned null for a - 1, b * 0, where b * 0 is zero for every b; it now expects the one family (1, t_1). Measured with the generated check from #2126 on 1190 square systems in x and y built from a known integer solution: the known solution missing 22 times on master and 13 here, with no other change in its findings and no wrong row on either. The 13 left are roots found by Newton's method, one of several, and systems with abs. Fixes https://github.com/asc-community/AngouriMath/issues/2128 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 18 +++ Sources/.editorconfig | 3 + .../Continuous/Solvers/EquationSolver.cs | 89 +++++++++++++- .../Algebra/SolveTest/ProductSystemTest.cs | 113 ++++++++++++++++++ .../Algebra/SolveTest/SolveSystem.cs | 6 +- 5 files changed, 224 insertions(+), 5 deletions(-) create mode 100644 Sources/Tests/UnitTests/Algebra/SolveTest/ProductSystemTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 74fe3f736..624c9cc75 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -60,6 +60,24 @@ is `null`. A row with symbols in it is tried at one value of them, and where it | `MathS.Equations("ln(x) + ln(y)", "x - y").Solve("x", "y")` | `[[-1, -1], [1, 1]]` | `[[1, 1]]`: `ln(-1) + ln(-1)` is `2 pi i` | | `MathS.Equations("sqrt(y) - x", "y - a").Solve("x", "y")` | `[[sqrt(a), (sqrt(a)) ^ 2], [-sqrt(a), (-sqrt(a)) ^ 2]]` | `null`: the second row is a solution only at `a = 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 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/.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] From 36c8662f34634b100b9b6367e33a109eb06506c9 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Mon, 26 Oct 2026 17:31:30 +0000 Subject: [PATCH 2/2] Put the #2128 entry first in Unreleased, now that #2127 has merged Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 13bac04a3..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 @@ -77,24 +95,6 @@ is `null`. A row with symbols in it is tried at one value of them, and where it | `MathS.Equations("ln(x) + ln(y)", "x - y").Solve("x", "y")` | `[[-1, -1], [1, 1]]` | `[[1, 1]]`: `ln(-1) + ln(-1)` is `2 pi i` | | `MathS.Equations("sqrt(y) - x", "y - a").Solve("x", "y")` | `[[sqrt(a), (sqrt(a)) ^ 2], [-sqrt(a), (-sqrt(a)) ^ 2]]` | `null`: the second row is a solution only at `a = 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 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