Jupyter notebooks

This is a Jupyter notebook using Python. You can install Jupyter locally to edit and interact with this notebook.

Finite Difference methods in 2 dimensions

Let's start by generalizing the 1D Laplacian,

\begin{align} - u''(x) &= f(x) \text{ on } \Omega = (a,b) & u(a) &= g_0(a) & u'(b) = g_1(b) \end{align}

to two dimensions

\begin{align} -\nabla\cdot \big( \nabla u(x,y) \big) &= f(x,y) \text{ on } \Omega \subset \mathbb R^2 & u|_{\Gamma_D} &= g_0(x,y) & \nabla u \cdot \hat n|_{\Gamma_N} &= g_1(x,y) \end{align}

where $\Omega$ is some well-connected open set (we will assume simply connected) and the Dirichlet boundary $\Gamma_D \subset \partial \Omega$ is nonempty.

We need to choose a system for specifying the domain $\Omega$ and ordering degrees of freedom. Perhaps the most significant limitation of finite difference methods is that this specification is messy for complicated domains. We will choose $$ \Omega = (0, 1) \times (0, 1) $$ and \begin{align} (x, y)_{im+j} &= (i h, j h) & h &= 1/(m-1) & i,j \in \{0, 1, \dotsc, m-1 \} . \end{align}


In [1]:
%matplotlib inline
import numpy
from matplotlib import pyplot
pyplot.style.use('ggplot')

def laplacian2d_dense(h, f, g0):
    m = int(1/h + 1)
    c = numpy.linspace(0, 1, m)
    y, x = numpy.meshgrid(c, c)
    u0 = g0(x, y).flatten()
    rhs = f(x, y).flatten()
    A = numpy.zeros((m*m, m*m))
    def idx(i, j):
        return i*m + j
    for i in range(m):
        for j in range(m):
            row = idx(i, j)
            if i in (0, m-1) or j in (0, m-1):
                A[row, row] = 1
                rhs[row] = u0[row]
            else:
                cols = [idx(*pair) for pair in
                        [(i-1, j), (i, j-1), (i, j), (i, j+1), (i+1, j)]]
                stencil = 1/h**2 * numpy.array([-1, -1, 4, -1, -1])
                A[row, cols] = stencil
    return x, y, A, rhs

x, y, A, rhs = laplacian2d_dense(.1, lambda x,y: 0*x+1, lambda x,y: 0*x)

pyplot.spy(A);



In [2]:
u = numpy.linalg.solve(A, rhs).reshape(x.shape)

pyplot.contourf(x, y, u)
pyplot.colorbar();



In [3]:
import cProfile
prof = cProfile.Profile()
prof.enable()
x, y, A, rhs = laplacian2d_dense(.0125, lambda x,y: 0*x+1, lambda x,y: 0*x)
u = numpy.linalg.solve(A, rhs).reshape(x.shape)
prof.disable()
prof.print_stats(sort='tottime')


         50365 function calls in 2.706 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    2.623    2.623    2.623    2.623 linalg.py:300(solve)
        1    0.063    0.063    0.083    0.083 <ipython-input-1-bc43fafc383c>:6(laplacian2d_dense)
     6251    0.009    0.000    0.009    0.000 {built-in method numpy.core.multiarray.array}
     6241    0.007    0.000    0.010    0.000 <ipython-input-1-bc43fafc383c>:22(<listcomp>)
    37766    0.005    0.000    0.005    0.000 <ipython-input-1-bc43fafc383c>:13(idx)
        3    0.000    0.000    0.000    0.000 {built-in method builtins.compile}
        2    0.000    0.000    0.000    0.000 {method 'copy' of 'numpy.ndarray' objects}
        1    0.000    0.000    0.000    0.000 function_base.py:25(linspace)
        3    0.000    0.000    2.706    0.902 interactiveshell.py:2880(run_code)
        3    0.000    0.000    0.000    0.000 codeop.py:132(__call__)
        2    0.000    0.000    0.000    0.000 stride_tricks.py:115(_broadcast_to)
        2    0.000    0.000    0.000    0.000 {method 'astype' of 'numpy.ndarray' objects}
        2    0.000    0.000    0.000    0.000 linalg.py:106(_makearray)
        2    0.000    0.000    0.000    0.000 {method 'flatten' of 'numpy.ndarray' objects}
        1    0.000    0.000    0.000    0.000 function_base.py:4554(meshgrid)
        1    0.000    0.000    0.000    0.000 {built-in method numpy.core.multiarray.zeros}
        1    0.000    0.000    0.000    0.000 <ipython-input-3-e477be7765b2>:4(<lambda>)
        1    0.000    0.000    0.083    0.083 <ipython-input-3-e477be7765b2>:4(<module>)
        3    0.000    0.000    0.000    0.000 hooks.py:142(__call__)
        1    0.000    0.000    2.623    2.623 <ipython-input-3-e477be7765b2>:5(<module>)
        1    0.000    0.000    0.000    0.000 linalg.py:139(_commonType)
        1    0.000    0.000    0.000    0.000 linalg.py:209(_assertNdSquareness)
        3    0.000    0.000    0.000    0.000 ipstruct.py:125(__getattr__)
        3    0.000    0.000    0.000    0.000 {method 'reshape' of 'numpy.ndarray' objects}
        3    0.000    0.000    2.706    0.902 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 stride_tricks.py:195(broadcast_arrays)
        1    0.000    0.000    0.000    0.000 {built-in method numpy.core.multiarray.arange}
        1    0.000    0.000    0.000    0.000 linalg.py:198(_assertRankAtLeast2)
        1    0.000    0.000    0.000    0.000 function_base.py:4671(<listcomp>)
        2    0.000    0.000    0.000    0.000 linalg.py:124(_realType)
        1    0.000    0.000    0.000    0.000 stride_tricks.py:176(_broadcast_shape)
        1    0.000    0.000    0.000    0.000 linalg.py:101(get_linalg_error_extobj)
        4    0.000    0.000    0.000    0.000 numeric.py:534(asanyarray)
        1    0.000    0.000    0.000    0.000 <ipython-input-3-e477be7765b2>:6(<module>)
        2    0.000    0.000    0.000    0.000 function_base.py:213(iterable)
        1    0.000    0.000    0.000    0.000 function_base.py:13(_index_deprecate)
        3    0.000    0.000    0.000    0.000 interactiveshell.py:1069(user_global_ns)
        1    0.000    0.000    0.000    0.000 function_base.py:4684(<listcomp>)
        2    0.000    0.000    0.000    0.000 stride_tricks.py:251(<genexpr>)
        3    0.000    0.000    0.000    0.000 linalg.py:111(isComplexType)
        2    0.000    0.000    0.000    0.000 {built-in method builtins.any}
        2    0.000    0.000    0.000    0.000 {built-in method builtins.getattr}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.max}
        6    0.000    0.000    0.000    0.000 stride_tricks.py:120(<genexpr>)
        1    0.000    0.000    0.000    0.000 stride_tricks.py:257(<listcomp>)
        2    0.000    0.000    0.000    0.000 numeric.py:463(asarray)
        1    0.000    0.000    0.000    0.000 {built-in method numpy.core.multiarray.result_type}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        3    0.000    0.000    0.000    0.000 hooks.py:207(pre_run_code_hook)
        1    0.000    0.000    0.000    0.000 {built-in method builtins.all}
        5    0.000    0.000    0.000    0.000 {built-in method builtins.issubclass}
        2    0.000    0.000    0.000    0.000 {built-in method builtins.len}
        2    0.000    0.000    0.000    0.000 {method 'get' of 'dict' objects}
        4    0.000    0.000    0.000    0.000 {method 'pop' of 'dict' objects}
        2    0.000    0.000    0.000    0.000 stride_tricks.py:25(_maybe_view_as_subclass)
        1    0.000    0.000    0.000    0.000 stride_tricks.py:247(<listcomp>)
        1    0.000    0.000    0.000    0.000 {method '__array_prepare__' of 'numpy.ndarray' objects}
        1    0.000    0.000    0.000    0.000 {built-in method _operator.index}
        2    0.000    0.000    0.000    0.000 {built-in method builtins.iter}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.min}



