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
43 changes: 43 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** | `"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 |
| **Silent** | `limit(e, e, 0).Evaled` | `2.718…` | `0` |
Expand All @@ -52,6 +53,48 @@ 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` |

### `Differentiate(x, n)` answers what `Differentiate(x)` n times answers

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 loop, so nothing was ever simplified — and because each pass
differentiated the *unsimplified* result of the last, every `0 *` and `* 1` the chain rule produces
was still there to be differentiated again.

```csharp
var x = MathS.Var("x");
"x ^ 3".ToEntity().Differentiate(x, 1) // was: 3 * x ^ 2 * 1 now: 3 * x ^ 2
"x ^ 3".ToEntity().Differentiate(x, 2) // was: (0 * x ^ 2 + 2 * x ^ 1 * 1 * 3) * 1 + 0 * 3 * x ^ 2 now: 2 * x * 3
```

At `n = 3` it is no longer just untidy:

```
"x ^ 4".Differentiate(x, 3)
// was: (0 * x ^ 3 + 3 * x ^ 2 * 1 * 0 + ((0 * x ^ 2 + 2 * x ^ 1 * 1 * 3) * 1 + 0 * 3 * x ^ 2) * 4
// + 0 * 3 * x ^ 2 * 1) * 1 + 0 * (0 * x ^ 3 + 3 * x ^ 2 * 1 * 4) + 0 * 4 * x ^ 3
// + (0 * x ^ 3 + 3 * x ^ 2 * 1 * 4) * 0
// now: 2 * x * 3 * 4
```

The value was never wrong — `Differentiate(x, 3)` and differentiating three times agree numerically
on both versions — so this is a change of *form*, and it breaks anyone matching on the shape of the
result. It also means the cost grew with the accumulated mess rather than with the derivative.

`n = 0` (returns the input) and `n < 0` (integrates) are unchanged.

**Why the two were not simply merged.** `Derivativef`'s simplification decides whether a derivative
can be taken by asking for it and keeping the node when a `Derivativef` comes back — that test is
what terminates it. Routing it through an overload that simplifies each pass makes it simplify the
very node it is deciding about, arrive back at itself, and recurse: `derivative(x!, x, 2)` overflowed
the stack after 3214 frames. So the raw loop still exists as an internal `InnerDifferentiate(Variable,
int)`, which is what that caller uses, and only the public overload simplifies. There are cases for
both in `work/crashcheck`.

Measured: suite 7378 passed, 0 failed; corpus unchanged at 116/119 with 0 wrong, no case's verdict or
answer altered; crashcheck 1834 cases, 0 crashed.
[#1002](https://github.com/asc-community/AngouriMath/issues/1002).

### A rewritten node keeps its `Codomain`, so a domain constraint no longer disappears

`Entity.Replace` rebuilds every node on the path to a change, and a rebuilt node started from its
Expand Down
32 changes: 31 additions & 1 deletion Sources/AngouriMath/Functions/Continuous/Differentiation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,29 @@ partial record Matrix
protected override Entity InnerDifferentiate(Variable variable) => Elementwise(e => e.InnerDifferentiate(variable));
}

/// <summary>
/// <paramref name="power"/> raw passes, with nothing simplified in between.
/// <see cref="Derivativef"/>'s simplification needs exactly this and not the public
/// overload: it asks for the derivative and keeps the node when what comes back is
/// still a <see cref="Derivativef"/>, so a simplification anywhere inside would ask
/// that same node to simplify, which asks for the derivative again, without end.
/// </summary>
internal Entity InnerDifferentiate(Variable x, int power)
{
var ent = this;
// A negative power integrates, exactly as the public overload does -- dropping that
// here turned derivative(apply(f, x), x, -1) into apply(f, x) rather than into the
// integral, because a loop that never runs returns the input and the input is not a
// Derivativef, so the caller took it for a resolved answer.
if (power < 0)
for (var _ = 0; _ < -power; _++)
ent = ent.Integrate(x);
else
for (var _ = 0; _ < power; _++)
ent = ent.InnerDifferentiate(x);
return ent;
}

