mt-metadata 0.3.6__py2.py3-none-any.whl → 0.3.8__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 (32) hide show
  1. mt_metadata/__init__.py +1 -1
  2. mt_metadata/base/helpers.py +9 -2
  3. mt_metadata/timeseries/filters/filtered.py +133 -75
  4. mt_metadata/timeseries/station.py +31 -0
  5. mt_metadata/timeseries/stationxml/xml_inventory_mt_experiment.py +1 -0
  6. mt_metadata/transfer_functions/__init__.py +38 -0
  7. mt_metadata/transfer_functions/core.py +7 -8
  8. mt_metadata/transfer_functions/io/edi/edi.py +40 -19
  9. mt_metadata/transfer_functions/io/edi/metadata/define_measurement.py +1 -0
  10. mt_metadata/transfer_functions/io/edi/metadata/emeasurement.py +4 -2
  11. mt_metadata/transfer_functions/io/edi/metadata/header.py +3 -1
  12. mt_metadata/transfer_functions/io/edi/metadata/information.py +13 -6
  13. mt_metadata/transfer_functions/io/emtfxml/emtfxml.py +8 -2
  14. mt_metadata/transfer_functions/io/emtfxml/metadata/data.py +1 -1
  15. mt_metadata/transfer_functions/io/emtfxml/metadata/estimate.py +1 -1
  16. mt_metadata/transfer_functions/io/emtfxml/metadata/period_range.py +6 -1
  17. mt_metadata/transfer_functions/io/emtfxml/metadata/provenance.py +6 -2
  18. mt_metadata/transfer_functions/io/emtfxml/metadata/standards/copyright.json +2 -1
  19. mt_metadata/transfer_functions/processing/aurora/channel_nomenclature.py +2 -44
  20. mt_metadata/transfer_functions/processing/aurora/decimation_level.py +5 -5
  21. mt_metadata/transfer_functions/processing/aurora/standards/regression.json +46 -1
  22. mt_metadata/transfer_functions/processing/aurora/station.py +17 -11
  23. mt_metadata/transfer_functions/processing/aurora/stations.py +4 -4
  24. mt_metadata/utils/list_dict.py +19 -12
  25. mt_metadata/utils/mttime.py +1 -1
  26. mt_metadata/utils/validators.py +11 -2
  27. {mt_metadata-0.3.6.dist-info → mt_metadata-0.3.8.dist-info}/METADATA +60 -3
  28. {mt_metadata-0.3.6.dist-info → mt_metadata-0.3.8.dist-info}/RECORD +32 -32
  29. {mt_metadata-0.3.6.dist-info → mt_metadata-0.3.8.dist-info}/AUTHORS.rst +0 -0
  30. {mt_metadata-0.3.6.dist-info → mt_metadata-0.3.8.dist-info}/LICENSE +0 -0
  31. {mt_metadata-0.3.6.dist-info → mt_metadata-0.3.8.dist-info}/WHEEL +0 -0
  32. {mt_metadata-0.3.6.dist-info → mt_metadata-0.3.8.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.6"
42
+ __version__ = "0.3.8"
43
43
 
44
44
  # =============================================================================
45
45
  # Imports
@@ -2,7 +2,7 @@
2
2
  """
3
3
  Created on Wed Dec 23 20:37:52 2020
4
4
 
5
- :copyright:
5
+ :copyright:
6
6
  Jared Peacock (jpeacock@usgs.gov)
7
7
 
8
8
  :license: MIT
@@ -637,11 +637,18 @@ def element_to_string(element):
637
637
  # Helper function to be sure everything is encoded properly
638
638
  # =============================================================================
639
639
  class NumpyEncoder(json.JSONEncoder):
640
+
640
641
  """
641
642
  Need to encode numpy ints and floats for json to work
642
643
  """
643
644
 
644
645
  def default(self, obj):
646
+ """
647
+
648
+ :param obj:
649
+ :type obj:
650
+ :return:
651
+ """
645
652
  if isinstance(
646
653
  obj,
647
654
  (
@@ -659,7 +666,7 @@ class NumpyEncoder(json.JSONEncoder):
659
666
  ),
660
667
  ):
661
668
  return int(obj)
662
- elif isinstance(obj, (np.float_, np.float16, np.float32, np.float64)):
669
+ elif isinstance(obj, (np.float16, np.float32, np.float64)):
663
670
  return float(obj)
664
671
  elif isinstance(obj, (np.ndarray)):
665
672
  if obj.dtype == complex:
@@ -17,6 +17,7 @@ from mt_metadata.base.helpers import write_lines
17
17
  from mt_metadata.base import get_schema, Base
18
18
  from mt_metadata.timeseries.standards import SCHEMA_FN_PATHS
19
19
  from mt_metadata.utils.exceptions import MTSchemaError
20
+ from typing import Optional, Union
20
21
 
21
22
  # =============================================================================
22
23
  attr_dict = get_schema("filtered", SCHEMA_FN_PATHS)
@@ -31,6 +32,14 @@ class Filtered(Base):
31
32
  __doc__ = write_lines(attr_dict)
32
33
 
33
34
  def __init__(self, **kwargs):
35
+ """
36
+ Constructor
37
+
38
+ :param kwargs:
39
+
40
+ TODO: Consider not setting self.applied = None, as this has the effect of self._applied = [True,]
41
+ """
42
+ self._applied_values_map = _applied_values_map()
34
43
  self._name = []
35
44
  self._applied = []
36
45
  self.name = None
@@ -53,7 +62,7 @@ class Filtered(Base):
53
62
  elif isinstance(names, list):
54
63
  self._name = [ss.strip().lower() for ss in names]
55
64
  elif isinstance(names, np.ndarray):
56
- names = names.astype(np.unicode_)
65
+ names = names.astype(np.str_)
57
66
  self._name = [ss.strip().lower() for ss in names]
58
67
  else:
59
68
  msg = "names must be a string or list of strings not {0}, type {1}"
@@ -68,25 +77,52 @@ class Filtered(Base):
68
77
  self.logger.warning(msg)
69
78
 
70
79
  @property
71
- def applied(self):
80
+ def applied(self) -> list:
72
81
  return self._applied
73
82
 
74
83
  @applied.setter
75
- def applied(self, applied):
84
+ def applied(
85
+ self,
86
+ applied: Union[list, str, None, int, tuple, np.ndarray, bool],
87
+ ) -> None:
88
+ """
89
+ Sets the value of the booleans for whether each filter has been applied or not
90
+
91
+ :type applied: Union[list, str, None, int, tuple]
92
+ :param applied: The value to set self._applied.
93
+
94
+ Notes:
95
+ self._applied is a list, but we allow this to be assigned by single values as well,
96
+ such as None, True, 0. Supporting these other values makes the logic a little bit involved.
97
+ If a null value is received, the filters are assumed to be applied.
98
+ If a simple value, such as True, None, 0, etc. is not received, the input argument
99
+ applied (which is iterable) is first converted to `applied_list`.
100
+ The values in `applied_list` are then mapped to booleans.
101
+
102
+
103
+ """
104
+ # Handle cases where we did not pass an iterable
76
105
  if not hasattr(applied, "__iter__"):
77
- if applied in [None, "none", "None", "NONE", "null"]:
78
- self._applied = [True]
79
- return
80
- elif applied in [0, "0"]:
81
- self._applied = [False]
82
- return
106
+ self._applied = [self._applied_values_map[applied], ]
107
+ return
108
+
109
+ # the returned type from a hdf5 dataset is a numpy array.
110
+ if isinstance(applied, np.ndarray):
111
+ applied = applied.tolist()
83
112
 
84
113
  #sets an empty list to one default value
85
114
  if isinstance(applied, list) and len(applied) == 0:
86
- self.applied = [True]
115
+ self._applied = [True]
87
116
  return
88
117
 
118
+ # Handle string case
89
119
  if isinstance(applied, str):
120
+ # Handle simple strings
121
+ if applied in self._applied_values_map.keys():
122
+ self._applied = [self._applied_values_map[applied], ]
123
+ return
124
+
125
+ # Handle string-lists (e.g. from json)
90
126
  if applied.find("[") >= 0:
91
127
  applied = applied.replace("[", "").replace("]", "")
92
128
  if applied.count(",") > 0:
@@ -97,44 +133,20 @@ class Filtered(Base):
97
133
  applied_list = [ss.lower() for ss in applied.split()]
98
134
  elif isinstance(applied, list):
99
135
  applied_list = applied
100
- # set integer strings to integers ["0","1"]--> [0, 1]
101
- for i, elt in enumerate(applied_list):
102
- if elt in ["0", "1",]:
103
- applied_list[i] = int(applied_list[i])
104
- # set integers to bools [0,1]--> [False, True]
105
- for i, elt in enumerate(applied_list):
106
- if elt in [0, 1,]:
107
- applied_list[i] = bool(applied_list[i])
108
- elif isinstance(applied, bool):
109
- applied_list = [applied]
110
- # the returned type from a hdf5 dataset is a numpy array.
111
- elif isinstance(applied, np.ndarray):
136
+ elif isinstance(applied, tuple):
112
137
  applied_list = list(applied)
113
- if applied_list == []:
114
- applied_list = [True]
115
138
  else:
116
- msg = "applied must be a string or list of strings not {0}"
117
- self.logger.error(msg.format(applied))
118
- raise MTSchemaError(msg.format(applied))
119
-
120
- bool_list = []
121
- for app_bool in applied_list:
122
- if app_bool is None:
123
- bool_list.append(True)
124
- elif isinstance(app_bool, str):
125
- if app_bool.lower() in ["false", "0"]:
126
- bool_list.append(False)
127
- elif app_bool.lower() in ["true", "1"]:
128
- bool_list.append(True)
129
- else:
130
- msg = "Filter.applied must be [ True | False ], not {0}"
131
- self.logger.error(msg.format(app_bool))
132
- raise MTSchemaError(msg.format(app_bool))
133
- elif isinstance(app_bool, (bool, np.bool_)):
134
- bool_list.append(bool(app_bool))
135
- else:
136
- msg = "Filter.applied must be [True | False], not {0}"
137
- self.logger.error(msg.format(app_bool))
139
+ msg = f"Input applied cannot be of type {type(applied)}"
140
+ self.logger.error(msg)
141
+ raise MTSchemaError(msg)
142
+
143
+ # Now we have a simple list -- map to bools
144
+ try:
145
+ bool_list = [self._applied_values_map[x] for x in applied_list]
146
+ except KeyError:
147
+ msg = f"A key in {applied_list} is not mapped to a boolean"
148
+ msg += "\n fix this by adding to _applied_values_map"
149
+ self.logger.error(msg)
138
150
  self._applied = bool_list
139
151
 
140
152
  # check for consistency
@@ -146,36 +158,82 @@ class Filtered(Base):
146
158
  self.logger.warning(msg)
147
159
 
148
160
 
149
- def _check_consistency(self):
150
- # check for consistency
151
- if self._name != []:
152
- if self._applied is None:
153
- self.logger.warning("Need to input filter.applied")
154
- return False
155
- if len(self._name) == 1:
156
- if len(self._applied) == 1:
157
- return True
158
- elif len(self._name) > 1:
159
- if len(self._applied) == 1:
160
- self.logger.debug(
161
- "Assuming all filters have been "
162
- + "applied as {0}".format(self._applied[0])
163
- )
164
- return True
165
- elif len(self._applied) > 1:
166
- if len(self._applied) != len(self._name):
167
- self.logger.warning(
168
- "Applied and filter names "
169
- + "should be the same length. "
170
- + "Appied={0}, names={1}".format(
171
- len(self._applied), len(self._name)
172
- )
173
- )
174
- return False
175
- else:
176
- return True
177
- elif self._name == [] and len(self._applied) > 0:
161
+ def _check_consistency(self) -> bool:
162
+ """
163
+ Logic to look for inconstencies in the configuration of the filter names and applied values.
164
+
165
+ In general, list of filter names should be same length as list of applied booleans.
166
+
167
+ Cases:
168
+ The filter has no name -- this could happen on intialization.
169
+
170
+ :return: bool
171
+ True if OK, False if not.
172
+
173
+ """
174
+ # This inconsistency is ok -- the filter may not have been assigned a name yet
175
+ if self._name == [] and len(self._applied) > 0:
178
176
  self.logger.debug("Name probably not yet initialized -- skipping consitency check")
179
177
  return True
178
+
179
+ # Otherwise self._name != []
180
+
181
+ # Applied not assigned - this is not OK
182
+ if self._applied is None:
183
+ self.logger.warning("Need to input filter.applied")
184
+ return False
185
+
186
+ # Name and applied have same length, 1. This is OK
187
+ if len(self._name) == 1:
188
+ if len(self._applied) == 1:
189
+ return True
190
+
191
+ # Multiple filter names (name not of length 0 or 1)
192
+ if len(self._name) > 1:
193
+ # If only one applied boolean, we allow it.
194
+ # TODO: consider being less tolerant here
195
+ if len(self._applied) == 1:
196
+ msg = f"Assuming all filters have been applied as {self._applied[0]}"
197
+ self.logger.debug(msg)
198
+ self._applied = len(self.name) * [self._applied[0],]
199
+ msg = f"Explicitly set filter applied state to {self._applied[0]}"
200
+ self.logger.debug(msg)
201
+ return True
202
+ elif len(self._applied) > 1:
203
+ # need to check the lists are really the same length
204
+ if len(self._applied) != len(self._name):
205
+ msg = "Applied and filter names should be the same length. "
206
+ msg += f"Appied={len(self._applied)}, names={len(self._name)}"
207
+ self.logger.warning(msg)
208
+ return False
209
+ else:
210
+ return True
180
211
  else:
212
+ # Some unknown configuration we have not yet encountered
213
+ msg = "Filter consistency check failed for an unknown reason"
214
+ self.logger.warning(msg)
181
215
  return False
216
+
217
+
218
+ def _applied_values_map(
219
+ treat_null_values_as: Optional[bool] = True
220
+ ) -> dict:
221
+ """
222
+ helper function to simplify logic in applied setter.
223
+
224
+ Notes:
225
+ The logic in the setter was getting quite complicated handling many types.
226
+ A reasonable solution seemed to be to map each of the allowed values to a bool
227
+ via dict and then use this dict when setting applied values.
228
+
229
+ :return: dict
230
+ Mapping of all tolerated single-values for setting applied booleans
231
+ """
232
+ null_values = [None, "none", "None", "NONE", "null"]
233
+ null_values_map = {x: treat_null_values_as for x in null_values}
234
+ true_values = [True, 1, "1", "True", "true"]
235
+ true_values_map = {x:True for x in true_values}
236
+ false_values = [False, 0, "0", "False", "false"]
237
+ false_values_map = {x:False for x in false_values}
238
+ values_map = {**null_values_map, **true_values_map, **false_values_map}
239
+ return values_map
@@ -59,6 +59,8 @@ attr_dict.add_dict(get_schema("copyright", SCHEMA_FN_PATHS), None)
59
59
  attr_dict["release_license"]["required"] = False
60
60
  attr_dict.add_dict(get_schema("citation", SCHEMA_FN_PATHS), None, keys=["doi"])
61
61
  attr_dict["doi"]["required"] = False
62
+
63
+
62
64
  # =============================================================================
63
65
  class Station(Base):
64
66
  __doc__ = write_lines(attr_dict)
@@ -316,3 +318,32 @@ class Station(Base):
316
318
  else:
317
319
  if self.time_period.end < max(end):
318
320
  self.time_period.end = max(end)
321
+
322
+ def sort_runs_by_time(self, inplace=True, ascending=True):
323
+ """
324
+ return a list of runs sorted by start time in the order of ascending or
325
+ descending.
326
+
327
+ :param ascending: DESCRIPTION, defaults to True
328
+ :type ascending: TYPE, optional
329
+ :return: DESCRIPTION
330
+ :rtype: TYPE
331
+
332
+ """
333
+
334
+ run_ids = []
335
+ run_starts = []
336
+ for run_key, run_obj in self.runs.items():
337
+ run_ids.append(run_key)
338
+ run_starts.append(run_obj.time_period.start.split("+")[0])
339
+
340
+ index = np.argsort(np.array(run_starts, dtype=np.datetime64))
341
+
342
+ new_runs = ListDict()
343
+ for ii in index:
344
+ new_runs[run_ids[ii]] = self.runs[run_ids[ii]]
345
+
346
+ if inplace:
347
+ self.runs = new_runs
348
+ else:
349
+ return new_runs
@@ -174,6 +174,7 @@ class XMLInventoryMTExperiment:
174
174
  xml_station.site.country = ",".join(
175
175
  [str(country) for country in mt_survey.country]
176
176
  )
177
+ # need to sort the runs by time
177
178
  for mt_run in mt_station.runs:
178
179
  xml_station = self.add_run(
179
180
  xml_station, mt_run, mt_survey.filters
@@ -1,3 +1,41 @@
1
+ # Define allowed sets of channel labellings
2
+ STANDARD_INPUT_CHANNELS = [
3
+ "hx",
4
+ "hy",
5
+ ]
6
+ STANDARD_OUTPUT_CHANNELS = [
7
+ "ex",
8
+ "ey",
9
+ "hz",
10
+ ]
11
+
12
+ CHANNEL_MAPS = {
13
+ "default": {"hx": "hx", "hy": "hy", "hz": "hz", "ex": "ex", "ey": "ey"},
14
+ "lemi12": {"hx": "bx", "hy": "by", "hz": "bz", "ex": "e1", "ey": "e2"},
15
+ "lemi34": {"hx": "bx", "hy": "by", "hz": "bz", "ex": "e3", "ey": "e4"},
16
+ "phoenix123": {"hx": "h1", "hy": "h2", "hz": "h3", "ex": "e1", "ey": "e2"},
17
+ "musgraves": {"hx": "bx", "hy": "by", "hz": "bz", "ex": "ex", "ey": "ey"},
18
+ }
19
+
20
+
21
+ def get_allowed_channel_names(standard_names):
22
+ """
23
+ :param standard_names: one of STANDARD_INPUT_NAMES, or STANDARD_OUTPUT_NAMES
24
+ :type standard_names: list
25
+ :return: allowed_names: list of channel names that are supported
26
+ :rtype: list
27
+ """
28
+ allowed_names = []
29
+ for ch in standard_names:
30
+ for _, channel_map in CHANNEL_MAPS.items():
31
+ allowed_names.append(channel_map[ch])
32
+ allowed_names = list(set(allowed_names))
33
+ return allowed_names
34
+
35
+
36
+ ALLOWED_INPUT_CHANNELS = get_allowed_channel_names(STANDARD_INPUT_CHANNELS)
37
+ ALLOWED_OUTPUT_CHANNELS = get_allowed_channel_names(STANDARD_OUTPUT_CHANNELS)
38
+
1
39
  from .core import TF
2
40
 
3
41
  __all__ = ["TF"]
@@ -274,7 +274,7 @@ class TF:
274
274
  )
275
275
  self.logger.error(msg)
276
276
  raise TypeError(msg)
277
- return run_metadata.copy()
277
+ return run_metadata
278
278
 
279
279
  def _validate_station_metadata(self, station_metadata):
280
280
  """
@@ -298,7 +298,7 @@ class TF:
298
298
  )
