diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index d8f06ecae..8f7f554af 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -181,6 +181,42 @@ keeps it true. [#363](https://github.com/asc-community/AngouriMath/issues/363), [#746](https://github.com/asc-community/AngouriMath/issues/746) item 79. +| **Silent** | `"derivative(y, x) + y - x".SolveEquation("y")`, and the same equation written `= 0` | `{ x }`, which is not a root of it | `{ y : derivative(y, x) + y - x = 0 }` | +| **Silent** | `"integral(y, x) + y - x".SolveEquation("y")` | `{ -(C + -x) / (x + 1) }` | `{ y : integral(y, x) + y - x = 0 }` | +| **Silent** | `"limit(y, x, 0) + y - x".SolveEquation("y")` | `{ x / 2 }` | `{ y : limit(y, x, 0) + y - x = 0 }` | +| **Silent** | `"sum(y, k, 1, 3) + y - k".SolveEquation("y")` | `{ k / 4 }` | `{ y : sum(y, k, 1, 3) + y - k = 0 }` | +| **Silent** | `"{ y : derivative(y, x) + y - x = 0 }".ToEntity().Simplify()` | `{ y : y - x = 0 }`, a different set | unchanged | + +### An equation whose unknown stands under a derivative is left unsolved + +`"derivative(y, x) + y - x".SolveEquation("y")` answered `{ x }`. Substituting that back — +with this library's own `Substitute` — gives `derivative(x, x) + x - x`, which is `1`. The set +named a member that is not a root. + +The derivative went to zero because `y` is not `x`, and every calculus operator does the same: +`limit(y, x, 0)` is `y`, `integral(y, x)` is `x * y + C`, `sum(y, k, 1, 3)` is `3 * y`. Each is a +decision about the *name* `y`, and the root the solver then returns says that name stands for an +expression in `x` — so the answer denies the step that produced it. Every one of the operators +was affected, and so was a quadratic in the unknown: `derivative(y, x) + y ^ 2 - x` answered +`{ sqrt(x), -sqrt(x) }`, which leave `x ^ (-1/2) / 2` and `-1/2 * x ^ (-1/2)`. + +The equation is not thereby unsatisfiable, so the empty set would replace one false claim with +another. What holds is the condition as written, and that is what comes back. Solving it needs a +differential-equation solver, which +[#746](https://github.com/asc-community/AngouriMath/issues/746) has as item 48 and which this +library does not yet have. + +A root that does not mention the name the operator is taken over denies nothing, and is returned +as before: `"derivative(y * x, x) + y - 1".SolveEquation("y")` is `{ 1/2 }`, and +`"derivative(y ^ 2, y) - 2".SolveEquation("y")` is `{ 1 }` — there the unknown *is* the name the +derivative binds, and no independence is claimed of it. + +The set builder had the same reading of its own bound name and lost it the same way: +`{ y : derivative(y, x) + y - x = 0 }` simplified to `{ y : y - x = 0 }`, which is `{ x }`. A +binder over `y` makes `y` range over values, and expressions in `x` are among them, so that +simplification settles a condition that was written to stay open. It no longer fires. + +[#964](https://github.com/asc-community/AngouriMath/issues/964) --- diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 089151dab..3569a5b9b 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -89,4 +89,5 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed 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/AotSmokeTest/*.cs] +[Tests/UnitTests/Algebra/SolveTest/CalculusEquationTest.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 diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Definition.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Definition.cs index 81eda2fb8..1d8853523 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Definition.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Definition.cs @@ -50,6 +50,34 @@ public abstract partial record CalculusOperator(Entity Expression, Entity Var) : /// The expression the operator is applied to, which may mention . public Entity Expression { get; init; } = Binding.Of(Var).In(Expression); + + /// + /// The names some calculus operator in is taken over, + /// where stands inside the body that operator is applied to. + /// + /// + /// Such an operator decides its value by taking to be free + /// of the name it is taken over: derivative(y, x) is 0 because y is not + /// x, limit(y, x, 0) is y, and integral(y, x) is + /// x * y + C. That is a decision about the name and not about a value put in + /// its place, so wherever stands for values rather than for + /// itself — a set builder ranges over them, a solver's answer supplies one — the + /// decision is not available and nothing may rest on it. + /// #964 + /// + /// The name an operator binds itself is not one of these: the y in + /// derivative(y ^ 2, y) is the one being differentiated over, and no + /// independence is claimed of it. + /// + internal static IReadOnlyCollection NamesAssumedFreeOf(Entity expression, Variable name) + { + HashSet? found = null; + foreach (var node in expression.Nodes) + if (node is CalculusOperator { Var: Variable over } calculus + && over != name && calculus.Expression.ContainsNode(name)) + (found ??= new HashSet()).Add(over); + return found ?? (IReadOnlyCollection)System.Array.Empty(); + } } /// diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver.cs index e9cbf2eb8..190d45d4e 100644 --- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver.cs @@ -40,7 +40,8 @@ internal static Set Solve(Entity equation, Variable x) // (2^x + 2^(2x) - 6 = 0).Solve(x) gave { 1 } while // "2^x + 2^(2x) - 6".SolveEquation(x) gave { 1, ln((-3) ^ (1 / ln(2))) }, // and the second of those is not a root of anything. - return StatementSolver.WithoutSpuriousRoots(finite, equation, x); + return StatementSolver.UnsolvedWhereIndependenceIsDenied( + StatementSolver.WithoutSpuriousRoots(finite, equation, x), equation.Equalizes(0), x); } else return solutions; diff --git a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/SolveStatement.cs b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/SolveStatement.cs index 358a5006e..cda428515 100644 --- a/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/SolveStatement.cs +++ b/Sources/AngouriMath/Functions/Continuous/Solvers/EquationSolver/SolveStatement.cs @@ -42,6 +42,34 @@ internal static Set WithoutSpuriousRoots(Set roots, Entity equation, Variable x) ? finite.Where(root => !IsSpurious(equation, x, root)).ToSet() : roots; + /// + /// Answers the condition itself where a root denies the independence the calculus + /// operators in the equation were evaluated under. + /// + /// + /// derivative(y, x) + y - x was answered { x }: the derivative went to + /// zero because y is not x, and the root then says that it is. Putting + /// it back gives derivative(x, x) + x - x, which is 1 — so the set named a + /// member that is not a root. A root free of that name is untouched, because nothing + /// was assumed that it goes on to deny: derivative(y * x, x) + y - 1 is still + /// { 1/2 }. + /// + /// The equation is not thereby unsatisfiable, so the empty set would be a second + /// false claim in place of the first. What holds is the condition as written, and + /// solving it needs a differential-equation solver this library does not have. + /// #964, + /// #746 + /// + internal static Set UnsolvedWhereIndependenceIsDenied(Set roots, Entity condition, Variable x) + { + if (roots is not FiniteSet finite) + return roots; + var assumed = CalculusOperator.NamesAssumedFreeOf(condition, x); + return assumed.Count > 0 && finite.Any(root => assumed.Any(root.ContainsNode)) + ? new ConditionalSet(x, condition) + : roots; + } + /// /// How large the residual has to be next to the terms it is the sum of before it /// counts as evidence against a root. @@ -127,7 +155,9 @@ internal static Set Solve(Entity expr, Variable x) => AnalyticalSetSolver.Solve(left, right, x), Equalsf(var left, var right) when left is not Set && right is not Set - => WithoutSpuriousRoots(AnalyticalEquationSolver.Solve(left - right, x), left - right, x), + => UnsolvedWhereIndependenceIsDenied( + WithoutSpuriousRoots(AnalyticalEquationSolver.Solve(left - right, x), left - right, x), + expr, x), Equalsf => Empty, diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Omni/Evaluation.Omni.Classes.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Omni/Evaluation.Omni.Classes.cs index a9727bce6..d47f879d9 100644 --- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Omni/Evaluation.Omni.Classes.cs +++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Omni/Evaluation.Omni.Classes.cs @@ -54,6 +54,17 @@ protected override Entity InnerSimplify(bool isExact) // as `{ } provided not x = 0`, where the x named in the condition is no // longer the x the set ranges over. // https://github.com/asc-community/AngouriMath/issues/878 + + // A calculus operator taken over some other name reads this one as free of + // it -- derivative(y, x) is 0 because y is not x. Under a binder over y that + // reading is not available: y ranges over values here, and expressions in x + // are among them. Simplifying anyway settles a condition that was written to + // stay open: `{ y : derivative(y, x) + y - x = 0 }` became `{ y : y - x = 0 }`, + // which names x, and putting x back into the condition it came from gives 1. + // https://github.com/asc-community/AngouriMath/issues/964 + if (Var is Variable bound && CalculusOperator.NamesAssumedFreeOf(Predicate, bound).Count > 0) + return this; + var predicate = Predicate.InnerSimplified(isExact); // `expr provided a provided b` is what the rules build where two operands diff --git a/Sources/Tests/UnitTests/Algebra/SolveTest/CalculusEquationTest.cs b/Sources/Tests/UnitTests/Algebra/SolveTest/CalculusEquationTest.cs new file mode 100644 index 000000000..f1d3405a0 --- /dev/null +++ b/Sources/Tests/UnitTests/Algebra/SolveTest/CalculusEquationTest.cs @@ -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 System.Linq; +using AngouriMath; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Algebra.SolveTest +{ + /// + /// An equation whose unknown stands inside a derivative, an integral, a limit, a sum + /// or a product, where the operator is taken over some other name. + /// https://github.com/asc-community/AngouriMath/issues/964 + /// + /// + /// Such an operator is evaluated by deciding that the unknown does not move with the + /// name the operator is taken over: derivative(y, x) is 0 because y is + /// not x. A root that mentions x denies exactly that, so it cannot be + /// asserted on the strength of a derivation that assumed it. + /// + [Trait("Area", "Algebra")] + public sealed class CalculusEquationTest + { + /// + /// The answer must not be a finite set holding something that is not a root. The + /// membership is checked by substituting back and simplifying, which is what the + /// issue did by hand. + /// + private static void AssertEveryRootSatisfies(string equation, string unknown) + { + var expr = equation.ToEntity(); + if (expr.SolveEquation(unknown) is not Entity.Set.FiniteSet finite) + return; + foreach (var root in finite) + Assert.True( + expr.Substitute(unknown, root).Simplify() == 0, + $"{root.Stringize()} is not a root of {equation}; it leaves " + + $"{expr.Substitute(unknown, root).Simplify().Stringize()}"); + } + + [Theory] + [InlineData("derivative(y, x) + y - x", "y")] + [InlineData("derivative(y, x) + y ^ 2 - x", "y")] + [InlineData("integral(y, x) + y - x", "y")] + [InlineData("limit(y, x, 0) + y - x", "y")] + [InlineData("sum(y, k, 1, 3) + y - k", "y")] + public void NoAssertedNonRoot(string equation, string unknown) + => AssertEveryRootSatisfies(equation, unknown); + + /// + /// The empty set says there is no such y, which is as false as naming one. + /// The condition itself is the only one of the three that is true. + /// + [Theory] + [InlineData("derivative(y, x) + y - x", "y")] + [InlineData("integral(y, x) + y - x", "y")] + [InlineData("limit(y, x, 0) + y - x", "y")] + [InlineData("sum(y, k, 1, 3) + y - k", "y")] + [InlineData("product(y, k, 1, 3) + y - k", "y")] + public void UnsolvedRatherThanEmpty(string equation, string unknown) + => Assert.IsType(equation.ToEntity().SolveEquation(unknown)); + + /// An equation and the same equation written = 0 answer alike. + [Theory] + [InlineData("derivative(y, x) + y - x", "y")] + [InlineData("limit(y, x, 0) + y - x", "y")] + public void StatementFormAnswersTheSame(string equation, string unknown) + => Assert.IsType( + (equation + " = 0").ToEntity().Solve(unknown)); + + /// + /// Where no root mentions the name the operator is taken over, nothing was assumed + /// that the answer then denies, and the answer stands. d(y*x)/dx is y + /// for a y free of x, and 1/2 is free of x. + /// + [Theory] + [InlineData("derivative(y * x, x) + y - 1", "y", "1/2")] + [InlineData("derivative(y ^ 2, y) - 2", "y", "1")] + [InlineData("limit(y * x, x, 2) - 4", "y", "2")] + [InlineData("sum(y, k, 1, 3) - 6", "y", "2")] + public void AnUnthreatenedRootIsStillReturned(string equation, string unknown, string root) + { + var finite = Assert.IsType(equation.ToEntity().SolveEquation(unknown)); + Assert.Contains(root.ToEntity(), finite.Select(e => e.Simplify())); + } + } +}