mt-metadata 0.3.8__py2.py3-none-any.whl → 0.4.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mt-metadata might be problematic. Click here for more details.

Files changed (97) hide show
  1. mt_metadata/__init__.py +1 -1
  2. mt_metadata/base/helpers.py +84 -9
  3. mt_metadata/base/metadata.py +137 -65
  4. mt_metadata/data/transfer_functions/test.edi +28 -28
  5. mt_metadata/features/__init__.py +14 -0
  6. mt_metadata/features/coherence.py +303 -0
  7. mt_metadata/features/cross_powers.py +29 -0
  8. mt_metadata/features/fc_coherence.py +81 -0
  9. mt_metadata/features/feature.py +72 -0
  10. mt_metadata/features/feature_decimation_channel.py +26 -0
  11. mt_metadata/features/feature_fc.py +24 -0
  12. mt_metadata/{transfer_functions/processing/aurora/decimation.py → features/feature_fc_run.py} +9 -4
  13. mt_metadata/features/feature_ts.py +24 -0
  14. mt_metadata/{transfer_functions/processing/aurora/window.py → features/feature_ts_run.py} +11 -18
  15. mt_metadata/features/standards/__init__.py +6 -0
  16. mt_metadata/features/standards/base_feature.json +46 -0
  17. mt_metadata/features/standards/coherence.json +57 -0
  18. mt_metadata/features/standards/fc_coherence.json +57 -0
  19. mt_metadata/features/standards/feature_decimation_channel.json +68 -0
  20. mt_metadata/features/standards/feature_fc_run.json +35 -0
  21. mt_metadata/features/standards/feature_ts_run.json +35 -0
  22. mt_metadata/features/standards/feature_weighting_window.json +46 -0
  23. mt_metadata/features/standards/weight_kernel.json +46 -0
  24. mt_metadata/features/standards/weights.json +101 -0
  25. mt_metadata/features/test_helpers/channel_weight_specs_example.json +156 -0
  26. mt_metadata/features/weights/__init__.py +0 -0
  27. mt_metadata/features/weights/base.py +44 -0
  28. mt_metadata/features/weights/channel_weight_spec.py +209 -0
  29. mt_metadata/features/weights/feature_weight_spec.py +194 -0
  30. mt_metadata/features/weights/monotonic_weight_kernel.py +275 -0
  31. mt_metadata/features/weights/standards/__init__.py +6 -0
  32. mt_metadata/features/weights/standards/activation_monotonic_weight_kernel.json +38 -0
  33. mt_metadata/features/weights/standards/base.json +36 -0
  34. mt_metadata/features/weights/standards/channel_weight_spec.json +35 -0
  35. mt_metadata/features/weights/standards/composite.json +36 -0
  36. mt_metadata/features/weights/standards/feature_weight_spec.json +13 -0
  37. mt_metadata/features/weights/standards/monotonic_weight_kernel.json +49 -0
  38. mt_metadata/features/weights/standards/taper_monotonic_weight_kernel.json +16 -0
  39. mt_metadata/features/weights/taper_weight_kernel.py +60 -0
  40. mt_metadata/helper_functions.py +69 -0
  41. mt_metadata/timeseries/filters/channel_response.py +77 -37
  42. mt_metadata/timeseries/filters/coefficient_filter.py +6 -5
  43. mt_metadata/timeseries/filters/filter_base.py +11 -15
  44. mt_metadata/timeseries/filters/fir_filter.py +8 -1
  45. mt_metadata/timeseries/filters/frequency_response_table_filter.py +26 -11
  46. mt_metadata/timeseries/filters/helper_functions.py +0 -2
  47. mt_metadata/timeseries/filters/obspy_stages.py +4 -1
  48. mt_metadata/timeseries/filters/pole_zero_filter.py +9 -5
  49. mt_metadata/timeseries/filters/time_delay_filter.py +8 -1
  50. mt_metadata/timeseries/location.py +20 -5
  51. mt_metadata/timeseries/person.py +14 -7
  52. mt_metadata/timeseries/standards/person.json +1 -1
  53. mt_metadata/timeseries/standards/run.json +2 -2
  54. mt_metadata/timeseries/station.py +4 -2
  55. mt_metadata/timeseries/stationxml/__init__.py +5 -0
  56. mt_metadata/timeseries/stationxml/xml_channel_mt_channel.py +38 -27
  57. mt_metadata/timeseries/stationxml/xml_inventory_mt_experiment.py +16 -47
  58. mt_metadata/timeseries/stationxml/xml_station_mt_station.py +25 -24
  59. mt_metadata/transfer_functions/__init__.py +3 -0
  60. mt_metadata/transfer_functions/core.py +16 -11
  61. mt_metadata/transfer_functions/io/emtfxml/metadata/location.py +5 -0
  62. mt_metadata/transfer_functions/io/emtfxml/metadata/provenance.py +14 -3
  63. mt_metadata/transfer_functions/io/tools.py +2 -0
  64. mt_metadata/transfer_functions/io/zonge/metadata/header.py +1 -1
  65. mt_metadata/transfer_functions/io/zonge/metadata/standards/header.json +1 -1
  66. mt_metadata/transfer_functions/io/zonge/metadata/standards/job.json +2 -2
  67. mt_metadata/transfer_functions/io/zonge/zonge.py +19 -23
  68. mt_metadata/transfer_functions/processing/__init__.py +2 -1
  69. mt_metadata/transfer_functions/processing/aurora/__init__.py +2 -4
  70. mt_metadata/transfer_functions/processing/aurora/band.py +46 -125
  71. mt_metadata/transfer_functions/processing/aurora/channel_nomenclature.py +27 -20
  72. mt_metadata/transfer_functions/processing/aurora/decimation_level.py +324 -152
  73. mt_metadata/transfer_functions/processing/aurora/frequency_bands.py +230 -0
  74. mt_metadata/transfer_functions/processing/aurora/processing.py +3 -3
  75. mt_metadata/transfer_functions/processing/aurora/run.py +32 -7
  76. mt_metadata/transfer_functions/processing/aurora/standards/decimation_level.json +7 -73
  77. mt_metadata/transfer_functions/processing/aurora/stations.py +33 -4
  78. mt_metadata/transfer_functions/processing/fourier_coefficients/decimation.py +176 -177
  79. mt_metadata/transfer_functions/processing/fourier_coefficients/fc.py +11 -9
  80. mt_metadata/transfer_functions/processing/fourier_coefficients/standards/decimation.json +1 -111
  81. mt_metadata/transfer_functions/processing/short_time_fourier_transform.py +64 -0
  82. mt_metadata/transfer_functions/processing/standards/__init__.py +6 -0
  83. mt_metadata/transfer_functions/processing/standards/short_time_fourier_transform.json +94 -0
  84. mt_metadata/transfer_functions/processing/{aurora/standards/decimation.json → standards/time_series_decimation.json} +17 -6
  85. mt_metadata/transfer_functions/processing/{aurora/standards → standards}/window.json +13 -2
  86. mt_metadata/transfer_functions/processing/time_series_decimation.py +50 -0
  87. mt_metadata/transfer_functions/processing/window.py +118 -0
  88. mt_metadata/transfer_functions/tf/standards/transfer_function.json +1 -1
  89. mt_metadata/transfer_functions/tf/station.py +17 -1
  90. mt_metadata/utils/mttime.py +22 -3
  91. mt_metadata/utils/validators.py +4 -2
  92. {mt_metadata-0.3.8.dist-info → mt_metadata-0.4.0.dist-info}/METADATA +39 -15
  93. {mt_metadata-0.3.8.dist-info → mt_metadata-0.4.0.dist-info}/RECORD +97 -57
  94. {mt_metadata-0.3.8.dist-info → mt_metadata-0.4.0.dist-info}/WHEEL +1 -1
  95. {mt_metadata-0.3.8.dist-info → mt_metadata-0.4.0.dist-info}/AUTHORS.rst +0 -0
  96. {mt_metadata-0.3.8.dist-info → mt_metadata-0.4.0.dist-info}/LICENSE +0 -0
  97. {mt_metadata-0.3.8.dist-info → mt_metadata-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,303 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Fri Jan 31 13:39:39 2025
4
+
5
+ @author: jpeacock
6
+
7
+ This module contains the simplest coherence feature.
8
+ The feature is computed with scipy.signal coherence.
9
+
10
+ Note that this coherence is one number for the entire time-series (per frequency), i.e.
11
+
12
+ The Window object is used to taper the time series before FFT.
13
+
14
+ Development Notes:
15
+ Coherence extends the Feature class. This means that it should have
16
+ all the attrs that a Feature instance does, as well as its own unique ones.
17
+ When setting up the attr_dict, one is confronted with the question of adding
18
+ BaseFeatures attrs one of two ways:
19
+ - To add the features directly, use:
20
+ attr_dict.add_dict(get_schema("base_feature", SCHEMA_FN_PATHS))
21
+ {
22
+ "coherence": {
23
+ "ch1": "ex",
24
+ "ch2": "hy",
25
+ "description": "Simple coherence between two channels derived directly from scipy.signal.coherence applied to time domain data",
26
+ "domain": "frequency",
27
+ "name": "coherence",
28
+ "window.clock_zero_type": "ignore",
29
+ "window.normalized": true,
30
+ "window.num_samples": 512,
31
+ "window.overlap": 128,
32
+ "window.type": "hamming"
33
+ }
34
+ }
35
+ - To nest the features use:
36
+ attr_dict.add_dict(BaseFeature()._attr_dict, "base_feature")
37
+ {
38
+ "coherence": {
39
+ "base_feature.description": null,
40
+ "base_feature.domain": null,
41
+ "base_feature.name": null,
42
+ "ch1": "ex",
43
+ "ch2": "hy",
44
+ "window.clock_zero_type": "ignore",
45
+ "window.normalized": true,
46
+ "window.num_samples": 512,
47
+ "window.overlap": 128,
48
+ "window.type": "hamming"
49
+ }
50
+ }
51
+
52
+ Devlopment Notes:
53
+ To specify a channel in the context of tf processing we need station and channel names.
54
+ I have been fighting the use of `rx` and `ry` for several reasons, including that the [ex, ey, hx, hy, hz, rx, ry]
55
+ convention forces the assumption that remote channels are remote magnetics, and are overly specific to the remote
56
+ reference processing convention.
57
+ Hoever, for a feature like this, it could seem to be a hassle to update the processing config with the station name all over the
58
+ feature definations. So, it seems that we should have a station field, and a channel field.
59
+ If the user wishes to specify station and channel, fine. If the user prefers the more general,
60
+ but less well defined [ex, ey, hx, hy, hz, rx, ry] nomenclature, then we can ddeduce this for them.
61
+
62
+ Development Note (2025-05-24):
63
+ Note that the simple coherence as computed here, just returns one number per frequency.
64
+ It is the average coherence over the entire run, and is not innately a "per-time-window feature".
65
+
66
+ To make it a per-time-window feature, we need to apply the transform on individual windows (not the whole run).
67
+ i.e. chunk a run into sub-windows, and then compute coherence on each of those individually. To accomplish this
68
+ we must shorten the window.num_samples to be smaller than the sub-window size, otherwise, coherence
69
+ degenerates to 1 everywhere. (Recall coherenc is th average cross-power over average sqrt auto-powers, and having
70
+ only one spectral estimate means there is no averaging).
71
+ Selection of an appropriate "window-within-the-sub-window" for spectral esimation comes with some caveats;
72
+
73
+ The length of the window-within-the-window must be small enough to get at least a few)
74
+ spectral estimates, meaning that the frequency content will not mirror that of the FFT
75
+ That said, we can know our lowest frequency of TF estimation (usually no fewer than 5 cycles),
76
+ so we could set the window-within-window width to be, say 1/5 the FFT window length, and then we'll get
77
+ something we can use, although it will be somwhat unrealiable at long period (but so is everything else:/).
78
+ Note that when we are using long FFT windows (such as for HF data processing) this is not such a concern
79
+
80
+ Way Forward: A "StridingWindowCoherence" (effectively a spectrogram of coherence) can be an extension of the
81
+ Cohernece feature. It will have the same properties, but will also have a "SubWindow". The SubWindow will be
82
+ another window function object, but it can be parameterized, for example, as a fraction of the
83
+ "Spectrogram Sliding Window".
84
+
85
+ The compute function could possibly be done by computing Coherence on each sub-window (kinda elegant
86
+ but may wind up being a bit slow with all the for-looping)
87
+
88
+ """
89
+
90
+ # =============================================================================
91
+ # Imports
92
+ # =============================================================================
93
+ from loguru import logger
94
+ from mt_metadata.base.helpers import write_lines
95
+ from mt_metadata.base import get_schema
96
+ from mt_metadata.transfer_functions.processing.window import Window
97
+ from mt_metadata.features.feature import Feature
98
+ from .standards import SCHEMA_FN_PATHS
99
+ from typing import Optional, Tuple
100
+
101
+ import numpy as np
102
+ import scipy.signal as ssig
103
+
104
+ # =============================================================================
105
+ attr_dict = get_schema("coherence", SCHEMA_FN_PATHS)
106
+ # attr_dict.add_dict(get_schema("feature", SCHEMA_FN_PATHS))
107
+ # attr_dict.add_dict(BaseFeature()._attr_dict, "base_feature")
108
+ attr_dict.add_dict(Window()._attr_dict, "window")
109
+
110
+ # Set the defaults for the coherence calculation parameters to scipys defaults
111
+ DEFAULT_SCIPY_WINDOW = Window()
112
+ DEFAULT_SCIPY_WINDOW.type = "hann"
113
+ DEFAULT_SCIPY_WINDOW.num_samples = 256
114
+ DEFAULT_SCIPY_WINDOW.overlap = 128
115
+
116
+
117
+ # =============================================================================
118
+ class Coherence(Feature):
119
+ __doc__ = write_lines(attr_dict)
120
+
121
+ def __init__(self, **kwargs):
122
+ self.window = Window()
123
+ self._detrend = None
124
+ self.station1 = ""
125
+ self.station2 = ""
126
+ Feature.__init__(self, **kwargs) # attr_dict=attr_dict,
127
+ self._attr_dict = attr_dict
128
+ self.name = "coherence"
129
+ self.domain = "frequency"
130
+ self.description = "Simple coherence between two channels derived " \
131
+ "directly from scipy.signal.coherence applied to " \
132
+ "time domain data"
133
+ self.window = DEFAULT_SCIPY_WINDOW
134
+
135
+ @property
136
+ def detrend(self):
137
+ return self._detrend
138
+
139
+ @detrend.setter
140
+ def detrend(self, value):
141
+ self._detrend = value
142
+
143
+ @property
144
+ def channel_pair_str(self) -> str:
145
+ return f"{self.ch1}, {self.ch2}"
146
+
147
+ def validate_station_ids(
148
+ self,
149
+ local_station_id: str,
150
+ remote_station_id: Optional[str] = None
151
+ ) -> None:
152
+ """
153
+ Make sure that ch1, ch2 are unambiguous.
154
+
155
+ Ideally the station for each channel is specified, but if not,
156
+ try deducing the channel.
157
+
158
+ Parameters
159
+ ----------
160
+ local_station_id: str
161
+ The name of the local station for a TF calculation
162
+ remote_station_id: Optional[str]
163
+ The name of the remote station for a TF calculation
164
+
165
+ """
166
+
167
+ # validate the station names:
168
+ active_stations = [local_station_id]
169
+ if remote_station_id:
170
+ active_stations.append(remote_station_id)
171
+
172
+ # if the feature has a station1, check that it is in the list of active stations
173
+ if self.station1: # not "" or None
174
+ if self.station1 not in active_stations:
175
+ msg = f"station1 not in expected stations -- setting to None"
176
+ logger.warning(msg)
177
+ self.station1 = None
178
+
179
+ if self.station2: # not "" or None
180
+ if self.station2 not in active_stations:
181
+ msg = f"station1 not in expected stations -- setting to None"
182
+ logger.warning(msg)
183
+ self.station2 = None
184
+
185
+ if not self.station1:
186
+ if self.ch1[0].lower() != "r":
187
+ self.station1 = local_station_id
188
+ else:
189
+ self.station1 = remote_station_id
190
+
191
+ if not self.station2:
192
+ if self.ch2[0].lower() != "r":
193
+ self.station2 = local_station_id
194
+ else:
195
+ self.station2 = remote_station_id
196
+
197
+ # by this time, all stations should be set. Confirm that we do not have a station that is None
198
+ # TODO Consier returning False if exception encountered here.
199
+ try:
200
+ assert self.station1 is not None
201
+ except Exception as e:
202
+ msg = "station1 is not set -- perhaps it was set to a remote that does not exist?"
203
+ logger.error(msg)
204
+ try:
205
+ assert self.station2 is not None
206
+ except Exception as e:
207
+ msg = "station2 is not set -- perhaps it was set to a remote that does not exist?"
208
+ logger.error(msg)
209
+
210
+ def compute(
211
+ self,
212
+ ts_1: np.ndarray,
213
+ ts_2: np.ndarray
214
+ ) -> Tuple[np.ndarray]:
215
+ """
216
+ Calls scipy's coherence function.
217
+ TODO: Consider making this return an xarray indexed by frequency.
218
+
219
+ Parameters
220
+ ----------
221
+ ts_1
222
+ ts_2
223
+
224
+ Returns
225
+ -------
226
+
227
+ """
228
+ frequencies, coh_squared = ssig.coherence(
229
+ ts_1,
230
+ ts_2,
231
+ window=self.window.type,
232
+ nperseg=self.window.num_samples,
233
+ noverlap=self.window.overlap,
234
+ detrend=self.detrend,
235
+ )
236
+ return frequencies, coh_squared
237
+
238
+
239
+ class StridingWindowCoherence(Coherence):
240
+ """
241
+ Computes coherence for each sub-window (FFT window) across the time series.
242
+ Returns a 2D array: (window index, frequency).
243
+ """
244
+ def __init__(self, subwindow=None, stride=None, **kwargs):
245
+ """
246
+ Parameters
247
+ ----------
248
+ subwindow : Window, optional
249
+ The window object used for the subwindow (the window used for the coherence calculation within each main window).
250
+ If not provided, a default Window is used.
251
+ stride : int, optional [DEPRECATED]
252
+ (Deprecated; use self.window.num_samples_advance instead.)
253
+ The stride (in samples) between the start of each main window (of length self.window.num_samples) as the main window
254
+ slides across the time series. If not provided, defaults to self.window.num_samples_advance.
255
+ kwargs : dict
256
+ Additional keyword arguments passed to the Coherence base class.
257
+ """
258
+ super().__init__(**kwargs)
259
+ self.name = "striding_window_coherence"
260
+ self.subwindow = subwindow if subwindow is not None else Window()
261
+ # Use window.num_samples_advance for main window stride
262
+ if stride is not None:
263
+ self._main_stride = int(stride)
264
+ else:
265
+ self._main_stride = self.window.num_samples_advance
266
+
267
+ def set_subwindow_from_window(self, fraction=0.2):
268
+ """
269
+ Set the subwindow as a fraction of the main window.
270
+ """
271
+ self.subwindow = Window()
272
+ self.subwindow.type = self.window.type
273
+ self.subwindow.num_samples = int(self.window.num_samples * fraction)
274
+ self.subwindow.overlap = int(self.subwindow.num_samples // 2)
275
+ # No need to update stride; main window stride is set by self.window.num_samples_advance
276
+
277
+ def compute(self, ts_1: np.ndarray, ts_2: np.ndarray):
278
+ """
279
+ For each main window (length self.window.num_samples, stride self.window.num_samples_advance),
280
+ compute coherence using the subwindow parameters (self.subwindow) within that main window.
281
+ Returns:
282
+ frequencies: 1D array of frequencies
283
+ coherences: 2D array (n_main_windows, n_frequencies)
284
+ """
285
+ n = len(ts_1)
286
+ main_win_len = self.window.num_samples
287
+ main_stride = self.window.num_samples_advance if hasattr(self.window, 'num_samples_advance') else main_win_len
288
+ results = []
289
+ for start in range(0, n - main_win_len + 1, main_stride):
290
+ end = start + main_win_len
291
+ seg1 = ts_1[start:end]
292
+ seg2 = ts_2[start:end]
293
+ f, coh = ssig.coherence(
294
+ seg1,
295
+ seg2,
296
+ window=self.subwindow.type,
297
+ nperseg=self.subwindow.num_samples,
298
+ noverlap=self.subwindow.overlap,
299
+ detrend=self.detrend,
300
+ )
301
+ results.append(coh)
302
+ return f, np.array(results)
303
+
@@ -0,0 +1,29 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Stub for CrossPowers feature.
4
+ """
5
+
6
+ from mt_metadata.features.feature import Feature
7
+
8
+ class CrossPowers(Feature):
9
+ """
10
+ Stub feature class for cross powers.
11
+ """
12
+ def __init__(self, **kwargs):
13
+ super().__init__(**kwargs)
14
+ self.name = "cross_powers"
15
+ self.add_base_attribute(
16
+ "name",
17
+ "cross_powers",
18
+ {
19
+ "type": str,
20
+ "required": True,
21
+ "style": "free form",
22
+ "description": "Name of the feature",
23
+ "units": None,
24
+ "options": [],
25
+ "alias": [],
26
+ "example": "cross_powers",
27
+ "default": "cross_powers",
28
+ },
29
+ )
@@ -0,0 +1,81 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+
4
+ This is a placeholder for FCCoherence feature class, which computes the magnitude-squared coherence
5
+ from frequency-domain Fourier coefficients (FCs). It is a work in progress and will be
6
+ implemented in the future.
7
+ """
8
+
9
+ # =============================================================================
10
+ # Imports
11
+ # =============================================================================
12
+ from mt_metadata.base.helpers import write_lines
13
+ from mt_metadata.base import get_schema, Base
14
+ from .standards import SCHEMA_FN_PATHS
15
+ from ..transfer_functions.processing.window import Window
16
+ from typing import Tuple
17
+
18
+ import numpy as np
19
+ import scipy.signal as ssig
20
+
21
+ # =============================================================================
22
+ attr_dict = get_schema("feature", SCHEMA_FN_PATHS)
23
+ attr_dict.add_dict(get_schema("coherence", SCHEMA_FN_PATHS), None)
24
+ attr_dict.add_dict(Window()._attr_dict, "window")
25
+
26
+
27
+ # =============================================================================
28
+ class FCCoherence(Base):
29
+ """
30
+ Computes magnitude-squared coherence from frequency-domain Fourier coefficients (FCs).
31
+
32
+ Given two sets of FCs (complex arrays, shape: [n_windows, n_freqs]), computes:
33
+ Cxy(f) = |Sxy(f)|^2 / (Sxx(f) * Syy(f))
34
+ where:
35
+ Sxy(f) = mean(FC1(f) * conj(FC2(f)), axis=0) # cross-power
36
+ Sxx(f) = mean(|FC1(f)|^2, axis=0) # auto-power
37
+ Syy(f) = mean(|FC2(f)|^2, axis=0) # auto-power
38
+ """
39
+ __doc__ = write_lines(attr_dict)
40
+
41
+ def __init__(self, **kwargs):
42
+ self.channel_1 = None
43
+ self.channel_2 = None
44
+ super().__init__(attr_dict=attr_dict, **kwargs)
45
+ self.name = "fc_coherence"
46
+ self.domain = "frequency"
47
+ self.description = (
48
+ "Magnitude-squared coherence computed from frequency-domain Fourier coefficients (FCs). "
49
+ "Cxy(f) = |Sxy(f)|^2 / (Sxx(f) * Syy(f)), where Sxy is the cross-power spectrum, "
50
+ "Sxx and Syy are auto-power spectra, all estimated by averaging over windows."
51
+ )
52
+
53
+ @property
54
+ def channel_pair_str(self) -> str:
55
+ return f"{self.channel_1}, {self.channel_2}"
56
+
57
+ def compute(self, fc1: np.ndarray, fc2: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
58
+ """
59
+ Compute magnitude-squared coherence from FCs.
60
+
61
+ Parameters
62
+ ----------
63
+ fc1 : np.ndarray
64
+ Fourier coefficients for channel 1, shape (n_windows, n_freqs)
65
+ fc2 : np.ndarray
66
+ Fourier coefficients for channel 2, shape (n_windows, n_freqs)
67
+
68
+ Returns
69
+ -------
70
+ freqs : np.ndarray
71
+ Frequency axis (if available, else None)
72
+ coherence : np.ndarray
73
+ Magnitude-squared coherence, shape (n_freqs,)
74
+ """
75
+ # Cross-power and auto-powers
76
+ sxy = np.mean(fc1 * np.conj(fc2), axis=0)
77
+ sxx = np.mean(np.abs(fc1) ** 2, axis=0)
78
+ syy = np.mean(np.abs(fc2) ** 2, axis=0)
79
+ # Magnitude-squared coherence
80
+ coherence = np.abs(sxy) ** 2 / (sxx * syy)
81
+ return None, coherence
@@ -0,0 +1,72 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Fri Jan 31 13:39:39 2025
4
+
5
+ @author: jpeacock
6
+ """
7
+
8
+ # =============================================================================
9
+ # Imports
10
+ # =============================================================================
11
+ from mt_metadata.base.helpers import write_lines
12
+ from mt_metadata.base import get_schema, Base
13
+ from .standards import SCHEMA_FN_PATHS
14
+
15
+ import xarray as xr
16
+ import numpy as np
17
+
18
+ # =============================================================================
19
+ attr_dict = get_schema("base_feature", SCHEMA_FN_PATHS)
20
+
21
+ # =============================================================================
22
+ class Feature(Base):
23
+ __doc__ = write_lines(attr_dict)
24
+
25
+ def __init__(self, **kwargs):
26
+ super().__init__(attr_dict=attr_dict, **kwargs)
27
+ self._data = None
28
+ self._supported_features = self._make_supported_features_dict()
29
+
30
+ @staticmethod
31
+ def _make_supported_features_dict():
32
+ from mt_metadata.features.coherence import Coherence
33
+ from mt_metadata.features.coherence import StridingWindowCoherence
34
+ from mt_metadata.features.cross_powers import CrossPowers
35
+ from mt_metadata.features.feature_ts import FeatureTS
36
+ from mt_metadata.features.feature_fc import FeatureFC
37
+ SUPPORTED_FEATURE_DICT = {}
38
+ SUPPORTED_FEATURE_DICT["coherence"] = Coherence
39
+ SUPPORTED_FEATURE_DICT["striding_window_coherence"] = StridingWindowCoherence
40
+ SUPPORTED_FEATURE_DICT["cross_powers"] = CrossPowers
41
+ SUPPORTED_FEATURE_DICT["feature_ts"] = FeatureTS
42
+ SUPPORTED_FEATURE_DICT["feature_fc"] = FeatureFC
43
+ return SUPPORTED_FEATURE_DICT
44
+
45
+ @classmethod
46
+ def from_feature_id(cls, meta_dict):
47
+ """
48
+ Factory: instantiate the correct feature class based on 'feature_id'.
49
+ """
50
+ if "feature_id" not in meta_dict:
51
+ raise KeyError("Feature metadata must include 'feature_id'.")
52
+ feature_id = meta_dict["feature_id"]
53
+ supported = cls._make_supported_features_dict()
54
+ if feature_id not in supported:
55
+ raise KeyError(f"Unknown feature_id '{feature_id}'. Supported: {list(supported.keys())}")
56
+ feature_cls = supported[feature_id]
57
+ obj = feature_cls()
58
+ obj.from_dict(meta_dict)
59
+ return obj
60
+
61
+ @property
62
+ def data(self):
63
+ return self._data
64
+
65
+ @data.setter
66
+ def data(self, value):
67
+ if not isinstance(value, (xr.DataArray, xr.Dataset, np.ndarray)):
68
+ raise TypeError("Data must be a numpy array or xarray.")
69
+ self._data = value
70
+
71
+
72
+
@@ -0,0 +1,26 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Fri Feb 25 15:20:59 2022
4
+
5
+ @author: jpeacock
6
+ """
7
+ # =============================================================================
8
+ # Imports
9
+ # =============================================================================
10
+ from mt_metadata.base.helpers import write_lines
11
+ from mt_metadata.base import get_schema, Base
12
+ from mt_metadata.timeseries import TimePeriod
13
+ from .standards import SCHEMA_FN_PATHS
14
+
15
+ # =============================================================================
16
+ attr_dict = get_schema("feature_decimation_channel", SCHEMA_FN_PATHS)
17
+ attr_dict.add_dict(TimePeriod()._attr_dict, "time_period")
18
+
19
+
20
+ # =============================================================================
21
+ class FeatureDecimationChannel(Base):
22
+ __doc__ = write_lines(attr_dict)
23
+
24
+ def __init__(self, **kwargs):
25
+ self.time_period = TimePeriod()
26
+ super().__init__(attr_dict=attr_dict, **kwargs)
@@ -0,0 +1,24 @@
1
+ from mt_metadata.features.feature import Feature
2
+
3
+ class FeatureFC(Feature):
4
+ """
5
+ Stub feature class for feature_fc.
6
+ """
7
+ def __init__(self, **kwargs):
8
+ super().__init__(**kwargs)
9
+ self.name = "feature_fc"
10
+ self.add_base_attribute(
11
+ "name",
12
+ "feature_fc",
13
+ {
14
+ "type": str,
15
+ "required": True,
16
+ "style": "free form",
17
+ "description": "Name of the feature (feature_fc)",
18
+ "units": None,
19
+ "options": [],
20
+ "alias": [],
21
+ "example": "feature_fc",
22
+ "default": "feature_fc",
23
+ },
24
+ )
@@ -1,9 +1,10 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """
3
- Created on Thu Feb 17 14:15:20 2022
3
+ Created on Fri Jan 31 13:39:39 2025
4
4
 
