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.

Files changed (41) hide show
  1. mt_metadata/__init__.py +12 -10
  2. mt_metadata/base/metadata.py +24 -13
  3. mt_metadata/data/transfer_functions/tf_zmm.zmm +1 -1
  4. mt_metadata/timeseries/channel.py +8 -3
  5. mt_metadata/timeseries/filters/__init__.py +2 -2
  6. mt_metadata/timeseries/filters/{channel_response_filter.py → channel_response.py} +62 -29
  7. mt_metadata/timeseries/filters/coefficient_filter.py +1 -3
  8. mt_metadata/timeseries/filters/filter_base.py +70 -37
  9. mt_metadata/timeseries/filters/filtered.py +32 -22
  10. mt_metadata/timeseries/filters/fir_filter.py +10 -11
  11. mt_metadata/timeseries/filters/frequency_response_table_filter.py +9 -8
  12. mt_metadata/timeseries/filters/helper_functions.py +112 -3
  13. mt_metadata/timeseries/filters/pole_zero_filter.py +9 -8
  14. mt_metadata/timeseries/filters/standards/filter_base.json +2 -2
  15. mt_metadata/timeseries/filters/time_delay_filter.py +9 -8
  16. mt_metadata/timeseries/stationxml/fdsn_tools.py +8 -7
  17. mt_metadata/timeseries/stationxml/xml_channel_mt_channel.py +1 -1
  18. mt_metadata/timeseries/stationxml/xml_inventory_mt_experiment.py +4 -1
  19. mt_metadata/timeseries/tools/from_many_mt_files.py +15 -3
  20. mt_metadata/transfer_functions/__init__.py +1 -1
  21. mt_metadata/transfer_functions/core.py +89 -49
  22. mt_metadata/transfer_functions/io/edi/edi.py +9 -5
  23. mt_metadata/transfer_functions/io/edi/metadata/define_measurement.py +7 -3
  24. mt_metadata/transfer_functions/io/emtfxml/emtfxml.py +1 -4
  25. mt_metadata/transfer_functions/io/jfiles/jfile.py +2 -1
  26. mt_metadata/transfer_functions/io/zfiles/zmm.py +108 -62
  27. mt_metadata/transfer_functions/io/zonge/zonge.py +2 -2
  28. mt_metadata/transfer_functions/processing/aurora/band.py +16 -0
  29. mt_metadata/transfer_functions/processing/aurora/channel_nomenclature.py +34 -31
  30. mt_metadata/transfer_functions/processing/aurora/processing.py +12 -5
  31. mt_metadata/transfer_functions/processing/aurora/stations.py +11 -2
  32. mt_metadata/transfer_functions/processing/fourier_coefficients/decimation.py +1 -1
  33. mt_metadata/transfer_functions/processing/fourier_coefficients/standards/decimation.json +1 -1
  34. mt_metadata/transfer_functions/processing/fourier_coefficients/standards/fc_channel.json +23 -1
  35. mt_metadata/transfer_functions/tf/transfer_function.py +93 -1
  36. {mt_metadata-0.3.3.dist-info → mt_metadata-0.3.5.dist-info}/METADATA +379 -379
  37. {mt_metadata-0.3.3.dist-info → mt_metadata-0.3.5.dist-info}/RECORD +41 -41
  38. {mt_metadata-0.3.3.dist-info → mt_metadata-0.3.5.dist-info}/WHEEL +1 -1
  39. {mt_metadata-0.3.3.dist-info → mt_metadata-0.3.5.dist-info}/AUTHORS.rst +0 -0
  40. {mt_metadata-0.3.3.dist-info → mt_metadata-0.3.5.dist-info}/LICENSE +0 -0
  41. {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.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
- "data/transfer_functions/tf_edi_phoenix.edi"
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
  )
@@ -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 != other_value:
121
- msg = f"{key}: {value} != {other_value}"
122
- self.logger.info(msg)
123
- fail = True
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 name is None:
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 name.lower() in options:
324
+ if value.lower() in options:
314
325
  return True, other_possible, None
315
- elif name.lower() not in options and other_possible:
326
+ elif value.lower() not in options and other_possible:
316
327
  msg = (
317
- "{0} not found in options list {1}, but other options"
318
- + " are allowed. Allowing {2} to be set to {0}."
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, "{0} not found in options list {1}"
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))
@@ -1,5 +1,5 @@
1
1
  TRANSFER FUNCTIONS IN MEASUREMENT COORDINATES
2
- ********** WITH FULL ERROR COVARINCE*********
2
+ ********** WITH FULL ERROR COVARIANCE*********
3
3
 
4
4
  300
5
5
  coordinate 34.727 -115.735 declination 13.10
@@ -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 ChannelResponseFilter
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 ChannelResponseFilter(filters_list=mt_filter_list)
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 .channel_response_filter import ChannelResponseFilter
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
- "ChannelResponseFilter",
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 ChannelResponseFilter(Base):
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
- ACCEPTABLE_FILTERS = [
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 is_acceptable_filter(item):
126
- if isinstance(item, tuple(ACCEPTABLE_FILTERS)):
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 is_acceptable_filter(item):
146
+ if is_supported_filter(item):
143
147
  return_list.append(item)
144
148
  else:
145
149
  fails.append(
146
- f"Item is not an acceptable filter type, {type(item)}"
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 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
- )
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
- # warn that there are no filters associated with channel?
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
- return self.filters_list[0].units_in
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=True,
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 OBSPY_MAPPING
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. We will extend this class for each specific
12
- type of filter we need to implement. Typical filters we will want to be able
13
- to support are:
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
- Note that many filters can be represented in more than one of these forms.
25
- For example a Coefficient Multiplier can be seen as an FIR with a single
26
- coefficient. Similarly, an FIR can be represented as a 'zpk' filter with
27
- no poles. An IIR filter can also be associated with a zpk representation.
28
- However, solving for the 'zpk' representation can be tedious and approximate
29
- and if we have for example, the known FIR coefficients, or FAP lookup table,
30
- then there is little to be gained by changing the representation.
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
- # Form is OBSPY_MAPPING['obspy_label'] = 'mth5_label'
57
- OBSPY_MAPPING = {}
58
- OBSPY_MAPPING["input_units"] = "units_in"
59
- OBSPY_MAPPING["name"] = "name"
60
- OBSPY_MAPPING["output_units"] = "units_out"
61
- OBSPY_MAPPING["stage_gain"] = "gain"
62
- OBSPY_MAPPING["description"] = "comments"
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.obspy_mapping = copy.deepcopy(OBSPY_MAPPING)
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().obspy_mapping
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
- print("Filter Base class does not have a complex response defined")
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
+