In [4]:
import scipy.sparse as sp
import scipy.sparse.linalg

def laplacian2d(h, f, g0):
    m = int(1/h + 1)  # Number of elements in terms of nominal grid spacing h
    h = 1 / (m-1)     # Actual grid spacing
    c = numpy.linspace(0, 1, m)
    y, x = numpy.meshgrid(c, c)
    u0 = g0(x, y).flatten()
    rhs = f(x, y).flatten()
    A = sp.lil_matrix((m*m, m*m))
    def idx(i, j):
        return i*m + j
    mask = numpy.zeros_like(x, dtype=int)
    mask[1:-1,1:-1] = 1
    mask = mask.flatten()
    for i in range(m):
        for j in range(m):
            row = idx(i, j)
            stencili = numpy.array([idx(*pair) for pair in 
                                    [(i-1, j), (i, j-1),
                                     (i, j),
                                     (i, j+1), (i+1, j)]])
            stencilw = 1/h**2 * numpy.array([-1, -1, 4, -1, -1])
            if mask[row] == 0: # Dirichlet boundary
                A[row, row] = 1
                rhs[row] = u0[row]
            else:
                smask = mask[stencili]
                cols = stencili[smask == 1]
                A[row, cols] = stencilw[smask == 1]
                bdycols = stencili[smask == 0]
                rhs[row] -= stencilw[smask == 0] @ u0[bdycols]
    return x, y, A.tocsr(), rhs

x, y, A, rhs = laplacian2d(.15, lambda x,y: 0*x+1, lambda x,y: 0*x)

pyplot.spy(A);
sp.linalg.norm(A - A.T)


Out[4]:
0.0

