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 system with a product equation is solved branch by branch

**Silent: solutions that were missing are answered.** `EquationSystem.Solve` eliminated `x * y = 0` for `y` as `0`,
which is the answer only where `x` is not zero, and lost the solutions where it is, in a system the triangularisation
does not take. A system with a product in more than one of its unknowns is now solved once for each factor, and its
solutions are theirs together, each put back into the system as written. A row is dropped where an equation, with the
row put in, is not a finite number, as `x ln(y)` is at `(0, 0)`. A square system with an unknown that no equation
mentions answers it as a parameter, as a short system does, where it was `null`
([#2128](https://github.com/asc-community/AngouriMath/issues/2128)).

| Input | Was (2.6.0) | Now |
|---|---|---|
| `MathS.Equations("x*y", "sin(x) + y - 1").Solve("x", "y")` | `[[pi / 2 + 2 * pi * n_1, 0], [pi - pi / 2 + 2 * pi * n_1, 0]]` | `[[0, 1], [pi / 2 + 2 * pi * n_1, 0], [pi - pi / 2 + 2 * pi * n_1, 0]]` |
| `MathS.Equations("(x - 1)*(y - 2)", "e^x - y").Solve("x", "y")` | `[[ln(2), 2]]` | `[[1, e], [ln(2), 2]]` |
| `MathS.Equations("x*e^y", "x^2*y").Solve("x", "y")` | `[[0, 0]]` | `[[0, t_1], [0, 0]]` |
| `MathS.Equations("x - 1", "x^2 - 1").Solve("x", "y")` | `null` | `[[1, t_1]]` |
| `MathS.Equations("a - 1", "b * 0").Solve("a", "b")` | `null` | `[[1, t_1]]` |

### A polynomial system is not answered with some of its solutions missing

**Silent: a polynomial system that lost solutions has all of them.** `EquationSystem.Solve` triangularises a
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/ProductSystemTest.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

Expand Down
89 changes: 85 additions & 4 deletions Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,21 @@ internal static Set Solve(Entity equation, Variable x)
equations, variables, out var family))
return family;

// A product is zero where any one of its factors is, and the elimination solves
// `x * y = 0` for `y` as `0`, which is the answer only where `x` is not zero. So a
// system with a product in more than one of its unknowns is solved once for each
// factor, and its solutions are theirs together.
// https://github.com/asc-community/AngouriMath/issues/2128
if (Branches(equations, variables) is { } branches)
return Together(branches, written, vars, variables);

// Nor is any other count: `x^2 + y^2 = 1` has a family of solutions in a
// parameter, and `sin(x) = 0` with `x = 0` has one solution. Those are eliminated,
// and a system whose answer cannot be shown to hold is declined, as `Solve`
// documents. https://github.com/asc-community/AngouriMath/issues/212
if (equations.Count != vars.Length)
// An unknown that no equation mentions is free, as one is in a short system, and
// the eliminator has no equation to take it from.
if (equations.Count != vars.Length || variables.Any(variable => !equations.Any(equation => equation.ContainsNode(variable))))
return NonSquareSystemSolver.Solve(equations, variables, ledger) is { } families
? new MatrixBuilder(families, vars.Length).ToMatrix()
: null;
Expand All @@ -171,6 +181,75 @@ internal static Set Solve(Entity equation, Variable x)
}
}

/// <summary>The most systems a system is split into by its products.</summary>
private const int MostBranches = 64;

/// <summary>
/// One system for each way of choosing a factor of each product in more than one of
/// <paramref name="variables"/>, with the product replaced by the factor, or
/// <see langword="null"/> where there is no such product or the choices are too many.
/// </summary>
/// <remarks>
/// A product whose factors all mention the same unknowns is left alone: solving it for
/// any of them reads every factor, as <c>x (x - 1)</c> is solved for <c>x</c> as
/// <c>0</c> and <c>1</c>.
/// </remarks>
private static List<List<Entity>>? Branches(List<Entity> equations, Variable[] variables)
{
var branches = new List<List<Entity>> { new(equations) };
var split = false;
for (var i = 0; i < equations.Count; i++)
{
if (equations[i] is not Mulf)
continue;
var factors = Mulf.LinearChildren(equations[i]).Where(factor => variables.Any(factor.ContainsNode)).ToList();
if (factors.Count < 2)
continue;
var first = variables.Where(factors[0].ContainsNode).ToList();
if (factors.All(factor => variables.Where(factor.ContainsNode).SequenceEqual(first)))
continue;
if (branches.Count * factors.Count > MostBranches)
return null;
split = true;
branches = branches.SelectMany(branch => factors.Select(factor =>
{
var chosen = new List<Entity>(branch);
chosen[i] = factor;
return chosen;
})).ToList();
}
return split ? branches : null;
}

