mt-metadata 0.3.3__py2.py3-none-any.whl → 0.3.5__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 +12 -10
- mt_metadata/base/metadata.py +24 -13
- mt_metadata/data/transfer_functions/tf_zmm.zmm +1 -1
- mt_metadata/timeseries/channel.py +8 -3
- mt_metadata/timeseries/filters/__init__.py +2 -2
- mt_metadata/timeseries/filters/{channel_response_filter.py → channel_response.py} +62 -29
- mt_metadata/timeseries/filters/coefficient_filter.py +1 -3
- mt_metadata/timeseries/filters/filter_base.py +70 -37
- mt_metadata/timeseries/filters/filtered.py +32 -22
- mt_metadata/timeseries/filters/fir_filter.py +10 -11
- mt_metadata/timeseries/filters/frequency_response_table_filter.py +9 -8
- mt_metadata/timeseries/filters/helper_functions.py +112 -3
- mt_metadata/timeseries/filters/pole_zero_filter.py +9 -8
- mt_metadata/timeseries/filters/standards/filter_base.json +2 -2
- mt_metadata/timeseries/filters/time_delay_filter.py +9 -8
- mt_metadata/timeseries/stationxml/fdsn_tools.py +8 -7
- mt_metadata/timeseries/stationxml/xml_channel_mt_channel.py +1 -1
- mt_metadata/timeseries/stationxml/xml_inventory_mt_experiment.py +4 -1
- mt_metadata/timeseries/tools/from_many_mt_files.py +15 -3
- mt_metadata/transfer_functions/__init__.py +1 -1
- mt_metadata/transfer_functions/core.py +89 -49
- mt_metadata/transfer_functions/io/edi/edi.py +9 -5
- mt_metadata/transfer_functions/io/edi/metadata/define_measurement.py +7 -3
- mt_metadata/transfer_functions/io/emtfxml/emtfxml.py +1 -4
- mt_metadata/transfer_functions/io/jfiles/jfile.py +2 -1
- mt_metadata/transfer_functions/io/zfiles/zmm.py +108 -62
- mt_metadata/transfer_functions/io/zonge/zonge.py +2 -2
- mt_metadata/transfer_functions/processing/aurora/band.py +16 -0
- mt_metadata/transfer_functions/processing/aurora/channel_nomenclature.py +34 -31
- mt_metadata/transfer_functions/processing/aurora/processing.py +12 -5
- mt_metadata/transfer_functions/processing/aurora/stations.py +11 -2
- mt_metadata/transfer_functions/processing/fourier_coefficients/decimation.py +1 -1
- mt_metadata/transfer_functions/processing/fourier_coefficients/standards/decimation.json +1 -1
- mt_metadata/transfer_functions/processing/fourier_coefficients/standards/fc_channel.json +23 -1
- mt_metadata/transfer_functions/tf/transfer_function.py +93 -1
- {mt_metadata-0.3.3.dist-info → mt_metadata-0.3.5.dist-info}/METADATA +379 -379
- {mt_metadata-0.3.3.dist-info → mt_metadata-0.3.5.dist-info}/RECORD +41 -41
- {mt_metadata-0.3.3.dist-info → mt_metadata-0.3.5.dist-info}/WHEEL +1 -1
- {mt_metadata-0.3.3.dist-info → mt_metadata-0.3.5.dist-info}/AUTHORS.rst +0 -0
- {mt_metadata-0.3.3.dist-info → mt_metadata-0.3.5.dist-info}/LICENSE +0 -0
- {mt_metadata-0.3.3.dist-info → mt_metadata-0.3.5.dist-info}/top_level.txt +0 -0
mt_metadata/__init__.py
CHANGED
|
@@ -39,7 +39,7 @@ you should only have to changes these dictionaries.
|
|
|
39
39
|
|
|
40
40
|
__author__ = """Jared Peacock"""
|
|
41
41
|
__email__ = "jpeacock@usgs.gov"
|
|
42
|
-
__version__ = "0.3.
|
|
42
|
+
__version__ = "0.3.5"
|
|
43
43
|
|
|
44
44
|
# =============================================================================
|
|
45
45
|
# Imports
|
|
@@ -82,6 +82,14 @@ REQUIRED_KEYS = [
|
|
|
82
82
|
"default",
|
|
83
83
|
]
|
|
84
84
|
|
|
85
|
+
DEFAULT_CHANNEL_NOMENCLATURE = {
|
|
86
|
+
"hx": "hx",
|
|
87
|
+
"hy": "hy",
|
|
88
|
+
"hz": "hz",
|
|
89
|
+
"ex": "ex",
|
|
90
|
+
"ey": "ey",
|
|
91
|
+
}
|
|
92
|
+
|
|
85
93
|
# =============================================================================
|
|
86
94
|
# Initiate loggers
|
|
87
95
|
# =============================================================================
|
|
@@ -152,19 +160,13 @@ TF_POOR_XML = DATA_DIR.joinpath("data/transfer_functions/tf_poor_xml.xml")
|
|
|
152
160
|
TF_XML_MULTIPLE_ATTACHMENTS = DATA_DIR.joinpath(
|
|
153
161
|
"data/transfer_functions/tf_xml_multiple_attachments.xml"
|
|
154
162
|
)
|
|
155
|
-
TF_EDI_PHOENIX = DATA_DIR.joinpath(
|
|
156
|
-
|
|
157
|
-
)
|
|
158
|
-
TF_EDI_EMPOWER = DATA_DIR.joinpath(
|
|
159
|
-
"data/transfer_functions/tf_edi_empower.edi"
|
|
160
|
-
)
|
|
163
|
+
TF_EDI_PHOENIX = DATA_DIR.joinpath("data/transfer_functions/tf_edi_phoenix.edi")
|
|
164
|
+
TF_EDI_EMPOWER = DATA_DIR.joinpath("data/transfer_functions/tf_edi_empower.edi")
|
|
161
165
|
TF_EDI_METRONIX = DATA_DIR.joinpath(
|
|
162
166
|
"data/transfer_functions/tf_edi_metronix.edi"
|
|
163
167
|
)
|
|
164
168
|
TF_EDI_CGG = DATA_DIR.joinpath("data/transfer_functions/tf_edi_cgg.edi")
|
|
165
|
-
TF_EDI_QUANTEC = DATA_DIR.joinpath(
|
|
166
|
-
"data/transfer_functions/tf_edi_quantec.edi"
|
|
167
|
-
)
|
|
169
|
+
TF_EDI_QUANTEC = DATA_DIR.joinpath("data/transfer_functions/tf_edi_quantec.edi")
|
|
168
170
|
TF_EDI_RHO_ONLY = DATA_DIR.joinpath(
|
|
169
171
|
"data/transfer_functions/tf_edi_rho_only.edi"
|
|
170
172
|
)
|
mt_metadata/base/metadata.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""
|
|
3
3
|
Created on Wed Dec 23 20:41:16 2020
|
|
4
4
|
|
|
5
|
-
:copyright:
|
|
5
|
+
:copyright:
|
|
6
6
|
Jared Peacock (jpeacock@usgs.gov)
|
|
7
7
|
|
|
8
8
|
:license: MIT
|
|
@@ -113,14 +113,25 @@ class Base:
|
|
|
113
113
|
try:
|
|
114
114
|
other_value = other_dict[key]
|
|
115
115
|
if isinstance(value, np.ndarray):
|
|
116
|
+
if value.size != other_value.size:
|
|
117
|
+
msg = f"Array sizes for {key} differ: {value.size} != {other_value.size}"
|
|
118
|
+
self.logger.info(msg)
|
|
119
|
+
fail=True
|
|
120
|
+
continue
|
|
116
121
|
if not (value == other_value).all():
|
|
117
122
|
msg = f"{key}: {value} != {other_value}"
|
|
118
123
|
self.logger.info(msg)
|
|
119
124
|
fail = True
|
|
120
|
-
elif value
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
elif isinstance(value, (float, int, complex)):
|
|
126
|
+
if not np.isclose(value, other_value):
|
|
127
|
+
msg = f"{key}: {value} != {other_value}"
|
|
128
|
+
self.logger.info(msg)
|
|
129
|
+
fail = True
|
|
130
|
+
else:
|
|
131
|
+
if value != other_value:
|
|
132
|
+
msg = f"{key}: {value} != {other_value}"
|
|
133
|
+
self.logger.info(msg)
|
|
134
|
+
fail = True
|
|
124
135
|
except KeyError:
|
|
125
136
|
msg = "Cannot find {0} in other".format(key)
|
|
126
137
|
self.logger.info(msg)
|
|
@@ -289,7 +300,7 @@ class Base:
|
|
|
289
300
|
self.logger.exception(error)
|
|
290
301
|
raise MTSchemaError(error)
|
|
291
302
|
|
|
292
|
-
def _validate_option(self, name, option_list):
|
|
303
|
+
def _validate_option(self, name, value, option_list):
|
|
293
304
|
"""
|
|
294
305
|
validate the given attribute name agains possible options and check
|
|
295
306
|
for aliases
|
|
@@ -304,21 +315,21 @@ class Base:
|
|
|
304
315
|
:rtype: TYPE
|
|
305
316
|
|
|
306
317
|
"""
|
|
307
|
-
if
|
|
318
|
+
if value is None:
|
|
308
319
|
return True, False, None
|
|
309
320
|
options = [ss.lower() for ss in option_list]
|
|
310
321
|
other_possible = False
|
|
311
322
|
if "other" in options:
|
|
312
323
|
other_possible = True
|
|
313
|
-
if
|
|
324
|
+
if value.lower() in options:
|
|
314
325
|
return True, other_possible, None
|
|
315
|
-
elif
|
|
326
|
+
elif value.lower() not in options and other_possible:
|
|
316
327
|
msg = (
|
|
317
|
-
"{
|
|
318
|
-
+ " are allowed. Allowing {
|
|
328
|
+
f"Value '{value}' not found for metadata field '{name}' in options list {option_list}, but other options"
|
|
329
|
+
+ f" are allowed. Allowing {option_list} to be set to {value}."
|
|
319
330
|
)
|
|
320
331
|
return True, other_possible, msg
|
|
321
|
-
return False, other_possible, "{
|
|
332
|
+
return False, other_possible, f"Value '{value}' for metadata field '{name}' not found in options list {option_list}"
|
|
322
333
|
|
|
323
334
|
def __setattr__(self, name, value):
|
|
324
335
|
"""
|
|
@@ -388,7 +399,7 @@ class Base:
|
|
|
388
399
|
# check options
|
|
389
400
|
if v_dict["style"] == "controlled vocabulary":
|
|
390
401
|
options = v_dict["options"]
|
|
391
|
-
accept, other, msg = self._validate_option(value, options)
|
|
402
|
+
accept, other, msg = self._validate_option(name, value, options)
|
|
392
403
|
if not accept:
|
|
393
404
|
self.logger.error(msg.format(value, options))
|
|
394
405
|
raise MTSchemaError(msg.format(value, options))
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""
|
|
3
3
|
Created on Wed Dec 23 21:30:36 2020
|
|
4
4
|
|
|
5
|
-
:copyright:
|
|
5
|
+
:copyright:
|
|
6
6
|
Jared Peacock (jpeacock@usgs.gov)
|
|
7
7
|
|
|
8
8
|
:license: MIT
|
|
@@ -16,7 +16,7 @@ from mt_metadata.base.helpers import write_lines
|
|
|
16
16
|
from mt_metadata.base import get_schema, Base
|
|
17
17
|
from .standards import SCHEMA_FN_PATHS
|
|
18
18
|
from . import DataQuality, Filtered, Location, TimePeriod, Instrument, Fdsn
|
|
19
|
-
from mt_metadata.timeseries.filters import
|
|
19
|
+
from mt_metadata.timeseries.filters import ChannelResponse
|
|
20
20
|
|
|
21
21
|
# =============================================================================
|
|
22
22
|
attr_dict = get_schema("channel", SCHEMA_FN_PATHS)
|
|
@@ -79,4 +79,9 @@ class Channel(Base):
|
|
|
79
79
|
self.logger.error(msg)
|
|
80
80
|
continue
|
|
81
81
|
# compute instrument sensitivity and units in/out
|
|
82
|
-
return
|
|
82
|
+
return ChannelResponse(filters_list=mt_filter_list)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def unit_object(self):
|
|
86
|
+
from mt_metadata.utils.units import get_unit_object
|
|
87
|
+
return get_unit_object(self.units)
|
|
@@ -3,7 +3,7 @@ from .fir_filter import FIRFilter
|
|
|
3
3
|
from .pole_zero_filter import PoleZeroFilter
|
|
4
4
|
from .time_delay_filter import TimeDelayFilter
|
|
5
5
|
from .frequency_response_table_filter import FrequencyResponseTableFilter
|
|
6
|
-
from .
|
|
6
|
+
from .channel_response import ChannelResponse
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
__all__ = [
|
|
@@ -12,5 +12,5 @@ __all__ = [
|
|
|
12
12
|
"PoleZeroFilter",
|
|
13
13
|
"TimeDelayFilter",
|
|
14
14
|
"FrequencyResponseTableFilter",
|
|
15
|
-
"
|
|
15
|
+
"ChannelResponse",
|
|
16
16
|
]
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
Channel Response Filter
|
|
4
4
|
==========================
|
|
5
5
|
|
|
6
|
-
Combines all filters for a given channel into a total response that can be used in
|
|
6
|
+
Combines all filters for a given channel into a total response that can be used in
|
|
7
7
|
the frequency domain.
|
|
8
8
|
|
|
9
|
-
.. note:: Time Delay filters should be applied in the time domain
|
|
10
|
-
otherwise bad things can happen.
|
|
9
|
+
.. note:: Time Delay filters should be applied in the time domain
|
|
10
|
+
otherwise bad things can happen.
|
|
11
11
|
"""
|
|
12
12
|
# =============================================================================
|
|
13
13
|
# Imports
|
|
@@ -24,6 +24,7 @@ from mt_metadata.timeseries.filters import (
|
|
|
24
24
|
FrequencyResponseTableFilter,
|
|
25
25
|
FIRFilter,
|
|
26
26
|
)
|
|
27
|
+
from mt_metadata.timeseries.filters.filter_base import FilterBase
|
|
27
28
|
from mt_metadata.utils.units import get_unit_object
|
|
28
29
|
from mt_metadata.timeseries.filters.plotting_helpers import plot_response
|
|
29
30
|
from obspy.core import inventory
|
|
@@ -33,9 +34,11 @@ attr_dict = get_schema("channel_response", SCHEMA_FN_PATHS)
|
|
|
33
34
|
# =============================================================================
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
class
|
|
37
|
+
class ChannelResponse(Base):
|
|
37
38
|
"""
|
|
38
39
|
This class holds a list of all the filters associated with a channel.
|
|
40
|
+
The list should be ordered to match the order in which the filters are applied to the signal.
|
|
41
|
+
|
|
39
42
|
It has methods for combining the responses of all the filters into a total
|
|
40
43
|
response that we will apply to a data segment.
|
|
41
44
|
"""
|
|
@@ -60,6 +63,7 @@ class ChannelResponseFilter(Base):
|
|
|
60
63
|
def __repr__(self):
|
|
61
64
|
return self.__str__()
|
|
62
65
|
|
|
66
|
+
|
|
63
67
|
@property
|
|
64
68
|
def filters_list(self):
|
|
65
69
|
"""filters list"""
|
|
@@ -106,7 +110,7 @@ class ChannelResponseFilter(Base):
|
|
|
106
110
|
|
|
107
111
|
def _validate_filters_list(self, filters_list):
|
|
108
112
|
"""
|
|
109
|
-
make sure the filters list is valid
|
|
113
|
+
make sure the filters list is valid.
|
|
110
114
|
|
|
111
115
|
:param filters_list: DESCRIPTION
|
|
112
116
|
:type filters_list: TYPE
|
|
@@ -114,7 +118,7 @@ class ChannelResponseFilter(Base):
|
|
|
114
118
|
:rtype: TYPE
|
|
115
119
|
|
|
116
120
|
"""
|
|
117
|
-
|
|
121
|
+
supported_filters = [
|
|
118
122
|
PoleZeroFilter,
|
|
119
123
|
CoefficientFilter,
|
|
120
124
|
TimeDelayFilter,
|
|
@@ -122,8 +126,8 @@ class ChannelResponseFilter(Base):
|
|
|
122
126
|
FIRFilter,
|
|
123
127
|
]
|
|
124
128
|
|
|
125
|
-
def
|
|
126
|
-
if isinstance(item, tuple(
|
|
129
|
+
def is_supported_filter(item):
|
|
130
|
+
if isinstance(item, tuple(supported_filters)):
|
|
127
131
|
return True
|
|
128
132
|
else:
|
|
129
133
|
return False
|
|
@@ -139,11 +143,11 @@ class ChannelResponseFilter(Base):
|
|
|
139
143
|
fails = []
|
|
140
144
|
return_list = []
|
|
141
145
|
for item in filters_list:
|
|
142
|
-
if
|
|
146
|
+
if is_supported_filter(item):
|
|
143
147
|
return_list.append(item)
|
|
144
148
|
else:
|
|
145
149
|
fails.append(
|
|
146
|
-
f"Item is not
|
|
150
|
+
f"Item is not a supported filter type, {type(item)}"
|
|
147
151
|
)
|
|
148
152
|
|
|
149
153
|
if fails:
|
|
@@ -222,15 +226,46 @@ class ChannelResponseFilter(Base):
|
|
|
222
226
|
total_delay += delay_filter.delay
|
|
223
227
|
return total_delay
|
|
224
228
|
|
|
229
|
+
def get_indices_of_filters_to_remove(self, include_decimation=False, include_delay=False):
|
|
230
|
+
indices = list(np.arange(len(self.filters_list)))
|
|
231
|
+
|
|
232
|
+
if not include_delay:
|
|
233
|
+
indices = [i for i in indices if self.filters_list[i].type != "time delay"]
|
|
234
|
+
|
|
235
|
+
if not include_decimation:
|
|
236
|
+
indices = [i for i in indices if not self.filters_list[i].decimation_active]
|
|
237
|
+
|
|
238
|
+
return indices
|
|
239
|
+
|
|
240
|
+
def get_list_of_filters_to_remove(self, include_decimation=False, include_delay=False):
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
:param include_decimation: bool
|
|
244
|
+
:param include_delay: bool
|
|
245
|
+
:return:
|
|
246
|
+
|
|
247
|
+
# Experimental snippet if we want to allow filters with the opposite convention
|
|
248
|
+
# into channel response -- I don't think we do.
|
|
249
|
+
# if self.correction_operation == "multiply":
|
|
250
|
+
# inverse_filters = [x.inverse() for x in self.filters_list]
|
|
251
|
+
# self.filters_list = inverse_filters
|
|
252
|
+
"""
|
|
253
|
+
indices = self.get_indices_of_filters_to_remove(include_decimation=include_decimation,
|
|
254
|
+
include_delay=include_delay)
|
|
255
|
+
return [self.filters_list[i] for i in indices]
|
|
256
|
+
|
|
225
257
|
def complex_response(
|
|
226
258
|
self,
|
|
227
259
|
frequencies=None,
|
|
260
|
+
filters_list=None,
|
|
261
|
+
include_decimation=False,
|
|
228
262
|
include_delay=False,
|
|
229
263
|
normalize=False,
|
|
230
|
-
include_decimation=True,
|
|
231
264
|
**kwargs,
|
|
232
265
|
):
|
|
233
266
|
"""
|
|
267
|
+
Computes the complex response of self.
|
|
268
|
+
Allows the user to optionally supply a subset of filters
|
|
234
269
|
|
|
235
270
|
:param frequencies: frequencies to compute complex response,
|
|
236
271
|
defaults to None
|
|
@@ -238,35 +273,31 @@ class ChannelResponseFilter(Base):
|
|
|
238
273
|
:param include_delay: include delay in complex response,
|
|
239
274
|
defaults to False
|
|
240
275
|
:type include_delay: bool, optional
|
|
241
|
-
:param normalize: normalize the response to 1, defaults to False
|
|
242
|
-
:type normalize: bool, optional
|
|
243
276
|
:param include_decimation: Include decimation in response,
|
|
244
277
|
defaults to True
|
|
245
278
|
:type include_decimation: bool, optional
|
|
279
|
+
:param normalize: normalize the response to 1, defaults to False
|
|
280
|
+
:type normalize: bool, optional
|
|
246
281
|
:return: complex response along give frequency array
|
|
247
282
|
:rtype: np.ndarray
|
|
248
283
|
|
|
249
284
|
"""
|
|
250
|
-
|
|
251
285
|
if frequencies is not None:
|
|
252
286
|
self.frequencies = frequencies
|
|
253
287
|
|
|
254
|
-
if
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
filters_list =
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
filters_list = deepcopy(
|
|
261
|
-
[x for x in filters_list if not x.decimation_active]
|
|
262
|
-
)
|
|
288
|
+
# make filters list if not supplied
|
|
289
|
+
if filters_list is None:
|
|
290
|
+
self.logger.warning("Filters list not provided, building list assuming all are applied")
|
|
291
|
+
filters_list = self.get_list_of_filters_to_remove(
|
|
292
|
+
include_decimation=include_decimation,
|
|
293
|
+
include_delay=include_delay)
|
|
263
294
|
|
|
264
295
|
if len(filters_list) == 0:
|
|
265
|
-
|
|
296
|
+
self.logger.warning(f"No filters associated with {self.__class__}, returning 1")
|
|
266
297
|
return np.ones(len(self.frequencies), dtype=complex)
|
|
267
298
|
|
|
299
|
+
# define the product of all filters as the total response function
|
|
268
300
|
result = filters_list[0].complex_response(self.frequencies)
|
|
269
|
-
|
|
270
301
|
for ff in filters_list[1:]:
|
|
271
302
|
result *= ff.complex_response(self.frequencies)
|
|
272
303
|
|
|
@@ -274,6 +305,7 @@ class ChannelResponseFilter(Base):
|
|
|
274
305
|
result /= np.max(np.abs(result))
|
|
275
306
|
return result
|
|
276
307
|
|
|
308
|
+
|
|
277
309
|
def compute_instrument_sensitivity(
|
|
278
310
|
self, normalization_frequency=None, sig_figs=6
|
|
279
311
|
):
|
|
@@ -310,8 +342,8 @@ class ChannelResponseFilter(Base):
|
|
|
310
342
|
"""
|
|
311
343
|
if self.filters_list is [] or len(self.filters_list) == 0:
|
|
312
344
|
return None
|
|
313
|
-
|
|
314
|
-
|
|
345
|
+
else:
|
|
346
|
+
return self.filters_list[0].units_in
|
|
315
347
|
|
|
316
348
|
@property
|
|
317
349
|
def units_out(self):
|
|
@@ -320,8 +352,9 @@ class ChannelResponseFilter(Base):
|
|
|
320
352
|
"""
|
|
321
353
|
if self.filters_list is [] or len(self.filters_list) == 0:
|
|
322
354
|
return None
|
|
355
|
+
else:
|
|
356
|
+
return self.filters_list[-1].units_out
|
|
323
357
|
|
|
324
|
-
return self.filters_list[-1].units_out
|
|
325
358
|
|
|
326
359
|
def _check_consistency_of_units(self):
|
|
327
360
|
"""
|
|
@@ -409,7 +442,7 @@ class ChannelResponseFilter(Base):
|
|
|
409
442
|
pb_tol=1e-1,
|
|
410
443
|
interpolation_method="slinear",
|
|
411
444
|
include_delay=False,
|
|
412
|
-
include_decimation=
|
|
445
|
+
include_decimation=False,
|
|
413
446
|
):
|
|
414
447
|
"""
|
|
415
448
|
Plot the response
|
|
@@ -4,11 +4,10 @@ from obspy.core import inventory
|
|
|
4
4
|
|
|
5
5
|
from mt_metadata.base import get_schema
|
|
6
6
|
from mt_metadata.timeseries.filters.filter_base import FilterBase
|
|
7
|
-
from mt_metadata.timeseries.filters.filter_base import
|
|
7
|
+
from mt_metadata.timeseries.filters.filter_base import get_base_obspy_mapping
|
|
8
8
|
from mt_metadata.timeseries.filters.standards import SCHEMA_FN_PATHS
|
|
9
9
|
from mt_metadata.base.helpers import write_lines
|
|
10
10
|
|
|
11
|
-
obspy_mapping = copy.deepcopy(OBSPY_MAPPING)
|
|
12
11
|
|
|
13
12
|
# =============================================================================
|
|
14
13
|
attr_dict = get_schema("filter_base", SCHEMA_FN_PATHS)
|
|
@@ -24,7 +23,6 @@ class CoefficientFilter(FilterBase):
|
|
|
24
23
|
|
|
25
24
|
super(FilterBase, self).__init__(attr_dict=attr_dict, **kwargs)
|
|
26
25
|
self.type = "coefficient"
|
|
27
|
-
self.obspy_mapping = obspy_mapping
|
|
28
26
|
|
|
29
27
|
if self.gain == 0.0:
|
|
30
28
|
self.gain = 1.0
|
|
@@ -2,36 +2,41 @@
|
|
|
2
2
|
"""
|
|
3
3
|
Created on Wed Dec 23 21:30:36 2020
|
|
4
4
|
|
|
5
|
-
:copyright:
|
|
5
|
+
:copyright:
|
|
6
6
|
Jared Peacock (jpeacock@usgs.gov)
|
|
7
7
|
Karl Kappler
|
|
8
8
|
|
|
9
9
|
:license: MIT
|
|
10
10
|
|
|
11
|
-
This is a base class for filters
|
|
12
|
-
|
|
13
|
-
to support
|
|
14
|
-
|
|
11
|
+
This is a base class for filters associated with calibration and instrument
|
|
12
|
+
and acquistion system responses. We will extend this class for each specific
|
|
13
|
+
type of filter we need to implement. Typical filters we will want to support:
|
|
14
|
+
|
|
15
15
|
- PoleZero (or 'zpk') responses like those provided by IRIS
|
|
16
16
|
- Frequency-Amplitude-Phase (FAP) tables: look up tables from laboratory
|
|
17
17
|
calibrations via frequency sweep on a spectrum analyser.
|
|
18
|
-
- Time Delay Filters: can come about in decimation, or from general
|
|
18
|
+
- Time Delay Filters: can come about in decimation, or from general
|
|
19
19
|
timing errors that have been characterized
|
|
20
20
|
- Coefficient multipliers, i.e. frequency independent gains
|
|
21
21
|
- FIR filters
|
|
22
22
|
- IIR filters
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
Many filters can be represented in more than one of these forms. For example
|
|
25
|
+
a Coefficient Multiplier can be seen as an FIR with a single coefficient.
|
|
26
|
+
Similarly, an FIR can be represented as a 'zpk' filter with no poles. An
|
|
27
|
+
IIR filter can also be associated with a zpk representation. However, solving
|
|
28
|
+
for the 'zpk' representation can be tedious and approximate and if we have for
|
|
29
|
+
example, the known FIR coefficients, or FAP lookup table, then there is little
|
|
30
|
+
to be gained by changing the representation.
|
|
31
31
|
|
|
32
32
|
The 'stages' that are described in the IRIS StationXML documentation appear
|
|
33
33
|
to cover all possible linear time invariant filter types we are likely to
|
|
34
34
|
encounter.
|
|
35
|
+
|
|
36
|
+
A FilterBase object has a direction, defined by has units_in and units_out attrs.
|
|
37
|
+
These are the units before and after multiplication by the complex_response
|
|
38
|
+
of the filter in frequency domain. It is very similar to an "obspy filter stage"
|
|
39
|
+
|
|
35
40
|
"""
|
|
36
41
|
# =============================================================================
|
|
37
42
|
# Imports
|
|
@@ -53,16 +58,35 @@ from mt_metadata.utils.mttime import MTime
|
|
|
53
58
|
attr_dict = get_schema("filter_base", SCHEMA_FN_PATHS)
|
|
54
59
|
# =============================================================================
|
|
55
60
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
def get_base_obspy_mapping():
|
|
62
|
+
"""
|
|
63
|
+
Different filters have different mappings, but the attributes mapped here are common to all of them.
|
|
64
|
+
Hence the name "base obspy mapping"
|
|
65
|
+
Note: If we wanted to support inverse forms of these filters, and argument specifying filter direction could be added.
|
|
66
|
+
|
|
67
|
+
:return: mapping to an obspy filter, mapping['obspy_label'] = 'mt_metadata_label'
|
|
68
|
+
:rtype: dict
|
|
69
|
+
"""
|
|
70
|
+
mapping = {}
|
|
71
|
+
mapping["description"] = "comments"
|
|
72
|
+
mapping["name"] = "name"
|
|
73
|
+
mapping["stage_gain"] = "gain"
|
|
74
|
+
mapping["input_units"] = "units_in"
|
|
75
|
+
mapping["output_units"] = "units_out"
|
|
76
|
+
return mapping
|
|
63
77
|
|
|
64
78
|
|
|
65
79
|
class FilterBase(Base):
|
|
80
|
+
"""
|
|
81
|
+
bstract base class is used to represent various forms of linear, time invariant (LTI) filters.
|
|
82
|
+
By convention, forward application of the filter is equivalent to multiplication in frequency domain by the
|
|
83
|
+
filter's complex response. Removing the filter (applying the inverse) can be achieved by divding by the
|
|
84
|
+
filter's complex response.
|
|
85
|
+
|
|
86
|
+
This class is intended to support the calibration of data from archived units to physical units, although
|
|
87
|
+
it may find more application in future.
|
|
88
|
+
|
|
89
|
+
"""
|
|
66
90
|
__doc__ = write_lines(attr_dict)
|
|
67
91
|
|
|
68
92
|
def __init__(self, **kwargs):
|
|
@@ -72,7 +96,7 @@ class FilterBase(Base):
|
|
|
72
96
|
|
|
73
97
|
self._calibration_dt = MTime()
|
|
74
98
|
self.comments = None
|
|
75
|
-
self.
|
|
99
|
+
self._obspy_mapping = None
|
|
76
100
|
self.gain = 1.0
|
|
77
101
|
|
|
78
102
|
super().__init__(attr_dict=attr_dict, **kwargs)
|
|
@@ -80,6 +104,10 @@ class FilterBase(Base):
|
|
|
80
104
|
if self.gain == 0.0:
|
|
81
105
|
self.gain = 1.0
|
|
82
106
|
|
|
107
|
+
def make_obspy_mapping(self):
|
|
108
|
+
mapping = get_base_obspy_mapping()
|
|
109
|
+
return mapping
|
|
110
|
+
|
|
83
111
|
@property
|
|
84
112
|
def obspy_mapping(self):
|
|
85
113
|
"""
|
|
@@ -88,8 +116,23 @@ class FilterBase(Base):
|
|
|
88
116
|
:rtype: dict
|
|
89
117
|
|
|
90
118
|
"""
|
|
119
|
+
if self._obspy_mapping is None:
|
|
120
|
+
self._obspy_mapping = self.make_obspy_mapping()
|
|
91
121
|
return self._obspy_mapping
|
|
92
122
|
|
|
123
|
+
@obspy_mapping.setter
|
|
124
|
+
def obspy_mapping(self, obspy_dict):
|
|
125
|
+
"""
|
|
126
|
+
set the obspy mapping: this is a dictionary relating attribute labels from obspy stage objects to
|
|
127
|
+
mt_metadata filter objects.
|
|
128
|
+
"""
|
|
129
|
+
if not isinstance(obspy_dict, dict):
|
|
130
|
+
msg = f"Input must be a dictionary not {type(obspy_dict)}"
|
|
131
|
+
self.logger.error(msg)
|
|
132
|
+
raise TypeError(msg)
|
|
133
|
+
|
|
134
|
+
self._obspy_mapping = obspy_dict
|
|
135
|
+
|
|
93
136
|
@property
|
|
94
137
|
def name(self):
|
|
95
138
|
"""
|
|
@@ -114,18 +157,6 @@ class FilterBase(Base):
|
|
|
114
157
|
else:
|
|
115
158
|
self._name = None
|
|
116
159
|
|
|
117
|
-
@obspy_mapping.setter
|
|
118
|
-
def obspy_mapping(self, obspy_dict):
|
|
119
|
-
"""
|
|
120
|
-
set the obspy mapping: this is a dictionary relating attribute labels from obspy stage objects to
|
|
121
|
-
mt_metadata filter objects.
|
|
122
|
-
"""
|
|
123
|
-
if not isinstance(obspy_dict, dict):
|
|
124
|
-
msg = f"Input must be a dictionary not {type(obspy_dict)}"
|
|
125
|
-
self.logger.error(msg)
|
|
126
|
-
raise TypeError(msg)
|
|
127
|
-
|
|
128
|
-
self._obspy_mapping = obspy_dict
|
|
129
160
|
|
|
130
161
|
@property
|
|
131
162
|
def calibration_date(self):
|
|
@@ -214,6 +245,7 @@ class FilterBase(Base):
|
|
|
214
245
|
@classmethod
|
|
215
246
|
def from_obspy_stage(cls, stage, mapping=None):
|
|
216
247
|
"""
|
|
248
|
+
Expected to return a multiply operation function
|
|
217
249
|
|
|
218
250
|
:param cls: a filter object
|
|
219
251
|
:type cls: filter object
|
|
@@ -231,11 +263,11 @@ class FilterBase(Base):
|
|
|
231
263
|
|
|
232
264
|
if not isinstance(stage, obspy.core.inventory.response.ResponseStage):
|
|
233
265
|
msg = f"Expected a Stage and got a {type(stage)}"
|
|
234
|
-
cls.logger.error(msg)
|
|
266
|
+
cls().logger.error(msg)
|
|
235
267
|
raise TypeError(msg)
|
|
236
268
|
|
|
237
269
|
if mapping is None:
|
|
238
|
-
mapping = cls().
|
|
270
|
+
mapping = cls().make_obspy_mapping()
|
|
239
271
|
kwargs = {}
|
|
240
272
|
for obspy_label, mth5_label in mapping.items():
|
|
241
273
|
try:
|
|
@@ -243,11 +275,11 @@ class FilterBase(Base):
|
|
|
243
275
|
except KeyError:
|
|
244
276
|
print(f"Key {obspy_label} not found in stage object")
|
|
245
277
|
raise Exception
|
|
246
|
-
|
|
247
278
|
return cls(**kwargs)
|
|
248
279
|
|
|
249
280
|
def complex_response(self, frqs):
|
|
250
|
-
|
|
281
|
+
msg = f"complex_response not defined for {self._class_name} class"
|
|
282
|
+
self.logger.info(msg)
|
|
251
283
|
return None
|
|
252
284
|
|
|
253
285
|
def pass_band(self, frequencies, window_len=5, tol=0.5, **kwargs):
|
|
@@ -371,3 +403,4 @@ class FilterBase(Base):
|
|
|
371
403
|
if self.decimation_factor != 1.0:
|
|
372
404
|
return True
|
|
373
405
|
return False
|
|
406
|
+
|