Source code for tidal.measurement._resonance

"""Resonance analysis for coupled-field systems.

Identifies resonant modes where ``omega_source(k) ~ omega_target(k)`` and
computes conversion bandwidth, peak conversion wavenumber, and the
adiabaticity parameter.

Requires flat + spatially homogeneous systems (inherits from dispersion).
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING

import numpy as np

from tidal.measurement._dispersion import DispersionResult, compute_dispersion
from tidal.measurement._mode_utils import find_shared_modes

if TYPE_CHECKING:
    from collections.abc import Sequence

    from numpy.typing import NDArray

    from tidal.measurement._io import SimulationData


[docs] @dataclass(frozen=True) class ResonanceResult: """Resonance analysis for coupled fields. Attributes ---------- wavenumbers : ndarray, shape ``(n_shared,)`` Shared wavenumber bins where both fields have active modes. omega_source : ndarray, shape ``(n_shared,)`` Source field frequencies at shared k-bins. omega_target : ndarray, shape ``(n_shared,)`` Target field frequencies at shared k-bins. resonance_mismatch : ndarray, shape ``(n_shared,)`` ``|omega_s - omega_t| / omega_avg`` per shared mode. resonant_modes : ndarray, shape ``(n_shared,)`` Boolean mask: which modes are near-resonant. n_resonant_modes : int Count of resonant modes. conversion_bandwidth : float FWHM of the resonance mismatch curve in k-space (0.0 if < 2 resonant). peak_conversion_k : float Wavenumber with minimum mismatch (closest to exact resonance). source_field : str Source field name. target_field : str Target field name. """ wavenumbers: NDArray[np.float64] omega_source: NDArray[np.float64] omega_target: NDArray[np.float64] resonance_mismatch: NDArray[np.float64] resonant_modes: NDArray[np.bool_] n_resonant_modes: int conversion_bandwidth: float peak_conversion_k: float source_field: str target_field: str
def _compute_bandwidth( wavenumbers: NDArray[np.float64], mismatch: NDArray[np.float64], ) -> float: """Compute FWHM of the resonance window in k-space. The "resonance window" is defined by inverting the mismatch: regions of low mismatch are the resonance peak. We find the width at half-minimum. """ if len(wavenumbers) < 2: # noqa: PLR2004 return 0.0 # Invert: peak where mismatch is minimum min_mismatch = mismatch.min() max_mismatch = mismatch.max() if max_mismatch <= min_mismatch: return 0.0 half_level = (min_mismatch + max_mismatch) / 2.0 # Find where mismatch crosses half_level (going from below to above) below_half = mismatch < half_level if not np.any(below_half): return 0.0 k_below = wavenumbers[below_half] return float(k_below[-1] - k_below[0]) def _find_shared_modes( disp_src: DispersionResult, disp_tgt: DispersionResult, ) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]: """Find wavenumber bins where both fields have active modes. Returns ------- tuple (shared_k, omega_source, omega_target) arrays. Raises ------ ValueError If no shared active modes found. """ active_src = disp_src.peak_frequencies > 0.0 active_tgt = disp_tgt.peak_frequencies > 0.0 idx_src, idx_tgt = find_shared_modes( disp_src.wavenumbers, disp_tgt.wavenumbers, active_src, active_tgt, ) if not idx_src: msg = "No shared active wavenumber modes between source and target fields" raise ValueError(msg) return ( disp_src.wavenumbers[idx_src], disp_src.peak_frequencies[idx_src], disp_tgt.peak_frequencies[idx_tgt], )
[docs] def compute_resonance_analysis( data: SimulationData, source_field: str | Sequence[str], target_field: str | Sequence[str], *, resonance_threshold: float = 0.1, min_amplitude: float = 1e-12, ) -> ResonanceResult: """Identify resonant modes and compute conversion bandwidth. A mode k is resonant when ``|omega_source(k) - omega_target(k)| / omega_avg(k) < threshold``. Parameters ---------- data : SimulationData Simulation output. source_field : str or sequence of str Source field name(s). target_field : str or sequence of str Target field name(s). resonance_threshold : float, optional Relative frequency mismatch threshold for resonance. min_amplitude : float, optional Minimum Fourier amplitude for active modes. Returns ------- ResonanceResult """ disp_src = compute_dispersion(data, source_field, min_amplitude=min_amplitude) disp_tgt = compute_dispersion(data, target_field, min_amplitude=min_amplitude) shared_k, omega_s, omega_t = _find_shared_modes(disp_src, disp_tgt) # Relative frequency mismatch omega_avg = (omega_s + omega_t) / 2.0 safe_avg = np.where(omega_avg > 0, omega_avg, 1.0) mismatch = np.abs(omega_s - omega_t) / safe_avg resonant = mismatch < resonance_threshold peak_k = float(shared_k[int(np.argmin(mismatch))]) bandwidth = _compute_bandwidth(shared_k, mismatch) src_name = ( source_field if isinstance(source_field, str) else ", ".join(source_field) ) tgt_name = ( target_field if isinstance(target_field, str) else ", ".join(target_field) ) return ResonanceResult( wavenumbers=shared_k, omega_source=omega_s, omega_target=omega_t, resonance_mismatch=mismatch, resonant_modes=resonant, n_resonant_modes=int(resonant.sum()), conversion_bandwidth=bandwidth, peak_conversion_k=peak_k, source_field=src_name, target_field=tgt_name, )