Source code for vane.modal.participation

"""Per-degree-of-freedom participation in each mode.

OpenFAST exposes no full-system mass matrix, so a classical mass-normalized modal
participation factor is unavailable. Instead, participation is quantified from the
right eigenvector: the magnitude of each state's component in a mode measures how
strongly that degree of freedom participates. Each mode is normalized so its
dominant component has magnitude one, and a sign is assigned from the phase of
each component relative to the dominant one (components more than 90 degrees out
of phase are negated), giving the signed contribution used for physical mode
identification.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING

import numpy as np

if TYPE_CHECKING:
    import numpy.typing as npt

    from vane.modal.eigensolver import ModalSolution

__all__ = ["ParticipationResult", "compute_participation", "participation_from_modes"]


[docs] @dataclass class ParticipationResult: """Per-state participation in each mode. Parameters ---------- magnitude : npt.NDArray[np.float64] Normalized participation magnitude, shape ``(n_dof, n_modes)``; each column's maximum is 1. signed_magnitude : npt.NDArray[np.float64] ``magnitude`` with the sign of components more than 90 degrees out of phase with the dominant component flipped negative. phase_deg : npt.NDArray[np.float64] Phase of each component in degrees, shape ``(n_dof, n_modes)``. dominant_state : npt.NDArray[np.int_] Index of the largest-magnitude state in each mode, shape ``(n_modes,)``. """ magnitude: npt.NDArray[np.float64] signed_magnitude: npt.NDArray[np.float64] phase_deg: npt.NDArray[np.float64] dominant_state: npt.NDArray[np.int_]
[docs] def compute_participation( mode_shapes: npt.NDArray[np.complex128], scale_factors: npt.NDArray[np.float64] | None = None, ) -> ParticipationResult: """Compute per-state participation from complex mode shapes. Parameters ---------- mode_shapes : npt.NDArray[np.complex128] Complex mode-shape matrix, shape ``(n_dof, n_modes)``. scale_factors : npt.NDArray[np.float64] or None, optional Per-DOF multipliers applied before normalization (e.g. ``1/blade_length`` for translational blade DOFs) so heterogeneous DOFs are comparable. When ``None``, no scaling is applied. Returns ------- ParticipationResult Normalized magnitudes, signed magnitudes, phases, and dominant states. Raises ------ ValueError If ``scale_factors`` length does not match the number of DOFs. """ phi = mode_shapes if scale_factors is not None: if scale_factors.shape[0] != phi.shape[0]: msg = ( f"scale_factors length {scale_factors.shape[0]} does not match " f"{phi.shape[0]} DOFs" ) raise ValueError(msg) phi = phi * scale_factors[:, np.newaxis] magnitude = np.abs(phi) n_dof, n_modes = phi.shape if n_dof == 0: empty = np.zeros((0, n_modes), dtype=np.float64) return ParticipationResult( magnitude=empty, signed_magnitude=empty, phase_deg=empty, dominant_state=np.zeros(n_modes, dtype=np.int_), ) column_max = magnitude.max(axis=0) with np.errstate(divide="ignore", invalid="ignore"): normalized = np.where(column_max > 0.0, magnitude / column_max, 0.0) phase = np.angle(phi, deg=True) dominant = np.argmax(magnitude, axis=0) dominant_phase = phase[dominant, np.arange(n_modes)] relative = np.mod(phase - dominant_phase[np.newaxis, :], 360.0) relative = np.where(relative > 180.0, relative - 360.0, relative) sign = np.where(np.abs(relative) > 90.0, -1.0, 1.0) return ParticipationResult( magnitude=np.asarray(normalized, dtype=np.float64), signed_magnitude=np.asarray(normalized * sign, dtype=np.float64), phase_deg=np.asarray(phase, dtype=np.float64), dominant_state=np.asarray(dominant, dtype=np.int_), )
[docs] def participation_from_modes( solution: ModalSolution, scale_factors: npt.NDArray[np.float64] | None = None, ) -> ParticipationResult: """Compute participation from a :class:`ModalSolution`'s mode shapes. Parameters ---------- solution : ModalSolution Modal solution whose ``mode_shapes`` are analysed. scale_factors : npt.NDArray[np.float64] or None, optional Per-DOF multipliers, as in :func:`compute_participation`. Returns ------- ParticipationResult The participation result for the solution's mode shapes. """ return compute_participation(solution.mode_shapes, scale_factors)