From 186f1e33935fe9dd7aa5d78d0f23047404c58968 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 27 Oct 2026 03:09:02 +0000 Subject: [PATCH] A system is not answered with a copy of one of its rows Two branches of an elimination can reach one solution, and each was answered. The circles (x - 3/2)^2 + (y - sqrt(3)/2)^2 = 1 and (x - 1)^2 + y^2 = 1 meet at (1/2, sqrt(3)/2) and (2, 0), and came back with the first of those twice. Where the second branch went through Newton's method the copy was a decimal: with the first circle centred at (1/2, sqrt(3)/2) instead, the rows were (0, 0), (3/2, sqrt(3)/2) and a third whose x was 1.4999999999999997. RowsThatHold now drops a row equal to an earlier one, and a row with a decimal in it whose entries are each within a part in a million of an exact row's. The tolerance is not the double's own precision because a root taken of the decimal multiplies its error: that copy's y was out by about 2e-8. A decimal row that no exact row agrees with is kept, as the search's answer, as a root of one equation is. syscheck, 1190 generated systems, is unchanged: 0 wrong rows and the same 13 known solutions missing, on this branch and on its base. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 14 +++++ Sources/.editorconfig | 3 ++ .../Continuous/Solvers/EquationSolver.cs | 52 ++++++++++++++++++- .../SolveTest/DuplicateSystemRowTest.cs | 50 ++++++++++++++++++ 4 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 Sources/Tests/UnitTests/Algebra/SolveTest/DuplicateSystemRowTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 5d84402f0..35e7ee831 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -130,6 +130,20 @@ exception around `Solve` to detect a non-square system no longer sees it. | `MathS.Equations("sqrt(y) - x").Solve("x", "y")` | `WrongNumberOfArgumentsException` | `null`: `y = x^2` is no solution where `x` is negative | | `MathS.Equations("x + y - a", "x - y - b", "x - c").Solve("x", "y")` | `WrongNumberOfArgumentsException` | `null`: there are solutions only where `a + b = 2c` | +### A system is not answered with a copy of one of its rows + +**Silent: a repeated row goes.** Two branches of an elimination can reach one solution, and each was answered: two +circles that meet at two points came back with three rows, one of them another again. Where the second branch went +through Newton's method the copy was a decimal, `x = 1.4999999999999997` beside the exact `x = 3/2`. A row equal to an +earlier one is dropped, and so is a row with a decimal in it whose entries are each within a part in a million of an +exact row's. A decimal row that no exact row agrees with is kept, as the search's answer +([#2138](https://github.com/asc-community/AngouriMath/issues/2138)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `MathS.Equations("(x-1/2)^2+(y-sqrt(3)/2)^2-1", "(x-1)^2+y^2-1").Solve("x", "y")` | `[[0, 0], [1.4999999999999997779553950749686919152736663818359375, …], [3/2, -(-2) * sqrt(3) * 1/2 / 2]]` | `[[0, 0], [3/2, -(-2) * sqrt(3) * 1/2 / 2]]` | +| `MathS.Equations("(x-3/2)^2+(y-sqrt(3)/2)^2-1", "(x-1)^2+y^2-1").Solve("x", "y")` | `[[1/2, -(-2) * sqrt(3) * 1/2 / 2], [2, 0], [1/2, -(-2) * sqrt(3) * 1/2 / 2]]` | `[[1/2, -(-2) * sqrt(3) * 1/2 / 2], [2, 0]]` | + ### `am_classify` is a preliminary tool, served by `amcli mcp --preliminary` **Not silent: what answered is a failure naming the flag.** `am_classify` guesses which field a formula comes from by diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 1e64fda62..090d52ebb 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -276,6 +276,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Algebra/SolveTest/NegativePowerInversionTest.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/DuplicateSystemRowTest.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 dc63d9658..3d5faf5b9 100644 --- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver.cs @@ -307,7 +307,57 @@ internal static Set Solve(Entity equation, Variable x) return null; kept.Add(row); } - return kept; + return WithoutDecimalCopies(kept); + } + + /// + /// without a row that is a copy of another: one equal to an + /// earlier row, and one with a number in it that is not exact, each of whose entries is + /// within a part in a million of an exact row's. + /// + /// + /// + /// Two branches of an elimination can reach one solution, and both were answered: the + /// circles (x - 3/2)^2 + (y - sqrt(3)/2)^2 = 1 and (x - 1)^2 + y^2 = 1 met at + /// (1/2, sqrt(3)/2) twice. Where one branch went through Newton's method the copy + /// is a decimal: (x - 1/2)^2 + (y - sqrt(3)/2)^2 = 1 with the same second circle + /// met at (3/2, sqrt(3)/2) and again where x is 1.4999999999999997. + /// + /// + /// The tolerance is a part in a million, not the double's own precision: a root taken + /// of the decimal multiplies its error, and that copy's y is out by about + /// 2e-8. A decimal row that no exact row agrees with is kept: it is the answer the + /// search found, as a root of one equation is. + /// https://github.com/asc-community/AngouriMath/issues/2138 + /// + /// + private static List> WithoutDecimalCopies(List> rows) + { + static bool HasADecimal(List row) + => row.Any(entry => entry.Nodes.Any(node => node is Number { IsExact: false })); + static Complex? ValueOf(Entity entry) + => entry.Vars.Any() ? null : entry.Evaled as Complex; + static bool Agree(List row, List exact) + { + for (var index = 0; index < row.Count; index++) + { + if (ValueOf(row[index]) is not { } value || ValueOf(exact[index]) is not { } other) + return false; + var difference = ((Complex)(value - other)).Abs().EDecimal; + var scale = PeterO.Numbers.EDecimal.Max(PeterO.Numbers.EDecimal.One, other.Abs().EDecimal); + if (difference.CompareTo(scale.Multiply(PeterO.Numbers.EDecimal.FromString("1e-6"))) > 0) + return false; + } + return true; + } + var distinct = new List>(rows.Count); + foreach (var row in rows) + if (!distinct.Any(earlier => earlier.SequenceEqual(row))) + distinct.Add(row); + var exactRows = distinct.Where(row => !HasADecimal(row)).ToList(); + if (exactRows.Count == distinct.Count) + return distinct; + return distinct.Where(row => !HasADecimal(row) || !exactRows.Any(exact => Agree(row, exact))).ToList(); } /// diff --git a/Sources/Tests/UnitTests/Algebra/SolveTest/DuplicateSystemRowTest.cs b/Sources/Tests/UnitTests/Algebra/SolveTest/DuplicateSystemRowTest.cs new file mode 100644 index 000000000..f86d687cb --- /dev/null +++ b/Sources/Tests/UnitTests/Algebra/SolveTest/DuplicateSystemRowTest.cs @@ -0,0 +1,50 @@ +// +// 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; +using static AngouriMath.Entity; + +namespace AngouriMath.Tests.Algebra.SolveTest +{ + /// + /// Two branches of an elimination can reach one solution, and each was answered: two circles + /// that meet at two points gave three rows, one of them the other again, and where the + /// second branch went through Newton's method, again as a decimal. + /// #2138 + /// + [Trait("Area", "Algebra")] + public sealed class DuplicateSystemRowTest + { + private static Matrix Solve(string first, string second) + { + var solved = MathS.Equations(first.ToEntity(), second.ToEntity()).Solve("x", "y"); + Assert.NotNull(solved); + return solved!; + } + + private static bool HasRow(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); + + [Theory] + // the copy was a decimal: x = 1.4999999999999997 + [InlineData("(x - 1/2)^2 + (y - sqrt(3)/2)^2 - 1", "0", "0", "3/2", "sqrt(3)/2")] + // the copy was the same row again + [InlineData("(x - 3/2)^2 + (y - sqrt(3)/2)^2 - 1", "1/2", "sqrt(3)/2", "2", "0")] + public void TwoCirclesMeetingTwiceGiveTwoExactRows(string circle, string x1, string y1, string x2, string y2) + { + var solutions = Solve(circle, "(x - 1)^2 + y^2 - 1"); + Assert.Equal(2, solutions.RowCount); + Assert.True(HasRow(solutions, x1, y1), $"({x1}, {y1}) is not among {solutions}"); + Assert.True(HasRow(solutions, x2, y2), $"({x2}, {y2}) is not among {solutions}"); + Assert.DoesNotContain(solutions.Nodes, node => node is Number { IsExact: false }); + } + } +}