"""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)