5
5
  @author: jpeacock
6
6
  """
7
+
7
8
  # =============================================================================
8
9
  # Imports
9
10
  # =============================================================================
@@ -11,14 +12,18 @@ from mt_metadata.base.helpers import write_lines
11
12
  from mt_metadata.base import get_schema, Base
12
13
  from .standards import SCHEMA_FN_PATHS
13
14
 
15
+ from mt_metadata.timeseries import TimePeriod
16
+
14
17
  # =============================================================================
15
- attr_dict = get_schema("decimation", SCHEMA_FN_PATHS)
16
- # =============================================================================
18
+ attr_dict = get_schema("feature_fc_run", SCHEMA_FN_PATHS)
19
+ attr_dict.add_dict(TimePeriod()._attr_dict, "time_period")
17
20
 
18
21
 
19
- class Decimation(Base):
22
+ # =============================================================================
23
+ class FeatureFCRun(Base):
20
24
  __doc__ = write_lines(attr_dict)
21
25
 
22
26
  def __init__(self, **kwargs):
23
27
 
28
+ self.time_period = TimePeriod()
24
29
  super().__init__(attr_dict=attr_dict, **kwargs)
@@ -0,0 +1,24 @@
1
+ from mt_metadata.features.feature import Feature
2
+
3
+ class FeatureTS(Feature):
4
+ """
5
+ Stub feature class for time series features.
6
+ """
7
+ def __init__(self, **kwargs):
8
+ super().__init__(**kwargs)
9
+ self.name = "feature_ts"
10
+ self.add_base_attribute(
11
+ "name",
12
+ "feature_ts",
13
+ {
14
+ "type": str,
15
+ "required": True,
16
+ "style": "free form",
17
+ "description": "Name of the feature (time series)",
18
+ "units": None,
19
+ "options": [],
20
+ "alias": [],
21
+ "example": "feature_ts",
22
+ "default": "feature_ts",
23
+ },
24
+ )
@@ -1,9 +1,10 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """
3
- Created on Thu Feb 17 14:15:20 2022
3
+ Created on Fri Jan 31 13:39:39 2025
4
4
 
