"""Vehicle-installation configuration loading and light validation.""" from __future__ import annotations from collections.abc import Mapping from pathlib import Path from typing import Any REQUIRED_TOP_LEVEL_KEYS = frozenset({"schema_version", "vehicle", "installation", "sensors", "time"}) def validate_config_shape(config: Mapping[str, object]) -> list[str]: """Return missing top-level keys without inventing default values.""" return sorted(REQUIRED_TOP_LEVEL_KEYS.difference(config)) def validate_config_semantics(config: Mapping[str, Any]) -> list[str]: """Return semantic issues that block calibration interpretation.""" issues: list[str] = [] sensors = config.get("sensors") if not isinstance(sensors, Mapping): return ["sensors must be a mapping"] imu = sensors.get("imu") lidar = sensors.get("lidar") if not isinstance(imu, Mapping): issues.append("sensors.imu missing") else: axes = ((imu.get("raw_frame") or {}) if isinstance(imu.get("raw_frame"), Mapping) else {}).get("axes") if not axes: issues.append("sensors.imu.raw_frame.axes is empty (declare axis meaning even if approximate)") if not isinstance(lidar, Mapping): issues.append("sensors.lidar missing") else: axes = ((lidar.get("raw_frame") or {}) if isinstance(lidar.get("raw_frame"), Mapping) else {}).get("axes") if not axes: issues.append("sensors.lidar.raw_frame.axes is empty (declare axis meaning even if approximate)") time_cfg = config.get("time") if not isinstance(time_cfg, Mapping): issues.append("time missing") else: for key in ("imu_timestamp_source", "lidar_timestamp_source", "lidar_frame_time_definition"): if not time_cfg.get(key): issues.append(f"time.{key} is empty") return issues def load_vehicle_config(path: str | Path) -> dict[str, Any]: """Load and lightly validate a YAML vehicle configuration.""" try: import yaml except ImportError as exc: # pragma: no cover raise ImportError("PyYAML is required to load vehicle configuration files") from exc config_path = Path(path) with config_path.open("r", encoding="utf-8") as handle: loaded = yaml.safe_load(handle) if not isinstance(loaded, dict): raise ValueError(f"vehicle config must be a mapping: {config_path}") missing = validate_config_shape(loaded) if missing: raise ValueError(f"vehicle config missing keys {missing}: {config_path}") semantic = validate_config_semantics(loaded) if semantic: raise ValueError("vehicle config semantic issues:\n- " + "\n- ".join(semantic)) return loaded def prior_enabled(config: Mapping[str, Any], name: str) -> bool: """Return whether an optional prior is enabled.""" init = config.get("initialization") if not isinstance(init, Mapping): return False prior = init.get(name) if not isinstance(prior, Mapping): return False return bool(prior.get("enabled", False))