Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,57 @@ internal static Set Solve(Entity equation, Variable x)
return null;
kept.Add(row);
}
return kept;
return WithoutDecimalCopies(kept);
}

/// <summary>
/// <paramref name="rows"/> 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.
/// </summary>
/// <remarks>
/// <para>
/// Two branches of an elimination can reach one solution, and both were answered: the
/// circles <c>(x - 3/2)^2 + (y - sqrt(3)/2)^2 = 1</c> and <c>(x - 1)^2 + y^2 = 1</c> met at
/// <c>(1/2, sqrt(3)/2)</c> twice. Where one branch went through Newton's method the copy
/// is a decimal: <c>(x - 1/2)^2 + (y - sqrt(3)/2)^2 = 1</c> with the same second circle
/// met at <c>(3/2, sqrt(3)/2)</c> and again where <c>x</c> is <c>1.4999999999999997</c>.
/// </para>
/// <para>
/// 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 <c>y</c> is out by about
/// <c>2e-8</c>. 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
/// </para>
/// </remarks>
private static List<List<Entity>> WithoutDecimalCopies(List<List<Entity>> rows)
{
static bool HasADecimal(List<Entity> 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<Entity> row, List<Entity> 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<List<Entity>>(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();
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// <a href="https://github.com/asc-community/AngouriMath/issues/2138">#2138</a>
/// </summary>
[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 });
}
}
}
Loading