Pendulum propagator example

Introduction

This example builds on the understanding of the previous one (Scalar Propagator example). Please take a look at it first before continueing here.

The current example shows how to propagate the motion of a pendulum under the force of gravity.

Setup

Import basic python and additional godot packages.

In [1]:
import numpy as np

%matplotlib inline
import matplotlib.pyplot as plt

Import basic godot packages regarding automatic differentiation.

In [2]:
from godot.core import num, tempo
from godot.core import autodif as ad
from godot.core.autodif import bridge as br

Import model packages for propagation.

In [3]:
from godot.model import common, prop
In [4]:
# optionally avoid verbose logging messages
import godot.core.util as util
util.suppressLogger()

Definition of differential equation

Here we define a class that implement the right-hand-side for integrating the motion of the pendulum. We parametrize the equation with a constant, omega, which is the gravitational force over the length of the pendulum:

x'' = -omega * sin(x)

where x denotes the angular displacement of the pendulum mass from the resting position, and x'' denotes the second derivative of x. This is second order differential equation which we need to write as two first order equations as following:

x' = y
y' = -omega * sin(x)

The class has to implement the common.VectorTimeEvaluable interface to be able to interact with the propagator.

In [5]:
class pendulumRHS(common.VectorTimeEvaluable):

    def __init__(self, vec, omega):
        # We have to call C++ trampoline class constructor explicitly
        common.VectorTimeEvaluable.__init__(self)

        # Assign member variables
        self.__vec = vec
        self.__omega = omega

    def eval(self, epoch):
        # Computes derivative of d/dx sin(w * t) = w * cos(w * t)
        x = self.__vec.eval(epoch)
        omega = self.__omega.eval(epoch)
        return br.concatenate([omega * x[1], -omega * br.sin(x[0])])

Script to setup propagator

We need to carefully setup the propagator inputs, initial states and differential equations for each thing(tm) we want to propagate. In this example there is only one vector quantity.

In [6]:
vec = prop.PropagatorVectorTimeEvaluable()

First, we setup the initial conditions, epoch and state. The state is created as a time evaluable object as required by the propagator input.

In [7]:
epoch0 = tempo.Epoch(0.0, tempo.TimeScale.TDB)
x0 = ad.Vector([0.0, 1.0], 'x0')
x0Func = common.ConstantVectorTimeEvaluable(x0)

We setup another parameter for the (linear) frequency of the sine functions, which is defined in 1/s = Hz. We choose a (linear) frequency equivalent to one day.

In [8]:
omega = ad.Scalar(1.0 / 86400.0, 'omega')
omegaFunc = common.ConstantScalarTimeEvaluable(omega)

Finally, we create the RHS object.

In [9]:
rhs = pendulumRHS(vec, omegaFunc)

With these we can create the propagator input that defines the quantity to propagate. We also need to define the relative and absolute propagation error tolerances.

In [10]:
rtol = [1e-9, 1e-9]
atol = [1e-9, 1e-9]
inp = [prop.VectorInput(vec, x0Func, rhs, rtol, atol)]

At this point the propagator can be created using this input. We also define a parameter for the delta in initial epoch.

In [11]:
dt0 = ad.Scalar(0.0, 'dt0')
dt0Func = common.ConstantScalarTimeEvaluable(dt0)
propagator = prop.Propagator('Pendulum', epoch0, dt0Func, inp)

Propagate the differential equation

To compute the propagation, we define the final epoch for the propagation. We choose two whole periods for the sine function, and carry out the propagation. We also specify with False flag that we do not wish to compute partial derivatives.

In [12]:
T = num.TwoPi / omega.value()
n = 2
epoch = epoch0 + n * T
propagator.compute(epoch, False)

To retrieve a certain propagated quantity to be evaluated, we use the output from the propagator.

In [13]:
x = vec.eval(epoch)
print("Angular displacement [rad] = ", x[0])
print("Angular velocity [rad/s] = ", x[1])
Angular displacement [rad] =  -0.7997163474419415
Angular velocity [rad/s] =  0.6275510527598043

The obtained value for the angular displacement is not zero, because the RHS is non-linear in x. If the sine function on the RHS had been approximated by x, the final state would indeed be zero since we had propagated for two (linear) periods.

In [ ]:

Plotting the solution

To get a full picture of the evolution of the angle, we can generate data and plot it for the range of the propagation. We will define a function to generate the solution as we will reuse this later.

In [14]:
def generatePlotData(pro):
    grid = pro.range().createGrid(60.0)
    t = [e.mjd() for e in grid]
    x = np.asarray([vec.eval(e) for e in grid])
    return t, x

We plot it using matplotlib.

In [15]:
t, x = generatePlotData(propagator)
plt.plot(t, x[:, 0], label = 'Angular displacement')
plt.plot(t, x[:, 1], label = 'Angular velocity')
plt.xlabel('Time (days)')
plt.legend()
plt.grid()
plt.show()

The plot shows that indeed the period of the solution is larger than 2 pi/omega because of the nonlinear RHS.

Propagate including partial derivatives

To compute partial derivatives we simplfy have to repropagate with partials flag set to True. The same final state is computed.

In [16]:
propagator.compute(epoch, True)
xepoch = tempo.XEpoch(epoch)
x = vec.eval(xepoch)
print(x)
         Value | Pendulum @ 2000-01-13T13:35:34.421 TDB[0] Pendulum @ 2000-01-13T13:35:34.421 TDB[1]
 -7.997163e-01 |                              1.000000e+00                              0.000000e+00
  6.275511e-01 |                              0.000000e+00                              1.000000e+00

To get the partial derivatives of the final state, we have to create and XEpoch and use that for the evaluation.

In [17]:
y = common.resolve(x)
print(y)
         Value |         x0[0]         x0[1] |         omega |           dt0
 -7.997163e-01 |  6.275511e-01 -2.168142e+00 |  6.813538e+05 | -7.263322e-06
  6.275511e-01 |  7.171584e-01 -8.842335e-01 |  7.786436e+05 | -8.300445e-06

The resolve() method computes the partial derivatives w.r.t. the leaf, x0. There are mainly two reasons why common.resolve is needed:

  1. x0 might depend on a large number of other parameters (leafs). To minimise the dimension of the Parameter-transition matrix right-hand-side, they are not considered during propagation, but need to be resolved to after propagation
  2. One might also be interested in partials of x w.r.t. x(t1) where t1 is between the initial and final propagation epoch (e.g. for OD). The decision on which epoch the user is interested is delayed until the partials are resolved

Changing parameter values

To modify the value of a parameter we can simply use the provided interface of the constant time evaluable object.

In [18]:
x0Func.set([0.5, -1.0])
dt0Func.set(86400)

Then, we can simplfy propagate again the solution which will use these changes.

In [19]:
propagator.compute(epoch, False)
t, x = generatePlotData(propagator)
plt.plot(t, x[:, 0], label = 'Angular displacement')
plt.plot(t, x[:, 1], label = 'Angular velocity')
plt.xlabel('Time (days)')
plt.legend()
plt.grid()
plt.show()