diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index aad6eccdf..74fe3f736 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 square system is not answered with rows that are no solution + +**Silent: a row that is no solution is gone, and a system with a row that fails for some values is `null`.** +`EquationSystem.Solve` with as many equations as unknowns returned every row the elimination found, without putting it +back into the system. Solving one equation can widen it: `sqrt(y) = x` is solved for `y` as `x^2`, which is no +solution where `x` is negative. Each row is now put back into the system as written. A row that leaves an equation +with no symbol in it a number away from zero is dropped, as a root of one equation is, and a system left with no row +is `null`. A row with symbols in it is tried at one value of them, and where it fails there the system is declined as +`null`, since the row may still hold for other values +([#2123](https://github.com/asc-community/AngouriMath/issues/2123)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `MathS.Equations("sqrt(y) - x", "x + 1").Solve("x", "y")` | `[[-1, 1]]` | `null`: `sqrt(1) - (-1)` is 2 | +| `MathS.Equations("sqrt(y) - x", "y - 4").Solve("x", "y")` | `[[2, 4], [-2, 4]]` | `[[2, 4]]` | +| `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 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 b1324ec83..d220dbbc3 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/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 + [Tests/UnitTests/Common/LambdaArrowSyntaxTest.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 902ebc064..168cd83f4 100644 --- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver.cs @@ -96,7 +96,8 @@ internal static Set Solve(Entity equation, Variable x) internal static Matrix? SolveSystem(IEnumerable inputEquations, ReadOnlySpan vars) { - var equations = new List(inputEquations.Select(equation => equation.InnerSimplified)); + var written = inputEquations.ToList(); + var equations = new List(written.Select(equation => equation.InnerSimplified)); // Triangularising first, where the system is polynomial over Q and the answer can // be checked exactly. Eliminating in radicals -- which is what InSolveSystem below @@ -155,7 +156,13 @@ internal static Set Solve(Entity equation, Variable x) throw new AngouriBugException("InSolveSystem incorrect output"); if (res.Count == 0) return null; - var tb = new MatrixBuilder(res, initVarCount); + // Each row is put back into the system as the caller wrote it, as a root of one + // equation is: `sqrt(y) = x` is solved for `y` as `x^2`, which is no solution + // where `x` is negative, and `sqrt(y) - x`, `x + 1` was answered [[-1, 1]]. + // https://github.com/asc-community/AngouriMath/issues/2123 + if (RowsThatHold(written, variables, res) is not { Count: > 0 } rows) + return null; + var tb = new MatrixBuilder(rows, initVarCount); return tb.ToMatrix(); } finally @@ -164,6 +171,72 @@ internal static Set Solve(Entity equation, Variable x) } } + /// + /// The rows of that satisfy each of , + /// or where a row is shown not to for some value of the symbols + /// left in it. + /// + /// + /// + /// 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. + /// + /// + /// A row with symbols left in it is tried at one value of them, and one that fails there + /// declines the system instead of being dropped. It may hold for other values, as + /// sqrt(t^2) = t does for every positive t, and the rows left would read as + /// all of the solutions. A symbol of the system's own is tried at a positive value that + /// is not whole, and a parameter that a row brings with it, a t_1 or an + /// n_1, at a whole one, since an n_1 stands for those only. + /// + /// + /// Anything else is kept, as a root of one equation is: a residual that cannot be + /// evaluated, or one too small next to its terms to be told from rounding. + /// + /// + private static List>? RowsThatHold(List equations, Variable[] variables, List> rows) + { + var own = new HashSet(equations.SelectMany(equation => equation.Vars)); + var replacements = new Dictionary(); + var kept = new List>(rows.Count); + foreach (var row in rows) + { + replacements.Clear(); + 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))) + continue; + if (residuals.Any(residual => residual.Vars.Any() && StatementSolver.IsEvidenceAgainst(AtASamplePoint(residual, own)))) + return null; + kept.Add(row); + } + return kept; + } + + /// + /// with each symbol of at a positive + /// value that is not whole, and every other symbol at 2. + /// + private static Entity AtASamplePoint(Entity residual, HashSet own) + { + var values = new[] { (137, 100), (61, 100), (223, 100), (191, 100), (83, 100), (113, 100) }; + var index = 0; + foreach (var symbol in residual.Vars.ToList()) + { + Entity value = Integer.Create(2); + if (own.Contains(symbol)) + { + var (numerator, denominator) = values[index++ % values.Length]; + value = Rational.Create(numerator, denominator); + } + residual = residual.Substitute(symbol, value); + } + return residual; + } + /// Solves system of equations /// of /// diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/SolveStatement.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/SolveStatement.cs index abb8f9192..ac48d0af9 100644 --- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/SolveStatement.cs +++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/SolveStatement.cs @@ -123,7 +123,17 @@ private static bool IsSpurious(Entity equation, Variable x, Entity root) return true; if (root.Vars.Any()) return false; - var substituted = equation.Substitute(x, root); + return IsEvidenceAgainst(equation.Substitute(x, root)); + } + + /// + /// Whether , an equation with what was found for its + /// unknowns put in and no symbol left, shows that what was put in is not a solution: + /// it evaluates to a number, and one that is large next to the terms it is the sum of. + /// See for why the size alone is not enough. + /// + internal static bool IsEvidenceAgainst(Entity substituted) + { if (substituted.Evaled is not Number.Complex residual || !residual.IsFinite) return false; var size = residual.Abs().EDecimal; diff --git a/Sources/Tests/UnitTests/Algebra/SolveTest/SpuriousSystemRowTest.cs b/Sources/Tests/UnitTests/Algebra/SolveTest/SpuriousSystemRowTest.cs new file mode 100644 index 000000000..c9805da31 --- /dev/null +++ b/Sources/Tests/UnitTests/Algebra/SolveTest/SpuriousSystemRowTest.cs @@ -0,0 +1,92 @@ +// +// 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 +{ + /// + /// Square systems where solving one equation widens it, so the elimination finds rows that + /// are no solution: sqrt(y) = x is solved for y as x^2, which holds only + /// where x is not negative. Each was answered with such a row, and the rows are now + /// put back into the system. + /// #2123 + /// + [Trait("Area", "Algebra")] + public sealed class SpuriousSystemRowTest + { + private static Entity.Matrix? Solve(params string[] equations) + => MathS.Equations(equations.Select(e => e.ToEntity()).ToArray()).Solve("x", "y"); + + /// + /// A row that fails an equation with no symbol left in it is no solution for any value + /// of anything, and is dropped. [-2, 4] leaves sqrt(4) - (-2) = 4, and + /// [-1, -1] leaves ln(-1) + ln(-1) = 2 pi i. + /// + [Theory] + [InlineData("sqrt(y) - x", "y - 4", "2", "4")] + [InlineData("ln(x) + ln(y)", "x - y", "1", "1")] + public void ARowThatIsNoSolutionIsDropped(string first, string second, string x, string y) + { + var solutions = Solve(first, second); + Assert.NotNull(solutions); + Assert.Equal(1, solutions!.RowCount); + Assert.Equal(Entity.Number.Integer.Create(0), (solutions[0, 0] - x).Simplify()); + Assert.Equal(Entity.Number.Integer.Create(0), (solutions[0, 1] - y).Simplify()); + } + + /// + /// [-1, 1] was the only row, and sqrt(1) - (-1) is 2: the system has no + /// solution, which answers as . + /// + [Fact] + public void ASystemWhoseOnlyRowIsNoSolutionHasNone() + => Assert.Null(Solve("sqrt(y) - x", "x + 1")); + + /// + /// [-sqrt(a), a] fails at a = 137/100 and holds at a = 0, so it + /// cannot be dropped as no solution, and the system is declined instead of being + /// answered by the other row alone. A row like it that holds for some values only, as + /// sqrt(t^2) = t does for positive t, would be lost by dropping it. + /// + [Fact] + public void ARowThatFailsForSomeValueDeclinesTheSystem() + => Assert.Null(Solve("sqrt(y) - x", "y - a")); + + /// + /// What was answered rightly still is: rows in a symbol of the system's own, and rows in + /// an integer parameter, which are tried with it at a whole number and not at the value + /// the system's own symbols are tried at. Each row is checked here at several values of + /// both, negative ones among them. + /// + [Theory] + [InlineData("x * y - a", "x - y", 2)] + [InlineData("x ^ 2 - a", "y - x", 2)] + [InlineData("sin(x) - y", "y - 1/2", 2)] + [InlineData("tan(x) - y", "y - 1", 1)] + [InlineData("sqrt(x) + y", "x - 1", 1)] + public void ARowThatHoldsIsKept(string first, string second, int rows) + { + var solutions = Solve(first, second); + Assert.NotNull(solutions); + Assert.Equal(rows, solutions!.RowCount); + foreach (var at in new[] { -1, 0, 2 }) + for (var row = 0; row < solutions.RowCount; row++) + foreach (var equation in new[] { first, second }) + { + var residual = equation.ToEntity().Substitute("x", solutions[row, 0]).Substitute("y", solutions[row, 1]); + foreach (var symbol in residual.Vars.ToList()) + residual = residual.Substitute(symbol, symbol.Name == "a" ? 1.7 + at : at); + var value = Assert.IsAssignableFrom(residual.Evaled); + Assert.True(((Entity.Number.Real)value.Abs()).EDecimal.ToDouble() < 1e-9, $"{equation} at row {row}, {at}: {value}"); + } + } + } +}