In [5]:
prof = cProfile.Profile()
prof.enable()
x, y, A, rhs = laplacian2d(.005, lambda x,y: 0*x+1, lambda x,y: 0*x)
u = sp.linalg.spsolve(A, rhs).reshape(x.shape)
prof.disable()
prof.print_stats(sort='tottime')


         5362376 function calls in 4.868 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
   158406    0.753    0.000    1.025    0.000 stride_tricks.py:115(_broadcast_to)
        1    0.752    0.752    4.644    4.644 <ipython-input-4-66fa325aa3de>:4(laplacian2d)
   516438    0.321    0.000    0.321    0.000 {built-in method numpy.core.multiarray.array}
    39601    0.283    0.000    0.283    0.000 {scipy.sparse._csparsetools.lil_fancy_set}
    40401    0.252    0.000    3.562    0.000 lil.py:333(__setitem__)
    79203    0.220    0.000    1.645    0.000 stride_tricks.py:195(broadcast_arrays)
        1    0.218    0.218    0.218    0.218 {built-in method scipy.sparse.linalg.dsolve._superlu.gssv}
    79202    0.167    0.000    0.349    0.000 sputils.py:331(_check_boolean)
    39601    0.160    0.000    1.592    0.000 sputils.py:351(_index_to_arrays)
    79203    0.158    0.000    0.167    0.000 stride_tricks.py:176(_broadcast_shape)
    39601    0.137    0.000    0.486    0.000 sputils.py:265(_unpack_index)
   834034    0.117    0.000    0.117    0.000 {built-in method builtins.isinstance}
    79202    0.101    0.000    0.273    0.000 shape_base.py:11(atleast_1d)
   118805    0.076    0.000    0.076    0.000 {built-in method builtins.hasattr}
   158406    0.070    0.000    0.121    0.000 {built-in method builtins.any}
    79203    0.069    0.000    1.094    0.000 stride_tricks.py:257(<listcomp>)
        1    0.067    0.067    0.067    0.067 lil.py:84(__init__)
   118804    0.063    0.000    0.089    0.000 <frozen importlib._bootstrap>:402(parent)
    39601    0.062    0.000    0.062    0.000 lil.py:486(_prepare_index_for_memoryview)
    39604    0.057    0.000    0.057    0.000 {method 'reshape' of 'numpy.ndarray' objects}
    40401    0.054    0.000    0.080    0.000 <ipython-input-4-66fa325aa3de>:20(<listcomp>)
   237609    0.052    0.000    0.085    0.000 base.py:1111(isspmatrix)
    39601    0.052    0.000    0.056    0.000 sputils.py:293(_check_ellipsis)
   396016    0.051    0.000    0.051    0.000 stride_tricks.py:120(<genexpr>)
   158406    0.051    0.000    0.051    0.000 stride_tricks.py:251(<genexpr>)
   158406    0.048    0.000    0.048    0.000 stride_tricks.py:25(_maybe_view_as_subclass)
    39603    0.047    0.000    0.052    0.000 numeric.py:2135(isscalar)
   158406    0.046    0.000    0.069    0.000 function_base.py:213(iterable)
    79203    0.045    0.000    0.079    0.000 stride_tricks.py:247(<listcomp>)
   242406    0.039    0.000    0.039    0.000 <ipython-input-4-66fa325aa3de>:12(idx)
    39603    0.036    0.000    0.102    0.000 sputils.py:183(isscalarlike)
   118804    0.033    0.000    0.110    0.000 <frozen importlib._bootstrap>:989(_handle_fromlist)
   278821    0.031    0.000    0.031    0.000 {built-in method builtins.len}
   118804    0.026    0.000    0.026    0.000 {method 'rpartition' of 'str' objects}
   158406    0.024    0.000    0.024    0.000 {built-in method builtins.iter}
    79206    0.024    0.000    0.096    0.000 numeric.py:534(asanyarray)
    79203    0.021    0.000    0.049    0.000 {built-in method builtins.all}
    80807    0.013    0.000    0.013    0.000 base.py:100(get_shape)
    79206    0.012    0.000    0.012    0.000 {method 'pop' of 'dict' objects}
    39614    0.011    0.000    0.067    0.000 numeric.py:463(asarray)
    79202    0.011    0.000    0.011    0.000 {method 'append' of 'list' objects}
    39601    0.011    0.000    0.015    0.000 sputils.py:227(isdense)
        1    0.010    0.010    0.043    0.043 lil.py:463(tocsr)
        1    0.006    0.006    4.650    4.650 <ipython-input-5-8cdfec67b31c>:3(<module>)
    80802    0.006    0.000    0.006    0.000 {method 'extend' of 'list' objects}
        1    0.004    0.004    0.006    0.006 lil.py:464(<listcomp>)
      800    0.002    0.000    0.002    0.000 {built-in method scipy.sparse._csparsetools.lil_insert}
        2    0.002    0.001    0.002    0.001 {method 'copy' of 'numpy.ndarray' objects}
        3    0.000    0.000    0.000    0.000 {method 'flatten' of 'numpy.ndarray' objects}
        1    0.000    0.000    0.000    0.000 <ipython-input-5-8cdfec67b31c>:3(<lambda>)
        1    0.000    0.000    0.000    0.000 {built-in method builtins.sum}
        2    0.000    0.000    0.000    0.000 {built-in method numpy.core.multiarray.empty}
        3    0.000    0.000    0.000    0.000 {built-in method builtins.compile}
        1    0.000    0.000    0.000    0.000 {built-in method scipy.sparse._sparsetools.csr_has_sorted_indices}
        1    0.000    0.000    0.000    0.000 {built-in method numpy.core.multiarray.empty_like}
        1    0.000    0.000    0.000    0.000 {method 'cumsum' of 'numpy.ndarray' objects}
        1    0.000    0.000    0.000    0.000 function_base.py:25(linspace)
        1    0.000    0.000    0.218    0.218 linsolve.py:62(spsolve)
        3    0.000    0.000    4.868    1.623 interactiveshell.py:2880(run_code)
        1    0.000    0.000    0.000    0.000 {built-in method numpy.core.multiarray.copyto}
        1    0.000    0.000    0.000    0.000 compressed.py:128(check_format)
        3    0.000    0.000    0.000    0.000 getlimits.py:507(__init__)
        3    0.000    0.000    0.000    0.000 sputils.py:119(get_index_dtype)
        1    0.000    0.000    0.000    0.000 {built-in method numpy.core.multiarray.concatenate}
        1    0.000    0.000    0.000    0.000 compressed.py:25(__init__)
        1    0.000    0.000    0.002    0.002 function_base.py:4554(meshgrid)
        3    0.000    0.000    0.000    0.000 codeop.py:132(__call__)
        2    0.000    0.000    0.000    0.000 sputils.py:188(isintlike)
        1    0.000    0.000    0.000    0.000 base.py:142(asfptype)
        1    0.000    0.000    0.000    0.000 numeric.py:87(zeros_like)
        2    0.000    0.000    0.000    0.000 base.py:70(__init__)
        2    0.000    0.000    0.000    0.000 sputils.py:200(isshape)
        1    0.000    0.000    0.218    0.218 <ipython-input-5-8cdfec67b31c>:4(<module>)
        3    0.000    0.000    0.000    0.000 ipstruct.py:125(__getattr__)
        1    0.000    0.000    0.000    0.000 sputils.py:95(getdtype)
        3    0.000    0.000    0.000    0.000 hooks.py:142(__call__)
        5    0.000    0.000    0.000    0.000 compressed.py:100(getnnz)
        1    0.000    0.000    0.000    0.000 function_base.py:4671(<listcomp>)
        1    0.000    0.000    0.000    0.000 fromnumeric.py:55(_wrapfunc)
        1    0.000    0.000    0.000    0.000 {built-in method numpy.core.multiarray.arange}
        1    0.000    0.000    0.000    0.000 compressed.py:1065(prune)
        8    0.000    0.000    0.000    0.000 cycler.py:227(<genexpr>)
        1    0.000    0.000    0.002    0.002 function_base.py:4684(<listcomp>)
        3    0.000    0.000    4.868    1.623 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 compressed.py:1025(__get_sorted)
        2    0.000    0.000    0.000    0.000 fromnumeric.py:2584(ndim)
        1    0.000    0.000    0.000    0.000 sputils.py:91(to_native)
        1    0.000    0.000    0.000    0.000 data.py:22(__init__)
        1    0.000    0.000    0.000    0.000 lil.py:130(set_shape)
        1    0.000    0.000    0.000    0.000 {built-in method numpy.core.multiarray.zeros}
        2    0.000    0.000    0.000    0.000 _util.py:128(_prune_array)
        1    0.000    0.000    0.000    0.000 base.py:77(set_shape)
        3    0.000    0.000    0.000    0.000 getlimits.py:532(max)
        1    0.000    0.000    0.000    0.000 fromnumeric.py:2053(cumsum)
        1    0.000    0.000    0.000    0.000 {method 'ravel' of 'numpy.ndarray' objects}
        5    0.000    0.000    0.000    0.000 base.py:193(nnz)
        1    0.000    0.000    0.000    0.000 <ipython-input-5-8cdfec67b31c>:5(<module>)
        1    0.000    0.000    0.000    0.000 {method 'newbyteorder' of 'numpy.dtype' objects}
        1    0.000    0.000    0.000    0.000 {built-in method numpy.core.multiarray.result_type}
        3    0.000    0.000    0.000    0.000 interactiveshell.py:1069(user_global_ns)
        1    0.000    0.000    0.000    0.000 {built-in method builtins.getattr}
        2    0.000    0.000    0.000    0.000 csc.py:220(isspmatrix_csc)
        1    0.000    0.000    0.000    0.000 base.py:562(__getattr__)
        1    0.000    0.000    0.000    0.000 function_base.py:13(_index_deprecate)
        1    0.000    0.000    0.000    0.000 {method 'astype' of 'numpy.ndarray' objects}
        1    0.000    0.000    0.000    0.000 {built-in method numpy.core.multiarray.promote_types}
        2    0.000    0.000    0.000    0.000 {built-in method builtins.max}
        3    0.000    0.000    0.000    0.000 csr.py:231(_swap)
        1    0.000    0.000    0.000    0.000 csr.py:458(isspmatrix_csr)
        1    0.000    0.000    0.000    0.000 compressed.py:1056(sort_indices)
        3    0.000    0.000    0.000    0.000 data.py:25(_get_dtype)
        3    0.000    0.000    0.000    0.000 hooks.py:207(pre_run_code_hook)
        1    0.000    0.000    0.000    0.000 {built-in method _operator.index}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}


A manufactured solution


In [6]:
class mms0:
    def u(x, y):
        return x*numpy.exp(-x)*numpy.tanh(y)
    def grad_u(x, y):
        return numpy.array([(1 - x)*numpy.exp(-x)*numpy.tanh(y),
                            x*numpy.exp(-x)*(1 - numpy.tanh(y)**2)])
    def laplacian_u(x, y):
        return ((2 - x)*numpy.exp(-x)*numpy.tanh(y)
                - 2*x*numpy.exp(-x)*(numpy.tanh(y)**2 - 1)*numpy.tanh(y))
    def grad_u_dot_normal(x, y, n):
        return grad_u(x, y) @ n

x, y, A, rhs = laplacian2d(.02, mms0.laplacian_u, mms0.u)
u = sp.linalg.spsolve(A, rhs).reshape(x.shape)
print(u.shape, numpy.linalg.norm((u - mms0.u(x,y)).flatten(), numpy.inf))
pyplot.contourf(x, y, u)
pyplot.colorbar()
pyplot.title('Numeric solution')
pyplot.figure()
pyplot.contourf(x, y, u - mms0.u(x, y))
pyplot.colorbar()
pyplot.title('Error');


(51, 51) 1.15397307746e-06

In [7]:
hs = numpy.logspace(-2, -.5, 12)
def mms_error(h):
    x, y, A, rhs = laplacian2d(h, mms0.laplacian_u, mms0.u)
    u = sp.linalg.spsolve(A, rhs).reshape(x.shape)
    return numpy.linalg.norm((u - mms0.u(x, y)).flatten(), numpy.inf)

pyplot.loglog(hs, [mms_error(h) for h in hs], 'o', label='numeric error')
pyplot.loglog(hs, hs**1/100, label='$h^1/100$')
pyplot.loglog(hs, hs**2/100, label='$h^2/100$')
pyplot.legend();


Neumann boundary conditions

Recall that in 1D, we would reflect the solution into ghost points according to

$$ u_{-i} = u_i - (x_i - x_{-i}) g_1(x_0, y) $$

