"""Wall-clock helpers for Medulla tick filtering. Two tick conventions appear in this dataset: - LiDAR DObject ``tic`` / recovered ``indices.log``: ``DateTime.Now.Ticks`` (local) - IMU / MSOP payload host receive fields: UTC ``DateTime.UtcNow.Ticks`` """ from __future__ import annotations from datetime import datetime, timedelta, timezone TICKS_PER_SECOND = 10_000_000 DOTNET_UNIX_EPOCH_TICKS = 621355968000000000 def _parse_local_wall(text: str) -> datetime: normalized = text.strip().replace(" ", "T") if normalized.endswith("Z"): raise ValueError("expected local wall time without Z; got UTC marker") if "+" in normalized[10:]: idx = normalized.find("+", 10) normalized = normalized[:idx] elif normalized.count("-") > 2: # timezone like -08:00 after the date idx = normalized.find("-", 10) if idx > 0 and ":" in normalized[idx + 1 :]: normalized = normalized[:idx] return datetime.fromisoformat(normalized).replace(tzinfo=None) def local_wall_to_dotnet_ticks(text: str) -> int: """Local wall time → ``DateTime.Now.Ticks`` (LiDAR DObject tic).""" dt = _parse_local_wall(text) delta = dt - datetime(1, 1, 1) return int(delta.total_seconds() * TICKS_PER_SECOND) def local_wall_to_utc_dotnet_ticks(text: str, *, tz_hours: float = 8.0) -> int: """Local wall time in ``tz_hours`` → UTC ``DateTime.UtcNow.Ticks`` (IMU host).""" dt = _parse_local_wall(text).replace(tzinfo=timezone(timedelta(hours=tz_hours))) unix = dt.timestamp() return int(round(unix * TICKS_PER_SECOND)) + DOTNET_UNIX_EPOCH_TICKS def dotnet_ticks_to_local_iso(ticks: int) -> str: dt = datetime(1, 1, 1) + timedelta(microseconds=ticks / 10.0) return dt.isoformat(timespec="milliseconds") def utc_dotnet_ticks_to_unix_s(ticks: int) -> float: """UTC ``DateTime.UtcNow.Ticks`` → Unix seconds.""" return (float(ticks) - float(DOTNET_UNIX_EPOCH_TICKS)) / float(TICKS_PER_SECOND)