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