Modal utils
Utils for converting between modal and physical coordinates.
::: {#cell-3 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’}
import numpy as np:::
The modal matrix $ ^{G K} $ represents the mode shapes of a system, with each column as a mode shape corresponding to a vibration mode. Wavenumbers $ ^K $ are given by $ k_= $ for modes $ $ in a system of length $ L $. The grid points $ ^G $ represent spatial discretization, with $ g_j = j x $.
The modal matrix is not a direct outer product of $ $ and $ $. Instead, it’s constructed by evaluating mode shapes at these grid points. For systems like vibrating strings, this often involves sinusoidal functions of $ k_$ and $ g_j $, but more complex systems may require detailed analysis or numerical methods.
::: {#cell-5 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’}
def create_modal_matrix(
mode_numbers: np.ndarray, # array of mode numbers (integers)
string_length: float = 1.0, # total length of the string in meters
grid: np.ndarray = None, # grid of points to evaluate the modes on
) -> np.ndarray:
"""
Creates a matrix with the modal shapes as columns.
:param mode_numbers: Array of mode numbers, representing different vibration modes.
:param string_length: Total length of the string.
:param grid: Grid of points to evaluate the modes on.
:return: Matrix with the modal shapes as columns (shape: (grid.size, mode_numbers.size)).
"""
return np.sin(np.outer(grid, mode_numbers * np.pi / string_length)):::
::: {#cell-6 .cell 0=‘t’ 1=‘e’ 2=‘s’ 3=‘t’}
M = create_modal_matrix(np.arange(1, 51), grid=np.linspace(0, 1, 100))
assert M.shape == (100, 50):::
To convert from the physical domain to the modal domain, we use the modal matrix \(\mathbf{M}\) and a scaling factor:
\[ \mathbf{u} = \frac{2}{l} \mathbf{M} \cdot \mathbf{q} \]
To convert from the modal domain to the physical domain, we use the inverse modal matrix \(\mathbf{M}^{-1}\) and a scaling factor. Since the modal matrix is orthogonal, the inverse is equal to the transpose:
\[ \mathbf{q} = \frac{l}{g} \mathbf{M}^T \cdot \mathbf{u} \]
where \(g\) is the number of grid points and \(l\) is the length of the string.
::: {#cell-8 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’}
def to_displacement(
modal_amplitudes: np.ndarray, # Amplitudes in the modal domain
modal_shapes: np.ndarray, # Modal shapes (eigenvectors)
string_length: float = 1.0, # Length of the string in meters
) -> np.ndarray:
"""
Convert modal amplitudes to physical displacement along the string.
:param modal_amplitudes: Array of amplitudes in the modal domain.
:param modal_shapes: Matrix of modal shapes (each column is a mode shape).
:param string_length: Length of the string.
:return: Array of physical displacements at the grid points.
"""
# Calculate scaling factor for modal shapes
scaling_factor = 2 / string_length
# Multiply modal shapes by modal amplitudes and scale
physical_displacement = scaling_factor * modal_shapes @ modal_amplitudes
return physical_displacement
def to_modal(
physical_displacement: np.ndarray, # Displacement at grid points
modal_shapes: np.ndarray, # Modal shapes (eigenvectors)
string_length: float = 1.0, # Length of the string in meters
num_gridpoints: int = 100, # Number of grid points
) -> np.ndarray:
"""
Convert physical displacement to modal amplitudes.
:param physical_displacement: Array of displacements at grid points.
:param modal_shapes: Matrix of modal shapes (each column is a mode shape).
:param string_length: Length of the string.
:param num_gridpoints: Number of grid points along the string.
:return: Array of amplitudes in the modal domain.
"""
# Calculate scaling factor for conversion to modal domain
scaling_factor = string_length / num_gridpoints
# Multiply transpose of modal shapes by physical displacement and scale
modal_amplitudes = scaling_factor * modal_shapes.T @ physical_displacement
return modal_amplitudes:::
::: {#cell-9 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’}
def create_pluck_modal(
mode_numbers: np.ndarray, # array of mode numbers (integers)
pluck_position: float = 0.28, # position of pluck on the string in meters
initial_deflection: float = 0.03, # initial deflection of the string in meters
string_length: float = 1.0, # total length of the string in meters
) -> np.ndarray:
"""
Calculate the Fourier-Sine coefficients of the initial deflection
of a plucked string in modal coordinates.
:param modes: Array of mode numbers, representing different vibration modes.
:param pluck_position: Position of the pluck on the string.
:param initial_deflection: Initial displacement of the string at the pluck position.
:param string_length: Total length of the string.
:return: Array of Fourier-Sine coefficients for each mode.
"""
# Calculate the wave number for each mode
wave_numbers = mode_numbers * np.pi / string_length
# Scaling factor for the initial deflection
deflection_scaling = initial_deflection * (
string_length / (string_length - pluck_position)
)
# Compute the Fourier-Sine coefficients
fourier_coefficients = (
deflection_scaling
* np.sin(wave_numbers * pluck_position)
/ (wave_numbers * pluck_position)
)
fourier_coefficients /= wave_numbers
return fourier_coefficients:::
::: {#cell-10 .cell 0=‘t’ 1=‘e’ 2=‘s’ 3=‘t’}
n_gridpoints = 100
dx = 1 / n_gridpoints # m spatial sampling interval
mu = np.arange(1, 50 + 1) # mode numbers
length = 1.0 # m
mode_numbers = np.arange(1, 50 + 1)
grid = np.arange(0, length, dx)
q = create_pluck_modal(
mode_numbers,
pluck_position=0.28,
initial_deflection=0.03,
)
M = create_modal_matrix(mu, length, grid)
u = to_displacement(q, M, length)
q = to_modal(u, M, length, n_gridpoints)
u0_new = to_displacement(q, M, length)
assert np.allclose(u, u0_new, atol=1e-5):::