From 48df10f41b5a64a155078fe40b4f6410f87116db Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 23 Aug 2026 12:21:28 +0000 Subject: [PATCH 1/2] Do not name a root that denies how it was found (#964) "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 reads a name it does not bind the same way -- limit(y, x, 0) is y, integral(y, x) is x * y + C, sum(y, k, 1, 3) is 3 * y. That is a decision about the name, and the root the solver then returns says the name stands for an expression in x. The answer denies the step that produced it. "derivative(y, x) + y - x".SolveEquation("y") { x } "derivative(y, x) + y ^ 2 - x".SolveEquation("y") { sqrt(x), -sqrt(x) } "integral(y, x) + y - x".SolveEquation("y") { -(C + -x) / (x + 1) } "limit(y, x, 0) + y - x".SolveEquation("y") { x / 2 } "sum(y, k, 1, 3) + y - k".SolveEquation("y") { k / 4 } All five now come back as the condition itself. The empty set would say there is no such y, which is a second false claim in place of the first; the condition says only that this was not settled, which is true. Solving it is #746 item 48, a differential-equation solver, and is not here. A root free of the name the operator is taken over denies nothing and is returned as before: derivative(y * x, x) + y - 1 is still { 1/2 }, and derivative(y ^ 2, y) - 2 is { 1 }, where 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: { 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 settled a condition written to stay open -- and it is what the answer above would have collapsed into. Guarded where the predicate is simplified. Not touched: the equality half of `<=` and `>=` goes through the same solver, but the inequality half is answered by AnalyticalInequalitySolver under the same assumption, so guarding one of the two would give a set that is honest in half of itself. That needs its own change. Suite 7549 passed, 0 failed, 14 skipped -- the 16 added here on top of the 7533 of the merge base. F# wrapper 134 passed. Both sides of every value above measured on a build: 6b93b401 and this tree. Claude-Session: https://claude.ai/code/session_01Bjumi5K7fg8yx6UK1mZTQd Co-authored-by: Claude Opus 5 --- BREAKING-CHANGES.md | 45 +++++++++ .../Entity.Continuous.Definition.cs | 28 ++++++ .../Continuous/Solvers/EquationSolver.cs | 3 +- .../Solvers/EquationSolver/SolveStatement.cs | 32 ++++++- .../Evaluation.Omni.Classes.cs | 11 +++ .../Algebra/SolveTest/CalculusEquationTest.cs | 92 +++++++++++++++++++ 6 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 Sources/Tests/UnitTests/Algebra/SolveTest/CalculusEquationTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index c194d9725..240446b13 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -15,6 +15,51 @@ read first. --- +## Unreleased + +### At a glance + +| Silent? | What | Was | Is | +|---|---|---|---| +| **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) + +--- + ## 2.3.0 — since 2.2.0 ### At a glance diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Definition.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Definition.cs index 944020777..a403ac16d 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..fcc0c95ff --- /dev/null +++ b/Sources/Tests/UnitTests/Algebra/SolveTest/CalculusEquationTest.cs @@ -0,0 +1,92 @@ +// +// Copyright (c) 2019-2022 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())); + } + } +} From 8441cf495834fe3329d0ced7161f46386b5287f8 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 23 Aug 2026 12:47:26 +0000 Subject: [PATCH 2/2] Date the new test file to the year it was written Sources/.editorconfig pins the header per directory rather than globally -- bumping the default would make IDE0073 fail on the 367 files that carry the 2022 one -- so a new file needs its own scope or it ships with a copyright four years out of date. The two other branches that add files append their scopes to the same place, so this costs whichever of the three merges later one append conflict, and all three entries survive it. --- Sources/.editorconfig | 3 +++ .../Tests/UnitTests/Algebra/SolveTest/CalculusEquationTest.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 7df11d04b..c500ee1cd 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -81,3 +81,6 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Analyzers/RuleRegistryGenerator/*.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/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/Tests/UnitTests/Algebra/SolveTest/CalculusEquationTest.cs b/Sources/Tests/UnitTests/Algebra/SolveTest/CalculusEquationTest.cs index fcc0c95ff..f1d3405a0 100644 --- a/Sources/Tests/UnitTests/Algebra/SolveTest/CalculusEquationTest.cs +++ b/Sources/Tests/UnitTests/Algebra/SolveTest/CalculusEquationTest.cs @@ -1,5 +1,5 @@ // -// Copyright (c) 2019-2022 Angouri. +// 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.