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
« 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====================
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
11Created on Tue Jun 11 10:53:23 2013
12Updated August 2020 (JP)
14:copyright:
15 Jared Peacock (jpeacock@usgs.gov)
17:license:
18 MIT
20"""
22# ==============================================================================
23from __future__ import annotations
25from pathlib import Path
26from typing import Any, BinaryIO
28import numpy as np
29from loguru import logger
32# ==============================================================================
33class Z3DHeader:
34 """
35 Read header information from a Z3D file and make each metadata entry an attribute.
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.
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.
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 """
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
119 self.fn: str | Path | None = fn
120 self.fid: BinaryIO | None = fid
122 self.header_str: bytes | None = None
123 self._header_len: int = 512
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
149 for key in kwargs:
150 setattr(self, key, kwargs[key])
152 @property
153 def data_logger(self) -> str:
154 """
155 Data logger name as ZEN{box_number}.
157 Returns
158 -------
159 str
160 Data logger name formatted as 'ZEN' followed by zero-padded box number.
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}"
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.
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.
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.
187 Raises
188 ------
189 UnicodeDecodeError
190 If header bytes cannot be decoded as text.
192 Notes
193 -----
194 This method reads the first 512 bytes of the Z3D file as the header.
195 It supports two formats:
197 1. Modern format: key=value pairs separated by newlines
198 2. Legacy format: comma-separated key:value pairs
200 The method automatically detects legacy format and sets old_version=True.
202 Coordinate values (lat/long) are automatically converted from radians
203 to degrees, with validation to ensure they fall within valid ranges.
205 Examples
206 --------
207 >>> header_obj = Z3DHeader()
208 >>> header_obj.read_header("/path/to/file.Z3D")
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)
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.
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.
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.
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.
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
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 """
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