[ math ]ninjasha 2c8e64ameasured-in-repo
I differentiate calculus exactly, depending on nothing.
ninja is a from-scratch computer algebra system in pure Python. Its core is a symbolic differentiation engine that parses an expression as text and returns the exact derivative, simplified, never numeric. No SymPy, no NumPy, no parser library in the symbolic core.
- [ ninja ], noun
- 1.a pure-Python computer algebra system, no deps in the core
- 2.parse, derive, print, simplify: symbolic, not numeric
- 3.exact rational coefficients, checked rule by rule
- engine size
- 711
- dependencies
- 0
- correctness
- 11/11
- lang
- Python
- license
- GPL-3.0
- size
- 1,430 LOC / 5 modules
- baseline
- hand-derived answer
- field std
- SymPy (sympy.diff)
- verdict
- measured-in-repo
the hardest rule, made physical
x^x has no power rule and no exponential rule. The engine differentiates it by logarithmic differentiation, two terms summed, every coefficient kept exact.
x^x has no plain power rule and no plain exponential rule, so the power node differentiates it by logarithmic differentiation: two terms, x^x · ln(x) and x · x^(x−1), summed and printed exact as x^x*ln(x)+x*x^(x−1).Most math toolkits wrap SymPy or stop at numbers
Most "I built a math toolkit" projects wrap SymPy or NumPy, or they stop at plugging numbers into a formula. I wanted the hard part: take a math expression as a plain string, build it into a tree, and differentiate it symbolically and exactly, then print the simplified result back as math.
Exactly means the coefficients stay integer or rational the whole way through. D("(x^2+1)^3") has to come out as 6x^5+12x^3+6x, not a float approximation that drifts in the last digit. That constraint rules out the easy paths and forces real algebra.
An algebraic type system over exact rationals
I model an expression as a small algebraic type system: Polynomial (a rational function carried as numerator and denominator coefficient lists), plus trig, log, ln, power, and a mixed_func node for the + − * / operators. A handwritten recursive-descent parser builds the tree, splitting on operator precedence with parenthesis-balance tracking, and it catches implicit multiplication like 2x, (..)(..), and x sin x.
Each node implements two methods. derive() encodes one calculus rule; ptf() (print-to-format) renders the node back to a string. The trig, log, and ln nodes apply the chain rule directly: sin maps to cos(arg) · arg', tan to arg' / cos², and a power tower f^g uses logarithmic differentiation. Underneath, a hand-rolled fraction layer and a polynomial GCD by Euclid keep every coefficient exact.
One pipeline, five stages, five flat modules
The engine is one pipeline in differentiate.py: a string goes in, an exact simplified derivative comes out. The whole project is five flat modules. The engine carries most of the weight. The rest is the rendering, plotting, and number-theory surface around it.
- string to node tree, recursive-descent
- canonicalize the tree
- per-node calculus rule
- print-to-format
- string cleanup
sin(x^2) example below. Recolored from the repo's own architecture diagram at sha 2c8e64a.Choices that cost robustness, kept in on purpose
The whole point was to learn the math by building it, so I made choices that cost robustness on purpose. I kept them in. Each names what I chose and what it costs.
- [01]road not taken
I rewrote rational arithmetic by hand
- [02]road not taken
Simplification rewrites strings, not a normalized tree
- [03]road not taken
It is an exploration, not a packaged library
There is no in-repo speed benchmark against SymPy. The evidence is correctness.
Checked against the answer I derived by hand
The field-standard pure-Python CAS is SymPy, and ninja's differentiator is a from-scratch alternative to sympy.diff.
I ran D() on 11 expressions that exercise every rule the engine implements, then set each output beside the answer I derived by hand. All 11 match, and the coefficients stay integer or rational throughout. Latency is from time.perf_counter on CPython, single thread, shown only as supporting detail.
11/11 expressions match the hand-derived answer
coefficients integer / rational · no float drift
| input | D(input) = engine output | rule | vs answer | µs/call |
|---|---|---|---|---|
| x^3+2*x | 3x^2+2 | power + sum | match ✓ | 295.1 |
| 3*x^4-5*x^2+7 | 12x^3-10x | power + difference | match ✓ | 463.4 |
| (x^2+1)^3 | 6x^5+12x^3+6x | chain (expanded exact) | match ✓ | 295.3 |
| sin(x^2) | 2x*cos(x^2) | chain + trig | match ✓ | 123.5 |
| cos(x) | -sin(x) | trig | match ✓ | 61.4 |
| x*ln(x) | 1+ln(x) | product + log | match ✓ | 146.7 |
| ln(x) | 1/x | log | match ✓ | 64.4 |
| x^2/(x+1) | (x^2+2x)/(x^2+2x+1) | quotient (GCD-reduced) | match ✓ | 323.1 |
| tan(x)/x | ((x)/((cos(x))^2)-tan(x))/(x^2) | quotient + trig | match ✓ | 176.3 |
| x^x | (x)^(x)*ln(x)+x*(x)^(x-1) | power-tower (log-diff) | match ✓ | 269.7 |
| x^5 | 5x^4 | power | match ✓ | 166.0 |
The code that proves it is real algebra
Five places where the engine does the work a wrapper skips. Each links into the source at the pinned SHA.
Run D() yourself, from the source
Every derivative on this page came from running the real D(). It has no hosted playground, only the function itself, callable straight from the source.