and similarly for the right boundary and in the $y$ direction. After this, we (optionally) scale the row in the matrix for symmetry and shift the known parts to the right hand side. Below, we implement the reflected symmetry, but not the inhomogeneous contribution or rescaling of the matrix row.


In [8]:
def laplacian2d_bc(h, f, g0, dirichlet=((),())):
    m = int(1/h + 1)  # Number of elements in terms of nominal grid spacing h
    h = 1 / (m-1)     # Actual grid spacing
    c = numpy.linspace(0, 1, m)
    y, x = numpy.meshgrid(c, c)
    u0 = g0(x, y).flatten()
    rhs = f(x, y).flatten()
    ai = []
    aj = []
    av = []
    def idx(i, j):
        i = (m-1) - abs(m-1 - abs(i))
        j = (m-1) - abs(m-1 - abs(j))
        return i*m + j
    mask = numpy.ones_like(x, dtype=int)
    mask[dirichlet[0],:] = 0
    mask[:,dirichlet[1]] = 0
    mask = mask.flatten()
    for i in range(m):
        for j in range(m):
            row = idx(i, j)
            stencili = numpy.array([idx(*pair) for pair in [(i-1, j), (i, j-1), (i, j), (i, j+1), (i+1, j)]])
            stencilw = 1/h**2 * numpy.array([-1, -1, 4, -1, -1])
            if mask[row] == 0: # Dirichlet boundary
                ai.append(row)
                aj.append(row)
                av.append(1)
                rhs[row] = u0[row]
            else:
                smask = mask[stencili]
                ai += [row]*sum(smask)
                aj += stencili[smask == 1].tolist()
                av += stencilw[smask == 1].tolist()
                bdycols = stencili[smask == 0]
                rhs[row] -= stencilw[smask == 0] @ u0[bdycols]
    A = sp.csr_matrix((av, (ai, aj)), shape=(m*m,m*m))
    return x, y, A, rhs

x, y, A, rhs = laplacian2d_bc(.05, lambda x,y: 0*x+1,
                              lambda x,y: 0*x, dirichlet=((0,),()))
u = sp.linalg.spsolve(A, rhs).reshape(x.shape)
print(sp.linalg.eigs(A, which='SM')[0])
pyplot.contourf(x, y, u)
pyplot.colorbar();


[ 1. +0.00000000e+00j  1. +1.29546009e-14j  1. -1.29546009e-14j
  1. +0.00000000e+00j  1. +0.00000000e+00j  1. +0.00000000e+00j]

In [9]:
# We used a different technique for assembling the sparse matrix.
# This is faster with scipy.sparse, but may be worse for other sparse matrix packages, such as PETSc.

