"""Helpers for loading radar sweep variants from a directory of JSON files.""" from __future__ import annotations from dataclasses import dataclass import json from pathlib import Path from python_app.models.run_config_model import RunConfigModel from python_app.storage.npz_store import radar_key_from_config _RADAR_SWEEP_KEYS = ( "start_hz", "stop_hz", "points", "if_bandwidth_hz", "stimulus_power_dbm", ) @dataclass(frozen=True, slots=True) class RadarConfigVariant: """One capture-ready radar sweep variant loaded from JSON.""" source_path: Path display_name: str config: RunConfigModel radar_key: str @dataclass(frozen=True, slots=True) class RadarConfigScanSummary: """Scan results for one radar-config directory refresh.""" directory_path: str json_file_count: int valid_variant_count: int skipped_file_count: int duplicate_variant_count: int issues: tuple[str, ...] def scan_radar_config_variants( directory_path: str, *, base_config: RunConfigModel, ) -> tuple[list[RadarConfigVariant], RadarConfigScanSummary]: """Load valid radar sweep variants from `directory_path` using `base_config` as the baseline.""" normalized_path = str(directory_path).strip() if not normalized_path: return [], RadarConfigScanSummary( directory_path="", json_file_count=0, valid_variant_count=0, skipped_file_count=0, duplicate_variant_count=0, issues=(), ) directory = Path(normalized_path).expanduser() if not directory.exists(): return [], RadarConfigScanSummary( directory_path=str(directory), json_file_count=0, valid_variant_count=0, skipped_file_count=0, duplicate_variant_count=0, issues=(f"Directory does not exist: {directory}",), ) if not directory.is_dir(): return [], RadarConfigScanSummary( directory_path=str(directory), json_file_count=0, valid_variant_count=0, skipped_file_count=0, duplicate_variant_count=0, issues=(f"Path is not a directory: {directory}",), ) variants: list[RadarConfigVariant] = [] issues: list[str] = [] duplicate_variant_count = 0 seen_radar_keys: set[str] = set() json_paths = sorted(path for path in directory.glob("*.json") if path.is_file()) for path in json_paths: try: variant = _load_radar_config_variant(path, base_config=base_config) except Exception as exc: # noqa: BLE001 issues.append(f"{path.name}: {type(exc).__name__}: {exc}") continue if variant.radar_key in seen_radar_keys: duplicate_variant_count += 1 issues.append( f"{path.name}: duplicate radar variant key {variant.radar_key}; keeping the first matching file only" ) continue seen_radar_keys.add(variant.radar_key) variants.append(variant) return variants, RadarConfigScanSummary( directory_path=str(directory), json_file_count=len(json_paths), valid_variant_count=len(variants), skipped_file_count=max(0, len(json_paths) - len(variants) - duplicate_variant_count), duplicate_variant_count=duplicate_variant_count, issues=tuple(issues), ) def _load_radar_config_variant(path: Path, *, base_config: RunConfigModel) -> RadarConfigVariant: """Load one radar sweep variant by overlaying JSON `radar.sweep` onto `base_config`.""" payload = json.loads(path.read_text(encoding="utf-8")) if not isinstance(payload, dict): raise ValueError("JSON root must be an object") radar_payload = payload.get("radar") if not isinstance(radar_payload, dict): raise ValueError("Missing radar section") sweep_payload = radar_payload.get("sweep") if not isinstance(sweep_payload, dict): raise ValueError("Missing radar.sweep section") if not any(key in sweep_payload for key in _RADAR_SWEEP_KEYS): raise ValueError("radar.sweep does not contain any supported sweep keys") config = base_config.clone() sweep = config.radar.sweep if "start_hz" in sweep_payload: sweep.start_hz = float(sweep_payload["start_hz"]) if "stop_hz" in sweep_payload: sweep.stop_hz = float(sweep_payload["stop_hz"]) if "points" in sweep_payload: sweep.points = int(sweep_payload["points"]) if "if_bandwidth_hz" in sweep_payload: sweep.if_bandwidth_hz = float(sweep_payload["if_bandwidth_hz"]) if "stimulus_power_dbm" in sweep_payload: sweep.power_dbm = float(sweep_payload["stimulus_power_dbm"]) radar_key = radar_key_from_config( model_name=config.radar.model, serial=config.radar.serial, sweep_start_hz=config.radar.sweep.start_hz, sweep_stop_hz=config.radar.sweep.stop_hz, sweep_points=config.radar.sweep.points, ifbw_hz=config.radar.sweep.if_bandwidth_hz, power_dbm=config.radar.sweep.power_dbm, extra_serials=( config.radar.multi_device.slave_serials if config.is_multi_device else None ), ) return RadarConfigVariant( source_path=path, display_name=path.stem, config=config, radar_key=radar_key, )