Coverage for C: \ Users \ peaco \ OneDrive \ Documents \ GitHub \ mth5 \ mth5 \ clients \ phoenix.py: 93%

91 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-27 20:09 -0800

1#!/usr/bin/env python 

2# coding: utf-8 

3 

4# # Make an MTH5 from Phoenix Data 

5# 

6# This example demonstrates how to read Phoenix data into an MTH5 file. The data comes from example data in [PhoenixGeoPy](https://github.com/torresolmx/PhoenixGeoPy). Here I downloaded those data into a local folder on my computer by forking the main branch. 

7 

8# ## Imports 

9 

10# ============================================================================= 

11# Imports 

12# ============================================================================= 

13from pathlib import Path 

14 

15from mt_metadata.timeseries import AppliedFilter 

16 

17from mth5 import read_file 

18from mth5.clients.base import ClientBase 

19from mth5.io.phoenix import PhoenixCollection 

20from mth5.io.phoenix.readers.calibrations import PhoenixCalibration 

21from mth5.mth5 import MTH5 

22 

23 

24# ============================================================================= 

25 

26 

27class PhoenixClient(ClientBase): 

28 def __init__( 

29 self, 

30 data_path: str | Path, 

31 sample_rates: list[int] = [150, 24000], 

32 save_path: str | Path | None = None, 

33 receiver_calibration_dict: dict | str | Path = {}, 

34 sensor_calibration_dict: dict | str | Path = {}, 

35 mth5_filename: str = "from_phoenix.h5", 

36 **kwargs: dict, 

37 ) -> None: 

38 super().__init__( 

39 data_path, 

40 save_path=save_path, 

41 sample_rates=sample_rates, 

42 mth5_filename=mth5_filename, 

43 **kwargs, 

44 ) 

45 

46 self.receiver_calibration_dict = receiver_calibration_dict 

47 self.sensor_calibration_dict = sensor_calibration_dict 

48 

49 self.collection = PhoenixCollection(self.data_path) 

50 

51 @property 

52 def receiver_calibration_dict(self) -> dict: 

53 """Receiver calibrations. 

54 

55 Returns 

56 ------- 

57 dict 

58 Dictionary mapping receiver IDs to calibration file paths. 

59 

60 Examples 

61 -------- 

62 >>> client = PhoenixClient('data/path') 

63 >>> client.receiver_calibration_dict = {'RX001': Path('RX001_rxcal.json')} 

64 >>> client.receiver_calibration_dict 

65 {'RX001': Path('RX001_rxcal.json')} 

66 """ 

67 return self._receiver_calibration_dict 

68 

69 @receiver_calibration_dict.setter 

70 def receiver_calibration_dict(self, value: dict | str | Path) -> None: 

71 """Set receiver calibration dictionary from dict or path. 

72 

73 Parameters 

74 ---------- 

75 value : dict or str or Path 

76 Dictionary of calibrations or path to calibration files. 

77 

78 Raises 

79 ------ 

80 TypeError 

81 If value is not a dict, str, or Path. 

82 

83 Examples 

84 -------- 

85 >>> client = PhoenixClient('data/path') 

86 >>> client.receiver_calibration_dict = 'calibrations/' 

87 >>> client.receiver_calibration_dict # doctest: +SKIP 

88 {'RX001': Path('calibrations/RX001_rxcal.json'), ...} 

89 """ 

90 self._receiver_calibration_dict = {} 

91 if isinstance(value, dict): 

92 self._receiver_calibration_dict = value 

93 elif isinstance(value, (str, Path)): 

94 receiver_path = Path(value) 

95 if receiver_path.is_dir(): 

96 self._receiver_calibration_dict = {} 

97 for fn in list(receiver_path.glob("*.rxcal.json")) + list( 

98 receiver_path.glob("*.rx_cal.json") 

99 ): 

100 self._receiver_calibration_dict[fn.stem.split("_")[0]] = fn 

101 elif receiver_path.is_file(): 

102 self._receiver_calibration_dict = {} 

103 key = receiver_path.stem.split("_")[0] 

104 self._receiver_calibration_dict[key] = receiver_path 

105 else: 

106 raise TypeError(f"type {type(value)} not supported.") 

107 

108 @property 

109 def sensor_calibration_dict(self) -> dict: 

110 """Sensor calibration dictionary. 

111 

112 Returns 

113 ------- 

114 dict 

115 Dictionary mapping sensor IDs to PhoenixCalibration objects. 

116 

117 Examples 

118 -------- 

119 >>> client = PhoenixClient('data/path') 

120 >>> client.sensor_calibration_dict = {'H001': PhoenixCalibration('H001_scal.json')} 

121 >>> client.sensor_calibration_dict['H001'] # doctest: +SKIP 

122 <PhoenixCalibration object> 

123 """ 

124 return self._sensor_calibration_dict 

125 

126 @sensor_calibration_dict.setter 

127 def sensor_calibration_dict(self, value: dict | str | Path) -> None: 

