This page was generated from unit-1.7-helmholtz/pml.ipynb.

# 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]:
```

```
from ngsolve import *
from ngsolve.webgui import Draw
from netgen.geom2d import SplineGeometry
import matplotlib.pyplot as plt
```

## Source problem¶

Consider the problem of finding \(u\) satisfying

together with the Sommerfeld (outgoing) radiation condition at infinity

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.4, leftdomain=2, bc="outerbnd")
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')
```

```
[2]:
```

```
BaseWebGuiScene
```

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 += grad(u)*grad(v)*dx - omega**2*u*v*dx
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)
```

```
[4]:
```

```
BaseWebGuiScene
```

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

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()
pnums = [geo.AddPoint(x, y, maxh=maxh)
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)
```

```
[5]:
```

```
BaseWebGuiScene
```

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)
a += grad(u)*grad(v)*dx
m = BilinearForm (fes, symmetric=True)
m += u*v*dx
a.Assemble()
m.Assemble()
```

```
[7]:
```

```
<ngsolve.comp.BilinearForm at 0x7fc72c31de70>
```

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')
with TaskManager():
lam = ArnoldiSolver(a.mat, m.mat, fes.FreeDofs(),
u.vecs, shift=400)
Draw(u)
```

```
Solve Hessenberg evp with Lapack ... done
```

```
[8]:
```

```
BaseWebGuiScene
```

```
[9]:
```

```
print ("lam: ", lam)
```

```
lam: (362.2,-28.5233)
(454.003,-51.6587)
(297.669,-12.4922)
(387.727,-124.281)
(259.41,-3.09254)
(357.799,-124.198)
(568.408,-2.46777)
(472.315,-134.067)
(255.359,-73.5477)
(608.194,-9.95495)
(574.913,-82.9194)
(543.33,-131.804)
(426.635,-182.194)
(269.062,-148.467)
(554.341,-163.392)
(674.95,-22.8256)
(167.966,-40.8405)
(314.928,-192.513)
(376.305,-191.856)
(624.323,-135.938)
(367.324,-224.169)
(488.978,-222.948)
(422.356,-240.158)
(353.629,-239.109)
(406.889,-248.251)
(402.379,-252.912)
(382.303,-250.923)
(429.603,-258.05)
(480.355,-246.903)
(363.213,-249.483)
(455.026,-257.964)
(465.563,-255.913)
(338.254,-243.451)
(302.019,-219.088)
(493.603,-255.529)
(318.495,-241.784)
(514.77,-253.844)
(305.975,-229.926)
(532.365,-253.72)
(259.938,-216.86)
(290.583,-231.559)
(550.957,-248.696)
(558.772,-246.074)
(685.714,-137.802)
(218.353,-193.785)
(269.35,-227.47)
(625.309,-211.597)
(181.771,-155.028)
(769.278,-41.1672)
(245.353,-217.559)
```

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()
```