Source code for suboptimumg.log_analysis.yaw_artifact

from __future__ import annotations

import pickle
from pathlib import Path

from perda.core_data_structures import SingleRunData
from pydantic import BaseModel, ConfigDict, Field

from .yaw_fit_models import YawFit
from .yaw_segmentation_models import MaskReport


[docs] class SegmentationConfig(BaseModel): """A record of how one log was cut into sub-windows. Recorded alongside the sub-windows so a saved artifact answers "where did these come from" without re-reading the notebook that made it. """ logfile: str = Field(description="Source log file path") trim_range_s: tuple[float, float] | None = Field( default=None, description="Time range the log was trimmed to (s), or None if untrimmed", ) manual_windows: list[tuple[float, float]] = Field( default_factory=list, description="Operator-picked coarse (start, end) windows (s)", ) gps_lag_s: float = Field(default=0.0, description="GPS channel lag correction applied (s)") min_speed_mps: float = Field(default=4.0, description="Speed floor a sample had to clear (m/s)") max_slip_deg: float = Field( default=7.0, description="Body-slip ceiling a sample had to stay under (deg)" ) min_window_s: float = Field(default=8.0, description="Shortest sub-window kept (s)") target_hz: float = Field( default=100.0, description="Uniform grid the sub-windows were resampled onto (Hz)", ) mask_reports: list[MaskReport] = Field( default_factory=list, description="Per-manual-window masking diagnostics" )
[docs] class YawArtifact(BaseModel): """One log's yaw-response work: its sub-windows, how they were made, and the fits. Bundles everything a later session needs to re-evaluate or compare against this log without re-running preparation. ``save`` / ``load`` round-trip it through pickle, because the PERDA ``SingleRunData`` sub-windows have no JSON form. Treat the file as a local cache, not an interchange format: it is tied to the installed PERDA/pydantic versions, and loading one executes arbitrary code, so only load artifacts you produced. """ model_config = ConfigDict(arbitrary_types_allowed=True) log_name: str = Field(description="Identifying name for the source log") config: SegmentationConfig = Field(description="How the sub-windows were produced") subwindows: list[SingleRunData] = Field(description="The clean, resampled sub-windows") fits: dict[str, YawFit] = Field( default_factory=dict, description="Fitted models, keyed by spec name" )
[docs] def add_fit(self, fit: YawFit) -> None: """Record ``fit`` under its spec name, replacing any fit of that name.""" self.fits[fit.spec.name] = fit
[docs] def save(self, path: str | Path) -> Path: """Pickle this artifact to ``path``, creating parent directories. Returns ------- Path The resolved output path. """ out = Path(path) out.parent.mkdir(parents=True, exist_ok=True) with open(out, "wb") as f: pickle.dump(self, f) return out
[docs] @staticmethod def load(path: str | Path) -> YawArtifact: """Inverse of ``save``. Only load artifacts you produced -- see the class docstring. Raises ------ TypeError If ``path`` does not hold a ``YawArtifact``. """ with open(Path(path), "rb") as f: obj = pickle.load(f) if not isinstance(obj, YawArtifact): raise TypeError(f"{path} did not contain a YawArtifact") return obj