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
20 changes: 20 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,26 @@ A not-equal written `<>` is read as `not (a = b)`, and printed that way.

## Unreleased — since 2.6.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
`WrongNumberOfArgumentsException` for a system with more or fewer equations than unknowns, unless it was polynomial
over the rationals with rational solutions or had fewer equations and was linear with rational coefficients. Such a
system is now eliminated one unknown at a time, and an unknown that nothing is left to constrain becomes a parameter,
`t_1`, as in the linear case. Every row is substituted back into every equation and kept only where each simplifies to
zero. Where one does not, or where a branch cannot be decided, the system is declined as `null`, which `Solve`
documents as no solution found ([#212](https://github.com/asc-community/AngouriMath/issues/212)). Code that caught the
exception around `Solve` to detect a non-square system no longer sees it.

| Input | Was (2.6.0) | Now |
|---|---|---|
| `MathS.Equations("x^2 + y^2 - 1").Solve("x", "y")` | `WrongNumberOfArgumentsException` | `[[t_1, sqrt(-(t_1 ^ 2 + -1))], [t_1, -sqrt(-(t_1 ^ 2 + -1))]]` |
| `MathS.Equations("a*x + y - 1").Solve("x", "y")` | `WrongNumberOfArgumentsException` | `[[t_1, -(a * t_1 + -1)]]` |
| `MathS.Equations("sin(x)", "x").Solve("x")` | `WrongNumberOfArgumentsException` | `[0]` |
| `MathS.Equations("sin(x) - y", "cos(x) - y", "x - pi/4").Solve("x", "y")` | `WrongNumberOfArgumentsException` | `[[pi / 4, 1/2 * sqrt(2)]]` |
| `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` |

### `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
6 changes: 6 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,15 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
[AngouriMath/Functions/Continuous/Solvers/EquationSolver/LinearSystemSolver.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

[AngouriMath/Functions/Continuous/Solvers/EquationSolver/NonSquareSystemSolver.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

[AngouriMath/Functions/Continuous/Solvers/EquationSolver/ModulusSolver.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/NonSquareSystemTest.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/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

Expand Down
5 changes: 4 additions & 1 deletion Sources/AngouriMath/Core/EquationSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ private static Entity AsAnExpressionEqualToZero(Entity equation)
/// Solves a system of equations, if possible.
/// </summary>
/// <param name="vars">
/// The umber of variables must match number of equations
/// The unknowns, as many as there are equations or not. Where there are more, or where a
/// system leaves one free, it becomes a parameter in the answer: <c>x^2 + y^2 = 1</c> for
/// <c>x</c> and <c>y</c> is two rows, <c>x = t_1</c> with <c>y</c> the positive and the
/// negative square root of <c>1 - t_1^2</c>.
/// </param>
/// <return>
/// If there exists at least one solution, returns a solution matrix
Expand Down
6 changes: 3 additions & 3 deletions Sources/AngouriMath/Docs/Usage/Exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,9 @@ largest base there are digits for, and the base that was asked for.
### `WrongNumberOfArgumentsException`

An API call whose arity does not match: a compiled function called with the wrong number of values,
a truth table asked for over a different number of variables than the expression has, a system of
equations with more or fewer equations than unknowns. Where the expression is in scope the message
names it and its variables.
or a truth table asked for over a different number of variables than the expression has. Where the
expression is in scope the message names it and its variables. A system of equations with more or
fewer equations than unknowns is not one: it is solved, or declined as `null`.

Compare `FunctionArgumentCountException`, which is the same complaint about a function call written
inside a string. **Use `FunctionArgumentCountException` from the parser and this one from
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ internal static Set Solve(Entity equation, Variable x)
if (Groebner.GroebnerSystemSolver.TrySolve(equations, variables, out var triangularised))
return triangularised;

// Fewer equations than unknowns is not a mistake by the caller, which is what
// the exception below says it is. A linear system of that shape has infinitely
// Fewer equations than unknowns is not a mistake by the caller, which is what an
// exception here used to say it was. A linear system of that shape has infinitely
// many solutions and one family that describes them all, so it is answered as
// that family -- the unknowns a row reduction leaves free become parameters.
// Only the short count is taken here: a square system that is rank-deficient
Expand All @@ -139,8 +139,14 @@ internal static Set Solve(Entity equation, Variable x)
equations, variables, out var family))
return family;

// 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)
throw new WrongNumberOfArgumentsException("Number of equations must be equal to that of vars");
return NonSquareSystemSolver.Solve(equations, variables, ledger) is { } families
? new MatrixBuilder(families, vars.Length).ToMatrix()
: null;
int initVarCount = vars.Length;

var res = InSolveSystem(equations, vars, Sumf.Sum(equations), ledger);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
//
// 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.Core.Budgets;
using AngouriMath.Core.Exceptions;
using System;
using static AngouriMath.Entity;
using static AngouriMath.Entity.Number;
using static AngouriMath.Entity.Set;

namespace AngouriMath.Functions.Algebra.AnalyticalSolving
{
/// <summary>
/// A system with more or fewer equations than unknowns that neither the triangularisation nor
/// <see cref="LinearSystemSolver"/> takes, such as <c>x^2 + y^2 = 1</c> for <c>x</c> and
/// <c>y</c>, or <c>sin(x) = 0</c> and <c>x = 0</c> for <c>x</c>. Each unknown is eliminated in
/// turn, an unknown that nothing is left to constrain becomes a parameter, and every family
/// found is substituted back into every equation.
/// <a href="https://github.com/asc-community/AngouriMath/issues/212">#212</a>
/// </summary>
/// <remarks>
/// <para>
/// <b>An answer is given only where it is shown to hold.</b> The elimination trusts each solve
/// it makes, and a solve can answer for a case its equation does not cover: <c>sqrt(y) = x</c>
/// is solved for <c>y</c> as <c>x^2</c>, which is no solution where <c>x</c> is negative. So
/// each family is kept only where every equation, with the family substituted, simplifies to
/// <c>0</c>. <c>sqrt(t^2) - t</c> does not, and that system is declined.
/// </para>
/// <para>
/// <b>A decline declines the whole system.</b> An equation left with no unknown in it is zero,
/// a number away from zero, or neither as far as can be told, like <c>(a + b)/2 - c</c>. A
/// branch where one is away from zero has no solutions and is dropped. A branch where one
/// cannot be told gives no answer for the system, rather than leaving the other branches to
/// read as all of its solutions.
/// </para>
/// <para>
/// A square system does not come here. It is the eliminator's in
/// <see cref="EquationSolver.InSolveSystem"/>, whose answers this would change.
/// </para>
/// </remarks>
internal static class NonSquareSystemSolver
{
/// <summary>
/// The solutions of <paramref name="equations"/> in <paramref name="variables"/>, a list of
/// values in their order for each family, or <see langword="null"/> where none were found.
/// </summary>
internal static List<List<Entity>>? Solve(List<Entity> equations, Variable[] variables, BudgetLedger ledger)
{
// One parameter per unknown that is left free, the same in every branch. Each is
// minted against the equations and the parameters already minted, since CreateUnique
// compares whole names in use and would otherwise hand back the same name twice.
var parameters = new Variable?[variables.Length];
Entity nameSource = equations.Count > 0 ? Sumf.Sum(equations) : Integer.Zero;
Variable ParameterFor(int index)
{
if (parameters[index] is { } minted)
return minted;
var parameter = Variable.CreateUnique(nameSource, "t");
nameSource += parameter;
return parameters[index] = parameter;
}

if (Eliminate(equations, variables, variables.Length, ParameterFor, ledger) is not { Count: > 0 } families)
return null;
foreach (var family in families)
foreach (var equation in equations)
{
var substituted = equation;
for (var index = 0; index < variables.Length; index++)
substituted = substituted.Substitute(variables[index], family[index]);
if (!Vanishes(substituted))
return null;
}
return families;
}

/// <summary>
/// The solutions in the first <paramref name="count"/> of <paramref name="variables"/>, each
/// a list of their values in order. Empty where there are none, and
/// <see langword="null"/> where that could not be told.
/// </summary>
private static List<List<Entity>>? Eliminate(List<Entity> equations, Variable[] variables, int count,
Func<int, Variable> parameterFor, BudgetLedger ledger)
{
// Charged as InSolveSystem charges it: one unit for each candidate explored.
if (!ledger.Spend())
throw new NotSufficientlySupportedException(
"this system of equations is not solvable within the budget. Raise "
+ "MathS.Settings.Budget to allow more, or pass a cancellation token with "
+ "MathS.Multithreading.SetLocalCancellationToken");

// No unknown is left, so each equation holds as it stands or does not.
if (count == 0)
{
var told = true;
foreach (var equation in equations)
switch (Holds(equation))
{
case false:
return new();
case null:
told = false;
break;
}
return told ? new() { new() } : null;
}

// No equation is left, so every unknown left is free.
if (equations.Count == 0)
{
var free = new List<Entity>();
for (var index = 0; index < count; index++)
free.Add(parameterFor(index));
return new() { free };
}

var variable = variables[count - 1];
// The equation with the fewest occurrences of the unknown first, which is solved by
// inverting it and has the plainest roots: of `cos(x) = sin(x)` and `x = pi/4`, the
// first is solved as ln(sqrt(i))/i and the second as pi/4.
var candidates = Enumerable.Range(0, equations.Count)
.Where(i => equations[i].ContainsNode(variable))
.OrderBy(i => equations[i].Nodes.Count(node => node == variable))
.ThenBy(i => equations[i].Complexity)
.ToList();
foreach (var i in candidates)
{
// An empty set is passed over as InSolveSystem passes it over: substituting an
// earlier unknown can leave an occurrence that cancels, and then the solve has
// nothing to say about this one.
if (equations[i].SolveEquation(variable).InnerSimplified is not FiniteSet roots || roots.Count == 0)
continue;
var rest = new List<Entity>(equations);
rest.RemoveAt(i);
if (EachRoot(roots, rest, variables, count, parameterFor, ledger) is { } solutions)
return solutions;
// This equation's roots left a question that could not be told. Another
// equation's may not: `sin(x) = 0` gives x = 2 pi n_1, where `x = 0` gives 0.
}

if (candidates.Count > 0)
return null;
// Nothing constrains this unknown, so it is a parameter in every solution of the rest.
var others = Eliminate(equations, variables, count - 1, parameterFor, ledger);
others?.ForEach(family => family.Add(parameterFor(count - 1)));
return others;
}

/// <summary>
/// The solutions where the last of the unknowns is each of <paramref name="roots"/> in
/// turn, or <see langword="null"/> where that could not be told for one of them.
/// </summary>
private static List<List<Entity>>? EachRoot(FiniteSet roots, List<Entity> rest, Variable[] variables, int count,
Func<int, Variable> parameterFor, BudgetLedger ledger)
{
var variable = variables[count - 1];
var solutions = new List<List<Entity>>();
foreach (var root in roots)
{
var substituted = rest.Select(equation => equation.Substitute(variable, root)).ToList();
if (Eliminate(substituted, variables, count - 1, parameterFor, ledger) is not { } families)
return null;
foreach (var family in families)
{
var value = root;
for (var index = 0; index < count - 1; index++)
value = value.Substitute(variables[index], family[index]);
family.Add(value.InnerSimplified);
solutions.Add(family);
}
}
return solutions;
}

/// <summary>
/// Whether <paramref name="equation"/>, with no unknown left in it, holds:
/// <see langword="true"/> where it simplifies to zero, <see langword="false"/> where it is a
/// number away from zero, and <see langword="null"/> where neither can be told.
/// </summary>
private static bool? Holds(Entity equation)
{
if (Vanishes(equation))
return true;
if (equation.Vars.Any() || equation.Evaled is not Complex value || value.IsNaN)
return null;
// An exact number that is not zero is not zero. A decimal is taken as away from zero
// only well past the digits its evaluation may have lost.
if (value is Rational)
return false;
return ((Real)value.Abs()).EDecimal.ToDouble() > 1e-9 ? false : null;
}

/// <summary>
/// Whether <paramref name="expression"/> is zero for every value of its symbols: zero as it
/// is evaluated, or simplified to zero. A simplification to <c>0 provided C</c> counts only
/// where <c>C</c> holds with the symbols pinned, since <c>t - c (t/c)</c> simplifies to
/// <c>0 provided not c = 0</c> for a <c>c</c> that is zero without being written
/// <c>0</c>, such as <c>(a + 1)^2 - a^2 - 2a - 1</c>.
/// </summary>
private static bool Vanishes(Entity expression)
{
if (expression.InnerSimplified.Evaled is Complex { IsZero: true })
return true;
// Away from zero at a point is not zero everywhere, which settles most without
// simplifying.
if (Pinned(expression).Evaled is Complex { IsNaN: false } atAPoint
&& ((Real)atAPoint.Abs()).EDecimal.ToDouble() > 1e-9)
return false;
return expression.Simplify() switch
{
Providedf(var simplified, var condition) =>
simplified.Evaled is Complex { IsZero: true } && Pinned(condition).Evaled is Entity.Boolean { Value: true },
var simplified => simplified.Evaled is Complex { IsZero: true },
};
}

/// <summary><paramref name="expression"/> with each of its symbols at a fixed value.</summary>
private static Entity Pinned(Entity expression)
{
var values = new[] { 1.37, 0.61, 2.23, 1.91, 0.83, 1.13 };
var index = 0;
foreach (var symbol in expression.Vars.ToList())
expression = expression.Substitute(symbol, values[index++ % values.Length]);
return expression;
}
}
}
Loading
Loading