Repository navigation
Differentiating over a constant answers as though it varies: sin(pi) differentiated by pi is -1 #993
Description
Activity
Measuring the routes before implementing changed what the question is. Two things, and the second means this should be decided before it is coded rather than after.
The defect is not
Differentiate'sEvery calculus entry point takes the constant and treats it as a variable. On
masterat5211ccd6:"pi ^ 2".ToEntity().Integrate(MathS.pi) // pi ^ 3 / 3 + C "x".ToEntity().Integrate(MathS.pi) // x * pi + C "pi ^ 2".ToEntity().Limit(MathS.pi, 0) // 0 "pi ^ 2".ToEntity().Substitute(MathS.pi, 3) // 3 ^ 2
So a guard at
Differentiate(Variable)would fix the one call I happened to file and leave the same category error in three of its neighbours — [[one rule fix exposes another]] in its usual shape. Whatever is decided here should be decided for the family.And it decides the
0-vs-refuse question, which I left open aboveI wrote that
0was "the smaller change and reads consistently". With the neighbours in view it stops being consistent:d/dc f = 0is defensible — the expression does not vary with something that cannot vary, andDifferentiatealready answers0for a variable that does not occur.∫ f dchas no0-shaped answer at all. There is nothing to integrate with respect to. The only honest result is a refusal.
Answering one with a value and the other with a refusal would be incoherent, so refusing throughout is the option that survives the family, and it is what AGENTS.md's own line points at — not answering is a legitimate answer; answering wrongly is not. It is also what SymPy does (
ValueError: Can't calculate derivative wrt pi).That is a change of shape rather than of value, so I would rather have it confirmed than assume it.
Substituteis the one I would leave alone either way — replacingpiwith a numeral is a thing a caller may legitimately want, and it is not a calculus operation.Two things separated out of this while measuring
- Differentiate(x, n) returns raw chain-rule output where Differentiate(x) n times returns the answer #1002 —
Differentiate(x, n)returns raw chain-rule output forn >= 2, with an ordinary variable and no constant in sight. The loop callsInnerDifferentiatewhere the single-argument overload reachesDifferentiateOnce, which simplifies."pi ^ 3".Differentiate(MathS.pi, 2)shows both defects at once, which is how it surfaced. - Should there be a universal set, and is Domain.Any it? (needs-design) #996 already holds the question of what
Domain.Anyand a universal set mean; this is the same boundary approached from the calculus side.
- added a commit that references this issue
on Aug 22, 2026 Fixed in #1004 as
0, which reverses what I argued for in my previous comment.I said refusing was "the option that survives the family", because
∫ f dchas no zero-shaped answer and answering one with a value while refusing the other would be incoherent. The two are not the same case:d(sin(pi))/d(pi)is settled. Decidepiis constant and the expression does not vary, so the answer is0. Refusing it declines something the library can answer.∫ f dpihas nothing to solve, so leaving it unevaluated is the existing "I could not settle this" doing its job — not a refusal of something settled.
So this is the first option in the original post,
IntegrateandLimitare untouched, and it costs no exception, no new API and no signature change anywhere.Two corrections to the original post
x!needed the guard higher than "where". I wrote thatVariable.InnerDifferentiateis where the comparison happens and that a guard atDifferentiate(Variable)"would cover every route in one place". The first half is right and insufficient:FactorialfandDerivativefbuild aDerivativefnode instead of recursing, so the chain rule's base case is never reached and"x!".Differentiate(MathS.pi)stayed unevaluated. It is0— the variable settles it whatever the expression is — so the guard is on the public overloads, withVariable.InnerDifferentiatecarrying it too for the internal callers that reach the chain rule directly.And the guard cannot be name-based.
variable.Evaled is Number, not "is it spelledpi". A name a binder declares can vary even when it is spelledpi, and it evaluates to itself — so a spelling test would undo #984. I measured the predicate onmasterand on the #984 branch before writing it.Scope, and how I found I had over-reached
My first attempt guarded
Derivativef.InnerSimplifyas well, so that the node form answered0too. Five tests failed when I composed it with #991, and they were right to: with that PR in, the parser bindspiinderivative(_, pi), soderivative(pi ^ 2, pi)is2 * pi_1— a derivative over the variable the binder holds. This issue's own text says so — "this call has no binder in it" — and I had gone past it. The guard is now on the direct API only.Composed with #990, #991 and #1003 on a local integration branch: 7484 passed, 0 failed, each behaviour intact.
sin(pi) by MathS.pi 0 derivative(pi ^ 2, pi) 2 * pi_1 derivative(x ^ 3, 3) declined "x ^ 4".Differentiate(x, 3) 2 * x * 3 * 4- added a commit that references this issue
on Aug 22, 2026
Entity.Differentiate(Variable)is typed to take aVariable, andMathS.piandMathS.eare ones — so the constants can be handed to it, and it differentiates as though they varied.Measured on
masterat4ee698da, and identically on the branch for #984, so this is neither introduced nor fixed by that:sin(pi)is0. Its derivative with respect to anything is0, and-1iscos(pi)— the chain rule run over a symbol that cannot change.Why it is separate from #984
That issue is about a name a binder declares.
derivative(pi ^ 2, pi)goes through the binder and is answered 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.What the answer should be
Two candidates, and I have no strong preference:
0— nothing in the expression varies with respect to a thing that cannot vary, andDifferentiatealready answers0for a variable that does not occur.diff(exp(2), pi)isValueError: Can't calculate derivative wrt pi), on the ground that the question has no meaning rather than a zero answer.0is the smaller change and reads consistently with the rest ofDifferentiate; refusing says more. Worth deciding rather than leaving it at-1.Where
Variable.InnerDifferentiatecompares the node with the target — aConstantequals aConstantof the same name, so it answers1for the constant itself and the chain rule takes it from there. A guard atDifferentiate(Variable)for aConstanttarget would cover every route in one place.