299
299
  self.logger.error(msg)
300
300
  raise TypeError(msg)
301
- return station_metadata.copy()
301
+ return station_metadata
302
302
 
303
303
  def _validate_survey_metadata(self, survey_metadata):
304
304
  """
@@ -328,7 +328,7 @@ class TF:
328
328
  )
329
329
  self.logger.error(msg)
330
330
  raise TypeError(msg)
331
- return survey_metadata.copy()
331
+ return survey_metadata
332
332
 
333
333
  ### Properties ------------------------------------------------------------
334
334
  @property
@@ -1475,9 +1475,8 @@ class TF:
1475
1475
  """
1476
1476
  self.station_metadata.id = validate_name(station_name)
1477
1477
  if self.station_metadata.runs[0].id is None:
1478
- r = self.station_metadata.runs[0].copy()
1478
+ r = self.station_metadata.runs.pop(None)
1479
1479
  r.id = f"{self.station_metadata.id}a"
1480
- self.station_metadata.runs.remove(None)
1481
1480
  self.station_metadata.runs.append(r)
1482
1481
 
1483
1482
  @property
@@ -2164,9 +2163,9 @@ class TF:
2164
2163
  """
2165
2164
  zmm_kwargs = {}
2166
2165
  zmm_kwargs["channel_nomenclature"] = self.channel_nomenclature
2167
- zmm_kwargs[
2168
- "inverse_channel_nomenclature"
2169
- ] = self.inverse_channel_nomenclature
2166
+ zmm_kwargs["inverse_channel_nomenclature"] = (
2167
+ self.inverse_channel_nomenclature
2168
+ )
2170
2169
  if hasattr(self, "decimation_dict"):
2171
2170
  zmm_kwargs["decimation_dict"] = self.decimation_dict
2172
2171
  zmm_obj = ZMM(**zmm_kwargs)
@@ -252,6 +252,22 @@ class EDI(object):
252
252
  return 1.0 / self.frequency
253
253
  return None
254
254
 
255
+ def _assert_descending_frequency(self):
256
+ """
257
+ Assert that the transfer function is ordered from high frequency to low
258
+ frequency.
259
+
260
+ """
261
+ if self.frequency[0] < self.frequency[1]:
262
+ self.logger.debug(
263
+ "Ordered arrays to be arranged from high to low frequency"
264
+ )
265
+ self.frequency = self.frequency[::-1]
266
+ self.z = self.z[::-1]
267
+ self.z_err = self.z_err[::-1]
268
+ self.t = self.t[::-1]
269
+ self.t_err = self.t_err[::-1]
270
+
255
271
  def read(self, fn=None, get_elevation=False):
256
272
  """
