Four scans agreed that the first point of every line loses a fixed
number of milliseconds rather than a fixed fraction -- 5.18 ms at 15 ms
per point, 5.36 at 20, 5.93 and 6.04 at 50 -- while the fraction itself
moved from 35% to 12%. Scan 473 was the exception at 16.09 ms, and its
own last/bulk of 1.207 said why: the intensity rises 20% along every
line in that scan, so the whole-line mean is a biased baseline and
manufactures a deficit at the low end.
Both ends are now measured against their own k nearest neighbours, and
the along-line trend is reported next to them so the confounder is
visible rather than folded in. On a synthetic file carrying a true 6.0 ms
deficit under a 20% ramp, the old baseline reported 14.46 ms and the new
one reports 6.09.
A channel only contributes to the summary if its deficit exceeds three
standard errors over the lines. mca8 sits at two counts and was donating
a meaningless 0.71 ms to the median.
Positioners are also now excluded by having a setpoint rather than by
their trace being monotonic. The shape test discarded any intensity that
drifts smoothly along a line -- which is precisely the case that
motivated this commit.
samx and samy are open-loop steppers, so there is nothing to ask where
the sample actually was, and the position readback in the file turned
out to be three loop checkpoints per line. Both routes to the answer
were closed.
The data still holds it. Two points taken at the same physical position
differ only by noise; two taken at different positions differ by the
sample as well. Comparing every point of a line against the last one,
across all lines at once, uses the sample's own line-to-line pattern as
a fingerprint and needs no knowledge of what the sample is.
The discriminator is the trend, not the level. While the stage moves the
residual grows with distance from the reference point, because the
sample decorrelates; while it stands still the residual is flat at the
noise floor. A smooth sample gives small residuals either way, so a
plain threshold would not separate them.
Against a synthetic scan built to run 1.85x too fast it recovers 1.81x
and locates the stop to within one point, and it stays silent on the
healthy control. The residual profile is printed so the shape can be
judged rather than trusted.
The deficit shows up on the integrated scattering too, which is a
different detector on a different gate, so the cause sits upstream of
both -- in the beam. The candidate is the shutter opening late against
the first gate of the burst.
That is falsifiable, but not through first/bulk, which scales with the
exposure time. A fixed shutter delay costs a fixed number of
milliseconds, so the same figure must come back from scans taken at
different exposure times. Report the deficit as (1 - first/bulk) *
exp_time and let the comparison across scans decide: constant ms means
timing, constant fraction means dose.
Scan 450 gives 6.0 ms out of 50. The prediction for scan 254 at 20 ms is
a ~30% first-point deficit, and for 473 at 100 ms a ~6% one.
Also adds the FIRST POINT LOW verdict, which the table could describe in
its numbers but had no label for.
Run against scan 450, the trace reported a flyback contaminating every
line. It is wrong, and the file says why: samy is recorded three times
per line, at 198.8163 / 198.8651 / 198.9238, and those are the same
three positions in line 0, line 20 and line 43. They sit in the bottom
47% of the range and run backwards. They are scan-loop checkpoints taken
after the burst and during the return -- the traverse itself is never
sampled. Describing the return move as if it were the line is the wrong
answer delivered confidently, which is worse than no answer.
Two guards. Per line, if the samples never advance along the commanded
direction, say so and name no mechanism. Globally, report what fraction
of the commanded travel the readback covers and how much of it runs the
right way, and stop before the span, stall and step-size numbers if the
answer is none -- those describe the loop, not the stage.
Repetition across lines is deliberately not the test. A healthy scan
repeats too, since every line does the same thing; an earlier version
tested for that and flagged all four synthetic controls.
Also rank the leading channel by range over counting noise rather than
range over level. On scan 450 the old score picked mca8, which sits at
2 counts and is pure Poisson, over mca6, which is the diode that formed
the image and carries the artefact.
Whole-line spans cannot tell the end-of-line faults apart. A stage that
runs too fast and parks, one that overshoots and settles back, and one
whose flyback starts before the last trigger all travel the same
distance per line. They differ only in the shape of the position trace
inside the line, and each implies a different fix.
--trace-line N dumps that shape: the per-point data on the scan grid
next to the position each point was meant for, then the position
readback with timestamps and the velocity between samples. The velocity
is the headline number -- scan 450 needs 0.1 mm/s on samy and appears to
have run closer to 0.18.
Progress is measured along the commanded direction rather than the
segment's own endpoints. Deriving it from the segment fails exactly
where it matters: when the return move falls inside the segment, first
and last sample coincide and a flyback reads as a healthy line.
Also extracts the line segmentation that Bug 2 had inline
(motor_candidates, slow_axis_breaks, rank_fast_traces) so both checks
delimit lines the same way, and adds read_request_inputs so the trace
measures the file against the command instead of a guess.
Asked where the scan command can be recovered reliably. It is in the master
file: the file writer stores the whole scan metadata under
/entry/collection/metadata/bec (file_writer.py:255), including request_inputs,
which holds the arguments the scan was actually called with -- arg_bundle,
inputs and kwargs.
That beats notebooks, logbooks and recollection, because it is written by the
component that ran the scan rather than by anyone describing it afterwards.
It also settles the open question directly: whether S00473 was 0.65 um or
6.5 um is answerable from the file, not from the email thread.
--command prints it and stops, request_inputs first.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
Two faults visible in the run against S00473 and S00254.
**Bug 1 reported nothing at all.** Candidates were restricted to the
`monitored` readout group, but the MCS channels -- which are the per-point scan
data, 203 x 428 = 86884 for S00473 -- are not in it. The filter excluded exactly
the arrays the check exists to examine, so both tables came out empty. The
restriction is gone; positions are still skipped, by ramp shape rather than by
group membership.
**Bug 2 could not find a line structure in S00473.** The monitored stream is
sampled while the line runs, so samples per line vary and the trace does not
reshape into a rectangle; requiring the sample count to divide evenly rejected
it outright. Segmentation now works from the boundaries themselves and tolerates
ragged lines.
That fix alone was not enough, and the reason matters. A line whose motion
failed produces no flyback, so it merges into its neighbour and the merged
segment shows a full, healthy span: **the stall erases the evidence of itself.**
Testing against ragged data with four stalls, the fast axis yielded 103 segments
for 107 lines and pronounced them all consistent.
So lines are now delimited by the **slow** axis, which steps once per line
whatever the fast axis did and stays a valid ruler across a stall. With that,
the same file gives exactly 107 segments and all four stalls, correctly reported
as clustering in the final tenth of the scan.
Where several positioner traces exist, the one with the largest travel per line
is taken as the fast axis and the runners-up are listed with their spans, so a
wrong pick is visible rather than silent.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first run against real files produced numbers that were entirely wrong,
in three separate ways. The metadata now reads correctly (num_lines,
frames_per_trigger), which is what exposed them.
**Wrong arrays.** These files hold two populations: the MCS channels, which
are the per-point scan data, and the monitored devices, sampled opportunistically
while a line runs. The monitored set has more signals, so "the length shared by
the most signals" landed on it -- and it is not on the scan grid at all. S00473
was analysed as 881x17 when the metadata says 203 x 428 = 86884, exactly the
MCS length. The grid length is now taken from the metadata when it matches an
array present in the file.
**Wrong axis.** The fast axis was chosen by how monotonically a signal ramps,
and picked `curr` -- the SLS ring current, which decays smoothly through any
window and beats the motor at its own metric. Positioners are now identified
structurally, by having a `<dev>_user_setpoint` sibling, which is what actually
makes a device a motor.
**Wrong assumption that both checks share one array.** They do not. Intensity
lives on the scan grid; motor position lives in the monitored stream at its own
rate and with its own line boundaries. The two checks now read their own data
and say so. This also surfaces a real limitation: cont_grid records roughly 8
position samples per line in S00254 against 205 data points, so a partial stall
can hide between samples. The script warns when sampling is that sparse rather
than reporting a confident negative.
**Truncated reporting hid the reported symptom.** The artefacts are said to
appear towards the end of scans, and the flagged-line list was cut after 25
entries with "... and 221 more" -- which is precisely where an end-of-scan
cluster would be lost. Flagged lines are now summarised by tenth of the scan,
with an explicit verdict on whether they cluster at the start, the end, or not
at all, and both ends of the list are printed. Bug 1 gains a matching
across-scan trend, since end-of-scan degradation is a different question from
end-of-line.
Verified against a file shaped like the real ones: MCS grid plus a sparse motor
stream, an end-of-line intensity drop, and stalls confined to the last tenth.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A --list on a real master file was unreadable: entry/collection/configuration
holds hundreds of scalar per-device settings, it sorts before the actual data,
and piping through head never reached anything useful.
Now arrays are listed largest first and scalars are counted rather than
printed, so the per-point data is visible immediately. Says so explicitly if a
file contains no arrays at all, since that would itself explain an empty
analysis.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous version still could not analyse the real files: inferring the line
geometry required num_lines / frames_per_trigger, and those keys are simply not
present in these master files. Better diagnostics did not help, because no
amount of reporting turns absent metadata into a geometry.
So stop depending on it. A scanned axis carries its own structure: a raster fast
axis ramps and snaps back, so line boundaries are the few steps far larger than
a typical one; a snake axis reverses instead, so they are where the step changes
sign. Both are tried, and the number of lines follows.
One subtlety decided the implementation. The first attempt required the detected
segments to be evenly sized, which failed on raster data -- because a line whose
motion never happened produces neither a flyback nor a reversal, so its boundary
is missing and its segment measures double. That is exactly the defect this
script looks for, so the check was rejecting the files that matter. It now takes
the *median* spacing between boundaries as the points-per-line, which survives
missing boundaries as long as most are present.
Verified against synthetic raster and snake files with no metadata whatsoever,
both carrying dead lines, at 205 x 107 -- the factorisation the real S00254
implies. Metadata is still preferred when present, and --lines/--points still
override; the output now says which of the three determined the geometry, so a
wrong guess is visible rather than silent.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
First run against real data (S00473, S00254) found the files but stopped at
"metadata: none of the expected keys found" and "could not work out the line
geometry". Two wrong assumptions, both now fixed.
**Metadata location.** It was looked up at two hard-coded paths. BEC nests this
differently across versions, so it is now found by searching the tree for the
keys by basename, attributes included. Verified against metadata one level
deeper than the original code expected.
**Which signal defines the geometry.** The point count was taken from the
*longest* signal. Real files hold signals of several lengths -- S00473 has
86884 points in something while the per-point arrays are shorter -- so a
monitor on its own clock hijacked the calculation. It now takes the length
shared by the most signals, since per-point data outnumbers everything else.
When the geometry still cannot be inferred, the script no longer just gives up
and asks for --list. It now prints every signal grouped by length, and the
factor pairs that could plausibly be (lines x points-per-line). That turns a
dead end into a readable diagnosis in one run rather than a round trip. If a
length does not factor usefully at all it says so explicitly, because a padded
or truncated per-point array is itself a finding.
Also wires up --lines/--points, which were declared but never reached the
analysis.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Read-only analysis of BEC master files, to test two user reports from the
15-20 July commissioning beamtime without needing beam.
**End-of-line intensity** (Andreas Apseros): reshapes each monitored signal
into lines and reports the last point and the first point against the interior
mean. It only calls it the reported bug when the *last* point is short and the
first is not -- if both ends are low that is a different mechanism, and the
script says so rather than confirming what we went looking for.
**Dropped line motion** (Kazu Hirosawa's reading, that the stage intermittently
stopped scanning in one direction): reshapes the fast-axis readback and reports
the travel per line. A collapsed span is motion that did not happen while the
scan still reported success -- the same shape as the silent-completion defects
already recorded against ccm_energy and the ECMC limit writes. It also reports
start/end scatter per line, since scatter at one end only would instead point
at the return move racing the next line.
Verified against synthetic files with both artefacts injected and a clean
control. That test caught a real flaw in the first version: the fast axis was
picked by within-line range, and a noisy diode channel out-ranged the axis and
was selected instead, breaking both checks. It now scores signals by how
monotonically they ramp within a line, which separates an axis from an
intensity regardless of scale.
Lives on a debug branch, not main: it is a diagnostic for one investigation,
not plugin code. bin/debug/README.md says what that means.
Caveat carried in both the docstring and the README: the motion check reads the
readback, so on an open-loop stepper without an encoder a healthy span proves
nothing. That is the same question as Ana Diaz's micro-stepping point and needs
settling before the null result means anything.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>