|
| 1 | +{ |
| 2 | + "cells": [ |
| 3 | + { |
| 4 | + "cell_type": "markdown", |
| 5 | + "metadata": {}, |
| 6 | + "source": [ |
| 7 | + "# The Helmholtz equation on the Torus\n", |
| 8 | + "\n", |
| 9 | + "Helmholtz equation is given as\n", |
| 10 | + "\n", |
| 11 | + "$$\n", |
| 12 | + "\\nabla^2 u + \\alpha u = f.\n", |
| 13 | + "$$\n", |
| 14 | + "\n", |
| 15 | + "In this notebook we will solve this equation (without boundary conditions) on the surface of a torus, using curvilinear coordinates. The surface of the torus is parametrized by \n", |
| 16 | + "\n", |
| 17 | + "$$\n", |
| 18 | + "\\begin{align}\n", |
| 19 | + "x(\\theta, \\phi) &= (R + r \\cos \\theta) \\cos \\phi \\\\\n", |
| 20 | + "y(\\theta, \\phi) &= (R + r \\cos \\theta) \\sin \\phi \\\\\n", |
| 21 | + "z(\\theta, \\phi) &= r \\sin \\theta\n", |
| 22 | + "\\end{align}\n", |
| 23 | + "$$\n", |
| 24 | + "\n", |
| 25 | + "where $\\theta, \\phi$ are angles which make a full circle, so that their values start and end at the same point, $R$ is the distance from the center of the tube to the center of the torus,\n", |
| 26 | + "$r$ is the radius of the tube. Note that $\\theta$ is the angle in the small circle (around its center), whereas $\\phi$ is the angle of the large circle, around origo.\n", |
| 27 | + "\n", |
| 28 | + "We start the implementation by importing necessary functionality from shenfun and sympy and then defining the coordinates of the surface of the torus. Note that `rv` represents the position vector spanning the surface." |
| 29 | + ] |
| 30 | + }, |
| 31 | + { |
| 32 | + "cell_type": "code", |
| 33 | + "execution_count": null, |
| 34 | + "metadata": {}, |
| 35 | + "outputs": [], |
| 36 | + "source": [ |
| 37 | + "from shenfun import *\n", |
| 38 | + "from shenfun.la import SolverGeneric1ND\n", |
| 39 | + "import sympy as sp\n", |
| 40 | + "from IPython.display import Math\n", |
| 41 | + "\n", |
| 42 | + "N = 96\n", |
| 43 | + "R = 3\n", |
| 44 | + "r = 1\n", |
| 45 | + "theta, phi = psi = sp.symbols('x,y', real=True, positive=True)\n", |
| 46 | + "rv = ((R + r*sp.cos(theta))*sp.cos(phi), (R + r*sp.cos(theta))*sp.sin(phi), r*sp.sin(theta)) " |
| 47 | + ] |
| 48 | + }, |
| 49 | + { |
| 50 | + "cell_type": "markdown", |
| 51 | + "metadata": {}, |
| 52 | + "source": [ |
| 53 | + "Now create necessary bases and function spaces. Due to the geometry of the problem, the solution will be periodic in both $\\theta$ and $\\phi$ directions, and we choose Fourier basis functions. The basis for the $\\phi$-direction can be either real to complex or complex to complex, depending on the type of the solution. Here we assume a real solution" |
| 54 | + ] |
| 55 | + }, |
| 56 | + { |
| 57 | + "cell_type": "code", |
| 58 | + "execution_count": null, |
| 59 | + "metadata": {}, |
| 60 | + "outputs": [], |
| 61 | + "source": [ |
| 62 | + "B1 = FunctionSpace(N, 'F', dtype='D', domain=(0, 2*np.pi))\n", |
| 63 | + "B2 = FunctionSpace(N, 'F', dtype='d', domain=(0, 2*np.pi))\n", |
| 64 | + "T = TensorProductSpace(comm, (B1, B2), coordinates=(psi, rv, sp.Q.positive(r*sp.cos(theta)+R)))\n", |
| 65 | + "V = VectorSpace(T)\n", |
| 66 | + "u = TrialFunction(T)\n", |
| 67 | + "v = TestFunction(T)\n", |
| 68 | + "T.coors.sg" |
| 69 | + ] |
| 70 | + }, |
| 71 | + { |
| 72 | + "cell_type": "markdown", |
| 73 | + "metadata": {}, |
| 74 | + "source": [ |
| 75 | + "Note the assumption `sp.Q.positive(r*sp.cos(theta)+R))`, which is there to help sympy when computing basis vectors and scaling factors. It is not completely necessary, but try to omit it and look at what happens to the expanded Helmholtz equation below." |
| 76 | + ] |
| 77 | + }, |
| 78 | + { |
| 79 | + "cell_type": "code", |
| 80 | + "execution_count": null, |
| 81 | + "metadata": {}, |
| 82 | + "outputs": [], |
| 83 | + "source": [ |
| 84 | + "alpha = 0\n", |
| 85 | + "du = div(grad(u))+alpha*u\n", |
| 86 | + "g = sp.Symbol('g', real=True, positive=True) # The Jacobian**2 (T.coors.sg**2)\n", |
| 87 | + "#replace = [(3*sp.cos(theta)+5, g/2), (sp.cos(theta)**2+6*sp.cos(theta)+10, sp.cos(theta)**2+g), (3*sp.cos(theta)+10, g/2+5)] # to simplify the look\n", |
| 88 | + "replace = [(sp.cos(theta)+3, g)]\n", |
| 89 | + "Math(du.tolatex(symbol_names={r: 'r', theta: '\\\\theta', phi: '\\\\phi'}, replace=replace))" |
| 90 | + ] |
| 91 | + }, |
| 92 | + { |
| 93 | + "cell_type": "markdown", |
| 94 | + "metadata": {}, |
| 95 | + "source": [ |
| 96 | + "We now create a manufactured solution that satisfies periodicity and compute the right hand side $f$." |
| 97 | + ] |
| 98 | + }, |
| 99 | + { |
| 100 | + "cell_type": "code", |
| 101 | + "execution_count": null, |
| 102 | + "metadata": {}, |
| 103 | + "outputs": [], |
| 104 | + "source": [ |
| 105 | + "ue = sp.sin(sp.cos(theta*4))*sp.cos(4*phi)\n", |
| 106 | + "f = (div(grad(u))+alpha*u).tosympy(basis=ue, psi=psi)\n", |
| 107 | + "fj = Array(T, buffer=f*T.sg)\n", |
| 108 | + "f_hat = Function(T)\n", |
| 109 | + "f_hat = inner(v, fj, output_array=f_hat)" |
| 110 | + ] |
| 111 | + }, |
| 112 | + { |
| 113 | + "cell_type": "markdown", |
| 114 | + "metadata": {}, |
| 115 | + "source": [ |
| 116 | + "Assemble coefficient matrix and solve problem. Note that the tensorproduct matrices along axis 0 can be non-diagonal due to the measure $\\cos \\theta + 3$. The matrices along the second axis will all be diagonal, so we can choose to use `SolverGeneric1ND`" |
| 117 | + ] |
| 118 | + }, |
| 119 | + { |
| 120 | + "cell_type": "code", |
| 121 | + "execution_count": null, |
| 122 | + "metadata": {}, |
| 123 | + "outputs": [], |
| 124 | + "source": [ |
| 125 | + "mats = inner(v*T.sg, (div(grad(u))+alpha*u))\n", |
| 126 | + "#mats = inner(grad(v*T.sg), -grad(u)) # + inner(v, alpha*u)\n", |
| 127 | + "u_hat = Function(T)\n", |
| 128 | + "sol = SolverGeneric1ND(mats)\n", |
| 129 | + "u_hat = sol(f_hat, u_hat, constraints=((0, 0),))\n", |
| 130 | + "uj = u_hat.backward()\n", |
| 131 | + "uq = Array(T, buffer=ue)\n", |
| 132 | + "print('Error =', np.sqrt(inner(1, (uj-uq)**2)))" |
| 133 | + ] |
| 134 | + }, |
| 135 | + { |
| 136 | + "cell_type": "code", |
| 137 | + "execution_count": null, |
| 138 | + "metadata": {}, |
| 139 | + "outputs": [], |
| 140 | + "source": [ |
| 141 | + "mats = inner(v*T.sg, u)" |
| 142 | + ] |
| 143 | + }, |
| 144 | + { |
| 145 | + "cell_type": "markdown", |
| 146 | + "metadata": {}, |
| 147 | + "source": [ |
| 148 | + "Finally, just plot the solution using plotly." |
| 149 | + ] |
| 150 | + }, |
| 151 | + { |
| 152 | + "cell_type": "code", |
| 153 | + "execution_count": null, |
| 154 | + "metadata": {}, |
| 155 | + "outputs": [], |
| 156 | + "source": [ |
| 157 | + "surf3D(u_hat, wrapaxes=[0, 1])" |
| 158 | + ] |
| 159 | + }, |
| 160 | + { |
| 161 | + "cell_type": "markdown", |
| 162 | + "metadata": {}, |
| 163 | + "source": [ |
| 164 | + "Inspect sparsity pattern at a given Fourier wavenumber " |
| 165 | + ] |
| 166 | + }, |
| 167 | + { |
| 168 | + "cell_type": "code", |
| 169 | + "execution_count": null, |
| 170 | + "metadata": {}, |
| 171 | + "outputs": [], |
| 172 | + "source": [ |
| 173 | + "import matplotlib.pyplot as plt\n", |
| 174 | + "%matplotlib notebook\n", |
| 175 | + "plt.figure(figsize=(6,4))\n", |
| 176 | + "plt.spy(sol.solvers1D[6].mat, markersize=0.1)" |
| 177 | + ] |
| 178 | + }, |
| 179 | + { |
| 180 | + "cell_type": "markdown", |
| 181 | + "metadata": {}, |
| 182 | + "source": [ |
| 183 | + "Inspect the vector Laplacian" |
| 184 | + ] |
| 185 | + }, |
| 186 | + { |
| 187 | + "cell_type": "code", |
| 188 | + "execution_count": null, |
| 189 | + "metadata": {}, |
| 190 | + "outputs": [], |
| 191 | + "source": [ |
| 192 | + "uv = TrialFunction(V)\n", |
| 193 | + "vv = TestFunction(V)\n", |
| 194 | + "du = div(grad(uv))\n", |
| 195 | + "Math(du.tolatex(symbol_names={r: 'r', theta: '\\\\theta', phi: '\\\\phi'}, replace=replace))" |
| 196 | + ] |
| 197 | + }, |
| 198 | + { |
| 199 | + "cell_type": "code", |
| 200 | + "execution_count": null, |
| 201 | + "metadata": {}, |
| 202 | + "outputs": [], |
| 203 | + "source": [ |
| 204 | + "Math(T.coors.latex_basis_vectors(symbol_names={r: 'r', theta: '\\\\theta', phi: '\\\\phi'}, replace=replace))" |
| 205 | + ] |
| 206 | + }, |
| 207 | + { |
| 208 | + "cell_type": "code", |
| 209 | + "execution_count": null, |
| 210 | + "metadata": {}, |
| 211 | + "outputs": [], |
| 212 | + "source": [ |
| 213 | + "%matplotlib notebook\n", |
| 214 | + "M = inner(vv*T.sg, du)\n", |
| 215 | + "B = BlockMatrix(M)\n", |
| 216 | + "plt.figure(figsize=(6,4))\n", |
| 217 | + "plt.spy(B.diags(it=(4,)), markersize=0.2)" |
| 218 | + ] |
| 219 | + } |
| 220 | + ], |
| 221 | + "metadata": { |
| 222 | + "kernelspec": { |
| 223 | + "display_name": "shenfun", |
| 224 | + "language": "python", |
| 225 | + "name": "shenfun" |
| 226 | + }, |
| 227 | + "language_info": { |
| 228 | + "codemirror_mode": { |
| 229 | + "name": "ipython", |
| 230 | + "version": 3 |
| 231 | + }, |
| 232 | + "file_extension": ".py", |
| 233 | + "mimetype": "text/x-python", |
| 234 | + "name": "python", |
| 235 | + "nbconvert_exporter": "python", |
| 236 | + "pygments_lexer": "ipython3", |
| 237 | + "version": "3.10.2" |
| 238 | + }, |
| 239 | + "toc": { |
| 240 | + "base_numbering": 1, |
| 241 | + "nav_menu": {}, |
| 242 | + "number_sections": false, |
| 243 | + "sideBar": true, |
| 244 | + "skip_h1_title": false, |
| 245 | + "title_cell": "Table of Contents", |
| 246 | + "title_sidebar": "Contents", |
| 247 | + "toc_cell": false, |
| 248 | + "toc_position": {}, |
| 249 | + "toc_section_display": true, |
| 250 | + "toc_window_display": false |
| 251 | + } |
| 252 | + }, |
| 253 | + "nbformat": 4, |
| 254 | + "nbformat_minor": 4 |
| 255 | +} |
0 commit comments