# 1.7.1 Perfectly Matched Layer (PML)¶

The use of (Perfectly Matched Layer) PML is a standard technique to numerically solve for outgoing waves in unbounded domains. Although scattering problems are posed in unbounded domains, by bounding the scatterer and any inhomogeneities within a PML, one is able to truncate to a bounded computational domain. In this tutorial we see how to use PML for - source problems, and - eigenvalue problems (resonances).

[1]:

import netgen.gui
from netgen.geom2d import SplineGeometry
from ngsolve import *
import matplotlib.pyplot as plt


## Source problem¶

Consider the problem of finding $$u$$ satisfying

$-\Delta u - \omega^2 u = f \qquad \text{ in } \mathbb{R}^2$

together with the Sommerfeld (outgoing) radiation condition at infinity

$\lim_{r \to \infty} r^{1/2} \bigg( \frac{\partial u }{ \partial r} - i \omega u \bigg) = 0$

where $$r$$ is the radial coordinate.

We consider an example where $$f$$ represents a time-harmonic pulse that is essentially zero except for a small region.

[2]:

geo = SplineGeometry()
geo.AddCircle( (0,0), 1, leftdomain=1, rightdomain=2, bc="innerbnd")
geo.SetMaterial(1, "inner")
geo.SetMaterial(2, "pmlregion")
mesh = Mesh(geo.GenerateMesh (maxh=0.1))
mesh.Curve(3)

f = exp(-20**2*((x-0.3)*(x-0.3)+y*y))
Draw(f, mesh, 'f')


Note how geometry is divided into an inner disk and and an outer annulus called pmlregion.

The PML facility in NGSolve implements a complex coordinate transformation on a given mesh region (which in this example is pmlregion). When this complex variable change is applied to the outgoing solution in the PML region, it becomes a a function that decays exponentially as $$r \to \infty$$, allowing us to truncate the unbounded domain.

With the following single line, we tell NGSolve to turn on this coordinate transformation.

[3]:

mesh.SetPML(pml.Radial(rad=1,alpha=1j,origin=(0,0)), "pmlregion")


Then a radial PML is set in the exterior of a disk centered at origin of radius rad. In addition to origin and rad, the keyword argument alpha may be used to set the PML strength, which determines the rate of increase in the imaginary part of the coordinate map as radius increases.

Having set the PML, the rest of the code now looks very much like that in 1.7:

[4]:

fes = H1(mesh, order=4, complex=True)
u = fes.TrialFunction()
v = fes.TestFunction()

omega = 10

a = BilinearForm(fes)
a += -1j*omega*u*v*ds("outerbnd")

b = LinearForm(fes)
b += f * v * dx

a.Assemble()
b.Assemble()

gfu = GridFunction(fes)
gfu.vec.data = a.mat.Inverse() * b.vec
Draw(gfu)


Note that above we have kept the same Robin boundary conditions as in 1.7. (Since the solution exponentially decays within PML, it is also common practice to put zero Dirichlet boundary conditions at the outer edge of the PML, an option that is also easily implementable in NGSolve using the dirichlet flag.)

## Eigenvalue problem¶

PML can also be used to compute resonances on unbounded domains. Resonances of a scatterer $$S$$ are outgoing waves $$u$$ satisfying the homogeneous Helmholtz equation

$\Delta u + \omega^2 u = 0 \qquad \text{ in } \mathbb{R}^2 \setminus S,$

i.e., $$u$$ is an eigenfunction of $$-\Delta$$ associated to eigenvalue $$\lambda :=\omega^2.$$

We truncate this unbounded-domain eigenproblem to a bounded-domain eigenproblem using PML. The following example computes the resonances of a cavity opening on one side into free space.

[5]:

geo   = SplineGeometry()
for x,y,maxh in
[(-2, 0, 1), (-1, 0, 1), (-0.2, 0, 0.01), (-0.2, -0.8, 1), (0.2, -0.8, 1), (0.2, 0, 0.01), (1,0,1), (2,0,1), (2,2,1), (0,2,1), (-2,2,1), (1,1,1), (0,1,1), (-1,1,1) ]
]
lines = [(0,1,"dir",2,0), (1,2,"dir", 1,0), (2,3,"dir",1,0), (3,4,"dir",1,0), (4,5,"dir",1,0), (5,6,"dir",1,0), (6,7,"dir",2,0)]
curves= [(7,8,9,"outer",2,0), (9,10,0,"outer",2,0), (6,11,12,"inner",1,2), (12,13,1,"inner",1,2) ]

