diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index d50c7c089..9f1a36a05 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -40,6 +40,26 @@ A not-equal written `<>` is read as `not (a = b)`, and printed that way. --- +## Unreleased — since 2.6.0 + +### An equality in an equation or a system is read as a difference equal to zero + +**Silent: wrong answers.** `MathS.SolveEquation`, `Entity.SolveEquation` and `MathS.Equations` read each equation as an +expression equal to zero, and passed an equality `a = b` on as it stood. One equation came back as the set of x where +`a = b` and `b = 0`, which is empty unless `b` is zero, and a system came back with no solution. An equality is now read +as `a - b = 0`, as a statement's own solve reads it: `"x + 1 = 3".Solve("x")` was already `{ 2 }` +([#1673](https://github.com/asc-community/AngouriMath/issues/1673)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `MathS.SolveEquation("x + 1 = 3", "x")` | `{ x : x + 1 = 3 and 3 = 0 }` | `{ 2 }` | +| `"x^2 = 4".SolveEquation("x")` | `{ x : x ^ 2 = 4 and 4 = 0 }` | `{ 2, -2 }` | +| `"a * x = b".ToEntity().SolveEquation("x")` | `{ x : a * x = b and b = 0 }`, which holds only where `b = 0` | `{ b / a }` | +| `MathS.Equations("x + y = 3", "x - y = 1").Solve("x", "y")` | `null` | `[[2, 1]]` | +| `MathS.Equations("x + y = 3", "x - y = 1").ToString()` | `x + y = 3 = 0` and `x - y = 1 = 0` | `x + y - 3 = 0` and `x - y - 1 = 0` | + +--- + ## 2.6.0 — since 2.5.0 ### A high half-odd power of a quadratic beside a power of another is reduced in exact rationals diff --git a/Sources/AngouriMath/Convenience/MathS.cs b/Sources/AngouriMath/Convenience/MathS.cs index 02812987f..1e60171c1 100644 --- a/Sources/AngouriMath/Convenience/MathS.cs +++ b/Sources/AngouriMath/Convenience/MathS.cs @@ -267,7 +267,7 @@ public static Integer GreatestCommonDivisor(Integer a, Integer b) public static EquationSystem Equations(IEnumerable equations) => new EquationSystem(equations); /// Solves one equation over one variable - /// An equation that is assumed to equal 0 + /// An expression that is assumed to equal 0, or an equality a = b, read as a - b = 0 /// Variable whose values we are looking for /// A of possible values or intervals of values /// diff --git a/Sources/AngouriMath/Core/EquationSystem.cs b/Sources/AngouriMath/Core/EquationSystem.cs index 224907e22..65175390e 100644 --- a/Sources/AngouriMath/Core/EquationSystem.cs +++ b/Sources/AngouriMath/Core/EquationSystem.cs @@ -22,7 +22,8 @@ public sealed class EquationSystem : ILatexizeable private readonly IEnumerable equations; /// - /// The equations you pass should not have an node. + /// Each equation is an expression equal to zero, or an equality a = b, which is read as + /// a - b = 0, as reads one in a single variable. /// After having created a system of equations, you may solve or latexize it. /// /// @@ -36,10 +37,11 @@ public sealed class EquationSystem : ILatexizeable /// }); /// /// - public EquationSystem(IEnumerable equations) => this.equations = equations; + public EquationSystem(IEnumerable equations) => this.equations = equations.Select(AsAnExpressionEqualToZero).ToList(); /// - /// The equations you pass should not have an node. + /// Each equation is an expression equal to zero, or an equality a = b, which is read as + /// a - b = 0, as reads one in a single variable. /// After having created a system of equations, you may solve or latexize it. /// /// @@ -53,7 +55,13 @@ public sealed class EquationSystem : ILatexizeable /// ); /// /// - public EquationSystem(params Entity[] equations) => this.equations = equations; + public EquationSystem(params Entity[] equations) => this.equations = equations.Select(AsAnExpressionEqualToZero).ToList(); + + // An equality was passed on as it stood and the system answered that it had no solution: + // `x + y = 3` and `x - y = 1` gave null, where `x + y - 3` and `x - y - 1` give `[[2, 1]]`. + // https://github.com/asc-community/AngouriMath/issues/1673 + private static Entity AsAnExpressionEqualToZero(Entity equation) + => equation is Equalsf(var left, var right) ? left - right : equation; /// /// Solves a system of equations, if possible. diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver.cs index f68490eee..de4091918 100644 --- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver.cs @@ -20,6 +20,12 @@ internal static class EquationSolver /// Solves one equation internal static Set Solve(Entity equation, Variable x) { + // An equality `a = b` is `a - b = 0`, as a statement's own solve reads it: passed on as it + // stood, `MathS.SolveEquation("x + 1 = 3", x)` was `{ x : x + 1 = 3 and 3 = 0 }`, which is + // empty, where "x + 1 = 3".Solve("x") is `{ 2 }`. + // https://github.com/asc-community/AngouriMath/issues/1673 + if (equation is Equalsf(var left, var right)) + equation = left - right; using var _ = MathS.Settings.PrecisionErrorZeroRange.Set(1e-12m); using var __ = MathS.Settings.FloatToRationalIterCount.Set(0); using var ___ = MathS.Settings.MaxExpansionTermCount.Set(50); diff --git a/Sources/MCP/AngouriMath.Mcp/SelfTest.cs b/Sources/MCP/AngouriMath.Mcp/SelfTest.cs index 913e7af76..5dad347e9 100644 --- a/Sources/MCP/AngouriMath.Mcp/SelfTest.cs +++ b/Sources/MCP/AngouriMath.Mcp/SelfTest.cs @@ -75,22 +75,6 @@ private static readonly (string Name, Func StillBroken, string Documented) [ ("Simplify(sqrt(x^2))", () => Value("sqrt(x^2)") != "abs(x)", "left as written rather than reduced to abs(x)"), - ("MathS.Equations does not solve a system written as equalities", () => - { - // x + y = 3 and x - y = 1 meet at (2, 1), which the same system written in '= 0' - // form is solved to. A check for an exception alone would read a system that comes - // back with no solution as fixed. https://github.com/asc-community/AngouriMath/issues/1673 - try - { - var solutions = MathS.Equations("x + y = 3".ToEntity(), "x - y = 1".ToEntity()).Solve("x", "y"); - return solutions is null || solutions.RowCount != 1 - || solutions[0, 0].Evaled != 2 || solutions[0, 1].Evaled != 1; - } - catch (Exception) - { - return true; - } - }, "x + y = 3, x - y = 1 has no solution when written as equalities; each equation must be given in '= 0' form"), ("DefiniteIntegral is a first-order rule", () => { // Halving the step count roughly doubles the error iff the rule is first order. diff --git a/Sources/MCP/AngouriMath.Mcp/Tools.cs b/Sources/MCP/AngouriMath.Mcp/Tools.cs index b260d487a..89b5359f7 100644 --- a/Sources/MCP/AngouriMath.Mcp/Tools.cs +++ b/Sources/MCP/AngouriMath.Mcp/Tools.cs @@ -1166,11 +1166,8 @@ private static JsonObject SolveSystem(JsonObject args) if (!TryParse(eq, "equations", false, out var e, out var w, out var error)) return error!; - // EquationSystem wants each equation in "= 0" form, and handed an equality it - // finds no solution, so normalise here: writing 'x + y = 3' is the natural thing - // for a caller to do, and it should just work. - // https://github.com/asc-community/AngouriMath/issues/1673 - parsed.Add(e is Equalsf equality ? equality.Left - equality.Right : e); + // An equality, 'x + y = 3', is read by the library as 'x + y - 3 = 0'. + parsed.Add(e); warnings.AddRange(w); } diff --git a/Sources/MCP/README.md b/Sources/MCP/README.md index 2a037db77..cfd5afc61 100644 --- a/Sources/MCP/README.md +++ b/Sources/MCP/README.md @@ -151,9 +151,8 @@ existing tool, then a prompt, and add a tool last. A tool's description is a rou documentation: say when to call it, and where a model would wrongly trust itself, say so. This is an adapter, not a second computer algebra system. Mathematics belongs in the library, where -the terminal and the notebooks get it too, and four workarounds here wait for it: +the terminal and the notebooks get it too, and three workarounds here wait for it: eigenvalues by the characteristic polynomial ([#1676](https://github.com/asc-community/AngouriMath/issues/1676)), a numeric comparison with a tolerance ([#1674](https://github.com/asc-community/AngouriMath/issues/1674)), -`a = b` rewritten to `a - b` for a system ([#1673](https://github.com/asc-community/AngouriMath/issues/1673)), and the numeric definite integral run twice to find its good digits ([#1675](https://github.com/asc-community/AngouriMath/issues/1675)). diff --git a/Sources/Tests/UnitTests/Algebra/SolveTest/AnEqualityReadAsADifferenceTest.cs b/Sources/Tests/UnitTests/Algebra/SolveTest/AnEqualityReadAsADifferenceTest.cs new file mode 100644 index 000000000..2babb1897 --- /dev/null +++ b/Sources/Tests/UnitTests/Algebra/SolveTest/AnEqualityReadAsADifferenceTest.cs @@ -0,0 +1,54 @@ +// +// 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.Extensions; +using Xunit; +using static AngouriMath.Entity.Set; + +namespace AngouriMath.Tests.Algebra +{ + /// + /// An equation written as an equality a = b is solved as a - b = 0 where an expression + /// equal to zero is expected, as a statement's own solve reads it. Passed on as it stood, a system + /// answered that it had no solution, and answered a set builder + /// with a false condition in it, which is empty. + /// #1673 + /// + [Trait("Area", "Algebra")] + public sealed class AnEqualityReadAsADifferenceTest + { + [Theory] + [InlineData("x + y = 3", "x - y = 1")] + [InlineData("x + y = 3", "x - y - 1")] + [InlineData("x + y - 3", "x - y = 1")] + [InlineData("x = 3 - y", "y = x - 1")] + public void ASystemOfEqualitiesIsSolved(string first, string second) + { + var solution = MathS.Equations(first.ToEntity(), second.ToEntity()).Solve("x", "y"); + Assert.NotNull(solution); + Assert.Equal(1, solution!.RowCount); + Assert.Equal(2, (int)solution[0, 0].EvalNumerical().RealPart); + Assert.Equal(1, (int)solution[0, 1].EvalNumerical().RealPart); + } + + [Fact] + public void ASystemPrintsEachEquationAsADifferenceEqualToZero() + => Assert.Equal("x + y - 3 = 0\nx - y - 1 = 0", MathS.Equations("x + y = 3".ToEntity(), "x - y = 1".ToEntity()).ToString()); + + [Fact] + public void OneEqualityIsSolvedInItsVariable() + => Assert.Equal(new FiniteSet(2), MathS.SolveEquation("x + 1 = 3", "x")); + + [Fact] + public void TheExtensionReadsAnEqualityTheSameWay() + => Assert.Equal(new FiniteSet(-2, 2), "x^2 = 4".SolveEquation("x")); + + [Fact] + public void AnEqualityWithAParameterIsSolvedForTheVariable() + => Assert.Equal(new FiniteSet("b / a".ToEntity().InnerSimplified), "a * x = b".ToEntity().SolveEquation("x").InnerSimplified); + } +}