|
|
|
@@ -3,22 +3,12 @@
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from collections import deque
|
|
|
|
|
from typing import TypeVar
|
|
|
|
|
from dataclasses import astuple
|
|
|
|
|
|
|
|
|
|
from python_app.models.dataset_model import ResultCollection, SweepCollection
|
|
|
|
|
from python_app.models.run_config_model import RunConfigModel
|
|
|
|
|
from python_app.orchestration.preprocess_assets import PREPROCESS_ASSET_KEYS, preprocess_asset_model
|
|
|
|
|
|
|
|
|
|
THistoryCollection = TypeVar("THistoryCollection", SweepCollection, ResultCollection)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def collection_key(collection: SweepCollection | ResultCollection) -> tuple[int, int]:
|
|
|
|
|
"""Identity of a sweep through the pipeline: its result keeps the same id and timestamp.
|
|
|
|
|
|
|
|
|
|
`monotonic_ns` is stamped by `data_preprocessor` on the system monotonic clock, so
|
|
|
|
|
it also orders sweeps by acquisition, across runs too (ids restart from 1).
|
|
|
|
|
"""
|
|
|
|
|
return int(collection.collection_id), int(collection.monotonic_ns)
|
|
|
|
|
from python_app.models.collection_identity import collection_key
|
|
|
|
|
from python_app.models.dataset_model import ResultCollection
|
|
|
|
|
from python_app.models.run_config_schema import RunConfigModel
|
|
|
|
|
from python_app.orchestration.preprocess_assets import PREPROCESS_ASSET_NAMES, preprocess_asset_model
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def record_result_history(
|
|
|
|
@@ -33,6 +23,8 @@ def record_result_history(
|
|
|
|
|
delivers sweeps whose first result the GUI missed, and appending those would
|
|
|
|
|
show them out of order.
|
|
|
|
|
"""
|
|
|
|
|
if result_history.maxlen == 0:
|
|
|
|
|
return False
|
|
|
|
|
key = collection_key(collection)
|
|
|
|
|
position = len(result_history)
|
|
|
|
|
while position > 0:
|
|
|
|
@@ -40,7 +32,7 @@ def record_result_history(
|
|
|
|
|
if collection_key(existing) == key:
|
|
|
|
|
result_history[position - 1] = collection
|
|
|
|
|
return False
|
|
|
|
|
if existing.monotonic_ns < collection.monotonic_ns:
|
|
|
|
|
if existing.capture_start_ns < collection.capture_start_ns:
|
|
|
|
|
break
|
|
|
|
|
position -= 1
|
|
|
|
|
|
|
|
|
@@ -53,48 +45,19 @@ def record_result_history(
|
|
|
|
|
result_history.popleft()
|
|
|
|
|
position -= 1
|
|
|
|
|
result_history.insert(position, collection)
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def remove_last_aligned_histories(
|
|
|
|
|
raw_history: list[SweepCollection],
|
|
|
|
|
preprocessed_history: list[SweepCollection],
|
|
|
|
|
result_history: list[ResultCollection],
|
|
|
|
|
) -> tuple[list[SweepCollection], list[SweepCollection], list[ResultCollection]]:
|
|
|
|
|
"""Remove the newest aligned history entry using results as preferred anchor."""
|
|
|
|
|
retained_raw = list(raw_history)
|
|
|
|
|
retained_preprocessed = list(preprocessed_history)
|
|
|
|
|
retained_results = list(result_history)
|
|
|
|
|
|
|
|
|
|
if retained_results:
|
|
|
|
|
target_key = _tail_occurrence_key(retained_results, len(retained_results) - 1)
|
|
|
|
|
retained_results.pop()
|
|
|
|
|
_remove_by_tail_occurrence_key(retained_raw, target_key)
|
|
|
|
|
_remove_by_tail_occurrence_key(retained_preprocessed, target_key)
|
|
|
|
|
return retained_raw, retained_preprocessed, retained_results
|
|
|
|
|
|
|
|
|
|
if retained_preprocessed:
|
|
|
|
|
target_key = _tail_occurrence_key(retained_preprocessed, len(retained_preprocessed) - 1)
|
|
|
|
|
retained_preprocessed.pop()
|
|
|
|
|
_remove_by_tail_occurrence_key(retained_raw, target_key)
|
|
|
|
|
return retained_raw, retained_preprocessed, retained_results
|
|
|
|
|
|
|
|
|
|
if retained_raw:
|
|
|
|
|
retained_raw.pop()
|
|
|
|
|
|
|
|
|
|
return retained_raw, retained_preprocessed, retained_results
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_processor_run_signature(
|
|
|
|
|
config: RunConfigModel,
|
|
|
|
|
) -> tuple[object, ...]:
|
|
|
|
|
"""Build signature of settings that change the data SHAPE the data_processor parses.
|
|
|
|
|
"""Settings consumed at processor startup, rather than through the live file.
|
|
|
|
|
|
|
|
|
|
Excludes the preprocess set names on purpose: the data_processor does not consume
|
|
|
|
|
calibration/reference sets (those are applied upstream by the data_preprocessor), so
|
|
|
|
|
changing a set must NOT restart the processor. Restarting it would also tear down the
|
|
|
|
|
locator TCP server it hosts and drop every connected client. Only genuine shape changes
|
|
|
|
|
(radar model, sweep, switch layout, combos) require a processor restart.
|
|
|
|
|
locator TCP server it hosts and drop every connected client. Shape, ring endpoints,
|
|
|
|
|
locator settings, live-file path and polling interval require a restart.
|
|
|
|
|
"""
|
|
|
|
|
combos_signature = tuple((int(combo.input), int(combo.output)) for combo in config.combos)
|
|
|
|
|
sweep_points_signature: object = "adc" if config.is_kamil_adc else int(config.radar.sweep.points)
|
|
|
|
@@ -110,49 +73,30 @@ def build_processor_run_signature(
|
|
|
|
|
float(config.radar.sweep.if_bandwidth_hz),
|
|
|
|
|
float(config.radar.sweep.power_dbm),
|
|
|
|
|
str(config.input_switch.driver_mode),
|
|
|
|
|
str(config.input_switch.driver),
|
|
|
|
|
int(config.input_switch.positions),
|
|
|
|
|
bool(config.input_switch.invert_logic),
|
|
|
|
|
str(config.output_switch.driver_mode),
|
|
|
|
|
str(config.output_switch.driver),
|
|
|
|
|
int(config.output_switch.positions),
|
|
|
|
|
bool(config.output_switch.invert_logic),
|
|
|
|
|
combos_signature,
|
|
|
|
|
astuple(config.rings),
|
|
|
|
|
astuple(config.runtime.locator_server),
|
|
|
|
|
str(config.runtime.processing_live_config_path),
|
|
|
|
|
int(config.runtime.idle_sleep_ms),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_run_history_signature(
|
|
|
|
|
config: RunConfigModel,
|
|
|
|
|
) -> tuple[object, ...]:
|
|
|
|
|
"""Build full signature for GUI display-history reset (processor shape + preprocess sets).
|
|
|
|
|
"""Reset display history when startup settings or preprocessing change.
|
|
|
|
|
|
|
|
|
|
Display history still resets when the reference/calibration set changes (the on-screen
|
|
|
|
|
B-scan would otherwise mix old- and new-reference frames), even though the processor
|
|
|
|
|
process itself is intentionally kept alive across that change.
|
|
|
|
|
"""
|
|
|
|
|
preprocess_signature = tuple(preprocess_asset_model(config, key).set_name for key in PREPROCESS_ASSET_KEYS)
|
|
|
|
|
return build_processor_run_signature(config) + (preprocess_signature,)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _tail_occurrence_key(history: list[THistoryCollection], index: int) -> tuple[int, int]:
|
|
|
|
|
"""Return `(collection_id, occurrence_from_tail)` for the item at `index`."""
|
|
|
|
|
collection_id = int(history[index].collection_id)
|
|
|
|
|
occurrence_from_tail = 0
|
|
|
|
|
for cursor in range(len(history) - 1, index, -1):
|
|
|
|
|
if int(history[cursor].collection_id) == collection_id:
|
|
|
|
|
occurrence_from_tail += 1
|
|
|
|
|
return collection_id, occurrence_from_tail
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _remove_by_tail_occurrence_key(history: list[THistoryCollection], key: tuple[int, int]) -> bool:
|
|
|
|
|
"""Remove newest matching entry identified by `(collection_id, occurrence_from_tail)`."""
|
|
|
|
|
collection_id, target_occurrence = key
|
|
|
|
|
occurrence_from_tail = 0
|
|
|
|
|
for index in range(len(history) - 1, -1, -1):
|
|
|
|
|
if int(history[index].collection_id) != collection_id:
|
|
|
|
|
continue
|
|
|
|
|
if occurrence_from_tail == target_occurrence:
|
|
|
|
|
history.pop(index)
|
|
|
|
|
return True
|
|
|
|
|
occurrence_from_tail += 1
|
|
|
|
|
return False
|
|
|
|
|
preprocess_signature = tuple(astuple(preprocess_asset_model(config, key)) for key in PREPROCESS_ASSET_NAMES)
|
|
|
|
|
notch = config.preprocess.notch
|
|
|
|
|
notch_signature = (
|
|
|
|
|
bool(notch.enabled), tuple(tuple(band) for band in notch.bands_hz),
|
|
|
|
|
float(notch.taper_width_hz), str(notch.taper_type),
|
|
|
|
|
)
|
|
|
|
|
return build_processor_run_signature(config) + (preprocess_signature, notch_signature)
|
|
|
|
|