Auto-align: level the sample on the DC bias levels from the camera window
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>
This commit is contained in:
@@ -14,6 +14,11 @@ sits still.
|
||||
|
||||
CH1 keeps the acquisition front-end so what is on screen is what a scan would
|
||||
record. CH3 and CH4 are rescaled as DC bias monitors (see BIAS_* below).
|
||||
|
||||
``read_bias_mv`` is the one exception to "nothing is transferred": it reads
|
||||
the two bias levels back as scalars, not waveforms, because the auto-align
|
||||
procedure (core.auto_align) has to close a loop on them. The operator still
|
||||
watches the same screen this configures.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -46,6 +51,12 @@ BIAS_POSITION_DIV = -3.5
|
||||
|
||||
BIAS_LABELS = {3: "Bias - A", 4: "Bias - B"}
|
||||
|
||||
# One MEAN measurement carries the shot-to-shot noise of a single record, and
|
||||
# the alignment loop has to resolve 5 mV. The median of a handful of reads
|
||||
# rejects the odd outlier without the averaging acquisition mode, which would
|
||||
# hide exactly the intermittent response the operator is watching CH1 for.
|
||||
BIAS_READS = 5
|
||||
|
||||
|
||||
def inspect_channel_profiles() -> dict:
|
||||
"""Channel front-end config for inspection.
|
||||
@@ -102,3 +113,24 @@ def stop_inspection(scope) -> None:
|
||||
stop the sweep — it does not try to restore the acquisition profile.
|
||||
"""
|
||||
scope.write("ACQuire:STATE STOP")
|
||||
|
||||
|
||||
def read_bias_mv(scope, reads: int = BIAS_READS) -> tuple[float, float]:
|
||||
"""Read the two DC bias levels in millivolts.
|
||||
|
||||
Returns ``(ch3_mv, ch4_mv)`` — DC 1 and DC 2 in the auto-align channel
|
||||
map. Each channel is read ``reads`` times and reduced by the median.
|
||||
|
||||
The two channels are read in separate batches rather than interleaved:
|
||||
switching the immediate-measurement source costs a round trip, and these
|
||||
are DC levels, so the few milliseconds between the batches are not a
|
||||
source of error the way they would be for a transient.
|
||||
"""
|
||||
if reads < 1:
|
||||
raise ValueError("read_bias_mv needs at least one read per channel")
|
||||
|
||||
levels = []
|
||||
for ch in BIAS_CHANNELS:
|
||||
samples = sorted(scope.measure_immediate(ch, "MEAN") for _ in range(reads))
|
||||
levels.append(samples[len(samples) // 2] * 1000.0)
|
||||
return levels[0], levels[1]
|
||||
|
||||
Reference in New Issue
Block a user