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

1# -*- coding: utf-8 -*- 

2""" 

3 

4Tools for FDSN standards 

5 

6Created on Wed Sep 30 11:47:01 2020 

7 

8:author: Jared Peacock 

9 

10:license: MIT 

11 

12""" 

13import numpy as np 

14 

15# ============================================================================= 

16# Imports 

17# ============================================================================= 

18from loguru import logger 

19 

20from mt_metadata.common import LicenseEnum 

21 

22 

23# ============================================================================= 

24 

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# } 

40 

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" 

53 

54 

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} 

73 

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} 

90 

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 

93 

94 

95def angle(value): 

96 return abs(np.cos(np.deg2rad(value))) 

97 

98 

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} 

108 

109mt_components_dict = { 

110 "electric": "e", 

111 "magnetic": "h", 

112 "temperature": "temperature", 

113} 

114 

115mt_orientation_dict = { 

116 "N": "x", 

117 "E": "y", 

118 "Z": "z", 

119 "1": "x", 

120 "2": "y", 

121 "3": "z", 

122} 

123 

124forced_orientation = {"x": "N", "y": "E", "z": "Z"} 

125 

126 

127def create_location_code(channel_obj): 

128 """ 

129 Get the location code given the components and channel number 

130 

131 :param channel_obj: Channel object 

132 :type channel_obj: :class:`~mth5.metadata.Channel` 

133 :return: 2 character location code 

134 :rtype: string 

135 

136 """ 

137 

138 location_code = "{0}{1}".format( 

139 channel_obj.component[0].upper(), channel_obj.channel_number % 10 

140 ) 

141 

142 return location_code 

143 

144 

145def get_period_code(sample_rate): 

146 """ 

147 Get the SEED sampling rate code given a sample rate 

148 

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 

153 

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 

161 

162 

163def get_measurement_code(measurement): 

164 """ 

165 get SEED sensor code given the measurement type 

166 

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 

175 

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 

182 

183 

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 

188 

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 

194 

195 """ 

196 if azimuth is not None: 

197 # angles are only from 0 to 360 

198 azimuth = azimuth % 360 

199 

200 value = abs(np.cos(np.deg2rad(azimuth))) 

201 

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" 

211 

212 elif orientation == "vertical": 

213 if value >= angle(15): 

214 return "Z" 

215 else: 

216 return "3" 

217 

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 ) 

225 

226 

227def make_channel_code(sample_rate, measurement_type, azimuth, orientation="horizontal"): 

228 """ 

229 

230 Make channel code from given parameters 

231 

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 

240 

241 """ 

242 

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) 

249 

250 channel_code = f"{period_code}{sensor_code}{orientation_code}" 

251 

252 return channel_code 

253 

254 

255def read_channel_code(channel_code): 

256 """ 

257 read FDSN channel code 

258 

259 :param channel_code: Three character string {Period}{Component}{Orientation} 

260 :type channel_code: string 

261 :return: DESCRIPTION 

262 :rtype: TYPE 

263 

264 """ 

265 

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) 

270 

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} 

279 

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) 

291 

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) 

301 

302 return { 

303 "period": period_range, 

304 "measurement": component, 

305 "orientation": orientation, 

306 } 

307 

308 

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 

315 

316 Create a component for an MT channel given the measurement and orientation 

317 

318 >>> create_mt_component("LQN") 

319 ex 

320 

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")) 

330 

331 mt_component = mt_components_dict[code_dict["measurement"]] 

332 mt_orientation = mt_orientation_dict[channel_code[2]] 

333 

334 return f"{mt_component}{mt_orientation}"