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)

:::