Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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,
Expand Down
42 changes: 36 additions & 6 deletions Sources/AngouriMath/Functions/Continuous/Differentiation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,20 @@ partial record Entity
/// </code>
/// </example>
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);

/// <summary>
/// What <see cref="Differentiate(Variable)"/> does, reachable by
Expand All @@ -67,13 +80,24 @@ partial record Variable

/// <inheritdoc/>
/// <remarks>
/// The node, not its name. A <see cref="Constant"/> and a <see cref="Variable"/> can
/// share a spelling — that is what a binder over <c>pi</c> produces — and comparing
/// the spelling made <c>derivative(arccos(0) * pi, pi)</c> differentiate the
/// <c>pi / 2</c> that <c>arccos(0)</c> simplifies to as though it were the index.
/// A name that cannot vary contributes nothing to any derivative, so
/// <c>d(anything)/d(pi)</c> is <c>0</c> and not <c>1</c> even where the name
/// matches, which is what
/// <a href="https://github.com/asc-community/AngouriMath/issues/993">#993</a> 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 <c>pi</c>, and it evaluates to itself.
///
/// And where it does vary, the comparison is of the node, not its name. A
/// <see cref="Constant"/> and a <see cref="Variable"/> can share a spelling — that
/// is what a binder over <c>pi</c> produces — and comparing the spelling made
/// <c>derivative(arccos(0) * pi, pi)</c> differentiate the <c>pi / 2</c> that
/// <c>arccos(0)</c> simplifies to as though it were the index.
/// <a href="https://github.com/asc-community/AngouriMath/issues/984">#984</a>
/// </remarks>
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
Expand Down Expand Up @@ -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);
Expand Down
62 changes: 62 additions & 0 deletions Sources/Tests/UnitTests/Calculus/DerivativeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,68 @@ [Fact] public void APartOfADerivativeSurvivesAFactorialItCannotTake()
}

/// <summary>
/// <see cref="Entity.Differentiate(Variable)"/> takes a <see cref="Variable"/>, and
/// <c>MathS.pi</c> and <c>MathS.e</c> are ones, so they could be handed to it and it
/// differentiated as though they varied: <c>sin(pi)</c> came back as <c>-1</c>, which is
/// <c>cos(pi)</c> — the chain rule run over a symbol that cannot change. Nothing varies
/// with respect to something that cannot vary, so the answer is <c>0</c>.
/// <a href="https://github.com/asc-community/AngouriMath/issues/993">#993</a>
/// </summary>
[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));

/// <summary>
/// The power overload too, where the guard has to come before the loop.
/// <a href="https://github.com/asc-community/AngouriMath/issues/993">#993</a>
/// </summary>
[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));

/// <summary>
/// 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.
/// <a href="https://github.com/asc-community/AngouriMath/issues/993">#993</a>
/// </summary>
[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));
}

/// <summary>
/// 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.
/// <a href="https://github.com/asc-community/AngouriMath/issues/993">#993</a>
/// </summary>
[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. <c>Differentiate(Variable)</c> goes through
/// the transformation, which ends at <c>DifferentiateOnce</c> and simplifies;
/// <c>Differentiate(Variable, int)</c> called <c>InnerDifferentiate</c> straight in its
Expand Down
Loading