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
18 changes: 18 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
77 changes: 75 additions & 2 deletions Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ internal static Set Solve(Entity equation, Variable x)

internal static Matrix? SolveSystem(IEnumerable<Entity> inputEquations, ReadOnlySpan<Variable> vars)
{
var equations = new List<Entity>(inputEquations.Select(equation => equation.InnerSimplified));
var written = inputEquations.ToList();
var equations = new List<Entity>(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
Expand Down Expand Up @@ -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
Expand All @@ -164,6 +171,72 @@ internal static Set Solve(Entity equation, Variable x)
}
}

/// <summary>
/// The rows of <paramref name="rows"/> that satisfy each of <paramref name="equations"/>,
/// or <see langword="null"/> where a row is shown not to for some value of the symbols
/// left in it.
/// </summary>
/// <remarks>
/// <para>
/// A row is dropped on the evidence a root of one equation is dropped on,
/// <see cref="StatementSolver.IsEvidenceAgainst"/>: 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.
/// </para>
/// <para>
/// 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
/// <c>sqrt(t^2) = t</c> does for every positive <c>t</c>, 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 <c>t_1</c> or an
/// <c>n_1</c>, at a whole one, since an <c>n_1</c> stands for those only.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
private static List<List<Entity>>? RowsThatHold(List<Entity> equations, Variable[] variables, List<List<Entity>> rows)
{
var own = new HashSet<Variable>(equations.SelectMany(equation => equation.Vars));
var replacements = new Dictionary<Variable, Entity>();
var kept = new List<List<Entity>>(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;
}

/// <summary>
/// <paramref name="residual"/> with each symbol of <paramref name="own"/> at a positive
/// value that is not whole, and every other symbol at 2.
/// </summary>
private static Entity AtASamplePoint(Entity residual, HashSet<Variable> 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;
}

/// <summary>Solves system of equations</summary>
/// <param name="equations"><see cref="List{T}"/> of <see cref="Entity"/></param>
/// <param name="vars">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

/// <summary>
/// Whether <paramref name="substituted"/>, 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 <see cref="RelativeResidualTolerance"/> for why the size alone is not enough.
/// </summary>
internal static bool IsEvidenceAgainst(Entity substituted)
{
if (substituted.Evaled is not Number.Complex residual || !residual.IsFinite)
return false;
var size = residual.Abs().EDecimal;
Expand Down
92 changes: 92 additions & 0 deletions Sources/Tests/UnitTests/Algebra/SolveTest/SpuriousSystemRowTest.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Square systems where solving one equation widens it, so the elimination finds rows that
/// are no solution: <c>sqrt(y) = x</c> is solved for <c>y</c> as <c>x^2</c>, which holds only
/// where <c>x</c> is not negative. Each was answered with such a row, and the rows are now
/// put back into the system.
/// <a href="https://github.com/asc-community/AngouriMath/issues/2123">#2123</a>
/// </summary>
[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");

/// <summary>
/// A row that fails an equation with no symbol left in it is no solution for any value
/// of anything, and is dropped. <c>[-2, 4]</c> leaves <c>sqrt(4) - (-2) = 4</c>, and
/// <c>[-1, -1]</c> leaves <c>ln(-1) + ln(-1) = 2 pi i</c>.
/// </summary>
[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());
}

/// <summary>
/// <c>[-1, 1]</c> was the only row, and <c>sqrt(1) - (-1)</c> is 2: the system has no
/// solution, which <see cref="EquationSystem.Solve"/> answers as <see langword="null"/>.
/// </summary>
[Fact]
public void ASystemWhoseOnlyRowIsNoSolutionHasNone()
=> Assert.Null(Solve("sqrt(y) - x", "x + 1"));

/// <summary>
/// <c>[-sqrt(a), a]</c> fails at <c>a = 137/100</c> and holds at <c>a = 0</c>, 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
/// <c>sqrt(t^2) = t</c> does for positive <c>t</c>, would be lost by dropping it.
/// </summary>
[Fact]
public void ARowThatFailsForSomeValueDeclinesTheSystem()
=> Assert.Null(Solve("sqrt(y) - x", "y - a"));

/// <summary>
/// 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.
/// </summary>
[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<Entity.Number.Complex>(residual.Evaled);
Assert.True(((Entity.Number.Real)value.Abs()).EDecimal.ToDouble() < 1e-9, $"{equation} at row {row}, {at}: {value}");
}
}
}
}
Loading