118 lines
4.2 KiB
Python
118 lines
4.2 KiB
Python
"""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()))
|