{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Simulating Vibratory Systems with Non-Linearities\n", "\n", "## Introduction\n", "\n", "In this notebook, we'll explore numerically simulating the motion of **non-linear** systems for which we can't (or can't easily) derive the exact solutions analytically. After completing the notebook, you will be able to:\n", "\n", "- Manipulate second-order differential equations (equations of motion) to obtain corresponding *systems of first-order differential equations*\n", "- Become familiar with the programming interface for numerically integrating systems of first-order ODEs\n", "- Simulate the free response of systems with non-linear characteristics\n", "\n", "### Systems of First-Order ODEs\n", "\n", "To simulate non-linear vibratory systems, we manipulate the equation(s) of motion into a specific form that allows generic numerical integration routines to work. This is achieved by selecting *state variables* for which we can write first-order ODEs corresponding to the equations of motion. That is, the goal is to write a system of first-order differential equations of the form:\n", "\n", "$$\\begin{align} \\dot{x}_1 &= f(x_1,\\, x_2,\\, \\ldots) \\\\ \\dot{x}_2 &= g(x_1,\\, x_2,\\, \\ldots) \\\\ \\vdots \\end{align}$$\n", "\n", "When we derive the equations of motion using the Newton-Euler method, we get second-order differential equations. For one-dimensional motion, a simple choice of variables is $x_1 = x$ (position) and $x_2 = \\dot{x}$ (velocity). Then we get $\\dot{x}_1 = x_2$ and $\\dot{x}_2 = \\ddot{x}$, so we can write our system of equations as\n", "\n", "$$\\begin{align} \\dot{x}_1 &= x_2 \\\\ \\dot{x}_2 &= \\frac{1}{m} \\sum F \\end{align}$$\n", "\n", "The first equation is trivial (because of our selection of state variables), and the second equation is just $F = ma$.\n", "\n", "### Numerically Integrating First-Order ODEs\n", "\n", "In our code, we define a Python function that takes:\n", "\n", "- the current values of our state variables ($x_1,\\, x_2,\\, \\ldots$) in an array\n", "- the current time value\n", "- (optional) additional parameters such as masses, spring constants, etc.\n", "\n", "The function uses these inputs to compute the derivatives of the state variables $\\left(\\dot{x}_1(t),\\,\\dot{x}_2(t),\\,\\ldots)\\right)$ and return them. This function is passed to an ordinary differential equation solver with the initial conditions and an array of time values, and the solver repeatedly runs our function and numerically integrates to evaulate the state variables at the requested time points.\n", "\n", "### Example: Linear mass-spring-damper system\n", "\n", "As an example, let's simulate the mass-spring-damper system with linear spring and damping elements. The equation of motion is:\n", "\n", "$$m\\ddot{x} + c\\dot{x} + k x = 0$$\n", "\n", "Since we're dealing with 1D motion, our selection of state variables is straightforward: $x$ (position) and $v = \\dot{x}$ (velocity). In this case, our state equations are:\n", "\n", "$$\\begin{align} \\dot{x} &= v \\\\ \\dot{v} &= -\\frac{k}{m}x - \\frac{c}{m} v \\end{align}$$\n", "\n", "Let's start with some imports." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "%matplotlib inline" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def _rk4(t, dt, x, f, args=None):\n", " \"\"\"4th-order Runge-Kutta integration step.\"\"\"\n", " x = np.asarray(x)\n", " k1 = np.asarray(f(x, t, *args))\n", " k2 = np.asarray(f(x + 0.5*dt*k1, t + 0.5*dt, *args))\n", " k3 = np.asarray(f(x + 0.5*dt*k2, t + 0.5*dt, *args))\n", " k4 = np.asarray(f(x + dt*k3, t + dt, *args))\n", " return x + dt*(k1 + 2*k2 + 2*k3 + k4)/6.0\n", "\n", "\n", "def rk4int(func, x0, t, args=None):\n", " \"\"\"4th-order Runge-Kutta integration.\n", "\n", " Parameters\n", " ----------\n", " func : callable(x, t, *args)\n", " Function that returns the derivatives of the state variables at time t.\n", " x0 : array_like, shape(n,)\n", " Initial values of the state variables.\n", " t : array, shape(m,)\n", " Array of time values at which to solve.\n", " args : tuple, optional\n", " Additional arguments to `func`.\n", "\n", " Returns\n", " -------\n", " x : ndarray, shape(m, n)\n", " Array containing the values of the state variables at the\n", " specified time values in `t`.\n", "\n", " \"\"\"\n", " x = np.zeros((len(t), len(x0)))\n", " x[0, :] = x0\n", " for i in range(1, len(t)):\n", " dt = t[i] - t[i-1]\n", " x[i] = _rk4(t[i], dt, x[i-1], func, args)\n", " return x" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's write the function that returns the derivatives of the state variables. The function takes the current values of the state variables (as an array), the current time, and any additional parameters we need (system parameters $m$, $k$, and $c$)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def msd_eqns(current_state, time, m, k, c):\n", " \"\"\"Equations for a linear mass-spring-damper system\"\"\"\n", " # unpack the state variables array\n", " x, v = current_state\n", " \n", " # compute the derivatives\n", " xdot = v\n", " vdot = -(k/m)*x - (c/m)*v\n", " \n", " # pack the state derivatives together to return (same order as current_state!)\n", " state_derivative = xdot, vdot\n", " \n", " return state_derivative" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can use an integration routine (here we'll use the Runge-Kutta method provided as `rk4int`) to integrate the equations we specified at a set of time values. Here's what `rk4int` expects:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "help(rk4int)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So we set things up by specifying the system parameters ($m$, $k$, and $c$), initial values of the state variables (position and velocity), and an array of time values to simulate over. Each state variable is then a column of the output of `rk4int`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# parameters\n", "m = 1\n", "k = 1000\n", "c = 10\n", "\n", "# initial conditions\n", "x0 = 5\n", "v0 = 0\n", "\n", "# array of time values\n", "t = np.arange(0, 1, 0.01)\n", "\n", "# call integration routine \n", "state_trajectory = rk4int(msd_eqns, [x0, v0], t, args=(m, k, c))\n", "# | | | |\n", "# | | | |_additional parameters for msd_eqns\n", "# | | |\n", "# | | |_time array\n", "# | |\n", "# | |_initial conditions\n", "# |\n", "# |_function returning state variable derivatives\n", "\n", "# unpack the columns of the state variable array\n", "x = state_trajectory[:, 0]\n", "v = state_trajectory[:, 1]\n", "\n", "plt.plot(t, x)\n", "plt.xlabel('$t$')\n", "plt.ylabel('$x(t)$')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exercise 1: Coulomb Friction\n", "\n", "A common source of nonlinearity is Coulomb friction, which is specific type of damping. In viscous damping, there is a force that is linearly proportional to the velocity:\n", "\n", "$$F_v = c \\dot{x}$$\n", "\n", "This simple source of energy dissipation allows us to derive the exact solution to the equations of motion for a mass-spring-damper system. With Coulomb friction, the damping force takes the form:\n", "\n", "$$F_c = \\begin{cases} -\\mu N & \\dot{x} > 0 \\\\ 0 & \\dot{x} = 0 \\\\ \\mu N & \\dot{x} < 0 \\end{cases}$$\n", "\n", "where $\\mu$ is the coefficient of sliding friction and $N$ is the normal force. Here the damping force is constant, always working against the motion of the system.\n", "\n", "The form of the equation of motion is the same as in the linear damping case, where we have\n", "\n", "$$m \\ddot{x} + F_{\\text{damping}} + F_{\\text{spring}} = 0$$\n", "\n", "But now the equation will have a nonlinearity because the damping force depends on the sign of $\\dot{x}$:\n", "\n", "$$m \\ddot{x} + \\mu m g \\,\\text{sgn}\\left( \\dot{x} \\right) + kx = 0$$\n", "\n", "Fill in the function definition below to compute the derivatives of the state variables $x$ and $v = \\dot{x}$." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def coulomb_eqns(state_vars, t, m, k, mu):\n", " \"\"\"Returns the derivative of the states.\n", " \n", " Parameters\n", " ==========\n", " state_vars : array_like, shape(n,)\n", " A 1D array containing the current state values.\n", " t : float\n", " A value for the current time.\n", " m : float\n", " A value for the mass.\n", " k : float\n", " A value for the stiffness.\n", " mu : float\n", " A value for the coefficient of kinetic friction.\n", " \n", " Returns\n", " =======\n", " xdot : ndarray, shape(n,)\n", " The derivative of the states at the current time.\n", " \"\"\"\n", " x, v = state_vars\n", " \n", " xdot = v\n", " vdot = -mu*9.81*np.sign(v) - (k/m)*x\n", " \n", " return xdot, vdot" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Use the function above to simulate a mass-spring system with Coulomb friction using the following parameters:\n", "\n", "- $m = 1000$ kg\n", "- $k = 5000$ N/m\n", "- $\\mu = 0.2$\n", "- duration: 10 seconds\n", "\n", "Use zero initial velocity and plot the position of the mass for two different initial positions: 5 m and 4.5 m. Add a grid to the plot and take note of where the system comes to rest." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": false }, "outputs": [], "source": [ "# system parameters\n", "m = 1000 # mass [kg]\n", "k = 5000 # spring stiffness [N/m]\n", "mu = 0.2 # coulomb friction coefficient\n", "d = 10 # duration [s]\n", "\n", "# initial conditions\n", "xinits = np.array([[5, 0], # simulation 1\n", " [4.5, 0]]) # simulation 2\n", "linestyles = ['-', '--']\n", "\n", "# time array with a fairly small time step\n", "t = np.arange(0, 10, 0.01)\n", "\n", "for (x0, v0), linestyle in zip(xinits, linestyles):\n", " state_trajectory = rk4int(coulomb_eqns, [x0, v0], t, args=(m, k, mu))\n", " x = state_trajectory[:, 0]\n", " v = state_trajectory[:, 1]\n", " plt.plot(t, x, linestyle=linestyle,\n", " color='k', label=\"x0 = {:.1f}m\".format(x0))\n", " \n", "plt.xlabel(r'$t$ [s]')\n", "plt.ylabel(r'$x(t)$ [m]')\n", "plt.legend()\n", "plt.grid()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Questions\n", "\n", "1) This system behaves similiarly to a vicously damped system, but there are some differences. What differences can you find when exploring the simulation? Think about how the oscillation decays over time and any unexpected behavior. You may want to plot both state variables to see more information. Describe these differences in the cell below." ] }, { "cell_type": "markdown", "metadata": {}, "source": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "2) With $v_0=0$ try to find the value of $x_0$ that bounds the equilibrium range by simulating the system with various initial positions. What is the significance of this boundary? What does it represent?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "## Exercise 2: Pendulum\n", "\n", "In class, we've looked at the simple pendulum, which has the following equation of motion:\n", "\n", "$$\\ddot{\\theta} + \\frac{g}{l} \\sin \\theta = 0$$\n", "\n", "To solve the equation of motion for this system, we linearized to get rid of a $\\sin\\theta$ term. This was done by noting that for small angles, $\\sin\\theta \\approx \\theta$. With this approximation, we find that the system oscillates at a natural frequency of $\\omega_n = \\sqrt{\\frac{g}{l}}$. Let's see what happens if we do not make the small angle approximation.\n", "\n", "Start by writing the function to return the (non-linear) derivatives of the state variables $\\theta$ and $\\omega = \\dot{\\theta}$." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def pend_eqns(state_vars, t, l, g):\n", " \"\"\"Returns the derivative of the states.\n", " \n", " Parameters\n", " ==========\n", " state_vars : array_like, shape(n,)\n", " A 1D array containing the current state values: theta and omega.\n", " t : float\n", " A value for the current time.\n", " l : float\n", " A value for the length of the pendulum.\n", " g : float\n", " A value for the acceleration due to gravity.\n", " \n", " Returns\n", " =======\n", " xdot : ndarray, shape(n,)\n", " The derivative of the states at the current time, i.e. theta' and omega'.\n", " \n", " \"\"\"\n", " theta, omega = state_vars\n", " \n", " thetadot = omega\n", " omegadot = - g / l * np.sin(theta)\n", " \n", " return thetadot, omegadot" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Use `interact` to plot $\\theta(t)$ such that you can interactively manipulate the initial angle between 0 and 90 degrees (use zero initial velocity). Use a value of $l$ which makes the linearized system oscillate at 1 Hz and simulate for 4 seconds.\n", "\n", "In addition to the response of the non-linear system, plot the response of the linearized system as well. Since we're using zero initial velocity, you may use the following equation to compute the response of the linearized system:\n", "\n", "$$\\theta_l(t) = \\theta_0 \\cos\\left(\\sqrt{\\frac{g}{l}} t\\right)$$" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from ipywidgets import interact\n", "\n", "# gravitational constant\n", "g = 9.81\n", "# oscillation frequency in Hz\n", "f = 1\n", "\n", "# convert frequency to rad/s\n", "omega_n = 2 * np.pi * f\n", "# compute length to get desired natural frequency\n", "l = g / omega_n**2\n", "\n", "t = np.arange(0, 10, 0.01)\n", "\n", "def simulate(ang_init=10):\n", " # linearized response\n", " theta_lin = ang_init * np.cos(np.sqrt(g / l)*t)\n", " \n", " # non-linear response\n", " x = rk4int(pend_eqns, [np.deg2rad(ang_init), 0], t, args=(l, g))\n", " x = np.rad2deg(x)\n", " theta_nonlin = x[:, 0]\n", " \n", " plt.plot(t, theta_lin, color='k', linestyle='-', label='linear')\n", " plt.plot(t, theta_nonlin, color='k', linestyle='--', label='nonlinear')\n", " plt.ylim(-90, 90)\n", " plt.xlabel(r'$t$ [s]')\n", " plt.ylabel(r'$\\theta(t)$ [deg]')\n", " plt.legend()\n", " plt.grid()\n", "\n", "interact(simulate, ang_init=(0, 90, 5))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Question\n", "\n", "Describe what happens when the initial angle is large, i.e. when the small angle approximation no longer holds?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Exercise 3: Unstable Pendulum\n", "\n", "In class we derived the non-linear and linear equations of motion for a pendulum that was held veritcal by two springs in series. We predicted that it would be unstable if:\n", "\n", "$$\n", "\\frac{k}{2 m} < \\frac{g}{l}\n", "$$\n", "\n", "Here is the second order non-linear equation of motion:\n", "\n", "$$\n", "g l m \\operatorname{sin}\\left(\\theta\\right) - \\frac{k l^{2}}{2} \\operatorname{sin}\\left(\\theta\\right) \\operatorname{cos}\\left(\\theta\\right) - l^{2} m \\ddot{\\theta} = 0\n", "$$\n", "\n", "Simulate this non-linear system along with the linear system using $m = 1.0, l=1.0, g=9.81$ and various values of $k$ that will cause the system to be stable and/or unstable. Initial conditions can be $\\theta=\\pi/10,\\dot{\\theta}=0$." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "def unstable_pend_eqns(state_vars, t, m, l, g):\n", " \"\"\"Returns the derivative of the states.\n", " \n", " Parameters\n", " ==========\n", " state_vars : array_like, shape(n,)\n", " A 1D array containing the current state values: theta and omega.\n", " t : float\n", " A value for the current time.\n", " m : float\n", " A value for the mass of the pendulum.\n", " l : float\n", " A value for the length of the pendulum.\n", " g : float\n", " A value for the acceleration due to gravity.\n", " \n", " Returns\n", " =======\n", " xdot : ndarray, shape(n,)\n", " The derivative of the states at the current time, i.e. theta' and omega'.\n", " \n", " \"\"\"\n", " theta, omega = state_vars\n", " \n", " thetadot = omega\n", " omegadot = (1 / m*l**2) * \\\n", " (g*l*m*np.sin(theta) - 0.5*k*l**2*np.sin(theta)*np.cos(theta))\n", " \n", " return thetadot, omegadot" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here we'll just set up the system parameters." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "m = 1.0 # mass [kg]\n", "l = 1.0 # length [m]\n", "g = 9.81 # gravitational constant\n", "\n", "# intial conditions\n", "theta_0 = np.pi / 10\n", "omega_0 = 0\n", "\n", "# array of time values to simulate over\n", "t = np.arange(0, 10, 0.01)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can specify some values of $k$ that will result in a stable and unstable system. To do this, we can use the stability criterion $\\frac{k}{2 m} < \\frac{g}{l}$ to find a critical value of $k_c = \\frac{2mg}{l}$ with coefficients greater than or less than 1 to control whether or not the criterion holds.\n", "\n", "We'll create a set of $k$ values for each condition (stable and unstable) and plot them on separate axes." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "k_crit = 2 * m * g / l\n", "\n", "k_stable = np.array([2, 1.5]) * k_crit\n", "k_unstable = np.array([0.5, 0.1]) * k_crit\n", "\n", "k_sets = [k_stable, k_unstable]\n", "fig, axes = plt.subplots(nrows=2, sharex=True)\n", "label = ['stable', 'unstable']\n", "\n", "for k_set, ax, label in zip(k_sets, axes, label):\n", " for k in k_set:\n", " x = rk4int(unstable_pend_eqns, [theta_0, omega_0], t, args=(m, l, g))\n", " x = np.rad2deg(x)\n", " theta = x[:, 0]\n", " \n", " ax.set_title(label)\n", " ax.plot(t, theta)\n", " ax.set_ylabel(r'$\\theta(t)$ [deg]')\n", " \n", "axes[-1].set_xlabel(r'$t$ [s]')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Question\n", "\n", "1) Is the system unstable with specific values of $k$? If so what are those values?\n", "\n", "2) Does this do what you expect it should do? Make sure to plot the state trajectories to see what is going on." ] }, { "cell_type": "markdown", "metadata": {}, "source": [] } ], "metadata": { "anaconda-cloud": {}, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.2" } }, "nbformat": 4, "nbformat_minor": 1 }