#!/usr/bin/env python
# coding: utf-8
#
Mass-Spring-Pendulum Example
# MCHE 513: Intermediate Dynamics
# Dr. Joshua Vaughan
# joshua.vaughan@louisiana.edu
# http://www.ucs.louisiana.edu/~jev9637/
#
#
# Figure 1: A Mass-Spring-Pendulum System
#
#
# This notebook demonstrates the analysis of the system shown in Figure 1. Mass $m_1$ is attached to ground via a spring and constrained to move horizontally. Its horizontal motion from equilibrium is described by $x$. Mass $m_2$ is suspended from the center of $m_1$ via a massless, inextensible, inflexible cable of length $l$. The angle of this cable from horizontal is described by $\theta$. The linearized equations of motion for the system are:
#
# $ \quad \left(m_1 + m_2\right) \ddot{x} - m_2 l \ddot{\theta} + k x = f $
#
# $ \quad -m_2 l \ddot{x} + m_2 l^2 \ddot{\theta} + m_2 g l \theta = 0 $
#
# We could also write this equation in matrix form:
#
# $ \quad \begin{bmatrix}m_1 + m_2 & -m_2 l \\ -m_2 l & \hphantom{-}m_2 l^2\end{bmatrix}\begin{bmatrix}\ddot{x} \\ \ddot{\theta}\end{bmatrix} + \begin{bmatrix}k & 0 \\ 0 & m_2 g l\end{bmatrix}\begin{bmatrix}x \\ \theta\end{bmatrix} = \begin{bmatrix}f \\ 0\end{bmatrix}$
#
# We'll use the [Sympy](http://sympy.org) tools to verify these equations of motion, then [NumPy](http://numpy.org) and [SciPy](http://scipy.org) to simulate the response of this system.
# In[1]:
# 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()
# In[2]:
# Define the genearlized coordinate
x, theta, f = dynamicsymbols('x theta f')
# Also define the first derivative
x_dot, theta_dot = dynamicsymbols('x theta', 1)
# Define the symbols for the other paramters
m1, m2, k, g, l, t = sympy.symbols('m_1, m_2, k, g, l, t')
# In[6]:
# 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, m1)
# 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, m2)
# Define the potential energy of the payload
mp.potential_energy = -m2 * g * l * sympy.cos(theta) # gravity
mtr.potential_energy = 1 / 2 * k * x**2 # spring potential
# 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()
# In[7]:
# 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)
# In[8]:
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.
# In[9]:
# Output the inertia/mass matrix of the system
LM.mass_matrix
# In[10]:
# 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.
# In[11]:
# 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 $x = \dot{x} = \theta = \dot{\theta} = 0$ operating point. The resulting equations returned are a system of first order ODEs in state-space form:
#
# $$ \dot{x} = Ax + Bu $$
#
# See the [SymPy Documentation](http://docs.sympy.org/0.7.6/modules/physics/mechanics/linearize.html#linearizing-lagrange-s-equations) for much more information.
# In[12]:
# 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)
# In[13]:
# simplify and print out the A matrix
A.simplify()
A
# In[14]:
# simplify and print out the B matrix
B.simplify()
B
# ## Checking the Result
# These equations match the linearized equations of motion from the top of this notebook, if they are written as a system of first order ODEs, rather than two second-order ODEs.
# To begin, let's define a state vector $\mathbf{w} = \left[x \quad \theta \quad \dot{x} \quad \dot{\theta}\right]^T $
#
# As mentioned above, we'll most often see the state space form writen as:
#
# $ \quad \dot{x} = Ax + Bu $
#
# where $x$ is the state vector, $A$ is the state transition matrix, $B$ is the input matrix, and $u$ is the input. We'll use $\mathbf{w}$ here and in the code to avoid confusion with our state $x$, the position of $m_1$.
#
# To begin, let's write the two equations of motion as:
#
# $ \quad \ddot{x} = \frac{1}{m_1 + m_2} \left(m_2 l \ddot{\theta} - k x + f \right)$
#
# $ \quad \ddot{\theta}= \frac{1}{m_2 l^2} \left(m_2 l \ddot{x} - m_2 g l \theta\right) = \frac{1}{l}\ddot{x} - \frac{g}{l}\theta $
#
# After some algebra and using the state vector defined above, we can write our equations of motion as:
#
# $$ \dot{\mathbf{w}} = \begin{bmatrix}0 & 0 & 1 & 0\\ 0 & 0 & 0 & 1 \\ -\frac{k}{m_1} & -\frac{m_2}{m_1}g & 0 & 0 \\ -\frac{k}{m_1} & -\left(\frac{m_1 + m_2}{m_1}\right)\frac{g}{l} & 0 & 0 \end{bmatrix}\mathbf{w} + \begin{bmatrix}0 \\ 0 \\ \frac{1}{m_1} \\ \frac{1}{m_1 l} \end{bmatrix} f $$
#
# Now, we have the option of automating the creation of the functions necessary for use in the ODE solver, like we've done in [some other notebooks](http://nbviewer.ipython.org/github/DocVaughan/MCHE513---Intermediate-Dynamics/blob/master/Planar%20Crane%20with%20SymPy.ipynb). Here, let's manually generate the necessary functions to examine that case.
# In[15]:
# import NumPy with namespace np
import numpy as np
# import the ode ODE solver
from scipy.integrate import odeint
# import the plotting functions from matplotlib
import matplotlib.pyplot as plt
# set up the notebook to display the plots inline
get_ipython().run_line_magic('matplotlib', 'inline')
# In[16]:
# Define the system parameters
m1 = 10.0 # Trolley mass (kg)
m2 = 1.0 # Payload mass (kg)
g = 9.81 # Gravity (m/s^2)
k = 4 * np.pi**2 # Spring constant (N/m)
l = 2.0 # Cable length (m)
# To use ```scipy.intergrate.odeint``` we need to define a funciton that represents the system of first order differential equations we want to solve. Here, that is simply the system of equations of motion. The ```odeint``` funciton requires the the arguments of this function be ```w, t, p```, where ```w``` is the vector of states, ```t``` is the time, and ```p``` is a list of other parameters, as necessary. The function should returnn the system as a list.
#
# We'll also define the forcing function, with arguments following the order of those requried by ```odeint```.
# In[17]:
# Define the system as a series of 1st order ODES (beginnings of state-space form)
def eq_of_motion(w, t, p):
"""
Defines the differential equations for the coupled spring-mass system.
Arguments:
w : vector of the state variables:
w = [x, theta, x_dot, theta_dot]
t : time
p : vector of the parameters:
p = [m1, m2, k, l, g, wf]
Returns:
sysODE : An list representing the system of equations of motion as 1st order ODEs
"""
x, theta, x_dot, theta_dot = w
m1, m2, k, l, g, wf = p
# Create sysODE = (x', theta', x_dot', theta_dot'):
sysODE = [x_dot,
theta_dot,
-k/m1 * x - m2/m1 * g * theta + f(t,p)/m1,
-k/(m1 * l) * x - (m1 + m2)/m1 * g/l * theta + f(t, p)/(m1 * l)]
return sysODE
# Define the forcing function
def f(t, p):
"""
Defines the forcing function
Arguments:
t : time
p : vector of the parameters:
p = [m1, m2, k, l, g, wf]
Returns:
f : forcing function at current timestep
"""
m1, m2, k, l, g, wf = p
# Uncomment below for no force input - use for initial condition response
#f = 0.0
# Uncomment below for sinusoidal forcing input at frequency wf rad/s
f = np.sin(wf * t)
return f
# In[18]:
# Set up simulation parameters
# ODE solver parameters
abserr = 1.0e-9
relerr = 1.0e-9
max_step = 0.01
stoptime = 25.0
numpoints = 2501
# Create the time samples for the output of the ODE solver
t = np.linspace(0.0, stoptime, numpoints)
# Initial conditions
x_init = 0.0 # initial position
x_dot_init = 0.0 # initial velocity
theta_init = 0.0 # initial angle
theta_dot_init = 0.0 # initial angular velocity
wf = np.sqrt(k / m1) # forcing function frequency
# Pack the parameters and initial conditions into arrays
p = [m1, m2, k, l, g, wf]
x0 = [x_init, theta_init, x_dot_init, theta_dot_init]
# In[19]:
# Call the ODE solver.
resp = odeint(eq_of_motion, x0, t, args=(p,), atol=abserr, rtol=relerr, hmax=max_step)
# In[20]:
# Let's plot the trolly position and cable angle as subplots, to make it easier to compare
# Make the figure pretty, then plot the results
# "pretty" parameters selected based on pdf output, not screen output
# Many of these setting could also be made default by the .matplotlibrc file
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12,4))
plt.subplots_adjust(bottom=0.12,left=0.17,top=0.96,right=0.96)
plt.setp(ax1.get_ymajorticklabels(),family='serif',fontsize=18)
plt.setp(ax1.get_xmajorticklabels(),family='serif',fontsize=18)
plt.setp(ax2.get_ymajorticklabels(),family='serif',fontsize=18)
plt.setp(ax2.get_xmajorticklabels(),family='serif',fontsize=18)
ax1.spines['right'].set_color('none')
ax1.spines['top'].set_color('none')
ax1.xaxis.set_ticks_position('bottom')
ax1.yaxis.set_ticks_position('left')
ax1.grid(True,linestyle=':',color='0.75')
ax1.set_axisbelow(True)
ax2.spines['right'].set_color('none')
ax2.spines['top'].set_color('none')
ax2.xaxis.set_ticks_position('bottom')
ax2.yaxis.set_ticks_position('left')
ax2.grid(True,linestyle=':',color='0.75')
ax2.set_axisbelow(True)
# Trolley Position plot
ax1.set_xlabel(r'Time (s)',family='serif',fontsize=22,weight='bold',labelpad=5)
ax1.set_ylabel(r'Displacement (m)',family='serif',fontsize=22,weight='bold',labelpad=10)
ax1.plot(t, resp[:,0], linewidth=2)
# Cable Angle plot
ax2.set_xlabel(r'Time (s)',family='serif',fontsize=22,weight='bold',labelpad=5)
ax2.set_ylabel(r'Angle (deg)', family='serif', fontsize=22, weight='bold',labelpad=10)
ax2.plot(t, resp[:,0] * 180/np.pi, linewidth=2)
# Adjust the page layout filling the page using the new tight_layout command
plt.tight_layout(pad=0.5)
# If you want to save the figure, uncomment the commands below.
# The figure will be saved in the same directory as your IPython notebook.
# Save the figure as a high-res pdf in the current folder
# savefig('MassSpringPend_Response.pdf', dpi=300)
fig.set_size_inches(18,6) # Resize the figure for better display in the notebook
#
# #### Licenses
# 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](http://creativecommons.org/licenses/by-nc/4.0/), CC-BY-NC 4.0.
# In[21]:
# This cell will just improve the styling of the notebook
from IPython.core.display import HTML
import urllib.request
response = urllib.request.urlopen("https://cl.ly/1B1y452Z1d35")
HTML(response.read().decode("utf-8"))