Coverage for C: \ Users \ peaco \ OneDrive \ Documents \ GitHub \ mth5 \ mth5 \ io \ zen \ z3d_header.py: 97%

95 statements  

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

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

2""" 

3==================== 

4Zen Header 

5==================== 

6 

7 * Tools for reading and writing files for Zen and processing software 

8 * Tools for copying data from SD cards 

9 * Tools for copying schedules to SD cards 

10 

11Created on Tue Jun 11 10:53:23 2013 

12Updated August 2020 (JP) 

13 

14:copyright: 

15 Jared Peacock (jpeacock@usgs.gov) 

16 

17:license: 

18 MIT 

19 

20""" 

21 

22# ============================================================================== 

23from __future__ import annotations 

24 

25from pathlib import Path 

26from typing import Any, BinaryIO 

27 

28import numpy as np 

29from loguru import logger 

30 

31 

32# ============================================================================== 

33class Z3DHeader: 

34 """ 

35 Read header information from a Z3D file and make each metadata entry an attribute. 

36 

37 Parameters 

38 ---------- 

39 fn : str | pathlib.Path, optional 

40 Full path to Z3D file. 

41 fid : BinaryIO, optional 

42 File object (e.g., open(Z3Dfile, 'rb')). 

43 **kwargs : dict 

44 Additional keyword arguments to set as attributes. 

45 

46 Attributes 

47 ---------- 

48 _header_len : int 

49 Length of header in bits (512). 

50 ad_gain : float | None 

51 Gain of channel. 

52 ad_rate : float | None 

53 Sampling rate in Hz. 

54 alt : float | None 

55 Altitude of the station (not reliable). 

56 attenchannelsmask : str | None 

57 Attenuation channels mask. 

58 box_number : float | None 

59 ZEN box number. 

60 box_serial : str | None 

61 ZEN box serial number. 

62 channel : float | None 

63 Channel number of the file. 

64 channelserial : str | None 

65 Serial number of the channel board. 

66 ch_factor : float 

67 Channel factor (default 9.536743164062e-10). 

68 channelgain : float 

69 Channel gain (default 1.0). 

70 duty : float | None 

71 Duty cycle of the transmitter. 

72 fid : BinaryIO | None 

73 File object. 

74 fn : str | pathlib.Path | None 

75 Full path to Z3D file. 

76 fpga_buildnum : float | None 

77 Build number of one of the boards. 

78 gpsweek : int 

79 GPS week (default 1740). 

80 header_str : bytes | None 

81 Full header string. 

82 lat : float | None 

83 Latitude of station in degrees. 

84 logterminal : str | None 

85 Log terminal setting. 

86 long : float | None 

87 Longitude of the station in degrees. 

88 main_hex_buildnum : float | None 

89 Build number of the ZEN box in hexadecimal. 

90 numsats : float | None 

91 Number of GPS satellites. 

92 old_version : bool 

93 Whether this is an old version Z3D file (default False). 

94 period : float | None 

95 Period of the transmitter. 

96 tx_duty : float | None 

97 Transmitter duty cycle. 

98 tx_freq : float | None 

99 Transmitter frequency. 

100 version : float | None 

101 Version of the firmware. 

102 

103 Examples 

104 -------- 

105 >>> from mth5.io.zen import Z3DHeader 

106 >>> Z3Dfn = r"/home/mt/mt01/mt01_20150522_080000_256_EX.Z3D" 

107 >>> header_obj = Z3DHeader(fn=Z3Dfn) 

108 >>> header_obj.read_header() 

109 """ 

110 

111 def __init__( 

112 self, 

113 fn: str | Path | None = None, 

114 fid: BinaryIO | None = None, 

115 **kwargs: Any, 

116 ) -> None: 

117 self.logger = logger 

118 

119 self.fn: str | Path | None = fn 

120 self.fid: BinaryIO | None = fid 

121 

122 self.header_str: bytes | None = None 

123 self._header_len: int = 512 

124 

125 self.ad_gain: float | None = None 

126 self.ad_rate: float | None = None 

127 self.alt: float | None = None 

128 self.attenchannelsmask: str | None = None 

129 self.box_number: float | None = None 

130 self.box_serial: str | None = None 

131 self.channel: float | None = None 

132 self.channelserial: str | None = None 

133 self.duty: float | None = None 

134 self.fpga_buildnum: float | None = None 

135 self.gpsweek: int = 1740 

136 self.lat: float | None = None 

137 self.logterminal: str | None = None 

138 self.long: float | None = None 

139 self.main_hex_buildnum: float | None = None 

140 self.numsats: float | None = None 

141 self.period: float | None = None 

142 self.tx_duty: float | None = None 

143 self.tx_freq: float | None = None 

144 self.version: float | None = None 

145 self.old_version: bool = False 

146 self.ch_factor: float = 9.536743164062e-10 

147 self.channelgain: float = 1.0 

148 

149 for key in kwargs: 

150 setattr(self, key, kwargs[key]) 

151 

152 @property 

153 def data_logger(self) -> str: 

154 """ 

155 Data logger name as ZEN{box_number}. 

156 

157 Returns 

158 ------- 

159 str 

160 Data logger name formatted as 'ZEN' followed by zero-padded box number. 

161 

162 Raises 

163 ------ 

164 TypeError 

165 If box_number is None or cannot be converted to int. 

166 """ 

167 return f"ZEN{int(self.box_number):03}" 

168 

169 def read_header( 

170 self, fn: str | Path | None = None, fid: BinaryIO | None = None 

171 ) -> None: 

