Why do I obtain inaccurate mode shapes from SLEPc PEP-solver for 3D elasticity problem with periodic BC?

Hi everyone,

I’m implementing an inverse dispersion solver in DOLFINx that computes complex wavenumbers k(ω) for waves in a periodic elastic 3D structure, using the SLEPc PEP-solver and Bloch periodic boundary conditions. The eigenvalues are correct and the dispersion curves look as expected. However, when plotting the mode shapes from the dispersion curve, the modes don’t look correct. Especially in the first branch, the modes don’t look accurate as there is only rigid body motion and no bending waves.

I have a related forward solver that computes ω(k) with the SLEPc EPS-solver for the same geometry, mesh, and material assignment, which yields a similar dispersion curve (with only real-values k-numbers). The mode shapes from that solver look accurate.

I’ve tried to extract the eigenvectors from the PEP-solver in different ways, plotting both the real part of the displacement as well as the magnitudes, but the problem persists.

The code for solving the inverse problem and generating the dataset with frequencies, eigenvalues (k), and eigenvectors is attached below, along with the code for plotting the dispersion curve and clicking on points to obtain the mode shapes. The STEP-file for the geometry is available at: GitHub - NadineTab02/FEniCSx_Project · GitHub

# Minimum Working Example - Solving for complex k (quadratic eigenvalue problem)

#--------------Import packages--------------------------

import numpy as np
import matplotlib.pyplot as plt
from petsc4py import PETSc
from mpi4py import MPI
import dolfinx
import gmsh
import dolfinx.mesh as msh
from dolfinx import mesh, fem, plot
from ufl import  dx, inner
from basix.ufl import element
from dolfinx import default_scalar_type
from dolfinx.fem import form, functionspace, assemble_scalar, Function
from dolfinx.fem.petsc import assemble_matrix, LinearProblem
from slepc4py import SLEPc
from dolfinx.io import gmsh as gmshio
import pickle 
import os 
import dolfinx_mpc.utils 
import ufl 
import pyvista as pv
from dolfinx_mpc import MultiPointConstraint, assemble_matrix
from dolfinx.fem import form
from slepc4py import SLEPc
from petsc4py import PETSc
import cmath
import os
import pickle
import numpy as np
import matplotlib.pyplot as plt

# Function for gmsh

def import_CAD_to_FEniCSx(input_file = "object_new_broken_cubic_case", output_file = "from_comsol", lc = 0.25):
    gmsh.initialize()
    comm = MPI.COMM_WORLD
    model_rank = 0
    model = gmsh.model()
    gmsh.model.add(output_file)
    gmsh.model.occ.importShapes(f"{input_file}.step")
    gmsh.model.occ.synchronize()
    
    volumes = gmsh.model.getEntities(dim=3)
    volume_tags = [v[1] for v in volumes]
    if len(volume_tags) > 1:
        gmsh.model.occ.fragment([(3, tag) for tag in volume_tags], [])
        gmsh.model.occ.synchronize()
        volumes = gmsh.model.getEntities(dim=3)
    
    gmsh.model.occ.synchronize()
    volumes = gmsh.model.getEntities(dim=3)
    if len(volumes) == 1:
        gmsh.model.addPhysicalGroup(3, [v[1] for v in volumes], tag=1)
    else:
        listvols1 = [1, 2, 3, 4, 5, 6, 7, 9]
        gmsh.model.addPhysicalGroup(3, listvols1, tag=101)
        listvols2 = [8, 10]
        gmsh.model.addPhysicalGroup(3, listvols2, tag=201)
    
    surfaces = gmsh.model.getEntities(dim=2)
    listsurf1 = [4]
    listsurf2 = [5]
    listsurf3 = [1, 2, 3, 6]
    gmsh.model.addPhysicalGroup(2, listsurf1, tag=1001)
    gmsh.model.addPhysicalGroup(2, listsurf2, tag=2001)
    gmsh.model.addPhysicalGroup(2, listsurf3, tag=3001)
    
    gmsh.option.setNumber("Mesh.ElementOrder", 2)
    gmsh.option.setNumber("Mesh.HighOrderOptimize", 2)
    gmsh.option.setNumber("Mesh.CharacteristicLengthMax", lc)
    gmsh.model.mesh.generate(3)
    gmsh.write(f"{output_file}.msh")
    
    mesh_data = gmshio.model_to_mesh(model, comm, model_rank)
    final_mesh = mesh_data.mesh
    facet_tags = mesh_data.facet_tags
    cell_tags = mesh_data.cell_tags
    
    gmsh.option.setNumber("Geometry.Surfaces", 1)
    gmsh.option.setNumber("Geometry.SurfaceLabels", 1)
    gmsh.fltk.run()
    gmsh.finalize()
    
    mesh_info = [final_mesh, cell_tags, facet_tags]
    return mesh_info, None