for p1,p2,bc,left,right in lines:
geo.Append(["line", pnums[p1], pnums[p2]], bc=bc, leftdomain=left, rightdomain=right)
for p1,p2,p3,bc,left,right in curves:
geo.Append(["spline3", pnums[p1], pnums[p2], pnums[p3]], bc=bc, leftdomain=left, rightdomain=right)

geo.SetMaterial(1, "air")
geo.SetMaterial(2, "pml")

mesh = Mesh(geo.GenerateMesh(maxh=0.1))
mesh.Curve(5)
Draw(mesh)


The top edge of the rectangular cavity opens into air, which is included in the computational domain as a semicircular region (neglecting backward propagating waves), while the remaining edges of the cavity are isolated from rest of space by perfect reflecting boundary conditions.

You can identify the PML region in the Netgen GUI by double clicking on the outer part of the air.

[6]:

mesh.SetPML(pml.Radial(rad=1, alpha=1j, origin=(0,0)), "pml")


We now make the complex symmetric system with PML incorporated.

[7]:

fes = H1(mesh, order=4, complex=True, dirichlet="dir")

u = fes.TrialFunction()
v = fes.TestFunction()

a = BilinearForm (fes, symmetric=True)

m = BilinearForm (fes, symmetric=True)
m += u*v*dx

a.Assemble()
m.Assemble()

[7]:

<ngsolve.comp.BilinearForm at 0x7f2670248630>


For solving the eigenproblem, we use an Arnoldi eigensolver, to which a GridFunction with many vectors (allocated via keyword argument multidim) is input, together with a and b. A shift argument to ArnoldiSolver indicates the approximate location of expected eigenvalues.

[8]:

u = GridFunction(fes, multidim=50, name='resonances')

lam = ArnoldiSolver(a.mat, m.mat, fes.FreeDofs(),
u.vecs, shift=400)
Draw(u)

[9]:

print ("lam: ", lam)

lam:   (362.229,-28.5594)
(454.056,-51.7308)
(297.682,-12.5067)
(387.323,-124.402)
(259.413,-3.09593)
(568.416,-2.47185)
(357.997,-124.681)
(472.032,-134.168)
(255.39,-73.6283)
(608.207,-9.9709)
(574.96,-83.0178)
(543.151,-131.762)
(426.641,-182.467)
(674.982,-22.8506)
(554.298,-163.818)
(269.056,-148.676)
(167.983,-40.8771)
(624.306,-135.517)
(314.829,-192.589)
(488.68,-222.298)
(769.304,-41.2561)
(367.218,-223.101)
(685.788,-137.367)
(421.975,-237.621)
(479.714,-244.768)
(459.25,-253.829)
(405.371,-245.99)
(556.339,-241.582)
(511.425,-261.397)
(353.815,-237.175)
(480.981,-267.678)
(443.576,-264.648)
(541.215,-252.329)
(424.563,-263.388)
(712.111,-134.883)
(532.042,-263.45)
(625.211,-210.828)
(381.978,-254.851)
(398.578,-259.43)
(336.775,-246.429)
(741.138,-118.086)
(359.235,-258.415)
(581.868,-258.149)
(608.19,-247.629)
(305.34,-229.495)
(260.33,-215.283)
(323.3,-249.381)
(634.998,-239.309)
(298.673,-238.708)
(107.766,-18.0413)



The more “confined” modes have resonance values $$\omega$$ that are closer to the real axis. Here are the computed $$\omega$$-values plotted in the complex plane.

[10]:

lamr = [sqrt(l).real for l in lam]
lami = [sqrt(l).imag for l in lam]
plt.plot(lamr, lami, ".")
plt.grid(True)
plt.title('Computed $\omega$ values')
plt.show()