257
273
  Read in an edi file and fill attributes of each section's classes.
@@ -419,8 +435,7 @@ class EDI(object):
419
435
  )
420
436
  elif key.startswith("t"):
421
437
  obj[:, ii, jj] = (
422
- data_dict[f"{key}r.exp"]
423
- + data_dict[f"{key}i.exp"] * 1j
438
+ data_dict[f"{key}r.exp"] + data_dict[f"{key}i.exp"] * 1j
424
439
  )
425
440
  try:
426
441
  error_key = [
@@ -457,13 +472,8 @@ class EDI(object):
457
472
  except KeyError as error:
458
473
  self.logger.debug(error)
459
474
  # check for order of frequency, we want high togit low
460
- if self.frequency[0] < self.frequency[1]:
461
- self.logger.debug(
462
- "Ordered arrays to be arranged from high to low frequency"
463
- )
464
- self.frequency = self.frequency[::-1]
465
- self.z = self.z[::-1]
466
- self.z_err = self.z_err[::-1]
475
+ self._assert_descending_frequency()
476
+
467
477
  try:
468
478
  self.rotation_angle = np.array(data_dict["zrot"])
469
479
  except KeyError:
@@ -756,10 +766,8 @@ class EDI(object):
756
766
  extra_lines.append(
757
767
  f"\toriginal_program.date={self.Header.progdate}\n"
758
768
  )
759
- if self.Header.fileby != "1980-01-01":
760
- extra_lines.append(
761
- f"\toriginal_file.date={self.Header.filedate}\n"
762
- )
769
+ if self.Header.filedate != "1980-01-01":
770
+ extra_lines.append(f"\toriginal_file.date={self.Header.filedate}\n")
763
771
  header_lines = self.Header.write_header(
764
772
  longitude_format=longitude_format, latlon_format=latlon_format
765
773
  )
@@ -907,15 +915,11 @@ class EDI(object):
907
915
  ]
