diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 04d00de29..4defd4d5d 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -35,6 +35,7 @@ read first. | **Silent** | `limit(i, i, 0)` | unevaluated | `0` | | **Silent** | `integral(i, i)` | `-1/2 + C` | `i_1 ^ 2 / 2 + C` | | | `lambda(i, i + 1)` | `InvalidArgumentParseException` | the lambda | +| **Silent** | `"sin(pi)".ToEntity().Differentiate(MathS.pi)`, and every derivative over `pi` or `e` | `-1`, the chain rule run over a symbol that cannot change | `0` | | **Silent** | `"x ^ 3".ToEntity().Differentiate(x, 2)`, and every `Differentiate(x, n)` with `n >= 1` | `(0 * x ^ 2 + 2 * x ^ 1 * 1 * 3) * 1 + 0 * 3 * x ^ 2` | `2 * x * 3` | | **Silent** | `derivative(e ^ 2, e)`, `derivative(pi ^ 2, pi)` | `0` | `2 * e_1`, `2 * pi_1` | | **Silent** | `{ e : e > 0 }` | `{ e : True }` | the set it describes | @@ -53,6 +54,51 @@ read first. | **Silent** | `integral(x, [x, y]T)` | `[[C + x ^ 2, C + x * y]]` | `[[x ^ 2 / 2 + C, x * y + C]]` | | **Silent** | `derivative(e ^ 2, e)`, over a named constant | `0` | `2 * e` | +### Differentiating over a constant is `0`, not the chain rule over a symbol that cannot change + +`Entity.Differentiate(Variable)` takes a `Variable`, and `MathS.pi` and `MathS.e` are ones — so they +could be handed to it, and it differentiated as though they varied. + +```csharp +"pi ^ 2".ToEntity().Differentiate(MathS.pi) // was: 2 * pi now: 0 +"sin(pi)".ToEntity().Differentiate(MathS.pi) // was: -1 now: 0 +"x * pi".ToEntity().Differentiate(MathS.pi) // was: x now: 0 +"sin(x * pi)".ToEntity().Differentiate(MathS.pi) // was: cos(x * pi) * x now: 0 +"x!".ToEntity().Differentiate(MathS.pi) // was: derivative(x!, pi) now: 0 +"e ^ 2".ToEntity().Differentiate(MathS.e) // was: 2 * e now: 0 +"pi ^ 3".ToEntity().Differentiate(MathS.pi, 2) // was: an unsimplified 0 now: 0 +``` + +`sin(pi)` is `0`, and its derivative with respect to anything is `0`. `-1` is `cos(pi)` — the chain +rule run over a symbol that cannot change. `x!` is the case where the library cannot take the +derivative at all, and it is `0` here regardless, because the *variable* is what settles it. + +An ordinary variable is untouched, including where a constant is present as a coefficient: +`"x ^ 2 * pi".Differentiate(x)` is `2 * x * pi` on both versions. The guard is about what is +differentiated **over**, not about what appears in the expression. `Differentiate(x, 0)` returns the +input and `Differentiate(x, n)` with `n < 0` integrates; neither changes. + +**The test is whether the name evaluates to a number, not whether it is spelled like a constant.** +That is what makes this compatible with +[#984](https://github.com/asc-community/AngouriMath/issues/984): a name a *binder* declares can vary +even when it is spelled `pi`, and it evaluates to itself. + +**The node form is a different question and belongs to #984.** On this version +`"derivative(x * pi, pi)".ToEntity().InnerSimplified` also goes from `x` to `0`, because the node +asks the same public method. Once #984's fix lands the parser *binds* `pi` there, so it becomes +`2 * pi_1` — a derivative over the variable the binder holds — and that is the intended answer, not +a regression of this one. The two compose: measured together, `sin(pi)` differentiated by `MathS.pi` +is `0` and `derivative(pi ^ 2, pi)` is `2 * pi_1`. + +`Integrate` and `Limit` over a constant are **not** changed. They have no value to give — there is +nothing to integrate with respect to a thing that cannot vary — so leaving them unevaluated is the +existing "I could not settle this", not a refusal of something settled. The derivative *is* settled, +which is why it is answered rather than declined. + +Measured: suite 7383 passed, 0 failed; corpus unchanged at 116/119 with 0 wrong, no case's verdict or +answer altered; crashcheck 1834 cases, 0 crashed. +[#993](https://github.com/asc-community/AngouriMath/issues/993). + ### `Differentiate(x, n)` answers what `Differentiate(x)` n times answers The two overloads reached different code. `Differentiate(Variable)` goes through the transformation, diff --git a/Sources/AngouriMath/Functions/Continuous/Differentiation.cs b/Sources/AngouriMath/Functions/Continuous/Differentiation.cs index ad6cb7ed1..736362c8c 100644 --- a/Sources/AngouriMath/Functions/Continuous/Differentiation.cs +++ b/Sources/AngouriMath/Functions/Continuous/Differentiation.cs @@ -44,7 +44,20 @@ partial record Entity /// /// public Entity Differentiate(Variable variable) - => Transformation.Differentiation(variable).ApplyOrKeep(this); + // Nothing varies with respect to a name that cannot vary, so the answer is 0 -- + // and it is an answer, not a refusal, which is why it is given here rather than + // left as an unresolved Derivativef for the nodes that build one. + // + // The test is whether the name evaluates to a number, not whether it is spelled + // like a constant: a name a *binder* declares can vary even when it is spelled + // `pi`, and it evaluates to itself. That is what keeps this compatible with + // https://github.com/asc-community/AngouriMath/issues/984, whose answer to + // `derivative(pi ^ 2, pi)` is over the variable the binder holds. This call has + // no binder in it, and the constant arrives directly in the position that says + // what varies. + // https://github.com/asc-community/AngouriMath/issues/993 + => variable.Evaled is Number ? 0 + : Transformation.Differentiation(variable).ApplyOrKeep(this); /// /// What does, reachable by @@ -67,13 +80,24 @@ partial record Variable /// /// - /// The node, not its name. A and a can - /// share a spelling — that is what a binder over pi produces — and comparing - /// the spelling made derivative(arccos(0) * pi, pi) differentiate the - /// pi / 2 that arccos(0) simplifies to as though it were the index. + /// A name that cannot vary contributes nothing to any derivative, so + /// d(anything)/d(pi) is 0 and not 1 even where the name + /// matches, which is what + /// #993 is + /// about. The test is whether the name evaluates to a number, not whether it is + /// spelled like a constant: a name a binder declares can vary even when it is + /// spelled pi, and it evaluates to itself. + /// + /// And where it does vary, the comparison is of the node, not its name. A + /// and a can share a spelling — that + /// is what a binder over pi produces — and comparing the spelling made + /// derivative(arccos(0) * pi, pi) differentiate the pi / 2 that + /// arccos(0) simplifies to as though it were the index. /// #984 /// - protected override Entity InnerDifferentiate(Variable variable) => this == variable ? 1 : 0; + protected override Entity InnerDifferentiate(Variable variable) => + variable.Evaled is Number ? 0 + : this == variable ? 1 : 0; } partial record Matrix @@ -109,6 +133,12 @@ internal Entity InnerDifferentiate(Variable x, int power) public Entity Differentiate(Variable x, int power) { var ent = this; + // As above -- and only for the differentiating direction, since a negative power + // integrates and an antiderivative with respect to something that cannot vary has + // no value to give at all. + // https://github.com/asc-community/AngouriMath/issues/993 + if (power > 0 && x.Evaled is Number) + return 0; if (power < 0) for (var _ = 0; _ < -power; _++) ent = ent.Integrate(x); diff --git a/Sources/Tests/UnitTests/Calculus/DerivativeTest.cs b/Sources/Tests/UnitTests/Calculus/DerivativeTest.cs index cda038183..4e8852599 100644 --- a/Sources/Tests/UnitTests/Calculus/DerivativeTest.cs +++ b/Sources/Tests/UnitTests/Calculus/DerivativeTest.cs @@ -240,6 +240,68 @@ [Fact] public void APartOfADerivativeSurvivesAFactorialItCannotTake() } /// + /// takes a , and + /// MathS.pi and MathS.e are ones, so they could be handed to it and it + /// differentiated as though they varied: sin(pi) came back as -1, which is + /// cos(pi) — the chain rule run over a symbol that cannot change. Nothing varies + /// with respect to something that cannot vary, so the answer is 0. + /// #993 + /// + [Theory] + [InlineData("pi ^ 2", "pi")] // was 2 * pi + [InlineData("sin(pi)", "pi")] // was -1 + [InlineData("x * pi", "pi")] // was x + [InlineData("sin(x * pi)", "pi")] // was cos(x * pi) * x + [InlineData("x ^ pi", "pi")] + [InlineData("x", "pi")] + [InlineData("e ^ 2", "e")] // was 2 * e + // a node whose derivative the library cannot take is still 0 over a name that cannot + // vary -- declining there would be declining something settled + [InlineData("x!", "pi")] + public void DifferentiatingOverAConstantIsZero(string exprRaw, string constant) + => Assert.Equal(0, exprRaw.ToEntity().Differentiate((Variable)constant)); + + /// + /// The power overload too, where the guard has to come before the loop. + /// #993 + /// + [Theory] + [InlineData("pi ^ 3", 1)] + [InlineData("pi ^ 3", 2)] + [InlineData("x * pi", 3)] + public void DifferentiatingOverAConstantNTimesIsZero(string exprRaw, int power) + => Assert.Equal(0, exprRaw.ToEntity().Differentiate((Variable)"pi", power)); + + /// + /// Zero times returns the input, and a negative power integrates — neither is a + /// derivative, and neither changes. An antiderivative with respect to something that + /// cannot vary has no value to give at all, which is a different question from this one. + /// #993 + /// + [Fact] public void ZeroTimesAndNegativePowersAreUntouched() + { + Assert.Equal("pi ^ 2".ToEntity(), "pi ^ 2".ToEntity().Differentiate((Variable)"pi", 0)); + Assert.Equal("pi ^ 2".ToEntity().Integrate((Variable)"pi"), + "pi ^ 2".ToEntity().Differentiate((Variable)"pi", -1)); + } + + /// + /// And an ordinary variable is untouched, including where a constant is present as a + /// coefficient — the guard is about what is differentiated *over*, not about what + /// appears in the expression. + /// #993 + /// + [Theory] + [InlineData("x * y", "y", "x")] + [InlineData("sin(x) * x", "x", "cos(x) * x + sin(x)")] + [InlineData("x ^ 2 * pi", "x", "2 * x * pi")] + [InlineData("e ^ x", "x", "e ^ x")] + [InlineData("pi * sin(x)", "x", "pi * cos(x)")] + public void DifferentiatingOverAnOrdinaryVariableIsUnchanged( + string exprRaw, string over, string expected) + => Assert.Equal(0, + (exprRaw.ToEntity().Differentiate((Variable)over) - expected.ToEntity()) + .Simplify().EvalNumerical()); /// The two overloads reached different code. Differentiate(Variable) goes through /// the transformation, which ends at DifferentiateOnce and simplifies; /// Differentiate(Variable, int) called InnerDifferentiate straight in its