prof = cProfile.Profile()
prof.enable()
x, y, A, rhs = laplacian2d_bc(.005, lambda x,y: 0*x+1, lambda x,y: 0*x)
u = sp.linalg.spsolve(A, rhs).reshape(x.shape)
prof.disable()
prof.print_stats(sort='tottime')


         1454850 function calls (1454848 primitive calls) in 1.504 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.660    0.660    1.233    1.233 <ipython-input-8-7866e0ac5ad1>:1(laplacian2d_bc)
        1    0.264    0.264    0.264    0.264 {built-in method scipy.sparse.linalg.dsolve._superlu.gssv}
   242406    0.168    0.000    0.231    0.000 <ipython-input-8-7866e0ac5ad1>:11(idx)
    80841    0.152    0.000    0.152    0.000 {built-in method numpy.core.multiarray.array}
    40401    0.097    0.000    0.097    0.000 {built-in method builtins.sum}
   969624    0.063    0.000    0.063    0.000 {built-in method builtins.abs}
    40401    0.056    0.000    0.236    0.000 <ipython-input-8-7866e0ac5ad1>:22(<listcomp>)
    80802    0.024    0.000    0.024    0.000 {method 'tolist' of 'numpy.ndarray' objects}
        1    0.009    0.009    0.009    0.009 {built-in method numpy.core.multiarray.lexsort}
        1    0.006    0.006    1.239    1.239 <ipython-input-9-b5abe0e23855>:6(<module>)
        1    0.002    0.002    0.012    0.012 coo.py:460(_sum_duplicates)
        1    0.001    0.001    0.001    0.001 {method 'reduceat' of 'numpy.ufunc' objects}
        1    0.001    0.001    0.001    0.001 {built-in method scipy.sparse._sparsetools.coo_tocsr}
        2    0.001    0.000    0.001    0.000 {method 'copy' of 'numpy.ndarray' objects}
        4    0.001    0.000    0.001    0.000 {method 'reduce' of 'numpy.ufunc' objects}
        3    0.000    0.000    0.000    0.000 {method 'flatten' of 'numpy.ndarray' objects}
        3    0.000    0.000    0.000    0.000 {built-in method builtins.compile}
        1    0.000    0.000    0.000    0.000 {method 'nonzero' of 'numpy.ndarray' objects}
        1    0.000    0.000    0.000    0.000 {built-in method scipy.sparse._sparsetools.csr_has_sorted_indices}
        1    0.000    0.000    0.000    0.000 <ipython-input-9-b5abe0e23855>:6(<lambda>)
        1    0.000    0.000    0.001    0.001 coo.py:212(_check)
        1    0.000    0.000    0.000    0.000 function_base.py:25(linspace)
        3    0.000    0.000    0.000    0.000 compressed.py:128(check_format)
        1    0.000    0.000    0.264    0.264 linsolve.py:62(spsolve)
        7    0.000    0.000    0.000    0.000 getlimits.py:507(__init__)
      3/1    0.000    0.000    0.040    0.040 compressed.py:25(__init__)
        7    0.000    0.000    0.000    0.000 sputils.py:119(get_index_dtype)
        1    0.000    0.000    0.027    0.027 coo.py:118(__init__)
        1    0.000    0.000    0.013    0.013 coo.py:301(tocsr)
        3    0.000    0.000    1.504    0.501 interactiveshell.py:2880(run_code)
        1    0.000    0.000    0.001    0.001 function_base.py:4554(meshgrid)
        2    0.000    0.000    0.000    0.000 stride_tricks.py:115(_broadcast_to)
        1    0.000    0.000    0.000    0.000 {built-in method numpy.core.multiarray.copyto}
        3    0.000    0.000    0.000    0.000 codeop.py:132(__call__)
        3    0.000    0.000    0.000    0.000 compressed.py:1065(prune)
        5    0.000    0.000    0.000    0.000 base.py:77(set_shape)
        1    0.000    0.000    0.000    0.000 {built-in method numpy.core.multiarray.concatenate}
        4    0.000    0.000    0.000    0.000 coo.py:186(getnnz)
        1    0.000    0.000    0.000    0.000 function_base.py:5100(append)
        3    0.000    0.000    0.000    0.000 sputils.py:200(isshape)
       17    0.000    0.000    0.000    0.000 base.py:193(nnz)
       23    0.000    0.000    0.000    0.000 numeric.py:463(asarray)
        1    0.000    0.000    0.000    0.000 stride_tricks.py:195(broadcast_arrays)
       45    0.000    0.000    0.000    0.000 {built-in method builtins.len}
        4    0.000    0.000    0.000    0.000 sputils.py:91(to_native)
        1    0.000    0.000    0.264    0.264 <ipython-input-9-b5abe0e23855>:7(<module>)
        4    0.000    0.000    0.000    0.000 base.py:70(__init__)
        3    0.000    0.000    0.000    0.000 {method 'reshape' of 'numpy.ndarray' objects}
        3    0.000    0.000    1.503    0.501 {built-in method builtins.exec}
       22    0.000    0.000    0.000    0.000 {built-in method builtins.isinstance}
        1    0.000    0.000    0.012    0.012 coo.py:449(sum_duplicates)
       13    0.000    0.000    0.000    0.000 compressed.py:100(getnnz)
        7    0.000    0.000    0.000    0.000 getlimits.py:532(max)
        3    0.000    0.000    0.000    0.000 {built-in method numpy.core.multiarray.empty_like}
        3    0.000    0.000    0.000    0.000 hooks.py:142(__call__)
        4    0.000    0.000    0.000    0.000 {built-in method builtins.max}
        1    0.000    0.000    0.000    0.000 numeric.py:197(ones_like)
        1    0.000    0.000    0.000    0.000 {built-in method numpy.core.multiarray.arange}
        3    0.000    0.000    0.000    0.000 ipstruct.py:125(__getattr__)
        3    0.000    0.000    0.000    0.000 {built-in method builtins.hasattr}
        4    0.000    0.000    0.000    0.000 base.py:1111(isspmatrix)
        4    0.000    0.000    0.000    0.000 data.py:22(__init__)
        1    0.000    0.000    0.000    0.000 stride_tricks.py:176(_broadcast_shape)
        3    0.000    0.000    0.000    0.000 {method 'ravel' of 'numpy.ndarray' objects}
        2    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:402(parent)
       16    0.000    0.000    0.000    0.000 base.py:100(get_shape)
        1    0.000    0.000    0.000    0.000 function_base.py:4671(<listcomp>)
        1    0.000    0.000    0.000    0.000 fromnumeric.py:55(_wrapfunc)
        4    0.000    0.000    0.000    0.000 {method 'newbyteorder' of 'numpy.dtype' objects}
        1    0.000    0.000    0.000    0.000 compressed.py:1025(__get_sorted)
        1    0.000    0.000    0.000    0.000 base.py:142(asfptype)
        1    0.000    0.000    0.013    0.013 base.py:249(asformat)
        1    0.000    0.000    0.000    0.000 <ipython-input-9-b5abe0e23855>:8(<module>)
        2    0.000    0.000    0.000    0.000 numeric.py:2135(isscalar)
        1    0.000    0.000    0.000    0.000 sputils.py:20(upcast)
        6    0.000    0.000    0.000    0.000 _util.py:128(_prune_array)
        6    0.000    0.000    0.000    0.000 numeric.py:534(asanyarray)
        3    0.000    0.000    0.000    0.000 {method 'astype' of 'numpy.ndarray' objects}
        2    0.000    0.000    0.000    0.000 {method 'min' of 'numpy.ndarray' objects}
        1    0.000    0.000    0.000    0.000 {built-in method numpy.core.multiarray.empty}
        2    0.000    0.000    0.000    0.000 compressed.py:117(_set_self)
        2    0.000    0.000    0.000    0.000 sputils.py:183(isscalarlike)
        5    0.000    0.000    0.000    0.000 data.py:25(_get_dtype)
        1    0.000    0.000    0.000    0.000 function_base.py:13(_index_deprecate)
        2    0.000    0.000    0.000    0.000 _methods.py:28(_amin)
        2    0.000    0.000    0.000    0.000 {method 'max' of 'numpy.ndarray' objects}
        1    0.000    0.000    0.000    0.000 compressed.py:1056(sort_indices)
        1    0.000    0.000    0.000    0.000 base.py:562(__getattr__)
        1    0.000    0.000    0.001    0.001 function_base.py:4684(<listcomp>)
        6    0.000    0.000    0.000    0.000 stride_tricks.py:120(<genexpr>)
        1    0.000    0.000    0.000    0.000 stride_tricks.py:247(<listcomp>)
        1    0.000    0.000    0.000    0.000 stride_tricks.py:257(<listcomp>)
        1    0.000    0.000    0.000    0.000 fromnumeric.py:1487(nonzero)
        2    0.000    0.000    0.000    0.000 _methods.py:25(_amax)
        3    0.000    0.000    0.000    0.000 interactiveshell.py:1069(user_global_ns)
        2    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:989(_handle_fromlist)
        2    0.000    0.000    0.000    0.000 sputils.py:188(isintlike)
        2    0.000    0.000    0.000    0.000 sputils.py:227(isdense)
        2    0.000    0.000    0.000    0.000 csc.py:220(isspmatrix_csc)
        2    0.000    0.000    0.000    0.000 stride_tricks.py:251(<genexpr>)
        2    0.000    0.000    0.000    0.000 function_base.py:213(iterable)
        1    0.000    0.000    0.000    0.000 fromnumeric.py:1380(ravel)
        1    0.000    0.000    0.000    0.000 {built-in method numpy.core.multiarray.result_type}
        2    0.000    0.000    0.000    0.000 {built-in method builtins.getattr}
        2    0.000    0.000    0.000    0.000 {method 'rpartition' of 'str' objects}
        1    0.000    0.000    0.000    0.000 csr.py:458(isspmatrix_csr)
        2    0.000    0.000    0.000    0.000 stride_tricks.py:25(_maybe_view_as_subclass)
        1    0.000    0.000    0.000    0.000 {built-in method numpy.core.multiarray.promote_types}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.all}
        2    0.000    0.000    0.000    0.000 {built-in method builtins.any}
        2    0.000    0.000    0.000    0.000 {built-in method builtins.iter}
        1    0.000    0.000    0.000    0.000 {method 'get' of 'dict' objects}
        4    0.000    0.000    0.000    0.000 {method 'pop' of 'dict' objects}
        9    0.000    0.000    0.000    0.000 csr.py:231(_swap)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        3    0.000    0.000    0.000    0.000 hooks.py:207(pre_run_code_hook)
        1    0.000    0.000    0.000    0.000 {built-in method _operator.index}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.hash}


Variable coefficients

In physical systems, it is common for equations to be given in divergence form (sometimes called conservative form), $$ -\nabla\cdot \Big( \kappa(x,y) \nabla u \Big) = f(x,y) . $$ This can be converted to non-divergence form, $$ - \kappa(x,y) \nabla\cdot \nabla u - \nabla \kappa(x,y) \cdot \nabla u = f(x,y) . $$

  • What assumptions did we just make on $\kappa(x,y)$?

In [10]:
def laplacian2d_nondiv(h, f, kappa, grad_kappa, g0, dirichlet=((),())):
    m = int(1/h + 1)  # Number of elements in terms of nominal grid spacing h
    h = 1 / (m-1)     # Actual grid spacing
    c = numpy.linspace(0, 1, m)
    y, x = numpy.meshgrid(c, c)
    u0 = g0(x, y).flatten()
    rhs = f(x, y).flatten()
    ai = []
    aj = []
    av = []
    def idx(i, j):
        i = (m-1) - abs(m-1 - abs(i))
        j = (m-1) - abs(m-1 - abs(j))
        return i*m + j
    mask = numpy.ones_like(x, dtype=int)
    mask[dirichlet[0],:] = 0
    mask[:,dirichlet[1]] = 0
    mask = mask.flatten()
    for i in range(m):
        for j in range(m):
            row = idx(i, j)
            stencili = numpy.array([idx(*pair) for pair in [(i-1, j), (i, j-1), (i, j), (i, j+1), (i+1, j)]])
            stencilw = kappa(i*h, j*h)/h**2 * numpy.array([-1, -1, 4, -1, -1])
            if grad_kappa is None:
                gk = 1/h * numpy.array([kappa((i+.5)*h,j*h) - kappa((i-.5)*h,j*h),
                                        kappa(i*h,(j+.5)*h) - kappa(i*h,(j-.5)*h)])
            else:
                gk = grad_kappa(i*h, j*h)
            stencilw -= gk[0] / (2*h) * numpy.array([-1, 0, 0, 0, 1])
            stencilw -= gk[1] / (2*h) * numpy.array([0, -1, 0, 1, 0]) 
            if mask[row] == 0: # Dirichlet boundary
                ai.append(row)
                aj.append(row)
                av.append(1)
                rhs[row] = u0[row]
            else:
                smask = mask[stencili]
                ai += [row]*sum(smask)
                aj += stencili[smask == 1].tolist()
                av += stencilw[smask == 1].tolist()
                bdycols = stencili[smask == 0]
                rhs[row] -= stencilw[smask == 0] @ u0[bdycols]
    A = sp.csr_matrix((av, (ai, aj)), shape=(m*m,m*m))
    return x, y, A, rhs