172 """ 

173 Read the header information into appropriate attributes. 

174 

175 Parses the header information from a Z3D file and populates the object's 

176 attributes with the extracted values. Supports both modern and legacy 

177 Z3D file formats. 

178 

179 Parameters 

180 ---------- 

181 fn : str | pathlib.Path, optional 

182 Full path to Z3D file. If None, uses the instance's fn attribute. 

183 fid : BinaryIO, optional 

184 File object (e.g., open(Z3Dfile, 'rb')). If None, uses the instance's 

185 fid attribute or opens the file specified by fn. 

186 

187 Raises 

188 ------ 

189 UnicodeDecodeError 

190 If header bytes cannot be decoded as text. 

191 

192 Notes 

193 ----- 

194 This method reads the first 512 bytes of the Z3D file as the header. 

195 It supports two formats: 

196 

197 1. Modern format: key=value pairs separated by newlines 

198 2. Legacy format: comma-separated key:value pairs 

199 

200 The method automatically detects legacy format and sets old_version=True. 

201 

202 Coordinate values (lat/long) are automatically converted from radians 

203 to degrees, with validation to ensure they fall within valid ranges. 

204 

205 Examples 

206 -------- 

207 >>> header_obj = Z3DHeader() 

208 >>> header_obj.read_header("/path/to/file.Z3D") 

209 

210 >>> with open("/path/to/file.Z3D", "rb") as fid: 

211 ... header_obj.read_header(fid=fid) 

212 """ 

213 if fn is not None: 

214 self.fn = fn 

215 if fid is not None: 

216 self.fid = fid 

217 if self.fn is None and self.fid is None: 

218 self.logger.warning("No Z3D file to read.") 

219 elif self.fn is None: 

220 if self.fid is not None: 

221 self.fid.seek(0) 

222 self.header_str = self.fid.read(self._header_len) 

223 elif self.fn is not None: 

224 if self.fid is None: 

225 self.fid = open(self.fn, "rb") 

226 self.header_str = self.fid.read(self._header_len) 

227 else: 

228 self.fid.seek(0) 

229 self.header_str = self.fid.read(self._header_len) 

230 header_list = self.header_str.split(b"\n") 

231 for h_str in header_list: 

232 h_str = h_str.decode() 

233 if h_str.find("=") > 0: 

234 h_list = h_str.split("=") 

235 h_key = h_list[0].strip().lower() 

236 h_key = h_key.replace(" ", "_").replace("/", "").replace(".", "_") 

237 h_value = self.convert_value(h_key, h_list[1].strip()) 

238 setattr(self, h_key, h_value) 

239 elif len(h_str) == 0: 

240 continue 

241 # need to adjust for older versions of z3d files 

242 elif h_str.count(",") > 1: 

243 self.old_version = True 

244 if h_str.find("Schedule") >= 0: 

245 h_str = h_str.replace(",", "T", 1) 

246 for hh in h_str.split(","): 

247 if hh.find(";") > 0: 

248 m_key, m_value = hh.split(";")[1].split(":") 

249 elif len(hh.split(":", 1)) == 2: 

250 m_key, m_value = hh.split(":", 1) 

251 else: 

252 self.logger.warning("found %s", hh) 

253 m_key = ( 

254 m_key.strip() 

255 .lower() 

256 .replace(" ", "_") 

257 .replace("/", "") 

258 .replace(".", "_") 

259 ) 

260 m_value = self.convert_value(m_key, m_value.strip()) 

261 setattr(self, m_key, m_value) 

262 

263 def convert_value(self, key_string: str, value_string: str) -> float | str: 

264 """ 

265 Convert the value to the appropriate units given the key. 

266 

267 Converts string values to appropriate types based on the key name. 

268 Special handling is provided for latitude and longitude values, which 

269 are converted from radians to degrees with validation. 

270 

271 Parameters 

272 ---------- 

273 key_string : str 

274 The metadata key name, used to determine conversion type. 

275 value_string : str 

276 The string value to convert. 

277 

278 Returns 

279 ------- 

280 float or str 

281 Converted value. Returns float for numeric values, str for 

282 non-numeric values. Latitude and longitude values are converted 

283 from radians to degrees. 

284 

285 Notes 

286 ----- 

287 - Attempts to convert all values to float first 

288 - If conversion fails, returns original string 

289 - For keys containing 'lat', 'lon', or 'long': 

290 - Converts from radians to degrees using np.rad2deg 

291 - Validates latitude range (±90°), sets to 0.0 if invalid 

292 - Validates longitude range (±180°), sets to 0.0 if invalid 

293 

294 Examples 

295 -------- 

296 >>> header = Z3DHeader() 

297 >>> header.convert_value("version", "4147") 

298 4147.0 

299 >>> header.convert_value("lat", "0.706816081") # radians 

300 40.49757833327694 # degrees 

301 >>> header.convert_value("channelserial", "0xD474777C") 

302 '0xD474777C' 

303 """ 

304 

305 try: 

306 return_value = float(value_string) 

307 except ValueError: 

308 return_value = value_string 

309 if key_string.lower() in ["lat", "lon", "long"]: 

310 return_value = np.rad2deg(float(value_string)) 

311 if "lat" in key_string.lower(): 

312 if abs(return_value) > 90: 

313 return_value = 0.0 

314 elif "lon" in key_string.lower(): 

315 if abs(return_value) > 180: 

316 return_value = 0.0 

317 return return_value