# Function for removing rows and columns to handle rigid body modes

def extract_sub_matrix(A: PETSc.Mat,
                       V: dolfinx.fem.FunctionSpace,
                       bcs: list[dolfinx.fem.DirichletBC] = None,
                       mpc=None) -> PETSc.Mat:
  

    num_dofs_local = V.dofmap.index_map.size_local * V.dofmap.index_map_bs
    num_ghosts = V.dofmap.index_map.num_ghosts * V.dofmap.index_map_bs

    if mpc is not None:
  
        slave_local = np.array(mpc.is_slave, dtype=np.int32) 
        mask = np.ones(num_dofs_local + num_ghosts, dtype=np.int32)
        mask[slave_local.astype(bool)] = 0
        idx_set = np.flatnonzero(mask).astype(np.int32)
        idx_set = idx_set[idx_set < num_dofs_local]

    elif bcs is not None:
        not_dirichlet = np.full(num_dofs_local + num_ghosts, 1, dtype=np.int32)
        for bc in bcs:
            not_dirichlet[bc.dof_indices()[0]] = 0

        idx_set = np.flatnonzero(not_dirichlet).astype(np.int32)
        idx_set = idx_set[idx_set < num_dofs_local]
    else:
        raise ValueError("Either bcs or mpc must be provided.")

    # Build PETSc index sets
    isx = PETSc.IS(A.getComm()).createGeneral(idx_set)
    lgm_row = A.getLGMap()[0].applyIS(isx)
    lgm_col = A.getLGMap()[1].applyIS(isx)

    # Extract the submatrix
    A_sub = A.createSubMatrix(lgm_row, lgm_col)
    A_sub.assemble()

    return A_sub, (idx_set, idx_set) 

# Functions to handle periodicity in the system 

def periodic_indicator(x, direction='z', toli=1e-8):
    """Returns True for points on the periodic boundary (max coordinate side)"""
    coord_map = {'x': 0, 'y': 1, 'z': 2}
    idx = coord_map[direction]
    x_max = mesh.geometry.x[:, idx].max()
    return np.isclose(x[idx], x_max, atol=toli)

def periodic_relation(x, direction='z', a_basis=None, toli=1e-8):
    """Maps points from max boundary to min boundary"""
    out = x.copy()
    coord_map = {'x': 0, 'y': 1, 'z': 2}
    idx = coord_map[direction]
    out[idx] = x[idx] - a_basis[idx]
    return out


#--------------Create mesh from the step file--------------------

step_filename, mesh_name = 'Test_periodic_rod_geo_r025', 'mesh_rod'
lc = 10 # characteristic mesh element size     

mesh_info, submesh_info = import_CAD_to_FEniCSx(step_filename, mesh_name, lc)

mesh, cell_tags, facet_tags = mesh_info
dim = mesh.geometry.dim
mesh.geometry.x[:] *= 0.001   # Need to downscale the geometry to original size 1 m
ds = ufl.Measure("ds", domain=mesh, subdomain_data=facet_tags)


#-------------------Geometry properties---------------------------

z_coords = mesh.geometry.x[:, 2]
L_f = z_coords.max() - z_coords.min()

# ---------------Define space function and element----------------------------

deg = 2 
Ve = element("Lagrange", mesh.basix_cell(), deg, shape=(dim,))  
V = functionspace(mesh, Ve)

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

#-------------Material parameters-----------------------------------------------------------

material_tags = np.unique(cell_tags.values) if cell_tags is not None else np.array([])
print(f"Material tags found in mesh: {material_tags}")

# Define material properties based on the tags from your import function

material_properties = {}