def kappa(x, y):
    #return 1 - 2*(x-.5)**2 - 2*(y-.5)**2
    return 1e-2 + 2*(x-.5)**2 + 2*(y-.5)**2
def grad_kappa(x, y):
    #return -4*(x-.5), -4*(y-.5)
    return 4*(x-.5), 4*(y-.5)

pyplot.contourf(x, y, kappa(x,y))
pyplot.colorbar();



In [11]:
x, y, A, rhs = laplacian2d_nondiv(.05, lambda x,y: 0*x+1,
                                  kappa, grad_kappa,
                                  lambda x,y: 0*x, dirichlet=((0,-1),()))
u = sp.linalg.spsolve(A, rhs).reshape(x.shape)
pyplot.contourf(x, y, u)
pyplot.colorbar();



In [12]:
x, y, A, rhs = laplacian2d_nondiv(.05, lambda x,y: 0*x,
                                  kappa, grad_kappa,
                                  lambda x,y: x, dirichlet=((0,-1),()))
u = sp.linalg.spsolve(A, rhs).reshape(x.shape)
pyplot.contourf(x, y, u)
pyplot.colorbar();



In [13]:
def laplacian2d_div(h, f, kappa, g0, dirichlet=((),())):
    m = int(1/h + 1)  # Number of elements in terms of nominal grid spacing h
    h = 1 / (m-1)     # Actual grid spacing
    c = numpy.linspace(0, 1, m)
    y, x = numpy.meshgrid(c, c)
    u0 = g0(x, y).flatten()
    rhs = f(x, y).flatten()
    ai = []
    aj = []
    av = []
    def idx(i, j):
        i = (m-1) - abs(m-1 - abs(i))
        j = (m-1) - abs(m-1 - abs(j))
        return i*m + j
    mask = numpy.ones_like(x, dtype=int)
    mask[dirichlet[0],:] = 0
    mask[:,dirichlet[1]] = 0
    mask = mask.flatten()
    for i in range(m):
        for j in range(m):
            row = idx(i, j)
            stencili = numpy.array([idx(*pair) for pair in [(i-1, j), (i, j-1), (i, j), (i, j+1), (i+1, j)]])
            stencilw = 1/h**2 * (  kappa((i-.5)*h, j*h) * numpy.array([-1, 0, 1, 0, 0])
                                 + kappa(i*h, (j-.5)*h) * numpy.array([0, -1, 1, 0, 0])
                                 + kappa(i*h, (j+.5)*h) * numpy.array([0, 0, 1, -1, 0])
                                 + kappa((i+.5)*h, j*h) * numpy.array([0, 0, 1, 0, -1]))
            if mask[row] == 0: # Dirichlet boundary
                ai.append(row)
                aj.append(row)
                av.append(1)
                rhs[row] = u0[row]
            else:
                smask = mask[stencili]
                ai += [row]*sum(smask)
                aj += stencili[smask == 1].tolist()
                av += stencilw[smask == 1].tolist()
                bdycols = stencili[smask == 0]
                rhs[row] -= stencilw[smask == 0] @ u0[bdycols]
    A = sp.csr_matrix((av, (ai, aj)), shape=(m*m,m*m))
    return x, y, A, rhs

x, y, A, rhs = laplacian2d_div(.05, lambda x,y: 0*x+1,
                                  kappa,
                                  lambda x,y: 0*x, dirichlet=((0,-1),()))
u = sp.linalg.spsolve(A, rhs).reshape(x.shape)
pyplot.contourf(x, y, u)
pyplot.colorbar();



In [14]:
x, y, A, rhs = laplacian2d_div(.05, lambda x,y: 0*x,
                                  kappa,
                                  lambda x,y: x, dirichlet=((0,-1),()))
u = sp.linalg.spsolve(A, rhs).reshape(x.shape)
pyplot.contourf(x, y, u)
pyplot.colorbar();



In [15]:
x, y, A, rhs = laplacian2d_nondiv(.05, lambda x,y: 0*x+1,
                                  kappa, grad_kappa,
                                  lambda x,y: 0*x, dirichlet=((0,-1),()))
u_nondiv = sp.linalg.spsolve(A, rhs).reshape(x.shape)

x, y, A, rhs = laplacian2d_div(.05, lambda x,y: 0*x+1,
                                  kappa,
                                  lambda x,y: 0*x, dirichlet=((0,-1),()))
u_div = sp.linalg.spsolve(A, rhs).reshape(x.shape)
pyplot.contourf(x, y, u_nondiv - u_div)
pyplot.colorbar();



In [16]:
class mms1:
    def __init__(self):
        import sympy
        x, y = sympy.symbols('x y')
        uexpr = x*sympy.exp(-2*x) * sympy.tanh(1.2*y+.1)
        kexpr = 1e-2 + 2*(x-.42)**2 + 2*(y-.51)**2
        self.u = sympy.lambdify((x,y), uexpr)
        self.kappa = sympy.lambdify((x,y), kexpr)
        def grad_kappa(xx, yy):
            kx = sympy.lambdify((x,y), sympy.diff(kexpr, x))
            ky = sympy.lambdify((x,y), sympy.diff(kexpr, y))
            return kx(xx, yy), ky(xx, yy)
        self.grad_kappa = grad_kappa
        self.div_kappa_grad_u = sympy.lambdify((x,y),
                                              -(  sympy.diff(kexpr * sympy.diff(uexpr, x), x)
                                                + sympy.diff(kexpr * sympy.diff(uexpr, y), y)))
        
mms = mms1()
x, y, A, rhs = laplacian2d_nondiv(.05, mms.div_kappa_grad_u,
                                  mms.kappa, mms.grad_kappa,
                                  mms.u, dirichlet=((0,-1),(0,-1)))
u_nondiv = sp.linalg.spsolve(A, rhs).reshape(x.shape)
pyplot.contourf(x, y, u_nondiv)
pyplot.colorbar()
numpy.linalg.norm((u_nondiv - mms.u(x, y)).flatten(), numpy.inf)


Out[16]:
0.0002746255119079194

In [17]:
x, y, A, rhs = laplacian2d_div(.05, mms.div_kappa_grad_u,
                                  mms.kappa,
                                  mms.u, dirichlet=((0,-1),(0,-1)))
u_div = sp.linalg.spsolve(A, rhs).reshape(x.shape)
pyplot.contourf(x, y, u_div)
pyplot.colorbar()
numpy.linalg.norm((u_div - mms.u(x, y)).flatten(), numpy.inf)


Out[17]:
0.00071980296434716318

