{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Introduction to SciPy\n", "Tutorial at EuroSciPy 2019, Bilbao\n", "## 3.1. Optimization – The hanging chain\n", "![chain link](images/chainlink.png)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "from scipy import optimize\n", "%matplotlib inline\n", "import matplotlib.pyplot as plt\n", "\n", "\n", "class Chain:\n", " def __init__(self, nlinks, hdist):\n", " if nlinks < hdist:\n", " raise ValueError('a hanging chain with {} links of '\n", " 'length 1 cannot have endpoints '\n", " 'separated by a distance {}'.format(\n", " nlinks, hdist))\n", " self.nlinks = nlinks\n", " self.hdist = hdist\n", " \n", " def equilibrium(self):\n", " phi0 = np.arcsin(self.hdist/self.nlinks)\n", " result = optimize.minimize(\n", " self.f_energy,\n", " np.linspace(-phi0, phi0, self.nlinks),\n", " method='SLSQP',\n", " constraints=[{'type': 'eq', 'fun': self.x_constraint},\n", " {'type': 'eq', 'fun': self.y_constraint}])\n", " return result.x\n", " \n", " def x_constraint(self, phi_vals):\n", " \"\"\"ensure the correct horizontal distance\n", " \n", " phi_vals: angles of links with respect to the horizontal\n", " \n", " \"\"\"\n", " return np.sum(np.cos(phi_vals))-self.hdist \n", " \n", " def y_constraint(self, phi_vals):\n", " \"\"\"ensure that endpoints are at the same height\n", " \n", " phi_vals: angles of links with respect to the horizontal\n", " \n", " \"\"\"\n", " return np.sum(np.sin(phi_vals))\n", "\n", " def f_energy(self, phi_vals):\n", " \"\"\"potential energy of all links\n", " \n", " \"\"\"\n", " return np.sum(np.arange(self.nlinks, 0, -1)*np.sin(phi_vals))\n", " " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def angles_to_coords(phi_vals):\n", " \"\"\"convert angles to coordinates of link endpoints\n", " \n", " phi_vals: angles of links with respect to the horizontal\n", " x, y: coordinates of link endpoints\n", " \n", " \"\"\"\n", " dim = phi_vals.shape[0]+1\n", " x = np.zeros(dim)\n", " x[1:] = np.cumsum(np.cos(phi_vals))\n", " y = np.zeros(dim)\n", " y[1:] = np.cumsum(np.sin(phi_vals))\n", " return x, y\n", "\n", "nlinks = 20\n", "hdist = 16\n", "\n", "c = Chain(nlinks, hdist)\n", "plt.axes().set_aspect('equal')\n", "_ = plt.plot(*angles_to_coords(c.equilibrium()), 'o-')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3.2. Solving a set of ordinary differential equations – The falling chain" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Literature \n", "W. Tomaszewski, P. Pieranski, J.-C. Geminard \n", "*The motion of a free falling chain tip* \n", "[American Journal of Physics **74**, 776 (2006)](https://dx.doi.org/10.1119/1.2204074)\n", "\n", "#### Equations of motion\n", "\n", "$$\\sum_{j=1}^n m_{i,j}\\cos(\\varphi_i-\\varphi_j)\\ddot\\varphi_j\n", " = -\\sum_{j=1}^n m_{i, j}\\sin(\\varphi_i-\\varphi_j)\\dot\\varphi_j^2\n", " +\\frac{r}{m\\ell^2}(\\dot\\varphi_{i-1}-2\\dot\\varphi_i+\\dot\\varphi_{i+1})\n", " -\\frac{g}{\\ell}a_i\\cos(\\varphi_i)$$\n", "with\n", "$$a_i = n-i+\\frac{1}{2}$$\n", "and\n", "$$m_{i,j} = \\begin{cases}\n", " n-i+\\frac{1}{3} & i=j \\\\\n", " n-\\max(i,j)+\\frac{1}{2} & i\\neq j\n", " \\end{cases}$$\n", " \n", "parameters: \n", "$m$ mass of chain element \n", "$\\ell$ length of chain element \n", "$g$ acceleration due to gravity \n", "$r$ damping constant\n", "\n", "#### How to deal with second-order differential equations\n", "\n", "Introduce a new variable\n", "$$p_\\varphi = \\dot\\varphi$$\n", "to replace the second-order differential equation\n", "$$\\ddot\\varphi = f(\\dot\\varphi, \\varphi)$$\n", "by two first-order differential equations\n", "$$\n", " \\begin{aligned}\n", " \\dot\\varphi &= p_\\varphi\\\\\n", " \\dot p_\\varphi &= f(p_\\varphi, \\varphi)\n", " \\end{aligned}\n", "$$" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy.linalg as LA\n", "from scipy import integrate\n", "\n", "class FallingChain(Chain):\n", " def __init__(self, nlinks, hdist, damping):\n", " super(FallingChain, self).__init__(nlinks, hdist)\n", " self.set_matrix_m()\n", " self.set_vector_a()\n", " self.set_matrix_damping(damping)\n", " \n", " def set_matrix_m(self):\n", " m = np.fromfunction(lambda i, j: self.nlinks-np.maximum(i, j)-0.5,\n", " (self.nlinks, self.nlinks))\n", " self.m = m-np.identity(self.nlinks)/6\n", "\n", " def set_vector_a(self):\n", " self.a = np.arange(self.nlinks, 0, -1)-0.5\n", " \n", " def set_matrix_damping(self, damping):\n", " self.damping = (-2*np.identity(self.nlinks, dtype=np.float64)\n", " + np.eye(self.nlinks, k=1)\n", " + np.eye(self.nlinks, k=-1))\n", " self.damping[0, 0] = -1\n", " self.damping[self.nlinks-1, self.nlinks-1] = -1\n", " self.damping = damping*self.damping\n", "\n", " def solve_eq_of_motion(self, time_i, time_f, nt):\n", " y0 = np.zeros(2*self.nlinks, dtype=np.float64)\n", " y0[self.nlinks:] = self.equilibrium()\n", " self.solution = integrate.solve_ivp(\n", " self.diff, (time_i, time_f), y0,\n", " method='RK45',\n", " t_eval=np.linspace(time_i, time_f, nt))\n", "\n", " def diff(self, t, y):\n", " momenta = y[:self.nlinks]\n", " angles = y[self.nlinks:]\n", " d_angles = momenta\n", " ci = np.cos(angles)\n", " cij = np.cos(angles[:, np.newaxis]-angles)\n", " sij = np.sin(angles[:, np.newaxis]-angles)\n", " mcinv = LA.inv(self.m*cij)\n", " d_momenta = -np.dot(self.m*sij, momenta*momenta)\n", " d_momenta = d_momenta + np.dot(self.damping, momenta)\n", " d_momenta = d_momenta - self.a*ci\n", " d_momenta = np.dot(mcinv, d_momenta)\n", " d = np.empty_like(y)\n", " d[:self.nlinks] = d_momenta\n", " d[self.nlinks:] = d_angles\n", " return d" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Set-up for animation" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from matplotlib import rc\n", "import matplotlib.animation as animation\n", "rc('animation', html='jshtml')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The following cell should always be executed before running the animation cell below.\n", "In this way, an undesired extra static image will be avoided." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%capture\n", "fig = plt.figure()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Animation of a falling chain" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def animate(i):\n", " x, y = angles_to_coords(c.solution.y[:, i][c.nlinks:])\n", " line.set_data(x, y)\n", " return line,\n", "\n", "def init():\n", " line.set_data([], [])\n", " return line,\n", "\n", "nlinks = 50\n", "hdist = 40\n", "damping = 1\n", "ti = 0\n", "tf = 200\n", "tsteps = 1000\n", "\n", "c = FallingChain(nlinks, hdist, damping)\n", "c.solve_eq_of_motion(ti, tf, tsteps)\n", "ax = fig.add_subplot(111, autoscale_on=False,\n", " xlim=(-nlinks, nlinks), ylim=(-nlinks, 0.3*nlinks))\n", "ax.set_aspect('equal')\n", "line, = ax.plot([], [])\n", "anim = animation.FuncAnimation(fig, animate, tsteps,\n", " interval=20, blit=True,\n", " init_func=init)\n", "anim" ] } ], "metadata": { "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.7.4" } }, "nbformat": 4, "nbformat_minor": 2 }