if len(material_tags) == 1:
    # Single material case
    print("Single material detected")
    # Use the actual tag from the mesh
    tag = material_tags[0]
    material_properties[tag] = {
        "name": "main_structure",
        "E": 2.10e14, 
        "rho": 7850, 
        "nu": 0.3
    }

print("Material configuration:")

for tag, props in material_properties.items():
    print(f"  Tag {tag} ({props['name']}): E={props['E']:.2e}, rho={props['rho']}, nu={props['nu']}")


Q = functionspace(mesh, ("DG", 0))

E = Function(Q)
rho = Function(Q)
nu = Function(Q)
mu = Function(Q)
lambda_ = Function(Q)


# Get cell tags values
if hasattr(cell_tags, 'values'):
    cell_values = cell_tags.values
else:
    cell_values = cell_tags

for tag, props in material_properties.items():
    cells = np.where(cell_values == tag)[0]


    if len(cells) > 0:
        E_val = props['E']
        rho_val = props['rho']
        nu_val = props['nu']
      
        E.x.array[cells] = np.full_like(cells, E_val, dtype=default_scalar_type)
        rho.x.array[cells] = np.full_like(cells, rho_val, dtype=default_scalar_type)
        nu.x.array[cells] = np.full_like(cells, nu_val, dtype=default_scalar_type)
    

        # Compute mu and lambda for these cells
        mu_val = E_val / (2 * (1 + nu_val))
        lambda_val = E_val * nu_val / ((1 + nu_val) * (1 - 2 * nu_val))  
        
        # Assign to mu and lambda functions
       
        mu.x.array[cells] = np.full_like(cells, mu_val, dtype=default_scalar_type) 
        lambda_.x.array[cells] = np.full_like(cells, lambda_val, dtype=default_scalar_type) 
        print(f"  Assigned to {len(cells)} cells for tag {tag}")

# ------------Boundary tagging-----------------------------------------------------

dx = ufl.Measure("dx", domain=mesh)

# ------------Define weak form for linear elasticity -----------

# Create coordinate component
direction  = 'z'
direction_map = {'x': 0, 'y': 1, 'z': 2}
d_idx = direction_map[direction]

# Unit vector in periodicity direction
e_d = ufl.as_vector([float(i == d_idx) for i in range(3)])


# Define weak forms  

def epsilon(u):
    return 0.5 * (ufl.nabla_grad(u) + ufl.nabla_grad(u).T)

def kappa(u):
    i, j = ufl.indices(2)
    Aij = u[i]*e_d[j] + e_d[i]*u[j]
    A = ufl.as_tensor(Aij, (i,j))
    return 0.5 * A


# Fourth-order elasticity tensor C

Id = ufl.Identity(dim)
indices = ufl.indices(4)

def delta_product(i, j, k, l):
    return ufl.as_tensor(Id[i, j] * Id[k, l], indices)

i, j, k, l = indices
C = lambda_ * delta_product(i, j, k, l) + mu * (delta_product(i, k, j, l) + delta_product(i, l, k, j))


# Bilinear forms with explicit conjugation (complex mode)

# K matrix
a_K = C[i, j, k, l] * ufl.conj(epsilon(v)[i, j]) * epsilon(u)[k, l] * dx

# L matrix  
a_linear = (-C[i, j, k, l] * ufl.conj(kappa(v)[i, j]) * epsilon(u)[k, l] + 
        C[i, j, k, l] * ufl.conj(epsilon(v)[i, j]) * kappa(u)[k, l]) * dx

# H matrix
a_quadratic = C[i, j, k, l] * ufl.conj(kappa(v)[i, j]) * kappa(u)[k, l] * dx

# Mass matrix
m = rho * ufl.conj(v[i]) * u[i] * dx

# For the standard stiffness 
a = a_K  


# Set periodicity parameters
direction = 'z'  # Periodicity along z-axis
direction_map = {'x': 0, 'y': 1, 'z': 2}
d_idx = direction_map[direction]

# Set basis vector
a_basis = np.zeros(3)
a_basis[d_idx] = L_f  # L_f is the length in periodic direction

# Create indicator and relation functions for MPC
indicator = lambda x: periodic_indicator(x, direction=direction)
relation = lambda x: periodic_relation(x, direction=direction, a_basis=a_basis)

