"""``coupler-measure`` command-line entry point. Three modes:: # measure an existing point cloud (production-like flow) coupler-measure --input-cloud scan.ply --out-dir runs/demo # research: synthesise a cloud from a STEP file and measure it coupler-measure --stp panel.stp --out-dir runs/from_cad # research: parameter sweep coupler-measure --sweep matrix.yaml --out-dir runs/sweep_xx """ from __future__ import annotations import argparse import json import logging import sys from pathlib import Path log = logging.getLogger("coupler_panel.cli") def _build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser( prog="coupler-measure", description="Coupler-panel CAD-STP to point-cloud measurement & validation.", ) src = p.add_mutually_exclusive_group(required=True) src.add_argument("--input-cloud", type=Path, help="Path to a PLY/XYZ/CSV/PCD point cloud.") src.add_argument("--stp", type=Path, help="Path to a STEP file (research mode).") src.add_argument("--sweep", type=Path, help="Path to a sweep matrix YAML.") p.add_argument("--out-dir", type=Path, required=True, help="Output directory.") p.add_argument("--config", type=Path, default=None, help="YAML config overrides.") p.add_argument( "--profile", type=str, default="auto", help="auto | default | high_density | low_density | custom", ) p.add_argument( "--target-holes", type=Path, default=None, help="DEPRECATED — the main pipeline no longer performs 4-hole Kabsch " "registration (it cancels the panel deflection signal). The flag " "is accepted for backward compatibility but is ignored.", ) p.add_argument( "--render", action="store_true", help="After measurement, render PNG diagnostics into /images/.", ) p.add_argument("-v", "--verbose", action="count", default=0, help="-v: INFO, -vv: DEBUG") return p def _setup_logging(verbose: int) -> None: level = logging.WARNING if verbose == 1: level = logging.INFO elif verbose >= 2: level = logging.DEBUG logging.basicConfig( level=level, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", ) def _on_progress(stage: str, fraction: float) -> None: log.info("[%5.1f%%] %s", fraction * 100, stage) def _maybe_render(out_dir: Path) -> None: try: from .viz import render_all render_all(out_dir) except ImportError: log.warning("matplotlib not installed; skip --render") except Exception as exc: log.warning("render_all failed: %s", exc) def main(argv: list[str] | None = None) -> int: args = _build_parser().parse_args(argv) _setup_logging(args.verbose) try: if args.input_cloud is not None: from .pipeline import run_coupler_measurement_pipeline if args.target_holes is not None: log.warning( "--target-holes is deprecated and ignored; the main pipeline " "no longer performs Kabsch registration." ) result = run_coupler_measurement_pipeline( input_cloud=args.input_cloud, out_dir=args.out_dir, config_path=args.config, profile=args.profile, progress=_on_progress, ) print( json.dumps( { "theta_right_deg": result.theta_right_deg, "theta_ref_deg": result.theta_ref_deg, "dx_y_max_mm": result.dx_y_max_mm, "dx_z_max_mm": result.dx_z_max_mm, "quality_flags": result.quality_flags, }, indent=2, ensure_ascii=False, ) ) elif args.stp is not None: from .pipeline import run_from_cad_pipeline summary = run_from_cad_pipeline( stp_path=args.stp, out_dir=args.out_dir, config_path=args.config, profile=args.profile, progress=_on_progress, ) print( json.dumps( { "diff": summary["diff"], "quality_flags": summary["measured"]["quality_flags"], }, indent=2, ensure_ascii=False, ) ) elif args.sweep is not None: from importlib import import_module mod = import_module("experiments.sweep_runner") mod.main(matrix_path=args.sweep, out_dir=args.out_dir, config_path=args.config) if args.render: _maybe_render(args.out_dir) except Exception as exc: log.exception("pipeline failed: %s", exc) return 2 return 0 if __name__ == "__main__": sys.exit(main())