"""Helpers for B-scan history signatures and cache rebuilding.""" from __future__ import annotations from collections import deque import numpy as np from python_app.models.dataset_model import ResultCollection from python_app.orchestration.live_processing_config import ProcessingLiveConfig def _result_tail( *, result_history: list[ResultCollection], history_limit: int, floor_collection_id: int, ) -> list[ResultCollection]: """Return filtered and de-duplicated result-history tail for B-scan usage.""" filtered = [ collection for collection in result_history[-history_limit:] if int(collection.collection_id) > int(floor_collection_id) ] unique_reversed_tail: list[ResultCollection] = [] seen_keys: set[tuple[int, int]] = set() for collection in reversed(filtered): key = (int(collection.collection_id), int(collection.monotonic_ns)) if key in seen_keys: continue seen_keys.add(key) unique_reversed_tail.append(collection) unique_reversed_tail.reverse() return unique_reversed_tail def build_bscan_signature( live_config: ProcessingLiveConfig, result_history: list[ResultCollection], history_limit: int, floor_collection_id: int, ) -> tuple[object, ...]: """Build deterministic signature used to detect B-scan cache invalidation.""" result_tail = _result_tail( result_history=result_history, history_limit=history_limit, floor_collection_id=floor_collection_id, ) return ( str(live_config.bscan_axis), str(live_config.bscan_channel), float(live_config.bscan_cut_m), float(live_config.bscan_max_depth_m), float(live_config.bscan_gain), float(live_config.bscan_start_freq_mhz), float(live_config.bscan_stop_freq_mhz), int(floor_collection_id), tuple((int(collection.collection_id), int(collection.monotonic_ns), len(collection.blocks)) for collection in result_tail), ) def rebuild_bscan_history_from_results( result_history: list[ResultCollection], history_limit: int, floor_collection_id: int, ) -> tuple[dict[tuple[int, int], deque[np.ndarray]], dict[tuple[int, int], np.ndarray]]: """Rebuild B-scan history and depth axes from processed result payloads.""" history_by_combo: dict[tuple[int, int], deque[np.ndarray]] = {} depth_axis_by_combo: dict[tuple[int, int], np.ndarray] = {} result_tail = _result_tail( result_history=result_history, history_limit=history_limit, floor_collection_id=floor_collection_id, ) for collection in result_tail: for block in collection.blocks: key = (block.combo.input_pos, block.combo.output_pos) for payload in block.payloads: if payload.kind != 1 or payload.processing_name != "bscan": continue if payload.frequency_hz.size == 0 or payload.trace.size == 0: continue if payload.frequency_hz.size != payload.trace.size: continue depth_axis = np.asarray(payload.frequency_hz, dtype=np.float32) amplitudes = np.asarray(np.real(payload.trace), dtype=np.float32) if depth_axis.size == 0 or amplitudes.size == 0: continue history = history_by_combo.get(key) stored_axis = depth_axis_by_combo.get(key) if ( history is None or stored_axis is None or stored_axis.shape != depth_axis.shape or not np.allclose(stored_axis, depth_axis, rtol=1e-4, atol=1e-6) ): history = deque(maxlen=history_limit) history_by_combo[key] = history depth_axis_by_combo[key] = depth_axis.copy() history.append(amplitudes.copy()) return history_by_combo, depth_axis_by_combo def pick_bscan_display_key( history_by_combo: dict[tuple[int, int], deque[np.ndarray]], ) -> tuple[int, int] | None: """Choose combo key to display when multiple histories are present.""" if not history_by_combo: return None return next(iter(history_by_combo.keys()))