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.
Using this library is pretty straightforward. There is essentially only a single public macro:
macro derivative(arg, wrt: untyped): untyped
This macro takes a Nim expression and a symbol to differentiate by.
For example:
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)), μ)
#...
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:
template ∂(arg, wrt: untyped): untyped
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:
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)
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.
Guess what we can do 😎:
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⁻¹
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?
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⁻¹
Yup.