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(
# array of mode numbers (integers)
mode_numbers: np.ndarray, float = 1.0, # total length of the string in meters
string_length: = None, # grid of points to evaluate the modes on
grid: np.ndarray -> 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’}
= create_modal_matrix(np.arange(1, 51), grid=np.linspace(0, 1, 100))
M 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(
# Amplitudes in the modal domain
modal_amplitudes: np.ndarray, # Modal shapes (eigenvectors)
modal_shapes: np.ndarray, float = 1.0, # Length of the string in meters
string_length: -> 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
= 2 / string_length
scaling_factor
# Multiply modal shapes by modal amplitudes and scale
= scaling_factor * modal_shapes @ modal_amplitudes
physical_displacement
return physical_displacement
def to_modal(
# Displacement at grid points
physical_displacement: np.ndarray, # Modal shapes (eigenvectors)
modal_shapes: np.ndarray, float = 1.0, # Length of the string in meters
string_length: int = 100, # Number of grid points
num_gridpoints: -> 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
= string_length / num_gridpoints
scaling_factor
# Multiply transpose of modal shapes by physical displacement and scale
= scaling_factor * modal_shapes.T @ physical_displacement
modal_amplitudes
return modal_amplitudes
:::
::: {#cell-9 .cell 0=‘e’ 1=‘x’ 2=‘p’ 3=‘o’ 4=‘r’ 5=‘t’}
def create_pluck_modal(
# array of mode numbers (integers)
mode_numbers: np.ndarray, float = 0.28, # position of pluck on the string in meters
pluck_position: float = 0.03, # initial deflection of the string in meters
initial_deflection: float = 1.0, # total length of the string in meters
string_length: -> 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
= mode_numbers * np.pi / string_length
wave_numbers
# Scaling factor for the initial deflection
= initial_deflection * (
deflection_scaling / (string_length - pluck_position)
string_length
)
# Compute the Fourier-Sine coefficients
= (
fourier_coefficients
deflection_scaling* np.sin(wave_numbers * pluck_position)
/ (wave_numbers * pluck_position)
)/= wave_numbers
fourier_coefficients
return fourier_coefficients
:::
::: {#cell-10 .cell 0=‘t’ 1=‘e’ 2=‘s’ 3=‘t’}
= 100
n_gridpoints = 1 / n_gridpoints # m spatial sampling interval
dx = np.arange(1, 50 + 1) # mode numbers
mu = 1.0 # m
length = np.arange(1, 50 + 1)
mode_numbers = np.arange(0, length, dx)
grid
= create_pluck_modal(
q
mode_numbers,=0.28,
pluck_position=0.03,
initial_deflection
)
= create_modal_matrix(mu, length, grid)
M
= to_displacement(q, M, length)
u = to_modal(u, M, length, n_gridpoints)
q = to_displacement(q, M, length)
u0_new assert np.allclose(u, u0_new, atol=1e-5)
:::