diff --git a/micromate/idf_file.py b/micromate/idf_file.py index eb8d70d..f3db878 100644 --- a/micromate/idf_file.py +++ b/micromate/idf_file.py @@ -438,6 +438,10 @@ def read_idf_file( peak_tran = max((iv.peak_ips("Tran") for iv in intervals), default=0.0) peak_vert = max((iv.peak_ips("Vert") for iv in intervals), default=0.0) peak_long = max((iv.peak_ips("Long") for iv in intervals), default=0.0) + # Mic peak in psi — Thor stores per-interval mic ADC counts in the + # binary; convert the max count to psi via the per-count factor. + mic_peak_count = max((iv.peak_count("MicL") for iv in intervals), default=0) + mic_peak_psi = mic_count_to_psi(mic_peak_count) if mic_peak_count else None rep = IdfReport( serial_number=md.serial, event_type="Full Histogram", @@ -451,7 +455,8 @@ def read_idf_file( vertical_ips=peak_vert, longitudinal_ips=peak_long, peak_vector_sum_ips=None, - mic_pspl_dbl=None, + mic_pspl_dbl=None, # IDFH binary doesn't carry the dB(L) value + mic_pspl_psi=mic_peak_psi, ) event = IdfEvent( serial=md.serial or "UNKNOWN", @@ -489,6 +494,11 @@ def read_idf_file( arr = decoded.get(ch, []) return geo_count_to_ips(max((abs(v) for v in arr), default=0)) + # Mic peak psi from binary: max absolute MicL ADC count × 2.14e-6 psi/count. + mic_arr = decoded.get("MicL", []) + mic_peak_count = max((abs(v) for v in mic_arr), default=0) + mic_peak_psi = mic_count_to_psi(mic_peak_count) if mic_peak_count else None + peaks = IdfPeaks( transverse_ips=_peak_ips("Tran"), vertical_ips=_peak_ips("Vert"), @@ -496,7 +506,9 @@ def read_idf_file( # PVS requires aligned per-sample √(T²+V²+L²); leave None — the # sidecar carries it and the bridge picks it up if present. peak_vector_sum_ips=None, - mic_pspl_dbl=None, + mic_pspl_dbl=None, # binary IDFW doesn't carry the dB(L) value; + # sidecar .txt fills it via IdfReport.from_dict + mic_pspl_psi=mic_peak_psi, ) event = IdfEvent( diff --git a/micromate/models.py b/micromate/models.py index d02a37f..68a91a7 100644 --- a/micromate/models.py +++ b/micromate/models.py @@ -159,12 +159,23 @@ class IdfReport: @dataclass class IdfPeaks: - """Geophone + mic peak values for one Thor event. Native Thor units.""" + """Geophone + mic peak values for one Thor event. Native Thor units. + + Thor stores the mic peak in two parallel forms — ``mic_pspl_dbl`` is + what the sidecar's top-level ``MicPSPL`` header field carries (dB(L)), + used in the report header. ``mic_pspl_psi`` is the psi value derived + either from the IDFW sample table / IDFH interval column 9, or from + the binary mic counts (~2.14e-6 psi/count). Needed because the + BW-shaped ``PeakValues.micl`` consumed by ``event_hdf5.write_event_hdf5`` + expects psi — feeding it dB(L) makes the h5 mic-chart scale factor + blow up. + """ transverse_ips: Optional[float] = None # in/s vertical_ips: Optional[float] = None # in/s longitudinal_ips: Optional[float] = None # in/s peak_vector_sum_ips: Optional[float] = None # in/s mic_pspl_dbl: Optional[float] = None # dB(L) + mic_pspl_psi: Optional[float] = None # psi @dataclass @@ -324,10 +335,14 @@ class IdfEvent: machinery without those code paths needing to know about Thor. Caveats of the bridge: - - ``mic_ppv`` on the produced Event carries Thor's dB(L) value - verbatim — the UI distinguishes via the ``device_family`` - column (Phase 1). Don't run the BW psi→dBL converter on - Series IV rows. + - ``PeakValues.micl`` carries the mic peak in **psi** (matching + BW's convention) — set from :attr:`IdfPeaks.mic_pspl_psi`, + with a dB(L)→psi fallback when only the dB(L) value is + available. This is what the h5 writer's mic-scale-factor + logic needs. The dB(L) value still flows through + ``bw_report.mic.pspl_dbl`` (set by the + ``idf_to_bw_report`` adapter) and the renderer reads it + from there for the report header. - Many Thor-specific fields (Peak Acceleration / Displacement, sensor self-check, calibration) don't have a slot in ``Event``. The full IdfReport is preserved on the @@ -349,11 +364,17 @@ class IdfEvent: minute=self.timestamp.minute, second=self.timestamp.second, ) + # Resolve mic peak as psi. Priority: binary-derived mic_pspl_psi + # (set by read_idf_file) > dB(L)→psi fallback via standard formula + # (psi = 2.9e-9 × 10^(dBL/20)) > None. + mic_psi = self.peaks.mic_pspl_psi + if mic_psi is None and self.peaks.mic_pspl_dbl is not None: + mic_psi = 2.9e-9 * (10.0 ** (self.peaks.mic_pspl_dbl / 20.0)) pv = PeakValues( tran=self.peaks.transverse_ips, vert=self.peaks.vertical_ips, long=self.peaks.longitudinal_ips, - micl=self.peaks.mic_pspl_dbl, # dB(L) — see caveat above + micl=mic_psi, # psi, matching BW's convention (h5 scaling depends on this) peak_vector_sum=self.peaks.peak_vector_sum_ips, ) pi = ProjectInfo( diff --git a/sfm/waveform_store.py b/sfm/waveform_store.py index 3063cf9..5144754 100644 --- a/sfm/waveform_store.py +++ b/sfm/waveform_store.py @@ -568,6 +568,16 @@ class WaveformStore: # precedence over the filename timestamp inside from_report(). idf_event = IdfEvent.from_report(report_dict, source_path.name) + # The binary mic peak (psi) isn't carried through from_report() — + # IdfReport.from_dict only sees the .txt's dB(L) value. Pull the + # binary-derived ``mic_pspl_psi`` onto the typed IdfEvent so the + # downstream bridge can populate ``PeakValues.micl`` (psi-shaped) + # and the h5 writer's per-count mic factor lands at a sensible + # value. Without this, the h5 mic chart auto-scales against the + # dB(L) value-as-pseudo-psi and renders ~flat. + if binary_peaks is not None and binary_peaks.mic_pspl_psi is not None: + idf_event.peaks.mic_pspl_psi = binary_peaks.mic_pspl_psi + # Operator-supplied serial_hint wins over the binary's filename # prefix when both are present (e.g. callers passing a known-good # serial that overrides a misnamed export).