#-----------------MPC, assembling matrices and handling rigid body modes -------------------------------------------------------------------------------------

# Create MPC with phase=1 for periodic boundary conditions (no phase shift for the base matrices)
mpc = dolfinx_mpc.MultiPointConstraint(V)
mpc.create_periodic_constraint_geometrical(V, indicator=indicator, relation=relation, bcs=[], scale=1.0)
mpc.finalize()


print("Assembling matrices with periodic boundary conditions (phase=1)...")

# Assemble ALL matrices ONCE with periodic BCs (phase=1)
K0 = dolfinx_mpc.assemble_matrix(form(a), mpc)
K1 = dolfinx_mpc.assemble_matrix(form(a_linear), mpc)
K2 = dolfinx_mpc.assemble_matrix(form(a_quadratic), mpc)
M = dolfinx_mpc.assemble_matrix(form(m), mpc)

K0.assemble()
K1.assemble()
K2.assemble()
M.assemble()

print("Matrix assembly complete!")

rows_K0, cols_K0= K0.getSize()
rows_K1, cols_K1= K1.getSize()
rows_K2, cols_K2= K2.getSize()
rows_M, cols_M= M.getSize()

K0_sub, (idx_set_row, idx_set_row) = extract_sub_matrix(K0, V, mpc=mpc)
K1_sub, (idx_set_row, idx_set_row) = extract_sub_matrix(K1, V, mpc=mpc)
K2_sub, (idx_set_row, idx_set_row) = extract_sub_matrix(K2, V, mpc=mpc)   
M_sub, (idx_set_row, idx_set_row) = extract_sub_matrix(M, V, mpc=mpc)  

print('Done with extracting dof')


#-------------------Frequency sweep to solve for k-------------------------------------

num_k = 20 # Minimum number of eigenvalues we want    
f_name = 'f'
num_f = 20  # Number of frequencies to sweep over   
f_values = np.linspace(0, 10000, num_f)               
num_f = len(f_values)

dispersion_k_real = []
dispersion_k_imag = []

all_modes_displacements = []  # Store displacement arrays

for idx, f in enumerate(f_values):

    print(f"\n{'='*60}")
    print(f"Frequency {idx+1}/{len(f_values)}: f = {f:.1f} Hz")
    print(f"{'='*60}")
    
    omega = 2 * np.pi * f
    omega_sq = omega**2
    
    # Build A0 = K0 - ω²M   
    A0 = K0_sub.copy()
    A0 += -omega_sq * M_sub   
    A0.assemble()


    A2 = K2_sub.copy()
    A2 *= -1.0
    
    pep = SLEPc.PEP().create(mesh.comm)
    pep.setOperators([A0, K1_sub, A2])                                                
    pep.setDimensions(nev=num_k)
    k0 = 1 
    target = 1j * k0 
    pep.setTarget(target)
    pep.setWhichEigenpairs(SLEPc.PEP.Which.TARGET_MAGNITUDE)

    st = pep.getST()
    st.setType(SLEPc.ST.Type.SINVERT)
    ksp = st.getKSP()
    ksp.setType("preonly")
    pc = ksp.getPC()
    pc.setType("lu")
    pc.setFactorSolverType("mumps")
    pep.setFromOptions()
    pep.solve()

    # Get converged eigenvalues
    nconv = pep.getConverged()
    print(f"Number of converged eigenvalues for f = {f} Hz is {nconv}")

    # Storage for this frequency (initialize empty lists)

    Lambda_values = []
    Lambda_values_real = []
    Lambda_values_imag = []

    modes_at_f = []  # Store mode indices


    for i in range(nconv):
         # Create vector to hold eigenvector
        vr = A0.createVecRight()

        Lambda = pep.getEigenpair(i, vr)
        Lambda_values.append(Lambda)
        Lambda_values_real.append(Lambda.real)
        Lambda_values_imag.append(Lambda.imag)

        k_real = Lambda.imag
        k_imag = -Lambda.real

        print(f"Mode {i}: Lambda = {Lambda.real:.6f} + {Lambda.imag:.6f}i "
                f"k = {k_real:.6f} + {k_imag:.6f}i")  

        
        # Create full vector in function space
        eh = Function(V)
        full_vec = eh.x.petsc_vec
        full_vec.set(0.0)
        full_vec.setValues(idx_set_row, vr.getArray())
        full_vec.assemble()
        
        dim = V.dofmap.index_map_bs
        mpc.backsubstitution(eh) 
        
        u_complex = eh.x.array.copy()
        u_complex_reshaped = u_complex.reshape(-1, dim)

        # Real and imaginary parts
        u_real = np.real(u_complex_reshaped)
        u_imag = np.imag(u_complex_reshaped)
        u_magnitude = np.abs(u_complex_reshaped)

        
        modes_at_f.append({
            'mode_index': i,
            'k_real': k_real,
            'k_imag': k_imag,
            'displacement_real': u_real,
            'displacement_imag': u_imag,
            'displacement_magnitude': u_magnitude,
            'displacement_complex': u_complex_reshaped,
        })
        

    # Append to dispersion data 

    dispersion_k_real.append(Lambda_values_imag)
    minus_Lambda_values_real = []
    for i in range(len(Lambda_values_real)):
        minus_Lambda_values_real.append(-Lambda_values_real[i])
    dispersion_k_imag.append(minus_Lambda_values_real)

    # Append mode shapes
    all_modes_displacements.append(modes_at_f)


