149 lines
4.4 KiB
Python
149 lines
4.4 KiB
Python
"""Result dataclasses for coupler_panel.
|
|
|
|
These types form the **stable public schema** of the package; their
|
|
field names and semantics are guarded by ``SCHEMA_VERSION`` (see
|
|
``_version.py``).
|
|
|
|
All numeric units are **mm** for distances and **degree** for angles
|
|
unless otherwise noted.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import asdict, dataclass, field
|
|
from typing import Any
|
|
|
|
import numpy as np
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Building blocks
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Hole:
|
|
"""A single detected (or GT) circular hole on the panel."""
|
|
|
|
center: tuple[float, float, float] # (x, y, z), mm
|
|
radius_mm: float
|
|
fit_residual_rms_mm: float # 0 for GT
|
|
role: str # "locating" | "central" | "unknown"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class HoleSet:
|
|
"""The four locating holes plus an optional central hole.
|
|
|
|
``locating`` is ordered consistently between GT and detection so that
|
|
Kabsch pairing is index-aligned.
|
|
"""
|
|
|
|
locating: tuple[Hole, Hole, Hole, Hole]
|
|
central: Hole | None = None
|
|
|
|
def locating_centers(self) -> np.ndarray:
|
|
"""Return a (4, 3) ndarray of locating-hole centres."""
|
|
return np.asarray([h.center for h in self.locating], dtype=np.float64)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PanelPlane:
|
|
"""The fitted (or GT) panel plane."""
|
|
|
|
normal: tuple[float, float, float] # unit vector, n_x > 0 enforced
|
|
point_on_plane: tuple[float, float, float]
|
|
d_offset: float # n . x + d = 0
|
|
inlier_ratio: float # 1.0 for GT
|
|
inlier_residual_rms_mm: float # 0 for GT
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ReferenceFrame:
|
|
"""Reference geometry derived from the four-hole panel centre."""
|
|
|
|
center_C: tuple[float, float, float]
|
|
# Reference line L = C + lam * e_x (always parallel to the +x axis).
|
|
line_direction: tuple[float, float, float] = (1.0, 0.0, 0.0)
|
|
# Reference plane: z = C_z. Stored as plane normal + offset.
|
|
reference_plane_z: float = 0.0
|
|
# Longitudinal mid-plane: y = C_y.
|
|
longitudinal_mid_y: float = 0.0
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class StageTiming:
|
|
"""Per-stage wall-clock and peak-memory cost."""
|
|
|
|
stage: str
|
|
seconds: float
|
|
peak_kib: float
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Top-level result
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass
|
|
class CouplerMeasurement:
|
|
"""Top-level result of the coupler-panel measurement pipeline.
|
|
|
|
See ``docs/USAGE.md`` for the stable-API contract.
|
|
"""
|
|
|
|
schema_version: str
|
|
algorithm_version: str
|
|
|
|
# --- Primary measurands ---
|
|
theta_right_deg: float # atan2(n_y, n_x), with right_axis_sign applied
|
|
theta_ref_deg: float # atan2(n_z, n_x)
|
|
D_y_max_mm: float # max |y_i - C_y| over panel ROI
|
|
D_z_max_mm: float # max |z_i - C_z| over panel ROI
|
|
dx_y_max_mm: float # |A| * D_y_max, A = -n_y / n_x
|
|
dx_z_max_mm: float # |B| * D_z_max, B = -n_z / n_x
|
|
|
|
# --- Geometry ---
|
|
center_C: tuple[float, float, float]
|
|
normal: tuple[float, float, float] # n_x > 0
|
|
holes: HoleSet
|
|
plane: PanelPlane
|
|
frame: ReferenceFrame
|
|
|
|
# --- Diagnostics ---
|
|
quality_flags: list[str] = field(default_factory=list)
|
|
timings: list[StageTiming] = field(default_factory=list)
|
|
report: dict[str, Any] = field(default_factory=dict)
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
"""Return a fully JSON-serialisable dict."""
|
|
return _to_jsonable(asdict(self))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _to_jsonable(obj: Any) -> Any:
|
|
"""Recursively convert numpy / tuple structures into plain Python types."""
|
|
if isinstance(obj, dict):
|
|
return {k: _to_jsonable(v) for k, v in obj.items()}
|
|
if isinstance(obj, (list, tuple)):
|
|
return [_to_jsonable(v) for v in obj]
|
|
if isinstance(obj, np.ndarray):
|
|
return obj.tolist()
|
|
if isinstance(obj, (np.floating, np.integer)):
|
|
return obj.item()
|
|
return obj
|
|
|
|
|
|
__all__ = [
|
|
"Hole",
|
|
"HoleSet",
|
|
"PanelPlane",
|
|
"ReferenceFrame",
|
|
"StageTiming",
|
|
"CouplerMeasurement",
|
|
]
|