ex13.py: Nonlinear eigenproblem with contour integral#
This example solves a nonlinear eigenvalue problem with the NEP module configured to apply a contour integral
method.
The problem arises from the PDE
where
and the boundary conditions are
For the discretization, \(n\) grid points are used, \(x_1=0,\dots,x_n=1\), with step size \(h = 1/(n-1)\). Hence,
The boundary condition at \(x=0\) is \(u_1=0\), and at \(x=1\):
and therefore
The Laplace term for \(u_n\) is
The above discretization allows us to write the nonlinear PDE in the following split-operator form
so \(f_1 = 1\), \(f_2 = n_c^2 \lambda^2\), \(f_3 = g(\lambda) \lambda^2D_0\), \(f_4 = 2i\lambda/h\), with coefficient matrices
Contributed by: Thomas Hisch.
The full source code for this demo can be downloaded here.
Initialization by importing slepc4py, petsc4py, numpy and scipy.
import sys
import slepc4py
slepc4py.init(sys.argv) # isort:skip
import numpy as np
try:
import scipy
import scipy.optimize
except ImportError:
scipy = None
from petsc4py import PETSc
from slepc4py import SLEPc
Print = PETSc.Sys.Print
Check that the selected PETSc/SLEPc are built with complex scalars.
if not np.issubdtype(PETSc.ScalarType, np.complexfloating):
Print("Demo should only be executed with complex PETSc scalars")
exit(0)
Long function that defines the nonlinear eigenproblem and solves it.
def solve(n):
L = 1.0
h = L / (n - 1)
nc = 1.2
ka = 10.0
gt = 4.0
D0 = 0.5
A = PETSc.Mat().create()
A.setSizes([n, n])
A.setFromOptions()
A.setOption(PETSc.Mat.Option.HERMITIAN, False)
rstart, rend = A.getOwnershipRange()
d0, d1, d2 = (
1 / h**2,
-2 / h**2,
1 / h**2,
)
Print(f"dterms={(d0, d1, d2)}")
if rstart == 0:
# dirichlet boundary condition at the left lead
A[0, 0] = 1.0
A[0, 1] = 0.0
A[1, 0] = 0.0
A[1, 1] = d1
A[1, 2] = d2
rstart += 2
if rend == n:
# at x=1.0 neumann boundary condition (not handled here but in a
# different matrix (D))
A[n - 1, n - 2] = 2.0 / h**2
A[n - 1, n - 1] = (-2) / h**2 # + 2j*k*h / h**2 (neumann)
rend -= 1
for i in range(rstart, rend):
A[i, i - 1 : i + 2] = [d0, d1, d2]
A.assemble()
Id = PETSc.Mat().create()
Id.setSizes([n, n])
Id.setFromOptions()
Id.setOption(PETSc.Mat.Option.HERMITIAN, True)
rstart, rend = Id.getOwnershipRange()
if rstart == 0:
# due to dirichlet BC
rstart += 1
for i in range(rstart, rend):
Id[i, i] = 1.0
Id.assemble()
D = PETSc.Mat().create()
D.setSizes([n, n])
D.setFromOptions()
D.setOption(PETSc.Mat.Option.HERMITIAN, True)
_, rend = D.getOwnershipRange()
if rend == n:
D[n - 1, n - 1] = 1
D.assemble()
Print(f"DOF: {A.getInfo()['nz_used']}, MEM: {A.getInfo()['memory']}")
f1 = SLEPc.FN().create()
f1.setType(SLEPc.FN.Type.RATIONAL)
f1.setRationalNumerator([1.0])
f2 = SLEPc.FN().create()
f2.setType(SLEPc.FN.Type.RATIONAL)
f2.setRationalNumerator([nc**2, 0.0, 0.0])
f3 = SLEPc.FN().create()
f3.setType(SLEPc.FN.Type.RATIONAL)
f3.setRationalNumerator([D0 * gt, 0.0, 0.0])
f3.setRationalDenominator([1.0, -ka + 1j * gt])
f4 = SLEPc.FN().create()
f4.setType(SLEPc.FN.Type.RATIONAL)
f4.setRationalNumerator([2j / h, 0])
# Setup the solver
nep = SLEPc.NEP().create()
nep.setSplitOperator(
[A, Id, Id, D],
[f1, f2, f3, f4],
PETSc.Mat.Structure.SUBSET,
)
# Customize options
nep.setTolerances(tol=1e-7)
nep.setDimensions(nev=24)
nep.setType(SLEPc.NEP.Type.CISS)
# the rg params are chosen s.t. the singularity at k = ka - 1j*gt is
# outside of the contour.
radius = 3 * gt
vscale = 0.5 * gt / radius
rg_params = (ka, 3 * gt, vscale)
R = nep.getRG()
R.setType(SLEPc.RG.Type.ELLIPSE)
Print(f"RG params: {rg_params}")
R.setEllipseParameters(*rg_params)
nep.setFromOptions()
# Solve the problem
nep.solve()
its = nep.getIterationNumber()
Print("Number of iterations of the method: %i" % its)
sol_type = nep.getType()
Print("Solution method: %s" % sol_type)
nev, ncv, mpd = nep.getDimensions()
Print("")
Print("Subspace dimension: %i" % ncv)
tol, maxit = nep.getTolerances()
Print("Stopping condition: tol=%.4g" % tol)
Print("")
nconv = nep.getConverged()
Print("Number of converged eigenpairs %d" % nconv)
x = A.createVecs("right")
evals = []
modes = []
if nconv > 0:
Print()
Print(" lam ||T(lam)x|| |lam-lam_exact|/|lam_exact| ")
Print("--------------------- ------------- -----------------------------")
for i in range(nconv):
lam = nep.getEigenpair(i, x)
error = nep.computeError(i)
def eigenvalue_error_term(k):
gkmu = gt / (k - ka + 1j * gt)
nceff = np.sqrt(nc**2 + gkmu * D0)
return -1j / np.tan(nceff * k * L) - 1 / nceff
# compute the expected_eigenvalue
# we assume that the numerically calculated eigenvalue is close to
# the exact one, which we can determine using a Newton-Raphson
# method.
if scipy:
expected_lam = scipy.optimize.newton(
eigenvalue_error_term, np.complex128(lam), rtol=1e-11
)
rel_err = abs(lam - expected_lam) / abs(expected_lam)
rel_err = "%6g" % rel_err
else:
rel_err = "scipy not installed"
Print(" %9f%+9f j %12g %s" % (lam.real, lam.imag, error, rel_err))
evals.append(lam)
modes.append(x.getArray().copy())
Print()
return np.asarray(evals), rg_params, ka, gt
The main function reads the problem size n from the command line,
solves the problem with the above function, and then plots the computed
eigenvalues.
def main():
opts = PETSc.Options()
n = opts.getInt("n", 256)
Print(f"n={n}")
evals, rg_params, ka, gt = solve(n)
if not opts.getBool("ploteigs", True) or PETSc.COMM_WORLD.getRank():
return
try:
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
except ImportError:
print("plot is not shown, because matplotlib is not installed")
else:
fig, ax = plt.subplots()
ax.plot(evals.real, evals.imag, "x")
height = 2 * rg_params[1] * rg_params[2]
ellipse = Ellipse(
xy=(rg_params[0], 0.0),
width=rg_params[1] * 2,
height=height,
edgecolor="r",
fc="None",
lw=2,
)
ax.add_patch(ellipse)
ax.grid()
ax.legend()
plt.show()
if __name__ == "__main__":
main()