# ----------------Save frequencies and k-values---------------------------------


with open(os.path.join('fenicsx_dump_inverse_cyl_testingJune25.pkl'), 'wb') as f: 
    pickle.dump([
        f_name,                    # 'f'
        num_f,                     # number of frequencies
        f_values,                  # frequency values
        dispersion_k_real,         # Re(k) for each frequency
        dispersion_k_imag,         # Im(k) for each frequency,     
        all_modes_displacements    # mode shapes for later plotting
    ], f)

print("\nData saved successfully!")    




# Minimum Working Example - Interactive plotting with point clicking

import numpy as np
import matplotlib.pyplot as plt
import pyvista as pv
import dolfinx
from dolfinx import mesh, fem, plot
from basix.ufl import element
from mpi4py import MPI
import gmsh
import dolfinx.mesh as msh
from dolfinx.io import gmsh as gmshio
import pickle
import os

#--------------Mesh import function (same as solver)--------------------------------

def import_CAD_to_FEniCSx(input_file = "object_new_broken_cubic_case", output_file = "from_comsol", lc = 0.25):
    gmsh.initialize()
    comm = MPI.COMM_WORLD
    model_rank = 0
    model = gmsh.model()
    gmsh.model.add(output_file)
    gmsh.model.occ.importShapes(f"{input_file}.step")
    gmsh.model.occ.synchronize()
    
    volumes = gmsh.model.getEntities(dim=3)
    volume_tags = [v[1] for v in volumes]
    if len(volume_tags) > 1:
        gmsh.model.occ.fragment([(3, tag) for tag in volume_tags], [])
        gmsh.model.occ.synchronize()
        volumes = gmsh.model.getEntities(dim=3)
    
    gmsh.model.occ.synchronize()
    volumes = gmsh.model.getEntities(dim=3)
    if len(volumes) == 1:
        gmsh.model.addPhysicalGroup(3, [v[1] for v in volumes], tag=1)
    else:
        listvols1 = [1, 2, 3, 4, 5, 6, 7, 9]
        gmsh.model.addPhysicalGroup(3, listvols1, tag=101)
        listvols2 = [8, 10]
        gmsh.model.addPhysicalGroup(3, listvols2, tag=201)
    
    surfaces = gmsh.model.getEntities(dim=2)
    listsurf1 = [4]
    listsurf2 = [5]
    listsurf3 = [1, 2, 3, 6]
    gmsh.model.addPhysicalGroup(2, listsurf1, tag=1001)
    gmsh.model.addPhysicalGroup(2, listsurf2, tag=2001)
    gmsh.model.addPhysicalGroup(2, listsurf3, tag=3001)
    
    gmsh.option.setNumber("Mesh.ElementOrder", 2)
    gmsh.option.setNumber("Mesh.HighOrderOptimize", 2)
    gmsh.option.setNumber("Mesh.CharacteristicLengthMax", lc)
    gmsh.model.mesh.generate(3)
    gmsh.write(f"{output_file}.msh")
    
    mesh_data = gmshio.model_to_mesh(model, comm, model_rank)
    final_mesh = mesh_data.mesh
    facet_tags = mesh_data.facet_tags
    cell_tags = mesh_data.cell_tags
    
    gmsh.option.setNumber("Geometry.Surfaces", 1)
    gmsh.option.setNumber("Geometry.SurfaceLabels", 1)
    gmsh.fltk.run()
    gmsh.finalize()
    
    mesh_info = [final_mesh, cell_tags, facet_tags]
    return mesh_info, None

