astGrad
astGrad copied to clipboard
Symbolic differentiation based on the Nim AST
- astGrad - Symbolic differentiation based on the Nim AST
This library performs symbolic differentiation based on the nodes of the Nim AST. This allows for compile time generation of derivatives to avoid approximations due to numerical methods.
Note: as this is dealing with symbolic differentiation and the code isn't extremely smart about doing simplification yet, the resulting function can become relatively large on higher orders. For example the 8th derivative of =tanh(x)= produces about ~7000 lines of code...
For lower orders it works perfectly fine though and as the additional code generation is exponential in nature, even something like the 5th order is still reasonable in case of =tanh= (O(70) lines).
Some simple simplification (of same addition / subtraction terms, multiplication / division) should help a lot.
** Usage
Using this library is pretty straightforward. There is essentially only a single public macro: #+begin_src nim macro derivative(arg, wrt: untyped): untyped #+end_src
This macro takes a Nim expression and a symbol to differentiate by.
For example: #+begin_src nim echo derivative(x * x, x) == 2 * x echo derivative(x * y, y) == x echo derivative(exp(x), x) == exp(x) echo derivative(sin(x) * cos(μ)^2 + exp(-((x - μ)^2) / (2 * σ^2)), μ) #... #+end_src you get the idea. Of course every symbol used in the expression for which the derivative is to be computed must exist in the Nim code (otherwise you get "undeclared identifier" errors after the macro computed your gradient).
In addition there is a helper template: #+begin_src nim template ∂(arg, wrt: untyped): untyped #+end_src to make the code a bit more pretty. In addition to this template higher order versions are defined using superscript unicode, e.g. =∂²=, =∂³= etc.
Feel free to wrap the call in a procedure to generate the full gradient procedure: #+begin_src nim proc gradSin(x: float): float = result = ∂(sin(x), x)
doAssert gradSin(-Pi) == cos(-Pi) doAssert gradSin(0.0) == cos(0.0) doAssert gradSin(Pi/2.0) == cos(Pi/2.0) #+end_src
Feel free to go crazy on your derivatives.
Note: the library currently has no introspection functionality to compute derivatives of user defined functions. For a purely mathematical procedure it should be rather straight forward. More complex statements are not really the goal of this library. Its aim is to provide a convenient way to generate gradients of functions that a) one is too lazy to write down or b) that might already be a bit annoying to compute by hand.
** Extra fun
Guess what we can do 😎:
#+begin_src nim import unchained import scinim/experimental/sugar # just used for the mathScope macro
mathScope: f(t, a) = ∂(1.0/2.0 * a * t^2, t) echo "Speed after ", 1.s, ": ", f(1.0.s, 9.81.m•s⁻²) echo "Speed after ", 2.s, ": ", f(2.0.s, 9.81.m•s⁻²) echo "Speed after ", 2.s, ": ", f(3.0.s, 9.81.m•s⁻²)
Speed after 1 Second: 9.81 Meter•Second⁻¹
Speed after 2 Second: 19.62 Meter•Second⁻¹
Speed after 2 Second: 29.43 Meter•Second⁻¹
#+end_src
Doing gradients with units? Pretty neat, huh?
You want more? What if you have some measurement uncertainties associated with your time values? And maybe include the variation of =g= around the world?
#+begin_src nim import measuremancer
And guess what if you have some measurement errors on top of your
measurement?
echo "Speed after ", 1.s, ": ", f(1.0.s ± 0.05.s, 9.81.m•s⁻² ± 0.03.m•s⁻²) echo "Speed after ", 2.s, ": ", f(2.0.s ± 0.05.s, 9.81.m•s⁻² ± 0.03.m•s⁻²) echo "Speed after ", 2.s, ": ", f(3.0.s ± 0.05.s, 9.81.m•s⁻² ± 0.03.m•s⁻²)
Speed after 1 Second: 9.81 ± 0.491 Meter•Second⁻¹
Speed after 2 Second: 19.6 ± 0.494 Meter•Second⁻¹
Speed after 2 Second: 29.4 ± 0.499 Meter•Second⁻¹
#+end_src
Yup.