"""Package calibration outputs as JSON-friendly artifacts.""" from __future__ import annotations import json from pathlib import Path from typing import Any import numpy as np from .contracts import CalibrationResult, CalibrationStatus from .geometry import rotation_matrix_to_quaternion_xyzw, rpy_deg_xyz def _to_serializable(value: Any) -> Any: if isinstance(value, np.ndarray): return value.tolist() if isinstance(value, (np.floating, np.integer, np.bool_)): return value.item() if isinstance(value, Path): return str(value) if isinstance(value, dict): return {str(k): _to_serializable(v) for k, v in value.items()} if isinstance(value, (list, tuple)): return [_to_serializable(v) for v in value] return value def finalize_result( *, status: CalibrationStatus, message: str, details: dict[str, Any], T_IMU_lidar: np.ndarray | None = None, time_offset_s: float | None = None, output_directory: Path | None = None, motion_pairs_payload: dict[str, Any] | None = None, ) -> CalibrationResult: """Build the result envelope and optionally write report files.""" result = CalibrationResult( status=status, message=message, details=_to_serializable(details), T_IMU_lidar=None if T_IMU_lidar is None else np.asarray(T_IMU_lidar, dtype=float), time_offset_s=time_offset_s, ) if output_directory is not None: output_directory = Path(output_directory) output_directory.mkdir(parents=True, exist_ok=True) summary = { "status": status.value, "message": message, "time_offset_s": time_offset_s, "details": result.details, } if result.T_IMU_lidar is not None: t = result.T_IMU_lidar summary["T_IMU_lidar"] = { "matrix": t.tolist(), "translation_m": t[:3, 3].tolist(), "rotation_quaternion_xyzw": rotation_matrix_to_quaternion_xyzw(t[:3, :3]).tolist(), "rpy_deg_xyz": rpy_deg_xyz(t[:3, :3]).tolist(), "convention": "p_IMU = T_IMU_lidar * p_lidar", } (output_directory / "T_IMU_lidar.json").write_text( json.dumps(summary["T_IMU_lidar"], indent=2), encoding="utf-8", ) if time_offset_s is not None: (output_directory / "time_offset.json").write_text( json.dumps({"delta_t_s": time_offset_s, "definition": "t_imu = t_lidar + delta_t"}, indent=2), encoding="utf-8", ) if motion_pairs_payload is not None: from .motion_pairs_io import save_motion_pairs save_motion_pairs(output_directory / "motion_pairs.json", motion_pairs_payload) summary["motion_pairs_file"] = "motion_pairs.json" (output_directory / "summary.json").write_text(json.dumps(summary, indent=2), encoding="utf-8") return result