908
916
  elif data_key.lower() == "freq":
909
917
  block_lines = [
910
- ">{0} // {1:.0f}\n".format(
911
- data_key.upper(), data_comp_arr.size
912
- )
918
+ ">{0} // {1:.0f}\n".format(data_key.upper(), data_comp_arr.size)
913
919
  ]
914
920
  elif data_key.lower() in ["zrot", "trot"]:
915
921
  block_lines = [
916
- ">{0} // {1:.0f}\n".format(
917
- data_key.upper(), data_comp_arr.size
918
- )
922
+ ">{0} // {1:.0f}\n".format(data_key.upper(), data_comp_arr.size)
919
923
  ]
920
924
  else:
921
925
  raise ValueError("Cannot write block for {0}".format(data_key))
@@ -1039,6 +1043,13 @@ class EDI(object):
1039
1043
  if survey.summary != None:
1040
1044
  self.Info.info_list.append(f"survey.summary = {survey.summary}")
1041
1045
 
1046
+ for key in survey.to_dict(single=True).keys():
1047
+ if "northwest" in key or "southeast" in key or "time_period" in key:
1048
+ continue
1049
+ value = survey.get_attr_from_name(key)
1050
+ if value != None:
1051
+ self.Info.info_list.append(f"survey.{key} = {value}")
1052
+
1042
1053
  @property
