Modal analysis with dolfinx + SLEPc gives wrong eigenfrequencies

Hey everyone,

I’m currently trying to run a modal analysis for a simple 3D elastic body using FEniCSx and SLEPc, but I’m having trouble understanding why my results do not make physical sense.

I’m trying to solve the Eigenvalue problem:

K*u = omega**2*M*u

I want to solve it using the SLEPc PEP solver and I also tested it with the EPS solver for comparison. In the future i would like to also add a damping matrix C, it is currently set to zero in the PEP solver. To my understanding, the results should be similar between the two Solvers, but they are different and not feasable.

When I increase the geometry, the eigenfrequencies increase while they should decrease.

The geometry should normally be t = 2e-3, h = 35e-3, 50e-3 and the first natural frequency should be around 1400 Hz based on experiments, but my solver returns a Eigenvalue of about 1.0.

I’m pretty sure my Dirichlet boundary conditions are correct, the metal piece should be fixed at the bottom.

I checked the norms and the symmetry of my matrices, they seem about fine, but feel free to double check.

Does anyone know, where my problem could be? Should the EPS and PEP solvers give identical results when the damping matrix is set to zero, or am I already mistaken there?

I would be happy to share my code snippet with you, if that helps. Thanks for any hints or sanity checks!

  • Robin
from mpi4py import MPI
import numpy as np
import ufl
from dolfinx import mesh, fem
import dolfinx
import dolfinx.fem.petsc
from slepc4py import SLEPc

#Define Object
t = 2e+3
h = 35e+3
w = 50e+3
domain = mesh.create_box(
    MPI.COMM_WORLD,
    [[0.0, 0.0, 0.0], [w, h, t]],
    [12,8,2],
    cell_type=mesh.CellType.hexahedron,
)
tdim = domain.topology.dim

#functionspace
V = fem.functionspace(domain, ("Lagrange", 2, (domain.geometry.dim,)))

#Material Data
rho     = 2.7e3             # kg/m^3
young   = 71e9              # Pa
nu      = 0.30              # Poisson
mu      = young / (2.0 * (1.0 + nu))
lmbda   = young * nu / ((1.0+nu)*(1.0-2.0*nu))

#Dirichlet
def bottom(x):
    return np.isclose(x[1], 0.0)

clamped_facets = dolfinx.mesh.locate_entities_boundary(domain, tdim - 1, bottom)
clamped_nodes = dolfinx.mesh.locate_entities(domain, 0, bottom)
num_facets = domain.topology.index_map(tdim - 1).size_local
markers = np.zeros(num_facets, dtype=np.int32)
clamped = 1
markers[clamped_facets] = clamped
facet_marker = dolfinx.mesh.meshtags(domain, tdim - 1, np.arange(num_facets, dtype=np.int32), markers)

clamped_dofs = dolfinx.fem.locate_dofs_topological(V, facet_marker.dim, facet_marker.find(clamped))
u_clamped = dolfinx.fem.Constant(domain, (0.0, 0.0, 0.0))

bcs = [
    dolfinx.fem.dirichletbc(u_clamped, clamped_dofs, V)
]

def epsilon(u):
    return ufl.sym(ufl.grad(u))

def sigma(u):
    return lmbda*ufl.tr(epsilon(u))*ufl.Identity(tdim) + 2.0*mu*epsilon(u)


u = ufl.TrialFunction(V)
v = ufl.TestFunction(V)

k = ufl.inner(sigma(u), epsilon(v)) * ufl.dx
m = rho * ufl.inner(u,v) * ufl.dx

k_compiled = dolfinx.fem.form(k)
K = dolfinx.fem.petsc.assemble_matrix(k_compiled, bcs=bcs)
K.assemble()

m_compiled = dolfinx.fem.form(m)
M = dolfinx.fem.petsc.assemble_matrix(m_compiled, bcs=bcs)
M.assemble()

#Damping
alpha = 0.0 
beta  = 0.0 
C = K.copy()
C.scale(beta)
C.axpy(alpha, M, True)
C.assemble()

#Solve
#Pep

pep = SLEPc.PEP().create(domain.comm)
pep.setOperators([M,C,K])
pep.setProblemType(SLEPc.PEP.ProblemType.GENERAL)
pep.setType(SLEPc.PEP.Type.QARNOLDI)
pep.setTolerances(tol=1e-8, max_it=200)
pep.setFromOptions()
pep.solve()

#eps
eps = SLEPc.EPS().create(domain.comm)
eps.setOperators(M, K)
eps.setProblemType(SLEPc.EPS.ProblemType.GHEP)
pep.setTolerances(tol=1e-8, max_it=200)
pep.setFromOptions()
eps.solve()

