Coverage for C: \ Users \ peaco \ OneDrive \ Documents \ GitHub \ mt_metadata \ mt_metadata \ timeseries \ stationxml \ fdsn_tools.py: 80%
103 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-10 00:11 -0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-10 00:11 -0800
1# -*- coding: utf-8 -*-
2"""
4Tools for FDSN standards
6Created on Wed Sep 30 11:47:01 2020
8:author: Jared Peacock
10:license: MIT
12"""
13import numpy as np
15# =============================================================================
16# Imports
17# =============================================================================
18from loguru import logger
20from mt_metadata.common import LicenseEnum
23# =============================================================================
25# release_dict = {
26# "CC-0": "open",
27# "CC-BY": "open",
28# "CC-BY-SA": "partial",
29# "CC-BY-ND": "partial",
30# "CC-BY-NC-SA": "partial",
31# "CC-BY-NC-NC": "closed",
32# None: "open",
33# "CC 0": "open",
34# "CC BY": "open",
35# "CC BY-SA": "partial",
36# "CC BY-ND": "partial",
37# "CC BY-NC-SA": "partial",
38# "CC BY-NC-NC": "closed",
39# }
41release_dict = {None: "open", "open": "open", "closed": "closed", "partial": "partial"}
42for key in LicenseEnum.__members__.keys():
43 if key.startswith("CC"):
44 if "SA" in key or "NA" in key or "ND" in key or "NC" in key:
45 if key.count("NC") > 1 or "ND" in key:
46 release_dict[key] = "closed"
47 else:
48 release_dict[key] = "partial"
49 else:
50 release_dict[key] = "open"
51 else:
52 release_dict[key] = "partial"
55period_code_dict = {
56 "F": {"min": 1000, "max": 5000},
57 "G": {"min": 1000, "max": 5000},
58 "D": {"min": 250, "max": 1000},
59 "C": {"min": 250, "max": 1000},
60 "E": {"min": 80, "max": 250},
61 "S": {"min": 10, "max": 80},
62 "H": {"min": 80, "max": 250},
63 "B": {"min": 10, "max": 80},
64 "M": {"min": 1, "max": 10},
65 "L": {"min": 0.95, "max": 1.05},
66 "V": {"min": 0.095, "max": 0.105},
67 "U": {"min": 0.0095, "max": 0.0105},
68 "R": {"min": 0.0001, "max": 0.001},
69 "P": {"min": 0.00001, "max": 0.0001},
70 "T": {"min": 0.000001, "max": 0.00001},
71 "Q": {"min": 0, "max": 0.000001},
72}
74measurement_code_dict = {
75 "tilt": "A",
76 "creep": "B",
77 "calibration": "C",
78 "pressure": "D",
79 "magnetic": "F",
80 "gravity": "G",
81 "humidity": "I",
82 "temperature": "K",
83 "water_current": "O",
84 "electric": "Q",
85 "rain_fall": "R",
86 "linear_strain": "S",
87 "tide": "T",
88 "wind": "W",
89}
91measurement_code_dict_reverse = dict([(v, k) for k, v in measurement_code_dict.items()])
92# measurement_code_dict_reverse["T"] = measurement_code_dict_reverse["F"] #HACK
95def angle(value):
96 return abs(np.cos(np.deg2rad(value)))
99# parts of a unit circle
100orientation_code_dict = {
101 "N": {"angle": 0, "variance": 15},
102 "E": {"angle": 90, "variance": 15},
103 "Z": {"angle": 0, "variance": 15},
104 "1": {"angle": 30, "variance": 15},
105 "2": {"angle": 60, "variance": 15},
106 "3": {"angle": 0, "variance": 15},
107}
109mt_components_dict = {
110 "electric": "e",
111 "magnetic": "h",
112 "temperature": "temperature",
113}
115mt_orientation_dict = {
116 "N": "x",
117 "E": "y",
118 "Z": "z",
119 "1": "x",
120 "2": "y",
121 "3": "z",
122}
124forced_orientation = {"x": "N", "y": "E", "z": "Z"}
127def create_location_code(channel_obj):
128 """
129 Get the location code given the components and channel number
131 :param channel_obj: Channel object
132 :type channel_obj: :class:`~mth5.metadata.Channel`
133 :return: 2 character location code
134 :rtype: string
136 """
138 location_code = "{0}{1}".format(
139 channel_obj.component[0].upper(), channel_obj.channel_number % 10
140 )
142 return location_code
145def get_period_code(sample_rate):
146 """
147 Get the SEED sampling rate code given a sample rate
149 :param sample_rate: sample rate in samples per second
150 :type sample_rate: float
151 :return: single character SEED sampling code
152 :rtype: string
154 """
155 period_code = "A"
156 for key, v_dict in sorted(period_code_dict.items()):
157 if (sample_rate >= v_dict["min"]) and (sample_rate <= v_dict["max"]):
158 period_code = key
159 break
160 return period_code
163def get_measurement_code(measurement):
164 """
165 get SEED sensor code given the measurement type
167 :param measurement: measurement type, e.g.
168 * temperature
169 * electric
170 * magnetic
171 :type measurement: string
172 :return: single character SEED sensor code, if the measurement type has
173 not been defined yet Y is returned.
174 :rtype: string
176 """
177 sensor_code = "Y"
178 for key, code in measurement_code_dict.items():
179 if measurement.lower() in key:
180 sensor_code = code
181 return sensor_code
184def get_orientation_code(azimuth=None, direction=None, orientation="horizontal"):
185 """
186 Get orientation code given angle and orientation. This is a general
187 code and the true azimuth is stored in channel
189 :param azimuth: angel assuming 0 is north, 90 is east, 0 is vertical down
190 :type azimuth: float
191 :param direction: character nominated direction [ x | y | z ]
192 :return: single character SEED orientation code
193 :rtype: string
195 """
196 if azimuth is not None:
197 # angles are only from 0 to 360
198 azimuth = azimuth % 360
200 value = abs(np.cos(np.deg2rad(azimuth)))
202 if orientation == "horizontal":
203 if value >= angle(15):
204 return "N"
205 elif value <= angle(105):
206 return "E"
207 elif (value < angle(15)) and (value >= angle(45)):
208 return "1"
209 elif (value < angle(45)) and (value >= angle(105)):
210 return "2"
212 elif orientation == "vertical":
213 if value >= angle(15):
214 return "Z"
215 else:
216 return "3"
218 elif direction is not None:
219 try:
220 return forced_orientation[direction.lower()]
221 except KeyError:
222 raise ValueError(
223 f"Could not match {direction} with allowed direction (x, y, z)"
224 )
227def make_channel_code(sample_rate, measurement_type, azimuth, orientation="horizontal"):
228 """
230 Make channel code from given parameters
232 :param sample_rate: sample rate in samples per second
233 :type sample_rate: float
234 :param measurement_type: type of measurement, e.g. 'electric'
235 :type measurement_type: string
236 :param orientation: orientation azimuth (degrees)
237 :type orientation: float
238 :return: three letter channel code
239 :rtype: string
241 """
243 period_code = get_period_code(sample_rate)
244 sensor_code = get_measurement_code(measurement_type)
245 if isinstance(azimuth, (float, int)):
246 orientation_code = get_orientation_code(azimuth, orientation=orientation)
247 elif isinstance(azimuth, (str)):
248 orientation_code = get_orientation_code(direction=azimuth)
250 channel_code = f"{period_code}{sensor_code}{orientation_code}"
252 return channel_code
255def read_channel_code(channel_code):
256 """
257 read FDSN channel code
259 :param channel_code: Three character string {Period}{Component}{Orientation}
260 :type channel_code: string
261 :return: DESCRIPTION
262 :rtype: TYPE
264 """
266 if len(channel_code) != 3:
267 msg = "Input FDSN channel code is not proper format, should be 3 letters"
268 logger.error(msg)
269 raise ValueError(msg)
271 try:
272 period_range = period_code_dict[channel_code[0]]
273 except KeyError:
274 msg = (
275 f"Could not find period range for {channel_code[0]}. ",
276 "Setting to 1",
277 )
278 period_range = {"min": 1, "max": 1}
280 try:
281 component = measurement_code_dict_reverse[channel_code[1]]
282 if component in ["tide"]:
283 logger.warning(
284 "Found channel code `Tide` Assuming older data changing to `magnetic`"
285 )
286 component = "magnetic"
287 except KeyError:
288 msg = f"Could not find component for {channel_code[1]}"
289 logger.error(msg)
290 raise ValueError(msg)
292 try:
293 orientation = orientation_code_dict[channel_code[2]]
294 except KeyError:
295 msg = (
296 f"Could not find orientation for {channel_code[2]}. ",
297 "Setting to 0.",
298 )
299 logger.error(msg)
300 raise ValueError(msg)
302 return {
303 "period": period_range,
304 "measurement": component,
305 "orientation": orientation,
306 }
309def create_mt_component(channel_code):
310 """
311 :param channel_code: Three character string {Period}{Component}{Orientation}
312 :type channel_code: string
313 :return: DESCRIPTION
314 :rtype: TYPE
316 Create a component for an MT channel given the measurement and orientation
318 >>> create_mt_component("LQN")
319 ex
321 """
322 code_dict = read_channel_code(channel_code)
323 if code_dict["measurement"] == "tide":
324 msg = (
325 "Channel code indicates tidal data -- Some historial MT data (PKD, "
326 "SAO) used 'T' as the code for feedback coil magnetometers"
327 )
328 logger.warning(msg)
329 code_dict = read_channel_code(channel_code.replace("T", "F"))
331 mt_component = mt_components_dict[code_dict["measurement"]]
332 mt_orientation = mt_orientation_dict[channel_code[2]]
334 return f"{mt_component}{mt_orientation}"