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:
+107
@@ -258,6 +258,9 @@ class FakeT3R:
|
||||
self._t = trace
|
||||
self.is_open = is_open
|
||||
self._motion_completes = motion_completes
|
||||
# Microsteps commanded per channel, so a test can read the tilt the
|
||||
# platform ended up at rather than replaying the move trace.
|
||||
self.positions = {ch: 0 for ch in range(4)}
|
||||
|
||||
def set_microstep(self, ch, micro):
|
||||
self._t.record("t3r_set_microstep", ch, micro)
|
||||
@@ -273,9 +276,113 @@ class FakeT3R:
|
||||
return round(self.MOTOR_FULL_STEPS_PER_REV * microsteps * ratio
|
||||
* angle_deg / 360.0)
|
||||
|
||||
def move(self, ch, steps, velocity, accel):
|
||||
self._t.record("t3r_move", ch, steps)
|
||||
self.positions[ch] += steps
|
||||
|
||||
def rotate_stage(self, angle_deg, microsteps, velocity, accel):
|
||||
self._t.record("t3r_rotate", round(angle_deg, 6))
|
||||
|
||||
def wait_motion_done(self, ch, timeout):
|
||||
self._t.record("t3r_wait_motion_done", ch)
|
||||
return self._motion_completes
|
||||
|
||||
|
||||
class FakeAlignRig:
|
||||
"""A tilted sample on the tilt platform, as the DC levels would read it.
|
||||
|
||||
The detection beam is fixed and the stage carries the sample under it, so
|
||||
the height error under the beam is the sample's slope times how far the
|
||||
stage has moved off the reference point. The T-axes tilt the sample the
|
||||
other way: their three heights define a plane, and its slope adds to the
|
||||
sample's. Nulling the split-detector difference therefore means cancelling
|
||||
the sample slope — which is exactly what an aligner has to work out.
|
||||
|
||||
The plane is fitted by least squares here, rather than reusing
|
||||
core.auto_align's closed form, so the two are independent statements of
|
||||
the same geometry.
|
||||
|
||||
``curvature_mv_per_mm2`` bends the surface: a curved sample needs opposite
|
||||
corrections at +1.5 mm and -1.5 mm, which is the disagreement the
|
||||
procedure is supposed to report instead of averaging away.
|
||||
"""
|
||||
|
||||
# Actuator azimuths on the platform, in degrees from stage +X.
|
||||
AZIMUTH_DEG = {0: 120.0, 1: 0.0, 2: 240.0}
|
||||
|
||||
def __init__(self, stage, t3r, ref_mm=(50.0, 40.0),
|
||||
x_slope_mv_per_mm=40.0, y_slope_mv_per_mm=-25.0,
|
||||
tilt_gain_mv_per_mm=0.2, base_mv=400.0,
|
||||
curvature_mv_per_mm2=0.0, jitter_mv=0.0):
|
||||
self._stage = stage
|
||||
self._t3r = t3r
|
||||
self.ref_mm = ref_mm
|
||||
self.x_slope_mv_per_mm = x_slope_mv_per_mm
|
||||
self.y_slope_mv_per_mm = y_slope_mv_per_mm
|
||||
self.tilt_gain_mv_per_mm = tilt_gain_mv_per_mm
|
||||
self.base_mv = base_mv
|
||||
self.curvature_mv_per_mm2 = curvature_mv_per_mm2
|
||||
self.jitter_mv = jitter_mv
|
||||
self._reads = 0
|
||||
|
||||
# -- geometry -----------------------------------------------------------
|
||||
def platform_tilt(self):
|
||||
"""(x_tilt, y_tilt) of the plane through the three actuator heights."""
|
||||
import numpy as np
|
||||
rows, heights = [], []
|
||||
for ch, azimuth in self.AZIMUTH_DEG.items():
|
||||
theta = np.radians(azimuth)
|
||||
rows.append([1.0, np.cos(theta), np.sin(theta)])
|
||||
heights.append(float(self._t3r.positions[ch]))
|
||||
_, x_tilt, y_tilt = np.linalg.lstsq(np.array(rows), np.array(heights),
|
||||
rcond=None)[0]
|
||||
return float(x_tilt), float(y_tilt)
|
||||
|
||||
def slopes_mv_per_mm(self):
|
||||
"""The residual sample slope the beam sees, after the platform tilt."""
|
||||
x_tilt, y_tilt = self.platform_tilt()
|
||||
return (self.x_slope_mv_per_mm + self.tilt_gain_mv_per_mm * x_tilt,
|
||||
self.y_slope_mv_per_mm + self.tilt_gain_mv_per_mm * y_tilt)
|
||||
|
||||
def difference_mv(self):
|
||||
x_off = self._stage.positions[0] - self.ref_mm[0]
|
||||
y_off = self._stage.positions[1] - self.ref_mm[1]
|
||||
slope_x, slope_y = self.slopes_mv_per_mm()
|
||||
return (slope_x * x_off + slope_y * y_off
|
||||
+ self.curvature_mv_per_mm2 * (x_off ** 2 + y_off ** 2))
|
||||
|
||||
# -- what the scope reports ---------------------------------------------
|
||||
def level_v(self, channel):
|
||||
"""CH3 and CH4 as volts: the difference straddling a constant sum.
|
||||
|
||||
The sum is fixed because tilt steers the beam across the detector
|
||||
rather than changing how much light comes back — so a nulled
|
||||
difference does put both levels back where they were.
|
||||
"""
|
||||
self._reads += 1
|
||||
# A deterministic alternating wobble, so a test can check the median
|
||||
# of several reads is what keeps the loop stable.
|
||||
jitter = self.jitter_mv * (1 if self._reads % 2 else -1)
|
||||
half = 0.5 * self.difference_mv()
|
||||
mv = self.base_mv + (half if channel == 3 else -half) + jitter
|
||||
return mv / 1000.0
|
||||
|
||||
|
||||
class FakeAlignScope(FakeScope):
|
||||
"""FakeScope that also answers the DC measurements auto-align reads."""
|
||||
|
||||
def __init__(self, trace: Trace, rig: FakeAlignRig, samples_per_frame=8,
|
||||
acquisitions_advance=True):
|
||||
super().__init__(trace, samples_per_frame=samples_per_frame)
|
||||
self._rig = rig
|
||||
self._acq = 0
|
||||
self._advance = acquisitions_advance
|
||||
|
||||
def measure_immediate(self, channel, measurement_type="MEAN"):
|
||||
self._t.record("measure_immediate", channel, measurement_type)
|
||||
return self._rig.level_v(channel)
|
||||
|
||||
def get_acquisition_count(self):
|
||||
if self._advance:
|
||||
self._acq += 1
|
||||
return self._acq
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
"""Auto-align, driven entirely by fake hardware.
|
||||
|
||||
The feature is a closed loop over hardware, so the tests are built round a
|
||||
model of the rig (fakes.FakeAlignRig): a sample at a known tilt, a platform
|
||||
whose three actuators tilt it, and DC levels that follow from both. A test
|
||||
therefore asks the question the operator does — is the sample level now? —
|
||||
rather than replaying a command sequence.
|
||||
|
||||
The other half is geometry. Which axis moves for which stage direction is
|
||||
the one thing here that cannot be discovered at run time, and getting it
|
||||
wrong would still converge (on the wrong axis, at the wrong point), so it is
|
||||
pinned separately and explicitly.
|
||||
"""
|
||||
from dataclasses import replace
|
||||
|
||||
import pytest
|
||||
|
||||
from core.auto_align import (
|
||||
AutoAligner, AutoAlignAborted, AutoAlignError, DEFAULT_ALIGN,
|
||||
T_AXIS_AZIMUTH_DEG, X_TILT, Y_TILT, tilt_response,
|
||||
)
|
||||
from core.scan_engine import AXIS_X, AXIS_Y
|
||||
from core.scope_inspect import BIAS_CHANNELS, BIAS_SCALE_V_DIV, INSPECT_TRIG_LEVEL_V
|
||||
from fakes import FakeAlignRig, FakeAlignScope, FakeStage, FakeT3R, Trace
|
||||
|
||||
REF_MM = (50.0, 40.0)
|
||||
|
||||
# No settle: the sleeps are there for the instrument, and every test here
|
||||
# takes a few dozen readings.
|
||||
FAST = replace(DEFAULT_ALIGN, settle_s=0.0)
|
||||
|
||||
|
||||
def build(*, settings=FAST, acquisitions_advance=True, ref_mm=REF_MM,
|
||||
should_abort=lambda: False, **rig_kwargs):
|
||||
trace = Trace()
|
||||
stage = FakeStage(trace)
|
||||
stage.positions = list(ref_mm)
|
||||
t3r = FakeT3R(trace)
|
||||
rig = FakeAlignRig(stage, t3r, ref_mm=ref_mm, **rig_kwargs)
|
||||
scope = FakeAlignScope(trace, rig, acquisitions_advance=acquisitions_advance)
|
||||
aligner = AutoAligner(stage, scope, t3r, settings=settings,
|
||||
should_abort=should_abort)
|
||||
return aligner, rig, trace, stage, t3r
|
||||
|
||||
|
||||
def moves(trace, ch=None):
|
||||
return [c for c in trace.of("t3r_move") if ch is None or c[1] == ch]
|
||||
|
||||
|
||||
# ── Geometry: the half that cannot be discovered at run time ─────────────────
|
||||
|
||||
def test_tilt_groups_are_the_moves_they_claim_to_be():
|
||||
"""X tilts along X only, Y along Y only — otherwise the phases interfere.
|
||||
|
||||
If this fails, the azimuth map and the groups have drifted apart and the
|
||||
Y phase would be undoing the X phase's correction.
|
||||
"""
|
||||
x_piston, x_x, x_y = tilt_response(X_TILT)
|
||||
y_piston, y_x, y_y = tilt_response(Y_TILT)
|
||||
|
||||
assert x_x != 0 and x_y == pytest.approx(0.0, abs=1e-9)
|
||||
assert y_y != 0 and y_x == pytest.approx(0.0, abs=1e-9)
|
||||
# The Y pair is equal and opposite, so it lifts nothing on average; the
|
||||
# single X axis unavoidably lifts the platform as well as tilting it.
|
||||
assert y_piston == pytest.approx(0.0, abs=1e-9)
|
||||
assert x_piston != 0
|
||||
|
||||
|
||||
def test_x_is_corrected_by_the_axis_lying_along_x():
|
||||
"""T1 sits at 0°, so it is the one that tilts the platform along X."""
|
||||
assert T_AXIS_AZIMUTH_DEG[1] == 0.0
|
||||
assert set(X_TILT.weights) == {1}
|
||||
|
||||
|
||||
def test_y_is_corrected_by_the_other_two_as_an_opposed_pair():
|
||||
assert set(Y_TILT.weights) == {0, 2}
|
||||
assert Y_TILT.weights[0] == -Y_TILT.weights[2]
|
||||
|
||||
|
||||
# ── The loop does what it is for ─────────────────────────────────────────────
|
||||
|
||||
def test_alignment_cancels_the_sample_slope_on_both_axes():
|
||||
"""The point of the whole procedure: a level sample when it finishes."""
|
||||
aligner, rig, _, _, _ = build()
|
||||
aligner.prepare()
|
||||
result = aligner.run()
|
||||
|
||||
slope_x, slope_y = rig.slopes_mv_per_mm()
|
||||
# Residual slope over the +/-1.5 mm the scan cares about, in mV.
|
||||
assert abs(slope_x * DEFAULT_ALIGN.offset_mm) <= DEFAULT_ALIGN.tolerance_mv
|
||||
assert abs(slope_y * DEFAULT_ALIGN.offset_mm) <= DEFAULT_ALIGN.tolerance_mv
|
||||
assert result.ok
|
||||
|
||||
|
||||
def test_every_search_ends_inside_the_tolerance():
|
||||
aligner, _, _, _, _ = build()
|
||||
reference = aligner.prepare()
|
||||
result = aligner.run()
|
||||
|
||||
for axis in result.axes:
|
||||
for offset in axis.offsets:
|
||||
assert offset.converged, offset.describe()
|
||||
assert offset.final.matches(reference, DEFAULT_ALIGN.tolerance_mv)
|
||||
assert result.final.matches(reference, DEFAULT_ALIGN.tolerance_mv)
|
||||
|
||||
|
||||
def test_a_flat_sample_gives_the_same_answer_in_both_directions():
|
||||
"""Both offsets measure one angle, so on a plane they must agree.
|
||||
|
||||
The agreement is what licenses averaging them; see the curved case below
|
||||
for what happens when it does not hold.
|
||||
"""
|
||||
aligner, _, _, _, _ = build()
|
||||
aligner.prepare()
|
||||
result = aligner.run()
|
||||
|
||||
for axis in result.axes:
|
||||
plus, minus = axis.offsets
|
||||
assert plus.correction_steps == pytest.approx(minus.correction_steps,
|
||||
rel=0.02, abs=5.0)
|
||||
assert axis.disagreement_steps < 10.0
|
||||
assert axis.applied
|
||||
|
||||
|
||||
def test_a_curved_sample_is_reported_rather_than_averaged_away():
|
||||
"""Curvature needs opposite corrections either side, and says so."""
|
||||
# Big enough that the near side is still outside the tolerance once the
|
||||
# far side's error has been curved past it — otherwise one search has
|
||||
# nothing to do and the disagreement never shows up.
|
||||
aligner, _, _, _, _ = build(curvature_mv_per_mm2=60.0)
|
||||
aligner.prepare()
|
||||
result = aligner.run()
|
||||
|
||||
x_axis = result.axes[0]
|
||||
plus, minus = x_axis.offsets
|
||||
assert plus.correction_steps * minus.correction_steps < 0 # opposite signs
|
||||
assert x_axis.disagreement_steps > 100.0
|
||||
|
||||
|
||||
def test_the_search_survives_a_noisy_detector():
|
||||
"""Five reads and a median, so a wobbling level still converges."""
|
||||
aligner, _, _, _, _ = build(jitter_mv=1.5)
|
||||
reference = aligner.prepare()
|
||||
result = aligner.run()
|
||||
assert result.final.matches(reference, DEFAULT_ALIGN.tolerance_mv)
|
||||
|
||||
|
||||
# ── Which hardware moves, and how ───────────────────────────────────────────
|
||||
|
||||
def test_the_x_phase_moves_t1_and_the_y_phase_moves_t0_and_t2():
|
||||
"""The phases stay on their own axes, in the order X then Y."""
|
||||
aligner, _, trace, _, t3r = build()
|
||||
aligner.prepare()
|
||||
aligner.run()
|
||||
|
||||
channels = [c[1] for c in moves(trace)]
|
||||
first_y = next(i for i, ch in enumerate(channels) if ch in (0, 2))
|
||||
assert set(channels[:first_y]) == {1}, "the X phase moved something else"
|
||||
assert set(channels[first_y:]) == {0, 2}, "the Y phase moved something else"
|
||||
|
||||
# The Y pair ends equal and opposite: anything else is a tilt along X the
|
||||
# Y phase had no business applying.
|
||||
assert t3r.positions[0] == -t3r.positions[2]
|
||||
assert t3r.positions[1] != 0
|
||||
|
||||
|
||||
def test_the_rotation_axis_is_never_touched():
|
||||
"""GR carries the scan's angle; an alignment that moved it would silently
|
||||
re-datum every subsequent scan."""
|
||||
aligner, _, trace, _, t3r = build()
|
||||
aligner.prepare()
|
||||
aligner.run()
|
||||
|
||||
assert moves(trace, ch=3) == []
|
||||
assert t3r.positions[3] == 0
|
||||
assert [c for c in trace.of("t3r_enable") if c[1] == 3] == []
|
||||
|
||||
|
||||
def test_the_t_axes_are_configured_before_they_are_moved():
|
||||
"""32 microsteps and 600 mA, applied rather than assumed — a correction is
|
||||
reported in microsteps, so what a microstep means has to be pinned."""
|
||||
aligner, _, trace, _, _ = build()
|
||||
aligner.prepare()
|
||||
|
||||
for ch in (0, 1, 2):
|
||||
assert ("t3r_set_microstep", ch, 32) in trace.calls
|
||||
run_ma = [c for c in trace.of("t3r_set_current") if c[1] == ch]
|
||||
assert run_ma and run_ma[0][2] == 600
|
||||
assert ("t3r_enable", ch) in trace.calls
|
||||
|
||||
names = trace.names()
|
||||
assert "t3r_move" not in names[:names.index("t3r_enable")]
|
||||
|
||||
|
||||
def test_the_stage_steps_either_side_and_comes_back():
|
||||
aligner, _, trace, stage, _ = build()
|
||||
aligner.prepare()
|
||||
aligner.run()
|
||||
aligner.stop()
|
||||
|
||||
x_targets = [c[2] for c in trace.of("move_axis_absolute") if c[1] == AXIS_X]
|
||||
y_targets = [c[2] for c in trace.of("move_axis_absolute") if c[1] == AXIS_Y]
|
||||
off = DEFAULT_ALIGN.offset_mm
|
||||
assert REF_MM[0] + off in x_targets and REF_MM[0] - off in x_targets
|
||||
assert REF_MM[1] + off in y_targets and REF_MM[1] - off in y_targets
|
||||
assert stage.positions == list(REF_MM)
|
||||
|
||||
|
||||
def test_the_scope_is_put_into_the_bias_reading_state():
|
||||
"""The same free-running, edge-triggered state the angle inspector uses:
|
||||
the operator has to be able to read CH1 while this runs."""
|
||||
aligner, _, trace, _, _ = build()
|
||||
aligner.prepare()
|
||||
|
||||
scales = {c[1]: c[2] for c in trace.of("set_channel_scale")}
|
||||
for ch in BIAS_CHANNELS:
|
||||
assert scales[ch] == BIAS_SCALE_V_DIV
|
||||
assert ("set_trigger_level", 2, INSPECT_TRIG_LEVEL_V) in trace.calls
|
||||
assert ("set_fastframe_state", False) in trace.calls
|
||||
assert "ACQuire:STATE RUN" in [c[1] for c in trace.of("write")]
|
||||
|
||||
# Only the two bias channels are ever measured.
|
||||
assert {c[1] for c in trace.of("measure_immediate")} == set(BIAS_CHANNELS)
|
||||
|
||||
|
||||
def test_the_gate_is_dropped_before_anything_moves():
|
||||
"""An armed TRIGOUT would drive the scan gate on every positioning move."""
|
||||
aligner, _, trace, _, _ = build()
|
||||
aligner.prepare()
|
||||
assert ("set_trigger_gate_off", AXIS_X) in trace.calls
|
||||
|
||||
|
||||
# ── Refusals ────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_a_dead_axis_stops_the_procedure():
|
||||
"""No response to a probe, however large: something is wrong upstream of
|
||||
the tilt platform, and stepping the actuators further will not find it."""
|
||||
aligner, _, _, _, _ = build(tilt_gain_mv_per_mm=0.0)
|
||||
aligner.prepare()
|
||||
with pytest.raises(AutoAlignError, match="laser"):
|
||||
aligner.run()
|
||||
|
||||
|
||||
def test_a_scope_that_never_retriggers_stops_the_procedure():
|
||||
"""A stale record reads as a rock-steady measurement — the one failure the
|
||||
loop cannot see for itself."""
|
||||
aligner, _, _, _, _ = build(acquisitions_advance=False)
|
||||
with pytest.raises(AutoAlignError, match="not triggered"):
|
||||
aligner.prepare()
|
||||
|
||||
|
||||
def test_an_axis_that_would_run_out_of_travel_stops_the_procedure():
|
||||
aligner, _, _, _, _ = build(x_slope_mv_per_mm=200.0,
|
||||
tilt_gain_mv_per_mm=0.01)
|
||||
aligner.prepare()
|
||||
with pytest.raises(AutoAlignError, match="safety limit"):
|
||||
aligner.run()
|
||||
|
||||
|
||||
def test_there_has_to_be_room_either_side_of_the_reference_point():
|
||||
aligner, _, _, _, _ = build(ref_mm=(0.5, 40.0))
|
||||
with pytest.raises(AutoAlignError, match="either side"):
|
||||
aligner.prepare()
|
||||
|
||||
|
||||
def test_run_before_the_operator_confirms_is_refused():
|
||||
aligner, _, _, _, _ = build()
|
||||
with pytest.raises(AutoAlignError, match="prepare"):
|
||||
aligner.run()
|
||||
|
||||
|
||||
def test_missing_hardware_is_named():
|
||||
trace = Trace()
|
||||
stage = FakeStage(trace)
|
||||
t3r = FakeT3R(trace, is_open=False)
|
||||
rig = FakeAlignRig(stage, t3r)
|
||||
scope = FakeAlignScope(trace, rig)
|
||||
|
||||
with pytest.raises(AutoAlignError, match="T3R"):
|
||||
AutoAligner(stage, scope, t3r, settings=FAST).prepare()
|
||||
with pytest.raises(AutoAlignError, match="Oscilloscope"):
|
||||
AutoAligner(stage, None, t3r, settings=FAST).prepare()
|
||||
with pytest.raises(AutoAlignError, match="BBD202"):
|
||||
AutoAligner(None, scope, t3r, settings=FAST).prepare()
|
||||
|
||||
|
||||
def test_an_abort_stops_the_run_and_still_parks_the_stage():
|
||||
"""Stopping is the operator's, so it must not leave the stage 1.5 mm off
|
||||
the point they were looking at."""
|
||||
calls = {"n": 0}
|
||||
|
||||
def abort_after_a_few_moves():
|
||||
calls["n"] += 1
|
||||
return calls["n"] > 12
|
||||
|
||||
aligner, _, _, stage, _ = build(should_abort=abort_after_a_few_moves)
|
||||
aligner.prepare()
|
||||
with pytest.raises(AutoAlignAborted):
|
||||
aligner.run()
|
||||
|
||||
aligner.stop()
|
||||
assert stage.positions == list(REF_MM)
|
||||
|
||||
|
||||
def test_stop_leaves_the_correction_applied():
|
||||
"""The tilt is the result — a stop parks the stage, not the platform."""
|
||||
aligner, _, _, _, t3r = build()
|
||||
aligner.prepare()
|
||||
aligner.run()
|
||||
applied = dict(t3r.positions)
|
||||
aligner.stop()
|
||||
assert t3r.positions == applied
|
||||
@@ -38,6 +38,51 @@ def test_sc3_aui_main_window(qapp):
|
||||
_pump(qapp)
|
||||
|
||||
|
||||
def test_camera_window_without_hardware_is_a_plain_viewer(qapp):
|
||||
"""No stage, no scope, no auto-align button to press."""
|
||||
import sc3_aui_app
|
||||
win = sc3_aui_app.CameraWindow()
|
||||
_pump(qapp)
|
||||
try:
|
||||
assert not win.uc480_auto_align_btn.isVisible()
|
||||
finally:
|
||||
win.deleteLater()
|
||||
_pump(qapp)
|
||||
|
||||
|
||||
def test_camera_window_auto_align_needs_every_device(qapp):
|
||||
"""The button appears once the three devices exist, and says which one is
|
||||
missing rather than starting and failing on the rig."""
|
||||
import sc3_aui_app
|
||||
from gui.qt_t3r import QtT3RAdapter
|
||||
win = sc3_aui_app.CameraWindow(QtT3RAdapter(), sc3_aui_app.BBD202Worker(),
|
||||
sc3_aui_app.OscopeWorker())
|
||||
_pump(qapp)
|
||||
try:
|
||||
assert win.uc480_auto_align_btn.isVisibleTo(win)
|
||||
assert "oscilloscope" in win._align_prerequisite_problem()
|
||||
finally:
|
||||
win.deleteLater()
|
||||
_pump(qapp)
|
||||
|
||||
|
||||
def test_auto_align_window_logs_a_result(qapp):
|
||||
import sc3_aui_app
|
||||
from core.auto_align import AlignResult, Reading
|
||||
win = sc3_aui_app.AutoAlignWindow()
|
||||
_pump(qapp)
|
||||
try:
|
||||
reference = Reading(400.0, 400.0)
|
||||
win.set_reference(reference)
|
||||
win.on_reading(Reading(403.0, 397.0))
|
||||
win.on_finished(AlignResult(reference=reference, final=reference))
|
||||
assert "Reference" in win.log.toPlainText()
|
||||
assert win.close_btn.isEnabled()
|
||||
finally:
|
||||
win.deleteLater()
|
||||
_pump(qapp)
|
||||
|
||||
|
||||
def test_sras_viewer_window(qapp):
|
||||
import sras_viewer
|
||||
win = sras_viewer.SrasViewerWindow()
|
||||
|
||||
Reference in New Issue
Block a user