"""IMU unit, axis, bias, and saturation audit.""" from __future__ import annotations from dataclasses import dataclass import numpy as np from .contracts import ImuSeries G = 9.80665 @dataclass(frozen=True) class ImuAuditReport: ok: bool gyro_bias_rad_s: np.ndarray static_acc_mean_m_s2: np.ndarray static_acc_norm_m_s2: float suggested_up_axis: int suggested_up_sign: float static_ratio: float notes: tuple[str, ...] = () def _static_mask(gyro: np.ndarray, acc: np.ndarray) -> np.ndarray: gyro_norm = np.linalg.norm(gyro, axis=1) acc_norm = np.linalg.norm(acc, axis=1) gyro_thr = max(0.02, float(np.percentile(gyro_norm, 20)) * 1.5) acc_thr_low = 0.7 * G acc_thr_high = 1.3 * G return (gyro_norm < gyro_thr) & (acc_norm > acc_thr_low) & (acc_norm < acc_thr_high) def audit_imu(imu: ImuSeries) -> ImuAuditReport: """Audit normalized IMU samples and estimate a static gyro bias.""" notes: list[str] = [] mask = _static_mask(imu.gyro_rad_s, imu.acc_m_s2) static_ratio = float(np.mean(mask)) if mask.size else 0.0 if static_ratio < 0.02: # Fall back to lowest-gyro percentile window. gyro_norm = np.linalg.norm(imu.gyro_rad_s, axis=1) cutoff = float(np.percentile(gyro_norm, 10)) mask = gyro_norm <= cutoff notes.append("few gravity-consistent static samples; using lowest-gyro percentile") static_ratio = float(np.mean(mask)) if not np.any(mask): notes.append("no static samples found") bias = np.zeros(3) acc_mean = np.zeros(3) acc_norm = 0.0 up_axis = 2 up_sign = 1.0 ok = False else: bias = np.mean(imu.gyro_rad_s[mask], axis=0) acc_mean = np.mean(imu.acc_m_s2[mask], axis=0) acc_norm = float(np.linalg.norm(acc_mean)) up_axis = int(np.argmax(np.abs(acc_mean))) up_sign = float(np.sign(acc_mean[up_axis]) or 1.0) if abs(acc_norm - G) > 2.5: notes.append( f"static |acc|={acc_norm:.3f} differs from g={G}; check units (expect m/s^2)" ) gyro_peak = float(np.max(np.linalg.norm(imu.gyro_rad_s, axis=1))) if gyro_peak > 20.0: notes.append( f"peak |gyro|={gyro_peak:.1f} rad/s looks extreme; check whether data is deg/s" ) ok = abs(acc_norm - G) < 3.5 or static_ratio > 0.05 notes.append( f"suggested up axis index={up_axis} sign={up_sign:+.0f} (0=x,1=y,2=z)" ) return ImuAuditReport( ok=ok, gyro_bias_rad_s=np.asarray(bias, dtype=float), static_acc_mean_m_s2=np.asarray(acc_mean, dtype=float), static_acc_norm_m_s2=float(acc_norm), suggested_up_axis=up_axis, suggested_up_sign=up_sign, static_ratio=static_ratio, notes=tuple(notes), )