Files
scanengine-3/gui/inspect_bridge.py
Thomas Ales 23546a03f7 Pre-scan angle inspection: park the rig per angle, read the response on the scope
A 9-angle scan takes hours, and an angle that responds poorly still produces
rows that look structurally fine in the file — the SAW packet is just not
there. This lets the operator walk the angles first, parking the rig at a
random point in each, and judge the response before committing to the run.

Nothing reads the scope. The operator inspects the instrument directly, so
there is no transfer path, no plotting, and no waveform crossing the module
boundary — test_inspection_never_reads_a_waveform_back pins that, since it is
the kind of premise a later change erodes without noticing.

core/scope_inspect.py — the scope state worth looking at, which is not the
scan's state:
- plain rising-edge trigger on CH2 at 2.0 V, not the scan's logic AND of the
  laser pulse and the stage gate, so a stationary stage still triggers
- FastFrame off, SAMPLE (no averaging) — a weak or intermittent response is
  exactly what is being looked for, and averaging would hide it
- free-running (STOPAfter RUNSTop + STATE RUN) so the trace keeps updating
  while the operator looks at it
- CH1 keeps the acquisition front-end verbatim, so what is on screen is what
  a scan would record
- CH3/CH4 become bias monitors sharing one scale and position, since the
  comparison is by eye and only works if a division means the same on each.
  100 mV/div with ground 3.5 divisions below centre puts 0–700 mV on screen
  with headroom on an 8- or 10-division graticule (the signal never goes
  negative, hence moving the trace down).

core/angle_inspect.py — AngleInspector, headless and Qt-free like ScanEngine.
Points are drawn from the angle's own bounding box: Y from its actual row
positions and X uniformly across its data window, so the point is somewhere
the scan would really sample rather than merely inside the box. New Point
re-rolls without rotating, which is what separates a bad spot on the sample
from a bad angle. The stage gate is held off throughout, and the rotator goes
home on stop.

gui/inspect_bridge.py — QtAngleInspector on the existing QueueWorker base.
Inspection is click-driven rather than one long run, so the worker blocks on
its queue between commands and an open window costs nothing. BBD position
polling is suppressed while inspecting, for the same reason the scan does it:
the shared TX queue.

sc3_aui_app.py — AngleInspectWindow (angle list, prev/next, New Point) driven
off the plan currently entered in the scan panel, so it inspects exactly the
scan about to be run. Navigation locks while the stage moves. The list syncs
via itemClicked rather than currentRowChanged, so echoing the worker's
position back does not re-trigger the move it is reporting.

README picks up the new modules, and scope_burst.py which the previous merge
left out of the structure listing. 114 tests passing, ruff clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 12:41:52 -05:00

114 lines
3.9 KiB
Python

"""Qt bridge over the headless AngleInspector.
Inspection is command-driven rather than one long run: the operator clicks an
angle, waits for the stage to park, looks at the scope, clicks again. That is
exactly the shape QueueWorker exists for — it blocks on the queue between
commands instead of polling, so an inspection window left open costs nothing.
Every stage move and rotation blocks for seconds, so all of it runs on this
worker's thread; the window only ever enqueues and reacts to signals.
"""
from __future__ import annotations
import traceback
from PyQt6.QtCore import pyqtSignal
from core.angle_inspect import AngleInspector, InspectCallbacks
from gui.qt_workers import QueueWorker
class QtAngleInspector(QueueWorker):
"""Runs an AngleInspector on its own QThread and republishes its events."""
ready = pyqtSignal(object) # InspectionPoint — start() succeeded
start_failed = pyqtSignal(str)
point_changed = pyqtSignal(object) # InspectionPoint
status_msg = pyqtSignal(str)
busy_changed = pyqtSignal(bool) # True while a move is in flight
stopped = pyqtSignal()
def __init__(self, stage, scope, rotator, plan, on_inspect_active=None):
super().__init__()
self._on_inspect_active = on_inspect_active
callbacks = InspectCallbacks(
on_status=self.status_msg.emit,
on_point=self.point_changed.emit,
on_busy=self.busy_changed.emit,
)
self._inspector = AngleInspector(stage, scope, rotator, plan,
callbacks=callbacks)
self._handlers = {
"start": self._do_start,
"goto": self._do_goto,
"new_point": self._do_new_point,
"stop": self._do_stop,
}
# ── Introspection (safe from the GUI thread: reads the plan, not the rig) ──
def angle_labels(self) -> list[str]:
return self._inspector.angle_labels()
@property
def n_angles(self) -> int:
return self._inspector.n_angles
# ── Command submission (GUI thread) ───────────────────────────────────────
def request_start(self):
self._enqueue("start")
def request_goto(self, angle_idx: int):
self._enqueue("goto", angle_idx=angle_idx)
def request_new_point(self):
self._enqueue("new_point")
def request_stop(self):
self._enqueue("stop")
# ── Handlers (worker thread) ──────────────────────────────────────────────
def _do_start(self):
if self._on_inspect_active is not None:
self._on_inspect_active(True)
try:
point = self._inspector.start()
except Exception as exc:
traceback.print_exc()
if self._on_inspect_active is not None:
self._on_inspect_active(False)
self.start_failed.emit(str(exc))
return
self.ready.emit(point)
def _do_goto(self, angle_idx: int):
self._inspector.goto_angle(angle_idx)
def _do_new_point(self):
self._inspector.new_point()
def _do_stop(self):
try:
self._inspector.stop()
finally:
if self._on_inspect_active is not None:
self._on_inspect_active(False)
self.stopped.emit()
def _on_stop(self):
"""Worker loop exiting — make sure the rig is left in a safe state.
Covers the case where the window is closed without a clean stop
command reaching the queue.
"""
try:
self._inspector.stop()
except Exception:
traceback.print_exc()
finally:
if self._on_inspect_active is not None:
self._on_inspect_active(False)