"""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