In [18]:
def mms_error(h):
    x, y, A, rhs = laplacian2d_nondiv(h, mms.div_kappa_grad_u,
                                  mms.kappa, mms.grad_kappa,
                                  mms.u, dirichlet=((0,-1),(0,-1)))
    u_nondiv = sp.linalg.spsolve(A, rhs).flatten()
    x, y, A, rhs = laplacian2d_div(h, mms.div_kappa_grad_u,
                                  mms.kappa, mms.u, dirichlet=((0,-1),(0,-1)))
    u_div = sp.linalg.spsolve(A, rhs).flatten()
    u_exact = mms.u(x, y).flatten()
    return numpy.linalg.norm(u_nondiv - u_exact, numpy.inf), numpy.linalg.norm(u_div - u_exact, numpy.inf)

hs = numpy.logspace(-1.5, -.5, 10)
errors = numpy.array([mms_error(h) for h in hs])
pyplot.loglog(hs, errors[:,0], 'o', label='nondiv')
pyplot.loglog(hs, errors[:,1], 's', label='div')
pyplot.plot(hs, hs**2, label='$h^2$')
pyplot.legend();



In [19]:
#kappablob = lambda x,y: .01 + ((x-.5)**2 + (y-.5)**2 < .125)
def kappablob(x, y):
    #return .01 + ((x-.5)**2 + (y-.5)**2 < .125)
    return .01 + (numpy.abs(x-.505) < .25) # + (numpy.abs(y-.5) < .25)
x, y, A, rhs = laplacian2d_div(.02, lambda x,y: 0*x, kappablob,
                                  lambda x,y:x, dirichlet=((0,-1),()))
u_div = sp.linalg.spsolve(A, rhs).reshape(x.shape)
pyplot.contourf(x, y, kappablob(x, y))
pyplot.colorbar();
pyplot.figure()
pyplot.contourf(x, y, u_div, 10)
pyplot.colorbar();



In [20]:
x, y, A, rhs = laplacian2d_nondiv(.01, lambda x,y: 0*x, kappablob, None,
                                  lambda x,y:x, dirichlet=((0,-1),()))
u_nondiv = sp.linalg.spsolve(A, rhs).reshape(x.shape)
pyplot.contourf(x, y, u_nondiv, 10)
pyplot.colorbar();


Weak forms

When we write

$$ {\huge "} - \nabla\cdot \big( \kappa \nabla u \big) = 0 {\huge "} \text{ on } \Omega $$

where $\kappa$ is a discontinuous function, that's not exactly what we mean the derivative of that discontinuous function doesn't exist. Formally, however, let us multiply by a "test function" $v$ and integrate,

\begin{split} - \int_\Omega v \nabla\cdot \big( \kappa \nabla u \big) = 0 & \text{ for all } v \\ \int_\Omega \nabla v \cdot \kappa \nabla u = \int_{\partial \Omega} v \kappa \nabla u \cdot \hat n & \text{ for all } v \end{split}

where we have used integration by parts. This is called the weak form of the PDE and will be what we actually discretize using finite element methods. All the terms make sense when $\kappa$ is discontinuous. Now suppose our domain is decomposed into two disjoint sub domains $$\overline{\Omega_1 \cup \Omega_2} = \overline\Omega $$ with interface $$\Gamma = \overline\Omega_1 \cap \overline\Omega_2$$ and $\kappa_1$ is continuous on $\overline\Omega_1$ and $\kappa_2$ is continuous on $\overline\Omega_2$, but possibly $\kappa_1(x) \ne \kappa_2(x)$ for $x \in \Gamma$,

\begin{split} \int_\Omega \nabla v \cdot \kappa \nabla u &= \int_{\Omega_1} \nabla v \cdot \kappa_1\nabla u + \int_{\Omega_2} \nabla v \cdot \kappa_2 \nabla u \\ &= -\int_{\Omega_1} v \nabla\cdot \big(\kappa_1 \nabla u \big) + \int_{\partial \Omega_1} v \kappa_1 \nabla u \cdot \hat n \\ &\qquad -\int_{\Omega_2} v \nabla\cdot \big(\kappa_2 \nabla u \big) + \int_{\partial \Omega_2} v \kappa_2 \nabla u \cdot \hat n \\ &= -\int_{\Omega} v \nabla\cdot \big(\kappa \nabla u \big) + \int_{\partial \Omega} v \kappa \nabla u \cdot \hat n + \int_{\Gamma} v (\kappa_1 - \kappa_2) \nabla u\cdot \hat n . \end{split}
  • Which direction is $\hat n$ for the integral over $\Gamma$?
  • Does it matter what we choose for the value of $\kappa$ on $\Gamma$ in the volume integral?

When $\kappa$ is continuous, the jump term vanishes and we recover the strong form $$ - \nabla\cdot \big( \kappa \nabla u \big) = 0 \text{ on } \Omega . $$ But if $\kappa$ is discontinuous, we would need to augment this with a jump condition ensuring that the flux $-\kappa \nabla u$ is continuous. We could go add this condition to our FD code to recover convergence in case of discontinuous $\kappa$, but it is messy.

Nonlinear problems

Let's consider the nonlinear problem $$ -\nabla \cdot \big(\underbrace{(1 + u^2)}_{\kappa(u)} \nabla u \big) = f \text{ on } (0,1)^2 $$ subject to Dirichlet boundary conditions. We will discretize the divergence form and thus will need $\kappa(u)$ evaluated at staggered points $(i-1/2,j)$, $(i,j-1/2)$, etc. We will calculate these by averaging $$ u_{i-1/2,j} = \frac{u_{i-1,j} + u_{i,j}}{2} $$ and similarly for the other staggered directions. To use a Newton method, we also need the derivatives $$ \frac{\partial \kappa_{i-1/2,j}}{\partial u_{i,j}} = 2 u_{i-1/2,j} \frac{\partial u_{i-1/2,j}}{\partial u_{i,j}} = u_{i-1/2,j} . $$

In the function below, we compute both the residual $$F(u) = -\nabla\cdot \kappa(u) \nabla u - f(x,y)$$ and its Jacobian $$J(u) = \frac{\partial F}{\partial u} . $$


In [21]:
def hgrid(h):
    m = int(1/h + 1)  # Number of elements in terms of nominal grid spacing h
    h = 1 / (m-1)     # Actual grid spacing
    c = numpy.linspace(0, 1, m)
    y, x = numpy.meshgrid(c, c)
    return x, y