1043
1054
  def station_metadata(self):
1044
1055
  sm = metadata.Station()
@@ -1192,6 +1203,8 @@ class EDI(object):
1192
1203
  self.Header.datum = sm.location.datum
1193
1204
  self.Header.units = sm.transfer_function.units
1194
1205
  self.Header.enddate = sm.time_period.end
1206
+ if sm.geographic_name is not None:
1207
+ self.Header.loc = sm.geographic_name
1195
1208
 
1196
1209
  ### write notes
1197
1210
  # write comments, which would be anything in the info section from an edi
@@ -1379,3 +1392,11 @@ class EDI(object):
1379
1392
  @property
1380
1393
  def rrhy_metadata(self):
1381
1394
  return self._get_magnetic_metadata("rrhy")
1395
+
1396
+ @property
1397
+ def rrhx_metadata(self):
1398
+ return self._get_magnetic_metadata("rrhx")
1399
+
1400
+ @property
1401
+ def rrhy_metadata(self):
1402
+ return self._get_magnetic_metadata("rrhy")
@@ -441,6 +441,7 @@ class DefineMeasurement(Base):
441
441
  "chtype": channel.component,
442
442
  "id": channel.channel_id,
443
443
  "acqchan": channel.channel_number,
444
+ "dip": channel.measurement_tilt,
444
445
  }
