{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Implementation\n", "
\n", "\n", "This section presents the complete computational algorithm, its\n", "implementation in Python code, animation of the solution, and\n", "verification of the implementation.\n", "\n", "A real implementation of the basic computational algorithm from\n", "the sections [Formulating a recursive algorithm](wave1D_fd1.ipynb#wave:string:alg) and [Sketch of an implementation](wave1D_fd1.ipynb#wave:string:impl) can be\n", "encapsulated in a function, taking all the input data for the problem\n", "as arguments. The physical input data consists of $c$, $I(x)$,\n", "$V(x)$, $f(x,t)$, $L$, and $T$. The numerical input is the mesh\n", "parameters $\\Delta t$ and $\\Delta x$.\n", "\n", "Instead of specifying $\\Delta t$ *and* $\\Delta x$, we can specify one\n", "of them and the Courant number $C$ instead, since having explicit\n", "control of the Courant number is convenient when investigating the\n", "numerical method. Many find it natural to prescribe the resolution of\n", "the spatial grid and set $N_x$. The solver function can then compute\n", "$\\Delta t = CL/(cN_x)$. However, for comparing $u(x,t)$ curves (as\n", "functions of $x$) for various Courant numbers\n", "it is more convenient to keep $\\Delta t$ fixed for\n", "all $C$ and let $\\Delta x$ vary according to $\\Delta x = c\\Delta t/C$.\n", "With $\\Delta t$ fixed, all frames correspond to the same time $t$,\n", "and this simplifies animations that compare simulations with different\n", "mesh resolutions. Plotting functions of $x$\n", "with different spatial resolution is trivial,\n", "so it is easier to let $\\Delta x$ vary in the simulations than $\\Delta t$.\n", "\n", "## Callback function for user-specific actions\n", "
\n", "\n", "\n", "The solution at all spatial points at a new time level is stored in an\n", "array `u` of length $N_x+1$. We need to decide what to do with\n", "this solution, e.g., visualize the curve, analyze the values, or write\n", "the array to file for later use. The decision about what to do is left to\n", "the user in the form of a user-supplied function \n", "`user_action(u, x, t, n)`, where `u` is the solution at the spatial points `x` at time `t[n]`.\n", "The `user_action` function is called from the solver at each time level `n`.\n", "\n", "If the user wants to plot the solution or store the solution at a\n", "time point, she needs to write such a function and take appropriate\n", "actions inside it. We will show examples on many such `user_action`\n", "functions.\n", "\n", "Since the solver function makes calls back to the user's code\n", "via such a function, this type of function is called a *callback function*.\n", "When writing general software, like our solver function, which also needs\n", "to carry out special problem- or solution-dependent actions\n", "(like visualization),\n", "it is a common technique to leave those actions to user-supplied\n", "callback functions.\n", "\n", "The callback function can be used to terminate the solution process\n", "if the user returns `True`. For example," ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "def my_user_action_function(u, x, t, n):\n", " return np.abs(u).max() > 10" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "is a callback function that will terminate the solver function (given below) of the\n", "amplitude of the waves exceed 10, which is here considered as a numerical\n", "instability.\n", "\n", "\n", "## The solver function\n", "
\n", "\n", "\n", "A first attempt at a solver function is listed below." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import time as time\n", "\n", "def python_solver(I, V, f, c, L, dt, C, T, user_action=None):\n", " \"\"\"Solve u_tt=c^2*u_xx + f on (0,L)x(0,T].\"\"\"\n", " Nt = int(round(T/dt))\n", " t = np.linspace(0, Nt*dt, Nt+1) # Mesh points in time\n", " dx = dt*c/float(C)\n", " Nx = int(round(L/dx))\n", " x = np.linspace(0, L, Nx+1) # Mesh points in space\n", " C2 = C**2 # Help variable in the scheme\n", " # Make sure dx and dt are compatible with x and t\n", " dx = x[1] - x[0]\n", " dt = t[1] - t[0]\n", "\n", " if f is None or f == 0 :\n", " f = lambda x, t: 0\n", " if V is None or V == 0:\n", " V = lambda x: 0\n", "\n", " u = np.zeros(Nx+1) # Solution array at new time level\n", " u_n = np.zeros(Nx+1) # Solution at 1 time level back\n", " u_nm1 = np.zeros(Nx+1) # Solution at 2 time levels back\n", "\n", " t0 = time.perf_counter() # Measure CPU time\n", "\n", " # Load initial condition into u_n\n", " for i in range(0,Nx+1):\n", " u_n[i] = I(x[i])\n", "\n", " if user_action is not None:\n", " user_action(u_n, x, t, 0)\n", "\n", " # Special formula for first time step\n", " n = 0\n", " for i in range(1, Nx):\n", " u[i] = u_n[i] + dt*V(x[i]) + \\\n", " 0.5*C2*(u_n[i-1] - 2*u_n[i] + u_n[i+1]) + \\\n", " 0.5*dt**2*f(x[i], t[n])\n", " u[0] = 0; u[Nx] = 0\n", "\n", " if user_action is not None:\n", " user_action(u, x, t, 1)\n", "\n", " # Switch variables before next step\n", " u_nm1[:] = u_n; u_n[:] = u\n", "\n", " for n in range(1, Nt):\n", " # Update all inner points at time t[n+1]\n", " for i in range(1, Nx):\n", " u[i] = - u_nm1[i] + 2*u_n[i] + \\\n", " C2*(u_n[i-1] - 2*u_n[i] + u_n[i+1]) + \\\n", " dt**2*f(x[i], t[n])\n", "\n", " # Insert boundary conditions\n", " u[0] = 0; u[Nx] = 0\n", " if user_action is not None:\n", " if user_action(u, x, t, n+1):\n", " break\n", "\n", " # Switch variables before next step\n", " u_nm1[:] = u_n; u_n[:] = u\n", "\n", " cpu_time = time.perf_counter() - t0\n", " return u, x, t, cpu_time" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A couple of remarks about the above code is perhaps necessary:\n", "\n", " * Although we give `dt` and compute `dx` via `C` and `c`, the resulting\n", " `t` and `x` meshes do not necessarily correspond exactly to these values\n", " because of rounding errors. To explicitly ensure that `dx` and `dt`\n", " correspond to the cell sizes in `x` and `t`, we recompute the values.\n", "\n", " * According to the particular choice made in the section [Callback function for user-specific actions](#wave:pde1:impl:useraction), a true value returned from `user_action` should terminate the simulation. This is here implemented by a `break` statement inside the for loop in the solver.\n", "\n", "\n", "\n", "\n", "\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Devito implementation\n", "Now let's take a look at the same algorithm, but this time, using Devito." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import time as time\n", "from devito import Constant, Grid, TimeFunction, SparseTimeFunction, Eq, solve, Operator, Buffer\n", "\n", "def devito_solver(I, V, f, c, L, dt, C, T, user_action=None):\n", " \"\"\"Solve u_tt=c^2*u_xx + f on (0,L)x(0,T].\"\"\"\n", " Nt = int(round(T/dt))\n", " t = np.linspace(0, Nt*dt, Nt+1) # Mesh points in time\n", " dx = dt*c/float(C)\n", " Nx = int(round(L/dx))\n", " x = np.linspace(0, L, Nx+1) # Mesh points in space\n", " C2 = C**2 # Help variable in the scheme\n", " # Make sure dx and dt are compatible with x and t\n", " dx = x[1] - x[0]\n", " dt = t[1] - t[0]\n", " a = Constant(name='a')\n", "\n", " # Set source term to 0 if not provided\n", " if f is None or f == 0 :\n", " f = lambda x, t: 0\n", " \n", " if V is None or V == 0:\n", " V = lambda x: 0\n", "\n", " # Initialise `u` for space and time order 2, using initialisation function I\n", " # across all values in x\n", " grid = Grid(shape=Nx+1, extent=L)\n", " u = TimeFunction(name='u', grid=grid, space_order=2, time_order=2, save=Nt)\n", " u.data[:] = I(x) # Forward, central and backward time steps all same - u_t(x, 0) = 0\n", " \n", " if user_action is not None:\n", " user_action(u.data[0], x, t, 0)\n", " \n", " dt_symbolic = grid.time_dim.spacing\n", " \n", " # Source term and injection into equation\n", " src = SparseTimeFunction(name='f', grid=grid, npoint=Nx+1, nt=Nt)\n", " src.coordinates.data[1:-1, 0] = f(x, t)\n", " src_term = src.inject(field=u.forward, expr=src * dt_symbolic**2)\n", " \n", " # Measure CPU time\n", " t0 = time.perf_counter()\n", " \n", " # Set up wave equation and solve for forward stencil point in time\n", " x_dim = grid.dimensions[0]\n", " time_dim = grid.time_dim\n", " eq = Eq(u.dt2, (a**2) * u.dx2 + f(x_dim, time_dim))\n", " stencil = solve(eq, u.forward)\n", " eq_stencil = Eq(u.forward, stencil)\n", " \n", " \n", " # Boundary conditions\n", " stepping_dim = grid.stepping_dim\n", " bc1 = [Eq(u[stepping_dim+1, 0], 0.)]\n", " bc2 = [Eq(u[stepping_dim+1, -1], 0.)]\n", " \n", " # Building operator\n", " op = Operator([eq_stencil] + bc1 + bc2 + src_term)\n", " op.apply(dt=dt.astype(np.float32), a=c)\n", " \n", " if user_action is not None:\n", " for i in range (1, Nt):\n", " user_action(u.data[i], x, t, i+1)\n", " \n", " cpu_time = time.perf_counter() - t0\n", " return u.data[-1], x, t, cpu_time" ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "scrolled": false }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "Operator `Kernel` run in 0.01 s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Python:\n" ] }, { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Devito:\n" ] }, { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "import matplotlib.pyplot as plt\n", "\n", "# Testing these two functions against each other\n", "# I = lambda x: np.cos(x)\n", "# V = None\n", "# f = lambda x, t: x\n", "\n", "def u_exact(x, t):\n", " return x*(L-x)*(1 + 0.5*t)\n", "\n", "def I(x):\n", " return u_exact(x, 0)\n", "\n", "def V(x):\n", " return 0.5*u_exact(x, 0)\n", "\n", "def f(x, t):\n", " return 2*(1 + 0.5*t)*c**2\n", "\n", "c = 1.5\n", "L = 2.5\n", "Nx = 100\n", "dt = C*(L/Nx)/c\n", "C = 0.75\n", "T = 18\n", "u_correct, x_arr_correct, t_arr_correct, cpu_time_corr = python_solver(I, V, None, c, L, dt, C, T)\n", "u_mine, x_arr, t_arr, cpu_time = devito_solver(I, V, None, c, L, dt, C, T)\n", "\n", "print(\"Python:\")\n", "plt.plot(x_arr_correct, u_correct)\n", "plt.xlabel('x')\n", "plt.ylabel('u')\n", "plt.show()\n", "\n", "print(\"Devito:\")\n", "plt.plot(x_arr, u_mine)\n", "plt.xlabel('x')\n", "plt.ylabel('u')\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Verification: exact quadratic solution\n", "
\n", "\n", "\n", "We use the test problem derived in the section [wave:pde2:fd](#wave:pde2:fd) for\n", "verification. Below is a unit test based on this test problem\n", "and realized as a proper *test function* compatible with the unit test\n", "frameworks nose or pytest." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "Data type float64 of runtime value `dt` does not match the Constant data type \n", "Operator `Kernel` run in 0.01 s\n" ] }, { "ename": "AssertionError", "evalue": "", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 29\u001b[0m \u001b[0mdevito_solver\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mI\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mV\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mf\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mc\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mL\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdt\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mC\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mT\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0muser_action\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0massert_no_error\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 30\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 31\u001b[0;31m \u001b[0mtest_quadratic\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;32m\u001b[0m in \u001b[0;36mtest_quadratic\u001b[0;34m()\u001b[0m\n\u001b[1;32m 27\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0mdiff\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0mtol\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 28\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 29\u001b[0;31m \u001b[0mdevito_solver\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mI\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mV\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mf\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mc\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mL\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdt\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mC\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mT\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0muser_action\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0massert_no_error\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 30\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 31\u001b[0m \u001b[0mtest_quadratic\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m\u001b[0m in \u001b[0;36mdevito_solver\u001b[0;34m(I, V, f, c, L, dt, C, T, user_action)\u001b[0m\n\u001b[1;32m 63\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0muser_action\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 64\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mi\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mNt\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 65\u001b[0;31m \u001b[0muser_action\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mu\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdata\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mt\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mi\u001b[0m\u001b[0;34m+\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 66\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 67\u001b[0m \u001b[0mcpu_time\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mperf_counter\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0mt0\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m\u001b[0m in \u001b[0;36massert_no_error\u001b[0;34m(u, x, t, n)\u001b[0m\n\u001b[1;32m 25\u001b[0m \u001b[0mdiff\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mabs\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mu\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0mu_e\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmax\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 26\u001b[0m \u001b[0mtol\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m1E-7\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 27\u001b[0;31m \u001b[0;32massert\u001b[0m \u001b[0mdiff\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0mtol\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 28\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 29\u001b[0m \u001b[0mdevito_solver\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mI\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mV\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mf\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mc\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mL\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdt\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mC\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mT\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0muser_action\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0massert_no_error\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mAssertionError\u001b[0m: " ] } ], "source": [ "def test_quadratic():\n", " \"\"\"Check that u(x,t)=x(L-x)(1+t/2) is exactly reproduced.\"\"\"\n", "\n", " def u_exact(x, t):\n", " return x*(L-x)*(1 + 0.5*t)\n", "\n", " def I(x):\n", " return u_exact(x, 0)\n", "\n", " def V(x):\n", " return 0.5*u_exact(x, 0)\n", "\n", " def f(x, t):\n", " return 2*(1 + 0.5*t)*(c**2)\n", "\n", " L = 2.5\n", " c = 1.5\n", " C = 0.75\n", " Nx = 6 # Very coarse mesh for this exact test\n", " dt = C*(L/Nx)/c\n", " T = 18\n", "\n", " def assert_no_error(u, x, t, n):\n", " u_e = u_exact(x, t[n])\n", " diff = np.abs(u - u_e).max()\n", " tol = 1E-7\n", " assert diff < tol\n", "\n", " devito_solver(I, V, f, c, L, dt, C, T, user_action=assert_no_error)\n", "\n", "test_quadratic()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When this function resides in the file `wave1D_u0.py`, one can run\n", "pytest to check that all test functions with names `test_*()`\n", "in this file work:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " Terminal> py.test -s -v wave1D_u0.py\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Verification: convergence rates\n", "
\n", "\n", "\n", "A more general method, but not so reliable as a verification method,\n", "is to compute the convergence rates and see if they coincide with\n", "theoretical estimates. Here we expect a rate of 2 according to\n", "the various results in the section [wave:pde1:analysis](#wave:pde1:analysis).\n", "A general function for computing convergence rates can be written like\n", "this:" ] }, { "cell_type": "code", "execution_count": 89, "metadata": {}, "outputs": [], "source": [ "def convergence_rates(\n", " u_exact, # Python function for exact solution\n", " I, V, f, c, L, # physical parameters\n", " dt0, num_meshes, C, T): # numerical parameters\n", " \"\"\"\n", " Half the time step and estimate convergence rates for\n", " for num_meshes simulations.\n", " \"\"\"\n", " # First define an appropriate user action function\n", " global error\n", " error = 0 # error computed in the user action function\n", "\n", " def compute_error(u, x, t, n):\n", " global error # must be global to be altered here\n", " # (otherwise error is a local variable, different\n", " # from error defined in the parent function)\n", " if n == 0:\n", " error = 0\n", " else:\n", " error = max(error, np.abs(u - u_exact(x, t[n])).max())\n", "\n", " # Run finer and finer resolutions and compute true errors\n", " E = []\n", " h = [] # dt, solver adjusts dx such that C=dt*c/dx\n", " dt = dt0\n", " for i in range(num_meshes):\n", " devito_solver(I, V, f, c, L, dt, C, T,\n", " user_action=compute_error)\n", " # error is computed in the final call to compute_error\n", " E.append(error)\n", " h.append(dt)\n", " dt /= 2 # halve the time step for next simulation\n", " print('E:', E)\n", " print('h:', h)\n", " # Convergence rates for two consecutive experiments\n", " r = [np.log(E[i]/E[i-1])/np.log(h[i]/h[i-1])\n", " for i in range(1,num_meshes)]\n", " return r" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Using the analytical solution from the section [wave:pde2:fd:standing:waves](#wave:pde2:fd:standing:waves), we can call `convergence_rates` to\n", "see if we get a convergence rate that approaches 2 and use the final\n", "estimate of the rate in an `assert` statement such that this function becomes\n", "a proper test function:" ] }, { "cell_type": "code", "execution_count": 91, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "Data type float64 of runtime value `dt` does not match the Constant data type \n", "Operator `Kernel` run in 0.01 s\n", "Data type float64 of runtime value `dt` does not match the Constant data type \n", "Operator `Kernel` run in 0.01 s\n", "Data type float64 of runtime value `dt` does not match the Constant data type \n", "Operator `Kernel` run in 0.01 s\n", "Data type float64 of runtime value `dt` does not match the Constant data type \n", "Operator `Kernel` run in 0.01 s\n", "Data type float64 of runtime value `dt` does not match the Constant data type \n", "Operator `Kernel` run in 0.01 s\n", "Data type float64 of runtime value `dt` does not match the Constant data type \n", "Operator `Kernel` run in 0.01 s\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "E: [Data(1.36646618), Data(0.98566641), Data(0.57493195), Data(0.30752397), Data(0.15659378), Data(0.07878391)]\n", "h: [0.1, 0.05, 0.025, 0.0125, 0.00625, 0.003125]\n" ] }, { "ename": "AssertionError", "evalue": "", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 19\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0mabs\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mr\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0;36m0.002\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 20\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 21\u001b[0;31m \u001b[0mtest_convrate_sincos\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;32m\u001b[0m in \u001b[0;36mtest_convrate_sincos\u001b[0;34m()\u001b[0m\n\u001b[1;32m 17\u001b[0m \u001b[0;31m# print('rates sin(x)*cos(t) solution:', \\\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 18\u001b[0m \u001b[0;31m# [round(r_,2) for r_ in r])\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 19\u001b[0;31m \u001b[0;32massert\u001b[0m \u001b[0mabs\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mr\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0;36m0.002\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 20\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 21\u001b[0m \u001b[0mtest_convrate_sincos\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mAssertionError\u001b[0m: " ] } ], "source": [ "def test_convrate_sincos():\n", " n = m = 2\n", " L = 1.0\n", " u_exact = lambda x, t: np.cos(m*np.pi/L*t)*np.sin(m*np.pi/L*x)\n", "\n", " r = convergence_rates(\n", " u_exact=u_exact,\n", " I=lambda x: u_exact(x, 0),\n", " V=lambda x: 0,\n", " f=0,\n", " c=1,\n", " L=L,\n", " dt0=0.1,\n", " num_meshes=6,\n", " C=0.9,\n", " T=1)\n", "# print('rates sin(x)*cos(t) solution:', \\\n", "# [round(r_,2) for r_ in r])\n", " assert abs(r[-1] - 2) < 0.002\n", " \n", "test_convrate_sincos()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Doing `py.test -s -v wave1D_u0.py` will run also this test function and\n", "show the rates 2.05, 1.98, 2.00, 2.00, and 2.00 (to two decimals).\n", "\n", "\n", "## Visualization: animating the solution\n", "
\n", "\n", "Now that we have verified the implementation it is time to do a\n", "real computation where we also display evolution of the waves\n", "on the screen. Since the `solver` function knows nothing about\n", "what type of visualizations we may want, it calls the callback function\n", "`user_action(u, x, t, n)`. We must therefore write this function and\n", "find the proper statements for plotting the solution.\n", "\n", "### Function for administering the simulation\n", "\n", "The following `viz` function\n", "\n", "1. defines a `user_action` callback function\n", " for plotting the solution at each time level,\n", "\n", "2. calls the `solver` function, and\n", "\n", "3. combines all the plots (in files) to video in different formats." ] }, { "cell_type": "code", "execution_count": 92, "metadata": {}, "outputs": [], "source": [ "%matplotlib inline\n", "\n", "def viz(\n", " I, V, f, c, L, dt, C, T, # PDE parameters\n", " umin, umax, # Interval for u in plots\n", " animate=True, # Simulation with animation?\n", " tool='matplotlib', # 'matplotlib' or 'scitools'\n", " solver_function=devito_solver, # Function with numerical algorithm\n", " ):\n", " \"\"\"Run solver and visualize u at each time level.\"\"\"\n", "\n", " def plot_u_st(u, x, t, n):\n", " \"\"\"user_action function for solver.\"\"\"\n", " plt.plot(x, u, 'r-',\n", " xlabel='x', ylabel='u',\n", " axis=[0, L, umin, umax],\n", " title='t=%f' % t[n], show=True)\n", " # Let the initial condition stay on the screen for 2\n", " # seconds, else insert a pause of 0.2 s between each plot\n", " time.sleep(2) if t[n] == 0 else time.sleep(0.2)\n", " plt.savefig('frame_%04d.png' % n) # for movie making\n", "\n", " class PlotMatplotlib:\n", " def __call__(self, u, x, t, n):\n", " \"\"\"user_action function for solver.\"\"\"\n", " if n == 0:\n", " plt.ion()\n", " self.lines = plt.plot(x, u, 'r-')\n", " plt.xlabel('x'); plt.ylabel('u')\n", " plt.axis([0, L, umin, umax])\n", " plt.legend(['t=%f' % t[n]], loc='lower left')\n", " else:\n", " self.lines[0].set_ydata(u)\n", " plt.legend(['t=%f' % t[n]], loc='lower left')\n", " plt.draw()\n", " time.sleep(2) if t[n] == 0 else time.sleep(0.2)\n", " plt.savefig('tmp_%04d.png' % n) # for movie making\n", "\n", " if tool == 'matplotlib':\n", " import matplotlib.pyplot as plt\n", " plot_u = PlotMatplotlib()\n", " elif tool == 'scitools':\n", " import scitools.std as plt # scitools.easyviz interface\n", " plot_u = plot_u_st\n", " import time, glob, os\n", "\n", " # Clean up old movie frames\n", " for filename in glob.glob('tmp_*.png'):\n", " os.remove(filename)\n", "\n", " # Call solver and do the simulaton\n", " user_action = plot_u if animate else None\n", " u, x, t, cpu = solver_function(\n", " I, V, f, c, L, dt, C, T, user_action)\n", "\n", " # Make video files\n", " fps = 4 # frames per second\n", " codec2ext = dict(flv='flv', libx264='mp4', libvpx='webm',\n", " libtheora='ogg') # video formats\n", " filespec = 'tmp_%04d.png'\n", " movie_program = 'ffmpeg'\n", " for codec in codec2ext:\n", " ext = codec2ext[codec]\n", " cmd = '%(movie_program)s -r %(fps)d -i %(filespec)s '\\\n", " '-vcodec %(codec)s movie.%(ext)s' % vars()\n", " os.system(cmd)\n", "\n", " if tool == 'scitools':\n", " # Make an HTML play for showing the animation in a browser\n", " plt.movie('tmp_*.png', encoder='html', fps=fps,\n", " output_file='movie.html')\n", " return cpu" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Dissection of the code\n", "\n", "The `viz` function can either use SciTools or Matplotlib for\n", "visualizing the solution. The `user_action` function based on SciTools\n", "is called `plot_u_st`, while the `user_action` function based on\n", "Matplotlib is a bit more complicated as it is realized as a class and\n", "needs statements that differ from those for making static plots.\n", "SciTools can utilize both Matplotlib and Gnuplot (and many other\n", "plotting programs) for doing the graphics, but Gnuplot is a relevant\n", "choice for large $N_x$ or in two-dimensional problems\n", "as Gnuplot is significantly faster than\n", "Matplotlib for screen animations.\n", "\n", "\n", "A function inside another function, like `plot_u_st` in the above code\n", "segment, has access to *and remembers* all the local variables in the\n", "surrounding code inside the `viz` function (!). This is known in\n", "computer science as a *closure* and is very convenient to program\n", "with. For example, the `plt` and `time` modules defined outside\n", "`plot_u` are accessible for `plot_u_st` when the function is called\n", "(as `user_action`) in the `solver` function. Some may think, however,\n", "that a class instead of a closure is a cleaner and\n", "easier-to-understand implementation of the user action function, see\n", "the section [wave:pde2:software](#wave:pde2:software).\n", "\n", "The `plot_u_st` function just makes a standard SciTools `plot` command\n", "for plotting `u` as a function of `x` at time `t[n]`. To achieve a\n", "smooth animation, the `plot` command should take keyword arguments\n", "instead of being broken into separate calls to `xlabel`, `ylabel`,\n", "`axis`, `time`, and `show`. Several `plot` calls will automatically\n", "cause an animation on the screen. In addition, we want to save each\n", "frame in the animation to file. We then need a filename where the\n", "frame number is padded with zeros, here `tmp_0000.png`,\n", "`tmp_0001.png`, and so on. The proper printf construction is then\n", "`tmp_%04d.png`.\n", "% if BOOK == \"book\":\n", "the section [vib:ode1:anim](#vib:ode1:anim) contains more basic\n", "information on making animations.\n", "% endif\n", "\n", "The solver is called with an argument `plot_u` as `user_function`.\n", "If the user chooses to use SciTools, `plot_u` is the `plot_u_st`\n", "callback function, but for Matplotlib it is an instance of the\n", "class `PlotMatplotlib`. Also this class makes use of variables\n", "defined in the `viz` function: `plt` and `time`.\n", "With Matplotlib, one has to make the first plot the standard way, and\n", "then update the $y$ data in the plot at every time level. The update\n", "requires active use of the returned value from `plt.plot` in the first\n", "plot. This value would need to be stored in a local variable if we\n", "were to use a closure for the `user_action` function when doing the\n", "animation with Matplotlib. mathcal{I}_t is much easier to store the\n", "variable as a class attribute `self.lines`. Since the class is essentially a\n", "function, we implement the function as the special method `__call__`\n", "such that the instance `plot_u(u, x, t, n)` can be called as a standard\n", "callback function from `solver`.\n", "\n", "### Making movie files\n", "\n", "From the\n", "`frame_*.png` files containing the frames in the animation we can\n", "make video files.\n", "% if BOOK == \"book\":\n", "the section [vib:ode1:anim](#vib:ode1:anim) presents basic information on how to\n", "use the `ffmpeg` program for producing video files\n", "in different modern formats: Flash, MP4, Webm, and Ogg.\n", "% else:\n", "We use the `ffmpeg` program to combine individual\n", "plot files to movies in modern formats: Flash, MP4, Webm, and Ogg.\n", "A typical `ffmpeg` command for creating a movie file\n", "in Ogg format\n", "with 4 frames per second built from a collection of\n", "plot files with names generated by `frame_%04d.png`,\n", "look like" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " Terminal> ffmpeg -r 25 -i frame_%04d.png -c:v libtheora movie.ogg\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The different formats require different video encoders (`-c:v`) to\n", "be installed: Flash applies `flv`, WebM applies `libvpx`, and MP4\n", "applies `libx264`:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " Terminal> ffmpeg -r 25 -i frame_%04d.png -c:v flv movie.flv\n", " Terminal> ffmpeg -r 25 -i frame_%04d.png -c:v libvpx movie.webm\n", " Terminal> ffmpeg -r 25 -i frame_%04d.png -c:v libx264 movie.mp4\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Players like `vlc`, `mplayer`, `gxine`, and `totem`\n", "can be used to play these movie files.\n", "\n", "Note that padding the frame counter with zeros in the `frame_*.png`\n", "files, as specified by the `%04d` format, is essential so that the wildcard\n", "notation `frame_*.png` expands to the correct set of files.\n", "% endif\n", "\n", "\n", "The `viz` function creates an `ffmpeg` command\n", "with the proper arguments for each of the formats Flash, MP4, WebM,\n", "and Ogg. The task is greatly simplified by having a\n", "`codec2ext` dictionary for mapping\n", "video codec names to filename extensions.\n", "% if BOOK == \"book\":\n", "As mentioned in the section [vib:ode1:anim](#vib:ode1:anim), only\n", "% else:\n", "Only\n", "% endif\n", "two formats are actually needed to ensure that all browsers can\n", "successfully play the video: MP4 and WebM.\n", "\n", "Some animations having a large number of plot files may not\n", "be properly combined into a video using `ffmpeg`.\n", "A method that always works is to play the PNG files as an animation\n", "in a browser using JavaScript code in an HTML file.\n", "The SciTools package has a function `movie` (or a stand-alone command\n", "`scitools movie`) for creating such an HTML player. The `plt.movie`\n", "call in the `viz` function shows how the function is used.\n", "The file `movie.html` can be loaded into a browser and features\n", "a user interface where the speed of the animation can be controlled.\n", "Note that the movie in this case consists of the `movie.html` file\n", "and all the frame files `tmp_*.png`.\n", "\n", "\n", "### Skipping frames for animation speed\n", "\n", "Sometimes the time step is small and $T$ is large, leading to an\n", "inconveniently large number of plot files and a slow animation on the\n", "screen. The solution to such a problem is to decide on a total number\n", "of frames in the animation, `num_frames`, and plot the solution only for\n", "every `skip_frame` frames. For example, setting `skip_frame=5` leads\n", "to plots of every 5 frames. The default value `skip_frame=1` plots\n", "every frame.\n", "The total number of time levels (i.e., maximum\n", "possible number of frames) is the length of `t`, `t.size` (or `len(t)`),\n", "so if we want `num_frames` frames in the animation,\n", "we need to plot every `t.size/num_frames` frames:" ] }, { "cell_type": "code", "execution_count": 93, "metadata": {}, "outputs": [], "source": [ "def plot_u(t):\n", " skip_frame = int(t.size/float(num_frames))\n", " if n % skip_frame == 0 or n == t.size-1:\n", " st.plot(x, u, 'r-', ...)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The initial condition (`n=0`) is included by `n % skip_frame == 0`,\n", "as well as every `skip_frame`-th frame.\n", "As `n % skip_frame == 0` will very seldom be true for the\n", "very final frame, we must also check if `n == t.size-1` to\n", "get the final frame included.\n", "\n", "A simple choice of numbers may illustrate the formulas: say we have\n", "801 frames in total (`t.size`) and we allow only 60 frames to be\n", "plotted. As `n` then runs from 801 to 0, we need to plot every 801/60\n", "frame, which with integer division yields 13 as `skip_frame`. Using\n", "the mod function, `n % skip_frame`, this operation is zero every time\n", "`n` can be divided by 13 without a remainder. That is, the `if` test\n", "is true when `n` equals $0, 13, 26, 39, ..., 780, 801$. The associated\n", "code is included in the `plot_u` function, inside the `viz` function,\n", "in the file [`wave1D_u0.py`](${src_wave}/wave1D/wave1D_u0.py).\n", "\n", "## Running a case\n", "
\n", "\n", "The first demo of our 1D wave equation solver concerns vibrations of a\n", "string that is initially deformed to a triangular shape, like when picking\n", "a guitar string:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "
\n", "\n", "$$\n", "\\begin{equation}\n", "I(x) = \\left\\lbrace\n", "\\begin{array}{ll}\n", "ax/x_0, & x < x_0,\\\\ \n", "a(L-x)/(L-x_0), & \\hbox{otherwise}\n", "\\end{array}\\right.\n", "\\label{wave:pde1:guitar:I} \\tag{1}\n", "\\end{equation}\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We choose $L=75$ cm, $x_0=0.8L$, $a=5$ mm, and a time frequency\n", "$\\nu = 440$ Hz. The relation between the wave speed $c$ and $\\nu$ is\n", "$c=\\nu\\lambda$, where $\\lambda$ is the wavelength, taken as $2L$ because\n", "the longest wave on the string forms frac{1}{2} a wavelength. There is no\n", "external force, so $f=0$ (meaning we can neglect gravity),\n", "and the string is at rest initially, implying $V=0$.\n", "\n", "Regarding numerical parameters, we need to specify a $\\Delta t$.\n", "Sometimes it is more natural to think of a spatial resolution instead\n", "of a time step. A natural semi-coarse spatial resolution in the present\n", "problem is $N_x=50$. We can then choose the associated $\\Delta t$ (as required\n", "by the `viz` and `solver` functions) as the stability limit:\n", "$\\Delta t = L/(N_xc)$. This is the $\\Delta t$ to be specified,\n", "but notice that if $C<1$, the actual $\\Delta x$ computed in `solver` gets\n", "larger than $L/N_x$: $\\Delta x = c\\Delta t/C = L/(N_xC)$. (The reason\n", "is that we fix $\\Delta t$ and adjust $\\Delta x$, so if $C$ gets\n", "smaller, the code implements this effect in terms of a larger $\\Delta x$.)\n", "\n", "A function for setting the physical and numerical parameters and\n", "calling `viz` in this application goes as follows:" ] }, { "cell_type": "code", "execution_count": 94, "metadata": {}, "outputs": [], "source": [ "def guitar(C):\n", " \"\"\"Triangular wave (pulled guitar string).\"\"\"\n", " L = 0.75\n", " x0 = 0.8*L\n", " a = 0.005\n", " freq = 440\n", " wavelength = 2*L\n", " c = freq*wavelength\n", " omega = 2*pi*freq\n", " num_periods = 1\n", " T = 2*pi/omega*num_periods\n", " # Choose dt the same as the stability limit for Nx=50\n", " dt = L/50./c\n", "\n", " def I(x):\n", " return a*x/x0 if x < x0 else a/(L-x0)*(L-x)\n", "\n", " umin = -1.2*a; umax = -umin\n", " cpu = viz(I, 0, 0, c, L, dt, C, T, umin, umax,\n", " animate=True, tool='scitools')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The associated program has the name [`wave1D_u0.py`](${src_wave}/wave1D/wave1D_u0.py). Run\n", "the program and watch the [movie of the vibrating string](${doc_notes}/wave/html/mov-wave/guitar_C0.8/movie.html).\n", "The string should ideally consist of straight segments, but these are\n", "somewhat wavy due to numerical approximation. Run the case with the\n", "`wave1D_u0.py` code and $C=1$ to see the exact solution.\n", "\n", "\n", "## Working with a scaled PDE model\n", "\n", "\n", "Depending on the model, it may be a substantial job to establish\n", "consistent and relevant physical parameter values for a case. The\n", "guitar string example illustrates the point. However, by *scaling*\n", "the mathematical problem we can often reduce the need to estimate\n", "physical parameters dramatically. The scaling technique consists of\n", "introducing new independent and dependent variables, with the aim that\n", "the absolute values of these lie in $[0,1]$. We introduce the\n", "dimensionless variables (details are found in Section 3.1.1 in [[Langtangen_scaling]](#Langtangen_scaling))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$$\n", "\\bar x = \\frac{x}{L},\\quad \\bar t = \\frac{c}{L}t,\\quad\n", "\\bar u = \\frac{u}{a}\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here, $L$ is a typical length scale, e.g., the length of the domain,\n", "and $a$ is a typical size of $u$, e.g., determined from the\n", "initial condition: $a=\\max_x|I(x)|$.\n", "\n", "We get by the chain rule that" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$$\n", "\\frac{\\partial u}{\\partial t} =\n", "\\frac{\\partial}{\\partial\\bar t}\\left(a\\bar u\\right)\n", "\\frac{d\\bar t}{dt} =\n", "\\frac{ac}{L}\\frac{\\partial\\bar u}{\\partial\\bar t}\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Similarly," ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$$\n", "\\frac{\\partial u}{\\partial x}\n", "= \\frac{a}{L}\\frac{\\partial\\bar u}{\\partial\\bar x}\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Inserting the dimensionless variables in the PDE gives, in case $f=0$," ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$$\n", "\\frac{a^2c^2}{L^2}\\frac{\\partial^2\\bar u}{\\partial\\bar t^2}\n", "= \\frac{a^2c^2}{L^2}\\frac{\\partial^2\\bar u}{\\partial\\bar x^2}\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Dropping the bars, we arrive at the scaled PDE" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "
\n", "\n", "$$\n", "\\begin{equation}\n", "\\frac{\\partial^2 u}{\\partial t^2} = \\frac{\\partial^2 u}{\\partial x^2},\n", "\\quad x\\in (0,1),\\ t\\in (0,cT/L),\n", "\\label{_auto1} \\tag{2}\n", "\\end{equation}\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "which has no parameter $c^2$ anymore. The initial conditions are scaled\n", "as" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$$\n", "a\\bar u(\\bar x, 0) = I(L\\bar x)\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "and" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$$\n", "\\frac{a}{L/c}\\frac{\\partial\\bar u}{\\partial\\bar t}(\\bar x,0) = V(L\\bar x),\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "resulting in" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$$\n", "\\bar u(\\bar x, 0) = \\frac{I(L\\bar x)}{\\max_x |I(x)|},\\quad\n", "\\frac{\\partial\\bar u}{\\partial\\bar t}(\\bar x,0) = \\frac{L}{ac}V(L\\bar x)\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the common case $V=0$ we see that there are no physical parameters to be\n", "estimated in the PDE model!\n", "\n", "If we have a program implemented for the physical wave equation with\n", "dimensions, we can obtain the dimensionless, scaled version by\n", "setting $c=1$. The initial condition of a guitar string,\n", "given in ([1](#wave:pde1:guitar:I)), gets its scaled form by choosing\n", "$a=1$, $L=1$, and $x_0\\in [0,1]$. This means that we only need to\n", "decide on the $x_0$ value as a fraction of unity, because\n", "the scaled problem corresponds to setting all\n", "other parameters to unity. In the code we can just set\n", "`a=c=L=1`, `x0=0.8`, and there is no need to calculate with\n", "wavelengths and frequencies to estimate $c$!\n", "\n", "The only non-trivial parameter to estimate in the scaled problem\n", "is the final end time of the simulation, or more precisely, how it relates\n", "to periods in periodic solutions in time, since we often want to\n", "express the end time as a certain number of periods.\n", "The period in the dimensionless problem is 2, so the end time can be\n", "set to the desired number of periods times 2.\n", "\n", "Why the dimensionless period is 2 can be explained by the following\n", "reasoning.\n", "Suppose that $u$ behaves as $\\cos (\\omega t)$ in time in the original\n", "problem with dimensions. The corresponding period is then $P=2\\pi/\\omega$, but\n", "we need to estimate $\\omega$. A typical solution of the wave\n", "equation is $u(x,t)=A\\cos(kx)\\cos(\\omega t)$, where $A$ is an amplitude\n", "and $k$ is related to the wave length $\\lambda$ in space: $\\lambda = 2\\pi/k$.\n", "Both $\\lambda$ and $A$ will be given by the initial condition $I(x)$.\n", "Inserting this $u(x,t)$ in the PDE yields $-\\omega^2 = -c^2k^2$, i.e.,\n", "$\\omega = kc$. The period is therefore $P=2\\pi/(kc)$.\n", "If the boundary conditions are $u(0,t)=u(L,t)$, we need to have\n", "$kL = n\\pi$ for integer $n$. The period becomes $P=2L/nc$. The longest\n", "period is $P=2L/c$. The dimensionless period $\\tilde P$ is obtained\n", "by dividing $P$ by the time scale $L/c$, which results in $\\tilde P=2$.\n", "Shorter waves in the initial condition will have a dimensionless\n", "shorter period $\\tilde P=2/n$ ($n>1$).\n", "\n", "# Vectorization\n", "
\n", "\n", "\n", "The computational algorithm for solving the wave equation visits one\n", "mesh point at a time and evaluates a formula for the new value\n", "$u_i^{n+1}$ at that point. Technically, this is implemented by a loop\n", "over array elements in a program. Such loops may run slowly in Python\n", "(and similar interpreted languages such as R and MATLAB). One\n", "technique for speeding up loops is to perform operations on entire\n", "arrays instead of working with one element at a time. This is referred\n", "to as *vectorization*, *vector computing*, or *array computing*.\n", "Operations on whole arrays are possible if the computations involving\n", "each element is independent of each other and therefore can, at least\n", "in principle, be performed simultaneously. That is, vectorization not\n", "only speeds up the code on serial computers, but also makes it easy to\n", "exploit parallel computing. Actually, there are Python tools like\n", "[Numba](http://numba.pydata.org) that can automatically turn\n", "vectorized code into parallel code.\n", "\n", "\n", "## Operations on slices of arrays\n", "
\n", "\n", "\n", "Efficient computing with `numpy` arrays demands that we avoid loops\n", "and compute with entire arrays at once (or at least large portions of them).\n", "Consider this calculation of differences $d_i = u_{i+1}-u_i$:" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "def differences(u):\n", " n = u.size\n", " d = np.zeros(n-1)\n", " for i in range(0, n-1):\n", " d[i] = u[i+1] - u[i]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "All the differences here are independent of each other.\n", "The computation of `d` can therefore alternatively be done by\n", "subtracting the array $(u_0,u_1,\\ldots,u_{n-1})$ from\n", "the array where the elements are shifted one index upwards:\n", "$(u_1,u_2,\\ldots,u_n)$, see [Figure](#wave:pde1:vec:fig1).\n", "The former subset of the array can be\n", "expressed by `u[0:n-1]`,\n", "`u[0:-1]`, or just\n", "`u[:-1]`, meaning from index 0 up to,\n", "but not including, the last element (`-1`). The latter subset\n", "is obtained by `u[1:n]` or `u[1:]`,\n", "meaning from index 1 and the rest of the array.\n", "The computation of `d` can now be done without an explicit Python loop:\n", "\n", "`d = u[1:] - u[:-1]`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "or with explicit limits if desired:\n", "\n", "`d = u[1:n] - u[0:n-1]`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Indices with a colon, going from an index to (but not including) another\n", "index are called *slices*. With `numpy` arrays, the computations\n", "are still done by loops, but in efficient, compiled, highly optimized\n", "C or Fortran code. Such loops are sometimes referred to as *vectorized\n", "loops*. Such loops can also easily be distributed\n", "among many processors on parallel computers. We say that the *scalar code*\n", "above, working on an element (a scalar) at a time, has been replaced by\n", "an equivalent *vectorized code*. The process of vectorizing code is called\n", "*vectorization*.\n", "\n", "\n", "\n", "
\n", "\n", "

Illustration of subtracting two slices of two arrays.

\n", "\n", "\n", "\n", "\n", "\n", "**Test your understanding.**\n", "\n", "Newcomers to vectorization are encouraged to choose\n", "a small array `u`, say with five elements,\n", "and simulate with pen and paper\n", "both the loop version and the vectorized version above.\n", "\n", "\n", "\n", "Finite difference schemes basically contain differences between array\n", "elements with shifted indices. As an example,\n", "consider the updating formula" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "ename": "NameError", "evalue": "name 'n' is not defined", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0;32mfor\u001b[0m \u001b[0mi\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mn\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0mu2\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m+\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mNameError\u001b[0m: name 'n' is not defined" ] } ], "source": [ "for i in range(1, n-1):\n", " u2[i] = u[i-1] - 2*u[i] + u[i+1]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The vectorization consists of replacing the loop by arithmetics on\n", "slices of arrays of length `n-2`:" ] }, { "cell_type": "code", "execution_count": 63, "metadata": {}, "outputs": [ { "ename": "TypeError", "evalue": "unsupported operand type(s) for -: 'list' and 'list'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mu2\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0mu2\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0mn\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0mn\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0mn\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;31m# alternative\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mTypeError\u001b[0m: unsupported operand type(s) for -: 'list' and 'list'" ] } ], "source": [ "u2 = u[:-2] - 2*u[1:-1] + u[2:]\n", "u2 = u[0:n-2] - 2*u[1:n-1] + u[2:n] # alternative" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that the length of `u2` becomes `n-2`. If `u2` is already an array of\n", "length `n` and we want to use the formula to update all the \"inner\"\n", "elements of `u2`, as we will when solving a 1D wave equation, we can write" ] }, { "cell_type": "code", "execution_count": 64, "metadata": {}, "outputs": [ { "ename": "TypeError", "evalue": "unsupported operand type(s) for -: 'list' and 'list'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mu2\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0mu2\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0mn\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0mn\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0mn\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0mn\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;31m# alternative\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mTypeError\u001b[0m: unsupported operand type(s) for -: 'list' and 'list'" ] } ], "source": [ "u2[1:-1] = u[:-2] - 2*u[1:-1] + u[2:]\n", "u2[1:n-1] = u[0:n-2] - 2*u[1:n-1] + u[2:n] # alternative" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The first expression's right-hand side is realized by the\n", "following steps, involving temporary arrays with intermediate results,\n", "since each array operation can only involve one or two arrays.\n", "The `numpy` package performs (behind the scenes) the first line above in\n", "four steps:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " temp1 = 2*u[1:-1]\n", " temp2 = u[:-2] - temp1\n", " temp3 = temp2 + u[2:]\n", " u2[1:-1] = temp3\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We need three temporary arrays, but a user does not need to worry about\n", "such temporary arrays.\n", "\n", "**Common mistakes with array slices.**\n", "\n", "Array expressions with slices demand that the slices have the same\n", "shape. mathcal{I}_t easy to make a mistake in, e.g.," ] }, { "cell_type": "code", "execution_count": 65, "metadata": {}, "outputs": [ { "ename": "NameError", "evalue": "name 'n' is not defined", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mu2\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0mn\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0mn\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0mn\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0mn\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mNameError\u001b[0m: name 'n' is not defined" ] } ], "source": [ "u2[1:n-1] = u[0:n-2] - 2*u[1:n-1] + u[2:n]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "and write" ] }, { "cell_type": "code", "execution_count": 66, "metadata": {}, "outputs": [ { "ename": "NameError", "evalue": "name 'n' is not defined", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mu2\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0mn\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0mn\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0mn\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0mn\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mNameError\u001b[0m: name 'n' is not defined" ] } ], "source": [ "u2[1:n-1] = u[0:n-2] - 2*u[1:n-1] + u[1:n]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now `u[1:n]` has wrong length (`n-1`) compared to the other array\n", "slices, causing a `ValueError` and the message\n", "`could not broadcast input array from shape 103 into shape 104`\n", "(if `n` is 105). When such errors occur one must closely examine\n", "all the slices. Usually, it is easier to get upper limits of slices\n", "right when they use `-1` or `-2` or empty limit rather than\n", "expressions involving the length.\n", "\n", "Another common mistake, when `u2` has length `n`, is to forget the slice in the array on the\n", "left-hand side," ] }, { "cell_type": "code", "execution_count": 67, "metadata": {}, "outputs": [ { "ename": "NameError", "evalue": "name 'n' is not defined", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mu2\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0mn\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0mn\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0mn\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mNameError\u001b[0m: name 'n' is not defined" ] } ], "source": [ "u2 = u[0:n-2] - 2*u[1:n-1] + u[1:n]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is really crucial: now `u2` becomes a *new* array of length\n", "`n-2`, which is the wrong length as we have no entries for the boundary\n", "values. We meant to insert the right-hand side array *into* the\n", "original `u2` array for the entries that correspond to the\n", "internal points in the mesh (`1:n-1` or `1:-1`).\n", "\n", "\n", "\n", "Vectorization may also work nicely with functions. To illustrate, we may\n", "extend the previous example as follows:" ] }, { "cell_type": "code", "execution_count": 73, "metadata": {}, "outputs": [ { "ename": "IndexError", "evalue": "list index out of range", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mIndexError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 6\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mi\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mn\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 8\u001b[0;31m \u001b[0mu2\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m+\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mf\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mIndexError\u001b[0m: list index out of range" ] } ], "source": [ "def f(x):\n", " return x**2 + 1\n", "\n", "n = 50\n", "u2 = np.zeros(n+1)\n", "\n", "for i in range(1, n-1):\n", " u2[i] = u[i-1] - 2*u[i] + u[i+1] + f(x[i])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Assuming `u2`, `u`, and `x` all have length `n`, the vectorized\n", "version becomes" ] }, { "cell_type": "code", "execution_count": 74, "metadata": {}, "outputs": [ { "ename": "TypeError", "evalue": "unsupported operand type(s) for -: 'list' and 'list'", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mu2\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m2\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mf\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mTypeError\u001b[0m: unsupported operand type(s) for -: 'list' and 'list'" ] } ], "source": [ "u2[1:-1] = u[:-2] - 2*u[1:-1] + u[2:] + f(x[1:-1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Obviously, `f` must be able to take an array as argument for `f(x[1:-1])`\n", "to make sense.\n", "\n", "## Finite difference schemes expressed as slices\n", "
\n", "\n", "\n", "We now have the necessary tools to vectorize the wave equation\n", "algorithm as described mathematically in the section [wave:string:alg](#wave:string:alg)\n", "and through code in the section [The solver function](#wave:pde1:impl:solver). There are\n", "three loops: one for the initial condition, one for the first time\n", "step, and finally the loop that is repeated for all subsequent time\n", "levels. Since only the latter is repeated a potentially large number\n", "of times, we limit our vectorization efforts to this loop. Within the time\n", "loop, the space loop reads:" ] }, { "cell_type": "code", "execution_count": 75, "metadata": {}, "outputs": [ { "ename": "NameError", "evalue": "name 'Nx' is not defined", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0;32mfor\u001b[0m \u001b[0mi\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mNx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0mu\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0mu_n\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0mu_nm1\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m\u001b[0;31m \u001b[0m\u001b[0;31m\\\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0mC2\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mu_n\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0mu_n\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mu_n\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m+\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mNameError\u001b[0m: name 'Nx' is not defined" ] } ], "source": [ "for i in range(1, Nx):\n", " u[i] = 2*u_n[i] - u_nm1[i] + \\\n", " C2*(u_n[i-1] - 2*u_n[i] + u_n[i+1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The vectorized version becomes" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "u[1:-1] = - u_nm1[1:-1] + 2*u_n[1:-1] + \\\n", " C2*(u_n[:-2] - 2*u_n[1:-1] + u_n[2:])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "or" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [], "source": [ "u[1:Nx] = 2*u_n[1:Nx]- u_nm1[1:Nx] + \\\n", " C2*(u_n[0:Nx-1] - 2*u_n[1:Nx] + u_n[2:Nx+1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "\n", "\n", "The program\n", "[`wave1D_u0v.py`](${src_wave}/wave1D/wave1D_u0v.py)\n", "contains a new version of the function `solver` where both the scalar\n", "and the vectorized loops are included (the argument `version` is\n", "set to `scalar` or `vectorized`, respectively).\n", "\n", "\n", "## Verification\n", "
\n", "\n", "\n", "We may reuse the quadratic solution $\\uex(x,t)=x(L-x)(1+{\\frac{1}{2}}t)$ for\n", "verifying also the vectorized code. A test function can now verify\n", "both the scalar and the vectorized version. Moreover, we may\n", "use a `user_action` function that compares the computed and exact\n", "solution at each time level and performs a test:" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [], "source": [ "def test_quadratic():\n", " \"\"\"\n", " Check the scalar and vectorized versions for\n", " a quadratic u(x,t)=x(L-x)(1+t/2) that is exactly reproduced.\n", " \"\"\"\n", " # The following function must work for x as array or scalar\n", " u_exact = lambda x, t: x*(L - x)*(1 + 0.5*t)\n", " I = lambda x: u_exact(x, 0)\n", " V = lambda x: 0.5*u_exact(x, 0)\n", " # f is a scalar (zeros_like(x) works for scalar x too)\n", " f = lambda x, t: np.zeros_like(x) + 2*c**2*(1 + 0.5*t)\n", "\n", " L = 2.5\n", " c = 1.5\n", " C = 0.75\n", " Nx = 3 # Very coarse mesh for this exact test\n", " dt = C*(L/Nx)/c\n", " T = 18\n", "\n", " def assert_no_error(u, x, t, n):\n", " u_e = u_exact(x, t[n])\n", " tol = 1E-13\n", " diff = np.abs(u - u_e).max()\n", " assert diff < tol\n", "\n", " solver(I, V, f, c, L, dt, C, T,\n", " user_action=assert_no_error, version='scalar')\n", " solver(I, V, f, c, L, dt, C, T,\n", " user_action=assert_no_error, version='vectorized')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Lambda functions.**\n", "\n", "The code segment above demonstrates how to achieve very\n", "compact code, without degraded readability,\n", "by use of lambda functions for the various\n", "input parameters that require a Python function. In essence," ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [], "source": [ "f = lambda x, t: L*(x-t)**2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "is equivalent to" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [], "source": [ "def f(x, t):\n", " return L(x-t)**2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that lambda functions can just contain a single expression and no\n", "statements.\n", "\n", "One advantage with lambda functions is that they can be used directly\n", "in calls:" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [], "source": [ "solver(I=lambda x: sin(pi*x/L), V=0, f=0, ...)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Efficiency measurements\n", "\n", "\n", "The `wave1D_u0v.py` contains our new `solver` function with both\n", "scalar and vectorized code. For comparing the efficiency\n", "of scalar versus vectorized code, we need a `viz` function\n", "as discussed in the section [Visualization: animating the solution](#wave:pde1:impl:animate).\n", "All of this `viz` function can be reused, except the call\n", "to `solver_function`. This call lacks the parameter\n", "`version`, which we want to set to `vectorized` and `scalar`\n", "for our efficiency measurements.\n", "\n", "One solution is to copy the `viz` code from `wave1D_u0` into\n", "`wave1D_u0v.py` and add a `version` argument to the `solver_function` call.\n", "Taking into account how much animation code we\n", "then duplicate, this is not a good idea.\n", "Alternatively,\n", "introducing the `version` argument in `wave1D_u0.viz`, so that this function\n", "can be imported into `wave1D_u0v.py`, is not\n", "a good solution either, since `version` has no meaning in that file.\n", "We need better ideas!\n", "\n", "### Solution 1\n", "\n", "Calling `viz` in `wave1D_u0` with `solver_function` as our new\n", "solver in `wave1D_u0v` works fine, since this solver has\n", "`version='vectorized'` as default value. The problem arises when we\n", "want to test `version='scalar'`. The simplest solution is then\n", "to use `wave1D_u0.solver` instead. We make a new `viz` function\n", "in `wave1D_u0v.py` that has a `version` argument and that just\n", "calls `wave1D_u0.viz`:" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [], "source": [ "def viz(\n", " I, V, f, c, L, dt, C, T, # PDE parameters\n", " umin, umax, # Interval for u in plots\n", " animate=True, # Simulation with animation?\n", " tool='matplotlib', # 'matplotlib' or 'scitools'\n", " solver_function=solver, # Function with numerical algorithm\n", " version='vectorized', # 'scalar' or 'vectorized'\n", " ):\n", " import wave1D_u0\n", " if version == 'vectorized':\n", " # Reuse viz from wave1D_u0, but with the present\n", " # modules' new vectorized solver (which has\n", " # version='vectorized' as default argument;\n", " # wave1D_u0.viz does not feature this argument)\n", " cpu = wave1D_u0.viz(\n", " I, V, f, c, L, dt, C, T, umin, umax,\n", " animate, tool, solver_function=solver)\n", " elif version == 'scalar':\n", " # Call wave1D_u0.viz with a solver with\n", " # scalar code and use wave1D_u0.solver.\n", " cpu = wave1D_u0.viz(\n", " I, V, f, c, L, dt, C, T, umin, umax,\n", " animate, tool,\n", " solver_function=wave1D_u0.solver)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Solution 2\n", "\n", "There is a more advanced and fancier solution featuring a very useful trick:\n", "we can make a new function that will always call `wave1D_u0v.solver`\n", "with `version='scalar'`. The `functools.partial` function from\n", "standard Python takes a function `func` as argument and\n", "a series of positional and keyword arguments and returns a\n", "new function that will call `func` with the supplied arguments,\n", "while the user can control all the other arguments in `func`.\n", "Consider a trivial example," ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [], "source": [ "def f(a, b, c=2):\n", " return a + b + c" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We want to ensure that `f` is always called with `c=3`, i.e., `f`\n", "has only two \"free\" arguments `a` and `b`.\n", "This functionality is obtained by" ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [], "source": [ "import functools\n", "f2 = functools.partial(f, c=3)\n", "\n", "print f2(1, 2) # results in 1+2+3=6" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now `f2` calls `f` with whatever the user supplies as `a` and `b`,\n", "but `c` is always `3`.\n", "\n", "Back to our `viz` code, we can do" ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [], "source": [ "import functools\n", "# Call wave1D_u0.solver with version fixed to scalar\n", "scalar_solver = functools.partial(wave1D_u0.solver, version='scalar')\n", "cpu = wave1D_u0.viz(\n", " I, V, f, c, L, dt, C, T, umin, umax,\n", " animate, tool, solver_function=scalar_solver)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The new `scalar_solver` takes the same arguments as\n", "`wave1D_u0.scalar` and calls `wave1D_u0v.scalar`,\n", "but always supplies the extra argument\n", "`version='scalar'`. When sending this `solver_function`\n", "to `wave1D_u0.viz`, the latter will call `wave1D_u0v.solver`\n", "with all the `I`, `V`, `f`, etc., arguments we supply, plus\n", "`version='scalar'`.\n", "\n", "### Efficiency experiments\n", "\n", "We now have a `viz` function that can call our solver function both in\n", "scalar and vectorized mode. The function `run_efficiency_experiments`\n", "in `wave1D_u0v.py` performs a set of experiments and reports the\n", "CPU time spent in the scalar and vectorized solver for\n", "the previous string vibration example with spatial mesh resolutions\n", "$N_x=50,100,200,400,800$. Running this function reveals\n", "that the vectorized\n", "code runs substantially faster: the vectorized code runs approximately\n", "$N_x/10$ times as fast as the scalar code!\n", "\n", "## Remark on the updating of arrays\n", "
\n", "\n", "\n", "At the end of each time step we need to update the `u_nm1` and `u_n`\n", "arrays such that they have the right content for the next time step:" ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [], "source": [ "u_nm1[:] = u_n\n", "u_n[:] = u" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The order here is important: updating `u_n` first, makes `u_nm1` equal\n", "to `u`, which is wrong!\n", "\n", "The assignment `u_n[:] = u` copies the content of the `u` array into\n", "the elements of the `u_n` array. Such copying takes time, but\n", "that time is negligible compared to the time needed for\n", "computing `u` from the finite difference formula,\n", "even when the formula has a vectorized implementation.\n", "However, efficiency of program code is a key topic when solving\n", "PDEs numerically (particularly when there are two or three\n", "space dimensions), so it must be mentioned that there exists a\n", "much more efficient way of making the arrays `u_nm1` and `u_n`\n", "ready for the next time step. The idea is based on *switching\n", "references* and explained as follows.\n", "\n", "A Python variable is actually a reference to some object (C programmers\n", "may think of pointers). Instead of copying data, we can let `u_nm1`\n", "refer to the `u_n` object and `u_n` refer to the `u` object.\n", "This is a very efficient operation (like switching pointers in C).\n", "A naive implementation like" ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [], "source": [ "u_nm1 = u_n\n", "u_n = u" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "will fail, however, because now `u_nm1` refers to the `u_n` object,\n", "but then the name `u_n` refers to `u`, so that this `u` object\n", "has two references, `u_n` and `u`, while our third array, originally\n", "referred to by `u_nm1`, has no more references and is lost.\n", "This means that the variables `u`, `u_n`, and `u_nm1` refer to two\n", "arrays and not three. Consequently, the computations at the next\n", "time level will be messed up, since updating the elements in\n", "`u` will imply updating the elements in `u_n` too, thereby destroying\n", "the solution at the previous time step.\n", "\n", "While `u_nm1 = u_n` is fine, `u_n = u` is problematic, so\n", "the solution to this problem is to ensure that `u`\n", "points to the `u_nm1` array. This is mathematically wrong, but\n", "new correct values will be filled into `u` at the next time step\n", "and make it right.\n", "\n", "The correct switch of references is" ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [], "source": [ "tmp = u_nm1\n", "u_nm1 = u_n\n", "u_n = u\n", "u = tmp" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can get rid of the temporary reference `tmp` by writing" ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [], "source": [ "u_nm1, u_n, u = u_n, u, u_nm1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This switching of references for updating our arrays\n", "will be used in later implementations.\n", "\n", "**Caution:**\n", "\n", "The update `u_nm1, u_n, u = u_n, u, u_nm1` leaves wrong content in `u`\n", "at the final time step. This means that if we return `u`, as we\n", "do in the example codes here, we actually return `u_nm1`, which is\n", "obviously wrong. mathcal{I}_t is therefore important to adjust the content\n", "of `u` to `u = u_n` before returning `u`. (Note that\n", "the `user_action` function\n", "reduces the need to return the solution from the solver.)\n", "\n", "\n", "\n", "\n", "\n", "# Exercises\n", "\n", "\n", "\n", "\n", "\n", "## Exercise 1: Simulate a standing wave\n", "
\n", "\n", "The purpose of this exercise is to simulate standing waves on $[0,L]$\n", "and illustrate the error in the simulation.\n", "Standing waves arise from an initial condition" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$$\n", "u(x,0)= A \\sin\\left(\\frac{\\pi}{L}mx\\right),\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "where $m$ is an integer and $A$ is a freely chosen amplitude.\n", "The corresponding exact solution can be computed and reads" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$$\n", "\\uex(x,t) = A\\sin\\left(\\frac{\\pi}{L}mx\\right)\n", "\\cos\\left(\\frac{\\pi}{L}mct\\right)\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**a)**\n", "Explain that for a function $\\sin kx\\cos \\omega t$ the wave length\n", "in space is $\\lambda = 2\\pi /k$ and the period in time is $P=2\\pi/\\omega$.\n", "Use these expressions to find the wave length in space and period in\n", "time of $\\uex$ above.\n", "\n", "\n", "\n", "**Solution.**\n", "Since the sin and cos functions depend on $x$ and $t$, respectively,\n", "the sin function will run through one period as $x$ increases by $\\frac{2\\pi}{k}$, while the cos function starts repeating as $t$ increases by $\\frac{2\\pi}{\\omega}$.\n", "\n", "The wave length in space becomes" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$$\n", "\\lambda = \\frac{2\\pi}{\\frac{\\pi}{L}m} = \\frac{2L}{m}\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The period in time becomes" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$$\n", "P = \\frac{2\\pi}{\\frac{\\pi}{L}mc} = \\frac{2L}{mc}\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "**b)**\n", "Import the `solver` function from `wave1D_u0.py` into a new file\n", "where the `viz` function is reimplemented such that it\n", "plots either the numerical *and* the exact solution, *or* the error.\n", "\n", "\n", "\n", "**Solution.**\n", "See code below.\n", "\n", "\n", "\n", "**c)**\n", "Make animations where you illustrate how the error\n", "$e^n_i =\\uex(x_i, t_n)- u^n_i$\n", "develops and increases in time. Also make animations of\n", "$u$ and $\\uex$ simultaneously.\n", "\n", "\n", "\n", "**Hint 1.**\n", "Quite long time simulations are needed in order to display significant\n", "discrepancies between the numerical and exact solution.\n", "\n", "\n", "\n", "\n", "\n", "**Hint 2.**\n", "A possible set of parameters is $L=12$, $m=9$, $c=2$, $A=1$, $N_x=80$,\n", "$C=0.8$. The error mesh function $e^n$ can be simulated for 10 periods,\n", "while 20-30 periods are needed to show significant differences between\n", "the curves for the numerical and exact solution.\n", "\n", "\n", "\n", "\n", "\n", "**Solution.**\n", "The code:" ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [], "source": [ "import sys, os\n", "sys.path.insert(0, os.path.join(os.pardir, os.pardir, \n", " 'src-wave', 'wave1D'))\n", "\n", "from wave1D_u0 import solver\n", "#from wave1D_u0v import solver # allows faster vectorized operations\n", "import numpy as np\n", "\n", "def viz(I, V, f, c, L, dt, C, T,\n", " ymax, # y axis: [-ymax, ymax]\n", " u_exact, # u_exact(x, t)\n", " animate='u and u_exact', # or 'error'\n", " movie_filename='movie',\n", " ):\n", " \"\"\"Run solver and visualize u at each time level.\"\"\"\n", " import scitools.std as plt\n", " import time, glob, os\n", "\n", " class Plot:\n", " def __init__(self, ymax, frame_name='frame'):\n", " self.max_error = [] # hold max amplitude errors\n", " self.max_error_t = [] # time points corresp. to max_error\n", " self.frame_name = frame_name\n", " self.ymax = ymax\n", "\n", " def __call__(self, u, x, t, n):\n", " \"\"\"user_action function for solver.\"\"\"\n", " if animate == 'u and u_exact':\n", " plt.plot(x, u, 'r-',\n", " x, u_exact(x, t[n]), 'b--',\n", " xlabel='x', ylabel='u',\n", " axis=[0, L, -self.ymax, self.ymax],\n", " title='t=%f' % t[n], show=True)\n", " else:\n", " error = u_exact(x, t[n]) - u\n", " local_max_error = np.abs(error).max()\n", " # self.max_error holds the increasing amplitude error\n", " if self.max_error == [] or \\\n", " local_max_error > max(self.max_error):\n", " self.max_error.append(local_max_error)\n", " self.max_error_t.append(t[n])\n", " # Use user's ymax until the error exceeds that value.\n", " # This gives a growing max value of the yaxis (but\n", " # never shrinking)\n", " self.ymax = max(self.ymax, max(self.max_error))\n", " plt.plot(x, error, 'r-',\n", " xlabel='x', ylabel='error',\n", " axis=[0, L, -self.ymax, self.ymax],\n", " title='t=%f' % t[n], show=True)\n", " plt.savefig('%s_%04d.png' % (self.frame_name, n))\n", "\n", " # Clean up old movie frames\n", " for filename in glob.glob('frame_*.png'):\n", " os.remove(filename)\n", "\n", " plot = Plot(ymax)\n", " u, x, t, cpu = solver(I, V, f, c, L, dt, C, T, plot)\n", "\n", " # Make plot of max error versus time\n", " plt.figure()\n", " plt.plot(plot.max_error_t, plot.max_error)\n", " plt.xlabel('time'); plt.ylabel('max abs(error)')\n", " plt.savefig('error.png')\n", " plt.savefig('error.pdf')\n", "\n", " # Make .flv movie file\n", " fps = 4 # Frames per second\n", " codec2ext = dict(flv='flv') #, libx64='mp4', \n", " #libvpx='webm', libtheora='ogg')\n", "\n", " filespec = 'frame_%04d.png'\n", " movie_program = 'ffmpeg'\n", " for codec in codec2ext:\n", " ext = codec2ext[codec]\n", " cmd = '%(movie_program)s -r %(fps)d -i %(filespec)s '\\\n", " '-vcodec %(codec)s %(movie_filename)s.%(ext)s' % vars()\n", " os.system(cmd)\n", "\n", "def simulations():\n", " from numpy import sin, cos, pi\n", " L = 12 # length of domain\n", " m = 9 # 2L/m: wave length or period in space (2*pi/k, k=pi*m/L)\n", " c = 2 # wave velocity\n", " A = 1 # amplitude\n", " Nx = 80\n", " C = 0.8\n", " P = 2*pi/(pi*m*c/L) # 1 period in time\n", " #T = 10*P\n", " # Choose dt the same as the stability limit for Nx=50\n", " dt = L/50./c \n", "\n", " def u_exact(x, t):\n", " return A*sin(pi*m*x/L)*cos(pi*m*c*t/L)\n", "\n", " def I(x):\n", " return u_exact(x, 0)\n", "\n", " V = 0\n", " f = 0\n", "\n", " viz(I, V, f, c, L, dt, C, 10.5*P,\n", " 0.1, u_exact,\n", " animate='error',\n", " movie_filename='error')\n", "\n", " # Very long simulation to demonstrate different curves\n", " viz(I, V, f, c, L, dt, C, 30*P,\n", " 1.2*A, u_exact,\n", " animate='u and u_exact',\n", " movie_filename='solution')\n", "\n", "if __name__ == '__main__':\n", " simulations()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "\n", "Filename: `wave_standing`.\n", "\n", "\n", "\n", "### Remarks\n", "\n", "The important\n", "parameters for numerical quality are $C$ and $k\\Delta x$, where\n", "$C=c\\Delta t/\\Delta x$ is the Courant number and $k$ is defined above\n", "($k\\Delta x$ is proportional to how many mesh points we have per wave length\n", "in space, see the section [wave:pde1:num:dispersion](#wave:pde1:num:dispersion) for explanation).\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "## Exercise 2: Add storage of solution in a user action function\n", "
\n", "\n", "Extend the `plot_u` function in the file `wave1D_u0.py` to also store\n", "the solutions `u` in a list.\n", "To this end, declare `all_u` as\n", "an empty list in the `viz` function, outside `plot_u`, and perform\n", "an append operation inside the `plot_u` function. Note that a\n", "function, like `plot_u`, inside another function, like `viz`,\n", "remembers all local variables in `viz` function, including `all_u`,\n", "even when `plot_u` is called (as `user_action`) in the `solver` function.\n", "Test both `all_u.append(u)` and `all_u.append(u.copy())`.\n", "Why does one of these constructions fail to store the solution correctly?\n", "Let the `viz` function return the `all_u` list\n", "converted to a two-dimensional `numpy` array.\n", "\n", "\n", "\n", "**Solution.**\n", "We have to explicitly use a copy of u, i.e. as `all_u.append(u.copy())`, otherwise we just get a reference to `u`, which goes on changing with the computations." ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [], "source": [ "#!/usr/bin/env python\n", "\"\"\"\n", "1D wave equation with u=0 at the boundary.\n", "Simplest possible implementation.\n", "\n", "The key function is::\n", "\n", " u, x, t, cpu = solver(I, V, f, c, L, dt, C, T, user_action)\n", "\n", "which solves the wave equation u_tt = c**2*u_xx on (0,L) with u=0\n", "on x=0,L, for t in (0,T]. Initial conditions: u=I(x), u_t=V(x).\n", "\n", "T is the stop time for the simulation.\n", "dt is the desired time step.\n", "C is the Courant number (=c*dt/dx), which specifies dx.\n", "f(x,t) is a function for the source term (can be 0 or None).\n", "I and V are functions of x.\n", "\n", "user_action is a function of (u, x, t, n) where the calling\n", "code can add visualization, error computations, etc.\n", "\"\"\"\n", "\n", "import numpy as np\n", "\n", "def solver(I, V, f, c, L, dt, C, T, user_action=None):\n", " \"\"\"Solve u_tt=c^2*u_xx + f on (0,L)x(0,T].\"\"\"\n", " Nt = int(round(T/dt))\n", " t = np.linspace(0, Nt*dt, Nt+1) # Mesh points in time\n", " dx = dt*c/float(C)\n", " Nx = int(round(L/dx))\n", " x = np.linspace(0, L, Nx+1) # Mesh points in space\n", " C2 = C**2 # Help variable in the scheme\n", " # Make sure dx and dt are compatible with x and t\n", " dx = x[1] - x[0]\n", " dt = t[1] - t[0]\n", "\n", " if f is None or f == 0 :\n", " f = lambda x, t: 0\n", " if V is None or V == 0:\n", " V = lambda x: 0\n", "\n", " u = np.zeros(Nx+1) # Solution array at new time level\n", " u_1 = np.zeros(Nx+1) # Solution at 1 time level back\n", " u_2 = np.zeros(Nx+1) # Solution at 2 time levels back\n", "\n", " import time; t0 = time.clock() # for measuring CPU time\n", "\n", " # Load initial condition into u_1\n", " for i in range(0,Nx+1):\n", " u_1[i] = I(x[i])\n", "\n", " if user_action is not None:\n", " user_action(u_1, x, t, 0)\n", "\n", " # Special formula for first time step\n", " n = 0\n", " for i in range(1, Nx):\n", " u[i] = u_1[i] + dt*V(x[i]) + \\\n", " 0.5*C2*(u_1[i-1] - 2*u_1[i] + u_1[i+1]) + \\\n", " 0.5*dt**2*f(x[i], t[n])\n", " u[0] = 0; u[Nx] = 0\n", "\n", " if user_action is not None:\n", " user_action(u, x, t, 1)\n", "\n", " # Switch variables before next step\n", " u_2[:] = u_1; u_1[:] = u\n", "\n", " for n in range(1, Nt):\n", " # Update all inner points at time t[n+1]\n", " for i in range(1, Nx):\n", " u[i] = - u_2[i] + 2*u_1[i] + \\\n", " C2*(u_1[i-1] - 2*u_1[i] + u_1[i+1]) + \\\n", " dt**2*f(x[i], t[n])\n", "\n", " # Insert boundary conditions\n", " u[0] = 0; u[Nx] = 0\n", " if user_action is not None:\n", " if user_action(u, x, t, n+1):\n", " break\n", "\n", " # Switch variables before next step\n", " u_2[:] = u_1; u_1[:] = u\n", "\n", " cpu_time = t0 - time.clock()\n", " return u, x, t, cpu_time\n", "\n", "def test_quadratic():\n", " \"\"\"Check that u(x,t)=x(L-x)(1+t/2) is exactly reproduced.\"\"\"\n", "\n", " def u_exact(x, t):\n", " return x*(L-x)*(1 + 0.5*t)\n", "\n", " def I(x):\n", " return u_exact(x, 0)\n", "\n", " def V(x):\n", " return 0.5*u_exact(x, 0)\n", "\n", " def f(x, t):\n", " return 2*(1 + 0.5*t)*c**2\n", "\n", " L = 2.5\n", " c = 1.5\n", " C = 0.75\n", " Nx = 6 # Very coarse mesh for this exact test\n", " dt = C*(L/Nx)/c\n", " T = 18\n", "\n", " def assert_no_error(u, x, t, n):\n", " u_e = u_exact(x, t[n])\n", " diff = np.abs(u - u_e).max()\n", " tol = 1E-13\n", " assert diff < tol\n", "\n", " solver(I, V, f, c, L, dt, C, T,\n", " user_action=assert_no_error)\n", "\n", "def test_constant():\n", " \"\"\"Check that u(x,t)=Q=0 is exactly reproduced.\"\"\"\n", " u_const = 0 # Require 0 because of the boundary conditions\n", " C = 0.75\n", " dt = C # Very coarse mesh\n", " u, x, t, cpu = solver(I=lambda x:\n", " 0, V=0, f=0, c=1.5, L=2.5,\n", " dt=dt, C=C, T=18)\n", " tol = 1E-14\n", " assert np.abs(u - u_const).max() < tol\n", "\n", "def viz(\n", " I, V, f, c, L, dt, C, T, # PDE parameters\n", " umin, umax, # Interval for u in plots\n", " animate=True, # Simulation with animation?\n", " tool='matplotlib', # 'matplotlib' or 'scitools'\n", " solver_function=solver, # Function with numerical algorithm\n", " ):\n", " \"\"\"Run solver, store and visualize u at each time level.\"\"\"\n", "\n", " all_u = []\n", " def plot_u_st(u, x, t, n):\n", " \"\"\"user_action function for solver.\"\"\"\n", " plt.plot(x, u, 'r-',\n", " xlabel='x', ylabel='u',\n", " axis=[0, L, umin, umax],\n", " title='t=%f' % t[n], show=True)\n", " # Let the initial condition stay on the screen for 2\n", " # seconds, else insert a pause of 0.2 s between each plot\n", " time.sleep(2) if t[n] == 0 else time.sleep(0.2)\n", " #plt.savefig('frame_%04d.png' % n) # for movie making\n", " plt.savefig('tmp_%04d.png' % n) # for movie making \n", " all_u.append(u.copy()) \n", "\n", " class PlotMatplotlib:\n", " def __call__(self, u, x, t, n):\n", " \"\"\"user_action function for solver.\"\"\"\n", " if n == 0:\n", " plt.ion()\n", " self.lines = plt.plot(x, u, 'r-')\n", " plt.xlabel('x'); plt.ylabel('u')\n", " plt.axis([0, L, umin, umax])\n", " plt.legend(['t=%f' % t[n]], loc='lower left')\n", " else:\n", " self.lines[0].set_ydata(u)\n", " plt.legend(['t=%f' % t[n]], loc='lower left')\n", " plt.draw()\n", " time.sleep(2) if t[n] == 0 else time.sleep(0.2)\n", " plt.savefig('tmp_%04d.png' % n) # for movie making\n", "\n", " if tool == 'matplotlib':\n", " import matplotlib.pyplot as plt\n", " plot_u = PlotMatplotlib()\n", " elif tool == 'scitools':\n", " import scitools.std as plt # scitools.easyviz interface\n", " plot_u = plot_u_st\n", " import time, glob, os\n", "\n", " # Clean up old movie frames\n", " for filename in glob.glob('tmp_*.png'):\n", " os.remove(filename)\n", "\n", " # Call solver and do the simulaton\n", " user_action = plot_u if animate else None\n", " u, x, t, cpu = solver_function(\n", " I, V, f, c, L, dt, C, T, user_action)\n", "\n", " # Make video files\n", " fps = 4 # frames per second\n", " codec2ext = dict(flv='flv', libx264='mp4', libvpx='webm',\n", " libtheora='ogg') # video formats\n", " filespec = 'tmp_%04d.png'\n", " movie_program = 'ffmpeg'\n", " for codec in codec2ext:\n", " ext = codec2ext[codec]\n", " cmd = '%(movie_program)s -r %(fps)d -i %(filespec)s '\\\n", " '-vcodec %(codec)s movie.%(ext)s' % vars()\n", " os.system(cmd)\n", "\n", " if tool == 'scitools':\n", " # Make an HTML play for showing the animation in a browser\n", " plt.movie('tmp_*.png', encoder='html', fps=fps,\n", " output_file='movie.html')\n", " return cpu, np.array(all_u)\n", "\n", "def guitar(C):\n", " \"\"\"Triangular wave (pulled guitar string).\"\"\"\n", " L = 0.75\n", " x0 = 0.8*L\n", " a = 0.005\n", " freq = 440\n", " wavelength = 2*L\n", " c = freq*wavelength\n", " from math import pi\n", " w = 2*pi*freq\n", " num_periods = 1\n", " T = 2*pi/w*num_periods\n", " # Choose dt the same as the stability limit for Nx=50\n", " dt = L/50./c\n", "\n", " def I(x):\n", " return a*x/x0 if x < x0 else a/(L-x0)*(L-x)\n", "\n", " umin = -1.2*a; umax = -umin\n", " cpu, all_u = viz(I, 0, 0, c, L, dt, C, T, umin, umax,\n", " animate=True, tool='scitools')\n", " # checking\n", " #for e in all_u:\n", " # print e[int(len(all_u[1])/2)]\n", "\n", "if __name__ == '__main__':\n", " #test_quadratic()\n", " import sys\n", " try:\n", " C = float(sys.argv[1])\n", " print 'C=%g' % C\n", " except IndexError:\n", " C = 0.85\n", " print 'Courant number: %.2f' % C\n", " guitar(C)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "Filename: `wave1D_u0_s_store`.\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "## Exercise 3: Use a class for the user action function\n", "
\n", "\n", "Redo [Exercise 2: Add storage of solution in a user action function](#wave:exer:store:list) using a class for the user\n", "action function. Let the `all_u` list be an attribute in this class\n", "and implement the user action function as a method (the special method\n", "`__call__` is a natural choice). The class versions avoid that the\n", "user action function depends on parameters defined outside the\n", "function (such as `all_u` in [Exercise 2: Add storage of solution in a user action function](#wave:exer:store:list)).\n", "\n", "\n", "\n", "**Solution.**\n", "Using a class, we get" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [], "source": [ "#!/usr/bin/env python\n", "\"\"\"\n", "1D wave equation with u=0 at the boundary.\n", "Simplest possible implementation.\n", "\n", "The key function is::\n", "\n", " u, x, t, cpu = solver(I, V, f, c, L, dt, C, T, user_action)\n", "\n", "which solves the wave equation u_tt = c**2*u_xx on (0,L) with u=0\n", "on x=0,L, for t in (0,T]. Initial conditions: u=I(x), u_t=V(x).\n", "\n", "T is the stop time for the simulation.\n", "dt is the desired time step.\n", "C is the Courant number (=c*dt/dx), which specifies dx.\n", "f(x,t) is a function for the source term (can be 0 or None).\n", "I and V are functions of x.\n", "\n", "user_action is a function of (u, x, t, n) where the calling\n", "code can add visualization, error computations, etc.\n", "\"\"\"\n", "\n", "import numpy as np\n", "\n", "def solver(I, V, f, c, L, dt, C, T, user_action=None):\n", " \"\"\"Solve u_tt=c^2*u_xx + f on (0,L)x(0,T].\"\"\"\n", " Nt = int(round(T/dt))\n", " t = np.linspace(0, Nt*dt, Nt+1) # Mesh points in time\n", " dx = dt*c/float(C)\n", " Nx = int(round(L/dx))\n", " x = np.linspace(0, L, Nx+1) # Mesh points in space\n", " C2 = C**2 # Help variable in the scheme\n", " # Make sure dx and dt are compatible with x and t\n", " dx = x[1] - x[0]\n", " dt = t[1] - t[0]\n", "\n", " if f is None or f == 0 :\n", " f = lambda x, t: 0\n", " if V is None or V == 0:\n", " V = lambda x: 0\n", "\n", " u = np.zeros(Nx+1) # Solution array at new time level\n", " u_1 = np.zeros(Nx+1) # Solution at 1 time level back\n", " u_2 = np.zeros(Nx+1) # Solution at 2 time levels back\n", "\n", " import time; t0 = time.clock() # for measuring CPU time\n", "\n", " # Load initial condition into u_1\n", " for i in range(0,Nx+1):\n", " u_1[i] = I(x[i])\n", "\n", " if user_action is not None:\n", " user_action(u_1, x, t, 0)\n", "\n", " # Special formula for first time step\n", " n = 0\n", " for i in range(1, Nx):\n", " u[i] = u_1[i] + dt*V(x[i]) + \\\n", " 0.5*C2*(u_1[i-1] - 2*u_1[i] + u_1[i+1]) + \\\n", " 0.5*dt**2*f(x[i], t[n])\n", " u[0] = 0; u[Nx] = 0\n", "\n", " if user_action is not None:\n", " user_action(u, x, t, 1)\n", "\n", " # Switch variables before next step\n", " u_2[:] = u_1; u_1[:] = u\n", "\n", " for n in range(1, Nt):\n", " # Update all inner points at time t[n+1]\n", " for i in range(1, Nx):\n", " u[i] = - u_2[i] + 2*u_1[i] + \\\n", " C2*(u_1[i-1] - 2*u_1[i] + u_1[i+1]) + \\\n", " dt**2*f(x[i], t[n])\n", "\n", " # Insert boundary conditions\n", " u[0] = 0; u[Nx] = 0\n", " if user_action is not None:\n", " if user_action(u, x, t, n+1):\n", " break\n", "\n", " # Switch variables before next step\n", " u_2[:] = u_1; u_1[:] = u\n", "\n", " cpu_time = t0 - time.clock()\n", " return u, x, t, cpu_time\n", "\n", "def test_quadratic():\n", " \"\"\"Check that u(x,t)=x(L-x)(1+t/2) is exactly reproduced.\"\"\"\n", "\n", " def u_exact(x, t):\n", " return x*(L-x)*(1 + 0.5*t)\n", "\n", " def I(x):\n", " return u_exact(x, 0)\n", "\n", " def V(x):\n", " return 0.5*u_exact(x, 0)\n", "\n", " def f(x, t):\n", " return 2*(1 + 0.5*t)*c**2\n", "\n", " L = 2.5\n", " c = 1.5\n", " C = 0.75\n", " Nx = 6 # Very coarse mesh for this exact test\n", " dt = C*(L/Nx)/c\n", " T = 18\n", "\n", " def assert_no_error(u, x, t, n):\n", " u_e = u_exact(x, t[n])\n", " diff = np.abs(u - u_e).max()\n", " tol = 1E-13\n", " assert diff < tol\n", "\n", " solver(I, V, f, c, L, dt, C, T,\n", " user_action=assert_no_error)\n", "\n", "def test_constant():\n", " \"\"\"Check that u(x,t)=Q=0 is exactly reproduced.\"\"\"\n", " u_const = 0 # Require 0 because of the boundary conditions\n", " C = 0.75\n", " dt = C # Very coarse mesh\n", " u, x, t, cpu = solver(I=lambda x:\n", " 0, V=0, f=0, c=1.5, L=2.5,\n", " dt=dt, C=C, T=18)\n", " tol = 1E-14\n", " assert np.abs(u - u_const).max() < tol\n", "\n", "def viz(\n", " I, V, f, c, L, dt, C, T, # PDE parameters\n", " umin, umax, # Interval for u in plots\n", " animate=True, # Simulation with animation?\n", " tool='matplotlib', # 'matplotlib' or 'scitools'\n", " solver_function=solver, # Function with numerical algorithm\n", " ):\n", " \"\"\"Run solver, store and visualize u at each time level.\"\"\"\n", "\n", " class PlotUst:\n", " def __init__(self):\n", " self.all_u = []\n", " def __call__(self, u, x, t, n):\n", " \"\"\"user_action function for solver.\"\"\"\n", " plt.plot(x, u, 'r-',\n", " xlabel='x', ylabel='u',\n", " axis=[0, L, umin, umax],\n", " title='t=%f' % t[n], show=True)\n", " # Let the initial condition stay on the screen for 2\n", " # seconds, else insert a pause of 0.2 s between each plot\n", " time.sleep(2) if t[n] == 0 else time.sleep(0.2)\n", " plt.savefig('tmp_%04d.png' % n) # for movie making \n", " self.all_u.append(u.copy())\n", "\n", " class PlotMatplotlib:\n", " def __call__(self, u, x, t, n):\n", " \"\"\"user_action function for solver.\"\"\"\n", " if n == 0:\n", " plt.ion()\n", " self.lines = plt.plot(x, u, 'r-')\n", " plt.xlabel('x'); plt.ylabel('u')\n", " plt.axis([0, L, umin, umax])\n", " plt.legend(['t=%f' % t[n]], loc='lower left')\n", " else:\n", " self.lines[0].set_ydata(u)\n", " plt.legend(['t=%f' % t[n]], loc='lower left')\n", " plt.draw()\n", " time.sleep(2) if t[n] == 0 else time.sleep(0.2)\n", " plt.savefig('tmp_%04d.png' % n) # for movie making\n", "\n", " if tool == 'matplotlib':\n", " import matplotlib.pyplot as plt\n", " plot_u = PlotMatplotlib()\n", " elif tool == 'scitools':\n", " import scitools.std as plt # scitools.easyviz interface\n", " plot_u = PlotUst()\n", " import time, glob, os\n", "\n", " # Clean up old movie frames\n", " for filename in glob.glob('tmp_*.png'):\n", " os.remove(filename)\n", "\n", " # Call solver and do the simulaton\n", " user_action = plot_u if animate else None\n", " u, x, t, cpu = solver_function(\n", " I, V, f, c, L, dt, C, T, user_action)\n", "\n", " # Make video files\n", " fps = 4 # frames per second\n", " codec2ext = dict(flv='flv', libx264='mp4', libvpx='webm',\n", " libtheora='ogg') # video formats\n", " filespec = 'tmp_%04d.png'\n", " movie_program = 'ffmpeg'\n", " for codec in codec2ext:\n", " ext = codec2ext[codec]\n", " cmd = '%(movie_program)s -r %(fps)d -i %(filespec)s '\\\n", " '-vcodec %(codec)s movie.%(ext)s' % vars()\n", " os.system(cmd)\n", "\n", " if tool == 'scitools':\n", " # Make an HTML play for showing the animation in a browser\n", " plt.movie('tmp_*.png', encoder='html', fps=fps,\n", " output_file='movie.html')\n", " return cpu, np.array(plot_u.all_u)\n", "\n", "def guitar(C):\n", " \"\"\"Triangular wave (pulled guitar string).\"\"\"\n", " L = 0.75\n", " x0 = 0.8*L\n", " a = 0.005\n", " freq = 440\n", " wavelength = 2*L\n", " c = freq*wavelength\n", " from math import pi\n", " w = 2*pi*freq\n", " num_periods = 1\n", " T = 2*pi/w*num_periods\n", " # Choose dt the same as the stability limit for Nx=50\n", " dt = L/50./c\n", "\n", " def I(x):\n", " return a*x/x0 if x < x0 else a/(L-x0)*(L-x)\n", "\n", " umin = -1.2*a; umax = -umin\n", " cpu, all_u = viz(I, 0, 0, c, L, dt, C, T, umin, umax,\n", " animate=True, tool='scitools')\n", " # checking\n", " #for e in all_u:\n", " # print e[int(len(all_u[1])/2)]\n", "\n", "if __name__ == '__main__':\n", " #test_quadratic()\n", " import sys\n", " try:\n", " C = float(sys.argv[1])\n", " print 'C=%g' % C\n", " except IndexError:\n", " C = 0.85\n", " print 'Courant number: %.2f' % C\n", " guitar(C)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "Filename: `wave1D_u0_s2c`.\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "## Exercise 4: Compare several Courant numbers in one movie\n", "
\n", "\n", "The goal of this exercise is to make movies where several curves,\n", "corresponding to different Courant numbers, are visualized. Write a\n", "program that resembles `wave1D_u0_s2c.py` in [Exercise 3: Use a class for the user action function](#wave:exer:store:list:class), but with a `viz` function that\n", "can take a list of `C` values as argument and create a movie with\n", "solutions corresponding to the given `C` values. The `plot_u` function\n", "must be changed to store the solution in an array (see [Exercise 2: Add storage of solution in a user action function](#wave:exer:store:list) or [Exercise 3: Use a class for the user action function](#wave:exer:store:list:class) for\n", "details), `solver` must be computed for each value of the Courant\n", "number, and finally one must run through each time step and plot all\n", "the spatial solution curves in one figure and store it in a file.\n", "\n", "The challenge in such a visualization is to ensure that the curves in\n", "one plot correspond to the same time point. The easiest remedy is to\n", "keep the time resolution constant and change the space resolution\n", "to change the Courant number. Note that each spatial grid is needed for\n", "the final plotting, so it is an option to store those grids too.\n", "\n", "\n", "\n", "**Solution.**\n", "Modifying the code to store all solutions for each $C$ value and also each corresponding spatial grid (needed for final plotting), we get" ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [], "source": [ "#!/usr/bin/env python\n", "\"\"\"\n", "1D wave equation with u=0 at the boundary.\n", "Simplest possible implementation.\n", "\n", "The key function is::\n", "\n", " u, x, t, cpu = solver(I, V, f, c, L, dt, C, T, user_action)\n", "\n", "which solves the wave equation u_tt = c**2*u_xx on (0,L) with u=0\n", "on x=0,L, for t in (0,T]. Initial conditions: u=I(x), u_t=V(x).\n", "\n", "T is the stop time for the simulation.\n", "dt is the desired time step.\n", "C is the Courant number (=c*dt/dx), which specifies dx.\n", "f(x,t) is a function for the source term (can be 0 or None).\n", "I and V are functions of x.\n", "\n", "user_action is a function of (u, x, t, n) where the calling\n", "code can add visualization, error computations, etc.\n", "\"\"\"\n", "\n", "import numpy as np\n", "\n", "def solver(I, V, f, c, L, dt, C, T, user_action=None):\n", " \"\"\"Solve u_tt=c^2*u_xx + f on (0,L)x(0,T].\"\"\"\n", " Nt = int(round(T/dt))\n", " t = np.linspace(0, Nt*dt, Nt+1) # Mesh points in time\n", " dx = c*dt/C\n", " Nx = int(round(L/dx))\n", " x = np.linspace(0, L, Nx+1) # Mesh points in space\n", " C2 = C**2 # Help variable in the scheme\n", " # Recompute to make sure dx and dt are compatible with x and t\n", " dx = x[1] - x[0]\n", " dt = t[1] - t[0]\n", "\n", " if f is None or f == 0 :\n", " f = lambda x, t: 0\n", " if V is None or V == 0:\n", " V = lambda x: 0\n", "\n", " u = np.zeros(Nx+1) # Solution array at new time level\n", " u_1 = np.zeros(Nx+1) # Solution at 1 time level back\n", " u_2 = np.zeros(Nx+1) # Solution at 2 time levels back\n", "\n", " import time; t0 = time.clock() # for measuring CPU time\n", "\n", " # Load initial condition into u_1\n", " for i in range(0,Nx+1):\n", " u_1[i] = I(x[i])\n", "\n", " if user_action is not None:\n", " user_action(u_1, x, t, 0)\n", "\n", " # Special formula for first time step\n", " n = 0\n", " for i in range(1, Nx):\n", " u[i] = u_1[i] + dt*V(x[i]) + \\\n", " 0.5*C2*(u_1[i-1] - 2*u_1[i] + u_1[i+1]) + \\\n", " 0.5*dt**2*f(x[i], t[n])\n", " u[0] = 0; u[Nx] = 0\n", "\n", " if user_action is not None:\n", " user_action(u, x, t, 1)\n", "\n", " # Switch variables before next step\n", " u_2[:] = u_1; u_1[:] = u\n", "\n", " for n in range(1, Nt):\n", " # Update all inner points at time t[n+1]\n", " for i in range(1, Nx):\n", " u[i] = - u_2[i] + 2*u_1[i] + \\\n", " C2*(u_1[i-1] - 2*u_1[i] + u_1[i+1]) + \\\n", " dt**2*f(x[i], t[n])\n", "\n", " # Insert boundary conditions\n", " u[0] = 0; u[Nx] = 0\n", " if user_action is not None:\n", " if user_action(u, x, t, n+1):\n", " break\n", "\n", " # Switch variables before next step\n", " u_2[:] = u_1; u_1[:] = u\n", "\n", " cpu_time = time.clock() - t0\n", " return u, x, t, cpu_time\n", "\n", "def viz(\n", " I, V, f, c, L, dt, C, T, # PDE parameters\n", " umin, umax, # Interval for u in plots\n", " animate=True, # Simulation with animation?\n", " tool='matplotlib', # 'matplotlib' or 'scitools'\n", " solver_function=solver, # Function with numerical algorithm\n", " ):\n", " \"\"\"\n", " Run solver, store and viz. u at each time level with all C values.\n", " \"\"\"\n", " \n", " class PlotUst:\n", " def __init__(self):\n", " self.all_u = []\n", " self.all_u_for_all_C = []\n", " self.x_mesh = [] # need each mesh for final plots\n", " def __call__(self, u, x, t, n):\n", " \"\"\"user_action function for solver.\"\"\"\n", " self.all_u.append(u.copy())\n", " if t[n] == T: # i.e., whole time interv. done for this C\n", " self.x_mesh.append(x)\n", " self.all_u_for_all_C.append(self.all_u)\n", " self.all_u = [] # reset to empty list\n", " \n", " if len(self.all_u_for_all_C) == len(C): # all C done\n", " print 'Finished all C. Proceed with plots...'\n", " # note: n will here be the last index in t[n]\n", " for n_ in range(0, n+1): # for each tn\n", " plt.plot(self.x_mesh[0], \n", " self.all_u_for_all_C[0][n_],\n", " axis=[0, L, umin, umax],\n", " title='Solutions for all \\\n", " C at t=%f' % t[n_])\n", " plt.hold('on')\n", " \n", " for j in range(1, len(C)):\n", " # build plot at this tn with each \n", " # sol. from the different C values\n", " plt.plot(self.x_mesh[j], \n", " self.all_u_for_all_C[j][n_],\n", " axis=[0, L, umin, umax])\n", " plt.xlabel('x'); plt.ylabel('u')\n", " plt.hold('off')\n", " plt.show()\n", " # Let the init. cond. stay on the screen for\n", " # 2 sec, else insert a pause of 0.2 s \n", " # between each plot \n", " time.sleep(2) if t[n_] == 0 else \\\n", " time.sleep(0.2) \n", " plt.savefig('tmp_%04d.png' % n_) # for movie\n", " \n", "\n", " class PlotMatplotlib:\n", " def __init__(self):\n", " self.all_u = []\n", " self.all_u_for_all_C = []\n", " def __call__(self, u, x, t, n):\n", " \"\"\"user_action function for solver.\"\"\"\n", " self.all_u.append(u.copy())\n", " if t[n] == T: # i.e., whole time interv. done for this C\n", " self.all_u_for_all_C.append(self.all_u)\n", " self.all_u = [] # reset to empty list\n", " \n", " if len(self.all_u_for_all_C) == len(C): # all C done\n", " print 'Finished all C. Proceed with plots...'\n", " plt.ion()\n", " # note: n will here be the last index in t[n]\n", " for n_ in range(0, n+1): # for each tn\n", " plt.plot(x, self.all_u_for_all_C[0][n_])\n", " plt.axis([0, L, umin, umax])\n", " plt.hold(True)\n", " for j in range(1, len(C)):\n", " # build plot at this tn with each \n", " # sol. from the different C values\n", " plt.plot(x, self.all_u_for_all_C[j][n_])\n", " plt.axis([0, L, umin, umax])\n", " plt.xlabel('x'); plt.ylabel('u')\n", " plt.title('Solutions for all \\\n", " C at t=%f' % t[n_])\n", " plt.hold(False)\n", " plt.draw()\n", " # Let the init. cond. stay on the screen for\n", " # 2 sec, else insert a pause of 0.2 s \n", " # between each plot \n", " time.sleep(2) if t[n_] == 0 else \\\n", " time.sleep(0.2) \n", " plt.savefig('tmp_%04d.png' % n_) # for movie\n", "\n", " if tool == 'matplotlib':\n", " import matplotlib.pyplot as plt\n", " plot_u = PlotMatplotlib()\n", " elif tool == 'scitools':\n", " import scitools.std as plt # scitools.easyviz interface\n", " plot_u = PlotUst()\n", " import time, glob, os\n", "\n", " # Clean up old movie frames\n", " for filename in glob.glob('tmp_*.png'):\n", " os.remove(filename)\n", "\n", " # Call solver and do the simulaton\n", " user_action = plot_u if animate else None\n", " for C_value in C:\n", " print 'C_value --------------------------------- ', C_value\n", " u, x, t, cpu = solver_function(\n", " I, V, f, c, L, dt, C_value, T, user_action)\n", "\n", " # Make video files\n", " fps = 4 # frames per second\n", " codec2ext = dict(flv='flv', libx264='mp4', libvpx='webm',\n", " libtheora='ogg') # video formats\n", " filespec = 'tmp_%04d.png'\n", " movie_program = 'ffmpeg'\n", " for codec in codec2ext:\n", " ext = codec2ext[codec]\n", " cmd = '%(movie_program)s -r %(fps)d -i %(filespec)s '\\\n", " '-vcodec %(codec)s movie.%(ext)s' % vars()\n", " os.system(cmd)\n", "\n", " if tool == 'scitools':\n", " # Make an HTML play for showing the animation in a browser\n", " plt.movie('tmp_*.png', encoder='html', fps=fps,\n", " output_file='movie.html')\n", " return cpu\n", "\n", "def guitar(C):\n", " \"\"\"Triangular wave (pulled guitar string).\"\"\"\n", " L = 0.75\n", " x0 = 0.8*L\n", " a = 0.005\n", " freq = 440\n", " wavelength = 2*L\n", " c = freq*wavelength\n", " from math import pi\n", " w = 2*pi*freq\n", " num_periods = 1\n", " T = 2*pi/w*num_periods\n", " # Choose dt the same as the stability limit for Nx=50\n", " dt = L/50./c\n", " dx = dt*c/float(C)\n", " # Now dt is considered fixed and a list of C \n", " # values is made by reducing increasing the dx value \n", " # in steps of 10%. \n", " all_C = [C]\n", " all_C.append(c*dt/(1.1*dx))\n", " all_C.append(c*dt/(1.2*dx))\n", "\n", " def I(x):\n", " return a*x/x0 if x < x0 else a/(L-x0)*(L-x)\n", "\n", " umin = -1.2*a; umax = -umin\n", " cpu = viz(I, 0, 0, c, L, dt, all_C, T, umin, umax,\n", " animate=True, tool='scitools')\n", " #cpu = viz(I, 0, 0, c, L, dt, all_C, T, umin, umax,\n", " # animate=True, tool='matplotlib')\n", " print 'cpu = ', cpu\n", "\n", "if __name__ == '__main__':\n", " import sys\n", " try:\n", " C = float(sys.argv[1])\n", " print 'C=%g' % C\n", " except IndexError:\n", " C = 0.85\n", " print 'Courant number: %.2f' % C\n", " # The list of C values will be generated from this C value\n", " guitar(C)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "Filename: `wave_numerics_comparison`.\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "## Exercise 5: Implementing the solver function as a generator\n", "
\n", "\n", "The callback function `user_action(u, x, t, n)` is called from the\n", "`solver` function (in, e.g., `wave1D_u0.py`) at every time level and lets\n", "the user work perform desired actions with the solution, like plotting it\n", "on the screen. We have implemented the callback function in the typical\n", "way it would have been done in C and Fortran. Specifically, the code looks\n", "like" ] }, { "cell_type": "code", "execution_count": 40, "metadata": {}, "outputs": [], "source": [ "if user_action is not None:\n", " if user_action(u, x, t, n):\n", " break" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Many Python programmers, however, may claim that `solver` is an iterative\n", "process, and that iterative processes with callbacks to the user code is\n", "more elegantly implemented as *generators*. The rest of the text has little\n", "meaning unless you are familiar with Python generators and the `yield`\n", "statement.\n", "\n", "Instead of calling `user_action`, the `solver` function\n", "issues a `yield` statement, which is a kind of `return` statement:" ] }, { "cell_type": "code", "execution_count": 41, "metadata": {}, "outputs": [], "source": [ "yield u, x, t, n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The program control is directed back to the calling code:" ] }, { "cell_type": "code", "execution_count": 42, "metadata": {}, "outputs": [], "source": [ "for u, x, t, n in solver(...):\n", " # Do something with u at t[n]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When the block is done, `solver` continues with the statement after `yield`.\n", "Note that the functionality of terminating the solution process if\n", "`user_action` returns a `True` value is not possible to implement in the\n", "generator case.\n", "\n", "Implement the `solver` function as a generator, and plot the solution\n", "at each time step.\n", "Filename: `wave1D_u0_generator`.\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "## Project 6: Calculus with 1D mesh functions\n", "
\n", "\n", "This project explores integration and differentiation of\n", "mesh functions, both with scalar and vectorized implementations.\n", "We are given a mesh function $f_i$ on a spatial one-dimensional\n", "mesh $x_i=i\\Delta x$, $i=0,\\ldots,N_x$, over the interval $[a,b]$.\n", "\n", "\n", "**a)**\n", "Define the discrete derivative of $f_i$ by using centered\n", "differences at internal mesh points and one-sided differences\n", "at the end points. Implement a scalar version of\n", "the computation in a Python function and write an associated unit test\n", "for the linear case $f(x)=4x-2.5$ where the discrete derivative should\n", "be exact.\n", "\n", "\n", "\n", "**Solution.**\n", "See code below.\n", "\n", "\n", "\n", "**b)**\n", "Vectorize the implementation of the discrete derivative.\n", "Extend the unit test to check the validity of the implementation.\n", "\n", "\n", "\n", "**Solution.**\n", "See code below.\n", "\n", "\n", "\n", "**c)**\n", "To compute the discrete integral $F_i$ of $f_i$, we assume that\n", "the mesh function $f_i$ varies linearly between the mesh points.\n", "Let $f(x)$ be such a linear interpolant of $f_i$. We then\n", "have" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$$\n", "F_i = \\int_{x_0}^{x_i} f(x) dx\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The exact integral of a piecewise linear function $f(x)$ is\n", "given by the Trapezoidal rule. Show\n", "that if $F_{i}$ is already computed, we can find $F_{i+1}$\n", "from" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$$\n", "F_{i+1} = F_i + \\frac{1}{2}(f_i + f_{i+1})\\Delta x\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Make a function for the scalar implementation of the discrete integral\n", "as a mesh function. That is, the function should return\n", "$F_i$ for $i=0,\\ldots,N_x$.\n", "For a unit test one can use the fact that the above defined\n", "discrete integral of a linear\n", "function (say $f(x)=4x-2.5$) is exact.\n", "\n", "\n", "\n", "**Solution.**\n", "We know that the difference $F_{i+1} - F_i$ must amount to the area\n", "of a trapezoid, which is exactly what $\\frac{1}{2}(f_i + f_{i+1})\\Delta x$ is.\n", "To show the relation above, we may start with the Trapezoidal rule:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$$\n", "F_{i+1} = \\Delta x \\left[\\frac{1}{2}f(x_0) + \\sum_{j=1}^{n-1}f(x_j) + \\frac{1}{2}f(x_n) \\right] \\thinspace . \\nonumber\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Since $n = i+1$, and since the final term in the sum may be separated out from the sum and split in two, this may be written as" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$$\n", "F_{i+1} = \\Delta x \\left[\\frac{1}{2}f(x_0) + \\sum_{j=1}^{i-1}f(x_j) + \\frac{1}{2}f(x_i) + \\frac{1}{2}f(x_i) + \\frac{1}{2}f(x_{i+1}) \\right] \\thinspace . \\nonumber\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This may further be written as" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$$\n", "F_{i+1} = \\Delta x \\left[\\frac{1}{2}f(x_0) + \\sum_{j=1}^{i-1}f(x_j) + \\frac{1}{2}f(x_i)\\right] + \\Delta x \\left[\\frac{1}{2}f(x_i) + \\frac{1}{2}f(x_{i+1}) \\right] \\thinspace . \\nonumber\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, this gives" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$$\n", "F_{i+1} = F_i + \\frac{1}{2}(f_i + f_{i+1})\\Delta x\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "See code below for implementation.\n", "\n", "\n", "\n", "**d)**\n", "Vectorize the implementation of the discrete integral.\n", "Extend the unit test to check the validity of the implementation.\n", "\n", "\n", "\n", "**Hint.**\n", "Interpret the recursive formula for $F_{i+1}$ as a sum.\n", "Make an array with each element of the sum and use the \"cumsum\"\n", "(`numpy.cumsum`) operation to compute the accumulative sum:\n", "`numpy.cumsum([1,3,5])` is `[1,4,9]`.\n", "\n", "\n", "\n", "\n", "\n", "**Solution.**\n", "See code below.\n", "\n", "\n", "\n", "**e)**\n", "Create a class `MeshCalculus` that can integrate and differentiate\n", "mesh functions. The class can just define some methods that call\n", "the previously implemented Python functions. Here is an example\n", "on the usage:" ] }, { "cell_type": "code", "execution_count": 43, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "calc = MeshCalculus(vectorized=True)\n", "x = np.linspace(0, 1, 11) # mesh\n", "f = np.exp(x) # mesh function\n", "df = calc.differentiate(f, x) # discrete derivative\n", "F = calc.integrate(f, x) # discrete anti-derivative" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "**Solution.**\n", "See code below.\n", "\n", "\n", "\n", "\n", "\n", "\n", "**Solution.**\n", "The final version of the code reads" ] }, { "cell_type": "code", "execution_count": 44, "metadata": {}, "outputs": [], "source": [ "# -*- coding: utf-8 -*-\n", "\"\"\"\n", "Calculus with a 1D mesh function.\n", "\"\"\"\n", "import numpy as np\n", "\n", "class MeshCalculus:\n", " def __init__(self, vectorized=True):\n", " self.vectorized = vectorized\n", "\n", " def differentiate(self, f, x):\n", " '''\n", " Computes the derivative of f by centered differences, but \n", " forw and back difference at the start and end, respectively.\n", " '''\n", " dx = x[1] - x[0]\n", " Nx = len(x) - 1 # number of spatial steps\n", " num_dfdx = np.zeros(Nx+1)\n", " # Compute approximate derivatives at end-points first\n", " num_dfdx[0] = (f(x[1]) - f(x[0]))/dx # FD approx.\n", " num_dfdx[Nx] = (f(x[Nx]) - f(x[Nx-1]))/dx # BD approx.\n", " # proceed with approximate derivatives for inner mesh points\n", " if self.vectorized:\n", " num_dfdx[1:-1] = (f(x[2:]) - f(x[:-2]))/(2*dx)\n", " else: # scalar version\n", " for i in range(1, Nx):\n", " num_dfdx[i] = (f(x[i+1]) - f(x[i-1]))/(2*dx)\n", " return num_dfdx\n", " \n", " def integrate(self, f, x):\n", " '''\n", " Computes the integral of f(x) over the interval \n", " covered by x. \n", " '''\n", " dx = x[1] - x[0]\n", " F = np.zeros(len(x)) \n", " F[0] = 0 # starting value for iterative scheme\n", " if self.vectorized:\n", " all_trapezoids = np.zeros(len(x)-1) \n", " all_trapezoids[:] = 0.5*(f(x[:-1]) + f(x[1:]))*dx \n", " F[1:] = np.cumsum(all_trapezoids)\n", " else: # scalar version\n", " for i in range(0, len(x)-1):\n", " F[i+1] = F[i] + 0.5*(f(x[i]) + f(x[i+1]))*dx \n", " return F\n", " \n", "def test_differentiate():\n", " def f(x):\n", " return 4*x - 2.5\n", " def dfdx(x):\n", " derivatives = np.zeros(len(x))\n", " derivatives[:] = 4\n", " return derivatives\n", " \n", " a = 0; b = 1; Nx = 10\n", " x = np.linspace(a, b, Nx+1) \n", " exact_dfdx = dfdx(x) # compute exact derivatives\n", " # test vectorized version\n", " calc_v = MeshCalculus(vectorized=True)\n", " num_dfdx = calc_v.differentiate(f, x)\n", " print np.abs(num_dfdx - exact_dfdx)\n", " diff = np.abs(num_dfdx - exact_dfdx).max()\n", " tol = 1E-14\n", " assert diff < tol\n", " # test scalar version\n", " calc = MeshCalculus(vectorized=False)\n", " num_dfdx = calc.differentiate(f, x)\n", " print np.abs(num_dfdx - exact_dfdx)\n", " diff = np.abs(num_dfdx - exact_dfdx).max()\n", " assert diff < tol\n", " \n", "def test_integrate():\n", " def f(x):\n", " return 4*x - 2.5\n", " a = 0; b = 1; Nx = 10\n", "# a = 2.5/4; b = 10; Nx = 2\n", " x = np.linspace(a, b, Nx+1) \n", " # The exact integral amounts to the total area of two triangles\n", " I_exact = 0.5*abs(2.5/4 - a)*f(a) + 0.5*abs(b - 2.5/4)*f(b)\n", " # test vectorized version \n", " calc_v = MeshCalculus(vectorized=True) \n", " F = calc_v.integrate(f, x)\n", " print F, I_exact\n", " diff = np.abs(F[-1] - I_exact)\n", " print diff\n", " tol = 1E-14\n", " assert diff < tol \n", " # test scalar version\n", " calc = MeshCalculus(vectorized=False) \n", " F = calc.integrate(f, x)\n", " print F, I_exact\n", " diff = np.abs(F[-1] - I_exact)\n", " print diff\n", " assert diff < tol\n", " \n", " \n", "if __name__ == '__main__':\n", " test_differentiate()\n", " test_integrate()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "Filename: `mesh_calculus_1D`.\n", "\n", "" ] } ], "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.8.3" } }, "nbformat": 4, "nbformat_minor": 4 }