/// <summary>Derives over <paramref name="x"/> <paramref name="power"/> times</summary>
public Entity Differentiate(Variable x, int power)
{
Expand All @@ -91,7 +114,14 @@ public Entity Differentiate(Variable x, int power)
ent = ent.Integrate(x);
else
for (var _ = 0; _ < power; _++)
ent = ent.InnerDifferentiate(x);
// DifferentiateOnce, not InnerDifferentiate: the raw chain rule leaves
// every `0 *` and `* 1` it produces in place, and the next pass
// differentiates those too, so the expression compounds rather than
// reduces. Differentiate(Variable) reaches DifferentiateOnce through the
// transformation, which is why one pass of it answered and n passes of
// this did not.
// https://github.com/asc-community/AngouriMath/issues/1002
ent = ent.DifferentiateOnce(x);
return ent;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,13 @@ protected override Entity InnerSimplify(bool isExact) =>
ExpandOnTwoAndTArguments(Expression, Var, Iterations,
(a, b, c) => (a, b, c) switch
{
// TODO: should we call InnerSimplified here?
// InnerDifferentiate, not the public Differentiate: the test below is
// "did the derivative come back unresolved", and the public overload
// simplifies each pass, so asking it would simplify the very
// Derivativef this is deciding about and arrive back here unchanged.
// https://github.com/asc-community/AngouriMath/issues/1002
(var expr, Variable var, int asInt)
when expr.Differentiate(var, asInt) is var res and not Derivativef
when expr.InnerDifferentiate(var, asInt) is var res and not Derivativef
=> Core.Binding.Written(var, res.InnerSimplified(isExact)),
(var expr, Variable var, int asInt) => null,
(Application, _, _) => null,
Expand All @@ -78,7 +82,7 @@ when otherExpr.Vars.Count > 0
&& Variable.CreateTemp(otherExpr.Vars) is var tempVar
&& expr.Substitute(otherExpr, tempVar) is var tempSubstituted
&& !otherExpr.Vars.Any(tempSubstituted.ContainsNode)
&& tempSubstituted.Differentiate(tempVar, asInt) is var res and not Derivativef
&& tempSubstituted.InnerDifferentiate(tempVar, asInt) is var res and not Derivativef
=> res.Substitute(tempVar, otherExpr).InnerSimplified(isExact),
// Otherwise it is a change of variables and not a rename, and the
// subexpression does not need to occur at all: with z = x + 1, x ^ 2 is
Expand Down
65 changes: 65 additions & 0 deletions Sources/Tests/UnitTests/Calculus/DerivativeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -238,5 +238,70 @@ [Fact] public void APartOfADerivativeSurvivesAFactorialItCannotTake()
Assert.True(limit.Evaled is Entity.Limitf,
$"the expansion should be refused here, and it came back {limit.Evaled}");
}

/// <summary>
/// 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
/// loop, so every <c>0 *</c> and <c>* 1</c> the chain rule produces stayed in and the
/// next iteration differentiated those too. <c>x ^ 3</c> twice came back as
/// <c>(0 * x ^ 2 + 2 * x ^ 1 * 1 * 3) * 1 + 0 * 3 * x ^ 2</c> where differentiating twice
/// by hand gives <c>2 * x * 3</c>.
/// <a href="https://github.com/asc-community/AngouriMath/issues/1002">#1002</a>
/// </summary>
[Theory]
[InlineData("x ^ 4", 0)]
[InlineData("x ^ 4", 1)]
[InlineData("x ^ 4", 2)]
[InlineData("x ^ 4", 3)]
[InlineData("sin(x) * x", 2)]
[InlineData("sin(x) / x", 2)]
[InlineData("x ^ 5 + sin(x)", 3)]
public void DifferentiatingNTimesIsDifferentiatingOnceNTimes(string exprRaw, int power)
{
var expr = exprRaw.ToEntity();
var once = expr;
for (var k = 0; k < power; k++)
once = once.Differentiate(x);
Assert.Equal(once, expr.Differentiate(x, power));
}

/// <summary>
/// The negative-power path integrates instead, and is not what changed.
/// <a href="https://github.com/asc-community/AngouriMath/issues/1002">#1002</a>
/// </summary>
[Fact] public void ANegativePowerStillIntegrates()
=> Assert.Equal("x ^ 2".ToEntity().Integrate(x), "x ^ 2".ToEntity().Differentiate(x, -1));

/// <summary>
/// **Regression guard for a stack overflow.** <see cref="Derivativef"/>'s simplification
/// asks for the derivative and keeps the node when what comes back is still a
/// <c>Derivativef</c> -- that test is what terminates it. Routing it through the public
/// <c>Differentiate(Variable, int)</c>, which simplifies each pass, made it simplify the
/// very node it was deciding about, arrive back at itself, and recurse until the stack
/// ran out: <c>derivative(x!, x, 2)</c> overflowed after 3214 frames. The raw
/// <c>InnerDifferentiate(Variable, int)</c> is what that caller needs, and the public
/// overload is free to simplify.
/// <a href="https://github.com/asc-community/AngouriMath/issues/1002">#1002</a>
/// </summary>
[Theory(Timeout = 30000)]
// a derivative that cannot be taken at all, so the node survives every pass
[InlineData("derivative(x!, x, 2)")]
[InlineData("derivative(x!, x, 3)")]
[InlineData("derivative(sin(x!) + x, x, 2)")]
public void ADerivativeThatCannotBeTakenTerminatesAtEveryPower(string exprRaw)
{
var expr = exprRaw.ToEntity();
Assert.NotNull(expr.InnerSimplified);
Assert.NotNull(expr.Evaled);
}

/// <summary>
/// And it is still refused rather than answered.
/// <a href="https://github.com/asc-community/AngouriMath/issues/1002">#1002</a>
/// </summary>
[Fact] public void ADerivativeThatCannotBeTakenIsStillUnevaluated()
=> Assert.True("derivative(x!, x, 2)".ToEntity().InnerSimplified is Derivativef,
$"expected it to stay a Derivativef, got {"derivative(x!, x, 2)".ToEntity().InnerSimplified}");
}
}
Loading