Autodif workshop demo¶
This demo is developed for the GODOT workshop and demonstrates how to carry out simple computations with GODOT automatic differentiation module.
import numpy as np
We can write simple functions using numpy
def f(x, y):
return np.sin(x) / (x + y)
a = 1.0
b = 2.0
c = f(a, b)
print(c)
Elementary Example¶
We can create two scalar ad
variables in godot:
from godot.core import autodif as ad
a = ad.Scalar(1.0, "a")
b = ad.Scalar(2.0, "b")
print(a)
We can do operations between these
a + b
a * b
And call mathematical functions on them
ad.sin(a)
ad.atan2(a, b)
We can redefine our function with ad
support.
# def f(x, y):
# return np.sin(x) / (x + y)
def g(x, y):
return ad.sin(x) / (x + y)
c = g(a, b)
print(c)
You can also call the new function with normal floating point.
g(1.0, 2.0)
We can inspect the value and gradient object separately as well as specific partials
print(c.value())
print(c.gradient())
print(c.gradient()["a"])
To test, we can numerically compute the derivative with respect to p
def num_diff(x, y, h):
return (f(x + h, y) - f(x - h, y)) / (2 * h)
eps = [10.0**(-n) for n in np.arange(0.5, 15, 0.1)]
res = [abs(num_diff(a.value(), b.value(), h) - c.gradient()["a"][0]) for h in eps]
import matplotlib.pyplot as plt
plt.loglog(eps, res)
plt.xlabel("Numerical difference step")
plt.ylabel("Relative error")
plt.grid()
plt.show()
We can also do the same with vectors:
v = ad.Vector([1, 2, 3], "v")
w = ad.Vector([-2, 1, -1], "w")
print(v)
We can do operations like dot product
ad.dot(v, w)
Numerical Integration¶
We can also use autodiff in more complex situations. As an example, we define a damped oscillator ODE
$$\ddot{x} + b \dot{x} + kx = 0$$We write this as a first order vector ODE:
$$\dot{x} = v$$$$\dot{v} = - bv - kx$$k = 1.0
b = 0.1
def f(t_, x):
v = x[1]
a = -k * x[0] - b * x[1]
return ad.concatenate(v, a)
We write a Runge Kutte 4th order stepper, and integrate the equation above:
# Runge-Kutte Order 4 stepper
def rk4(t ,x, f, h):
k1 = f(t, x)
k2 = f(t + h/2, x + h * k1 / 2)
k3 = f(t + h/2, x + h * k2 / 2)
k4 = f(t + h, x + h * k3)
return x + h * (k1 + k2 + k3 + k4) / 6
# Fixed step size propagation of the oscillator state
def propagate(x0, f, t0, t1, h):
t = np.arange(t0, t1, h)
x = [x0]
for i in t[1:]:
x.append(rk4(i, x[-1], f, h))
return t, x
h = 0.1
x0 = ad.Vector([0, 1], "x0")
t, x = propagate(x0, f, 0, 100, h)
Now we plot the results
import matplotlib.pyplot as plt
plt.rcParams['figure.figsize'] = [12, 6]
def plot(times,state):
pos = [s[0] for s in state]
vel = [s[1] for s in state]
fig, (ax1, ax2) = plt.subplots(1,2)
ax1.grid()
ax1.plot(times,[p.value() for p in pos],label='pos')
ax1.plot(times,[p.value() for p in vel],label='vel')
ax1.set_xlabel('Time')
ax1.axhline(0.1, ls='--', lw='1', color='r')
ax1.axvline(20, ls='--', lw='1', color='r')
ax1.legend()
ax2.grid()
ax2.plot(
[p.value() for p in pos],
[p.value() for p in vel]
)
ax2.set_xlabel('Pos')
ax2.set_ylabel('Vel')
ax2.set_aspect('equal', adjustable='box')
plot(t,x)
But we also have the state transition matrix computed:
x_final = x[-1]
print(x_final)
Now, I redefine b to be an autodiff variable and recompute:
k = ad.Scalar(1.0, "k")
b = ad.Scalar(0.1, "b")
t, x = propagate(x0, f, 0, 100, h)
x_final = x[-1]
print(x_final)
# print the value
value = x_final.value()
print( f"The value is: \n{value}\n" )
# print the state transition matrix
phi = x_final.at("x0")
print( f"The STM is \n{phi}\n" )
# print the state transition matrix
psi = x_final.at(["k", "b"])
print( f"The STM with respect to parameters is \n{psi}\n" )
# print the full state transition matrix
phi2 = x_final.at(["x0", "k", "b"])
print( f"The full STM is \n{phi2}\n" )
We can use this to compute an impulse at some time to control the state at a future time, for example.
We can perform optimisations, estimations, etc.
As a simple example, we control the damping parameter 'b' to fix the amplitude at time t = 20.
for i in range(5):
# get the index of t = 20
index = int(20 / h)
x[index]
# fix the target amplitude value
x_target = 0.1
# extract the gradient of the amplitude at the index with respect to b.
dx_db = x[index][0].at("b")
# perform the linear update to b
db = (x_target - x[index].value()) / dx_db
b += db[0]
# re-propagate and plot
t, x = propagate(x0, f, 0, 100, h)
# check the value of x at the index
print(f"The amplitude is {x[index].value()[0]} at t = 20")
plot(t,x)