445
446
  )
446
447
  setattr(self, f"meas_{channel.component.lower()}", meas)
@@ -17,6 +17,7 @@ from .standards import SCHEMA_FN_PATHS
17
17
  # =============================================================================
18
18
  attr_dict = get_schema("emeasurement", SCHEMA_FN_PATHS)
19
19
 
20
+
20
21
  # ==============================================================================
21
22
  # magnetic measurements
22
23
  # ==============================================================================
@@ -40,8 +41,9 @@ class EMeasurement(Base):
40
41
 
41
42
  super().__init__(attr_dict=attr_dict, **kwargs)
42
43
 
43
- if self.x != 0 or self.y != 0 or self.x2 != 0 or self.y2 != 0:
44
- self.azm = self.azimuth
44
+ if self.azm == 0:
45
+ if self.x != 0 or self.x2 != 0 or self.y != 0 or self.y2 != 0:
46
+ self.azm = self.azimuth
45
47
 
46
48
  def __str__(self):
47
49
  return "\n".join(
@@ -213,7 +213,7 @@ class Header(Location):
213
213
  self,
214
214
  longitude_format="LON",
215
215
  latlon_format="dms",
216
- required=True,
216
+ required=False,
217
217
  ):
218
218
  """
219
219
  Write header information to a list of lines.
@@ -243,6 +243,8 @@ class Header(Location):
243
243
  for key, value in self.to_dict(single=True, required=required).items():
244
244
  if key in ["x", "x2", "y", "y2", "z", "z2"]:
245
245
  continue
246
+ if value in [None, "None"]:
247
+ continue
246
248
  if key in ["latitude"]:
247
249
  key = "lat"
248
250
  elif key in ["longitude"]:
@@ -10,6 +10,7 @@ Created on Sat Dec 4 14:13:37 2021
10
10
  from mt_metadata.base import Base
11
11
  from mt_metadata.base.helpers import validate_name
12
12
 
13
+
13
14
  # ==============================================================================
14
15
  # Info object
15
16
  # ==============================================================================
@@ -444,11 +445,17 @@ class Information(Base):
444
445
  new_dict[new_key] = value.split()[0]
445
446
  elif key.lower().endswith("sen"):
446
447
  comp = key.lower().split()[0]
447
- new_dict[
448
- f"{comp}.sensor.manufacturer"
449
- ] = "Phoenix Geophysics"
448
+ new_dict[f"{comp}.sensor.manufacturer"] = (
449
+ "Phoenix Geophysics"
450
+ )
450
451
  new_dict[f"{comp}.sensor.type"] = "Induction Coil"
451
452
  new_dict[new_key] = value
453
+ elif new_key in [
454
+ "survey.time_period.start_date",
455
+ "survey.time_period.end_date",
456
+ ]:
457
+ if value.count("-") == 1:
458
+ new_dict[new_key] = value.split("-")[0]
452
459
  else:
453
460
  new_dict[new_key] = value
454
461
 
@@ -461,8 +468,8 @@ class Information(Base):
461
468
  new_dict[key] = value
462
469
 
463
470
  if processing_parameters != []:
464
- new_dict[
465
- "transfer_function.processing_parameters"
466
- ] = processing_parameters
471
+ new_dict["transfer_function.processing_parameters"] = (
472
+ processing_parameters
473
+ )
467
474
 
468
475
  self.info_dict = new_dict