128 """Set sensor calibration dictionary from dict or path. 

129 

130 Parameters 

131 ---------- 

132 value : dict or str or Path 

133 Dictionary of calibrations or path to calibration files. 

134 

135 Raises 

136 ------ 

137 ValueError 

138 If value is not a dict, str, or Path. 

139 

140 Examples 

141 -------- 

142 >>> client = PhoenixClient('data/path') 

143 >>> client.sensor_calibration_dict = 'calibrations/' 

144 >>> client.sensor_calibration_dict # doctest: +SKIP 

145 {'H001': <PhoenixCalibration object>, ...} 

146 """ 

147 if isinstance(value, dict): 

148 self._sensor_calibration_dict = value 

149 elif isinstance(value, (str, Path)): 

150 self._sensor_calibration_dict = {} 

151 cal_path = Path(value) 

152 if cal_path.is_dir(): 

153 for fn in cal_path.glob("*scal.json"): 

154 self._sensor_calibration_dict[ 

155 fn.stem.split("_")[0] 

156 ] = PhoenixCalibration(fn) 

157 elif cal_path.is_file(): 

158 if not cal_path.exists(): 

159 raise IOError(f"Could not find {cal_path}") 

160 key = cal_path.stem.split("_")[0] 

161 self._sensor_calibration_dict[key] = PhoenixCalibration(cal_path) 

162 else: 

163 raise ValueError("calibration_path cannot be None") 

164 

165 def make_mth5_from_phoenix(self, **kwargs: dict) -> str | Path | None: 

166 """Make an MTH5 from Phoenix files. 

167 

168 Split into runs, account for filters. Updates the MTH5 file with Phoenix data. 

169 

170 Parameters 

171 ---------- 

172 **kwargs : dict 

173 Optional keyword arguments to override instance attributes. 

174 

175 Returns 

176 ------- 

177 str, Path, or None 

178 Path to the saved MTH5 file. 

179 

180 Examples 

181 -------- 

182 >>> client = PhoenixClient('data/path', save_path='output.h5') 

183 >>> client.make_mth5_from_phoenix() 

184 'output.h5' 

185 """ 

186 for key, value in kwargs.items(): 

187 if value is not None: 

188 setattr(self, key, value) 

189 

190 run_dict = self.get_run_dict() 

191 

192 with MTH5(**self.h5_kwargs) as m: 

193 m.open_mth5(self.save_path, self.mth5_file_mode) 

194 

195 for station_id, station_dict in run_dict.items(): 

196 collection_metadata = self.collection.metadata_dict[station_id] 

197 survey_metadata = collection_metadata.survey_metadata 

198 survey_group = m.add_survey(survey_metadata.id) 

199 

200 station_metadata = self.collection.metadata_dict[ 

201 station_id 

202 ].station_metadata 

203 station_group = survey_group.stations_group.add_station( 

204 station_metadata.id, station_metadata=station_metadata 

205 ) 

206 for run_id, run_df in station_dict.items(): 

207 run_metadata = collection_metadata.run_metadata 

208 run_metadata.id = run_id 

209 run_metadata.sample_rate = float(run_df.sample_rate.unique()[0]) 

210 

211 run_group = station_group.add_run( 

212 run_metadata.id, run_metadata=run_metadata 

213 ) 

214 for row in run_df.itertuples(): 

215 try: 

216 ch_ts = read_file( 

217 row.fn, 

218 **{ 

219 "channel_map": collection_metadata.channel_map, 

220 "rxcal_fn": self.receiver_calibration_dict[ 

221 collection_metadata.instrument_id 

222 ], 

223 }, 

224 ) 

225 except OSError: 

226 print(f"OSError: skipping {row.fn.name} likely too small") 

227 continue 

228 

229 if ch_ts.component in ["h1", "h2", "h3"]: 

230 # for phx coils from generic response curves 

231 if ( 

232 ch_ts.channel_metadata.sensor.id 

233 in self.sensor_calibration_dict.keys() 

234 ): 

235 pc = self.sensor_calibration_dict[ 

236 ch_ts.channel_metadata.sensor.id 

237 ] 

238 for key in pc.__dict__.keys(): 

239 if key.startswith("h"): 

240 break 

241 coil_fap = getattr(pc, key) 

242 

243 # add filter 

244 applied_filter = AppliedFilter( 

245 name=coil_fap.name, applied=True, stage=1 

246 ) 

247 ch_ts.channel_metadata.add_filter(applied_filter) 

248 ch_ts.channel_response.filters_list.append(coil_fap) 

249 else: 

250 self.logger.warning( 

251 f"Could not find coil {ch_ts.channel_metadata.sensor.id} in sensor calibrations." 

252 ) 

253 

254 # add channel to the run group 

255 run_group.from_channel_ts(ch_ts) 

256 

257 run_group.update_metadata() 

258 

259 station_group.update_metadata() 

260 survey_group.update_metadata() 

261 

262 return self.save_path