diff --git a/bin/debug/scan_artefact_check.py b/bin/debug/scan_artefact_check.py index f80b5267..cf95ba61 100755 --- a/bin/debug/scan_artefact_check.py +++ b/bin/debug/scan_artefact_check.py @@ -478,7 +478,32 @@ def rank_fast_traces(cands: dict, slow_name, slow_breaks) -> list: return scored -def check_line_motion_from_motor(signals, motors, meta) -> None: +def readback_coverage(segs, travel_cmd: float, direction: float): + """(coverage, forward), each as a fraction of the commanded travel. + + coverage -- how much of the line the samples span at all. + forward -- how far they get in the direction the line was commanded to run. + + Repetition across lines cannot be the test for "is this a trace of the + motion": a healthy continuous scan repeats too, because every line does the + same thing. What separates a trace from a handful of loop checkpoints is + whether the samples follow the line at all. + """ + if travel_cmd <= 0 or not direction: + return None, None + cov, fwd = [], [] + for sg in segs: + if sg.size < 2: + continue + cov.append(float(np.ptp(sg)) / travel_cmd) + cov_adv = direction * (sg - sg[0]) + fwd.append(float(cov_adv.max()) / travel_cmd) + if not cov: + return None, None + return float(np.median(cov)), float(np.median(fwd)) + + +def check_line_motion_from_motor(signals, motors, meta, req=None) -> None: """Bug 2, using a real positioner trace rather than whatever looks rampy.""" print("\n BUG 2 -- did the fast axis move on every line?") cands = motor_candidates(signals, motors) @@ -522,6 +547,24 @@ def check_line_motion_from_motor(signals, motors, meta) -> None: ratio = spans / median print(f" samples per line : ~{per_line} median line span : {median:.6g}") + fs, fe = (req or {}).get("fast_start"), (req or {}).get("fast_end") + if isinstance(fs, (int, float)) and isinstance(fe, (int, float)) and fe != fs: + cov, fwd = readback_coverage(segs, abs(fe - fs), np.sign(fe - fs)) + if cov is not None: + print(f" readback covers {cov:.0%} of the commanded travel," + f" {fwd:.0%} of it in the commanded direction") + if fwd < 0.1: + print("\n STOP: the readback never advances along the commanded") + print(" direction. It is sampled at fixed points in the scan loop --") + print(" after the burst and during the return -- and not while the") + print(" line runs. The spans, stalls and step sizes below are") + print(" properties of the loop, not of the stage, and none of them is") + print(" evidence about the motion.\n") + elif cov < 0.6: + print(f"\n CAUTION: the readback spans only {cov:.0%} of the line, so" + " it misses") + print(" part of every traverse. The travel below is understated.\n") + # 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 @@ -685,7 +728,11 @@ def _trace_grid_data(signals, motors, grid, nominal, index) -> None: bulk = float(np.median(np.abs(row))) if not np.isfinite(bulk) or bulk <= 0: continue - ranked.append((float(np.ptp(row)) / bulk, name, row)) + # Range in units of counting noise, not of level. A channel sitting at + # 2 counts has an enormous *relative* range and no information in it; + # ranking by ptp/median floats that Poisson noise to the top and buries + # the diode that actually formed the image. + ranked.append((float(np.ptp(row)) / max(np.sqrt(bulk), 1.0), name, row)) if not ranked: print("\n no per-point data on the scan grid for this line.") return @@ -784,6 +831,20 @@ def _trace_position(signals, times, motors, meta, nominal, v_cmd, index, per_lin stop_i = int(np.argmax(adv)) # first sample at maximum travel point_step = travel_cmd / max(1, per_line - 1) + # If nothing ever advances, these samples are not a trace of the traverse and + # nothing about the traverse can be read out of them. Saying so is the whole + # point: the alternative is to describe the return move as if it were the + # line, which is exactly the wrong answer confidently delivered. + if float(adv.max()) <= 0.1 * travel_cmd: + print(f" NOT A TRACE OF THE LINE. The samples span" + f" {float(seg.min()):.6f} .. {float(seg.max()):.6f}" + f" -- {float(np.ptp(seg)) / travel_cmd:.0%} of the commanded travel, and") + print(" none of it in the commanded direction. They are taken at fixed") + print(" points in the scan loop -- after the burst and during the return --") + print(" not while the line runs. No mechanism can be named from this file;") + print(" it needs a faster monitored readout or a measurement on the motor.") + return + if t is not None and stop_i > 0 and t[stop_i] > 0: v_ach = adv[stop_i] / t[stop_i] line = f" velocity while moving : {v_ach:.6g}/s" @@ -982,7 +1043,7 @@ def analyse(path: str, do_list: bool, force_lines=None, force_points=None, # motor position does not -- it is sampled while the line runs, at whatever # rate the readout loop managed, so it has its own length and its own # line boundaries. - check_line_motion_from_motor(signals, motors, meta) + check_line_motion_from_motor(signals, motors, meta, read_request_inputs(h5)) check_end_of_line(signals, grid, mon, None) if trace_lines: times = numeric_timestamps(h5)