"""Helpers for extracting GPR objects and locator observations from results.""" from __future__ import annotations from datetime import datetime from typing import Any import numpy as np from python_app.models.dataset_model import ResultCollection, ResultPayload def collection_payload_by_name( collection: ResultCollection, name: str, kind: int | None = None, ) -> ResultPayload | None: """Return the first collection payload matching name and optional kind.""" for payload in collection.collection_payloads: if payload.processing_name != name: continue if kind is not None and int(payload.kind) != int(kind): continue return payload return None def collection_payloads_by_prefix( collection: ResultCollection, prefix: str, kind: int | None = None, ) -> list[ResultPayload]: """Return collection payloads matching a processing-name prefix.""" payloads: list[ResultPayload] = [] for payload in collection.collection_payloads: if not str(payload.processing_name).startswith(prefix): continue if kind is not None and int(payload.kind) != int(kind): continue payloads.append(payload) return payloads def collection_has_gpr_payloads(collection: ResultCollection) -> bool: """Return whether collection carries GPR-specific collection payloads.""" return any( str(payload.processing_name).startswith("gpr_") for payload in collection.collection_payloads ) def gpr_object_rows(collection: ResultCollection) -> np.ndarray: """Return object rows as `[x_m, z_m, pair_count]` from a GPR collection.""" points_payload = collection_payload_by_name(collection, "gpr_points", kind=4) if points_payload is not None: points = np.asarray(points_payload.table, dtype=np.float32) if points.ndim == 2 and points.shape[1] >= 3: return points[:, :3] centers_payload = collection_payload_by_name(collection, "gpr_region_centers", kind=4) if centers_payload is not None: centers = np.asarray(centers_payload.table, dtype=np.float32) if centers.ndim == 2 and centers.shape[1] >= 3: return centers[:, :3] return np.zeros((0, 3), dtype=np.float32) def locator_observations_from_collection( collection: ResultCollection, min_pair_count: float, *, visible_bounds: tuple[float, float, float, float] | None = None, ) -> list[dict[str, float]]: """Build locator observations from GPR rows using pair threshold and optional X/Z bounds.""" rows = gpr_object_rows(collection) if rows.size == 0: return [] finite_mask = np.all(np.isfinite(rows[:, :3]), axis=1) visible_mask = finite_mask & (rows[:, 2] >= float(min_pair_count)) if visible_bounds is not None: x_min, x_max, z_min, z_max = (float(value) for value in visible_bounds) visible_mask &= ( (rows[:, 0] >= x_min) & (rows[:, 0] <= x_max) & (rows[:, 1] >= z_min) & (rows[:, 1] <= z_max) ) filtered = rows[visible_mask] observations: list[dict[str, float]] = [] for x_m, z_m, _pair_count in filtered: observations.append( { "dst": round(float(z_m), 2), "crs": round(float(x_m), 2), } ) return observations def build_locator_payload( observations: list[dict[str, float]], *, protocol_version: int, status: int = 1, ) -> dict[str, Any]: """Assemble one outbound locator payload from precomputed observations.""" return { "ver": int(protocol_version), "tim": _format_timestamp(), "sts": int(status), "obs": observations, } def _format_timestamp() -> str: """Return wall-clock timestamp with millisecond precision.""" return datetime.now().strftime("%H:%M:%S.%f")[:-3]