#--------------Create mesh--------------------------------

step_filename, mesh_name = 'Test_periodic_rod_geo_r025', 'mesh_rod'
lc = 10
mesh_info, submesh_info = import_CAD_to_FEniCSx(step_filename, mesh_name, lc)

mesh, cell_tags, facet_tags = mesh_info
dim = mesh.geometry.dim
mesh.geometry.x[:] *= 0.001

# Get length parameter L_f for scaling
z_coords = mesh.geometry.x[:, 2]
L_f = z_coords.max() - z_coords.min()
k_scaling = L_f / np.pi

#--------------Load data--------------------------------

print("Loading data from fenicsx_dump_inverse_cyl_testingJune25.pkl...")
with open('fenicsx_dump_inverse_cyl_testingJune25.pkl', 'rb') as f:
    obj = pickle.load(f)
    f_name = obj[0]
    num_f = obj[1]
    f_values = obj[2]
    dispersion_k_real = obj[3]
    dispersion_k_imag = obj[4]
    all_modes_displacements = obj[5]

print(f"Loaded {len(f_values)} frequencies")
print(f"Frequency range: {f_values[0]:.1f} - {f_values[-1]:.1f} Hz")

# Scale k values
print("Scaling k values...")
for i in range(len(dispersion_k_real)):
    for j in range(len(dispersion_k_real[i])):
        dispersion_k_real[i][j] = dispersion_k_real[i][j] * k_scaling
        dispersion_k_imag[i][j] = dispersion_k_imag[i][j] * k_scaling

#--------------Interactive plot with point clicking--------------------------------

print("\n" + "="*50)
print("INTERACTIVE MODE SHAPE PICKER")
print("Click on points in the dispersion curve to plot mode shapes")
print("="*50)

# Collect data for plotting
all_k = []
all_f = []
points_data = []

for i, f in enumerate(f_values):
    for j, k in enumerate(dispersion_k_real[i]):
        if k > 0:  # Only positive k
            all_k.append(k)
            all_f.append(f)
            points_data.append({
                'k': k,
                'f': f,
                'f_idx': i,
                'mode_j': j,
                'mode_index': all_modes_displacements[i][j].get('mode_index', j) if j < len(all_modes_displacements[i]) else j
            })

# Create interactive plot
fig, ax = plt.subplots(figsize=(10, 8))
sc = ax.scatter(all_k, all_f, c='blue', s=20, picker=True)

ax.set_xlabel(r'Re$(k)$ [$\pi$/a]')
ax.set_ylabel(r'$f$ [Hz]')
ax.set_xlim(0, 1)
ax.set_ylim(0, 70000)
ax.grid(True)

# Store clicked points
clicked_points = []

def on_pick(event):
    ind = event.ind[0]
    point = points_data[ind]
    
    print(f"\n{'='*50}")
    print(f"CLICKED POINT {len(clicked_points)+1}:")
    print(f"  f = {point['f']:.2f} Hz")
    print(f"  k = {point['k']:.6f} (π/a)")
    print(f"  f_idx = {point['f_idx']}")
    print(f"  mode_j = {point['mode_j']}")
    print(f"  mode_index = {point['mode_index']}")
    print(f"{'='*50}")
    
    clicked_points.append(point)

fig.canvas.mpl_connect('pick_event', on_pick)
plt.show()

#--------------Plot mode shapes for clicked points--------------------------------

print(f"\n{'='*50}")
print(f"Plotting mode shapes for {len(clicked_points)} clicked points...")
print(f"{'='*50}")

VISUAL_SCALE = 10

