The operator frames a good spot, confirms the two DC levels the detector
reads there, and the rig then measures its own tilt: step 1.5 mm either side
on X and then on Y, and tilt the platform until those levels come back. The
correction that fixes an offset point is the correction that levels the whole
travel — height error and tilt effect are both proportional to the offset —
so the procedure ends by applying it and leaving it applied.
Both directions are measured from the same starting tilt and averaged, which
makes their disagreement a flatness read-out rather than something averaged
away silently.
core/auto_align.py holds the geometry and the search, Qt-free. The three
T-axes' azimuths are the whole geometry: T1 lies along +X so it alone tilts
along X, and T0/T2 move as an equal-and-opposite pair to tilt along Y without
touching X (tilt_response derives that, and the tests pin it — an axis map
that drifts would still converge, on the wrong axis). The search is a secant
null on the split-detector difference: probe once to learn what a microstep
is worth, sign included, then step at the null. It refuses to servo on a
scope that has not re-triggered, escalates a probe that reads as no response
before calling an axis dead, and stops at a per-axis travel limit.
gui/align_bridge.py runs it on a worker thread; stopping is a threading.Event
rather than a queued command, because the worker is inside a long handler for
the whole run. The camera window carries the button and the progress window,
and locks the scan panel and the jog pads while a run owns the stage.
Adds immediate MEAN measurements and an acquisition count to the scope
driver, and read_bias_mv to core/scope_inspect — the one scalar the
inspection state was missing.
KNOWN_ISSUES.md records what only the rig can settle: the probe step, the
travel limit, the hold current, and whether the piston the X phase applies
alongside its tilt matters.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>