From f0f8f620ded0d53c55e574b76f635433d70f2db6 Mon Sep 17 00:00:00 2001 From: menzel Date: Tue, 25 Aug 2026 12:24:01 +0200 Subject: [PATCH] debug: report the achieved geometry, and catch stalls that keep a full span The first clean run said both scans were fine, which conflicts with scan 473 being classified as having the artefact. Two gaps explain how that verdict could be wrong. **The file was never checked against the intended scan.** Measured fast-axis travel implies roughly 6.2 um per point in S00473 and 9.2 um in S00254, while the scans were run at 0.65 um and 1.00 um -- both about 9.3x larger. Until that is resolved the analysis rests on an unverified assumption about which axis is which and in what units. The check now prints the fast and slow axis ranges, the per-line slow step, and the implied step size, so the file can be compared directly against the scan command. A mismatch means the wrong axis, the wrong units, or motion that was not commanded, and all three matter more than anything downstream. **Whole-line spans cannot see a partial stall.** An axis that freezes part-way through a line and then continues still covers its full range, so the span test passes. Since the report describes intermittent motion loss rather than whole lines going missing, that is the more likely shape of the fault. Lines are now also scanned for runs of consecutive position samples that did not advance, reported with the same start/middle/end distribution. Verified against a file whose stalls leave every span intact: the span test sees nothing, the new one finds all eight and places them in the final tenth. Note the sampling limit still applies -- 74 position samples against 428 data points in S00473, and about 8 against 205 in S00254. A stall shorter than the sampling interval remains invisible, so a clean result here is not proof. Co-Authored-By: Claude Opus 5 (1M context) --- bin/debug/scan_artefact_check.py | 45 ++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/bin/debug/scan_artefact_check.py b/bin/debug/scan_artefact_check.py index 0552e26d..83390f67 100755 --- a/bin/debug/scan_artefact_check.py +++ b/bin/debug/scan_artefact_check.py @@ -429,6 +429,51 @@ def check_line_motion_from_motor(signals, motors, meta) -> None: return ratio = spans / median print(f" samples per line : ~{per_line} median line span : {median:.6g}") + + # Enough geometry to check this file against the scan that was intended. + # A step size that disagrees with the scan command means either the wrong + # axis has been identified or the motion was not what was asked for -- and + # both make everything below meaningless. + grid_pts = int(meta.get("frames_per_trigger") or 0) + print(f" fast axis range : {arr.min():.6g} .. {arr.max():.6g}") + if slow_name is not None: + sarr = signals[slow_name] + print(f" slow axis range : {sarr.min():.6g} .. {sarr.max():.6g}" + f" ({found} lines -> {(sarr.max() - sarr.min()) / max(1, found - 1):.4g} per line)") + if grid_pts: + print(f" implied step : {median / grid_pts:.4g} per point" + f" ({median:.4g} over {grid_pts} points)") + print(" ^ compare with the step size in the scan command. A mismatch means") + print(" the wrong axis, the wrong units, or motion that was not commanded.") + + # Whole-line spans only catch a line that never moved. An axis that stalls + # part-way and then continues still spans the full range, so look for runs of + # samples that did not advance. + stalled = [] + for i, sg in enumerate(segs): + if sg.size < 4: + continue + step = np.abs(np.diff(sg)) + typical = float(np.median(step)) + if typical <= 0: + continue + frozen = step < 0.1 * typical + run, longest = 0, 0 + for f in frozen: + run = run + 1 if f else 0 + longest = max(longest, run) + if longest >= 3: + stalled.append((i, longest, sg.size)) + if stalled: + print(f"\n within-line stalls: {len(stalled)} lines contain a run of >=3" + " consecutive samples that did not advance") + report_distribution(np.array([i for i, _, _ in stalled]), found, "within-line stalls") + for i, longest, size in stalled[:10]: + print(f" line {i:>5} {longest} frozen of {size} samples") + print(" A line can span its full range and still have stopped part-way;") + print(" this is the partial-stall case the span check cannot see.") + else: + print(" no within-line stalls either (no run of >=3 frozen samples).") if per_line < 5: print(f" WARNING: only ~{per_line} position samples per line. The motor trace") print(" is far sparser than the data, so a partial stall can hide between")