Dr. Joshua Vaughan
joshua.vaughan@louisiana.edu
http://www.ucs.louisiana.edu/~jev9637/
In this example, we'll determine the equations of motion for the planar crane model shown in Figure 1. The system consists of a point mass, $m$, connected to the crane trolley, which has mass $m_t$, by an ideal pin via a massless rod of length $l$. The rotation of the suspension cable is represented by $\theta$. There is a force, $f$, acting on the crane trolley.
Figure 1: Planar Crane Model
# Import the SymPy Module
import sympy
# Import the necessary sub-modules and methods for dynamics
from sympy.physics.mechanics import dynamicsymbols
from sympy.physics.mechanics import LagrangesMethod, Lagrangian
from sympy.physics.mechanics import Particle, Point, ReferenceFrame
# initiate better printing of SymPy results
sympy.init_printing()
# Define the genearlized coordinates and other dynamic symbols
x, theta, f = dynamicsymbols('x theta f')
# Also define the first derivatives
x_dot, theta_dot = dynamicsymbols('x theta', 1)
# Define the symbols for the other paramters
m, mt, g, l, t = sympy.symbols('m, m_t, g, l, t')
# Define the Newtonian reference frame
N = ReferenceFrame('N')
# Define a body-fixed frame along the pendulum, with y aligned from m to the pin
A = N.orientnew('A', 'Axis', [theta, N.z])
# Define the trolley point and its velocity
T = Point('T')
T.set_vel(N, x_dot * N.x)
# Treat the trolley as a particle
mtr = Particle('mtr', T, mt)
# Define the payload point and set its velocity
P = Point('P')
P.set_vel(N, x_dot * N.x + l * theta_dot * A.x)
# The payload is a particle (a point mass)
mp = Particle('mp', P, m)
# Define the potential energy of the payload - just gravity here
mp.potential_energy = -m * g * l * sympy.cos(theta)
# Set up the force list - each item follows the form:
# (the location where the force is applied, its magnitude and direction)
forces = [(T, f * N.x)]
# Form the Lagrangian, then simplify and print
L = Lagrangian(N, mtr, mp)
L.simplify()
# This creates a LagrangesMethod class instance that will allow us to form the equations of motion, etc
LM = LagrangesMethod(L, [x, theta], forcelist = forces, frame = N)
LM.form_lagranges_equations()
The LagrangesMethod class gives us lots of information about the system. For example, we can output the mass/inertia matrix and the forcing terms. Note that the forcing terms include what might be conservative forces and would therefore normally appear in a stiffness matrix.
# Output the inertia/mass matrix of the system
LM.mass_matrix
# Output the forcing terms of the system
LM.forcing
We can also use builtin functions to write the sytsem as a set of first order ODEs, suitable for simluation.
# Make the call to set up in state-space-ish form q_dot = f(q, t)
lrhs = LM.rhs()
# Simplify the results
lrhs.simplify()
# Output the result
lrhs
We can also linearize these equations with builtin SymPy methods. Let's do so about the operating point:
$$ x = \dot{x} = \theta = \dot{\theta} = 0 $$The resulting equations returned are a system of first order ODEs in state-space form:
$$ \dot{x} = Ax + Bu $$See the SymPy Documentation for much more information.
# Define the point to linearize around
operating_point = {x: 0.0, x_dot: 0.0, theta: 0.0, theta_dot: 0.0}
# Make the call to the linearizer
A, B, inp_vec = LM.linearize([x, theta],
[x_dot, theta_dot],
op_point = operating_point,
A_and_B = True)
A
B
Given these two matrices, the system of equations match those that we'd expect:
$$ \begin{bmatrix}\dot{w}_1 \\ \dot{w}_2 \\ \dot{w}_3 \\ \dot{w}_4\end{bmatrix} = A \begin{bmatrix}w_1 \\ w_2 \\ w_3 \\ w_4 \end{bmatrix} + B f(t)$$where:
$$ \mathbf{w} = \begin{bmatrix}w_1 \\ w_2 \\ w_3 \\ w_4 \end{bmatrix} = \begin{bmatrix}x \\ \theta \\ \dot{x} \\ \dot{\theta}\end{bmatrix} $$We can pass these equations of motion to numerical solver for simluation. To do so, we need to import NumPy and the SciPy ode solver, ode
. We'll also import matplotlib to enable plotting of the results.
For a system as simple as this one, we could easily set up the necessary components for the numerical simulation manually. However, here will automate as much as possible. Following a similar procedure on more complicated systems would be necessary.
# import NumPy with namespace np
import numpy as np
# import the ode ODE solver
from scipy.integrate import ode
# import the plotting functions from matplotlib
import matplotlib.pyplot as plt
# set up the notebook to display the plots inline
%matplotlib inline
Let's define the forcing function, so that it can be substituted into the generated equations of motion. Here, we'll use a simple bang-bang input.
# define the forcing function
def force(t):
# set the maximum force output
fmax = 10.0
# return a bang-bang command
return fmax * ((t > 0.25) - 2 * (t >= 1.25) + (t >= 2.25))
Let's plot the input to make sure it matches the bang-bang profile we precribed.
# Set the plot size - 3x2 aspect ratio is best
fig = plt.figure(figsize=(6, 4))
ax = plt.gca()
plt.subplots_adjust(bottom=0.17, left=0.17, top=0.96, right=0.96)
# Change the axis units to serif
plt.setp(ax.get_ymajorticklabels(), family='serif', fontsize=18)
plt.setp(ax.get_xmajorticklabels(), family='serif', fontsize=18)
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
# Turn on the plot grid and set appropriate linestyle and color
ax.grid(True,linestyle=':', color='0.75')
ax.set_axisbelow(True)
# Define the X and Y axis labels
plt.xlabel('Time (s)', family='serif', fontsize=22, weight='bold', labelpad=5)
plt.ylabel('Force (N)', family='serif', fontsize=22, weight='bold', labelpad=10)
# Plot the force over 10 seconds
force_plot_time = np.linspace(0.0, 10.0, 1001)
plt.plot(force_plot_time, force(force_plot_time), linewidth=2, linestyle='-', label=r'Force')
# uncomment below and set limits if needed
# plt.xlim(0, 5)
# plt.ylim(0, 10)
# Create the legend, then fix the fontsize
# leg = plt.legend(loc='upper right', ncol = 1, fancybox=True)
# ltext = leg.get_texts()
# plt.setp(ltext, family='serif', fontsize=20)
# Adjust the page layout filling the page using the new tight_layout command
plt.tight_layout(pad=0.5)
# Uncomment to save the figure as a high-res pdf in the current folder
# It's saved at the original 6x4 size
# plt.savefig('Bang_Bang_Force.pdf')
fig.set_size_inches(9, 6) # Resize the figure for better display in the notebook
From this plot, you can see why this type of command is called bang-bang. It "bangs" on, then "bangs" negative, then is zero for all other time.
Now, let's continue setting up the simluation by creating a version of the generated equations of motion that we can use in a numerical ODE solver.
# Define the states and state vector
w1, w2, w3, w4 = sympy.symbols('w1 w2 w3 w4', cls=sympy.Function)
w = [w1(t), w2(t), w3(t), w4(t)]
# Set up the state definitions and parameter substitution
sub_params = {x : w1(t),
theta : w2(t),
x_dot : w3(t),
theta_dot : w4(t),
mt : 10.0,
m : 1.0,
g : 9.81,
l : 2.0,
f: force(t)}
# Create a function from the equations of motion
# Here, we substitude the states and parameters as appropriate prior to the lamdification
eq_of_motion = sympy.lambdify((t, w),
lrhs.subs(sub_params))
# Set up the initial conditions for the solver
x_init = 0.0 # Initial trolley position (m)
x_dot_init = 0.0 # Initial trolley velocity (m/s)
theta_init = 0.0 * np.pi/180 # Initial angle (rad)
theta_dot_init = 0.0 # Initial angular velocity (rad/s)
# Pack the initial conditions into an array
x0 = [x_init, theta_init, x_dot_init, theta_dot_init]
# Create the time samples for the output of the ODE solver
sim_time = np.linspace(0.0, 10.0, 1001) # 0-10s with 1001 points in between
# Set up the initial point for the ode solver
r = ode(eq_of_motion).set_initial_value(x0, sim_time[0])
# define the sample time
dt = sim_time[1] - sim_time[0]
# pre-populate the response array with zeros
response = np.zeros((len(sim_time), len(x0)))
# Set the initial index to 0
index = 0
# Now, numerically integrate the ODE while:
# 1. the last step was successful
# 2. the current time is less than the desired simluation end time
while r.successful() and r.t < sim_time[-1]:
response[index, :] = r.y
r.integrate(r.t + dt)
index += 1
Now, let's plot the results. The first column of the response
vector is the position of the trolley, $x$, and the second column of the response
vector is the angle of the pendulum, $\theta$. We'll plot both below, after setting up plotting parameters to make the plot more readable.
# Plot the payload response
# Set the plot size - 3x2 aspect ratio is best
fig = plt.figure(figsize=(6, 4))
ax = plt.gca()
plt.subplots_adjust(bottom=0.17, left=0.17, top=0.96, right=0.96)
# Change the axis units to serif
plt.setp(ax.get_ymajorticklabels(), family='serif', fontsize=18)
plt.setp(ax.get_xmajorticklabels(), family='serif', fontsize=18)
# Remove top and right axes border
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
# Only show axes ticks on the bottom and left axes
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
# Turn on the plot grid and set appropriate linestyle and color
ax.grid(True,linestyle=':', color='0.75')
ax.set_axisbelow(True)
# Define the X and Y axis labels
plt.xlabel('Time (s)', family='serif', fontsize=22, weight='bold', labelpad=5)
plt.ylabel('Position (m)', family='serif', fontsize=22, weight='bold', labelpad=10)
# Plot the data
plt.plot(sim_time, response[:, 0], linewidth=2, linestyle='-', label = '$x$')
# uncomment below and set limits if needed
# plt.xlim(0, 5)
# plt.ylim(-1, 1)
# Create the legend, then fix the fontsize
# leg = plt.legend(loc='upper right', ncol = 1, fancybox=True)
# ltext = leg.get_texts()
# plt.setp(ltext, family='serif', fontsize=20)
# Adjust the page layout filling the page using the new tight_layout command
plt.tight_layout(pad=0.5)
# Uncomment to save the figure as a high-res pdf in the current folder
# It's saved at the original 6x4 size
# plt.savefig('Trolley_Response.pdf')
fig.set_size_inches(9, 6) # Resize the figure for better display in the notebook
Now, let's plot the suspension cable angle, $\theta$.
# Set the plot size - 3x2 aspect ratio is best
fig = plt.figure(figsize=(6, 4))
ax = plt.gca()
plt.subplots_adjust(bottom=0.17, left=0.17, top=0.96, right=0.96)
# Change the axis units to serif
plt.setp(ax.get_ymajorticklabels(), family='serif', fontsize=18)
plt.setp(ax.get_xmajorticklabels(), family='serif', fontsize=18)
# Remove top and right axes border
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
# Only show axes ticks on the bottom and left axes
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
# Turn on the plot grid and set appropriate linestyle and color
ax.grid(True,linestyle=':', color='0.75')
ax.set_axisbelow(True)
# Define the X and Y axis labels
plt.xlabel('Time (s)', family='serif', fontsize=22, weight='bold', labelpad=5)
plt.ylabel('Angle (deg)', family='serif', fontsize=22, weight='bold', labelpad=10)
# Plot the data
plt.plot(sim_time, response[:, 1] * 180/np.pi, linewidth=2, linestyle='-', label = '$\theta$')
# uncomment below and set limits if needed
# plt.xlim(0, 5)
# plt.ylim(-1, 1)
# Create the legend, then fix the fontsize
# leg = plt.legend(loc='upper right', ncol = 1, fancybox=True)
# ltext = leg.get_texts()
# plt.setp(ltext, family='serif', fontsize=20)
# Adjust the page layout filling the page using the new tight_layout command
plt.tight_layout(pad=0.5)
# Uncomment to save the figure as a high-res pdf in the current folder
# It's saved at the original 6x4 size
# plt.savefig('Payload_Response.pdf')
fig.set_size_inches(9, 6) # Resize the figure for better display in the notebook
We can also plot the horizontal position of the trolley and payload over time, to better understand how they interact. To do so, we need to define the horizontal position of the payload, $x_p$. It is simply the position of the trolley, $x$, plus the horizontal position of the point mass relative to it, $l\sin{\theta}$:
$$ x_p = x + l\sin{\theta} $$# Define the horizontal position of the payload
xp = response[:,0] + 2.0 * np.sin(response[:,1])
# Now plot the responses together
# Set the plot size - 3x2 aspect ratio is best
fig = plt.figure(figsize=(6, 4))
ax = plt.gca()
plt.subplots_adjust(bottom=0.17, left=0.17, top=0.96, right=0.96)
# Change the axis units to serif
plt.setp(ax.get_ymajorticklabels(), family='serif', fontsize=18)
plt.setp(ax.get_xmajorticklabels(), family='serif', fontsize=18)
# Remove top and right axes border
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
# Only show axes ticks on the bottom and left axes
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
# Turn on the plot grid and set appropriate linestyle and color
ax.grid(True,linestyle=':', color='0.75')
ax.set_axisbelow(True)
# Define the X and Y axis labels
plt.xlabel('Time (s)', family='serif', fontsize=22, weight='bold', labelpad=5)
plt.ylabel('Position (m)', family='serif', fontsize=22, weight='bold', labelpad=10)
# Plot the data
plt.plot(sim_time, response[:, 0], linewidth=2, linestyle='--', label = 'Trolley')
plt.plot(sim_time, xp, linewidth=2, linestyle='-', label = 'Payload')
# uncomment below and set limits if needed
# plt.xlim(0, 5)
plt.ylim(0, 2)
# Create the legend, then fix the fontsize
leg = plt.legend(loc='upper right', ncol = 2, fancybox=True)
ltext = leg.get_texts()
plt.setp(ltext, family='serif', fontsize=20)
# Adjust the page layout filling the page using the new tight_layout command
plt.tight_layout(pad=0.5)
# Uncomment to save the figure as a high-res pdf in the current folder
# It's saved at the original 6x4 size
# plt.savefig('TrolleyAndPayload_Response.pdf')
fig.set_size_inches(9, 6) # Resize the figure for better display in the notebook
Given our choice of masses, we see that the payload is able to back-drive the trolley, causing the trolley to move opposite of the payload once the bang-bang command is over.
Code is licensed under a 3-clause BSD style license. See the licenses/LICENSE.md file.
Other content is provided under a Creative Commons Attribution-NonCommercial 4.0 International License, CC-BY-NC 4.0.
# This cell will just improve the styling of the notebook
# You can ignore it, if you are okay with the default sytling
from IPython.core.display import HTML
import urllib.request
response = urllib.request.urlopen("https://cl.ly/1B1y452Z1d35")
HTML(response.read().decode("utf-8"))