#Analyse Output
nconv = pep.getConverged()
print("converged eigenpairs:", nconv)
for i in range(min(nconv, 10)):
    Eigenvalue = pep.getEigenpair(i)  

    omega_re = np.real(Eigenvalue)
    omega_im = np.imag(Eigenvalue)

    omega = abs(omega_im) 

    freq = np.sqrt(omega) / (2*np.pi)
    zeta = -omega_re / np.sqrt(omega_re**2 + omega_im**2)

    if omega_re < -1e-10:
        print(f"Mode {i}: freq = {freq:.3f} Hz, damping ratio = {zeta:.4f}")
    elif omega_re > 1e-10:
        print(f"Mode {i}: freq = {freq:.3f} Hz, damping ratio = {zeta:.4f}")
    else: 
        print(f"Mode {i} : freq = {freq:.3f} Hz, damping ratio = {zeta:.4f}")

nconv = eps.getConverged()
print("converged eigenpairs:", nconv)
for i in range(min(nconv, 10)):
    Eigenvalue = eps.getEigenpair(i)  

    omega = Eigenvalue 
    freq = np.sqrt(omega) / (2*np.pi)

    print(f"Mode {i}: freq = {freq:.3f} Hz")



I can have a more in-depth look tomorrow, but the first thing I see is that you state

While your code has

That’s not the source of your problem, is it?

Hey Stein,
thank you for your reply!
No, unfortunately that is not my problem. I adjusted them to check the Eigenvalues for different dimensions of my geometry and with increasing size the natural frequencies increase as well, which I don’t understand. It also wouldn’t explain the difference between the EPS and PEP solver.

I will work on it tonight, but if I don’t post anything else I couldn’t get closer to an answer. So your help would me much appreciated!

~Robin

Hi.

There are a couple of facts that I would like to point out from your approach.

  1. You should set different diag values when assembling the meshes if using Dirichlet boundary conditions for your case.
K = dolfinx.fem.petsc.assemble_matrix(k_compiled, bcs=bcs, diag=1)

M = dolfinx.fem.petsc.assemble_matrix(m_compiled, bcs=bcs, diag=0)
  1. When using PEP, your are solving, in general, P(\lambda) = A_0 + \lambda A_1 + \dots + \lambda^d A_d (see PEPSetOperators — SLEPc 3.24.1 documentation). Hence, for your case, you are solving P(\omega) = M + \omega C + \omega^2 K, so the solver will give you \omega. You don’t need to compute np.sqrt(omega).
freq = omega / (2*np.pi)
# instead of np.sqrt(omega) / (2*np.pi) 
  1. You forgot to compute \omega=1/\sqrt{\hat{\omega}} (for EPS) or \omega=1/{\hat{\omega}} (for PEP) since your’e solving Mx=\hat{\omega}^2Kx (for EPS) or the system in point 2. with \hat{\omega}, where \hat{\omega}=1/\omega.
#for PEP solver
omega_re = 1/np.real(Eigenvalue)
omega_im = 1/np.imag(Eigenvalue)
#for EPS solver
omega = 1/Eigenvalue

The resulting code is

from mpi4py import MPI
import numpy as np
import ufl
from dolfinx import mesh, fem
import dolfinx
import dolfinx.fem.petsc
from slepc4py import SLEPc

# Define Object
t = 2e-3
h = 35e-3
w = 50e-3
domain = mesh.create_box(
    MPI.COMM_WORLD,
    [[0.0, 0.0, 0.0], [w, h, t]],
    [12, 8, 2],
    cell_type=mesh.CellType.hexahedron,
)
tdim = domain.topology.dim

# functionspace
V = fem.functionspace(domain, ("Lagrange", 2, (domain.geometry.dim,)))

# Material Data
rho = 2.7e3             # kg/m^3
young = 71e9              # Pa
nu = 0.30              # Poisson
mu = young / (2.0 * (1.0 + nu))
lmbda = young * nu / ((1.0+nu)*(1.0-2.0*nu))

# Dirichlet


def bottom(x):
    return np.isclose(x[1], 0.0)


clamped_facets = dolfinx.mesh.locate_entities_boundary(
    domain, tdim - 1, bottom)
clamped_nodes = dolfinx.mesh.locate_entities(domain, 0, bottom)
num_facets = domain.topology.index_map(tdim - 1).size_local
markers = np.zeros(num_facets, dtype=np.int32)
clamped = 1
markers[clamped_facets] = clamped
facet_marker = dolfinx.mesh.meshtags(
    domain, tdim - 1, np.arange(num_facets, dtype=np.int32), markers)

clamped_dofs = dolfinx.fem.locate_dofs_topological(
    V, facet_marker.dim, facet_marker.find(clamped))
u_clamped = dolfinx.fem.Constant(domain, (0.0, 0.0, 0.0))

bcs = [
    dolfinx.fem.dirichletbc(u_clamped, clamped_dofs, V)
]


def epsilon(u):
    return ufl.sym(ufl.grad(u))


def sigma(u):
    return lmbda*ufl.tr(epsilon(u))*ufl.Identity(tdim) + 2.0*mu*epsilon(u)


u = ufl.TrialFunction(V)
v = ufl.TestFunction(V)

