├── KdV Examples.ipynb
├── LICENSE
├── PSPython_01-linear-PDEs.ipynb
├── PSPython_02-pseudospectral-collocation.ipynb
├── PSPython_03-FFT-aliasing-filtering.ipynb
├── PSPython_04-Operator-splitting.ipynb
├── PSPython_05-ImEx-RK.ipynb
├── PSPython_06-Exponential-integrators.ipynb
├── README.md
├── aliasing_frequencies.png
└── custom.css
/KdV Examples.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "id": "cfc06113",
7 | "metadata": {},
8 | "outputs": [],
9 | "source": [
10 | "import numpy as np\n",
11 | "%matplotlib inline\n",
12 | "import matplotlib\n",
13 | "import matplotlib.pyplot as plt\n",
14 | "import matplotlib.animation\n",
15 | "from IPython.display import HTML\n",
16 | "font = {'size' : 15}\n",
17 | "matplotlib.rc('font', **font)"
18 | ]
19 | },
20 | {
21 | "cell_type": "code",
22 | "execution_count": null,
23 | "id": "0209d0a0",
24 | "metadata": {},
25 | "outputs": [],
26 | "source": [
27 | "def rk3(u,xi,rhs):\n",
28 | " y2 = u + dt*rhs(u,xi)\n",
29 | " y3 = 0.75*u + 0.25*(y2 + dt*rhs(y2,xi))\n",
30 | " u_new = 1./3 * u + 2./3 * (y3 + dt*rhs(y3,xi))\n",
31 | " return u_new\n",
32 | "\n",
33 | "\n",
34 | "def rhs(u, xi, epsilon=1.0):\n",
35 | " uhat = np.fft.fft(u)\n",
36 | " return -u*np.real(np.fft.ifft(1j*xi*uhat)) - epsilon*np.real(np.fft.ifft(-1j*xi**3*uhat))\n",
37 | " \n",
38 | "def solve_KdV(u0,tmax=1.,m=256,epsilon=1.0, ylims=(-100,300)):\n",
39 | " \"\"\"Solve the KdV equation using Fourier spectral collocation in space\n",
40 | " and SSPRK3 in time, on the domain (-pi, pi). The input u0 should be a function.\n",
41 | " \"\"\"\n",
42 | " # Grid\n",
43 | " L = 2*np.pi\n",
44 | " x = np.arange(-m/2,m/2)*(L/m)\n",
45 | " xi = np.fft.fftfreq(m)*m*2*np.pi/L\n",
46 | "\n",
47 | " dt = 1.73/((m/2)**3)\n",
48 | " u = u0(x)\n",
49 | " uhat2 = np.abs(np.fft.fft(u))\n",
50 | "\n",
51 | " num_plots = 400\n",
52 | " nplt = np.floor((tmax/num_plots)/dt)\n",
53 | " nmax = int(round(tmax/dt))\n",
54 | "\n",
55 | " fig = plt.figure(figsize=(12,8))\n",
56 | " axes = fig.add_subplot(111)\n",
57 | " line, = axes.plot(x,u,lw=3)\n",
58 | " xi_max = np.max(np.abs(xi))\n",
59 | " axes.set_xlabel(r'$x$',fontsize=30)\n",
60 | " plt.close()\n",
61 | "\n",
62 | " frames = [u.copy()]\n",
63 | " tt = [0]\n",
64 | " uuhat = [uhat2]\n",
65 | "\n",
66 | " for n in range(1,nmax+1):\n",
67 | " u_new = rk3(u,xi,rhs)\n",
68 | "\n",
69 | " u = u_new.copy()\n",
70 | " t = n*dt\n",
71 | " # Plotting\n",
72 | " if np.mod(n,nplt) == 0:\n",
73 | " frames.append(u.copy())\n",
74 | " tt.append(t)\n",
75 | " \n",
76 | " def plot_frame(i):\n",
77 | " line.set_data(x,frames[i])\n",
78 | " axes.set_title('t= %.2e' % tt[i])\n",
79 | " axes.set_xlim((-np.pi,np.pi))\n",
80 | " axes.set_ylim(ylims)\n",
81 | "\n",
82 | " anim = matplotlib.animation.FuncAnimation(fig, plot_frame,\n",
83 | " frames=len(frames), interval=100)\n",
84 | "\n",
85 | " return HTML(anim.to_jshtml())"
86 | ]
87 | },
88 | {
89 | "cell_type": "markdown",
90 | "id": "3f0cfe80",
91 | "metadata": {},
92 | "source": [
93 | "## Initial sinusoid\n",
94 | "\n",
95 | "Here we set up something similar to the FPUT experiment, with a single low-frequency mode as initial condition on a periodic domain. Notice how, at some later times, the solution comes close to the initial condition."
96 | ]
97 | },
98 | {
99 | "cell_type": "code",
100 | "execution_count": null,
101 | "id": "7bd939ca",
102 | "metadata": {},
103 | "outputs": [],
104 | "source": [
105 | "def u0(x):\n",
106 | " return 100*np.sin(x)\n",
107 | "solve_KdV(u0)"
108 | ]
109 | },
110 | {
111 | "cell_type": "markdown",
112 | "id": "784f0474",
113 | "metadata": {},
114 | "source": [
115 | "## Formation of a soliton train from an initial positive pulse."
116 | ]
117 | },
118 | {
119 | "cell_type": "code",
120 | "execution_count": null,
121 | "id": "c38c986f",
122 | "metadata": {},
123 | "outputs": [],
124 | "source": [
125 | "def u0(x):\n",
126 | " return 2000*np.exp(-10*(x+2)**2)\n",
127 | "solve_KdV(u0, tmax=0.005, ylims=(-100,3000))"
128 | ]
129 | },
130 | {
131 | "cell_type": "markdown",
132 | "id": "f191670a",
133 | "metadata": {},
134 | "source": [
135 | "# Interaction of two solitons"
136 | ]
137 | },
138 | {
139 | "cell_type": "code",
140 | "execution_count": null,
141 | "id": "76b69166",
142 | "metadata": {},
143 | "outputs": [],
144 | "source": [
145 | "A = 25; B = 16;\n",
146 | "def u0(x):\n",
147 | " return 3*A**2/np.cosh(0.5*(A*(x+2.)))**2 + 3*B**2/np.cosh(0.5*(B*(x+1)))**2\n",
148 | "solve_KdV(u0,tmax = 0.006, ylims=(-10,3000))"
149 | ]
150 | },
151 | {
152 | "cell_type": "markdown",
153 | "id": "fa18aa7b",
154 | "metadata": {},
155 | "source": [
156 | "The next simulation shows a comparison between the propagation of a single soliton versus the interaction of two solitons."
157 | ]
158 | },
159 | {
160 | "cell_type": "code",
161 | "execution_count": null,
162 | "id": "b24c8e5d",
163 | "metadata": {},
164 | "outputs": [],
165 | "source": [
166 | "# Grid\n",
167 | "m = 256\n",
168 | "L = 2*np.pi\n",
169 | "x = np.arange(-m/2,m/2)*(L/m)\n",
170 | "xi = np.fft.fftfreq(m)*m*2*np.pi/L\n",
171 | "\n",
172 | "dt = 1.73/((m/2)**3)\n",
173 | "\n",
174 | "A = 25; B = 16;\n",
175 | "u = 3*A**2/np.cosh(0.5*(A*(x+2.)))**2 + 3*B**2/np.cosh(0.5*(B*(x+1)))**2\n",
176 | "v = 3*A**2/np.cosh(0.5*(A*(x+2.)))**2\n",
177 | "\n",
178 | "tmax = 0.006\n",
179 | "\n",
180 | "uhat2 = np.abs(np.fft.fft(u))\n",
181 | "\n",
182 | "num_plots = 400\n",
183 | "nplt = np.floor((tmax/num_plots)/dt)\n",
184 | "nmax = int(round(tmax/dt))\n",
185 | "\n",
186 | "fig = plt.figure(figsize=(12,8))\n",
187 | "axes = fig.add_subplot(111)\n",
188 | "line, = axes.plot(x,u,lw=3)\n",
189 | "line2, = axes.plot(x,v,lw=3)\n",
190 | "xi_max = np.max(np.abs(xi))\n",
191 | "axes.set_xlabel(r'$x$',fontsize=30)\n",
192 | "plt.close()\n",
193 | "\n",
194 | "frames = [u.copy()]\n",
195 | "vframes = [v.copy()]\n",
196 | "tt = [0]\n",
197 | "uuhat = [uhat2]\n",
198 | "\n",
199 | "for n in range(1,nmax+1):\n",
200 | " u_new = rk3(u,xi,rhs)\n",
201 | " v_new = rk3(v,xi,rhs)\n",
202 | "\n",
203 | " u = u_new.copy()\n",
204 | " v = v_new.copy()\n",
205 | " t = n*dt\n",
206 | " # Plotting\n",
207 | " if np.mod(n,nplt) == 0:\n",
208 | " frames.append(u.copy())\n",
209 | " vframes.append(v.copy())\n",
210 | " tt.append(t)\n",
211 | " uhat2 = np.abs(np.fft.fft(u))\n",
212 | " uuhat.append(uhat2)\n",
213 | " \n",
214 | "def plot_frame(i):\n",
215 | " line.set_data(x,frames[i])\n",
216 | " line2.set_data(x,vframes[i])\n",
217 | " power_spectrum = np.abs(uuhat[i])**2\n",
218 | " axes.set_title('t= %.2e' % tt[i])\n",
219 | " axes.set_xlim((-np.pi,np.pi))\n",
220 | " axes.set_ylim((-10,3000))\n",
221 | " \n",
222 | "anim = matplotlib.animation.FuncAnimation(fig, plot_frame,\n",
223 | " frames=len(frames), interval=100)\n",
224 | "\n",
225 | "HTML(anim.to_jshtml())"
226 | ]
227 | },
228 | {
229 | "cell_type": "markdown",
230 | "id": "70b44acc",
231 | "metadata": {},
232 | "source": [
233 | "## Formation of a dispersive shockwave"
234 | ]
235 | },
236 | {
237 | "cell_type": "code",
238 | "execution_count": null,
239 | "id": "758b2d7e",
240 | "metadata": {},
241 | "outputs": [],
242 | "source": [
243 | "def u0(x):\n",
244 | " return -500*np.exp(-10*(x-2)**2)\n",
245 | "solve_KdV(u0, tmax=0.005, epsilon=0.1, ylims=(-600,300))"
246 | ]
247 | }
248 | ],
249 | "metadata": {
250 | "kernelspec": {
251 | "display_name": "Python 3 (ipykernel)",
252 | "language": "python",
253 | "name": "python3"
254 | },
255 | "language_info": {
256 | "codemirror_mode": {
257 | "name": "ipython",
258 | "version": 3
259 | },
260 | "file_extension": ".py",
261 | "mimetype": "text/x-python",
262 | "name": "python",
263 | "nbconvert_exporter": "python",
264 | "pygments_lexer": "ipython3",
265 | "version": "3.10.4"
266 | }
267 | },
268 | "nbformat": 4,
269 | "nbformat_minor": 5
270 | }
271 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 David Ketcheson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/PSPython_01-linear-PDEs.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "from IPython.core.display import HTML\n",
10 | "css_file = './custom.css'\n",
11 | "HTML(open(css_file, \"r\").read())"
12 | ]
13 | },
14 | {
15 | "cell_type": "markdown",
16 | "metadata": {},
17 | "source": [
18 | "###### Content provided under a Creative Commons Attribution license, CC-BY 4.0; code under MIT License. (c)2015 [David I. Ketcheson](http://davidketcheson.info)"
19 | ]
20 | },
21 | {
22 | "cell_type": "markdown",
23 | "metadata": {},
24 | "source": [
25 | "##### Version 0.3 - May 2022"
26 | ]
27 | },
28 | {
29 | "cell_type": "markdown",
30 | "metadata": {},
31 | "source": [
32 | "# Pseudospectral methods for wave equations in Python "
33 | ]
34 | },
35 | {
36 | "cell_type": "markdown",
37 | "metadata": {},
38 | "source": [
39 | "Welcome to PseudoSpectralPython, a short course that will teach you how to solve wave equations using pseudospectral collocation methods. This notebook is the first lesson, on solving linear problems. Pseudospectral methods are great for wave problems where:\n",
40 | "\n",
41 | "- The solution is smooth (no shocks)\n",
42 | "- The domain is simple; e.g., cartesian or spherical domains. In the course, we'll focus on periodic Cartesian domains."
43 | ]
44 | },
45 | {
46 | "cell_type": "markdown",
47 | "metadata": {
48 | "heading_collapsed": true
49 | },
50 | "source": [
51 | "## Table of contents:\n",
52 | " \n",
53 | "- [Advection-diffusion](#Advection-diffusion)\n",
54 | "- [Approximate solution](#Approximate-solution-by-discrete-Fourier-transforms)\n",
55 | "- [General linear PDEs](#General-linear-evolution-PDEs)"
56 | ]
57 | },
58 | {
59 | "cell_type": "markdown",
60 | "metadata": {},
61 | "source": [
62 | "## Advection-diffusion\n",
63 | "Let's get started! Run the code cell below to import a bunch of Python libraries we'll use."
64 | ]
65 | },
66 | {
67 | "cell_type": "code",
68 | "execution_count": null,
69 | "metadata": {},
70 | "outputs": [],
71 | "source": [
72 | "%matplotlib inline\n",
73 | "import numpy as np\n",
74 | "import matplotlib.pyplot as plt\n",
75 | "import matplotlib\n",
76 | "from matplotlib import animation\n",
77 | "from IPython.display import HTML"
78 | ]
79 | },
80 | {
81 | "cell_type": "markdown",
82 | "metadata": {},
83 | "source": [
84 | "To begin, let's consider the partial differential equation (PDE)\n",
85 | "\n",
86 | "$$u_t + a u_x = \\epsilon u_{xx}$$\n",
87 | "\n",
88 | "referred to as the *advection-diffusion* equation, for reasons we'll soon discover. Here we wish to find $u(x,t)$, which might be the density or concentration of some substance. The subscripts denote partial differentiation; e.g. $u_t$ is the partial derivative of $u$ with respect to $t$. The coefficients $a$ and $\\epsilon$ are constants that determine the strength of the advective and diffusive effects."
89 | ]
90 | },
91 | {
92 | "cell_type": "markdown",
93 | "metadata": {},
94 | "source": [
95 | "### Exact solution by Fourier analysis\n",
96 | "Let's solve this equation on a periodic domain $[-\\pi,\\pi]$, with some initial data\n",
97 | "\n",
98 | "$$u(x,0) = u_0(x).$$\n",
99 | "\n",
100 | "If we suppose for a moment that our solution is composed of a single Fourier mode with wavenumber $\\xi$ and time-dependent amplitude $\\hat{u}$:\n",
101 | "\n",
102 | "$$u(x,t; \\xi) = \\hat{u}(t) e^{i\\xi x},$$\n",
103 | "\n",
104 | "Then we obtain a simple ordinary differential equation (ODE) for $\\hat{u}$:\n",
105 | "\n",
106 | "$$\\hat{u}'(t; \\xi) + i\\xi a \\hat{u} = -\\xi^2 \\epsilon \\hat{u}$$\n",
107 | "\n",
108 | "We can solve this scalar ODE exactly:\n",
109 | "\n",
110 | "$$\\hat{u}(t; \\xi) = e^{(-i \\xi a - \\epsilon \\xi^2)t} \\hat{u}(0).$$"
111 | ]
112 | },
113 | {
114 | "cell_type": "markdown",
115 | "metadata": {},
116 | "source": [
117 | "We've transformed the original PDE into a simple ODE, but you may wonder whether this is useful, since we assumed a very simple form for the solution. The marvelous fact is that every solution of our advection-diffusion equation can be written as a linear combination (a *superposition*) of simple solutions of the form above, with different wavenumbers $\\xi$. We can construct the general solution as follows.\n",
118 | "\n",
119 | "First, we take a **Fourier transform** of the initial data:\n",
120 | "\n",
121 | "$$\\hat{u}(t=0;\\xi) = \\frac{1}{\\sqrt{2\\pi}} \\int_{-\\infty}^\\infty u_0(x) e^{-i\\xi x}dx.$$\n",
122 | "\n",
123 | "Then each mode **evolves** according to the solution of the ODE above:\n",
124 | "\n",
125 | "$$\\hat{u}(t; \\xi) = e^{(-i \\xi a - \\epsilon \\xi^2)t} \\hat{u}(0;\\xi).$$\n",
126 | "\n",
127 | "Finally, we construct the solution again by taking the **inverse Fourier transform**. This just means summing up all the Fourier modes:\n",
128 | "\n",
129 | "$$u(x,t) = \\frac{1}{\\sqrt{2\\pi}} \\int_{-\\infty}^\\infty \\hat{u}(t; \\xi) e^{i\\xi x}d\\xi.$$"
130 | ]
131 | },
132 | {
133 | "cell_type": "markdown",
134 | "metadata": {},
135 | "source": [
136 | "If you haven't seen Fourier analysis at all before, now is a good time to go read up a bit and then come back."
137 | ]
138 | },
139 | {
140 | "cell_type": "markdown",
141 | "metadata": {},
142 | "source": [
143 | "## Approximate solution by discrete Fourier transforms"
144 | ]
145 | },
146 | {
147 | "cell_type": "markdown",
148 | "metadata": {},
149 | "source": [
150 | "We can't evaluate the integrals above exactly on the computer (at least, not for arbitrary initial data $u_0$). Instead, we need to **discretize**. To do so, we introduce a grid with a finite set of points in space and time:"
151 | ]
152 | },
153 | {
154 | "cell_type": "code",
155 | "execution_count": null,
156 | "metadata": {},
157 | "outputs": [],
158 | "source": [
159 | "# Spatial grid\n",
160 | "m=64 # Number of grid points in space\n",
161 | "L = 2 * np.pi # Width of spatial domain\n",
162 | "x = np.arange(-m/2,m/2)*(L/m) # Grid points\n",
163 | "dx = x[1]-x[0] # Grid spacing\n",
164 | "\n",
165 | "# Temporal grid\n",
166 | "tmax = 4.0 # Final time\n",
167 | "N = 25 # number grid points in time\n",
168 | "k = tmax/N # interval between output times"
169 | ]
170 | },
171 | {
172 | "cell_type": "code",
173 | "execution_count": null,
174 | "metadata": {},
175 | "outputs": [],
176 | "source": [
177 | "x"
178 | ]
179 | },
180 | {
181 | "cell_type": "markdown",
182 | "metadata": {},
183 | "source": [
184 | "and a corresponding set of discrete wavenumber values $\\xi$:"
185 | ]
186 | },
187 | {
188 | "cell_type": "code",
189 | "execution_count": null,
190 | "metadata": {},
191 | "outputs": [],
192 | "source": [
193 | "xi = np.fft.fftfreq(m)*m*2*np.pi/L # Wavenumber \"grid\"\n",
194 | "xi\n",
195 | "# (this is the order in which numpy's FFT gives the frequencies)"
196 | ]
197 | },
198 | {
199 | "cell_type": "markdown",
200 | "metadata": {},
201 | "source": [
202 | "The functions $u, \\hat{u}$ discussed above are replaced by finite-dimensional vectors. These vectors are related through the discrete version of the Fourier transform, aptly called the **discrete Fourier transform** (DFT). We'll look at the DFT in more detail in the next lesson. For now, let's set the initial condition to\n",
203 | "\n",
204 | "$$u_0(x) = \\begin{cases} \\sin^2(2x) & -\\pi \\le x < -\\pi/2 \\\\ 0 & x>-\\pi/2 \\end{cases}$$\n",
205 | "\n",
206 | "and compute its DFT:"
207 | ]
208 | },
209 | {
210 | "cell_type": "code",
211 | "execution_count": null,
212 | "metadata": {},
213 | "outputs": [],
214 | "source": [
215 | "# Initial data\n",
216 | "u = np.sin(2*x)**2 * (x<-L/4)\n",
217 | "uhat0 = np.fft.fft(u)\n",
218 | "plt.plot(x,u)"
219 | ]
220 | },
221 | {
222 | "cell_type": "markdown",
223 | "metadata": {},
224 | "source": [
225 | "In the plot above, we have simply \"connected the dots\", using the values of the function at the grid points. We can obtain a more accurate representation by employing the underlying Fourier series representation of the solution, evaluated on a finer grid:"
226 | ]
227 | },
228 | {
229 | "cell_type": "code",
230 | "execution_count": null,
231 | "metadata": {},
232 | "outputs": [],
233 | "source": [
234 | "def spectral_representation(x0,uhat):\n",
235 | " u_fun = lambda y : np.real(np.sum(uhat*np.exp(1j*xi*(y+x0))))/len(uhat)\n",
236 | " u_fun = np.vectorize(u_fun)\n",
237 | " return u_fun\n",
238 | "\n",
239 | "u_spectral = spectral_representation(x[0],uhat0)\n",
240 | "x_fine = np.linspace(x[0],x[-1],1000)\n",
241 | "plt.plot(x_fine,u_spectral(x_fine));"
242 | ]
243 | },
244 | {
245 | "cell_type": "markdown",
246 | "metadata": {},
247 | "source": [
248 | "Next, we set a value for epsilon and compute the solution:"
249 | ]
250 | },
251 | {
252 | "cell_type": "code",
253 | "execution_count": null,
254 | "metadata": {},
255 | "outputs": [],
256 | "source": [
257 | "epsilon=0.01 # Diffusion coefficient\n",
258 | "a = 1.0 # Advection coefficient\n",
259 | "\n",
260 | "# Store solutions in a list for plotting later\n",
261 | "frames = [u.copy()]\n",
262 | "\n",
263 | "# Now we solve the problem\n",
264 | "for n in range(1,N+1):\n",
265 | " t = n*k\n",
266 | " uhat = np.exp(-(1.j*xi*a + epsilon*xi**2)*t) * uhat0\n",
267 | " u = np.real(np.fft.ifft(uhat))\n",
268 | " frames.append(u.copy())"
269 | ]
270 | },
271 | {
272 | "cell_type": "markdown",
273 | "metadata": {},
274 | "source": [
275 | "We have computed and stored the solution. The code below plots it as an animation."
276 | ]
277 | },
278 | {
279 | "cell_type": "code",
280 | "execution_count": null,
281 | "metadata": {},
282 | "outputs": [],
283 | "source": [
284 | "# Set up plotting\n",
285 | "fig = plt.figure(figsize=(9,4)); axes = fig.add_subplot(111)\n",
286 | "line, = axes.plot([],[],lw=3)\n",
287 | "axes.set_xlim((x[0],x[-1])); axes.set_ylim((0.,1.))\n",
288 | "plt.close()\n",
289 | "\n",
290 | "def plot_frame(i):\n",
291 | " line.set_data(x,frames[i])\n",
292 | " axes.set_title('t='+str(i*k))\n",
293 | " fig.canvas.draw()\n",
294 | " return fig\n",
295 | "\n",
296 | "# Animate the solution\n",
297 | "anim = matplotlib.animation.FuncAnimation(fig, plot_frame,\n",
298 | " frames=len(frames),\n",
299 | " interval=200,\n",
300 | " repeat=False)\n",
301 | "\n",
302 | "HTML(anim.to_jshtml())"
303 | ]
304 | },
305 | {
306 | "cell_type": "markdown",
307 | "metadata": {},
308 | "source": [
309 | "### Exercise\n",
310 | "*Rerun the last three code cells above with different values of $a$ and $\\epsilon$. What does each of these coefficients do?*\n",
311 | "\n",
312 | "*Rerun the code with different initial data. Does it behave as you expect?*"
313 | ]
314 | },
315 | {
316 | "cell_type": "markdown",
317 | "metadata": {},
318 | "source": [
319 | "## Review\n",
320 | "What did we just do? We solved a partial differential equation computationally. It's time to think about how accurate the solution is and what approximations we made.\n",
321 | "\n",
322 | "The first approximation we made was to take the initial data and approximate it by just the first terms in its Fourier series. How many terms did we include? The vector $\\hat{u}$ we computed contains just the first 64 Fourier modes (because we chose to use 64 points in our spatial grid vector $x$).\n",
323 | "\n",
324 | "What about the evolution in time? In fact, our time evolution of the solution is **exact** for the initial data vector, since it just uses the exact solution formula for the ODE that we derived above.\n",
325 | "\n",
326 | "In plotting the solution, note that we only used the values at the 64 spatial grid points. The `plot()` function merely connects these values by straight lines. We could plot a better representation of the solution by evaluating the Fourier series on a finer grid, but for now we won't worry about that."
327 | ]
328 | },
329 | {
330 | "cell_type": "markdown",
331 | "metadata": {
332 | "heading_collapsed": true
333 | },
334 | "source": [
335 | "## General linear evolution PDEs"
336 | ]
337 | },
338 | {
339 | "cell_type": "markdown",
340 | "metadata": {
341 | "hidden": true
342 | },
343 | "source": [
344 | "The approach we just described isn't particular to the advection-diffusion equation. In fact, it can be used to solve any linear evolution PDE (including systems of PDEs, but here we'll stick to scalar problems):\n",
345 | "\n",
346 | "$$u_t = \\sum_{j=0}^n \\alpha_j \\frac{\\partial^j u}{\\partial x^j}.$$\n",
347 | "\n",
348 | "If we take the Fourier transform, or simply apply our ansatz:\n",
349 | "\n",
350 | "$$u(x,t; \\xi) = \\hat{u}(t) e^{i\\xi x},$$\n",
351 | "\n",
352 | "we get the linear ODE\n",
353 | "\n",
354 | "$$\\hat{u}'(t) = \\left(\\sum_{j=0}^n \\alpha_j (i\\xi)^j\\right) \\hat{u}(t) = p(\\xi)\\hat{u}(t)$$\n",
355 | "\n",
356 | "with solution\n",
357 | "\n",
358 | "$$\\hat{u}(t) = e^{p(\\xi)t}\\hat{u}(0)$$\n",
359 | "\n",
360 | "so that\n",
361 | "\n",
362 | "$$u(x,t; \\xi) = e^{i\\xi x + p(\\xi)t} \\hat{u}(0).$$\n",
363 | "\n",
364 | "Here $p(\\xi)$ is a polynomial with coefficients $i^j \\alpha_j$."
365 | ]
366 | },
367 | {
368 | "cell_type": "markdown",
369 | "metadata": {
370 | "hidden": true
371 | },
372 | "source": [
373 | "We can see that odd-derivative terms correspond to imaginary terms in $p(\\xi)$, which (in the exponential) lead to changes in the phase of the solution, while even-derivative terms correspond to real terms in $p(\\xi)$, which lead to changes in the amplitude of the solution."
374 | ]
375 | },
376 | {
377 | "cell_type": "code",
378 | "execution_count": null,
379 | "metadata": {
380 | "hidden": true
381 | },
382 | "outputs": [],
383 | "source": []
384 | }
385 | ],
386 | "metadata": {
387 | "kernelspec": {
388 | "display_name": "Python 3 (ipykernel)",
389 | "language": "python",
390 | "name": "python3"
391 | },
392 | "language_info": {
393 | "codemirror_mode": {
394 | "name": "ipython",
395 | "version": 3
396 | },
397 | "file_extension": ".py",
398 | "mimetype": "text/x-python",
399 | "name": "python",
400 | "nbconvert_exporter": "python",
401 | "pygments_lexer": "ipython3",
402 | "version": "3.13.0"
403 | },
404 | "toc": {
405 | "base_numbering": 1,
406 | "nav_menu": {},
407 | "number_sections": true,
408 | "sideBar": true,
409 | "skip_h1_title": false,
410 | "title_cell": "Table of Contents",
411 | "title_sidebar": "Contents",
412 | "toc_cell": false,
413 | "toc_position": {},
414 | "toc_section_display": true,
415 | "toc_window_display": false
416 | }
417 | },
418 | "nbformat": 4,
419 | "nbformat_minor": 4
420 | }
421 |
--------------------------------------------------------------------------------
/PSPython_02-pseudospectral-collocation.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "from IPython.core.display import HTML\n",
10 | "css_file = './custom.css'\n",
11 | "HTML(open(css_file, \"r\").read())"
12 | ]
13 | },
14 | {
15 | "cell_type": "markdown",
16 | "metadata": {},
17 | "source": [
18 | "###### Content provided under a Creative Commons Attribution license, CC-BY 4.0; code under MIT License. (c)2015 [David I. Ketcheson](http://davidketcheson.info)"
19 | ]
20 | },
21 | {
22 | "cell_type": "markdown",
23 | "metadata": {},
24 | "source": [
25 | "##### Version 0.3 - May 2022"
26 | ]
27 | },
28 | {
29 | "cell_type": "code",
30 | "execution_count": null,
31 | "metadata": {},
32 | "outputs": [],
33 | "source": [
34 | "%matplotlib inline\n",
35 | "import numpy as np\n",
36 | "import matplotlib.pyplot as plt\n",
37 | "import matplotlib\n",
38 | "from matplotlib import animation\n",
39 | "from IPython.display import HTML\n",
40 | "font = {'size' : 15}\n",
41 | "matplotlib.rc('font', **font)"
42 | ]
43 | },
44 | {
45 | "cell_type": "markdown",
46 | "metadata": {},
47 | "source": [
48 | "# Pseudospectral collocation methods"
49 | ]
50 | },
51 | {
52 | "cell_type": "markdown",
53 | "metadata": {},
54 | "source": [
55 | "Welcome to lesson 2 -- in which we'll learn about the main focus of this course: pseudospectral collocation methods for wave equations. But first, a bit of review."
56 | ]
57 | },
58 | {
59 | "cell_type": "markdown",
60 | "metadata": {},
61 | "source": [
62 | "## Advection-diffusion (again)\n",
63 | "\n",
64 | "$$\n",
65 | "\\newcommand{\\F}{\\mathcal F}\n",
66 | "\\newcommand{\\Finv}{{\\mathcal F}^{-1}}\n",
67 | "$$\n",
68 | "\n",
69 | "In notebook 1 we solved the scalar, linear 1D evolution equation:\n",
70 | "\n",
71 | "$$u_t + u_x = \\epsilon u_{xx}$$\n",
72 | "\n",
73 | "by taking the Fourier transform, which \"diagonalizes\" this infinite-dimensional dynamical system:\n",
74 | "\n",
75 | "$$\\hat{u}_t + i\\xi\\hat{u} = -\\xi^2 \\epsilon \\hat{u}$$\n",
76 | "\n",
77 | "with solution\n",
78 | "\n",
79 | "$$\\hat{u}(t) = e^{(-i \\xi - \\epsilon \\xi^2)t} \\hat{u}(0)$$\n",
80 | "\n",
81 | "for each mode.\n",
82 | "\n",
83 | "To construct the full solution, we simply take the inverse Fourier transform. All together, this algorithm looks like:\n",
84 | "\n",
85 | "$$\n",
86 | "u(t) = \\Finv \\left(D\\left[e^{(-i \\xi - \\epsilon \\xi^2)t}\\right]\\F(u) \\right),\n",
87 | "$$\n",
88 | "\n",
89 | "where we have written $D[f(\\xi)]$ to denote the diagonal matrix whose $(j,j)$ entry is given by $f(\\xi_j)$.\n",
90 | "In the exact solution, the wavenumbers $\\xi$ range from $-\\infty$ to $+\\infty$ (and $D$ is infinite), but in practice we compute on a finite interval of length $L$ with $m$ collocation points.\n",
91 | "The wavenumbers are then given by the formula\n",
92 | "\n",
93 | "\\begin{align}\n",
94 | "\\xi_j & = \\frac{2 \\pi j}{L} & \\text{for } -m/2 \\le j < m/2,\n",
95 | "\\end{align}\n",
96 | "\n",
97 | "although the FFT routine orders them differently."
98 | ]
99 | },
100 | {
101 | "cell_type": "markdown",
102 | "metadata": {},
103 | "source": [
104 | "We can make the algorithm even more explicit by recognizing that the FFT (and its inverse) is a linear map, so it can be represented as a matrix multiplication. Thus\n",
105 | "\n",
106 | "$$\\F(u) = Fu$$\n",
107 | "\n",
108 | "where $F$ is a certain $m \\times m$ matrix. This matrix has a number of interesting properties (see e.g. Trefethen, Ch. 3), but for the moment we are only interested in the fact that it is a linear operator. We can reverse engineer it by applying $\\F$ to the columns of the identity matrix:"
109 | ]
110 | },
111 | {
112 | "cell_type": "code",
113 | "execution_count": null,
114 | "metadata": {},
115 | "outputs": [],
116 | "source": [
117 | "def F_matrix(m):\n",
118 | " F = np.zeros((m,m),dtype=complex)\n",
119 | " for j in range(m):\n",
120 | " v = np.zeros(m)\n",
121 | " v[j] = 1.\n",
122 | " F[:,j] = np.fft.fft(v)\n",
123 | " return F\n",
124 | "\n",
125 | "print(F_matrix(4))"
126 | ]
127 | },
128 | {
129 | "cell_type": "markdown",
130 | "metadata": {},
131 | "source": [
132 | "Notice that $F$ is symmetric; this is true for any value of $m$. Also, $F$ is essentially unitary -- that is, it is possible to choose a normalization such that $F$ is unitary, but the normalization in common use means that $FF^*=mI$ (and $F^{-1}(F^{-1})^* = m^{-1} I$)."
133 | ]
134 | },
135 | {
136 | "cell_type": "markdown",
137 | "metadata": {},
138 | "source": [
139 | "We won't work with this matrix in practice since applying it naively is much slower than the FFT. But it is a useful representation. Thus we can write our \"algorithm\" simply as\n",
140 | "\n",
141 | "$$\n",
142 | "u(t) = F^{-1} D\\left[e^{(-i \\xi - \\epsilon \\xi^2)(t-t_0)}\\right] F u(t_0).\n",
143 | "$$\n",
144 | "\n",
145 | "Now it is completely clear that we are simply applying a similarity transformation that diagonalizes the dynamics of our system:\n",
146 | "\n",
147 | "$$\n",
148 | "F u(t) = D\\left[e^{(-i \\xi - \\epsilon \\xi^2)(t-t_0)}\\right] F u(t_0).\n",
149 | "$$\n",
150 | "\n",
151 | "We can solve any linear, scalar constant-coefficient 1D evolution equation (with periodic boundary conditions) in similar manner, by simply replacing the argument of $D$. Note that this algorithm is:\n",
152 | "\n",
153 | "- Exact in time (the only error is due to our initial truncation of the Fourier series by sampling the data at a finite set of points); and\n",
154 | "- Unconditionally stable.\n",
155 | "\n",
156 | "Here is the implementation as a reminder."
157 | ]
158 | },
159 | {
160 | "cell_type": "code",
161 | "execution_count": null,
162 | "metadata": {},
163 | "outputs": [],
164 | "source": [
165 | "def spectral_representation(x0,uhat):\n",
166 | " u_fun = lambda y : np.real(np.sum(uhat*np.exp(1j*xi*(y+x0))))/len(uhat)\n",
167 | " u_fun = np.vectorize(u_fun)\n",
168 | " return u_fun\n",
169 | "\n",
170 | "# Spatial grid\n",
171 | "m=64 # Number of grid points in space\n",
172 | "L = 2 * np.pi # Width of spatial domain\n",
173 | "x = np.arange(-m/2,m/2)*(L/m) # Grid points\n",
174 | "dx = x[1]-x[0] # Grid spacing\n",
175 | "\n",
176 | "# Temporal grid\n",
177 | "tmax = 4.0 # Final time\n",
178 | "N = 25 # number grid points in time\n",
179 | "k = tmax/N # interval between output times\n",
180 | "\n",
181 | "xi = np.fft.fftfreq(m)*m*2*np.pi/L # Wavenumber \"grid\"\n",
182 | "# (this is the order in which numpy's FFT gives the frequencies)\n",
183 | "\n",
184 | "# Initial data\n",
185 | "u = np.sin(2*x)**2 * (x<-L/4)\n",
186 | "uhat0 = np.fft.fft(u)\n",
187 | "\n",
188 | "epsilon=0.01 # Diffusion coefficient\n",
189 | "a = 1.0 # Advection coefficient\n",
190 | "\n",
191 | "# Store solutions in a list for plotting later\n",
192 | "frames = [u.copy()]\n",
193 | "ftframes = [uhat0.copy()]\n",
194 | "\n",
195 | "# Now we solve the problem\n",
196 | "for n in range(1,N+1):\n",
197 | " t = n*k\n",
198 | " uhat = np.exp(-(1.j*xi*a + epsilon*xi**2)*t) * uhat0\n",
199 | " u = np.real(np.fft.ifft(uhat))\n",
200 | " frames.append(u.copy())\n",
201 | " ftframes.append(uhat.copy())\n",
202 | " \n",
203 | "# Set up plotting\n",
204 | "fig = plt.figure(figsize=(9,4)); axes = fig.add_subplot(111)\n",
205 | "line, = axes.plot([],[],lw=3)\n",
206 | "axes.set_xlim((x[0],x[-1])); axes.set_ylim((0.,1.))\n",
207 | "plt.close()\n",
208 | "\n",
209 | "x_fine = np.linspace(x[0],x[-1],1000)\n",
210 | "\n",
211 | "def plot_frame(i):\n",
212 | " uhat = ftframes[i]\n",
213 | " u_spectral = spectral_representation(x[0],uhat)\n",
214 | " line.set_data(x_fine,u_spectral(x_fine));\n",
215 | " #line.set_data(x,frames[i])\n",
216 | " axes.set_title('t='+str(i*k))\n",
217 | "\n",
218 | "# Animate the solution\n",
219 | "anim = matplotlib.animation.FuncAnimation(fig, plot_frame,\n",
220 | " frames=len(frames),\n",
221 | " interval=200)\n",
222 | "\n",
223 | "HTML(anim.to_jshtml())"
224 | ]
225 | },
226 | {
227 | "cell_type": "markdown",
228 | "metadata": {},
229 | "source": [
230 | "## Variable coefficients\n",
231 | "\n",
232 | "Things become more interesting as soon as we introduce spatially-varying coefficients or nonlinearity. In either case, the Fourier transform no longer provides a global diagonalization -- instead, it can be thought of as diagonalizing the system at one instant in time. That means that we will have to discretize in time, and the time step we can use will be restricted by accuracy and (possibly) stability considerations.\n",
233 | "\n",
234 | "Consider now the **variable-coefficient advection equation**\n",
235 | "\n",
236 | "$$ u_t + a(x) u_x = 0.$$\n",
237 | "\n",
238 | "In a purely spectral method, we would take the Fourier transform of this equation and we would need to compute a convolution of $a(x)$ with $u$, which is computationally quite expensive relative to the rest of what we will do. Specifically, the convolution would require ${\\mathcal O}(m^2)$ operations, whereas the FFT requires only ${\\mathcal O}(m \\log m)$. To avoid this expense, we use the \"pseudospectral\" approach: we compute the derivative $u_x$ in the spectral way:\n",
239 | "\n",
240 | "$$ u_x = F^{-1} \\left(D\\left[i\\xi\\right] F u\\right),$$\n",
241 | "\n",
242 | "and then multiply by $a(x)$ in physical space:\n",
243 | "\n",
244 | "$$ (a(x) u_x)_j = a(x_j) \\left(F^{-1} \\left(D\\left[i\\xi\\right] F u\\right) \\right)_j.$$"
245 | ]
246 | },
247 | {
248 | "cell_type": "markdown",
249 | "metadata": {
250 | "collapsed": true
251 | },
252 | "source": [
253 | "Letting\n",
254 | "$$\n",
255 | " A = \\begin{pmatrix} a(x_1) \\\\ & a(x_2) \\\\ & & \\ddots \\\\ & & & a(x_m) \\end{pmatrix},\n",
256 | "$$\n",
257 | "\n",
258 | "we have the semi-discrete system\n",
259 | "\n",
260 | "$$\n",
261 | "U'(t) = -A \\Finv \\left(D\\left[i\\xi\\right] \\F(U)\\right).\n",
262 | "$$"
263 | ]
264 | },
265 | {
266 | "cell_type": "markdown",
267 | "metadata": {},
268 | "source": [
269 | "### Time discretization"
270 | ]
271 | },
272 | {
273 | "cell_type": "markdown",
274 | "metadata": {
275 | "collapsed": true
276 | },
277 | "source": [
278 | "Next we need to integrate in time. To choose an appropriate time integrator, we would like to know the spectrum of our semi-discretization. We can compute it explicitly."
279 | ]
280 | },
281 | {
282 | "cell_type": "markdown",
283 | "metadata": {},
284 | "source": [
285 | "We have a linear ODE system of the form $U'(t) = MU(t)$, where\n",
286 | "\n",
287 | "$$M = -AF^{-1}D[i\\xi]F.$$\n",
288 | "\n",
289 | "The code below computes the spectrum of $M$ for a few choices of $a(x)$."
290 | ]
291 | },
292 | {
293 | "cell_type": "code",
294 | "execution_count": null,
295 | "metadata": {},
296 | "outputs": [],
297 | "source": [
298 | "m = 32\n",
299 | "F = F_matrix(m)\n",
300 | "Finv = np.linalg.inv(F)\n",
301 | "L = 2 * np.pi\n",
302 | "xi = np.fft.fftfreq(m)*m*2*np.pi/L\n",
303 | "D = np.diag(1.j*xi)\n",
304 | "x = np.arange(-m/2,m/2)*(L/m)\n",
305 | "\n",
306 | "M = np.dot(Finv,np.dot(D,F))"
307 | ]
308 | },
309 | {
310 | "cell_type": "code",
311 | "execution_count": null,
312 | "metadata": {},
313 | "outputs": [],
314 | "source": [
315 | "def plot_spectrum(a,m=64):\n",
316 | " F = F_matrix(m)\n",
317 | " Finv = np.linalg.inv(F)\n",
318 | " L = 2 * np.pi\n",
319 | " xi = np.fft.fftfreq(m)*m*2*np.pi/L\n",
320 | " D = np.diag(1.j*xi)\n",
321 | " x = np.arange(-m/2,m/2)*(L/m)\n",
322 | " A = np.diag(a(x))\n",
323 | " M = -np.dot(A,np.dot(Finv,np.dot(D,F)))\n",
324 | " lamda = np.linalg.eigvals(M)\n",
325 | " print(np.max(np.abs(lamda)))\n",
326 | " fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8,4),\n",
327 | " gridspec_kw={'width_ratios': [3, 1]})\n",
328 | " ax1.plot(x,a(x)); ax1.set_xlim(x[0],x[-1])\n",
329 | " ax1.set_xlabel(r'$x$'); ax1.set_ylabel(r'$a(x)$')\n",
330 | " ax2.plot(np.real(lamda),np.imag(lamda),'ko')\n",
331 | " ax2.axis('equal')\n",
332 | " ax2.set_xlabel(r'$Re(\\lambda)$')\n",
333 | " ax2.set_ylabel(r'$Im(\\lambda)$')\n",
334 | " plt.tight_layout()"
335 | ]
336 | },
337 | {
338 | "cell_type": "code",
339 | "execution_count": null,
340 | "metadata": {},
341 | "outputs": [],
342 | "source": [
343 | "a = lambda x : np.ones(len(x))\n",
344 | "plot_spectrum(a,m=32)"
345 | ]
346 | },
347 | {
348 | "cell_type": "code",
349 | "execution_count": null,
350 | "metadata": {},
351 | "outputs": [],
352 | "source": [
353 | "a = lambda x : 2 + np.sin(x)\n",
354 | "plot_spectrum(a,m=64)"
355 | ]
356 | },
357 | {
358 | "cell_type": "markdown",
359 | "metadata": {},
360 | "source": [
361 | "We see that the eigenvalues of $M$ are purely imaginary, regardless of the choice of $a(x)$. This is not surprising, since $M$ is the product of a real diagonal matrix $A$ and a matrix that is similar to $D$.\n",
362 | "\n",
363 | "It is also straightforward to see that the largest eigenvalues of $M$ have magnitude equal to $\\max_i |a(x_i)| m/2$."
364 | ]
365 | },
366 | {
367 | "cell_type": "markdown",
368 | "metadata": {},
369 | "source": [
370 | "Thus we should choose a time integrator whose absolute stability region includes part of the imaginary axis -- ideally, a large part of it. A simple integrator of this type is the explicit midpoint method (also referred to as the *leapfrog* method). For our problem it amounts to\n",
371 | "\n",
372 | "$$\n",
373 | "U^{n+1} = U^{n-1} + 2\\Delta t M U^n.\n",
374 | "$$\n",
375 | "\n",
376 | "Since this is a 2-step method, we need some other way to take the first step. For that, we use the explicit Euler method:\n",
377 | "\n",
378 | "$$\n",
379 | "U^{n+1} = U^n + \\Delta t M U^n.\n",
380 | "$$"
381 | ]
382 | },
383 | {
384 | "cell_type": "code",
385 | "execution_count": null,
386 | "metadata": {},
387 | "outputs": [],
388 | "source": [
389 | "#a = lambda x : np.ones(len(x))\n",
390 | "a = lambda x : 2 + np.sin(x)\n",
391 | "\n",
392 | "m = 64\n",
393 | "F = F_matrix(m)\n",
394 | "Finv = np.linalg.inv(F)\n",
395 | "L = 2 * np.pi\n",
396 | "xi = np.fft.fftfreq(m)*m*2*np.pi/L\n",
397 | "D = np.diag(1.j*xi)\n",
398 | "x = np.arange(-m/2,m/2)*(L/m)\n",
399 | "A = np.diag(a(x))\n",
400 | "#M = -np.dot(A,np.dot(Finv,np.dot(D,F)))\n",
401 | "M = -A@Finv@D@F\n",
402 | "\n",
403 | "# Initial data\n",
404 | "u = np.sin(2*x)**2 * (x<-L/4)\n",
405 | "dx = x[1]-x[0]\n",
406 | "dt = 2.0/m/np.max(np.abs(a(x)))\n",
407 | "#dt = 1./86.73416328005729 + 1e-4\n",
408 | "T = 10.\n",
409 | "N = int(np.round(T/dt))\n",
410 | "\n",
411 | "frames = [u.copy()]\n",
412 | "skip = N//100\n",
413 | "\n",
414 | "# Start with an explicit Euler step\n",
415 | "u_new = u + dt*np.dot(M,u)\n",
416 | "\n",
417 | "# Now we solve the problem\n",
418 | "for n in range(1,N+1):\n",
419 | " t = n*dt\n",
420 | " u_old = u.copy()\n",
421 | " u = u_new.copy()\n",
422 | " u_new = np.real(u_old + 2*dt*np.dot(M,u))\n",
423 | " if ((n % skip) == 0):\n",
424 | " frames.append(u_new.copy())\n",
425 | " \n",
426 | "# Set up plotting\n",
427 | "fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8,8), sharex=True)\n",
428 | "\n",
429 | "line1, = ax1.plot([],[],lw=3)\n",
430 | "ax1.set_xlim((x[0],x[-1])); ax1.set_ylim((-0.1,1.1))\n",
431 | "ax2.plot(x,a(x),lw=3); ax2.set_ylim(0,3.1)\n",
432 | "plt.close()\n",
433 | "\n",
434 | "def plot_frame(i):\n",
435 | " line1.set_data(x,frames[i])\n",
436 | " ax1.set_title('t='+str(i*skip*dt))\n",
437 | "\n",
438 | "# Animate the solution\n",
439 | "anim = matplotlib.animation.FuncAnimation(fig, plot_frame,\n",
440 | " frames=len(frames),\n",
441 | " interval=200)\n",
442 | "\n",
443 | "HTML(anim.to_jshtml())"
444 | ]
445 | },
446 | {
447 | "cell_type": "markdown",
448 | "metadata": {},
449 | "source": [
450 | "### Exercise: absolute stability\n",
451 | "The region of absolute stability for the midpoint method is the interval $S=[-i,i]$. The numerical solution will be stable only if $\\lambda \\Delta t \\in S$ for all eigenvalues $\\lambda$ of $M$. \n",
452 | "\n",
453 | "1. Determine the maximum stable step size. What happens if you take a step size larger than this value?"
454 | ]
455 | },
456 | {
457 | "cell_type": "markdown",
458 | "metadata": {},
459 | "source": [
460 | "Now, in addition to truncating the Fourier series of the initial condition, we are truncating the Taylor series of the evolution in time. This leads to truncation error that accumulates, leading to a solution that is less accurate as time progresses. \n",
461 | "\n",
462 | "2. Set $\\Delta t$ equal to the largest stable step size and notice the oscillations that appear in front of the pulse as it propagates. Try reducing the size of $\\Delta t$ to decrease the amount of truncation error. Try some different velocity functions $a(x)$."
463 | ]
464 | },
465 | {
466 | "cell_type": "markdown",
467 | "metadata": {},
468 | "source": [
469 | "## A nonlinear wave equation"
470 | ]
471 | },
472 | {
473 | "cell_type": "markdown",
474 | "metadata": {},
475 | "source": [
476 | "Finally, let us consider the case in which the speed of propagation is given by the solution value $u$ itself:\n",
477 | "\n",
478 | "$$\n",
479 | " u_t + u u_x = 0.\n",
480 | "$$\n",
481 | "\n",
482 | "We will again use the pseudospectral approach to approximate the term $uu_x$, by differentiating in frequency space and then multiplying in physical space.\n",
483 | "\n",
484 | "We can implement this easily, using the code above but replacing the entries in the matrix $A$ by the values of $u$ on the grid. We need to construct this matrix at each time step now, since $u$ is changing in time.\n",
485 | "\n",
486 | "This implementation is very inefficient but since we are working on a small grid in one dimension we can afford to ignore efficiency for now. Run the code below. Can you explain what happens? Can you \"fix\" the problem that occurs by adjusting the parameters in the code?"
487 | ]
488 | },
489 | {
490 | "cell_type": "code",
491 | "execution_count": null,
492 | "metadata": {},
493 | "outputs": [],
494 | "source": [
495 | "m = 64\n",
496 | "F = F_matrix(m)\n",
497 | "Finv = np.linalg.inv(F)\n",
498 | "L = 2 * np.pi\n",
499 | "xi = np.fft.fftfreq(m)*m*2*np.pi/L\n",
500 | "D = np.diag(1.j*xi)\n",
501 | "x = np.arange(-m/2,m/2)*(L/m)\n",
502 | "\n",
503 | "\n",
504 | "# Initial data\n",
505 | "u = np.sin(2*x)**2 * (x<-L/4) + 1.\n",
506 | "dx = x[1]-x[0]\n",
507 | "dt = 2.0/m/np.max(np.abs(a(x)))/2.\n",
508 | "T = 5.\n",
509 | "N = int(np.round(T/dt))\n",
510 | "\n",
511 | "ftframes = [np.fft.fft(u)]\n",
512 | "skip = N//100\n",
513 | "\n",
514 | "# Start with an explicit Euler step\n",
515 | "A = np.diag(u)\n",
516 | "M = -np.dot(A,np.dot(Finv,np.dot(D,F)))\n",
517 | "u_new = u + dt*np.dot(M,u)\n",
518 | "\n",
519 | "# Now we solve the problem\n",
520 | "for n in range(1,N+1):\n",
521 | " t = n*dt\n",
522 | " u_old = u.copy()\n",
523 | " u = u_new.copy()\n",
524 | " \n",
525 | " A = np.diag(u)\n",
526 | " M = -np.dot(A,np.dot(Finv,np.dot(D,F)))\n",
527 | " u_new = np.real(u_old + 2*dt*np.dot(M,u))\n",
528 | " if ((n % skip) == 0):\n",
529 | " ftframes.append(np.fft.fft(u_new))\n",
530 | " \n",
531 | "# Set up plotting\n",
532 | "fig, ax1 = plt.subplots(1, 1, figsize=(8,4))\n",
533 | "\n",
534 | "line1, = ax1.plot([],[],lw=3)\n",
535 | "ax1.set_xlim((x[0],x[-1])); ax1.set_ylim((0.,2.1))\n",
536 | "plt.close()\n",
537 | "\n",
538 | "def plot_frame(i):\n",
539 | " uhat = ftframes[i]\n",
540 | " u_spectral = spectral_representation(x[0],uhat)\n",
541 | " line1.set_data(x_fine,u_spectral(x_fine));\n",
542 | " ax1.set_title('t='+str(i*skip*dt))\n",
543 | "\n",
544 | "# Animate the solution\n",
545 | "anim = matplotlib.animation.FuncAnimation(fig, plot_frame,\n",
546 | " frames=len(ftframes),\n",
547 | " interval=200)\n",
548 | "\n",
549 | "HTML(anim.to_jshtml())"
550 | ]
551 | },
552 | {
553 | "cell_type": "code",
554 | "execution_count": null,
555 | "metadata": {},
556 | "outputs": [],
557 | "source": []
558 | }
559 | ],
560 | "metadata": {
561 | "kernelspec": {
562 | "display_name": "Python 3 (ipykernel)",
563 | "language": "python",
564 | "name": "python3"
565 | },
566 | "language_info": {
567 | "codemirror_mode": {
568 | "name": "ipython",
569 | "version": 3
570 | },
571 | "file_extension": ".py",
572 | "mimetype": "text/x-python",
573 | "name": "python",
574 | "nbconvert_exporter": "python",
575 | "pygments_lexer": "ipython3",
576 | "version": "3.13.0"
577 | },
578 | "toc": {
579 | "base_numbering": 1,
580 | "nav_menu": {},
581 | "number_sections": true,
582 | "sideBar": true,
583 | "skip_h1_title": false,
584 | "title_cell": "Table of Contents",
585 | "title_sidebar": "Contents",
586 | "toc_cell": false,
587 | "toc_position": {},
588 | "toc_section_display": true,
589 | "toc_window_display": false
590 | }
591 | },
592 | "nbformat": 4,
593 | "nbformat_minor": 4
594 | }
595 |
--------------------------------------------------------------------------------
/PSPython_03-FFT-aliasing-filtering.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "from IPython.core.display import HTML\n",
10 | "css_file = './custom.css'\n",
11 | "HTML(open(css_file, \"r\").read())"
12 | ]
13 | },
14 | {
15 | "cell_type": "markdown",
16 | "metadata": {},
17 | "source": [
18 | "###### Content provided under a Creative Commons Attribution license, CC-BY 4.0; code under MIT License. (c)2015 [David I. Ketcheson](http://davidketcheson.info)"
19 | ]
20 | },
21 | {
22 | "cell_type": "markdown",
23 | "metadata": {},
24 | "source": [
25 | "##### Version 0.3 - May 2022"
26 | ]
27 | },
28 | {
29 | "cell_type": "code",
30 | "execution_count": null,
31 | "metadata": {},
32 | "outputs": [],
33 | "source": [
34 | "import numpy as np\n",
35 | "%matplotlib inline\n",
36 | "import matplotlib\n",
37 | "import matplotlib.pyplot as plt\n",
38 | "import matplotlib.animation\n",
39 | "from IPython.display import HTML\n",
40 | "font = {'size' : 15}\n",
41 | "matplotlib.rc('font', **font)"
42 | ]
43 | },
44 | {
45 | "cell_type": "markdown",
46 | "metadata": {},
47 | "source": [
48 | "# The FFT, aliasing, and filtering"
49 | ]
50 | },
51 | {
52 | "cell_type": "markdown",
53 | "metadata": {},
54 | "source": [
55 | "Welcome to lesson 3. Here we'll learn about the Fast Fourier Transform, which we've been using all along. We'll also learn about a numerical pathology of pseudospectral methods (known as *aliasing*) and one way to avoid it (known as *filtering* or *dealiasing*)."
56 | ]
57 | },
58 | {
59 | "cell_type": "markdown",
60 | "metadata": {},
61 | "source": [
62 | "## The fast Fourier transform\n",
63 | "\n",
64 | "We won't go into great detail regarding the FFT algorithm, since there is already an excellent explanation of the Fast Fourier Transform in Jupyter Notebook form available on the web:\n",
65 | "\n",
66 | "- [Understanding the FFT Algorithm](https://jakevdp.github.io/blog/2013/08/28/understanding-the-fft/) by Jake Vanderplas\n",
67 | "\n",
68 | "Suffice it to say that the FFT is a fast algorithm for computing the discrete Fourier transform (DFT):\n",
69 | "\n",
70 | "$$\n",
71 | "\\hat{u}_\\xi = \\sum_{j=0}^{m-1} u_j e^{-2\\pi i \\xi j/m}\n",
72 | "$$\n",
73 | "\n",
74 | "or its inverse. The DFT, as we know, is linear and can be computed by multiplying $u$ by a certain $m\\times m$ dense matrix $F$. Multiplication by a dense matrix requires ${\\mathcal O}(m^2)$ operations.\n",
75 | "The FFT is a shortcut to compute that matrix-vector product in just ${\\mathcal O}(m \\log m)$ operations by taking advantage of the special structure of $F$. \n",
76 | "\n",
77 | "This is very important for pseudospectral methods, since most of the computational work occurs in computing the Fourier transform and its inverse. It's also important that we make use of a compiled version of the FFT, since native Python code is relatively slow. The `np.fft` function is an interface to a compiled FFT library."
78 | ]
79 | },
80 | {
81 | "cell_type": "markdown",
82 | "metadata": {},
83 | "source": [
84 | "### Ordering of wavenumbers in FFT output\n",
85 | "The vector returned by `np.fft` contains the Fourier coefficients of $u$ corresponding to the wavenumbers\n",
86 | "\n",
87 | "$$\n",
88 | "\\frac{2\\pi}{L} \\{-m/2, -m/2 + 1, \\dots, m/2 - 1\\}.\n",
89 | "$$\n",
90 | "\n",
91 | "However, for computational efficiency the output vector does not use the natural ordering above. The ordering it uses can be obtained with the following command."
92 | ]
93 | },
94 | {
95 | "cell_type": "code",
96 | "execution_count": null,
97 | "metadata": {},
98 | "outputs": [],
99 | "source": [
100 | "m = 16\n",
101 | "L = 2*np.pi\n",
102 | "\n",
103 | "xi=np.fft.fftfreq(m)*m/(L/(2*np.pi))\n",
104 | "print(xi)"
105 | ]
106 | },
107 | {
108 | "cell_type": "markdown",
109 | "metadata": {},
110 | "source": [
111 | "As you can see, the return vector starts with the nonnegative wavenumbers, followed by the negative wavenumbers. It may seem strange to you that the range of wavenumbers returned is not symmetric; in the case above, it includes $-8$ but not $+8$. This apparent asymmetry can be explained once one understands the phenomenon known as *aliasing*."
112 | ]
113 | },
114 | {
115 | "cell_type": "markdown",
116 | "metadata": {},
117 | "source": [
118 | "## Aliasing\n",
119 | "\n",
120 | "A numerical grid has a limited resolution. If you try to represent a rapidly-oscillating function with relatively few grid points, you will observe an effect known as **aliasing**. This naturally limits the range of frequencies that can be modelled on a given grid. It can also lead to instabilities in pseudospectral simulations, when generation of high frequencies leads to buildup of lower-frequency energy due to aliasing.\n",
121 | "\n",
122 | "The code below plots a sine wave of a given wavenumber $\\xi$, along with its representation on a grid with $m$ points. Try changing $\\xi$ and notice how for $ |\\xi| \\ge m$ the function looks like a lower-frequency mode."
123 | ]
124 | },
125 | {
126 | "cell_type": "code",
127 | "execution_count": null,
128 | "metadata": {},
129 | "outputs": [],
130 | "source": [
131 | "from ipywidgets import widgets\n",
132 | "from ipywidgets import interact, interactive\n",
133 | "\n",
134 | "def plot_sine(wavenumber=4,grid_points=12,plot_sine=True):\n",
135 | " \"Plot sin(2*pi*p), sampled at m equispaced points.\"\n",
136 | " x = np.linspace(0,1,grid_points+1); # grid\n",
137 | " xf = np.linspace(0,1,1000) # fine grid\n",
138 | " y = np.sin(wavenumber*np.pi*x)\n",
139 | " yf = np.sin(wavenumber*np.pi*xf)\n",
140 | " fig = plt.figure(figsize = (8, 6));\n",
141 | " ax = fig.add_subplot(1,1,1);\n",
142 | " if plot_sine:\n",
143 | " ax.plot(xf, yf, 'r-', linewidth=2);\n",
144 | " ax.plot(x, y, 'o-', lw=2)\n",
145 | "\n",
146 | "interact(plot_sine, wavenumber=(-30,30,1), \n",
147 | " grid_points=(5, 16, 1));"
148 | ]
149 | },
150 | {
151 | "cell_type": "markdown",
152 | "metadata": {},
153 | "source": [
154 | "### Exercise\n",
155 | "\n",
156 | "Try to answer the questions below with pencil and paper; then check them by modifying the code above.\n",
157 | "\n",
158 | "1. For a given number of grid points $m$, which wavenumbers $p$ will be aliased to the $p=0$ mode? Which will be aliased to $p=1$? Can you explain why?\n",
159 | "2. What is the highest frequency mode that can be represented on a given grid?"
160 | ]
161 | },
162 | {
163 | "cell_type": "markdown",
164 | "metadata": {},
165 | "source": [
166 | "You will find that the sampled frequencies (i.e., the set of values on the grid) repeat in the pattern illustrated below:\n",
167 | "\n",
168 | "\n",
169 | "\n",
170 | "For the points labeled with the same color, the values of the function when sampled on the grid are identical. For the points with the corresponding lighter color, the values on the grid are the same except for multiplication by $-1$."
171 | ]
172 | },
173 | {
174 | "cell_type": "markdown",
175 | "metadata": {},
176 | "source": [
177 | "## Aliasing as a source of numerical instability\n",
178 | "\n",
179 | "As we have seen, aliasing means that wavenumbers of magnitude greater than $\\pi m/L$ are incorrectly represented as lower wavenumbers on a grid with $m$ points. This suggests that we shouldn't allow larger wavenumbers in our numerical solution. For linear problems, this simply means that we should represent the initial condition by a truncated Fourier series containing modes with wavenumbers less than $\\pi m/L$. This happens naturally when we sample the function at the grid points. As we evolve in time, higher frequencies are not generated due to the linearity of the problem.\n",
180 | "\n",
181 | "Nonlinear problems are a different story. Let's consider what happens when we have a quadratic term like $u^2$, as in Burgers' equation. In general, if the grid function $u$ contains wavenumbers up to $\\pi m/L$, then $u^2$ contains frequencies up to $2 \\pi m/L$. So each time we compute this term, we generate high frequencies that get aliased back to lower frequencies on our grid. Clearly this has nothing to do with the correct mathematical solution and will lead to errors. Even worse, this aliasing effect can, as it is repeated at every step, lead to an instability that causes the numerical solution to blow up."
182 | ]
183 | },
184 | {
185 | "cell_type": "markdown",
186 | "metadata": {},
187 | "source": [
188 | "$$\n",
189 | "\\newcommand{\\F}{\\mathcal F}\n",
190 | "\\newcommand{\\Finv}{{\\mathcal F}^{-1}}\n",
191 | "$$\n",
192 | "## An illustration of aliasing instability: the Korteweg-de Vries equation\n",
193 | "\n",
194 | "To see aliasing in practice, we'll consider the KdV equation, which describes certain kinds of water waves:\n",
195 | "\n",
196 | "\n",
197 | "$$\n",
198 | "u_t = -u u_x - u_{xxx}\n",
199 | "$$\n",
200 | "\n",
201 | "A natural pseudospectral discretization is obtained if we compute the spatial derivatives via\n",
202 | "\n",
203 | "\\begin{align}\n",
204 | "u_x & = \\Finv(i\\xi \\F(u)) \\\\\n",
205 | "u_{xxx} & = \\Finv(-i\\xi^3 \\F(u)).\n",
206 | "\\end{align}\n",
207 | "This gives\n",
208 | "$$\n",
209 | "U'(t) = -D[U] \\Finv(i\\xi \\F(U)) - \\Finv(-i\\xi^3 \\F(U)).\n",
210 | "$$\n",
211 | "\n",
212 | "This is identical to our discretization of Burgers' equation, except that now we have a third-derivative term. In Fourier space, the third derivative gives a purely imaginary factor, which -- like the first derivative -- causes the solution to travel over time. Unlike the first derivative, the third derivative term causes different wavenumber modes to travel at different speeds; this is referred to as *dispersion*.\n",
213 | "\n",
214 | "The largest-magnitude eigenvalues of the Jacobian for this semi-discretization are related to the 3rd-derivative term. If we consider only that term, the eigenvalues are\n",
215 | "\n",
216 | "$$-i \\xi^3$$\n",
217 | "\n",
218 | "where $\\xi$ lies in the range $(-m/2, m/2)$. So we need the time step to satisfy $\\Delta t (m/2)^3 \\in S$, where $S$ is the region of absolute stability of a given time integrator."
219 | ]
220 | },
221 | {
222 | "cell_type": "markdown",
223 | "metadata": {},
224 | "source": [
225 | "For this example we'll use a 3rd-order Runge-Kutta method:"
226 | ]
227 | },
228 | {
229 | "cell_type": "code",
230 | "execution_count": null,
231 | "metadata": {},
232 | "outputs": [],
233 | "source": [
234 | "def rk3(u,xi,rhs):\n",
235 | " y2 = u + dt*rhs(u,xi)\n",
236 | " y3 = 0.75*u + 0.25*(y2 + dt*rhs(y2,xi))\n",
237 | " u_new = 1./3 * u + 2./3 * (y3 + dt*rhs(y3,xi))\n",
238 | " return u_new"
239 | ]
240 | },
241 | {
242 | "cell_type": "markdown",
243 | "metadata": {},
244 | "source": [
245 | "Let's check the size of the imaginary axis interval contained in this method's absolute stability region:"
246 | ]
247 | },
248 | {
249 | "cell_type": "code",
250 | "execution_count": null,
251 | "metadata": {},
252 | "outputs": [],
253 | "source": [
254 | "from nodepy import rk\n",
255 | "ssp33 = rk.loadRKM('SSP33')\n",
256 | "print(ssp33.imaginary_stability_interval())"
257 | ]
258 | },
259 | {
260 | "cell_type": "markdown",
261 | "metadata": {},
262 | "source": [
263 | "Now we'll go ahead and implement our solution, making sure to set the time step according to the condition above."
264 | ]
265 | },
266 | {
267 | "cell_type": "code",
268 | "execution_count": null,
269 | "metadata": {},
270 | "outputs": [],
271 | "source": [
272 | "def rhs(u, xi, equation='KdV'):\n",
273 | " uhat = np.fft.fft(u)\n",
274 | " if equation == 'Burgers': \n",
275 | " return -u*np.real(np.fft.ifft(1j*xi*uhat)) + np.real(np.fft.ifft(-xi**2*uhat))\n",
276 | " elif equation == 'KdV':\n",
277 | " return -u*np.real(np.fft.ifft(1j*xi*uhat)) - np.real(np.fft.ifft(-1j*xi**3*uhat))\n",
278 | " \n",
279 | "# Grid\n",
280 | "m = 256\n",
281 | "L = 2*np.pi\n",
282 | "x = np.arange(-m/2,m/2)*(L/m)\n",
283 | "xi = np.fft.fftfreq(m)*m*2*np.pi/L\n",
284 | "\n",
285 | "dt = 1.73/((m/2)**3)\n",
286 | "\n",
287 | "A = 25; B = 16;\n",
288 | "\n",
289 | "#u = 3*A**2/np.cosh(0.5*(A*(x+2.)))**2 + 3*B**2/np.cosh(0.5*(B*(x+1)))**2\n",
290 | "#tmax = 0.006\n",
291 | "\n",
292 | "# Try this one first:\n",
293 | "u = 1500*np.exp(-10*(x+2)**2)\n",
294 | "tmax = 0.005\n",
295 | "\n",
296 | "uhat2 = np.abs(np.fft.fft(u))\n",
297 | "\n",
298 | "num_plots = 50\n",
299 | "nplt = np.floor((tmax/num_plots)/dt)\n",
300 | "nmax = int(round(tmax/dt))\n",
301 | "\n",
302 | "fig = plt.figure(figsize=(12,8))\n",
303 | "axes = fig.add_subplot(211)\n",
304 | "axes2 = fig.add_subplot(212)\n",
305 | "line, = axes.plot(x,u,lw=3)\n",
306 | "line2, = axes2.semilogy(xi,uhat2)\n",
307 | "xi_max = np.max(np.abs(xi))\n",
308 | "axes2.semilogy([xi_max/2.,xi_max/2.],[1.e-6,4e8],'--r')\n",
309 | "axes2.semilogy([-xi_max/2.,-xi_max/2.],[1.e-6,4e8],'--r')\n",
310 | "axes.set_xlabel(r'$x$',fontsize=30)\n",
311 | "axes2.set_xlabel(r'$\\xi$',fontsize=30)\n",
312 | "plt.tight_layout()\n",
313 | "plt.close()\n",
314 | "\n",
315 | "frames = [u.copy()]\n",
316 | "tt = [0]\n",
317 | "uuhat = [uhat2]\n",
318 | "\n",
319 | "for n in range(1,nmax+1):\n",
320 | " u_new = rk3(u,xi,rhs)\n",
321 | "\n",
322 | " u = u_new.copy()\n",
323 | " t = n*dt\n",
324 | " # Plotting\n",
325 | " if np.mod(n,nplt) == 0:\n",
326 | " frames.append(u.copy())\n",
327 | " tt.append(t)\n",
328 | " uhat2 = np.abs(np.fft.fft(u))\n",
329 | " uuhat.append(uhat2)\n",
330 | " \n",
331 | "def plot_frame(i):\n",
332 | " line.set_data(x,frames[i])\n",
333 | " power_spectrum = np.abs(uuhat[i])**2\n",
334 | " line2.set_data(np.sort(xi),power_spectrum[np.argsort(xi)])\n",
335 | " axes.set_title('t= %.2e' % tt[i])\n",
336 | " axes.set_xlim((-np.pi,np.pi))\n",
337 | " axes.set_ylim((-200,3000))\n",
338 | " \n",
339 | "anim = matplotlib.animation.FuncAnimation(fig, plot_frame,\n",
340 | " frames=len(frames), interval=100,\n",
341 | " repeat=False)\n",
342 | "\n",
343 | "HTML(anim.to_jshtml())"
344 | ]
345 | },
346 | {
347 | "cell_type": "markdown",
348 | "metadata": {},
349 | "source": [
350 | "In the output, we're plotting the solution (top plot) and its power spectrum ($|\\hat{u}|^2$) (bottom plot). There are a lot of interesting things to say about the solution, but for now let's focus on the Fourier transform. Notice how the amount of energy in the high wavenumbers present (those outside the dashed red lines) remains relatively small. Because of this, no aliasing occurs.\n",
351 | "\n",
352 | "Now change the code above to use only $m=128$ grid points. What happens?"
353 | ]
354 | },
355 | {
356 | "cell_type": "markdown",
357 | "metadata": {},
358 | "source": [
359 | "## Explanation\n",
360 | "\n",
361 | "Here we will give a somewhat simplified explanation of the blow-up just observed. First, this blowup has nothing to do with the absoute stability condition -- when we change $m$, the time step is automatically changed in a way that will ensure absolute stability. If you're not convinced, try taking the time step even smaller; you will still observe the blowup.\n",
362 | "\n",
363 | "By taking $m=128$, we cut by half the wavenumbers that can be represented on the grid. As you can see from the plots, this means that after the solution begins to steepen, a significant amount of the energy present in the solution is in the upper half of the representable range of wavenumbers (i.e., outside the dashed red lines). That means that the highest wavenumbers generated by the quadratic term will be aliased -- and they will be aliased back into that upper-half range. This leads to a gradual and incorrect accumulation of high-wavenumber modes, easily visible in both plots. Eventually the high-wavenumber modes dominate the numerical solution and lead to blowup.\n",
364 | "\n",
365 | "For a detailed discussion of aliasing instabilities, see Chapter 11 of John Boyd's \"Chebyshev and Fourier Spectral Methods\"."
366 | ]
367 | },
368 | {
369 | "cell_type": "markdown",
370 | "metadata": {},
371 | "source": [
372 | "## Filtering\n",
373 | "\n",
374 | "How can we avoid aliasing instability? The proper approach is to ensure that the solution is well resolved, so that the instability never appears. However, this may entail a very substantial computational cost. One way to ensure stability even if the solution is underresolved is by *filtering*, which is also known as *dealiasing*. In general it is unwise to rely on filtering, since it can mask the fact that the solution is not resolved (and hence inaccurate). But understanding filtering can give a bit more insight into aliasing instability itself."
375 | ]
376 | },
377 | {
378 | "cell_type": "markdown",
379 | "metadata": {},
380 | "source": [
381 | "At the most basic level, filtering means removing the modes that lead to aliasing. This can be done by damping the high wavenumbers or simply zeroing them when computing the $(u^2)_x$ term. The obvious approach would be to filter the upper half of all wavenumbers, but this is overkill. In fact, it is sufficient to filter only the uppermost third. To see why, notice that the aliased modes resulting from the lower two-thirds will appear in the uppermost third of the range of modes, and so will be filtered at the next step.\n",
382 | "\n",
383 | "A simple 2/3 filter is implemented in the code below."
384 | ]
385 | },
386 | {
387 | "cell_type": "code",
388 | "execution_count": null,
389 | "metadata": {},
390 | "outputs": [],
391 | "source": [
392 | "def rhs(u, xi, filtr, equation='KdV'):\n",
393 | " uhat = np.fft.fft(u)\n",
394 | " if equation == 'Burgers': \n",
395 | " return -u*np.real(np.fft.ifft(1j*xi*uhat*filtr)) \\\n",
396 | " + np.real(np.fft.ifft(-xi**2*uhat))\n",
397 | " elif equation == 'KdV':\n",
398 | " return -u*np.real(np.fft.ifft(1j*xi*uhat*filtr)) \\\n",
399 | " - np.real(np.fft.ifft(-1j*xi**3*uhat))\n",
400 | " \n",
401 | "def rk3(u,xi,rhs,filtr):\n",
402 | " y2 = u + dt*rhs(u,xi,filtr)\n",
403 | " y3 = 0.75*u + 0.25*(y2 + dt*rhs(y2,xi,filtr))\n",
404 | " u_new = 1./3 * u + 2./3 * (y3 + dt*rhs(y3,xi,filtr))\n",
405 | " return u_new\n",
406 | " \n",
407 | "# Grid\n",
408 | "m = 128\n",
409 | "L = 2*np.pi\n",
410 | "x = np.arange(-m/2,m/2)*(L/m)\n",
411 | "xi = np.fft.fftfreq(m)*m*2*np.pi/L\n",
412 | "filtr = np.ones_like(xi)\n",
413 | "xi_max = np.max(np.abs(xi))\n",
414 | "filtr[np.where(np.abs(xi)>xi_max*2./3)] = 0.\n",
415 | "\n",
416 | "dt = 1.73/((m/2)**3)\n",
417 | "\n",
418 | "A = 25; B = 16;\n",
419 | "u = 3*A**2/np.cosh(0.5*(A*(x+2.)))**2 + 3*B**2/np.cosh(0.5*(B*(x+1)))**2\n",
420 | "#u = 1500*np.exp(-10*(x+2)**2)\n",
421 | "tmax = 0.012\n",
422 | "\n",
423 | "uhat2 = np.abs(np.fft.fft(u))\n",
424 | "\n",
425 | "num_plots = 200\n",
426 | "nplt = np.floor((tmax/num_plots)/dt)\n",
427 | "nmax = int(round(tmax/dt))\n",
428 | "\n",
429 | "fig = plt.figure(figsize=(12,8))\n",
430 | "axes = fig.add_subplot(211)\n",
431 | "axes2 = fig.add_subplot(212)\n",
432 | "line, = axes.plot(x,u,lw=3)\n",
433 | "line2, = axes2.semilogy(xi,uhat2)\n",
434 | "axes2.semilogy([xi_max/2.,xi_max/2.],[1.e-6,4e8],'--r')\n",
435 | "axes2.semilogy([-xi_max/2.,-xi_max/2.],[1.e-6,4e8],'--r')\n",
436 | "axes.set_xlabel(r'$x$',fontsize=30)\n",
437 | "axes2.set_xlabel(r'$\\xi$',fontsize=30)\n",
438 | "plt.tight_layout()\n",
439 | "plt.close()\n",
440 | "\n",
441 | "frames = [u.copy()]\n",
442 | "tt = [0]\n",
443 | "uuhat = [uhat2]\n",
444 | "\n",
445 | "for n in range(1,nmax+1):\n",
446 | " u_new = rk3(u,xi,rhs,filtr)\n",
447 | "\n",
448 | " u = u_new.copy()\n",
449 | " t = n*dt\n",
450 | " # Plotting\n",
451 | " if np.mod(n,nplt) == 0:\n",
452 | " frames.append(u.copy())\n",
453 | " tt.append(t)\n",
454 | " uhat2 = np.abs(np.fft.fft(u))\n",
455 | " uuhat.append(uhat2)\n",
456 | " \n",
457 | "def plot_frame(i):\n",
458 | " line.set_data(x,frames[i])\n",
459 | " power_spectrum = np.abs(uuhat[i])**2\n",
460 | " line2.set_data(np.sort(xi),power_spectrum[np.argsort(xi)])\n",
461 | " axes.set_title('t= %.2e' % tt[i])\n",
462 | " axes.set_xlim((-np.pi,np.pi))\n",
463 | " axes.set_ylim((-100,3000))\n",
464 | " \n",
465 | "anim = matplotlib.animation.FuncAnimation(fig, plot_frame,\n",
466 | " frames=len(frames), interval=20)\n",
467 | "\n",
468 | "HTML(anim.to_jshtml())"
469 | ]
470 | },
471 | {
472 | "cell_type": "markdown",
473 | "metadata": {},
474 | "source": [
475 | "Notice how the solution remains stable, but small wiggles appear throughout the domain. These are a hint that something is not sufficiently resolved."
476 | ]
477 | },
478 | {
479 | "cell_type": "markdown",
480 | "metadata": {},
481 | "source": [
482 | "## Exercises\n",
483 | "The examples we have looked at behave in a way that agrees with the ideas that have been presented here. However, the stability of nonlinear pseudospectral PDE discretizations is complicated, and it's easy to find examples that aren't fully explained by the discussion above. You can explore this by experiments like the following.\n",
484 | "\n",
485 | "1. Try decreasing $m$ even further in the example above. You may find that the solution blows up for some values. Can you prevent this by adjusting $\\Delta t$? Can you prevent it by filtering more wavenumbers?\n",
486 | "\n",
487 | "2. Try solving the inviscid Burgers' equation with a 2/3 filter. For this problem, there is no value of $m$ large enough to resolve the gradients that appear, since the gradient blows up in finite time. After the blowup time, traditionally one employs the notion of \"vanishing viscosity weak solutions\" in which a discontinuous solution is acceptable if it is given by the limit of solutions of\n",
488 | "$$\n",
489 | "u_t + uu_x = \\epsilon u_{xx}\n",
490 | "$$\n",
491 | "as $\\epsilon \\to 0$. By using filtering, can you obtain such solutions?"
492 | ]
493 | },
494 | {
495 | "cell_type": "code",
496 | "execution_count": null,
497 | "metadata": {},
498 | "outputs": [],
499 | "source": []
500 | }
501 | ],
502 | "metadata": {
503 | "kernelspec": {
504 | "display_name": "Python 3 (ipykernel)",
505 | "language": "python",
506 | "name": "python3"
507 | },
508 | "language_info": {
509 | "codemirror_mode": {
510 | "name": "ipython",
511 | "version": 3
512 | },
513 | "file_extension": ".py",
514 | "mimetype": "text/x-python",
515 | "name": "python",
516 | "nbconvert_exporter": "python",
517 | "pygments_lexer": "ipython3",
518 | "version": "3.10.4"
519 | },
520 | "toc": {
521 | "base_numbering": 1,
522 | "nav_menu": {},
523 | "number_sections": true,
524 | "sideBar": true,
525 | "skip_h1_title": false,
526 | "title_cell": "Table of Contents",
527 | "title_sidebar": "Contents",
528 | "toc_cell": false,
529 | "toc_position": {},
530 | "toc_section_display": true,
531 | "toc_window_display": false
532 | }
533 | },
534 | "nbformat": 4,
535 | "nbformat_minor": 1
536 | }
537 |
--------------------------------------------------------------------------------
/PSPython_04-Operator-splitting.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "id": "6fe6c9dc",
7 | "metadata": {},
8 | "outputs": [],
9 | "source": [
10 | "from IPython.core.display import HTML\n",
11 | "css_file = './custom.css'\n",
12 | "HTML(open(css_file, \"r\").read())"
13 | ]
14 | },
15 | {
16 | "cell_type": "markdown",
17 | "id": "3a7679d9",
18 | "metadata": {},
19 | "source": [
20 | "###### Content provided under a Creative Commons Attribution license, CC-BY 4.0; code under MIT License. (c)2015-2023 [David I. Ketcheson](http://davidketcheson.info)"
21 | ]
22 | },
23 | {
24 | "cell_type": "markdown",
25 | "id": "1e1834c1",
26 | "metadata": {},
27 | "source": [
28 | "##### Version 0.4 - March 2023"
29 | ]
30 | },
31 | {
32 | "cell_type": "code",
33 | "execution_count": null,
34 | "id": "9568f686",
35 | "metadata": {},
36 | "outputs": [],
37 | "source": [
38 | "import numpy as np\n",
39 | "%matplotlib inline\n",
40 | "import matplotlib\n",
41 | "import matplotlib.pyplot as plt\n",
42 | "import matplotlib.animation\n",
43 | "from IPython.display import HTML\n",
44 | "font = {'size' : 15}\n",
45 | "matplotlib.rc('font', **font)\n",
46 | "\n",
47 | "fft = np.fft.fft\n",
48 | "ifft = np.fft.ifft"
49 | ]
50 | },
51 | {
52 | "cell_type": "markdown",
53 | "id": "3f91ec28",
54 | "metadata": {},
55 | "source": [
56 | "# Time discretization for pseudospectral methods"
57 | ]
58 | },
59 | {
60 | "cell_type": "markdown",
61 | "id": "f2ccd06f",
62 | "metadata": {},
63 | "source": [
64 | "In previous notebooks we have touched briefly on the topic of time discretization. So far, we have used explicit Runge-Kutta methods and a simple heuristic for determining a stable time step size."
65 | ]
66 | },
67 | {
68 | "cell_type": "markdown",
69 | "id": "14cc0ddd",
70 | "metadata": {},
71 | "source": [
72 | "## Stiff semilinear problems\n",
73 | "\n",
74 | "Many important applications of pseudospectral methods involve evolution PDEs of the form\n",
75 | "\n",
76 | "$$\n",
77 | " u_t = f(u) + L(u)\n",
78 | "$$\n",
79 | "\n",
80 | "where $f$ is a non-stiff, nonlinear operator and $L$ is a stiff, linear operator. Most often, $f$ involves at most first-order derivatives while $L$ involves higher-order derivatives. An example that we have already dealt with is the KdV equation\n",
81 | "\n",
82 | "$$\n",
83 | " u_t + uu_x + u_{xxx} = 0\n",
84 | "$$\n",
85 | "\n",
86 | "in which $f(u)=-uu_x$ and $L(u)=-u_{xxx}$. Other applications which share this overall structure include many other dispersive wave models, the Navier-Stokes equations, the Kuramoto-Sivashinsky equation, and many more.\n",
87 | "\n",
88 | "Application of an explicit Runge-Kutta method to such problems is requires that the time step satisfy a condition of the form\n",
89 | "\n",
90 | "$$\n",
91 | " \\Delta t \\le C (\\Delta x)^j\n",
92 | "$$\n",
93 | "\n",
94 | "where $j$ is the order of the highest derivative in $L(u)$ and $C$ is a constant depending on the spectrum of $g$ and the stability region of the RK method. This is inefficient, since discretizations based on spectral differentiation in space and high order RK in time can usually give a reasonable local error with $\\Delta t \\approx \\Delta x$. The computational cost of an explicit time discretization becomes especially noticeable if a large number of spatial grid points is required to resolve the solution.\n",
95 | "\n",
96 | "One way to overcome this is to use a fully implicit time discretization with unconditional stability. However, this incurs the substantial cost of solving a nonlinear algebraic system of equations at each step.\n",
97 | "\n",
98 | "A number of specialized classes of time discretizations have been developed to efficiently solve problems in this class. In this notebook we will briefly examine each of the following:\n",
99 | "\n",
100 | "- Simple operator splitting (fractional-step methods)\n",
101 | "- ImEx additive Runge-Kutta methods\n",
102 | "- Exponential integrators\n",
103 | "\n",
104 | "In the examples here, we consider only stiff semilinear problems. For some problems of interest, the stiff operator $L$ is also nonlinear. In such problems, similar approaches can be employed, but the cost per step will be noticeably higher.\n",
105 | "\n",
106 | "For an excellent (though now somewhat out-of-date) study of these and other methods, see [the 2005 paper of Kassam and Trefethen](https://epubs.siam.org/doi/epdf/10.1137/S1064827502410633)."
107 | ]
108 | },
109 | {
110 | "cell_type": "markdown",
111 | "id": "d83e8dac",
112 | "metadata": {},
113 | "source": [
114 | "## Explicit integration\n",
115 | "Let's time our earlier implementation."
116 | ]
117 | },
118 | {
119 | "cell_type": "code",
120 | "execution_count": null,
121 | "id": "eb68c66a",
122 | "metadata": {},
123 | "outputs": [],
124 | "source": [
125 | "def rk3(u,xi,rhs,dt,filtr):\n",
126 | " y2 = u + dt*rhs(u,xi,filtr)\n",
127 | " y3 = 0.75*u + 0.25*(y2 + dt*rhs(y2,xi,filtr))\n",
128 | " u_new = 1./3 * u + 2./3 * (y3 + dt*rhs(y3,xi,filtr))\n",
129 | " return u_new\n",
130 | "\n",
131 | "def rhs(u, xi, filtr):\n",
132 | " uhat = np.fft.fft(u)\n",
133 | " return -u*np.real(np.fft.ifft(1j*xi*uhat)) - \\\n",
134 | " np.real(np.fft.ifft(-1j*xi**3*uhat))\n",
135 | " \n",
136 | "def solve_KdV_ERK(m,dt):\n",
137 | " L = 2*np.pi\n",
138 | " x = np.arange(-m/2,m/2)*(L/m)\n",
139 | " xi = np.fft.fftfreq(m)*m*2*np.pi/L\n",
140 | "\n",
141 | " u = 1500*np.exp(-10*(x+2)**2)\n",
142 | " tmax = 0.005\n",
143 | "\n",
144 | " uhat2 = np.abs(np.fft.fft(u))\n",
145 | "\n",
146 | " num_plots = 50\n",
147 | " nplt = np.floor((tmax/num_plots)/dt)\n",
148 | " nmax = int(round(tmax/dt))\n",
149 | "\n",
150 | " frames = [u.copy()]\n",
151 | " tt = [0]\n",
152 | " uuhat = [uhat2]\n",
153 | " filtr = np.ones_like(u)\n",
154 | " \n",
155 | " for n in range(1,nmax+1):\n",
156 | " u_new = rk3(u,xi,rhs,dt,filtr)\n",
157 | "\n",
158 | " u = u_new.copy()\n",
159 | " t = n*dt\n",
160 | " # Plotting\n",
161 | " if np.mod(n,nplt) == 0:\n",
162 | " frames.append(u.copy())\n",
163 | " tt.append(t)\n",
164 | " uhat2 = np.abs(np.fft.fft(u))\n",
165 | " uuhat.append(uhat2)\n",
166 | " return frames, uuhat, x, tt, xi\n",
167 | " \n",
168 | "\n",
169 | "\n",
170 | "def plot_solution(frames, uuhat, x, tt, xi):\n",
171 | " fig = plt.figure(figsize=(12,8))\n",
172 | " axes = fig.add_subplot(211)\n",
173 | " axes2 = fig.add_subplot(212)\n",
174 | " line, = axes.plot(x,frames[0],lw=3)\n",
175 | " line2, = axes2.semilogy(xi,np.abs(np.fft.fft(frames[0])))\n",
176 | " xi_max = np.max(np.abs(xi))\n",
177 | " axes2.semilogy([xi_max/2.,xi_max/2.],[1.e-6,4e8],'--r')\n",
178 | " axes2.semilogy([-xi_max/2.,-xi_max/2.],[1.e-8,4e10],'--r')\n",
179 | " axes.set_xlabel(r'$x$',fontsize=30)\n",
180 | " axes2.set_xlabel(r'$\\xi$',fontsize=30)\n",
181 | " plt.tight_layout()\n",
182 | " plt.close()\n",
183 | "\n",
184 | " def plot_frame(i):\n",
185 | " line.set_data(x,frames[i])\n",
186 | " power_spectrum = np.abs(uuhat[i])**2\n",
187 | " line2.set_data(np.sort(xi),power_spectrum[np.argsort(xi)])\n",
188 | " axes.set_title('t= %.2e' % tt[i])\n",
189 | " axes.set_xlim((-np.pi,np.pi))\n",
190 | " axes.set_ylim((-200,3000))\n",
191 | "\n",
192 | " anim = matplotlib.animation.FuncAnimation(fig, plot_frame,\n",
193 | " frames=len(frames), interval=100,\n",
194 | " repeat=False)\n",
195 | " return HTML(anim.to_jshtml())"
196 | ]
197 | },
198 | {
199 | "cell_type": "code",
200 | "execution_count": null,
201 | "id": "3f6b20fa",
202 | "metadata": {},
203 | "outputs": [],
204 | "source": [
205 | "%%timeit\n",
206 | "\n",
207 | "m = 512\n",
208 | "dt = 1.73/((m/2)**3)\n",
209 | "\n",
210 | "frames, uuhat, x, tt, xi = solve_KdV_ERK(m,dt)"
211 | ]
212 | },
213 | {
214 | "cell_type": "markdown",
215 | "id": "1abe216a",
216 | "metadata": {},
217 | "source": [
218 | "# Operator splitting"
219 | ]
220 | },
221 | {
222 | "cell_type": "markdown",
223 | "id": "5660e223",
224 | "metadata": {},
225 | "source": [
226 | "Operator splitting consists of alternately solving the evolution equations\n",
227 | "\n",
228 | "\\begin{align}\n",
229 | " u_t & = f(u) \\\\\n",
230 | " u_t & = L(u).\n",
231 | "\\end{align}\n",
232 | "\n",
233 | "In the simplest operator splitting approach, known in different contexts as Lie-Trotter splitting or Godunov splitting, one time step simply consists of a full time step on each equation. For the substeps, one may use any desired time integration method. For instance, if the explicit Euler method is used, the discretization takes the form\n",
234 | "\n",
235 | "\\begin{align}\n",
236 | " u^* & = u^n + \\Delta t f(u^n) \\\\\n",
237 | " u^{n+1} & = u^* + \\Delta t L(u^*).\n",
238 | "\\end{align}\n",
239 | "\n",
240 | "We can write this more abstractly as\n",
241 | "\n",
242 | "$$\n",
243 | " u^{n+1} = \\exp(\\Delta t L) \\exp(\\Delta t f) u^n,\n",
244 | "$$\n",
245 | "\n",
246 | "where it is understood that $\\exp(L)$ represents an approximation to the solution operator for the equation $u_t = L(u)$. \n",
247 | "\n",
248 | "## Accuracy\n",
249 | "\n",
250 | "Clearly, splitting methods involve two sources of discretization error:\n",
251 | "\n",
252 | " - Errors due to the discretization of the substeps\n",
253 | " - Errors due to the splitting itself\n",
254 | " \n",
255 | "In the case of Lie-Trotter splitting, even if the substeps are solved exactly, the splitting error results in a first-order accurate method. Second-order accuracy can be achieved using **Strang splitting**:\n",
256 | "\n",
257 | "$$\n",
258 | " u^{n+1} = \\exp((\\Delta t/2) L) \\exp(\\Delta t f) \\exp((\\Delta t/2) L) u^n.\n",
259 | "$$\n",
260 | "\n",
261 | "Although this seems to require 50% more substeps, in practice the cost is negligible since the half-steps with $L$ in adjacent steps can be combined, so one only needs to take a half-step at the beginning and a half-step at the end. However, in practice one often sees relatively little difference in accuracy between Lie-Trotter and Strang splitting.\n",
262 | "\n",
263 | "## Stability and time step size\n",
264 | "\n",
265 | "For stiff semilinear problems (such as the KdV equation), since $g$ is a linear differential operator, one can use a pure spectral discretization in order to solve $u_t = L(u)$ exactly (and cheaply), as discussed in the first notebook of this course. With this approach, the $L$ substep is also unconditionally stable, so the step size can be chosen based entirely on the properties of $f$ (or based on desired accuracy).\n",
266 | "\n",
267 | "For some problems, such as the nonlinear Schrodinger equation, operator splitting allows for both substeps to be solved exactly."
268 | ]
269 | },
270 | {
271 | "cell_type": "code",
272 | "execution_count": null,
273 | "id": "9d2c9697",
274 | "metadata": {
275 | "code_folding": []
276 | },
277 | "outputs": [],
278 | "source": [
279 | "def rhs_f(u, xi, filtr):\n",
280 | " # Evaluate only the non-stiff nonlinear term\n",
281 | " uhat = np.fft.fft(u)\n",
282 | " return -u*np.real(np.fft.ifft(1j*xi*uhat*filtr))\n",
283 | "\n",
284 | "#def rhs_f(u, xi, filtr):\n",
285 | "# return -np.real(ifft(1j*xi*fft(u**2)*filtr))\n",
286 | "\n",
287 | "def substep_g(u, xi, dt):\n",
288 | " # Advance the solution using only the stiff linear term\n",
289 | " uhat = np.fft.fft(u)\n",
290 | " return np.real(np.fft.ifft(np.exp(1j*xi**3*dt)*uhat))\n",
291 | "\n",
292 | "def solve_KdV_Lie_Trotter(m,dt,use_filter=True):\n",
293 | " L = 2*np.pi\n",
294 | " x = np.arange(-m/2,m/2)*(L/m)\n",
295 | " xi = np.fft.fftfreq(m)*m*2*np.pi/L\n",
296 | "\n",
297 | " u = 1500*np.exp(-10*(x+2)**2)\n",
298 | " tmax = 0.005\n",
299 | "\n",
300 | " uhat2 = np.abs(np.fft.fft(u))\n",
301 | "\n",
302 | " num_plots = 50\n",
303 | " nplt = np.floor((tmax/num_plots)/dt)\n",
304 | " nmax = int(round(tmax/dt))\n",
305 | "\n",
306 | " frames = [u.copy()]\n",
307 | " tt = [0]\n",
308 | " uuhat = [uhat2]\n",
309 | "\n",
310 | " filtr = np.ones_like(xi)\n",
311 | " \n",
312 | " if use_filter:\n",
313 | " xi_max = np.max(np.abs(xi))\n",
314 | " filtr[np.where(np.abs(xi)>xi_max*2./3)] = 0.\n",
315 | "\n",
316 | " for n in range(1,nmax+1):\n",
317 | " u_star = rk3(u,xi,rhs_f,dt,filtr)\n",
318 | " u_new = substep_g(u_star,xi,dt)\n",
319 | " \n",
320 | " u = u_new.copy()\n",
321 | " t = n*dt\n",
322 | " # Plotting\n",
323 | " if np.mod(n,nplt) == 0:\n",
324 | " frames.append(u.copy())\n",
325 | " tt.append(t)\n",
326 | " uhat2 = np.abs(np.fft.fft(u))\n",
327 | " uuhat.append(uhat2)\n",
328 | " return frames, uuhat, x, tt, xi"
329 | ]
330 | },
331 | {
332 | "cell_type": "code",
333 | "execution_count": null,
334 | "id": "28880661",
335 | "metadata": {},
336 | "outputs": [],
337 | "source": [
338 | "m = 2048\n",
339 | "umax = 3000\n",
340 | "dt = 1.73/(m/2)/umax\n",
341 | "use_filter=True\n",
342 | "\n",
343 | "frames, uuhat, x, tt, xi = solve_KdV_Lie_Trotter(m,dt,use_filter)"
344 | ]
345 | },
346 | {
347 | "cell_type": "code",
348 | "execution_count": null,
349 | "id": "e0debf94",
350 | "metadata": {
351 | "scrolled": false
352 | },
353 | "outputs": [],
354 | "source": [
355 | "plot_solution(frames, uuhat, x, tt, xi)"
356 | ]
357 | },
358 | {
359 | "cell_type": "markdown",
360 | "id": "fabbab6b",
361 | "metadata": {},
362 | "source": [
363 | "Notes:\n",
364 | "- Instability usually appears even with a fine spatial mesh, unless filtering is applied. This appears to be due to the first-order operator splitting itself exciting an aliasing instability.\n",
365 | "- Due to the improvement in linear stability, we can take a drastically larger time step. Even with a highly-refined mesh ($m=2048$) the simulation above runs in less than one second.\n",
366 | "- A more efficient implementation could reduce by one the number of FFTs required per step."
367 | ]
368 | },
369 | {
370 | "cell_type": "markdown",
371 | "id": "c77331c3",
372 | "metadata": {},
373 | "source": [
374 | "## A high-order operator splitting method\n",
375 | "\n",
376 | "Many higher-order operator splitting methods have been developed; a collection of many of them is maintained at https://www.asc.tuwien.ac.at/~winfried/splitting/index.php. Here we implement a 4th-order method that takes the form\n",
377 | "\n",
378 | "$$\n",
379 | "u^{n+1} = \\prod_{j=1}^5 e^{b_j\\Delta t f}e^{a_j\\Delta t L} u^n,\n",
380 | "$$\n",
381 | "\n",
382 | "i.e. we alternate between solving the stiff and non-stiff parts, 5 times each (actually just 4 times for the non-stiff part since $b_5=0$). This particular scheme is symmetric, meaning that $a_j = a_{6-j}$ and $b_j=b_{5-j}$."
383 | ]
384 | },
385 | {
386 | "cell_type": "code",
387 | "execution_count": null,
388 | "id": "bfc3b537",
389 | "metadata": {},
390 | "outputs": [],
391 | "source": [
392 | "a = np.array([0.267171359000977615,-0.0338279096695056672,\n",
393 | " 0.5333131013370561044,-0.0338279096695056672\n",
394 | " ,0.267171359000977615])\n",
395 | "b = np.array([-0.361837907604416033,0.861837907604416033,\n",
396 | " 0.861837907604416033,-0.361837907604416033,0.])\n",
397 | "\n",
398 | "def rhs_f(u, xi, filtr):\n",
399 | " # Evaluate only the non-stiff nonlinear term\n",
400 | " uhat = np.fft.fft(u)\n",
401 | " return -u*np.real(np.fft.ifft(1j*xi*uhat*filtr))\n",
402 | "\n",
403 | "def substep_L(u, xi, dt):\n",
404 | " # Advance the solution using only the stiff linear term\n",
405 | " uhat = np.fft.fft(u)\n",
406 | " return np.real(np.fft.ifft(np.exp(1j*xi**3*dt)*uhat))\n",
407 | "\n",
408 | "def solve_KdV_AK4(m,dt,use_filter=True):\n",
409 | " L = 2*np.pi\n",
410 | " x = np.arange(-m/2,m/2)*(L/m)\n",
411 | " xi = np.fft.fftfreq(m)*m*2*np.pi/L\n",
412 | "\n",
413 | " u = 1500*np.exp(-10*(x+2)**2)\n",
414 | " tmax = 0.005\n",
415 | "\n",
416 | " uhat2 = np.abs(np.fft.fft(u))\n",
417 | "\n",
418 | " num_plots = 50\n",
419 | " nplt = np.floor((tmax/num_plots)/dt)\n",
420 | " nmax = int(round(tmax/dt))\n",
421 | "\n",
422 | " frames = [u.copy()]\n",
423 | " tt = [0]\n",
424 | " uuhat = [uhat2]\n",
425 | "\n",
426 | " filtr = np.ones_like(xi)\n",
427 | " \n",
428 | " if use_filter:\n",
429 | " xi_max = np.max(np.abs(xi))\n",
430 | " filtr[np.where(np.abs(xi)>xi_max*2./3)] = 0.\n",
431 | "\n",
432 | " for n in range(1,nmax+1):\n",
433 | " u_star = u.copy()\n",
434 | " for j in range(5):\n",
435 | " u_star = substep_L(u_star,xi,a[j]*dt)\n",
436 | " u_star = rk3(u_star,xi,rhs_f,b[j]*dt,filtr)\n",
437 | " \n",
438 | " u = u_star.copy()\n",
439 | " t = n*dt\n",
440 | " # Plotting\n",
441 | " if np.mod(n,nplt) == 0:\n",
442 | " frames.append(u.copy())\n",
443 | " tt.append(t)\n",
444 | " uhat2 = np.abs(np.fft.fft(u))\n",
445 | " uuhat.append(uhat2)\n",
446 | " return frames, uuhat, x, tt, xi"
447 | ]
448 | },
449 | {
450 | "cell_type": "code",
451 | "execution_count": null,
452 | "id": "905439c3",
453 | "metadata": {},
454 | "outputs": [],
455 | "source": [
456 | "m = 2048\n",
457 | "umax = 3000\n",
458 | "dt = 1.73/(m/2)/umax\n",
459 | "use_filter=True\n",
460 | "\n",
461 | "frames, uuhat, x, tt, xi = solve_KdV_AK4(m,dt,use_filter)"
462 | ]
463 | },
464 | {
465 | "cell_type": "code",
466 | "execution_count": null,
467 | "id": "e0da6a40",
468 | "metadata": {},
469 | "outputs": [],
470 | "source": [
471 | "plot_solution(frames, uuhat, x, tt, xi)"
472 | ]
473 | },
474 | {
475 | "cell_type": "markdown",
476 | "id": "cd9d94a6",
477 | "metadata": {},
478 | "source": [
479 | "With this method also, it seems that filtering is necessary regardless of the spatial resolution."
480 | ]
481 | },
482 | {
483 | "cell_type": "markdown",
484 | "id": "4446025f",
485 | "metadata": {},
486 | "source": [
487 | "## Comparison"
488 | ]
489 | },
490 | {
491 | "cell_type": "code",
492 | "execution_count": null,
493 | "id": "a08dab3f",
494 | "metadata": {},
495 | "outputs": [],
496 | "source": [
497 | "m = 1024\n",
498 | "umax = 3000\n",
499 | "dt = 1.73/(m/2)/umax\n",
500 | "use_filter=True\n",
501 | "\n",
502 | "frames1, uuhat, x, tt, xi = solve_KdV_Lie_Trotter(m,dt,use_filter)\n",
503 | "frames4, uuhat, x, tt, xi = solve_KdV_AK4(m,dt,use_filter)\n",
504 | "\n",
505 | "plt.figure(figsize=(12,8))\n",
506 | "plt.plot(x,frames1[-1])\n",
507 | "plt.plot(x,frames4[-1])\n",
508 | "plt.legend(['1st-order','4th-order']);"
509 | ]
510 | },
511 | {
512 | "cell_type": "markdown",
513 | "id": "402a657c",
514 | "metadata": {},
515 | "source": [
516 | "For this problem, we don't see any difference between the 4th-order solution and the 1st-order solution. This suggests that the temporal splitting error is not the dominant part of the numerical error."
517 | ]
518 | }
519 | ],
520 | "metadata": {
521 | "kernelspec": {
522 | "display_name": "Python 3 (ipykernel)",
523 | "language": "python",
524 | "name": "python3"
525 | },
526 | "language_info": {
527 | "codemirror_mode": {
528 | "name": "ipython",
529 | "version": 3
530 | },
531 | "file_extension": ".py",
532 | "mimetype": "text/x-python",
533 | "name": "python",
534 | "nbconvert_exporter": "python",
535 | "pygments_lexer": "ipython3",
536 | "version": "3.10.4"
537 | }
538 | },
539 | "nbformat": 4,
540 | "nbformat_minor": 5
541 | }
542 |
--------------------------------------------------------------------------------
/PSPython_05-ImEx-RK.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "id": "1da9be32",
7 | "metadata": {},
8 | "outputs": [],
9 | "source": [
10 | "from IPython.core.display import HTML\n",
11 | "css_file = './custom.css'\n",
12 | "HTML(open(css_file, \"r\").read())"
13 | ]
14 | },
15 | {
16 | "cell_type": "markdown",
17 | "id": "20544b57",
18 | "metadata": {},
19 | "source": [
20 | "###### Content provided under a Creative Commons Attribution license, CC-BY 4.0; code under MIT License. (c)2015-2023 [David I. Ketcheson](http://davidketcheson.info)"
21 | ]
22 | },
23 | {
24 | "cell_type": "markdown",
25 | "id": "c45a2d8a",
26 | "metadata": {},
27 | "source": [
28 | "##### Version 0.4 - March 2023"
29 | ]
30 | },
31 | {
32 | "cell_type": "code",
33 | "execution_count": null,
34 | "id": "9f727307",
35 | "metadata": {},
36 | "outputs": [],
37 | "source": [
38 | "import numpy as np\n",
39 | "%matplotlib inline\n",
40 | "import matplotlib\n",
41 | "import matplotlib.pyplot as plt\n",
42 | "import matplotlib.animation\n",
43 | "from IPython.display import HTML\n",
44 | "font = {'size' : 15}\n",
45 | "matplotlib.rc('font', **font)\n",
46 | "\n",
47 | "fft = np.fft.fft\n",
48 | "ifft = np.fft.ifft"
49 | ]
50 | },
51 | {
52 | "cell_type": "code",
53 | "execution_count": null,
54 | "id": "13604515",
55 | "metadata": {},
56 | "outputs": [],
57 | "source": [
58 | "def plot_solution(frames, uuhat, x, tt, xi):\n",
59 | " fig = plt.figure(figsize=(12,8))\n",
60 | " axes = fig.add_subplot(211)\n",
61 | " axes2 = fig.add_subplot(212)\n",
62 | " line, = axes.plot(x,frames[0],lw=3)\n",
63 | " line2, = axes2.semilogy(xi,np.abs(np.fft.fft(frames[0])))\n",
64 | " xi_max = np.max(np.abs(xi))\n",
65 | " axes2.semilogy([xi_max/2.,xi_max/2.],[1.e-6,4e8],'--r')\n",
66 | " axes2.semilogy([-xi_max/2.,-xi_max/2.],[1.e-8,4e10],'--r')\n",
67 | " axes.set_xlabel(r'$x$',fontsize=30)\n",
68 | " axes2.set_xlabel(r'$\\xi$',fontsize=30)\n",
69 | " plt.tight_layout()\n",
70 | " plt.close()\n",
71 | "\n",
72 | " def plot_frame(i):\n",
73 | " line.set_data(x,frames[i])\n",
74 | " power_spectrum = np.abs(uuhat[i])**2\n",
75 | " line2.set_data(np.sort(xi),power_spectrum[np.argsort(xi)])\n",
76 | " axes.set_title('t= %.2e' % tt[i])\n",
77 | " axes.set_xlim((-np.pi,np.pi))\n",
78 | " axes.set_ylim((-200,3000))\n",
79 | "\n",
80 | " anim = matplotlib.animation.FuncAnimation(fig, plot_frame,\n",
81 | " frames=len(frames), interval=100,\n",
82 | " repeat=False)\n",
83 | " return HTML(anim.to_jshtml())"
84 | ]
85 | },
86 | {
87 | "cell_type": "markdown",
88 | "id": "7091030c",
89 | "metadata": {},
90 | "source": [
91 | "# ImEx methods"
92 | ]
93 | },
94 | {
95 | "cell_type": "markdown",
96 | "id": "452a935a",
97 | "metadata": {},
98 | "source": [
99 | "Next we consider the use of ImEx additive Runge-Kutta methods. In these methods, $f$ is handled explicitly while $g$ is handled implicitly. An ImEx RK method takes the form\n",
100 | "\n",
101 | "\\begin{align} \\label{ark}\n",
102 | " y_i & = u^n + h \\sum_{j=1}^{i} a_{ij} L(y_j) + h \\sum_{j=1}^{i-1}\\hat{a}_{ij} f(y_j) \\\\\n",
103 | " u^{n+1} & = u^n + h \\sum_{j=1}^s \\left(b_j L(y_j) + \\hat{b}_j f(y_j)\\right).\n",
104 | "\\end{align}\n",
105 | "\n",
106 | "The method can be seen as a combination of a diagonally-implicit RK method with coefficients $(A,b)$ and an explicit RK method with coefficients $(\\hat{A},\\hat{b})$.\n",
107 | "\n",
108 | "The idea is to benefit from treating the stiff linear term $L$ implicitly and the non-stiff nonlinear term $f$ explicitly. Stability of additive Runge-Kutta methods is complicated and a subject of ongoing research, but roughly speaking one hopes that if the implicit part of the method is A-stable then the overall method will be stable if the time step is chosen so that the explicit integrator applied to $u_t=f(u)$ would be stable.\n",
109 | "\n",
110 | "The coefficients of an additive Runge-Kutta method must satisfy additional order conditions beyond those required for the component methods.\n",
111 | "\n",
112 | "The implementation here uses a scheme developed by [Higueras et. al.](https://www.sciencedirect.com/science/article/pii/S0377042714002477) (see Eqn. (17) in that paper). It has the following useful properties:\n",
113 | "\n",
114 | "- Second-order accuracy\n",
115 | "- Some imaginary axis stability for the explicit method\n",
116 | "- L-stability for the implicit method\n",
117 | "\n",
118 | "The method coefficients are:\n",
119 | "\n",
120 | "\\begin{align}\n",
121 | "\\begin{array}{c|ccc}\n",
122 | "0 & 0 & 0 & 0 \\\\\n",
123 | "\\frac{5}{6} & \\frac{5}{6} & 0 & 0 \\\\\n",
124 | "\\frac{11}{12} & \\frac{11}{24} & \\frac{11}{24} & 0 \\\\\n",
125 | "\\hline\n",
126 | " & \\frac{24}{55} & \\frac{1}{5} & \\frac{4}{11}\n",
127 | "\\end{array}\n",
128 | "& &\n",
129 | "\\begin{array}{c|ccc}\n",
130 | "\\frac{2}{11} & \\frac{2}{11} & 0 & 0 \\\\\n",
131 | "\\frac{289}{462} & \\frac{205}{462} & \\frac{2}{11} & 0 \\\\\n",
132 | "\\frac{751}{924} & \\frac{2033}{4620} & \\frac{21}{110} & \\frac{2}{11} \\\\\n",
133 | "\\hline\n",
134 | " & \\frac{24}{55} & \\frac{1}{5} & \\frac{4}{11}\n",
135 | "\\end{array}\n",
136 | "\\end{align}\n",
137 | "\n",
138 | "The explicit part of this method includes the interval $[-1.2i,1.2i]$ in its stability interval, so we incorporate this factor into the time step size."
139 | ]
140 | },
141 | {
142 | "cell_type": "markdown",
143 | "id": "0a97c300",
144 | "metadata": {},
145 | "source": [
146 | "## Efficient implementation\n",
147 | "\n",
148 | "In the implementation of pseudospectral methods, it's important to use the FFT and inverse FFT rather than naively performing dense matrix multiplications and solves. For a pseudospectral discretization, we have\n",
149 | "\n",
150 | "$$\n",
151 | " L = F^{-1} D F,\n",
152 | "$$\n",
153 | "where $D$ is a diagonal matrix and $F, F^{-1}$ are the discrete Fourier transform and its inverse.\n",
154 | "\n",
155 | "one stage of an ImEx RK method then takes the form\n",
156 | "$$\n",
157 | " y_i = u^n + h \\sum_{j=1}^{i} a_{ij} F^{-1} D F y_j + h \\sum_{j=1}^{i-1}\\hat{a}_{ij} f(y_j).\n",
158 | "$$\n",
159 | "\n",
160 | "Solving this for $y_i$ gives\n",
161 | "$$\n",
162 | " y_i = (I-a_{ii} h F^{-1} D F)^{-1} \\left(u^n + h \\sum_{j=1}^{i-1} a_{ij} F^{-1} D F y_j + h \\sum_{j=1}^{i-1}\\hat{a}_{ij} f(y_j)\\right).\n",
163 | "$$\n",
164 | "\n",
165 | "We can write\n",
166 | "\n",
167 | "$$\n",
168 | " (I-a_{ii} h F^{-1} D F)^{-1} = F^{-1}(I-a_{ii} h D)^{-1} F.\n",
169 | "$$\n",
170 | "\n",
171 | "Note that $I-a_{ii} h D$ is a diagonal matrix, so its inverse is trivial to compute. Then the stage can be implemented in the form\n",
172 | "\n",
173 | "$$\n",
174 | " y_i = F^{-1} (I-a_{ii} h D)^{-1} F \\left(u^n + h \\sum_{j=1}^{i-1} a_{ij} F^{-1} D F y_j + h \\sum_{j=1}^{i-1}\\hat{a}_{ij} f(y_j)\\right).\n",
175 | "$$"
176 | ]
177 | },
178 | {
179 | "cell_type": "code",
180 | "execution_count": null,
181 | "id": "7059071f",
182 | "metadata": {},
183 | "outputs": [],
184 | "source": [
185 | "# Higueras (17)\n",
186 | "A = np.array([[0,0,0],[5/6.,0,0],[11/24,11/24,0]])\n",
187 | "Ahat = np.array([[2./11,0,0],[205/462.,2./11,0],[2033/4620,21/110,2/11]])\n",
188 | "b = np.array([24/55.,1./5,4./11])\n",
189 | "bhat = b\n",
190 | "\n",
191 | "def rhs_f(u, xi, filtr):\n",
192 | " # Evaluate only the non-stiff nonlinear term\n",
193 | " uhat = np.fft.fft(u)\n",
194 | " return -u*np.real(np.fft.ifft(1j*xi*uhat*filtr))\n",
195 | "\n",
196 | "def solve_KdV_ImEx2(m,dt,use_filter=True):\n",
197 | " L = 2*np.pi\n",
198 | " x = np.arange(-m/2,m/2)*(L/m)\n",
199 | " xi = np.fft.fftfreq(m)*m*2*np.pi/L\n",
200 | "\n",
201 | " u = 1500*np.exp(-10*(x+2)**2)\n",
202 | " tmax = 0.05\n",
203 | "\n",
204 | " uhat2 = np.abs(np.fft.fft(u))\n",
205 | "\n",
206 | " num_plots = 50\n",
207 | " nplt = np.floor((tmax/num_plots)/dt)\n",
208 | " nmax = int(round(tmax/dt))\n",
209 | "\n",
210 | " frames = [u.copy()]\n",
211 | " tt = [0]\n",
212 | " uuhat = [uhat2]\n",
213 | "\n",
214 | " filtr = np.ones_like(xi)\n",
215 | " \n",
216 | " if use_filter:\n",
217 | " xi_max = np.max(np.abs(xi))\n",
218 | " filtr[np.where(np.abs(xi)>xi_max*2./3)] = 0.\n",
219 | "\n",
220 | " # Assumes all diagonal entries of Ahat are equal:\n",
221 | " Lfactor = 1/(1-Ahat[0,0]*dt*1j*xi**3)\n",
222 | " D = 1j*xi**3\n",
223 | " \n",
224 | " for n in range(1,nmax+1):\n",
225 | " y1 = ifft(Lfactor*fft(u))\n",
226 | " Ly1 = ifft(D*fft(y1))\n",
227 | " fy1 = rhs_f(y1,xi,filtr)\n",
228 | " y2rhs = u + dt*(A[1,0]*fy1+Ahat[1,0]*Ly1)\n",
229 | " y2 = ifft(Lfactor*fft(y2rhs))\n",
230 | " Ly2 = ifft(D*fft(y2))\n",
231 | " fy2 = rhs_f(y2,xi,filtr)\n",
232 | " y3rhs = u + dt*(A[2,0]*fy1+A[2,1]*fy2 + Ahat[2,0]*Ly1 + \\\n",
233 | " Ahat[2,1]*Ly2)\n",
234 | " y3 = ifft(Lfactor*fft(y3rhs))\n",
235 | " Ly3 = ifft(D*fft(y3))\n",
236 | " fy3 = rhs_f(y3,xi,filtr)\n",
237 | " u_new = u + dt*np.real(b[0]*(fy1+Ly1) + b[1]*(fy2+Ly2) + \\\n",
238 | " b[2]*(fy3+Ly3))\n",
239 | " \n",
240 | " u = u_new.copy()\n",
241 | " t = n*dt\n",
242 | " # Plotting\n",
243 | " if np.mod(n,nplt) == 0:\n",
244 | " frames.append(u.copy())\n",
245 | " tt.append(t)\n",
246 | " uhat2 = np.abs(np.fft.fft(u))\n",
247 | " uuhat.append(uhat2)\n",
248 | " return frames, uuhat, x, tt, xi"
249 | ]
250 | },
251 | {
252 | "cell_type": "code",
253 | "execution_count": null,
254 | "id": "6387a5ba",
255 | "metadata": {},
256 | "outputs": [],
257 | "source": [
258 | "m = 1024\n",
259 | "umax = 3000\n",
260 | "dt = 1.2/(m/2)/umax\n",
261 | "use_filter=False\n",
262 | "\n",
263 | "frames, uuhat, x, tt, xi = solve_KdV_ImEx2(m,dt,use_filter)"
264 | ]
265 | },
266 | {
267 | "cell_type": "code",
268 | "execution_count": null,
269 | "id": "e13aac63",
270 | "metadata": {},
271 | "outputs": [],
272 | "source": [
273 | "plot_solution(frames, uuhat, x, tt, xi)"
274 | ]
275 | },
276 | {
277 | "cell_type": "markdown",
278 | "id": "5d7ace3e",
279 | "metadata": {},
280 | "source": [
281 | "We note that this method appears to be stable without filtering, as long as our spatial grid is fine enough to resolve the solution."
282 | ]
283 | },
284 | {
285 | "cell_type": "markdown",
286 | "id": "f4b4cfa5",
287 | "metadata": {},
288 | "source": [
289 | "# Concluding remarks\n",
290 | "In the [comparison by Kassam & Trefethen](https://epubs.siam.org/doi/epdf/10.1137/S1064827502410633), ImEx multistep methods did not perform well. In fact, they did not work at all for the KdV equation in particular. Here we see that an ImEx Runge-Kutta method with appropriately-designed properties seems to work extremely well for the KdV equation. In fact, out of all the efficient time-stepping methods we will study, this is the only one that does not seem to require filtering."
291 | ]
292 | }
293 | ],
294 | "metadata": {
295 | "kernelspec": {
296 | "display_name": "Python 3 (ipykernel)",
297 | "language": "python",
298 | "name": "python3"
299 | },
300 | "language_info": {
301 | "codemirror_mode": {
302 | "name": "ipython",
303 | "version": 3
304 | },
305 | "file_extension": ".py",
306 | "mimetype": "text/x-python",
307 | "name": "python",
308 | "nbconvert_exporter": "python",
309 | "pygments_lexer": "ipython3",
310 | "version": "3.10.4"
311 | }
312 | },
313 | "nbformat": 4,
314 | "nbformat_minor": 5
315 | }
316 |
--------------------------------------------------------------------------------
/PSPython_06-Exponential-integrators.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "id": "060a6dda",
7 | "metadata": {},
8 | "outputs": [],
9 | "source": [
10 | "from IPython.core.display import HTML\n",
11 | "css_file = './custom.css'\n",
12 | "HTML(open(css_file, \"r\").read())"
13 | ]
14 | },
15 | {
16 | "cell_type": "markdown",
17 | "id": "081c8761",
18 | "metadata": {},
19 | "source": [
20 | "###### Content provided under a Creative Commons Attribution license, CC-BY 4.0; code under MIT License. (c)2015-2023 [David I. Ketcheson](http://davidketcheson.info)\n",
21 | "\n",
22 | "##### Version 0.4 - March 2023"
23 | ]
24 | },
25 | {
26 | "cell_type": "code",
27 | "execution_count": null,
28 | "id": "2563c3b2",
29 | "metadata": {},
30 | "outputs": [],
31 | "source": [
32 | "import numpy as np\n",
33 | "%matplotlib inline\n",
34 | "import matplotlib\n",
35 | "import matplotlib.pyplot as plt\n",
36 | "import matplotlib.animation\n",
37 | "from IPython.display import HTML\n",
38 | "font = {'size' : 15}\n",
39 | "matplotlib.rc('font', **font)\n",
40 | "\n",
41 | "fft = np.fft.fft\n",
42 | "ifft = np.fft.ifft"
43 | ]
44 | },
45 | {
46 | "cell_type": "code",
47 | "execution_count": null,
48 | "id": "d6eebd99",
49 | "metadata": {},
50 | "outputs": [],
51 | "source": [
52 | "def plot_solution(frames, uuhat, x, tt, xi):\n",
53 | " fig = plt.figure(figsize=(12,8))\n",
54 | " axes = fig.add_subplot(211)\n",
55 | " axes2 = fig.add_subplot(212)\n",
56 | " line, = axes.plot(x,frames[0],lw=3)\n",
57 | " line2, = axes2.semilogy(xi,np.abs(np.fft.fft(frames[0])))\n",
58 | " xi_max = np.max(np.abs(xi))\n",
59 | " axes2.semilogy([xi_max/2.,xi_max/2.],[1.e-6,4e8],'--r')\n",
60 | " axes2.semilogy([-xi_max/2.,-xi_max/2.],[1.e-8,4e10],'--r')\n",
61 | " axes.set_xlabel(r'$x$',fontsize=30)\n",
62 | " axes2.set_xlabel(r'$\\xi$',fontsize=30)\n",
63 | " plt.tight_layout()\n",
64 | " plt.close()\n",
65 | "\n",
66 | " def plot_frame(i):\n",
67 | " line.set_data(x,frames[i])\n",
68 | " power_spectrum = np.abs(uuhat[i])**2\n",
69 | " line2.set_data(np.sort(xi),power_spectrum[np.argsort(xi)])\n",
70 | " axes.set_title('t= %.2e' % tt[i])\n",
71 | " axes.set_xlim((-np.pi,np.pi))\n",
72 | " axes.set_ylim((-200,3000))\n",
73 | "\n",
74 | " anim = matplotlib.animation.FuncAnimation(fig, plot_frame,\n",
75 | " frames=len(frames), interval=100,\n",
76 | " repeat=False)\n",
77 | " return HTML(anim.to_jshtml())"
78 | ]
79 | },
80 | {
81 | "cell_type": "markdown",
82 | "id": "720e5800",
83 | "metadata": {},
84 | "source": [
85 | "# Exponential methods"
86 | ]
87 | },
88 | {
89 | "cell_type": "markdown",
90 | "id": "2a9798b9",
91 | "metadata": {},
92 | "source": [
93 | "As we have seen, the exact solution of a linear PDE can be obtained simply by applying the corresponding exponential operator. We took advantage of this in the operator splitting approach above in order to solve the linear part of the problem exactly. It's natural to ask if the idea of using the exponential operator can be incorporated directly into a linear multistep or Runge-Kutta type method, in order to avoid the splitting error. This leads to the concept of exponential integrators.\n",
94 | "\n",
95 | "A natural way to derive many exponential integrators starts from Duhamel's principle (also known as the variation-of-constants formula:\n",
96 | "\n",
97 | "$$\n",
98 | "u(t_n+\\Delta t) = e^{\\Delta t L}u(t_n) + \\int_0^{\\Delta t} e^{(\\Delta t-\\tau) L} f(u(t_n+\\tau)).\n",
99 | "$$\n",
100 | "\n",
101 | "By choosing different discrete approximations of the integral, different exponential methods are obtained. One of the simplest is the first-order **exponential Euler** method:\n",
102 | "\n",
103 | "$$\n",
104 | " u^{n+1} = e^{\\Delta t L}u^n + \\Delta t \\phi_1(\\Delta t L) f(u^n).\n",
105 | "$$\n",
106 | "\n",
107 | "Here $\\phi_1(z)=(e^z-1)/z$. This method is first-order accurate. Like the explicit Euler method, it is not typically suitable for wave equations because of its stability properties."
108 | ]
109 | },
110 | {
111 | "cell_type": "markdown",
112 | "id": "6da9094c",
113 | "metadata": {},
114 | "source": [
115 | "## Exponential Euler implementation\n",
116 | "This one is linearly unstable because Euler's method doesn't contain any part of the imaginary axis. But if we take a small enough timestep, the solution looks reasonable for some time."
117 | ]
118 | },
119 | {
120 | "cell_type": "code",
121 | "execution_count": null,
122 | "id": "eaf86821",
123 | "metadata": {},
124 | "outputs": [],
125 | "source": [
126 | "def solve_KdV_Exp1(m,dt):\n",
127 | " L = 2*np.pi\n",
128 | " x = np.arange(-m/2,m/2)*(L/m)\n",
129 | " xi = np.fft.fftfreq(m)*m*2*np.pi/L\n",
130 | "\n",
131 | " u = 1500*np.exp(-10*(x+2)**2)\n",
132 | " tmax = 0.005\n",
133 | "\n",
134 | " uhat2 = np.abs(np.fft.fft(u))\n",
135 | "\n",
136 | " num_plots = 50\n",
137 | " nplt = np.floor((tmax/num_plots)/dt)\n",
138 | " nmax = int(round(tmax/dt))\n",
139 | "\n",
140 | " frames = [u.copy()]\n",
141 | " tt = [0]\n",
142 | " uuhat = [uhat2]\n",
143 | " filtr = np.ones_like(u)\n",
144 | "\n",
145 | " D = 1j*xi**3*dt\n",
146 | " eps = 1.e-16\n",
147 | " phi1 = (np.exp(D)-1.)/(1j*xi**3*dt+eps)\n",
148 | " phi1 = np.diag(phi1)\n",
149 | " \n",
150 | " for n in range(1,nmax+1):\n",
151 | " uhat = np.fft.fft(u)\n",
152 | " uux = -u*np.real(np.fft.ifft(1j*xi*uhat*filtr))\n",
153 | " uuxhat = np.fft.fft(uux)\n",
154 | " u_new = np.real(np.fft.ifft(np.exp(1j*xi**3*dt)*uhat) \\\n",
155 | " + dt*(np.fft.ifft(phi1@uuxhat)))\n",
156 | " \n",
157 | " u = u_new.copy()\n",
158 | " t = n*dt\n",
159 | " # Plotting\n",
160 | " if np.mod(n,nplt) == 0:\n",
161 | " frames.append(u.copy())\n",
162 | " tt.append(t)\n",
163 | " uhat2 = np.abs(np.fft.fft(u))\n",
164 | " uuhat.append(uhat2)\n",
165 | " return frames, uuhat, x, tt, xi"
166 | ]
167 | },
168 | {
169 | "cell_type": "code",
170 | "execution_count": null,
171 | "id": "802cb4f6",
172 | "metadata": {},
173 | "outputs": [],
174 | "source": [
175 | "m = 512\n",
176 | "umax = 1500\n",
177 | "dt = 1.73/(m/2)/umax * 0.1\n",
178 | "\n",
179 | "frames, uuhat, x, tt, xi = solve_KdV_Exp1(m,dt)"
180 | ]
181 | },
182 | {
183 | "cell_type": "code",
184 | "execution_count": null,
185 | "id": "9ef79e02",
186 | "metadata": {},
187 | "outputs": [],
188 | "source": [
189 | "plot_solution(frames, uuhat, x, tt, xi)"
190 | ]
191 | },
192 | {
193 | "cell_type": "markdown",
194 | "id": "7ca5fe57",
195 | "metadata": {},
196 | "source": [
197 | "Here we see that, even though we've taken a relatively small step size, the solution grows over time.\n",
198 | "\n",
199 | "Higher-order exponential Runge-Kutta methods can be constructed using additional stages and higher order $\\phi$ functions, which are defined recursively by\n",
200 | "$\\phi_{k+1}(z) = (\\phi_k(z)-\\phi_k(0))/z$. The accurate evaluation of these functions for both large and small values of $z$ requires special techniques, such as those employed by [Kassam & Trefethen](https://epubs.siam.org/doi/epdf/10.1137/S1064827502410633)."
201 | ]
202 | },
203 | {
204 | "cell_type": "markdown",
205 | "id": "668a2e21",
206 | "metadata": {},
207 | "source": [
208 | "## 4th-order exponential (Lawson4) method"
209 | ]
210 | },
211 | {
212 | "cell_type": "markdown",
213 | "id": "8d883ab1",
214 | "metadata": {},
215 | "source": [
216 | "Alternatively, one can design methods that use only the exponential function itself. Such methods are often referred to as Lawson-type methods. Here we implement a 4th-order Lawson-type method that is similar to a Runge-Kutta method:\n",
217 | "\n",
218 | "\\begin{align}\n",
219 | "y_i & = \\exp(c_i \\Delta t L)u^n + h \\sum_{j=1}^s a_{ij}(\\Delta t L)f(y_j) \\\\\n",
220 | "u^{n+1} & = \\exp(\\Delta t L)u^n + h \\sum_{j=1}^s b_j(\\Delta t L)f(y_j).\n",
221 | "\\end{align}\n",
222 | "\n",
223 | "The coefficients are functions of the linear operator $\\Delta t L$. Starting from a traditional Runge-Kutta method, the coefficients of a Lawson method are simply\n",
224 | "\n",
225 | "\\begin{align}\n",
226 | " a_{ij}(z) & = \\tilde{a}_{ij}(z) \\exp((c_i-c_j)z) \\\\\n",
227 | " b_j(z) & = \\tilde{b}_j(z) \\exp((1-c_j)z).\n",
228 | "\\end{align}\n",
229 | "\n",
230 | "The method implemented below is based on the classical 4th-order Runge-Kutta method, and has the following coefficients:\n",
231 | "\n",
232 | "\\begin{align}\n",
233 | "\\begin{array}{c|cccc}\n",
234 | " & & & & \\\\\n",
235 | "\\frac{1}{2} & \\frac{1}{2}e^{z/2} & & & \\\\\n",
236 | "\\frac{1}{2} & & \\frac{1}{2} & & \\\\\n",
237 | "1 & & & e^{z/2} & \\\\\n",
238 | "\\hline\n",
239 | " & \\frac{1}{6}e^z & \\frac{1}{3}e^{z/2} & \\frac{1}{3}e^{z/2} & \\frac{1}{6}\\\\\n",
240 | "\\end{array}\n",
241 | "\\end{align}"
242 | ]
243 | },
244 | {
245 | "cell_type": "code",
246 | "execution_count": null,
247 | "id": "2a08e3d7",
248 | "metadata": {},
249 | "outputs": [],
250 | "source": [
251 | "def uux(u,xi,filtr):\n",
252 | " return -u*np.real(ifft(1j*xi*fft(u)*filtr))\n",
253 | "\n",
254 | "def solve_KdV_Exp4(m,dt,use_filter=True):\n",
255 | " L = 2*np.pi\n",
256 | " x = np.arange(-m/2,m/2)*(L/m)\n",
257 | " xi = np.fft.fftfreq(m)*m*2*np.pi/L\n",
258 | "\n",
259 | " u = 1500*np.exp(-10*(x+2)**2)\n",
260 | " tmax = 0.005\n",
261 | "\n",
262 | " uhat2 = np.abs(np.fft.fft(u))\n",
263 | "\n",
264 | " num_plots = 50\n",
265 | " nplt = np.floor((tmax/num_plots)/dt)\n",
266 | " nmax = int(round(tmax/dt))\n",
267 | "\n",
268 | " frames = [u.copy()]\n",
269 | " tt = [0]\n",
270 | " uuhat = [uhat2]\n",
271 | " filtr = np.ones_like(u)\n",
272 | " if use_filter:\n",
273 | " xi_max = np.max(np.abs(xi))\n",
274 | " filtr[np.where(np.abs(xi)>xi_max*2./3)] = 0.\n",
275 | " \n",
276 | " z = 1j*xi**3*dt\n",
277 | " ez2 = np.exp(0.5*z)\n",
278 | " ez = np.exp(z)\n",
279 | " \n",
280 | " for n in range(1,nmax+1):\n",
281 | " uhat = fft(u)\n",
282 | " \n",
283 | " y2 = ifft(ez2*uhat) + dt/2 * ifft(ez2*fft(uux(u,xi,filtr)))\n",
284 | " y3 = ifft(ez2*uhat) + dt/2 * uux(y2,xi,filtr)\n",
285 | " y4 = ifft(ez*uhat) + dt * ifft(ez2*fft(uux(y3,xi,filtr)))\n",
286 | " u_new = np.real(ifft(ez*uhat) + dt/6 * (ifft(ez*fft(uux(u,xi,filtr))) \\\n",
287 | " + 2*ifft(ez2*fft(uux(y2,xi,filtr))) \\\n",
288 | " + 2*ifft(ez2*fft(uux(y3,xi,filtr))) \\\n",
289 | " + uux(y4,xi,filtr) ))\n",
290 | " u_new = ifft(filtr*fft(u_new))\n",
291 | " \n",
292 | " u = u_new.copy()\n",
293 | " t = n*dt\n",
294 | " # Plotting\n",
295 | " if np.mod(n,nplt) == 0:\n",
296 | " frames.append(u.copy())\n",
297 | " tt.append(t)\n",
298 | " uhat2 = np.abs(np.fft.fft(u))\n",
299 | " uuhat.append(uhat2)\n",
300 | " return frames, uuhat, x, tt, xi"
301 | ]
302 | },
303 | {
304 | "cell_type": "code",
305 | "execution_count": null,
306 | "id": "e46d4f65",
307 | "metadata": {},
308 | "outputs": [],
309 | "source": [
310 | "m = 2048\n",
311 | "umax = 3000\n",
312 | "dt = 1./(m/2)/umax\n",
313 | "\n",
314 | "frames, uuhat, x, tt, xi = solve_KdV_Exp4(m,dt,True)"
315 | ]
316 | },
317 | {
318 | "cell_type": "code",
319 | "execution_count": null,
320 | "id": "82dfa65a",
321 | "metadata": {},
322 | "outputs": [],
323 | "source": [
324 | "plot_solution(frames, uuhat, x, tt, xi)"
325 | ]
326 | },
327 | {
328 | "cell_type": "markdown",
329 | "id": "412cbc8e",
330 | "metadata": {},
331 | "source": [
332 | "The implementation here is not optimal and could be made even faster. Nevertheless, we see that a highly accurate solution can be obtained in a fraction of the time required for our original explicit RK implementation. We see that filtering is required, and even with filtering the solution becomes unstable after sufficiently long times."
333 | ]
334 | },
335 | {
336 | "cell_type": "markdown",
337 | "id": "20507268",
338 | "metadata": {},
339 | "source": [
340 | "## Krasny filtering\n",
341 | "As an alternative filtering approach, one can simply remove all energy from Fourier modes that already have only a small amount of energy. This is implemented below."
342 | ]
343 | },
344 | {
345 | "cell_type": "code",
346 | "execution_count": null,
347 | "id": "26b8b22f",
348 | "metadata": {},
349 | "outputs": [],
350 | "source": [
351 | "fft = np.fft.fft\n",
352 | "ifft = np.fft.ifft\n",
353 | "\n",
354 | "def uux(u,xi,filtr):\n",
355 | " return -u*np.real(ifft(1j*xi*fft(u)))\n",
356 | "\n",
357 | "def solve_KdV_Exp4(m,dt,use_filter=\"Krasny\"):\n",
358 | " L = 2*np.pi\n",
359 | " x = np.arange(-m/2,m/2)*(L/m)\n",
360 | " xi = np.fft.fftfreq(m)*m*2*np.pi/L\n",
361 | "\n",
362 | " u = 1500*np.exp(-10*(x+2)**2)\n",
363 | " tmax = 0.005\n",
364 | "\n",
365 | " uhat2 = np.abs(np.fft.fft(u))\n",
366 | "\n",
367 | " num_plots = 50\n",
368 | " nplt = np.floor((tmax/num_plots)/dt)\n",
369 | " nmax = int(round(tmax/dt))\n",
370 | "\n",
371 | " frames = [u.copy()]\n",
372 | " tt = [0]\n",
373 | " uuhat = [uhat2]\n",
374 | " \n",
375 | " z = 1j*xi**3*dt\n",
376 | " ez2 = np.exp(0.5*z)\n",
377 | " ez = np.exp(z)\n",
378 | " filtr = np.ones_like(u)\n",
379 | " \n",
380 | " for n in range(1,nmax+1):\n",
381 | " uhat = fft(u)\n",
382 | " \n",
383 | " y2 = ifft(ez2*uhat) + dt/2 * ifft(ez2*fft(uux(u,xi,filtr)))\n",
384 | " y3 = ifft(ez2*uhat) + dt/2 * uux(y2,xi,filtr)\n",
385 | " y4 = ifft(ez*uhat) + dt * ifft(ez2*fft(uux(y3,xi,filtr)))\n",
386 | " u_new = np.real(ifft(ez*uhat) + dt/6 * (ifft(ez*fft(uux(u,xi,filtr))) \\\n",
387 | " + 2*ifft(ez2*fft(uux(y2,xi,filtr))) \\\n",
388 | " + 2*ifft(ez2*fft(uux(y3,xi,filtr))) \\\n",
389 | " + uux(y4,xi,filtr) ))\n",
390 | " if use_filter == \"Krasny\":\n",
391 | " power = np.abs(fft(u_new))\n",
392 | " maxpow = np.max(power)\n",
393 | " filtr = np.ones_like(u)\n",
394 | " filtr[np.where(power/maxpow<1.e-8)] = 0.\n",
395 | " u_new = np.real(ifft(filtr*fft(u_new)))\n",
396 | " \n",
397 | " u = u_new.copy()\n",
398 | " t = n*dt\n",
399 | " # Plotting\n",
400 | " if np.mod(n,nplt) == 0:\n",
401 | " frames.append(u.copy())\n",
402 | " tt.append(t)\n",
403 | " uhat2 = np.abs(np.fft.fft(u))\n",
404 | " uuhat.append(uhat2)\n",
405 | " return frames, uuhat, x, tt, xi"
406 | ]
407 | },
408 | {
409 | "cell_type": "code",
410 | "execution_count": null,
411 | "id": "4cd35c54",
412 | "metadata": {},
413 | "outputs": [],
414 | "source": [
415 | "m = 2048\n",
416 | "umax = 1500\n",
417 | "dt = 1./(m/2)/umax\n",
418 | "\n",
419 | "frames, uuhat, x, tt, xi = solve_KdV_Exp4(m,dt,\"Krasny\")"
420 | ]
421 | },
422 | {
423 | "cell_type": "code",
424 | "execution_count": null,
425 | "id": "89be3664",
426 | "metadata": {},
427 | "outputs": [],
428 | "source": [
429 | "plot_solution(frames, uuhat, x, tt, xi)"
430 | ]
431 | },
432 | {
433 | "cell_type": "markdown",
434 | "id": "041a6437",
435 | "metadata": {},
436 | "source": [
437 | "The solution with Krasny filtering seems to be more stable overall, and doesn't blow up even after long times. The downside of this approach is that it requires a cutoff parameter. If this parameter is set too small, the solution may be unstable, while if it is see too large then accuracy may be lost."
438 | ]
439 | },
440 | {
441 | "cell_type": "markdown",
442 | "id": "6eedde83",
443 | "metadata": {},
444 | "source": [
445 | "# References\n",
446 | "\n",
447 | "There are a couple of somewhat-outdated but still useful review papers on this topic:\n",
448 | "\n",
449 | "- [Minchev & Wright](https://cds.cern.ch/record/848122/files/cer-002531456.pdf)\n",
450 | "- [Hochbruck & Ostermann](https://doi.org/10.1017/S0962492910000048)\n",
451 | "\n",
452 | "The development of this class methods continues to be a very active area of research."
453 | ]
454 | },
455 | {
456 | "cell_type": "code",
457 | "execution_count": null,
458 | "id": "f2e0966e",
459 | "metadata": {},
460 | "outputs": [],
461 | "source": []
462 | }
463 | ],
464 | "metadata": {
465 | "kernelspec": {
466 | "display_name": "Python 3 (ipykernel)",
467 | "language": "python",
468 | "name": "python3"
469 | },
470 | "language_info": {
471 | "codemirror_mode": {
472 | "name": "ipython",
473 | "version": 3
474 | },
475 | "file_extension": ".py",
476 | "mimetype": "text/x-python",
477 | "name": "python",
478 | "nbconvert_exporter": "python",
479 | "pygments_lexer": "ipython3",
480 | "version": "3.10.4"
481 | }
482 | },
483 | "nbformat": 4,
484 | "nbformat_minor": 5
485 | }
486 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PseudoSpectralPython
2 | A short course in pseudospectral collocation methods for wave equations, with implementations in Python.
3 |
4 |
5 | ###### Content provided under a Creative Commons Attribution license, CC-BY 4.0; code under MIT License. (c)2015 [David I. Ketcheson](http://davidketcheson.info)
6 |
7 | ##### Version 0.3 - May 2022
8 |
9 | Welcome to PseudoSpectralPython, a short course that will teach you how to solve wave equations using pseudospectral collocation methods. This notebook zero is just some preliminary material. If you want to dive in, just skip to [the first notebook](./PSPython_01-linear-PDEs.ipynb)!
10 |
11 | ###Table of contents:
12 |
13 | - [Course scope](#course-scope)
14 | - [Pre-requisites](#pre-requisites)
15 | - [Additional resources](#additional-resources)
16 | - [Errors, suggestions, etc.](#errors,-suggestions,-etc.)
17 |
18 | ## Course scope
19 |
20 | Pseudospectral methods are one of the simplest and fastest ways to solve many nonlinear or variable-coefficient wave equations, including
21 |
22 | - [Navier-Stokes](http://en.wikipedia.org/wiki/Navier%E2%80%93Stokes_equations) (fluid flow; both compressible and incompressible)
23 | - [Korteweg-de Vries](http://en.wikipedia.org/wiki/Korteweg%E2%80%93de_Vries_equation) (water waves)
24 | - [Nonlinear Schrodinger](http://en.wikipedia.org/wiki/Nonlinear_Schr%C3%B6dinger_equation) (photonics)
25 |
26 | and more. Pseudospectral methods are best suited to simple geometries, and in this short course we'll only consider periodic Cartesian domains. The benefit of this is that we can get our hands on interesting solutions of complicated wave equations with relatively little code and with only the computing power in your laptop.
27 |
28 | As this course is fast-paced and hands-on, we will not spend much time on deriving physical models or mathematical justifications. Such topics are better suited to a textbook format, and there are many good resources already available.
29 |
30 | ## Pre-requisites
31 |
32 | ### Python
33 |
34 | The code for this course is written in Python, which is a programming language designed to promote code that is easy to read and write. Python has become one of the most important languages in scientific computing. It is high-level like MATLAB, but unlike MATLAB it is free and is intended as a general-purpose language.
35 |
36 | You should know a little Python before starting the course. In particular, you should be familiar with the packages [numpy](http://www.numpy.org/) and [matplotlib](http://matplotlib.org/). If you aren't, a good place to start is [Lesson 0 of the HyperPython course](http://nbviewer.ipython.org/github/ketch/HyperPython/blob/master/Lesson_00_Python.ipynb). If you're completely new to Python, or to programming in general, you may wish to go through one of the many great free online Python tutorials available on the web.
37 |
38 | [IPython](http://www.ipython.org) is a collection of tools for interactive programming in Python. Most importantly for us, IPython includes an interactive shell and a browser-based notebook, now known as the Jupyter notebook. You will need at least version 3 of IPython in order to open the notebooks for this course.
39 |
40 | ### Mathematics
41 |
42 | The course will make more sense if you have had a course in partial differential equations, including Fourier transforms. Some basic complex analysis and theory of ordinary differential equations will also serve you well. However, none of the lessons are intended to be mathematically rigorous, and it's certainly possible to work through them with a less complete background.
43 |
44 | ### Software
45 |
46 | To run the code in this course, you'll need an installation of Python, numpy, matplotlib, and IPython (version >= 3.0). The easiest way to get them all is to use [SageMathCloud](http://cloud.sagemath.org) -- just create a free account, start a new project, open a terminal, and type
47 |
48 | git clone git@github.com:ketch/PseudoSpectralPython.git
49 |
50 | You'll also need JSAnimation; follow instructions [here](https://gist.github.com/gforsyth/188c32b6efe834337d8a).
51 |
52 | Open the first notebook there and you're off.
53 |
54 | You can also use [Wakari](http://wakari.io), or install everything locally on your own machine. For local installation, [Anaconda](https://store.continuum.io/cshop/anaconda/) is convenient, or you can just use pip. All of these are free.
55 |
56 | ## Additional Resources
57 |
58 | This course has benefitted from my own reading of several texts. I strongly recommend any of the following for a much more thorough introduction to spectral methods:
59 |
60 | - *Spectral methods in MATLAB*, by L. N. Trefethen. Very accessible and includes MATLAB code demonstrating everything. My favorite introduction. Some of the codes in PseudoSpectralPython benefitted directly from codes in this book.
61 | - *Chebyshev and Fourier Spectral Methods*, by John Boyd. Very verbose and with lots of diagrams. Especially relevant to this course are the following chapters: 4, 9, 10, 11, 12, 13, 14.
62 | - *A Practical Guide to Pseudospectral Methods*, by Bengt Fornberg. Much more concise than Boyd's text, and in a similar vein.
63 |
64 | The three books above are similar to PseudoSpectralPython in that they are aimed at practitioners and de-emphasize purely theoretical aspects. For a more purely mathematical treatment, I recommend *Spectral Methods for Time-Dependent Problems* by Hesthaven, Gottlieb, and Gottlieb.
65 |
66 |
67 | ## Errors, suggestions, etc.
68 |
69 | If you notice anything that could be improved -- or if you want to contribute an example, exercise, or lesson to the course -- please [raise an issue](https://github.com/ketch/PseudoSpectralPython/issues), [send a pull request](https://github.com/ketch/PseudoSpectralPython/pulls), or simply [email me](mailto:dketch@gmail.com).
70 |
--------------------------------------------------------------------------------
/aliasing_frequencies.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ketch/PseudoSpectralPython/ed1d3ab21b2301e76e258c937f01fc8d9809b2a0/aliasing_frequencies.png
--------------------------------------------------------------------------------
/custom.css:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
111 |
--------------------------------------------------------------------------------