Differentiate n times where n times was asked for (#1002) - #1003
Merged
Conversation
Differentiate(Variable) goes through the transformation, which ends at
DifferentiateOnce and simplifies. Differentiate(Variable, int) called
InnerDifferentiate straight in its loop, so nothing was simplified -- and
because each pass differentiated the unsimplified result of the last, every
`0 *` and `* 1` the chain rule produces was there to be differentiated
again.
"x ^ 3".Differentiate(x, 2)
was: (0 * x ^ 2 + 2 * x ^ 1 * 1 * 3) * 1 + 0 * 3 * x ^ 2
now: 2 * x * 3
The value was never wrong; the form was, and the cost grew with the mess
rather than with the derivative. n = 0 and n < 0 are unchanged.
The two overloads cannot simply be merged, which is the part the issue got
wrong. Derivativef's simplification decides whether a derivative can be
taken by asking for it and keeping the node when a Derivativef comes back,
and 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. The raw loop stays as an internal
InnerDifferentiate(Variable, int) for that caller, and only the public
overload simplifies.
The raw variant must keep the negative-power branch. Without it the loop
never runs, returns the input, and the input is not a Derivativef -- so
derivative(apply(f, x), x, -1) came back as apply(f, x) rather than as the
integral. Three tests in the suite said so, which is the only reason it did
not ship.
Suite 7378 passed, 0 failed. Corpus unchanged at 116/119 with 0 wrong.
crashcheck 1834 cases, 0 crashed, with seven cases added for this shape.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbKcbJP266A3EGyQ5kq7Ru
Rafael-SOWNet
added a commit
that referenced
this pull request
Aug 22, 2026
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. "sin(pi)".Differentiate(MathS.pi) was: -1 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. 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 -- which is what keeps this compatible with #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. The guard is on the two public overloads rather than deeper, so that x! -- whose derivative the library cannot take at all -- is answered too: the variable settles it whatever the expression is. Variable.InnerDifferentiate carries it as well, for the internal callers that reach the chain rule without going through Differentiate. Integrate and Limit over a constant are not changed. They have no value to give, so leaving them unevaluated is the existing "I could not settle this", not a refusal of something settled. The derivative is settled. Suite 7383 passed, 0 failed. Corpus unchanged at 116/119 with 0 wrong. crashcheck 1834 cases, 0 crashed. Composed with #990, #991 and #1003 on a local integration branch: 7484 passed, 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbKcbJP266A3EGyQ5kq7Ru
This was referenced Aug 22, 2026
…ower-simplifies-1002 # Conflicts: # BREAKING-CHANGES.md # Sources/AngouriMath/Functions/Evaluation/Evaluation.Continuous/Evaluation.Continuous.Calculus.Classes.cs
Rafael-SOWNet
added a commit
that referenced
this pull request
Aug 22, 2026
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. "sin(pi)".Differentiate(MathS.pi) was: -1 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. 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 -- which is what keeps this compatible with #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. The guard is on the two public overloads rather than deeper, so that x! -- whose derivative the library cannot take at all -- is answered too: the variable settles it whatever the expression is. Variable.InnerDifferentiate carries it as well, for the internal callers that reach the chain rule without going through Differentiate. Integrate and Limit over a constant are not changed. They have no value to give, so leaving them unevaluated is the existing "I could not settle this", not a refusal of something settled. The derivative is settled. Suite 7383 passed, 0 failed. Corpus unchanged at 116/119 with 0 wrong. crashcheck 1834 cases, 0 crashed. Composed with #990, #991 and #1003 on a local integration branch: 7484 passed, 0 failed. Claude-Session: https://claude.ai/code/session_01KbKcbJP266A3EGyQ5kq7Ru Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1002 — and the fix that issue proposes is wrong; see Why the two cannot simply be merged.
The defect
Differentiate(Variable)goes through the transformation, which ends atDifferentiateOnceand simplifies.Differentiate(Variable, int)calledInnerDifferentiatestraight in its loop, so nothing was ever simplified — and because each pass differentiated the unsimplified result of the last, every0 *and* 1the chain rule produces was still there to be differentiated again.I wrote in the issue that
power = 1was fine. It is not — one raw pass still leaks its* 1. Atn = 3it stops being untidy:The value was never wrong — the two routes agree numerically on both versions — so this is a change of form, and it breaks anyone matching on the shape. It also means the cost grew with the accumulated mess rather than with the derivative.
Why the two cannot simply be merged
#1002 says "use
DifferentiateOncein the loop". Doing exactly that overflows the stack:Derivativef's simplification decides whether a derivative can be taken by asking for it and keeping the node when aDerivativefcomes back. That test is what terminates it. An overload that simplifies each pass makes it simplify the very node it is deciding about, which asks for the derivative again, forever.So the raw loop stays, as an internal
InnerDifferentiate(Variable, int)thatDerivativef.InnerSimplifyuses, and only the public overload simplifies. That is also the answer to the// TODO: should we call InnerSimplified here?sitting above that arm — no, and now there is a comment saying why.And the raw variant must keep the negative-power branch
My first version of it did not, on the reasoning that "raw differentiation" has nothing to do with integration. Three tests disagreed:
A loop that never runs returns its input, and the input is not a
Derivativef, so the caller took it for a resolved answer and the integral vanished. That is a wrong answer rather than a crash, so nothing but the suite would have caught it.Measured
work/crashcheck: 1834 cases, 0 crashed, with seven cases added for this shape (both signs of the power, and a derivative that cannot be taken nested inside an expression).[Theory(Timeout = 30000)]guarding the overflow.BREAKING-CHANGES.mdentry with both values measured on a build of each arm.Against the other open PRs
Derived with
git merge-tree --write-tree— this one is not doc-only:fix/bound-name-must-be-symbolicBREAKING-CHANGES.md+Evaluation.Continuous.Calculus.Classes.csconstant-node-984BREAKING-CHANGES.md+Evaluation.Continuous.Calculus.Classes.csBREAKING-CHANGES.mdonlyBoth source conflicts are one line each and additive — #990 adds a guard to the arm, #991 wraps the result in
Core.Binding.Written, and this changes which method the arm calls. I resolved all three on a local integration branch of #1002 + #990 + #991 and ran it: 7467 passed, 0 failed. I will do the rebase whichever way they land.🤖 Generated with Claude Code
https://claude.ai/code/session_01KbKcbJP266A3EGyQ5kq7Ru