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()));
+ }
+ }
+}