k = ufl.inner(sigma(u), epsilon(v)) * ufl.dx
m = rho * ufl.inner(u, v) * ufl.dx

k_compiled = dolfinx.fem.form(k)
K = dolfinx.fem.petsc.assemble_matrix(k_compiled, bcs=bcs, diag=1)
K.assemble()

m_compiled = dolfinx.fem.form(m)
M = dolfinx.fem.petsc.assemble_matrix(m_compiled, bcs=bcs, diag=0)
M.assemble()

# Damping
alpha = 0.0
beta = 0.0
C = K.copy()
C.scale(beta)
C.axpy(alpha, M, True)
C.assemble()

# Solve
# Pep

pep = SLEPc.PEP().create(domain.comm)
pep.setOperators([M, C, K])
pep.setProblemType(SLEPc.PEP.ProblemType.GENERAL)
pep.setType(SLEPc.PEP.Type.QARNOLDI)
pep.setTolerances(tol=1e-8, max_it=200)
pep.setFromOptions()
pep.solve()

# eps
eps = SLEPc.EPS().create(domain.comm)
eps.setOperators(M, K)
eps.setProblemType(SLEPc.EPS.ProblemType.GHEP)
pep.setTolerances(tol=1e-8, max_it=200)
pep.setFromOptions()
eps.solve()

# Analyse Output
nconv = pep.getConverged()
print("converged eigenpairs:", nconv)
for i in range(min(nconv, 10)):
    Eigenvalue = pep.getEigenpair(i)

    omega_re = 1/np.real(Eigenvalue)
    omega_im = 1/np.imag(Eigenvalue)

    omega = abs(omega_im)

    freq = omega / (2*np.pi)
    zeta = -omega_re / np.sqrt(omega_re**2 + omega_im**2)

    if omega_re < -1e-10:
        print(f"Mode {i}: freq = {freq:.3f} Hz, damping ratio = {zeta:.4f}")
    elif omega_re > 1e-10:
        print(f"Mode {i}: freq = {freq:.3f} Hz, damping ratio = {zeta:.4f}")
    else:
        print(f"Mode {i} : freq = {freq:.3f} Hz, damping ratio = {zeta:.4f}")

nconv = eps.getConverged()
print("converged eigenpairs:", nconv)
for i in range(min(nconv, 10)):
    Eigenvalue = eps.getEigenpair(i)

    omega = 1/Eigenvalue
    freq = np.sqrt(omega) / (2*np.pi)

    print(f"Mode {i}: freq = {freq:.3f} Hz")

which gives

converged eigenpairs: 6
Mode 0: freq = 1408.790 Hz, damping ratio = 1.0000
Mode 1: freq = 1408.790 Hz, damping ratio = 1.0000
Mode 2: freq = 2632.262 Hz, damping ratio = 1.0000
Mode 3: freq = 2632.262 Hz, damping ratio = 1.0000
Mode 4: freq = 6120.300 Hz, damping ratio = 1.0000
Mode 5: freq = 6120.300 Hz, damping ratio = 1.0000
converged eigenpairs: 5
Mode 0: freq = 1408.790 Hz
Mode 1: freq = 2632.262 Hz
Mode 2: freq = 6120.271 Hz
Mode 3: freq = 8774.530 Hz
Mode 4: freq = 10449.806 Hz

The double multiplicity for PEP is because of the complex pairing.

Hope this helps.

Cheers.

4 Likes

Thank you so much!
That solved the issue completely.

As a follow up question, if you have the time to answer: Are the diag values arbitrary or is there a reason behind the 1 for the stiffness matrix and the 0 for the mass matrix?

Also, should the damping matrix have a modified diag value as well? Right now I’m using the Rayleigh damping model as it contains the mass and stiffness matrix with alpha and beta to be their respective influence. But in the future I might change the damping model.

So if you have an article or an explanation for the diag value I would appreciate any help.
Thanks again
~ Robin

The diag values correspond to the value assigned (it can be a penalization) to the DOF in the matrix diagonal corresponding to Dirichlet boundary conditions. See for example Application of Dirichlet boundary conditions — FEniCS Tutorial @ Sorbonne for the application of the Dirichlet boundary conditions. Note that assemble_matrix has by default diag=1. Hence if both K and M have diag=1, then you will have something like

K=\left(\begin{aligned} &K_{II} & 0\\ &0 & I \end{aligned} \right),\qquad M=\left(\begin{aligned} &M_{II} & 0\\ &0 & I \end{aligned} \right)

so the eigenvalues on the Dirichlet bc will be fixed to 1 and you’ll get spurious eigenvalues (as you posted it). Instead, if you set ‘diag=0’ to M, then the matrix becomes

M=\left(\begin{aligned} &M_{II} & 0\\ &0 & 0 \end{aligned} \right)

And the eigenvalues are computed by solving K_{II}u_{II}=\lambda M_{II}u_{II}. I have seen diag=1e2 and diag=1e-2 for K and M, respectively, in some evp problems, but I find 1 and 0 the safest choice.

Cheers.

1 Like