mt-metadata 0.3.4__py2.py3-none-any.whl → 0.3.6__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.

@@ -1,476 +0,0 @@
1
- """
2
- ==========================
3
- Channel Response Filter
4
- ==========================
5
-
6
- Combines all filters for a given channel into a total response that can be used in
7
- the frequency domain.
8
-
9
- .. note:: Time Delay filters should be applied in the time domain
10
- otherwise bad things can happen.
11
- """
12
- # =============================================================================
13
- # Imports
14
- # =============================================================================
15
- from copy import deepcopy
16
- import numpy as np
17
-
18
- from mt_metadata.base import Base, get_schema
19
- from mt_metadata.timeseries.filters.standards import SCHEMA_FN_PATHS
20
- from mt_metadata.timeseries.filters import (
21
- PoleZeroFilter,
22
- CoefficientFilter,
23
- TimeDelayFilter,
24
- FrequencyResponseTableFilter,
25
- FIRFilter,
26
- )
27
- from mt_metadata.utils.units import get_unit_object
28
- from mt_metadata.timeseries.filters.plotting_helpers import plot_response
29
- from obspy.core import inventory
30
-
31
- # =============================================================================
32
- attr_dict = get_schema("channel_response", SCHEMA_FN_PATHS)
33
- # =============================================================================
34
-
35
-
36
- class ChannelResponseFilter(Base):
37
- """
38
- This class holds a list of all the filters associated with a channel.
39
- It has methods for combining the responses of all the filters into a total
40
- response that we will apply to a data segment.
41
- """
42
-
43
- def __init__(self, **kwargs):
44
- self.filters_list = []
45
- self.frequencies = np.logspace(-4, 4, 100)
46
- self.normalization_frequency = None
47
-
48
- super().__init__(attr_dict=attr_dict)
49
- for k, v in kwargs.items():
50
- setattr(self, k, v)
51
-
52
- def __str__(self):
53
- lines = ["Filters Included:\n", "=" * 25, "\n"]
54
- for f in self.filters_list:
55
- lines.append(f.__str__())
56
- lines.append(f"\n{'-'*20}\n")
57
-
58
- return "".join(lines)
59
-
60
- def __repr__(self):
61
- return self.__str__()
62
-
63
- @property
64
- def filters_list(self):
65
- """filters list"""
66
- return self._filters_list
67
-
68
- @filters_list.setter
69
- def filters_list(self, filters_list):
70
- """set the filters list and validate the list"""
71
- self._filters_list = self._validate_filters_list(filters_list)
72
- self._check_consistency_of_units()
73
-
74
- @property
75
- def frequencies(self):
76
- """frequencies to estimate filters"""
77
- return self._frequencies
78
-
79
- @frequencies.setter
80
- def frequencies(self, value):
81
- """
82
- Set the frequencies, make sure the input is validated
83
-
84
- Linear frequencies
85
- :param value: Linear Frequencies
86
- :type value: iterable
87
-
88
- """
89
- if value is None:
90
- self._frequencies = None
91
-
92
- elif isinstance(value, (list, tuple, np.ndarray)):
93
- self._frequencies = np.array(value, dtype=float)
94
- else:
95
- msg = f"input values must be an list, tuple, or np.ndarray, not {type(value)}"
96
- self.logger.error(msg)
97
- raise TypeError(msg)
98
-
99
- @property
100
- def names(self):
101
- """names of the filters"""
102
- names = []
103
- if self.filters_list:
104
- names = [f.name for f in self.filters_list]
105
- return names
106
-
107
- def _validate_filters_list(self, filters_list):
108
- """
109
- make sure the filters list is valid
110
-
111
- :param filters_list: DESCRIPTION
112
- :type filters_list: TYPE
113
- :return: DESCRIPTION
114
- :rtype: TYPE
115
-
116
- """
117
- ACCEPTABLE_FILTERS = [
118
- PoleZeroFilter,
119
- CoefficientFilter,
120
- TimeDelayFilter,
121
- FrequencyResponseTableFilter,
122
- FIRFilter,
123
- ]
124
-
125
- def is_acceptable_filter(item):
126
- if isinstance(item, tuple(ACCEPTABLE_FILTERS)):
127
- return True
128
- else:
129
- return False
130
-
131
- if filters_list in [[], None]:
132
- return []
133
-
134
- if not isinstance(filters_list, list):
135
- msg = f"Input filters list must be a list not {type(filters_list)}"
136
- self.logger.error(msg)
137
- raise TypeError(msg)
138
-
139
- fails = []
140
- return_list = []
141
- for item in filters_list:
142
- if is_acceptable_filter(item):
143
- return_list.append(item)
144
- else:
145
- fails.append(
146
- f"Item is not an acceptable filter type, {type(item)}"
147
- )
148
-
149
- if fails:
150
- raise TypeError(", ".join(fails))
151
-
152
- return return_list
153
-
154
- @property
155
- def pass_band(self):
156
- """estimate pass band for all filters in frequency"""
157
- if self.frequencies is None:
158
- raise ValueError(
159
- "frequencies are None, must be input to calculate pass band"
160
- )
161
- pb = []
162
- for f in self.filters_list:
163
- if hasattr(f, "pass_band"):
164
- f_pb = f.pass_band(self.frequencies)
165
- if f_pb is None:
166
- continue
167
- pb.append((f_pb.min(), f_pb.max()))
168
-
169
- if pb != []:
170
- pb = np.array(pb)
171
- return np.array([pb[:, 0].max(), pb[:, 1].min()])
172
- return None
173
-
174
- @property
175
- def normalization_frequency(self):
176
- """get normalization frequency from ZPK or FAP filter"""
177
-
178
- if self._normalization_frequency == 0.0:
179
- if self.pass_band is not None:
180
- return np.round(10 ** np.mean(np.log10(self.pass_band)), 3)
181
-
182
- return self._normalization_frequency
183
-
184
- @normalization_frequency.setter
185
- def normalization_frequency(self, value):
186
- """Set normalization frequency if input"""
187
-
188
- self._normalization_frequency = value
189
-
190
- @property
191
- def non_delay_filters(self):
192
- """
193
-
194
- :return: all the non-time_delay filters as a list
195
-
196
- """
197
- non_delay_filters = [
198
- x for x in self.filters_list if x.type != "time delay"
199
- ]
200
- return non_delay_filters
201
-
202
- @property
203
- def delay_filters(self):
204
- """
205
-
206
- :return: all the time delay filters as a list
207
-
208
- """
209
- delay_filters = [x for x in self.filters_list if x.type == "time delay"]
210
- return delay_filters
211
-
212
- @property
213
- def total_delay(self):
214
- """
215
-
216
- :return: the total delay of all filters
217
-
218
- """
219
- delay_filters = self.delay_filters
220
- total_delay = 0.0
221
- for delay_filter in delay_filters:
222
- total_delay += delay_filter.delay
223
- return total_delay
224
-
225
- def complex_response(
226
- self,
227
- frequencies=None,
228
- include_delay=False,
229
- normalize=False,
230
- include_decimation=True,
231
- **kwargs,
232
- ):
233
- """
234
-
235
- :param frequencies: frequencies to compute complex response,
236
- defaults to None
237
- :type frequencies: np.ndarray, optional
238
- :param include_delay: include delay in complex response,
239
- defaults to False
240
- :type include_delay: bool, optional
241
- :param normalize: normalize the response to 1, defaults to False
242
- :type normalize: bool, optional
243
- :param include_decimation: Include decimation in response,
244
- defaults to True
245
- :type include_decimation: bool, optional
246
- :return: complex response along give frequency array
247
- :rtype: np.ndarray
248
-
249
- """
250
-
251
- if frequencies is not None:
252
- self.frequencies = frequencies
253
-
254
- if include_delay:
255
- filters_list = deepcopy(self.filters_list)
256
- else:
257
- filters_list = deepcopy(self.non_delay_filters)
258
-
259
- if not include_decimation:
260
- filters_list = deepcopy(
261
- [x for x in filters_list if not x.decimation_active]
262
- )
263
-
264
- if len(filters_list) == 0:
265
- # warn that there are no filters associated with channel?
266
- return np.ones(len(self.frequencies), dtype=complex)
267
-
268
- result = filters_list[0].complex_response(self.frequencies)
269
-
270
- for ff in filters_list[1:]:
271
- result *= ff.complex_response(self.frequencies)
272
-
273
- if normalize:
274
- result /= np.max(np.abs(result))
275
- return result
276
-
277
- def compute_instrument_sensitivity(
278
- self, normalization_frequency=None, sig_figs=6
279
- ):
280
- """
281
- Compute the StationXML instrument sensitivity for the given normalization frequency
282
-
283
- :param normalization_frequency: DESCRIPTION
284
- :type normalization_frequency: TYPE
285
- :return: DESCRIPTION
286
- :rtype: TYPE
287
-
288
- """
289
- if normalization_frequency is not None:
290
- self.normalization_frequency = normalization_frequency
291
- sensitivity = 1.0
292
- for mt_filter in self.filters_list:
293
- complex_response = mt_filter.complex_response(
294
- self.normalization_frequency
295
- )
296
- sensitivity *= complex_response.astype(complex)
297
- try:
298
- sensitivity = np.abs(sensitivity[0])
299
- except (IndexError, TypeError):
300
- sensitivity = np.abs(sensitivity)
301
-
302
- return round(
303
- sensitivity, sig_figs - int(np.floor(np.log10(abs(sensitivity))))
304
- )
305
-
306
- @property
307
- def units_in(self):
308
- """
309
- :return: the units of the channel
310
- """
311
- if self.filters_list is [] or len(self.filters_list) == 0:
312
- return None
313
-
314
- return self.filters_list[0].units_in
315
-
316
- @property
317
- def units_out(self):
318
- """
319
- :return: the units of the channel
320
- """
321
- if self.filters_list is [] or len(self.filters_list) == 0:
322
- return None
323
-
324
- return self.filters_list[-1].units_out
325
-
326
- def _check_consistency_of_units(self):
327
- """
328
- confirms that the input and output units of each filter state are consistent
329
- """
330
- if len(self._filters_list) > 1:
331
- previous_units = self._filters_list[0].units_out
332
- for mt_filter in self._filters_list[1:]:
333
- if mt_filter.units_in != previous_units:
334
- msg = (
335
- "Unit consistency is incorrect. "
336
- f"The input units for {mt_filter.name} should be "
337
- f"{previous_units} not {mt_filter.units_in}"
338
- )
339
- self.logger.error(msg)
340
- raise ValueError(msg)
341
- previous_units = mt_filter.units_out
342
-
343
- return True
344
-
345
- def to_obspy(self, sample_rate=1):
346
- """
347
- Output :class:`obspy.core.inventory.InstrumentSensitivity` object that
348
- can be used in a stationxml file.
349
-
350
- :param normalization_frequency: DESCRIPTION
351
- :type normalization_frequency: TYPE
352
- :return: DESCRIPTION
353
- :rtype: TYPE
354
-
355
- """
356
- total_sensitivity = self.compute_instrument_sensitivity()
357
-
358
- units_in_obj = get_unit_object(self.units_in)
359
- units_out_obj = get_unit_object(self.units_out)
360
-
361
- total_response = inventory.Response()
362
- total_response.instrument_sensitivity = inventory.InstrumentSensitivity(
363
- total_sensitivity,
364
- self.normalization_frequency,
365
- units_in_obj.abbreviation,
366
- units_out_obj.abbreviation,
367
- input_units_description=units_in_obj.name,
368
- output_units_description=units_out_obj.name,
369
- )
370
-
371
- for ii, f in enumerate(self.filters_list, 1):
372
- if f.type in ["coefficient"]:
373
- if f.units_out not in ["count"]:
374
- self.logger.debug(
375
- f"converting CoefficientFilter {f.name} to PZ"
376
- )
377
- pz = PoleZeroFilter()
378
- pz.gain = f.gain
379
- pz.units_in = f.units_in
380
- pz.units_out = f.units_out
381
- pz.comments = f.comments
382
- pz.name = f.name
383
- else:
384
- pz = f
385
-
386
- total_response.response_stages.append(
387
- pz.to_obspy(
388
- stage_number=ii,
389
- normalization_frequency=self.normalization_frequency,
390
- sample_rate=sample_rate,
391
- )
392
- )
393
- else:
394
- total_response.response_stages.append(
395
- f.to_obspy(
396
- stage_number=ii,
397
- normalization_frequency=self.normalization_frequency,
398
- sample_rate=sample_rate,
399
- )
400
- )
401
-
402
- return total_response
403
-
404
- def plot_response(
405
- self,
406
- frequencies=None,
407
- x_units="period",
408
- unwrap=True,
409
- pb_tol=1e-1,
410
- interpolation_method="slinear",
411
- include_delay=False,
412
- include_decimation=True,
413
- ):
414
- """
415
- Plot the response
416
-
417
- :param frequencies: frequencies to compute response, defaults to None
418
- :type frequencies: np.ndarray, optional
419
- :param x_units: [ period | frequency ], defaults to "period"
420
- :type x_units: string, optional
421
- :param unwrap: Unwrap phase, defaults to True
422
- :type unwrap: bool, optional
423
- :param pb_tol: pass band tolerance, defaults to 1e-1
424
- :type pb_tol: float, optional
425
- :param interpolation_method: Interpolation method see scipy.signal.interpolate
426
- [ slinear | nearest | cubic | quadratic | ], defaults to "slinear"
427
- :type interpolation_method: string, optional
428
- :param include_delay: include delays in response, defaults to False
429
- :type include_delay: bool, optional
430
- :param include_decimation: Include decimation in response,
431
- defaults to True
432
- :type include_decimation: bool, optional
433
-
434
- """
435
-
436
- if frequencies is not None:
437
- self.frequencies = frequencies
438
-
439
- # get only the filters desired
440
- if include_delay:
441
- filters_list = deepcopy(self.filters_list)
442
- else:
443
- filters_list = deepcopy(self.non_delay_filters)
444
-
445
- if not include_decimation:
446
- filters_list = deepcopy(
447
- [x for x in filters_list if not x.decimation_active]
448
- )
449
-
450
- cr_kwargs = {"interpolation_method": interpolation_method}
451
-
452
- # get response of individual filters
453
- cr_list = [
454
- f.complex_response(self.frequencies, **cr_kwargs)
455
- for f in filters_list
456
- ]
457
-
458
- # compute total response
459
- cr_kwargs["include_delay"] = include_delay
460
- cr_kwargs["include_decimation"] = include_decimation
461
- complex_response = self.complex_response(self.frequencies, **cr_kwargs)
462
-
463
- cr_list.append(complex_response)
464
- labels = [f.name for f in filters_list] + ["Total Response"]
465
-
466
- # plot with proper attributes.
467
- kwargs = {
468
- "title": f"Channel Response: [{', '.join([f.name for f in filters_list])}]",
469
- "unwrap": unwrap,
470
- "x_units": x_units,
471
- "pass_band": self.pass_band,
472
- "label": labels,
473
- "normalization_frequency": self.normalization_frequency,
474
- }
475
-
476
- plot_response(self.frequencies, cr_list, **kwargs)