Coverage for C: \ Users \ peaco \ OneDrive \ Documents \ GitHub \ mt_metadata \ mt_metadata \ features \ fc_coherence.py: 100%
37 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-10 00:11 -0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-10 00:11 -0800
1# =====================================================
2# Imports
3# =====================================================
4from typing import Annotated
6import numpy as np
7from pydantic import computed_field, Field, model_validator
9from mt_metadata.common.enumerations import StrEnumerationBase
10from mt_metadata.features.coherence import Coherence
11from mt_metadata.features.feature import Feature
14# =====================================================
15class BandDefinitionTypeEnum(StrEnumerationBase):
16 Q = "Q"
17 fractional_bandwidth = "fractional bandwidth"
18 user_defined = "user defined"
21class QRadiusEnum(StrEnumerationBase):
22 constant_Q = "constant Q"
23 user_defined = "user defined"
26class FCCoherence(Coherence, Feature):
27 minimum_fcs: Annotated[
28 int,
29 Field(
30 default=2,
31 description="The minimum number of Fourier coefficients needed to compute the feature.",
32 alias=None,
33 json_schema_extra={
34 "units": None,
35 "required": True,
36 "examples": ["2"],
37 },
38 ),
39 ]
41 band_definition_type: Annotated[
42 BandDefinitionTypeEnum,
43 Field(
44 default="Q",
45 description="How the feature frequency bands are defined.",
46 alias=None,
47 json_schema_extra={
48 "units": None,
49 "required": True,
50 "examples": ["user defined"],
51 },
52 ),
53 ]
55 q_radius: Annotated[
56 QRadiusEnum,
57 Field(
58 default="constant Q",
59 description="How the feature frequency bands are defined.",
60 alias=None,
61 json_schema_extra={
62 "units": None,
63 "required": True,
64 "examples": ["user defined"],
65 },
66 ),
67 ]
69 @model_validator(mode="before")
70 @classmethod
71 def set_defaults(cls, data: dict) -> dict:
72 data["name"] = "fc_coherence"
73 data["domain"] = "frequency"
74 data["description"] = (
75 "Magnitude-squared coherence computed from frequency-domain Fourier coefficients (FCs). "
76 "Cxy(f) = |Sxy(f)|^2 / (Sxx(f) * Syy(f)), where Sxy is the cross-power spectrum, "
77 "Sxx and Syy are auto-power spectra, all estimated by averaging over windows."
78 )
79 return data
81 @computed_field
82 @property
83 def channel_pair_str(self) -> str:
84 return f"{self.channel_1}, {self.channel_2}"
86 def compute(
87 self, fc1: np.ndarray, fc2: np.ndarray
88 ) -> tuple[np.ndarray | None, np.ndarray]:
89 """
90 Compute magnitude-squared coherence from FCs.
92 Parameters
93 ----------
94 fc1 : np.ndarray
95 Fourier coefficients for channel 1, shape (n_windows, n_freqs)
96 fc2 : np.ndarray
97 Fourier coefficients for channel 2, shape (n_windows, n_freqs)
99 Returns
100 -------
101 freqs : np.ndarray
102 Frequency axis (if available, else None)
103 coherence : np.ndarray
104 Magnitude-squared coherence, shape (n_freqs,)
105 """
106 # Cross-power and auto-powers
107 sxy = np.mean(fc1 * np.conj(fc2), axis=0)
108 sxx = np.mean(np.abs(fc1) ** 2, axis=0)
109 syy = np.mean(np.abs(fc2) ** 2, axis=0)
111 # Magnitude-squared coherence with protection against division by zero
112 denominator = sxx * syy
114 # Use numpy error handling to suppress division warnings
115 with np.errstate(divide="ignore", invalid="ignore"):
116 coherence = np.abs(sxy) ** 2 / denominator
118 # Replace any infinite or NaN values with 0
119 coherence = np.where(np.isfinite(coherence), coherence, 0.0)
120 return None, coherence