def nonlinear2d_div(h, x, y, u, forcing, g0, dirichlet=((),())):
    m = x.shape[0]
    u0 = g0(x, y).flatten()
    F = -forcing(x, y).flatten()
    ai = []
    aj = []
    av = []
    def idx(i, j):
        i = (m-1) - abs(m-1 - abs(i))
        j = (m-1) - abs(m-1 - abs(j))
        return i*m + j
    mask = numpy.ones_like(x, dtype=bool)
    mask[dirichlet[0],:] = False
    mask[:,dirichlet[1]] = False
    mask = mask.flatten()
    u = u.flatten()
    F[mask == False] = u[mask == False] - u0[mask == False]
    u[mask == False] = u0[mask == False]
    for i in range(m):
        for j in range(m):
            row = idx(i, j)
            stencili = numpy.array([idx(*pair) for pair in [(i-1, j), (i, j-1), (i, j), (i, j+1), (i+1, j)]])
            # Stencil to evaluate gradient at four staggered points
            grad = numpy.array([[-1, 0, 1, 0, 0],
                                [0, -1, 1, 0, 0],
                                [0, 0, -1, 1, 0],
                                [0, 0, -1, 0, 1]]) / h
            # Stencil to average at four staggered points
            avg  = numpy.array([[1, 0, 1, 0, 0],
                                [0, 1, 1, 0, 0],
                                [0, 0, 1, 1, 0],
                                [0, 0, 1, 0, 1]]) / 2
            # Stencil to compute divergence at cell centers from fluxes at four staggered points
            div = numpy.array([-1, -1, 1, 1]) / h
            ustencil = u[stencili]
            ustag = avg @ ustencil
            kappa = 1 + ustag**2
            if mask[row] == 0: # Dirichlet boundary
                ai.append(row)
                aj.append(row)
                av.append(1)
            else:
                F[row] -= div @ (kappa[:,None] * grad @ ustencil)
                Jstencil = -div @ (kappa[:,None] * grad
                    + 2*(ustag*(grad @ ustencil))[:,None] * avg)
                smask = mask[stencili]
                ai += [row]*sum(smask)
                aj += stencili[smask].tolist()
                av += Jstencil[smask].tolist()
    J = sp.csr_matrix((av, (ai, aj)), shape=(m*m,m*m))
    return F, J

h = .1
x, y = hgrid(h)
u = 0*x
F, J = nonlinear2d_div(h, x, y, u, lambda x,y: 0*x+1,
                       lambda x,y: 0*x, dirichlet=((0,-1),(0,-1)))
deltau = sp.linalg.spsolve(J, -F).reshape(x.shape)
pyplot.contourf(x, y, deltau)
pyplot.colorbar();



In [22]:
def solve_nonlinear(h, g0, dirichlet, atol=1e-8, verbose=False):
    x, y = hgrid(h)
    u = 0*x
    for i in range(50):
        F, J = nonlinear2d_div(h, x, y, u, lambda x,y: 0*x+1,
                               lambda x,y: 0*x, dirichlet=((0,-1),(0,-1)))
        anorm = numpy.linalg.norm(F, numpy.inf)
        if verbose:
            print('{:2d}: anorm {:8e}'.format(i,anorm))
        if anorm < atol:
            break
        deltau = sp.linalg.spsolve(J, -F)
        u += deltau.reshape(x.shape)
    return x, y, u, i

x, y, u, i = solve_nonlinear(.1, lambda x,y: 0*x, dirichlet=((0,-1),(0,-1)), verbose=True)
pyplot.contourf(x, y, u)
pyplot.colorbar();


 0: anorm 1.000000e+00
 1: anorm 5.162198e-03
 2: anorm 1.035113e-07
 3: anorm 3.552714e-15

Homework 3: Due 2017-11-03

Write a solver for the regularized $p$-Laplacian, $$ -\nabla\cdot\big( \kappa(\nabla u) \nabla u \big) = 0 $$ where $$ \kappa(\nabla u) = \big(\frac 1 2 \epsilon^2 + \frac 1 2 \nabla u \cdot \nabla u \big)^{\frac{p-2}{2}}, $$ $ \epsilon > 0$, and $1 < p < \infty$. The case $p=2$ is the conventional Laplacian. This problem gets more strongly nonlinear when $p$ is far from 2 and when $\epsilon$ approaches zero. The $p \to 1$ limit is related to plasticity and has applications in non-Newtonion flows and structural mechanics.

  1. Implement a "Picard" solver, which is like a Newton solver except that the Jacobian is replaced by the linear system $$ J_{\text{Picard}}(u) \delta u \sim -\nabla\cdot\big( \kappa(\nabla u) \nabla \delta u \big) . $$ This is much easier to implement than the full Newton linearization. How fast does this method converge for values of $p < 2$ and $p > 2$?
  • Use the linearization above as a preconditioner to a Newton-Krylov method. That is, use scipy.sparse.linalg.LinearOperator to apply the Jacobian to a vector $$ \tilde J(u) v = \frac{F(u + h v) - F(u)}{h} . $$ Then for each linear solve, use scipy.sparse.linalg.gmres and pass as a preconditioner, a direct solve with the Picard linearization above. (You might find scipy.sparse.linalg.factorized to be useful. Compare algebraic convergence to that of the Picard method.

  • Can you directly implement a Newton linearization? Either do it or explain what is involved. How will its nonlinear convergence compare to that of the Newton-Krylov method?

Wave equations and multi-component systems

The acoustic wave equation with constant wave speed $c$ can be written $$ \ddot u - c^2 \nabla\cdot \nabla u = 0 $$ where $u$ is typically a pressure. We can convert to a first order system $$ \begin{bmatrix} \dot u \\ \dot v \end{bmatrix} = \begin{bmatrix} 0 & I \\ c^2 \nabla\cdot \nabla & 0 \end{bmatrix} \begin{bmatrix} u \\ v \end{bmatrix} . $$ We will choose a zero-penetration boundary condition $\nabla u \cdot \hat n = 0$, which will cause waves to reflect.


In [62]:
%run fdtools.py

x, y, L, _ = laplacian2d_bc(.1, lambda x,y: 0*x,
                              lambda x,y: 0*x, dirichlet=((),()))
A = sp.bmat([[None, sp.eye(*L.shape)],
             [-L, None]])
eigs = sp.linalg.eigs(A, 10, which='LM')[0]
print(eigs)
maxeig = max(eigs.imag)
u0 = numpy.concatenate([numpy.exp(-8*(x**2 + y**2)), 0*x], axis=None)
hist = ode_rkexplicit(lambda t, u: A @ u, u0, tfinal=2, h=2/maxeig)

def plot_wave(x, y, time, U):
    u = U[:x.size].reshape(x.shape)
    pyplot.contourf(x, y, u)
    pyplot.colorbar()
    pyplot.title('Wave solution t={:f}'.format(time));

for step in numpy.linspace(0, len(hist)-1, 6, dtype=int):
    pyplot.figure()
    plot_wave(x, y, *hist[step])


[ -1.11393533e-13+28.28427125j  -1.11393533e-13-28.28427125j
   1.90602742e-13+27.93604493j   1.90602742e-13-27.93604493j
   4.85222973e-13+28.11069731j   4.85222973e-13-28.11069731j
   2.72039336e-14+28.11069731j   2.72039336e-14-28.11069731j
  -9.42371181e-14+27.6007862j   -9.42371181e-14-27.6007862j ]
<matplotlib.figure.Figure at 0x7f4354a8b400>
  • This was a second order discretization, but we could extend it to higher order.
  • The largest eigenvalues of this operator are proportional to $c/h$.
  • Formally, we can write this equation in conservative form $$ \begin{bmatrix} \dot u \\ \dot{\mathbf v} \end{bmatrix} = \begin{bmatrix} 0 & c\nabla\cdot \\ c \nabla & 0 \end{bmatrix} \begin{bmatrix} u \\ \mathbf v \end{bmatrix} $$ where $\mathbf{v}$ is now a momentum vector and $\nabla u = \nabla\cdot (u I)$. This formulation could produce an anti-symmetric ($A^T = -A$) discretization. Discretizations with this property are sometimes called "mimetic".
  • A conservative form is often preferred when studiying waves traveling through materials with different wave speeds $c$.
  • This is a Hamiltonian system. While high order Runge-Kutta methods can be quite accurate, "symplectic" time integrators are needed to preserve the structure of the Hamiltonian (related to energy conservation) over long periods of time. The midpoint method (aka $\theta=1/2$) is one such method. There are also explicit symplectic methods such as Verlet methods, though these can be fragile.