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.
- mt_metadata/__init__.py +1 -1
- mt_metadata/base/helpers.py +84 -9
- mt_metadata/base/metadata.py +137 -65
- mt_metadata/data/transfer_functions/test.edi +28 -28
- mt_metadata/features/__init__.py +14 -0
- mt_metadata/features/coherence.py +303 -0
- mt_metadata/features/cross_powers.py +29 -0
- mt_metadata/features/fc_coherence.py +81 -0
- mt_metadata/features/feature.py +72 -0
- mt_metadata/features/feature_decimation_channel.py +26 -0
- mt_metadata/features/feature_fc.py +24 -0
- mt_metadata/{transfer_functions/processing/aurora/decimation.py → features/feature_fc_run.py} +9 -4
- mt_metadata/features/feature_ts.py +24 -0
- mt_metadata/{transfer_functions/processing/aurora/window.py → features/feature_ts_run.py} +11 -18
- mt_metadata/features/standards/__init__.py +6 -0
- mt_metadata/features/standards/base_feature.json +46 -0
- mt_metadata/features/standards/coherence.json +57 -0
- mt_metadata/features/standards/fc_coherence.json +57 -0
- mt_metadata/features/standards/feature_decimation_channel.json +68 -0
- mt_metadata/features/standards/feature_fc_run.json +35 -0
- mt_metadata/features/standards/feature_ts_run.json +35 -0
- mt_metadata/features/standards/feature_weighting_window.json +46 -0
- mt_metadata/features/standards/weight_kernel.json +46 -0
- mt_metadata/features/standards/weights.json +101 -0
- mt_metadata/features/test_helpers/channel_weight_specs_example.json +156 -0
- mt_metadata/features/weights/__init__.py +0 -0
- mt_metadata/features/weights/base.py +44 -0
- mt_metadata/features/weights/channel_weight_spec.py +209 -0
- mt_metadata/features/weights/feature_weight_spec.py +194 -0
- mt_metadata/features/weights/monotonic_weight_kernel.py +275 -0
- mt_metadata/features/weights/standards/__init__.py +6 -0
- mt_metadata/features/weights/standards/activation_monotonic_weight_kernel.json +38 -0
- mt_metadata/features/weights/standards/base.json +36 -0
- mt_metadata/features/weights/standards/channel_weight_spec.json +35 -0
- mt_metadata/features/weights/standards/composite.json +36 -0
- mt_metadata/features/weights/standards/feature_weight_spec.json +13 -0
- mt_metadata/features/weights/standards/monotonic_weight_kernel.json +49 -0
- mt_metadata/features/weights/standards/taper_monotonic_weight_kernel.json +16 -0
- mt_metadata/features/weights/taper_weight_kernel.py +60 -0
- mt_metadata/helper_functions.py +69 -0
- mt_metadata/timeseries/filters/channel_response.py +77 -37
- mt_metadata/timeseries/filters/coefficient_filter.py +6 -5
- mt_metadata/timeseries/filters/filter_base.py +11 -15
- mt_metadata/timeseries/filters/fir_filter.py +8 -1
- mt_metadata/timeseries/filters/frequency_response_table_filter.py +26 -11
- mt_metadata/timeseries/filters/helper_functions.py +0 -2
- mt_metadata/timeseries/filters/obspy_stages.py +4 -1
- mt_metadata/timeseries/filters/pole_zero_filter.py +9 -5
- mt_metadata/timeseries/filters/time_delay_filter.py +8 -1
- mt_metadata/timeseries/location.py +20 -5
- mt_metadata/timeseries/person.py +14 -7
- mt_metadata/timeseries/standards/person.json +1 -1
- mt_metadata/timeseries/standards/run.json +2 -2
- mt_metadata/timeseries/station.py +4 -2
- mt_metadata/timeseries/stationxml/__init__.py +5 -0
- mt_metadata/timeseries/stationxml/xml_channel_mt_channel.py +38 -27
- mt_metadata/timeseries/stationxml/xml_inventory_mt_experiment.py +16 -47
- mt_metadata/timeseries/stationxml/xml_station_mt_station.py +25 -24
- mt_metadata/transfer_functions/__init__.py +3 -0
- mt_metadata/transfer_functions/core.py +16 -11
- mt_metadata/transfer_functions/io/emtfxml/metadata/location.py +5 -0
- mt_metadata/transfer_functions/io/emtfxml/metadata/provenance.py +14 -3
- mt_metadata/transfer_functions/io/tools.py +2 -0
- mt_metadata/transfer_functions/io/zonge/metadata/header.py +1 -1
- mt_metadata/transfer_functions/io/zonge/metadata/standards/header.json +1 -1
- mt_metadata/transfer_functions/io/zonge/metadata/standards/job.json +2 -2
- mt_metadata/transfer_functions/io/zonge/zonge.py +19 -23
- mt_metadata/transfer_functions/processing/__init__.py +2 -1
- mt_metadata/transfer_functions/processing/aurora/__init__.py +2 -4
- mt_metadata/transfer_functions/processing/aurora/band.py +46 -125
- mt_metadata/transfer_functions/processing/aurora/channel_nomenclature.py +27 -20
- mt_metadata/transfer_functions/processing/aurora/decimation_level.py +324 -152
- mt_metadata/transfer_functions/processing/aurora/frequency_bands.py +230 -0
- mt_metadata/transfer_functions/processing/aurora/processing.py +3 -3
- mt_metadata/transfer_functions/processing/aurora/run.py +32 -7
- mt_metadata/transfer_functions/processing/aurora/standards/decimation_level.json +7 -73
- mt_metadata/transfer_functions/processing/aurora/stations.py +33 -4
- mt_metadata/transfer_functions/processing/fourier_coefficients/decimation.py +176 -177
- mt_metadata/transfer_functions/processing/fourier_coefficients/fc.py +11 -9
- mt_metadata/transfer_functions/processing/fourier_coefficients/standards/decimation.json +1 -111
- mt_metadata/transfer_functions/processing/short_time_fourier_transform.py +64 -0
- mt_metadata/transfer_functions/processing/standards/__init__.py +6 -0
- mt_metadata/transfer_functions/processing/standards/short_time_fourier_transform.json +94 -0
- mt_metadata/transfer_functions/processing/{aurora/standards/decimation.json → standards/time_series_decimation.json} +17 -6
- mt_metadata/transfer_functions/processing/{aurora/standards → standards}/window.json +13 -2
- mt_metadata/transfer_functions/processing/time_series_decimation.py +50 -0
- mt_metadata/transfer_functions/processing/window.py +118 -0
- mt_metadata/transfer_functions/tf/standards/transfer_function.json +1 -1
- mt_metadata/transfer_functions/tf/station.py +17 -1
- mt_metadata/utils/mttime.py +22 -3
- mt_metadata/utils/validators.py +4 -2
- {mt_metadata-0.3.8.dist-info → mt_metadata-0.4.0.dist-info}/METADATA +39 -15
- {mt_metadata-0.3.8.dist-info → mt_metadata-0.4.0.dist-info}/RECORD +97 -57
- {mt_metadata-0.3.8.dist-info → mt_metadata-0.4.0.dist-info}/WHEEL +1 -1
- {mt_metadata-0.3.8.dist-info → mt_metadata-0.4.0.dist-info}/AUTHORS.rst +0 -0
- {mt_metadata-0.3.8.dist-info → mt_metadata-0.4.0.dist-info}/LICENSE +0 -0
- {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
|
+
)
|
mt_metadata/{transfer_functions/processing/aurora/decimation.py → features/feature_fc_run.py}
RENAMED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
"""
|
|
3
|
-
Created on
|
|
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("
|
|
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
|
-
|
|
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
|
|
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("
|
|
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
|
|
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
|
-
|
|
29
|
-
|
|
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)
|