Files
radar_system/python_app/gui/runtime/history.py
T
2026-05-08 21:23:52 +03:00

115 lines
4.6 KiB
Python

"""Helpers for GUI-side runtime history management."""
from __future__ import annotations
from collections import deque
from typing import TypeVar
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 record_result_history(
result_history: deque[ResultCollection],
collection: ResultCollection,
) -> bool:
"""Append new result or replace existing entry by stable collection key."""
for index in range(len(result_history) - 1, -1, -1):
existing = result_history[index]
if (
existing.collection_id == collection.collection_id
and existing.monotonic_ns == collection.monotonic_ns
):
result_history[index] = collection
return True
result_history.append(collection)
return True
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
def build_run_history_signature(
config: RunConfigModel,
) -> tuple[object, ...]:
"""Build deterministic signature to detect run-settings changes (excluding live processing params)."""
combos_signature = tuple((int(combo.input), int(combo.output)) for combo in config.combos)
preprocess_signature = tuple(preprocess_asset_model(config, key).set_name for key in PREPROCESS_ASSET_KEYS)
sweep_points_signature: object = "adc" if config.is_kamil_adc else int(config.radar.sweep.points)
return (
str(config.radar.model),
str(config.radar.driver_mode),
str(config.radar.serial),
tuple(str(value) for value in config.radar.multi_device.slave_serials),
bool(config.radar.multi_device.force_external_reference),
float(config.radar.sweep.start_hz),
float(config.radar.sweep.stop_hz),
sweep_points_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),
preprocess_signature,
combos_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