/// <summary>
/// The solutions of each of <paramref name="branches"/> together, each kept only where
/// it satisfies the system as written: a factor can be zero where another is not
/// defined, as <c>x ln(y)</c> is at <c>x = 0</c> and <c>y = 0</c>.
/// </summary>
/// <remarks>
/// A branch answered by <see langword="null"/> is taken to have no solutions, which is
/// what <see cref="EquationSystem.Solve"/> says of it: <c>x e^y = 0</c> has none where
/// <c>e^y</c> is zero.
/// </remarks>
private static Matrix? Together(List<List<Entity>> branches, List<Entity> written, ReadOnlySpan<Variable> vars, Variable[] variables)
{
var rows = new List<List<Entity>>();
foreach (var branch in branches)
{
if (SolveSystem(branch, vars) is not { } solved)
continue;
for (var row = 0; row < solved.RowCount; row++)
{
var values = Enumerable.Range(0, solved.ColumnCount).Select(column => solved[row, column]).ToList();
if (!rows.Any(other => other.SequenceEqual(values)))
rows.Add(values);
}
}
if (rows.Count == 0 || RowsThatHold(written, variables, rows) is not { Count: > 0 } kept)
return null;
return new MatrixBuilder(kept, vars.Length).ToMatrix();
}

/// <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
Expand All @@ -180,8 +259,9 @@ internal static Set Solve(Entity equation, Variable x)
/// <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.
/// in, has no symbol left and is a number away from zero, or is no finite number at all, as
/// <c>x ln(y)</c> is at <c>(0, 0)</c>. 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
Expand All @@ -207,7 +287,8 @@ internal static Set Solve(Entity equation, Variable x)
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)))
if (residuals.Any(residual => !residual.Vars.Any()
&& (residual.Evaled is Complex { IsFinite: false } || StatementSolver.IsEvidenceAgainst(residual))))
continue;
if (residuals.Any(residual => residual.Vars.Any() && StatementSolver.IsEvidenceAgainst(AtASamplePoint(residual, own))))
return null;
Expand Down
113 changes: 113 additions & 0 deletions Sources/Tests/UnitTests/Algebra/SolveTest/ProductSystemTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//
// 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>
/// Systems with an equation that is a product in more than one unknown, which the
/// elimination solved for one unknown as if the other factors were not zero: <c>x y = 0</c>
/// for <c>y</c> is <c>0</c>, and the branch <c>x = 0</c> was lost. Each is now solved once
/// for each factor.
/// <a href="https://github.com/asc-community/AngouriMath/issues/2128">#2128</a>
/// </summary>
[Trait("Area", "Algebra")]
public sealed class ProductSystemTest
{
private static Entity.Matrix? Solve(params string[] equations)
=> MathS.Equations(equations.Select(e => e.ToEntity()).ToArray()).Solve("x", "y");

/// <summary>
/// Each row satisfies each equation, at several values of what is left in it: an
/// integer parameter at whole numbers, anything else at values of both signs.
/// </summary>
private static void EveryRowHolds(Entity.Matrix solutions, params string[] equations)
{
for (var row = 0; row < solutions.RowCount; row++)
foreach (var equation in equations)
{
var residual = equation.ToEntity().Substitute("x", solutions[row, 0]).Substitute("y", solutions[row, 1]);
foreach (var at in new[] { -1, 0, 2 })
{
var pinned = residual;
foreach (var symbol in residual.Vars.ToList())
pinned = pinned.Substitute(symbol, symbol.Name.StartsWith("n_") ? at : at + 0.3);
var value = Assert.IsAssignableFrom<Entity.Number.Complex>(pinned.Evaled);
Assert.True(((Entity.Number.Real)value.Abs()).EDecimal.ToDouble() < 1e-9, $"{equation} at row {row}, {at}: {value}");
}
}
}

private static bool HasRow(Entity.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);

/// <summary>
/// The branch the elimination lost: <c>(0, 1)</c> for the first two, where <c>x</c> is
/// zero, and <c>(1, e)</c> for the third, where <c>x - 1</c> is.
/// </summary>
[Theory]
[InlineData("x * y", "sin(x) + y - 1", 3, "0", "1")]
[InlineData("x * y", "y - cos(x)", 3, "0", "1")]
[InlineData("(x - 1) * (y - 2)", "e^x - y", 2, "1", "e")]
[InlineData("(x - 1) * (y - 2)", "e^x - y", 2, "ln(2)", "2")]
public void EachFactorIsABranch(string first, string second, int rows, string x, string y)
{
var solutions = Solve(first, second);
Assert.NotNull(solutions);
Assert.Equal(rows, solutions!.RowCount);
Assert.True(HasRow(solutions, x, y), $"({x}, {y}) is not among {solutions}");
EveryRowHolds(solutions, first, second);
}

/// <summary>
/// <c>x e^y = 0</c> and <c>x^2 y = 0</c> hold wherever <c>x</c> is zero, which was
/// answered as the one point <c>(0, 0)</c>.
/// </summary>
[Fact]
public void ABranchThatLeavesAnUnknownFreeIsAFamily()
{
var solutions = Solve("x * e^y", "x^2 * y");
Assert.NotNull(solutions);
Assert.Contains(Enumerable.Range(0, solutions!.RowCount),
row => solutions[row, 0] == 0 && solutions[row, 1] is Entity.Variable);
EveryRowHolds(solutions, "x * e^y", "x^2 * y");
}

/// <summary>
/// The branch <c>x = 0</c> of <c>x ln(y) = 0</c> with <c>x = y</c> is <c>(0, 0)</c>,
/// where <c>ln(0)</c> is not a number and the product is not zero.
/// </summary>
[Fact]
public void AFactorZeroWhereAnotherIsUndefinedIsNoSolution()
{
var solutions = Solve("x * ln(y)", "x - y");
Assert.NotNull(solutions);
Assert.Equal(1, solutions!.RowCount);
Assert.True(HasRow(solutions, "1", "1"));
}

/// <summary>
/// A branch can leave an unknown that no equation mentions, and so can a caller. It is
/// a parameter, as in a system with fewer equations than unknowns; the elimination had
/// no equation to take it from and answered nothing.
/// </summary>
[Fact]
public void AnUnknownNoEquationMentionsIsAParameter()
{
var solutions = Solve("x - 1", "x^2 - 1");
Assert.NotNull(solutions);
Assert.Equal(1, solutions!.RowCount);
Assert.Equal(Entity.Number.Integer.Create(1), solutions[0, 0]);
Assert.IsType<Entity.Variable>(solutions[0, 1]);
}
}
}
6 changes: 5 additions & 1 deletion Sources/Tests/UnitTests/Algebra/SolveTest/SolveSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,15 @@ public void TestTrigSystem3() => AssertSystemSolvable(new Entity[] {
"3a+4b-4c"
}, new Entity.Variable[] { "a", "b", "c" }, 1);

// b * 0 is zero for every b, so b is free and the solutions are (1, t) for every t. This
// was answered null, because no equation was left to eliminate b from; an unknown that
// no equation mentions is a parameter now, as it is in a short system.
// https://github.com/asc-community/AngouriMath/issues/2128
[Fact]
public void TestTrigSystem4() => AssertSystemSolvable(new Entity[] {
"a - 1",
"b * 0"
}, new Entity.Variable[] { "a", "b" }, 0);
}, new Entity.Variable[] { "a", "b" }, 1);

// https://www.youtube.com/watch?v=dVs26SSUJSA
[Fact]
Expand Down
Loading