Demo - Eigenvalues on the Möbius strip#
Mikael Mortensen (email: mikaem@math.uio.no
), Department of Mathematics, University of Oslo.
Date: October 15, 2020
Summary. This is a demonstration of how the Python module shenfun can be used to
compute eigenvalues and vectors of the Laplace-Beltrami
operator on a Möbius strip. The absolute value of the eigenvector
corresponding to the eigth smallest eigenvalue
Figure 1
The Möbius strip#
A Möbius strip is the simplest non-orientable surface embedded in
where
The twisted condition does not lend itself easily to a regular tensor product basis. On the other hand, by moving twice around the strip, regular periodic boundary conditions
will apply [Kalvoda2020], and we can discretize using Fourier
exponentials in the
Note that the reference domain corresponds to
For the
The Laplace Beltrami operator#
We consider the eigenvalue problem of the Laplace Beltrami operator by solving
where
To solve this problem with the spectral Galerkin method we
choose an appropriate space
where
With shenfun it is enough to operate in general coordinates as above and let the curvilinear mathematics all happen under the hood. However, for this example it is interesting to see what the Laplace-Beltrami operator looks like in computational coordinates. This is quite a bit of work by hand, and the starting point is the position vector
From the position vector we compute the he covariant
basis vectors
and
Likewise, the contravariant metric tensor
Please note that all these metrics and other terms are computed under the hood by shenfun, and a user does not normally have to worry about these.
It can be shown that the Laplace-Beltrami operator in curvilinear coordinates is given as
with summation on repeated indices.
Using this and the surface element
Summing and expanding some derivatives, we get
Note that the variational problem contains unseparable variable coefficients,
like
where the left hand side can be assembled using tensor product matrices, where no single 1D matrix has more than 9 diagonals.
Fortunately we do not have to do all this by hand since we have a software that automatically assembles such matrices for us. Now let’s see how this problem can be handled with shenfun.
Implementation#
First we need to create function spaces for each direction
from shenfun import *
import sympy as sp
from IPython.display import Math, Latex, display
from scipy.sparse.linalg import eigs
config['basisvectors'] = 'covariant'
phi, t = psi = sp.symbols('x,y', real=True)
RR = sp.Rational(132, 20)/sp.pi # Same as Kalvoda et al
#RR = 2
# Use a symbolic R first, then later substitute for the value in RR
R = sp.Symbol('R', real=True, positive=True)
rv = ((R-t*sp.cos(phi))*sp.cos(2*phi),
(R-t*sp.cos(phi))*sp.sin(2*phi),
-t*sp.sin(phi))
def discourage_powers(expr):
POW = sp.Symbol('POW')
count = sp.count_ops(expr, visual=True)
count = count.replace(POW, 100)
count = count.replace(sp.Symbol, type(sp.S.One))
return count
N = (80, 40)
B0 = FunctionSpace(N[0], 'F', domain=(-np.pi, np.pi), dtype='D')
B1 = FunctionSpace(N[1], 'L', bc=(0, 0), domain=(-0.75, 0.75)) # Use same domain as Kalvoda et al
T = TensorProductSpace(comm, (B0, B1), coordinates=(psi, rv, True, (), discourage_powers), axes=(1, 0))
u = TrialFunction(T)
v = TestFunction(T)
Note the discourage_powers
function. Try to turn it off (if
you are watching this in an interactive setting) and see what happens
to, e.g., T.coors.sg
, which is
We can now check to see what the Laplace-Beltrami operator looks like when computed with shenfun:
g = sp.Function('g')(phi, t)
replace = [(T.coors.sg**2, g)]
Math((div(grad(u))).tolatex(funcname='f', symbol_names={phi:'\\varphi', t:'t'}, replace=replace))
Not surprisingly, this is the same (except not multiplied by
At this point we replace the symbol R with a number in order to
assemble floating point matrices. The number R can be changed
above as the variable RR
. For now it is set to use a value used also
by Kalvoda et al., such that we can doublecheck our eigenvalues.
We multiply forms with
T.coors.subs(R, RR)
M = inner(v*T.coors.sg**3, -div(grad(u)))
B = inner(v*T.coors.sg**3, u)
Here M
and B
are lists of instances of the tensor product
matrix class TPMatrix.
print(f'Number of matrices for M = {len(M)} and B = {len(B)}')
So quite a few matrices, but they are all sparse. We solve by using a Kronecker product solver Solver2D that flattens the tensor product matrices and solution vector C-style.
mm = la.Solver2D(M)
bb = la.Solver2D(B)
Mc = mm.mat.copy()
Bc = bb.mat.copy()
Finally, solve the eigenvalue problem using a sparse eigenvalue solver from scipy, which is wrapping ARPACK. Note that ARPACK is better at finding large than small eigenvalues and for this reason we use a shift-inverted version, see https://docs.scipy.org/doc/scipy/reference/tutorial/arpack.html.
f = eigs(Mc, k=40, M=Bc, which='LM', sigma=0)
We have now found all eigenvalues on the Möbius strip with two rotations. So some of the eigenvalues/eigenvectors will not have the correct twisted periodic boundary conditions. To get only the correct eigenvalues, we filter a little bit, checking the boundary:
u_hat = Function(T)
eigvals = []
for i in range(f[1].shape[1]):
u_hat[:, :-2] = np.reshape(f[1][:, i], T.dims())
tt = u_hat.eval(np.array([[-np.pi, 0, -np.pi, 0], [0.5, -0.5, 0.65, -0.65]]))
dt = Dx(u_hat, 0, 1).eval(np.array([[-np.pi, 0, -np.pi, 0], [0.5, -0.5, 0.65, -0.65]]))
# Check for twisted periodic
if abs(tt[0]-tt[1]+tt[2]-tt[3]) < 1e-7 and abs(dt[0]-dt[1]+dt[2]-dt[3]) < 1e-8:
eigvals.append((i, f[0][i].real))
print(f'Twisted eigenvalue {len(eigvals):2d} {i:2d} {f[0][i].real:2.12e} Error {np.linalg.norm(Mc*f[1][:, i] - f[0][i]*Bc*f[1][:, i]):2.4e}')
These are the lowest true eigenvalues of the Möbius strip. We note that they are very similar to Table 2 in [Kalvoda2020]. We can now plot the eigenvectors using, e.g., mayavi or plotly. Choose the eigenvalue number first and then the rest follows
l = 7
u_hat[:, :-2] = np.reshape(f[1][:, eigvals[l][0]], T.dims())
u_hat2 = u_hat.refine([2*N[0], 2*N[1]])
N0 = u_hat2.function_space().shape(False)[0]//2+1
fig = surf3D(u_hat2, slices=(slice(0, N0), slice(None)))
fig.show()
Or make subplot of several of the eigenvectors:
import plotly
from plotly.subplots import make_subplots
import plotly.graph_objects as go
rows = 3
cols = 2
fig = make_subplots(rows=rows, cols=cols, start_cell="top-left", specs=[[dict(type='surface')]*cols]*rows,
subplot_titles=(f'$\\lambda_1={eigvals[0][1]:2.5f}$', f'$\\lambda_3={eigvals[2][1]:2.5f}$',
f'$\\lambda_5={eigvals[4][1]:2.5f}$', f'$\\lambda_7={eigvals[6][1]:2.5f}$',
f'$\\lambda_9={eigvals[8][1]:2.5f}$', f'$\\lambda_{{11}}={eigvals[10][1]:2.5f}$'))
N0 = T.shape(False)[0]//2+1 # Remember, data is for two rounds around the strip, we need only 1
x, y, z = T.local_cartesian_mesh()
x, y, z = x[:N0], y[:N0], z[:N0]
d = {'visible': False, 'showgrid': False, 'zeroline': False}
for l in range(rows*cols):
u_hat[:, :-2] = np.reshape(f[1][:, eigvals[2*l][0]], T.dims())
s = go.Surface(x=x, y=y, z=z, surfacecolor=abs(u_hat.backward()[:N0]),
colorscale=plotly.colors.sequential.Jet,
showscale=False)
fig.add_trace(s, row=l//cols+1, col=l%cols+1)
scene = 'scene' if l == 0 else f'scene{l+1}'
fig.update_layout({scene: {'xaxis': d, 'yaxis': d, 'zaxis': d, 'camera': {'eye': dict(x=0.85, y=0.85, z=0.85)}}})
fig.show()
Finally, plot the sparsity pattern of the coefficient matrix. You
need to zoom in order to see the pattern properly and for
this reason we use plotly instead of the more convenient
matplotlib spy
function (%matplotlib notebook
does not work in an
interactive jupyterlab and as such a jupyter book session).
If you are curious, then please change basis to Chebyshev and
note the difference from Legendre. Otherwise, results should be very
much alike.
import plotly.express as px
z = np.where(abs(mm.mat.toarray()) > 1e-6, 0, 1).astype(bool)
fig = px.imshow(z, binary_string=True)
fig.show()
T. Kalvoda, D. Krejcirik and K. Zahradová. Effective Quantum Dynamics on the Möbius Strip, Journal of Physics A: Mathematical and Theoretical, 53(37), pp. 375201, doi: 10.1088/1751-8121/ab8b3a, 2020.