{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "###### Content under Creative Commons Attribution license CC-BY 4.0, code under MIT license © 2015 L.A. Barba, G.F. Forsyth, B. Knaepen" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Relax and hold steady" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is the fourth and last notebook of **Module 5** (*\"Relax and hold steady\"*), dedicated to elliptic PDEs. In the [previous notebook](https://nbviewer.jupyter.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/05_relax/05_03_Iterate.This.ipynb), we examined how different algebraic formulations can speed up the iterative solution of the Laplace equation, compared to the simplest (but slowest) Jacobi method. The Gauss-Seidel and successive-over relaxation methods both provide faster algebraic convergence than Jacobi. But there is still room for improvement. \n", "\n", "In this lesson, we'll take a look at the very popular [conjugate gradient](https://en.wikipedia.org/wiki/Conjugate_gradient_method) (CG) method. \n", "The CG method solves linear systems with coefficient matrices that are symmetric and positive-definite. It is either used on its own, or in conjunction with multigrid—a technique that we'll explore later on its own (optional) course module.\n", "\n", "For a real understanding of the CG method, there is no better option than studying the now-classic monograph by Jonathan Shewchuck: *\"An introduction to the conjugate gradient method without the agonizing pain\"* (1994). Here, we try to give you a brief summary to explain the implementation in Python." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Test problem" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's return to the Poisson equation example from [Lesson 2](https://nbviewer.jupyter.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/05_relax/05_02_2D.Poisson.Equation.ipynb).\n", "\n", "$$\n", "\\begin{equation}\n", "\\nabla^2 p = -2\\left(\\frac{\\pi}{2}\\right)^2\\sin\\left( \\frac{\\pi x}{L_x} \\right) \\cos\\left(\\frac{\\pi y}{L_y}\\right)\n", "\\end{equation}\n", "$$\n", "\n", "in the domain \n", "\n", "$$\n", "\\left\\lbrace \\begin{align*}\n", "0 &\\leq x\\leq 1 \\\\\n", "-0.5 &\\leq y \\leq 0.5 \n", "\\end{align*} \\right.\n", "$$\n", "\n", "where $L_x = L_y = 1$ and with boundary conditions \n", "\n", "$$\n", "p=0 \\text{ at } \\left\\lbrace \n", "\\begin{align*}\n", "x&=0\\\\\n", "y&=0\\\\\n", "y&=-0.5\\\\\n", "y&=0.5\n", "\\end{align*} \\right.\n", "$$\n", "\n", "We will solve this equation by assuming an initial state of $p=0$ everywhere, and applying boundary conditions to relax via the Laplacian operator." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Head in the right direction!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Recall that in its discretized form, the Poisson equation reads,\n", "\n", "$$\n", "\\frac{p_{i+1,j}^{k}-2p_{i,j}^{k}+p_{i-1,j}^{k}}{\\Delta x^2}+\\frac{p_{i,j+1}^{k}-2 p_{i,j}^{k}+p_{i,j-1}^{k}}{\\Delta y^2}=b_{i,j}^{k}\n", "$$\n", "\n", "The left hand side represents a linear combination of the values of $p$ at several grid points and this linear combination has to be equal to the value of the source term, $b$, on the right hand side.\n", "\n", "Now imagine you gather the values $p_{i,j}$ of $p$ at all grid points into a big vector ${\\bf p}$ and you do the same for $b$ using the same ordering. Both vectors ${\\bf p}$ and ${\\bf b}$ contain $N=nx*ny$ values and thus belong to $\\mathbb{R}^N$. The discretized Poisson equation corresponds to the following linear system:\n", "\n", "$$\n", "\\begin{equation}\n", "A{\\bf p}={\\bf b},\n", "\\end{equation}\n", "$$\n", "\n", "where $A$ is an $N\\times N$ matrix. Although we will not directly use the matrix form of the system in the CG algorithm, it is useful to examine the problem this way to understand how the method works.\n", "\n", "All iterative methods start with an initial guess, $\\mathbf{p}^0$, and modify it in a way such that we approach the solution. This can be viewed as modifying the vector of discrete $p$ values on the grid by adding another vector, i.e., taking a step of magnitude $\\alpha$ in a direction $\\mathbf{d}$, as follows:\n", "\n", "$$\n", "\\begin{equation}\n", "{\\bf p}^{k+1}={\\bf p}^k + \\alpha {\\bf d}^k\n", "\\end{equation}\n", "$$\n", "\n", "The iterations march towards the solution by taking steps along the direction vectors ${\\bf d}^k$, with the scalar $\\alpha$ dictating how big a step to take at each iteration. We *could* converge faster to the solution if we just knew how to carefully choose the direction vectors and the size of the steps. But how to do that?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The residual" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "One of the tools we use to find the right direction to step to is called the *residual*. What is the residual? We're glad you asked!\n", "\n", "We know that, as the iterations proceed, there will be some error between the calculated value, $p^k_i$, and the exact solution $p^{exact}_i$. We may not know what the exact solution is, but we know it's out there. The error is:\n", "\n", "$$\n", "\\begin{equation}\n", "e^k_i = p^k_i - p^{exact}_i\n", "\\end{equation}\n", "$$\n", "\n", "**Note:** We are talking about error at a specific point $i$, not a measure of error across the entire domain. \n", "\n", "What if we recast the Poisson equation in terms of a not-perfectly-relaxed $\\bf p^k$?\n", "\n", "$$\n", "\\begin{equation}\n", "A \\bf p^k \\approx b\n", "\\end{equation}\n", "$$\n", "\n", "We write this as an approximation because $\\bf p^k \\neq p$. To \"fix\" the equation, we need to add an extra term to account for the difference in the Poisson equation $-$ that extra term is called the residual. We can write out the modified Poisson equation like this:\n", "\n", "$$\n", "\\begin{equation}\n", "{\\bf r^k} + A \\bf p^k = b\n", "\\end{equation}\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The method of steepest descent" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Before considering the more-complex CG algorithm, it is helpful to introduce a simpler approach called the *method of steepest descent*. At iteration $0$, we choose an initial guess. Unless we are immensely lucky, it will not satisfy the Poisson equation and we will have,\n", "\n", "$$\n", "\\begin{equation}\n", "{\\bf b}-A{\\bf p}^0={\\bf r}^0\\ne {\\bf 0}\n", "\\end{equation}\n", "$$\n", "\n", "The vector ${\\bf r}^0$ is the initial residual and measures how far we are from satisfying the linear system. We can monitor the residual vector at each iteration, as it gets (hopefully) smaller and smaller: \n", "\n", "$$\n", "\\begin{equation}\n", "{\\bf r}^k={\\bf b}-A{\\bf p}^k\n", "\\end{equation}\n", "$$\n", "\n", "We make two choices in the method of steepest descent:\n", "\n", "1. the direction vectors are the residuals ${\\bf d}^k = {\\bf r}^k$, and\n", "2. the length of the step makes the $k+1^{th}$ residual orthogonal to the $k^{th}$ residual.\n", "\n", "There are good (not very complicated) reasons to justify these choices and you should read one of the references to understand them. But since we want you to converge to the end of the notebook in a shorter time, please accept them for now. \n", "\n", "Choice 2 requires that,\n", "\n", "$$\n", "\\begin{align}\n", "{\\bf r}^{k+1}\\cdot {\\bf r}^{k} = 0 \\nonumber \\\\\n", "\\Leftrightarrow ({\\bf b}-A{\\bf p}^{k+1}) \\cdot {\\bf r}^{k} = 0 \\nonumber \\\\\n", "\\Leftrightarrow ({\\bf b}-A({\\bf p}^{k}+\\alpha {\\bf r}^k)) \\cdot {\\bf r}^{k} = 0 \\nonumber \\\\\n", "\\Leftrightarrow ({\\bf r}^k-\\alpha A{\\bf r}^k) \\cdot {\\bf r}^{k} = 0 \\nonumber \\\\\n", "\\alpha = \\frac{{\\bf r}^k \\cdot {\\bf r}^k}{A{\\bf r}^k \\cdot {\\bf r}^k}.\n", "\\end{align}\n", "$$\n", "\n", "We are now ready to test this algorithm.\n", "\n", "To begin, let's import libraries and some helper functions and set up our mesh." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import numpy\n", "from helper import l2_norm, poisson_2d_jacobi, poisson_solution" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "# Set parameters.\n", "nx = 101 # number of points in the x direction\n", "ny = 101 # number of points in the y direction\n", "xmin, xmax = 0.0, 1.0 # limits in the x direction\n", "ymin, ymax = -0.5, 0.5 # limits in the y direction\n", "Lx = xmax - xmin # domain length in the x direction\n", "Ly = ymax - ymin # domain length in the y direction\n", "dx = Lx / (nx - 1) # grid spacing in the x direction\n", "dy = Ly / (ny - 1) # grid spacing in the y direction\n", "\n", "# Create the gridline locations and the mesh grid.\n", "x = numpy.linspace(xmin, xmax, num=nx)\n", "y = numpy.linspace(ymin, ymax, num=ny)\n", "X, Y = numpy.meshgrid(x, y)\n", "\n", "# Create the source term.\n", "b = (-2.0 * (numpy.pi / Lx) * (numpy.pi / Ly) *\n", " numpy.sin(numpy.pi * X / Lx) *\n", " numpy.cos(numpy.pi * Y / Ly))\n", "\n", "# Set the initial conditions.\n", "p0 = numpy.zeros((ny, nx))\n", "\n", "# Compute the analytical solution.\n", "p_exact = poisson_solution(x, y, Lx, Ly)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Time to code steepest descent! \n", "\n", "Let's quickly review the solution process:\n", "\n", "1. Calculate the residual, $\\bf r^k$, which also serves as the direction vector, $\\bf d^k$\n", "2. Calculate the step size $\\alpha$\n", "3. Update ${\\bf p}^{k+1}={\\bf p}^k + \\alpha {\\bf d}^k$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### How do we calculate the residual? \n", "\n", "We have an equation for the residual above:\n", "\n", "$$\n", "\\begin{equation}\n", "{\\bf r}^k={\\bf b}-A{\\bf p}^k\n", "\\end{equation}\n", "$$\n", "\n", "Remember that $A$ is just a stand-in for the discrete Laplacian, which taking $\\Delta x=\\Delta y$ is:\n", "\n", "$$\n", "\\begin{equation}\n", "\\nabla^2 p^k = \\frac{-4p^k_{i,j} + \\left(p^{k}_{i,j-1} + p^k_{i,j+1} + p^{k}_{i-1,j} + p^k_{i+1,j} \\right)}{\\Delta x^2}\n", "\\end{equation}\n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### What about calculating $\\alpha$?\n", "\n", "The calculation of $\\alpha$ is relatively straightforward, but does require evaluating the term $A{\\bf r^k}$, but we just wrote the discrete $A$ operator above. You just need to apply that same formula to $\\mathbf{r}^k$." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "def poisson_2d_steepest_descent(p0, b, dx, dy,\n", " maxiter=20000, rtol=1e-6):\n", " \"\"\"\n", " Solves the 2D Poisson equation on a uniform grid,\n", " with the same grid spacing in both directions,\n", " for a given forcing term\n", " using the method of steepest descent.\n", " \n", " The function assumes Dirichlet boundary conditions with value zero.\n", " The exit criterion of the solver is based on the relative L2-norm\n", " of the solution difference between two consecutive iterations.\n", "\n", " Parameters\n", " ----------\n", " p0 : numpy.ndarray\n", " The initial solution as a 2D array of floats.\n", " b : numpy.ndarray\n", " The forcing term as a 2D array of floats.\n", " dx : float\n", " Grid spacing in the x direction.\n", " dy : float\n", " Grid spacing in the y direction.\n", " maxiter : integer, optional\n", " Maximum number of iterations to perform;\n", " default: 20000.\n", " rtol : float, optional\n", " Relative tolerance for convergence;\n", " default: 1e-6.\n", "\n", " Returns\n", " -------\n", " p : numpy.ndarray\n", " The solution after relaxation as a 2D array of floats.\n", " ite : integer\n", " The number of iterations performed.\n", " conv : list\n", " The convergence history as a list of floats.\n", " \"\"\"\n", " def A(p):\n", " # Apply the Laplacian operator to p.\n", " return (-4.0 * p[1:-1, 1:-1] +\n", " p[1:-1, :-2] + p[1:-1, 2:] +\n", " p[:-2, 1:-1] + p[2:, 1:-1]) / dx**2\n", " p = p0.copy()\n", " r = numpy.zeros_like(p) # initial residual\n", " Ar = numpy.zeros_like(p) # to store the mat-vec multiplication\n", " conv = [] # convergence history\n", " diff = rtol + 1 # initial difference\n", " ite = 0 # iteration index\n", " while diff > rtol and ite < maxiter:\n", " pk = p.copy()\n", " # Compute the residual.\n", " r[1:-1, 1:-1] = b[1:-1, 1:-1] - A(p)\n", " # Compute the Laplacian of the residual.\n", " Ar[1:-1, 1:-1] = A(r)\n", " # Compute the step size.\n", " alpha = numpy.sum(r * r) / numpy.sum(r * Ar)\n", " # Update the solution.\n", " p = pk + alpha * r\n", " # Dirichlet boundary conditions are automatically enforced.\n", " # Compute the relative L2-norm of the difference.\n", " diff = l2_norm(p, pk)\n", " conv.append(diff)\n", " ite += 1\n", " return p, ite, conv" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's see how it performs on our example problem." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Method of steepest descent: 2 iterations to reach a relative difference of 1.3307695446303778e-16\n" ] } ], "source": [ "# Compute the solution using the method of steepest descent.\n", "p, ites, conv_sd = poisson_2d_steepest_descent(p0, b, dx, dy,\n", " maxiter=20000,\n", " rtol=1e-10)\n", "print('Method of steepest descent: {} iterations '.format(ites) +\n", " 'to reach a relative difference of {}'.format(conv_sd[-1]))" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "8.225076220929745e-05" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Compute the relative L2-norm of the error.\n", "l2_norm(p, p_exact)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Not bad! it took only *two* iterations to reach a solution that meets our exit criterion. Although this seems great, the steepest descent algorithm is not too good when used with large systems or more complicated right-hand sides in the Poisson equation (we'll examine this below!). We can get better performance if we take a little more care in selecting the direction vectors, $\\bf d^k$." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The method of conjugate gradients" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "With steepest descent, we know that two **successive** jumps are orthogonal, but that's about it. There is nothing to prevent the algorithm from making several jumps in the same (or a similar) direction. Imagine you wanted to go from the intersection of 5th Avenue and 23rd Street to the intersection of 9th Avenue and 30th Street. Knowing that each segment has the same computational cost (one iteration), would you follow the red path or the green path?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Figure 1. Do you take the red path or the green path?" ] }, { "cell_type": "markdown", "metadata": { "variables": { "\\bf r}^{k+1} \\cdot {\\bf r}^{k+1": {} } }, "source": [ "The method of conjugate gradients reduces the number of jumps by making sure the algorithm never selects the same direction twice. The size of the jumps is now given by:\n", "\n", "$$\n", "\\begin{equation}\n", "\\alpha = \\frac{{\\bf r}^k \\cdot {\\bf r}^k}{A{\\bf d}^k \\cdot {\\bf d}^k}\n", "\\end{equation}\n", "$$\n", "\n", "and the direction vectors by:\n", "\n", "$$\n", "\\begin{equation}\n", "{\\bf d}^{k+1}={\\bf r}^{k+1}+\\beta{\\bf d}^{k}\n", "\\end{equation}\n", "$$\n", "\n", "where $\\beta = \\frac{{\\bf r}^{k+1} \\cdot {\\bf r}^{k+1}}{{\\bf r}^k \\cdot {\\bf r}^k}$.\n", "\n", "The search directions are no longer equal to the residuals but are instead a linear combination of the residual and the previous search direction. It turns out that CG converges to the exact solution (up to machine accuracy) in a maximum of $N$ iterations! When one is satisfied with an approximate solution, many fewer steps are needed than with any other method. Again, the derivation of the algorithm is not immensely difficult and can be found in Shewchuk (1994)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Implementing Conjugate Gradients" ] }, { "cell_type": "markdown", "metadata": { "variables": { "\\bf r}^{k+1} \\cdot {\\bf r}^{k+1": {} } }, "source": [ "We will again update $\\bf p$ according to \n", "\n", "$$\n", "\\begin{equation}\n", "{\\bf p}^{k+1}={\\bf p}^k + \\alpha {\\bf d}^k\n", "\\end{equation}\n", "$$\n", "\n", "but use the modified equations above to calculate $\\alpha$ and ${\\bf d}^k$. \n", "\n", "You may have noticed that $\\beta$ depends on both ${\\bf r}^{k+1}$ and ${\\bf r}^k$ and that makes the calculation of ${\\bf d}^0$ a little bit tricky. Or impossible (using the formula above). Instead we set ${\\bf d}^0 = {\\bf r}^0$ for the first step and then switch for all subsequent iterations. \n", "\n", "Thus, the full set of steps for the method of conjugate gradients is:\n", "\n", "Calculate ${\\bf d}^0 = {\\bf r}^0$ (just once), then\n", "\n", "1. Calculate $\\alpha = \\frac{{\\bf r}^k \\cdot {\\bf r}^k}{A{\\bf d}^k \\cdot {\\bf d}^k}$\n", "2. Update ${\\bf p}^{k+1}$\n", "3. Calculate ${\\bf r}^{k+1} = {\\bf r}^k - \\alpha A {\\bf d}^k$ $\\ \\ \\ \\ $(see Shewchuk (1994))\n", "4. Calculate $\\beta = \\frac{{\\bf r}^{k+1} \\cdot {\\bf r}^{k+1}}{{\\bf r}^k \\cdot {\\bf r}^k}$\n", "5. Calculate ${\\bf d}^{k+1}={\\bf r}^{k+1}+\\beta{\\bf d}^{k}$\n", "6. Repeat!" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "def poisson_2d_conjugate_gradient(p0, b, dx, dy,\n", " maxiter=20000, rtol=1e-6):\n", " \"\"\"\n", " Solves the 2D Poisson equation on a uniform grid,\n", " with the same grid spacing in both directions,\n", " for a given forcing term\n", " using the method of conjugate gradients.\n", " \n", " The function assumes Dirichlet boundary conditions with value zero.\n", " The exit criterion of the solver is based on the relative L2-norm\n", " of the solution difference between two consecutive iterations.\n", "\n", " Parameters\n", " ----------\n", " p0 : numpy.ndarray\n", " The initial solution as a 2D array of floats.\n", " b : numpy.ndarray\n", " The forcing term as a 2D array of floats.\n", " dx : float\n", " Grid spacing in the x direction.\n", " dy : float\n", " Grid spacing in the y direction.\n", " maxiter : integer, optional\n", " Maximum number of iterations to perform;\n", " default: 20000.\n", " rtol : float, optional\n", " Relative tolerance for convergence;\n", " default: 1e-6.\n", "\n", " Returns\n", " -------\n", " p : numpy.ndarray\n", " The solution after relaxation as a 2D array of floats.\n", " ite : integer\n", " The number of iterations performed.\n", " conv : list\n", " The convergence history as a list of floats.\n", " \"\"\"\n", " def A(p):\n", " # Apply the Laplacian operator to p.\n", " return (-4.0 * p[1:-1, 1:-1] +\n", " p[1:-1, :-2] + p[1:-1, 2:] +\n", " p[:-2, 1:-1] + p[2:, 1:-1]) / dx**2\n", " p = p0.copy()\n", " r = numpy.zeros_like(p) # initial residual\n", " Ad = numpy.zeros_like(p) # to store the mat-vec multiplication\n", " conv = [] # convergence history\n", " diff = rtol + 1 # initial difference\n", " ite = 0 # iteration index\n", " # Compute the initial residual.\n", " r[1:-1, 1:-1] = b[1:-1, 1:-1] - A(p)\n", " # Set the initial search direction to be the residual.\n", " d = r.copy()\n", " while diff > rtol and ite < maxiter:\n", " pk = p.copy()\n", " rk = r.copy()\n", " # Compute the Laplacian of the search direction.\n", " Ad[1:-1, 1:-1] = A(d)\n", " # Compute the step size.\n", " alpha = numpy.sum(r * r) / numpy.sum(d * Ad)\n", " # Update the solution.\n", " p = pk + alpha * d\n", " # Update the residual.\n", " r = rk - alpha * Ad\n", " # Update the search direction.\n", " beta = numpy.sum(r * r) / numpy.sum(rk * rk)\n", " d = r + beta * d\n", " # Dirichlet boundary conditions are automatically enforced.\n", " # Compute the relative L2-norm of the difference.\n", " diff = l2_norm(p, pk)\n", " conv.append(diff)\n", " ite += 1\n", " return p, ite, conv" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Method of conjugate gradients: 2 iterations to reach a relative difference of 1.2982770796281907e-16\n" ] } ], "source": [ "# Compute the solution using the method of conjugate gradients.\n", "p, ites, conv_cg = poisson_2d_conjugate_gradient(p0, b, dx, dy,\n", " maxiter=20000,\n", " rtol=1e-10)\n", "print('Method of conjugate gradients: {} iterations '.format(ites) +\n", " 'to reach a relative difference of {}'.format(conv_cg[-1]))" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "8.225076220929585e-05" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Compute the relative L2-norm of the error.\n", "l2_norm(p, p_exact)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The method of conjugate gradients also took two iterations to reach a solution that meets our exit criterion. But let's compare this to the number of iterations needed for the Jacobi iteration:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Jacobi relaxation: 31227 iterations to reach a relative difference of 9.997923503623598e-11\n" ] } ], "source": [ "# Compute the solution using Jacobi relaxation.\n", "p, ites, conv_jacobi = poisson_2d_jacobi(p0, b, dx, dy,\n", " maxiter=40000,\n", " rtol=1e-10)\n", "print('Jacobi relaxation: {} iterations '.format(ites) +\n", " 'to reach a relative difference of {}'.format(conv_jacobi[-1]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For our test problem, we get substantial gains in terms of computational cost using the method of steepest descent or the conjugate gradient method." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## More difficult Poisson problems" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The conjugate gradient method really shines when one needs to solve more difficult Poisson problems. To get an insight into this, let's solve the Poisson problem using the same boundary conditions as the previous problem but with the following right-hand side,\n", "\n", "$$\n", "\\begin{equation}\n", "b = \\sin\\left(\\frac{\\pi x}{L_x}\\right) \\cos\\left(\\frac{\\pi y}{L_y}\\right) + \\sin\\left(\\frac{6\\pi x}{L_x}\\right) \\cos\\left(\\frac{6\\pi y}{L_y}\\right)\n", "\\end{equation}\n", "$$" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "# Modify the source term of the Poisson system.\n", "b = (numpy.sin(numpy.pi * X / Lx) *\n", " numpy.cos(numpy.pi * Y / Ly) +\n", " numpy.sin(6.0 * numpy.pi * X / Lx) *\n", " numpy.cos(6.0 * numpy.pi * Y / Ly))" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Jacobi relaxation: 31226 iterations\n", "Method of steepest descent: 31591 iterations\n", "Method of conjugate gradients: 72 iterations\n" ] } ], "source": [ "maxiter, rtol = 40000, 1e-10\n", "p, ites, conv = poisson_2d_jacobi(p0, b, dx, dy,\n", " maxiter=maxiter, rtol=rtol)\n", "print('Jacobi relaxation: {} iterations'.format(ites))\n", "p, ites, conv = poisson_2d_steepest_descent(p0, b, dx, dy,\n", " maxiter=maxiter,\n", " rtol=rtol)\n", "print('Method of steepest descent: {} iterations'.format(ites))\n", "p, ites, conv = poisson_2d_conjugate_gradient(p0, b, dx, dy,\n", " maxiter=maxiter,\n", " rtol=rtol)\n", "print('Method of conjugate gradients: {} iterations'.format(ites))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can really appreciate the marvel of the CG method!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## References" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "Shewchuk, J. (1994). [An Introduction to the Conjugate Gradient Method Without the Agonizing Pain (PDF)](http://www.cs.cmu.edu/~quake-papers/painless-conjugate-gradient.pdf)\n", "\n", "Ilya Kuzovkin, [The Concept of Conjugate Gradient Descent in Python](http://ikuz.eu/2015/04/15/the-concept-of-conjugate-gradient-descent-in-python/)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "###### The cell below loads the style of this notebook." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from IPython.core.display import HTML\n", "css_file = '../../styles/numericalmoocstyle.css'\n", "HTML(open(css_file, 'r').read())" ] } ], "metadata": { "anaconda-cloud": {}, "kernelspec": { "display_name": "Python 3 (MAE6286)", "language": "python", "name": "py36-mae6286" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.9" } }, "nbformat": 4, "nbformat_minor": 2 }