astGrad icon indicating copy to clipboard operation
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.