Files
radar_system/python_app/gui/controllers/app_window_snapshot_mixin.py
T
2026-03-26 15:48:16 +03:00

255 lines
11 KiB
Python

"""Snapshot and ring-drain helper mixin for the main GUI window."""
from __future__ import annotations
from pathlib import Path
import time
from PyQt6.QtWidgets import QFileDialog
from python_app.gui.runtime.history import record_result_history, remove_last_aligned_histories
class AppWindowSnapshotMixin:
"""Saves runtime data snapshots and maintains ring-reader freshness."""
def _save_snapshot(self) -> None:
"""Save runtime snapshot in numpy-directory format."""
self._drain_runtime_rings_for_snapshot()
if not self._raw_history and not self._pre_history and not self._result_history:
self._show_error("No runtime data is available for save")
return
try:
last_n = int(self._save_count.value())
output_root = Path(self._save_path_input.text().strip()).expanduser()
snapshot_name = self._save_name_input.text().strip()
snapshot_dir, summary = self._store.save_runtime_snapshot_numpy(
output_root,
snapshot_name,
list(self._raw_history),
list(self._pre_history),
list(self._result_history),
last_n,
)
self._log(
f"Saved numpy snapshot: {snapshot_dir} "
f"(raw={summary.get('raw_count', 0)}, "
f"preprocessed={summary.get('preprocessed_count', 0)}, "
f"results={summary.get('result_count', 0)}, "
f"mode={summary.get('selection_mode', 'unknown')}, "
f"anchor={summary.get('anchor_stage', 'unknown')}, "
f"aligned_keys={summary.get('aligned_key_count', 0)}, "
f"raw_missing={summary.get('raw_missing_count', 0)}, "
f"pre_missing={summary.get('preprocessed_missing_count', 0)}, "
f"result_missing={summary.get('result_missing_count', 0)}, "
f"requested_last_n={last_n})"
)
except Exception as exc: # noqa: BLE001
self._show_error(f"Failed to save snapshot: {exc}")
def _save_vna_history_json(self) -> None:
"""Save runtime history as vna_system-compatible JSON file."""
self._drain_runtime_rings_for_snapshot()
if not self._raw_history and not self._pre_history and not self._result_history:
self._show_error("No runtime data is available for save")
return
try:
last_n = int(self._save_count.value())
input_index = int(self._vna_json_input_index.value())
output_index = int(self._vna_json_output_index.value())
output_root = Path(self._save_path_input.text().strip()).expanduser()
output_name = self._save_name_input.text().strip()
output_path, summary = self._store.save_runtime_vna_history_json(
output_root,
output_name,
list(self._raw_history),
list(self._pre_history),
list(self._result_history),
last_n,
input_index=input_index,
output_index=output_index,
primary_stage="preprocessed",
)
self._log(
f"Saved VNA history JSON: {output_path} "
f"(sweeps={summary.get('sweep_count', 0)}, "
f"raw_records={summary.get('raw_record_count', 0)}, "
f"preprocessed_records={summary.get('preprocessed_record_count', 0)}, "
f"raw={summary.get('raw_count', 0)}, "
f"preprocessed={summary.get('preprocessed_count', 0)}, "
f"results={summary.get('result_count', 0)}, "
f"mode={summary.get('selection_mode', 'unknown')}, "
f"anchor={summary.get('anchor_stage', 'unknown')}, "
f"input={input_index}, "
f"output={output_index}, "
f"requested_last_n={last_n})"
)
except Exception as exc: # noqa: BLE001
self._show_error(f"Failed to save VNA history JSON: {exc}")
def _remove_last_runtime_history(self) -> None:
"""Remove the newest runtime measurement from all stages and processor replay state."""
self._apply_runtime_history_deletion(remove_last_only=True)
def _clear_all_runtime_history(self) -> None:
"""Clear all runtime histories, ring backlogs, and processor replay state."""
self._apply_runtime_history_deletion(remove_last_only=False)
def _apply_runtime_history_deletion(self, *, remove_last_only: bool) -> None:
"""Apply destructive runtime-history deletion across readers, caches, and processor replay state."""
if self._capture_session is not None:
self._show_error("Cannot modify runtime history during active capture sequence")
return
resume_acquisition = self._supervisor.is_running()
dropped_raw = 0
dropped_pre = 0
dropped_results = 0
history_command = "remove_last" if remove_last_only else "clear_all"
action_label = "last runtime measurement removed" if remove_last_only else "runtime history fully cleared"
error_action = "remove last runtime measurement" if remove_last_only else "clear runtime history"
try:
if resume_acquisition:
self._stop_run()
dropped_raw = self._raw_reader.drop_all() if self._raw_reader is not None else 0
dropped_pre = self._pre_reader.drop_all() if self._pre_reader is not None else 0
dropped_results = self._result_reader.drop_all() if self._result_reader is not None else 0
if remove_last_only:
retained_raw, retained_pre, retained_result = remove_last_aligned_histories(
list(self._raw_history),
list(self._pre_history),
list(self._result_history),
)
else:
retained_raw = []
retained_pre = []
retained_result = []
self._replace_runtime_history(
retained_raw=retained_raw,
retained_pre=retained_pre,
retained_result=retained_result,
)
self._bscan_history_floor_collection_id = 0
self._clear_history_mode_caches()
self._write_live_processing_config(history_command=history_command, bump_history_seq=True)
if self._supervisor.is_processor_running():
self._drain_results_until_quiet(timeout_s=0.35, poll_s=0.01)
if self._result_reader is not None:
dropped_results += self._result_reader.drop_all()
self._update_history_indicator()
self._redraw_after_history_deletion()
self._log(
f"{action_label}: "
f"dropped raw={dropped_raw}, "
f"preprocessed={dropped_pre}, "
f"results={dropped_results}"
)
if resume_acquisition:
self._start_run()
except Exception as exc: # noqa: BLE001
self._show_error(f"Failed to {error_action}: {exc}")
def _browse_save_path(self) -> None:
"""Open directory picker for snapshot output path."""
selected = QFileDialog.getExistingDirectory(
self,
"Select Snapshot Directory",
self._save_path_input.text().strip() or str(self._project_root),
)
if selected:
self._save_path_input.setText(selected)
def _drain_runtime_rings_for_snapshot(self) -> None:
"""Drain readers before snapshot to reduce partial-history races."""
if self._raw_reader is None and self._pre_reader is None and self._result_reader is None:
return
try:
start_raw_count = len(self._raw_history)
start_pre_count = len(self._pre_history)
start_result_count = len(self._result_history)
deadline = time.monotonic() + 1.2
while True:
progress = False
if self._raw_reader is not None:
for _ in range(self._max_pop_per_snapshot_drain):
collection = self._raw_reader.pop_raw_collection()
if collection is None:
break
self._raw_history.append(collection)
progress = True
if self._pre_reader is not None:
for _ in range(self._max_pop_per_snapshot_drain):
collection = self._pre_reader.pop_preprocessed_collection()
if collection is None:
break
self._pre_history.append(collection)
progress = True
if self._result_reader is not None:
for _ in range(self._max_pop_per_snapshot_drain):
collection = self._result_reader.pop_result_collection()
if collection is None:
break
record_result_history(self._result_history, collection)
progress = True
if progress:
continue
raw_count = len(self._raw_history)
pre_count = len(self._pre_history)
result_count = len(self._result_history)
missing_raw = self._raw_reader is not None and raw_count == start_raw_count
missing_pre = self._pre_reader is not None and pre_count == start_pre_count
missing_results = self._result_reader is not None and result_count == start_result_count
got_results = result_count > start_result_count
got_raw_or_pre = raw_count > start_raw_count or pre_count > start_pre_count
if got_results and (missing_raw or missing_pre) and time.monotonic() < deadline:
time.sleep(0.01)
continue
if got_raw_or_pre and missing_results and time.monotonic() < deadline:
time.sleep(0.01)
continue
if (
self._result_reader is not None
and max(raw_count, pre_count) > result_count
and time.monotonic() < deadline
):
time.sleep(0.01)
continue
break
except Exception as exc: # noqa: BLE001
self._log(f"Snapshot drain warning: {exc}")
def _drop_pending_ring_payloads(self, *, include_results: bool = True) -> None:
"""Drop unread payloads from active readers."""
dropped_raw = self._raw_reader.drop_all() if self._raw_reader is not None else 0
dropped_pre = self._pre_reader.drop_all() if self._pre_reader is not None else 0
dropped_results = 0
if include_results and self._result_reader is not None:
dropped_results = self._result_reader.drop_all()
if dropped_raw or dropped_pre or dropped_results:
self._log(
"Single capture ring reset: "
f"dropped raw={dropped_raw}, "
f"preprocessed={dropped_pre}, "
f"results={dropped_results}"
)