5
5
  @author: jpeacock
6
6
  """
7
+
7
8
  # =============================================================================
8
9
  # Imports
9
10
  # =============================================================================
@@ -11,26 +12,18 @@ from mt_metadata.base.helpers import write_lines
11
12
  from mt_metadata.base import get_schema, Base
12
13
  from .standards import SCHEMA_FN_PATHS
13
14
 
15
+ from mt_metadata.timeseries import TimePeriod
16
+
14
17
  # =============================================================================
15
- attr_dict = get_schema("window", SCHEMA_FN_PATHS)
18
+ attr_dict = get_schema("feature_ts_run", SCHEMA_FN_PATHS)
19
+ attr_dict.add_dict(TimePeriod()._attr_dict, "time_period")
20
+
21
+
16
22
  # =============================================================================
17
- class Window(Base):
23
+ class FeatureTSRun(Base):
18
24
  __doc__ = write_lines(attr_dict)
19
25
 
20
26
  def __init__(self, **kwargs):
21
- super().__init__(attr_dict=attr_dict, **kwargs)
22
- self.additional_args = {}
23
-
24
- @property
25
- def additional_args(self):
26
- return self._additional_args
27
27
 
28
- @additional_args.setter
29
- def additional_args(self, args):
30
- if not isinstance(args, dict):
31
- raise TypeError("additional_args must be a dictionary")
32
- self._additional_args = args
33
-
34
- @property
35
- def num_samples_advance(self):
36
- return self.num_samples - self.overlap
28
+ self.time_period = TimePeriod()
29
+ super().__init__(attr_dict=attr_dict, **kwargs)
@@ -0,0 +1,6 @@
1
+ # package file
2
+ from pathlib import Path
3
+
4
+ SCHEMA_PATH = Path(__file__).parent
5
+
6
+ SCHEMA_FN_PATHS = list(SCHEMA_PATH.glob("*.json"))