Source code for vane.sysid.modal_to_ss

r"""Real modal state-space realization from modal parameters.

A set of under-damped modes is realized as a real, block-diagonal state-space
system. Each mode with eigenvalue :math:`\lambda = \sigma + j\omega` contributes a
``2x2`` block

.. math::

    \begin{bmatrix} \sigma & -\omega \\ \omega & \sigma \end{bmatrix},

whose eigenvalues are :math:`\sigma \pm j\omega`, acting on the real and imaginary
parts of the modal coordinate. When mode shapes are supplied, the output matrix
maps the modal states to physical responses via :math:`[\,2\operatorname{Re}\phi,
\,-2\operatorname{Im}\phi\,]`. The result is a compact reduced-order model of the
identified modes.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np
from scipy.linalg import block_diag

from vane.sysid.state_space import StateSpace

if TYPE_CHECKING:
    import numpy.typing as npt

    from vane.modal.eigensolver import ModalSolution

__all__ = ["modal_state_space", "modal_state_space_from_solution"]










def _modal_output_matrix(
    mode_shapes: npt.NDArray[np.complex128] | None, n_modes: int
) -> npt.NDArray[np.float64] | None:
    """Return the output matrix mapping modal states to physical responses."""
    if mode_shapes is None:
        return None
    n_outputs = mode_shapes.shape[0]
    c = np.zeros((n_outputs, 2 * n_modes), dtype=np.float64)
    for mode in range(n_modes):
        c[:, 2 * mode] = 2.0 * mode_shapes[:, mode].real
        c[:, 2 * mode + 1] = -2.0 * mode_shapes[:, mode].imag
    return c