Commit Graph
3 Commits
Author SHA1 Message Date
leonarski_fandClaude Opus 5 c84b91be8a calibration: take the beam centre from the rings too
The header's beam centre was the last input the ring fit had to be roughly
right about. Each ring is looked for in a window a few pixels of radius wide,
and a centre wrong by (dx, dy) puts a ring at a different q in every sector, so
past about ten pixels the ring leaves that window over much of the turn - and
the fit then reads its cos(phi) signal off whichever sectors are left, which are
the ones where the signal is weakest. A 20 px error ended 31 px wrong.

The rings answer this without a calibrant and without a distance. A powder ring
is a conic centred on the beam, so a wrong centre makes EVERY ring's radius
oscillate once per turn by the same amount: r(phi) = R + dx cos(phi) + dy
sin(phi), solved directly and pooled over every ring the profile shows, with
each ring searched about its own measured radius rather than about where a
standard says it should be.

Using it needs the extraction to follow the rings sector by sector, which is
what ProfileRingTrack now does - exactly, and in all five parameters at once,
by walking the ring in the geometry believed true and asking the binned geometry
what q and azimuth it would have given each point. That replaces the
flat-detector distance correction it grew out of.

Following the rings is not free, and the reason is worth stating: a window that
moves with phi makes every systematic of the peak finder - where the background
line is taken, how the centroid sits in the window - vary with phi as well, and
phi is exactly the axis the beam centre is read off. Measured, it costs rms
0.415 -> 0.525 px on a good 110 mm fit, and 0.831 when the window follows the
fitted tilt too. So a second measurement is taken with a window that is the same
in every sector - the binned geometry with only its DISTANCE replaced, which is
phi-independent by construction - and both are offered to the same rule that
ranks everything else here. Acquire by following, measure by holding still.

The seeded centre is likewise a hypothesis and not a belief. It reads a
once-per-turn wobble, and a tilt puts a term of that shape there too - one that
grows as the radius squared, where a centre error does not - so pooling the
rings absorbs part of the tilt into the centre. Believed outright it made a good
110 mm fit worse; offered as an alternative start it costs one more fit and
needs no rule about when it applies. It is skipped entirely below a pixel, where
it is not a different hypothesis at all, which keeps a well-headed run at 0.71 s.

Measured on the 110 mm LaB6 exposure, whose true PONI is 765.90: a header centre
20 px out now lands within 0.5 px, where before it landed 31 px away. All five
datasets are unchanged from their correct headers, and the distance still
recovers from any header between 25 and 1200 mm.

The limit is now understood rather than merely reached. Past a few pixels the
azimuthally averaged profile stops showing rings: a ring tracing r(phi) piles up
density where that turns round, so it averages into the two HORNS of the
sinusoid, at R-|d| and R+|d|. The radius finder reports two rings where there is
one, and the gap between them is 2|d| - the search window shrinks to exactly the
offset it was meant to span. That caps recovery at roughly half the ring
spacing, about 20 px here and failing by 40. Beyond it nothing is left in an
azimuthally binned profile, and --calibration spots, which works from the spot
positions themselves, is the method that still can.

One pre-existing limit measured and NOT introduced here: a wrong distance
together with a centre more than about 5 px out fails, because the centre error
splits the radius list the distance search reads. The committed code before this
change fails identically on those cases.

Also fixed: fit_from now takes a whole geometry rather than a distance, and the
declined-tilt refit was inheriting rot1/rot2 from it - pinning the tilt at
exactly the unvalidated value the gate had just rejected. Same fault the gate
exists to catch, one level up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NfuDvf5ipV3Hi8TiCUKD27
2026-08-31 17:38:36 +02:00
leonarski_fandClaude Opus 5 01ad1e1743 calibration: only report a tilt the fit actually measured
The detector tilt is refined by default, and on a pattern that cannot separate
it from the beam centre the fit returns one anyway - there was nothing to stop
it. Both displace a ring's radius as cos(phi), and only how that amplitude grows
with the ring's radius tells them apart, which takes two well-sampled rings. At
500 mm on the LaB6 series only two rings reach the detector and the outer one is
barely there: the tilt came out at the opposite sign to every shorter distance,
dragged the PONI 28 px, and bought a residual of 0.960 px against 0.962 pinned.
The covariance says so plainly - 0.1 sigma, and a beam centre quoted to +-180 px.

So ask it. A tilt is kept only where the fit had it free AND it stands at least
three times its own uncertainty; otherwise rot1/rot2 go back to the header's
values and the beam centre and distance are refitted around them. Over the
series the tilt stands at 50, 33, 15 and 8 sigma at 110 to 300 mm and 0.1 at
500 mm, so any threshold between 2 and 5 gives the same verdict on all five -
this says which regime a fit is in, not where a line was drawn. The declined
500 mm fit lands on a direct beam of 773.53 px, against 773.56 for the pinned
fit measured independently.