for idx, point in enumerate(clicked_points):
    print(f"\nPlotting {idx+1}/{len(clicked_points)}: f = {point['f']:.2f} Hz, k = {point['k']:.6f}")
    
    f_idx = point['f_idx']
    f_actual = f_values[f_idx]
    modes_at_f = all_modes_displacements[f_idx]
    
    # Find mode by mode_index
    mode_found = None
    for mode in modes_at_f:
        if mode.get('mode_index') == point['mode_index']:
            mode_found = mode
            break
    
    # Try using mode_j as fallback
    if mode_found is None:
        print(f"  Trying mode_j = {point['mode_j']} instead...")
        for mode in modes_at_f:
            if mode.get('mode_index') == point['mode_j']:
                mode_found = mode
                print(f"  Found mode with mode_index = {point['mode_j']}")
                break
    
    if mode_found is None:
        print(f"  No mode found. Skipping...")
        continue
    
    u_plot = mode_found['displacement_real']
    k_real_scaled = mode_found['k_real'] * k_scaling
    k_imag_scaled = mode_found['k_imag'] * k_scaling
    
    # Create grid and attach displacement
    topology, cell_types, geometry = plot.vtk_mesh(mesh)
    grid = pv.UnstructuredGrid(topology, cell_types, geometry)
    grid["u"] = u_plot
    
    # Calculate displacement magnitude
    u_mag = np.linalg.norm(u_plot, axis=1)
    max_u = np.max(u_mag)
    
    # Normalize for colorbar
    if max_u > 0:
        u_mag_normalized = u_mag / max_u
    else:
        u_mag_normalized = u_mag
    
    grid["Normalized |u|"] = u_mag_normalized
    
    # Warp the mesh
    warped = grid.warp_by_vector("u", factor=VISUAL_SCALE)
    warped["Normalized |u|"] = u_mag_normalized
    
    # Create plotter
    p = pv.Plotter(window_size=(1000, 800))
    p.set_background('white')
    
    # Show undeformed mesh as wireframe
    p.add_mesh(grid, style="wireframe", color="lightgray", line_width=1)
    
    # Show deformed mesh colored by normalized displacement
    p.add_mesh(warped, scalars="Normalized |u|", cmap="plasma", show_scalar_bar=True,
               scalar_bar_args={
                   'title': 'Normalized |u|',
                   'n_labels': 5,
                   'fmt': '%.2f'
               })
    
    # Add text info
    p.add_text(f"Point {idx+1}: f = {f_actual:.1f} Hz\nk = {k_real_scaled:.4f} + {k_imag_scaled:.4f}i\nScale: {VISUAL_SCALE:.3f}x", 
               font_size=12, position='upper_left', color='black')
    
    p.show_axes()
    p.view_isometric()
    p.show()

print("\nDone! All mode shapes plotted.")

I would be very grateful for any advice on how to solve this problem!

/Nadine

If you have only rigid body movement, how would you have a nonzero eigenvalue?

This is an inverse problem where the eigenvalues are k (I solve for the the complex wavenumber rather than for frequencies). The rigid body motion should be a part of the mode shape with progression towards bending motion, but for this inverse problem it seems like the modes are not extracted correctly, so when I plot them, it appears that only rigid body motion is present. Solving for the exact same branch in the forward problem (when solving for frequencies instead) gives the correct mode shapes, starting with rigid body motion and then progressing along the branch to bending motion.

Maybe I should ask a more specific question. When you plug in the eigenmode to your operator, does it satisfy the eigenvalue equation?

Yes, and the eigenvalues are correct as well, since I’ve compared them to the results from the forward problem, which leads to the same dispersion plots (for Re(k)). It is just the mode shape plotting that is incorrect, which is why I assumed that the problem is in how the modes are extracted.

If it satifies the eigenproblem, then no it is not an extraction problem. It is either a) you understanding of what the mode should be, or b) your formulation of the eigen equation.

Here is a table comparing two mode shapes at about same frequencies, one being from the forward problem and the other from the inverse problem. At this frequency and k-value, I expect to have a mode shape looking like the one from the forward problem.

The two codes for extracting the eigenvectors are also shown here.

I tried changing the extraction code for the inverse problem by re-applying Bloch periodicity, and yielded better results for at least this frequency:

Still not sure if this is a correct solution as the other modes shapes are not yet completely similar to the forward problem.