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')
sys.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(f'Number of iterations of the method: {its}')
sol_type = nep.getType()
Print(f'Solution method: {sol_type}')
_nev, ncv, _mpd = nep.getDimensions()
Print('')
Print(f'Subspace dimension: {ncv}')
tol, _maxit = nep.getTolerances()
Print(f'Stopping condition: tol={tol:.4g}')
Print('')
nconv = nep.getConverged()
Print(f'Number of converged eigenpairs {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 = f'{rel_err:6g}'
else:
rel_err = 'scipy not installed'
Print(f' {lam.real:9f}{lam.imag:+9f} j {error:12g} {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()