It is a rejection criterion and nothing more. Clearing it does not certify a
tilt: that estimator is limited by systematics rather than by this sigma, and a
coherent half-pixel error in the ring positions fakes a tilt of the usual size
while leaving sigma small. The report says "refined", never "verified".

Writing the gate turned up a related fault in the pass loop. RingOptimizer pins
the tilt by itself when every point it is given lies on one ring, and on a
barely-sampled pattern a later pass lands in exactly that state - which froze
the tilt at whatever the FIRST pass had produced and returned it with sigma
zero, an unmeasured tilt wearing the appearance of a fixed one. The gate reads
that as "not measured" and refits pinned, which is why it is stated over the
geometry that gets reported rather than over what the last fit happened to do.
Both paths are covered, profile and spots; the spots path was reporting a
refined tilt as declined for the same reason.

Two things measured and NOT taken:

A robust loss. A Cauchy loss scaled to the previous pass's median residual
changed nothing on the series - rms 0.415 to 0.421 at 110 mm, no case improved,
every direct beam within 0.06 px. Ring points are per-sector peaks that already
had to stand 3 sigma clear of their own background, so there are no gross
outliers left to reject. Recorded at the call site rather than left as an unused
option.

A quality gate that refuses a bad calibration. Three candidate signals, all
measured against naming the wrong standard on LaB6 data: sigma(PONI) does not
see it at all (0.52-0.65 px, indistinguishable from healthy); the residual only
half sees it (3.2-3.6 px wrong against 0.4-1.0 right, but a correct run from a
wrong header sits at 1.0-2.4 and would be caught too); and the seed's match
score is dominated by how many rings the calibrant lists, scoring 0.29 for a
perfect LaB6 fit against 0.21 for a wrongly named silicon. None of the three
separates, so no gate is shipped. What the run does say is the recovered
distance against the header, and a wrong standard moves that to 446 mm on a
110 mm exposure - unmissable, and the operator's call rather than a threshold's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NfuDvf5ipV3Hi8TiCUKD27
2026-08-31 17:07:37 +02:00
leonarski_fandClaude Opus 5 a5f416fcdc calibration: take the detector distance from the rings, not from the header
A powder calibration is run because nobody is sure the header is right, and the
header's distance was the one number the fit could not survive being wrong
about. The ring search is local - each ring is looked for inside a window a few
pixels of radius wide - so a distance more than a percent or two out puts every
ring outside its own window, and the fit then converges on whatever background
fluctuation each window contains. It does not fail: a 110 mm exposure told the
detector was at 150 mm reported 149.8 mm, with 146 ring points and exit 0. Only
its residual said anything, 5.3 px against 0.4 px, and nothing read it.

Measure the distance from the rings instead. The peaks of the azimuthally
averaged profile give ring RADII, and a radius does not depend on the assumed
distance at all - bin i holds the pixels at one particular radius whatever q
that radius was called - so the radii are a property of the image. Against the
calibrant's d-spacings, r = D tan(2 asin(lambda/2d)) then has one unknown. It is
scanned rather than solved because the pairing of observed rings to d-spacings
is unknown too, and the winning basin is solved in closed form. Nothing here
reads the header distance except to bin the profile; it needs only the
wavelength, the pixel size and the detector's extent.

A powder pattern has genuine distance aliases, so one answer is not enough. A
cubic primitive standard puts its rings at radii proportional to sqrt(N), and
scaling the distance by sqrt(2) maps ring N onto ring 2N - most of the comb
still lands on peaks. Measured: the 110 mm exposure with a 115 mm header scored
its best at 156.5 mm, which is 110*sqrt(2). No adjustment of the score removes an
alias the lattice really has, so the scan hands back the few best distances and
each is fitted, the header among them as one hypothesis of several. The residual
then separates them - 0.4 px against 5.2 px on that case - subject to an attempt
explaining a comparable share of the pattern first, because a start so wrong
that one ring point survives leaves a residual of exactly zero.

Each attempt re-extracts at the geometry it converged to and fits again. The
seed is measured from blended peaks and is good to about a per cent, close
enough to converge from but far enough to sit every search window a few pixels
off its ring, and an off-centre window takes its background off the ring's own
flank. Nothing is re-read from disk, so the loop is free.

Measured on the LaB6 distance series. A 110 mm dataset now recovers 110.03-110.17
mm from any header between 25 and 1200 mm, against +-2 mm before. All five
datasets recover their own distance from a fixed wrong 250 mm header. With
correct headers, four of the five are bit-identical to before and the 500 mm one
moves by a single ring point - the two-ring fit whose tilt is 0.1 sigma anyway.
Run time is unchanged at 0.62 s.

The residual is larger on a run whose header was wrong (1.1 px against 0.4 px on
the 110 mm case), because the profile was still binned at the wrong distance and
its radial sampling is correspondingly coarse. The geometry is right; only the
scatter about it is inflated. Re-running with the recovered distance recovers
the residual too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NfuDvf5ipV3Hi8TiCUKD27
2026-08-31 16:48:32 +02:00