Text provided under a Creative Commons Attribution license, CC-BY, code provided under an MIT license. (c) Aron J. Ahmadia, Lorena A. Barba, 2014. Thanks: Gilbert Forsyth and Olivier Mesnard, and NSF for support via CAREER award #1149784.
Let us consider a particle sliding on a parabola which has a profile of:
\begin{align} y = x^2 \end{align}The coordinates of the particle are shown in Figure 1.
In [3]:
SVG("./Figure_1.svg")
Out[3]:
The reference frames of the particle can be defined by an inertial reference frame $I = (O, \vec{e}_x, \vec{e}_y)$ and a local reference frame $B = (P, \vec{e}_t, \vec{e}_n)$ as shown in Figure 2.
In [4]:
SVG("./Figure_2.svg")
Out[4]:
We can then draw a free-body diagram for this particle as depicted in Figure 3.
Figure 3: Free-body diagram
In [5]:
SVG("./Figure_3.svg")
Out[5]:
The total force includes the gravitational force $-m_pg\vec{e}_y$ and the resultant force $N\vec{e}_n$.
The kinematics of the particle can be written as the following.
The position vector of the particle in the inertial reference frame is:
\begin{align} \vec{r} = x\vec{e}_x + y\vec{e}_y = x\vec{e}_x + x^2\vec{e}_y \end{align}The velocity vector of the particle is therefore:
\begin{align} \vec{v} = \dot{x}\vec{e}_x + \dot{y}\vec{e}_y = \dot{x}\vec{e}_x + 2x\dot{x}\vec{e}_y \end{align}The acceleration vector of the particle can be derived as:
\begin{align} \vec{a} = \ddot{x}\vec{e}_x + 2\left(\dot{x}^2 + x\ddot{x}\right)\vec{e}_y \end{align}Thus, according to Newton's Second Law, we have:
The tangent direction unit vector shown in Figure 2 can be related to the velocity vector through $\vec{e}_t = \frac{\vec{v}}{||\vec{v}||}$
One can compute the unit normal vector:
\begin{align} \vec{e_n} = \vec{e_z} \times \vec{e}_t = \frac{-2x\vec{e}_x + \vec{e}_y}{\sqrt{1+4x^2}} \end{align}Equation (1) can now be split into two scalar equations:
Equation (4) can be split into two first-order ODEs
In [1]:
# As usual, we begin by importing the NumPy package
import numpy as np
# Then, we define the gravitational constant
g = 9.8
# Now, we define the derivative of our vector Q
def Q_Dot(Q):
"""Calculate the derivative of Q"""
x, u = Q[0], Q[1]
x_dot = u
u_dot = -2*x*(g+2*u**2)/(1+4*x**2)
return np.asarray([x_dot, u_dot])
Go ahead and execute the above cell fragment. None of the three actions (importing a package, defining a variable, defining a function) produce any output, so IPython lets us know that the cell has executed by adding a number between the In [X]
marker to the left of the cell.
Recall that to execute a code fragment, we need to activate the cell by clicking on it, then either pressing the "Play" button, or holding down the Shift key and pressing Enter.
Let's check our derivative function at a few initial conditions. We'll do that with a simple test function that simply prints input and output values. Once we've defined the test function, we can easily use it for a range of inputs without having to type the same print statements again and again.
In [2]:
def check_Q_Dot(x_0, u_0):
"""Test function for Q_Dot"""
print "initial velocity : %g" % x_0
print "initial acceleration : %g" % u_0
x_dot, u_dot = Q_Dot([x_0, u_0])
print "computed velocity derivative : %g" % x_dot
print "computed acceleration derivative : %g" % u_dot
First, if the parabola is at rest ($x=0, u=0$), we don't expect there to be any changes in velocity or acceleration.
In [3]:
check_Q_Dot(0.0, 0.0)
Good! Note that the acceleration derivative is zero, but has a negative sign caused by the slight rounding in our computations. Let's slightly improve our test. The computed velocity derivative should always be equal to the current acceleration, so let's make it an error when it doesn't!
In [4]:
def check_Q_Dot(x_0, u_0):
"""Test function for Q_Dot"""
print "initial velocity : %g" % x_0
print "initial acceleration : %g" % u_0
x_dot, u_dot = Q_Dot([x_0, u_0])
print "computed velocity derivative : %g" % x_dot
print "computed acceleration derivative : %g" % u_dot
assert(x_dot == u_0)
check_Q_Dot(0.0, 2.0)
In [5]:
#check_Q_Dot( , )
Now that we have our transformed our second-order scalar ODE into a system of first-order ODEs, we can solve this system using a numerical integrator.
The simplest integrator is Euler's method. Recall that if we know that:
\begin{align} \dot{y}(t) &= f(t,y(t)) \\ y(t_0) &= y_0 \end{align}then we can solve for $y$ at a set of discrete time steps of size $h$, such that $t_n = t_0 + n*h$. Euler's method relies on the recurrence relation:
\begin{align} y_{n+1} = y_n + hf(t_n,y_n) \end{align}
In [6]:
def euler_step(f, y_n, n, h):
"""Take a single step using Euler's method.
y_n - The current value of the vector y at t=n
f - function with with calling form f(y,t), computes the derivative of y at t
"""
y_dot = f(y_n, n)
y_n_plus_1 = y_n + h*y_dot
return y_n_plus_1
As always, we verify with a test example. Let's assume we are observing simple particle motion, with initial position $y(0)=0$ and constant velocity $\dot{y}(t) = 10$. If we take a time step $h=0.5$, we expect $y(0.5) = 5$.
In [7]:
def constant_velocity_10(y_n, n):
"""Derivative of particle's position that is at a constant velocity of 10"""
return 10
euler_step(constant_velocity_10, 0, 0, 0.5)
Out[7]:
We could have also used a lambda
statement here to define the constant_velocity_10
function, but function definitions in Python are so easy that we just created another one!
Now that we have a way to take a single time step, we'd like to be able to take several of them in order to calculate $y$ at some final time, $t_{final}$. To do that, we'll need to use a for loop, once for each time step we'd like to take. More sophisticated numerical integration techniques take time steps of varying length, but for this exercise we'll take $n=\frac{t_{final}-t_0}{h}$ Euler steps.
In [8]:
def simple_integrator(f, y_0, t, h):
"""Compute trajectory of values of y using Euler's method
f - Computes the derivative of y at t, with calling form f(y,t)
y_0 - Initial value of vector y
t - t_0, t_final (distance should be a multiple of h)
h - Requested distance between time steps
"""
t_0, t_final = t
# n - number of steps to take
n = int((t_final-t_0)/h)
# d - dimension of the problem, equal to number of values in vector y
d = len(y_0)
# y_traj - trajectory of y at each step i, represented as [i,:]
# The trajectory stores y at every time, including the initial time t_0.
# y_traj will store the output of $n$ steps + 1 for the initial value.
y_traj = np.zeros((n+1,d))
# t - complete set of values of t at each step i
t = np.linspace(t_0, t_final, n+1)
y_traj[0,:] = y_0
for i in range(n):
y_traj[i+1,:] = euler_step(f, y_traj[i,:], t[i], h)
return t, y_traj
Whew, that might seem like a lot of code, but the majority of the code fragment is comments, and as long as you're comfortable with for loops in Python and using np.zeros
to create empty arrays and np.linspace
to create sets of equally spaced points, this function should make sense.
Let's test it out, using a visual inspection this time. Again, the following two lines of code tell IPython that we want to view our output figures in the Notebook and that we'll be using plt
as shorthand for matplotlib.pyplot
.
In [9]:
%matplotlib inline
import matplotlib.pyplot as plt
One final trick. Our general integrator calls functions that might do something with the current time (that is, they are functions of $y(t)$ and $t$). Since our Q_Dot
only relies on $y$, we need to create an adapter function around Q_Dot
that acts like it will use $t$, but really doesn't. Here's how to do that:
In [10]:
def Q_Dot_t(y, t):
"""Adapter function to allow us to use general integrator with Q_Dot"""
return Q_Dot(y)
The sign of well-designed code is that all of the complexity is hidden from the rest of the program. Here's our call to simple_integrator
for a test problem with initial velocity of 0.5 and initial acceleration of 0.5 from time 0 to 3, with time steps of size 0.001.
In [11]:
t, Q_Traj = simple_integrator(Q_Dot_t, [0.5, 0.5], [0, 3], 0.001)
plt.plot(t, Q_Traj[:,0]);
In [12]:
from IPython.core.display import HTML
def css_styling():
styles = open("../styles/custom.css", "r").read()
return HTML(styles)
css_styling()
Out[12]:
In [1]:
from IPython.core.display import SVG
If needed, you can modify any of the Figures used in this IPython Notebook by modifying the Asymptote code below. This code is not distributed to students.
In [13]:
%install_ext http://raw.github.com/jrjohansson/ipython-asymptote/master/asymptote.py
In [14]:
%load_ext asymptote
In [15]:
%%asymptote Figure_1
defaultpen(1.5);
import graph;
size(200,200);
real f(real x) {return x^2;}
pair F(real x) {return (x,f(x));}
xaxis();
yaxis();
draw(graph(f,-2.5,2.5,operator ..));
pair zx=(2,0);
pair zy=(0,4);
pair P=(2,4);
dot("$P$",P);
draw(zx--P,red+dashed);
draw(zy--P,red+dashed);
label("$x$",(0.5*zx.x,4),N);
label("$y$",(2,0.5*zy.y),E);
Out[15]:
In [16]:
%%asymptote Figure_2
defaultpen(1.5);
import graph;
size(200,200);
real f(real x) {return x^2;}
pair F(real x) {return (x,f(x));}
xaxis("$\vec{e}_x$");
yaxis("$\vec{e}_y$");
draw(graph(f,-2.5,2.5,operator ..));
pair P=(2,4);
pair et=P+0.5*(1,4);
pair en=P+0.5*(-4,1);
dot("$P$",P);
draw(P--et,blue,EndArrow(size=10));
label("$\vec{e}_t$", et, SE, blue);
draw(P--en,blue,EndArrow(size=10));
label("$\vec{e}_n$", en, 3*E, blue);
Out[16]:
In [17]:
%%asymptote Figure_3
defaultpen(1.5);
import graph;
size(200,200);
real f(real x) {return x^2;}
pair F(real x) {return (x,f(x));}
draw(graph(f,-2.5,2.5,operator ..));
pair P=(2,4);
pair eg=P+0.5*sqrt(17)*(0,-1);
pair en=P+0.5*(-4,1);
dot("$P$",P);
draw(P--eg,blue,EndArrow(size=10));
label("$-m_pg\vec{e}_y$", eg, SE, blue);
draw(P--en,blue,EndArrow(size=10));
label("$N\vec{e}_n$", en, 3*E, blue);
Out[17]: