Files
Jungfraujoch/image_analysis/pixel_refinement/METHODS.md
T
leonarski_fandClaude Opus 4.8 e6a50b45c7 Integration: mean background + global error model (trustworthy sigmas)
Background estimate: use the mean of the local ring, not the median. For a
right-skewed (Poisson) background the median sits below the mean, so subtracting
it under-subtracts and biases every weak intensity positive; over multiplicity
this becomes fake <I/sig> of a few in no-signal high-resolution shells. Fixed in
both PixelRefine and BraggIntegrate2D (the classical route had the same bug).
<I/sig> now tracks CC honestly and the true resolution limit is visible.

Error model: fit a global a, b (XDS form sigma'^2 = a*sigma^2 + (b*I)^2) from the
scatter of symmetry equivalents at the merge level (so both integrators benefit),
and print it with ISa = 1/b in jfjoch_process. The (b*I)^2 term uses the
reflection mean (not the per-observation I_i, which biases the weights and
collapses CC); a,b come from a relative-weighted bin regression. Replaces the
earlier per-resolution-shell variant, which was partly masking the background bias.

METHODS.md: document both (Sections 6-7), integrator-agnostic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 18:39:32 +02:00

14 KiB
Raw Blame History

PixelRefine — methods and improvements

This note documents the changes made to the still-image pixel-refinement integrator (PixelRefine) and, more importantly, why each one was needed. It is written from a methods point of view; the equations are the load-bearing part.

PixelRefine integrates Bragg reflections on a still image by fitting a per-pixel forward model against a known reference intensity set (e.g. F_calc from a deposited model). Unlike a rotation experiment, a still samples only a thin slice of each reflection, so the integrator must (a) model the partiality of that slice, (b) refine the per-image geometry well enough that the high-resolution shoeboxes land on signal, and (c) scale each image onto the reference. Each of those three is where the original code went wrong.

Throughout, a reflection's shoebox is a small box of raw detector pixels I_p with a local flat background B; the (area-normalised) model spot profile at pixel p is P_p, and v_p is the variance used to weight pixel p.


0. The forward model

The recorded amplitude of a still reflection is estimated by profile fitting:


J \;=\; \frac{\sum_p w_p\,P_p\,(I_p - B)}{\sum_p w_p\,P_p^{2}},
\qquad w_p = \frac{1}{v_p},
\qquad \operatorname{var}(J) = \frac{1}{\sum_p P_p^{2}/v_p}.

The full (rotation-equivalent) intensity is recovered by dividing out the factors a still does not record,


I \;=\; \frac{J}{p\,B_\mathrm{DW}\,\mathrm{pol}},\qquad
p = \exp\!\left(-\frac{\epsilon_r^{2}}{R_0^{2}}\right),\quad
B_\mathrm{DW}=\exp\!\left(-\frac{B_\mathrm{fac}}{4 d^{2}}\right),

where the partiality p is the fraction of the mosaic block crossing the Ewald sphere, \epsilon_r = 1/\lambda - |S_{hkl}| is the excitation error, R_0 the radial (rocking) width, and \mathrm{pol} the polarisation correction. The per-image model intensity for a pixel is


I_p^\mathrm{model} = G\,I^\mathrm{ref}\,B_\mathrm{DW}\,p\,P^\mathrm{tang}_p\,\mathrm{pol} + B,

with G the per-image scale. The per-image least squares minimises \chi^2 = \sum_p w_p\,(I_p^\mathrm{model}-I_p)^2 over geometry, orientation, G, R.


1. De-biased variance (the load-bearing fix)

Symptom. Mean intensities went negative in the high-resolution shells (\langle I/\sigma\rangle down to -12), which a box-sum integrator never does. The per-image scale G also collapsed to 0 on most images, dropping ~80 % of observations.

Cause. Both the extraction and the fit weighted each pixel by its observed count, v_p = I_p. For a background pixel that fluctuated down (I_p < B), v_p is small, so w_p = 1/v_p is large, and its contribution P_p (I_p-B)/v_p < 0 is large in magnitude. Summed over the many (mostly empty) shoebox pixels, this drags J below zero — the classic inverse-observed-count (Poisson-on-data) bias. It bites hardest where the true signal is weakest, i.e. at high resolution. In the fit it manifests differently but identically in origin: the weighted empty pixels make "no signal" (G=0) the cheapest solution, so G\to 0.

Fix. For background-limited (weak) reflections the correct variance is the local background, constant over the shoebox:


v_p = \max(B,\,1)\quad\Longrightarrow\quad
J = \frac{\sum_p P_p\,(I_p-B)}{\sum_p P_p^{2}},

the unbiased uniform-variance estimator. This single change turned \langle I/\sigma\rangle positive at all resolutions and stopped the scale collapse. It is applied to both the extraction weight and the fit weight.


2. Prediction band and multiplicity

Symptom. PixelRefine recorded ~4× fewer observations per unique reflection than the classical integrator — completeness was fine, redundancy was not.

Cause. A reflection is given a shoebox only when it lies within a radial band of the Ewald sphere,


\bigl|\,|S_{hkl}| - 1/\lambda\,\bigr| \le \delta .

For randomly oriented stills the number of images on which a given hkl satisfies this is \propto \delta. The default \delta = 5\times10^{-4}\,\text{Å}^{-1} was 46× tighter than the classical integrator's \delta = 2\text{}3\times r_\mathrm{profile}, so each reflection was recorded on 46× fewer images.

Fix. Widen to \delta = 2\times10^{-3}\,\text{Å}^{-1}. Multiplicity rose from ~240 k to ~950 k observations and CC$_\mathrm{ref}$ from 49.7 % to 55.9 %. Widening is only safe once the fit is well-behaved (Sections 1, 3, 4); with the original unconstrained fit it caused divergence.


3. Regularising the per-image fit

Symptom. Freeing any per-image parameter (orientation, R, even the scalar scale G) collapsed the merged data (CC$_{1/2}$ from 90 % to a few %). The predict↔refine loop with all parameters frozen was, by contrast, byte-identical to extraction-only — proving the fit, not the loop, was at fault.

Cause. The per-image problem regularised nothing: orientation had no prior, R and G only a lower bound. Three orientation DOF (plus R, G) against a handful of signal pixels per still overfit the noise, and an unconstrained 1/G then scrambled the cross-image merge.

Fix. Anchor each refined parameter to its prior with a data-scaled weight, as ScaleOnTheFly already does for rotation data. The data term has one residual per pixel, so the prior weight must scale with the pixel count:


w_\theta = \sqrt{\frac{N_\mathrm{pix}}{\sigma_\theta^{2}}}\,,
\qquad \chi^2_\mathrm{reg} = w_\theta^{2}\,(\theta-\theta_0)^2 .

Using \sqrt{N_\mathrm{refl}} instead (as in the rotation scaler) is a factor \sqrt{N_\mathrm{pix}/N_\mathrm{refl}}\approx\sqrt{49}\approx 7 too weak and is simply not felt. Applied to:

  • Scale: \theta=G,\ \theta_0=1. Prevents 1/G from wandering; restored CC$_{1/2}$ from 4 % back to 87 %.
  • Orientation: \theta = Rodrigues vector, \theta_0 = spot-centroid orientation, \sigma_\theta in radians. At \sigma_\theta\!\sim\!1^\circ this gives the best CC$_\mathrm{ref}$; beyond \sim 2^\circ the fit overfits and the merge collapses, so \sigma_\theta is the safety knob.

4. Signal-weighting the fit

Cause. Even de-biased, a shoebox is ~80 % empty pixels (≈ 40 of 49 for a radius-3 box). They carry no information on G, R or orientation but add noise and, near the overfitting edge, destabilise the fit.

Fix. Multiply the fit weight by a detector-space Gaussian centred on the predicted spot,


w_p \;\to\; w_p \cdot \exp\!\left(-\frac{r_p^{2}}{2\sigma_s^{2}}\right),
\qquad r_p = \lVert (x_p,y_p) - (x_\mathrm{pred},y_\mathrm{pred})\rVert,

so the signal-bearing core drives the refined parameters (\sigma_s\approx 1.5 px). With the regularised orientation this lifted CC$_\mathrm{ref}$ from 60.5 % to 62.6 %.


5. Global orientation + cell-scale sweep

Motivation. The classical refinement (XtalOptimizer) works on spot centroids from spot-finding, which are poor value at high resolution; the local LSQ above is a heavy gradient step that cannot make the ~degree-scale global moves needed to pull a mis-indexed crystal's high-resolution reflections onto their shoeboxes. A small, global sweep that simply asks "at which orientation does the most high-resolution signal appear where the reference says it should?" is structurally better suited.

Score. Pearson CC of the box-summed intensities against the reference, over all matched reflections (not just the strong ones):


\mathrm{CC}_\mathrm{ref}(L) = \operatorname{corr}_{hkl}\bigl(\,I^\mathrm{box}_{hkl}(L),\ I^\mathrm{ref}_{hkl}\,\bigr).

The strong low-resolution reflections anchor the CC (moving them off their boxes collapses it), so the change in CC across the sweep is driven almost entirely by weak high-resolution reflections falling onto — or off — real signal. This is the "appearing out of the void" behaviour.

Geometry-derived bounds (parameter-free). A spot at resolution d sits at detector radius


r(d) = \frac{L}{p}\,\frac{\lambda}{d}\quad[\text{px}],

(L = detector distance, p = pixel size). Both a crystal rotation \delta\theta and a fractional cell-scale \epsilon displace that spot by an amount proportional to its radius:


\Delta_\mathrm{px} = r(d)\,\delta\theta \quad(\text{rotation}),\qquad
\Delta_\mathrm{px} = r(d)\,\epsilon \quad(\text{cell scale}).

So high-resolution spots (large r) move most. The two natural constraints fix the grid:

  • Step = 1 px at the highest resolution: \;\delta\theta_\mathrm{step} = 1/r_\mathrm{max} (finer is below the detector's resolving power).
  • Range = the orientation uncertainty \Delta\theta_u (a few px at high res). The number of steps per axis is then

n = \frac{\Delta\theta_u}{\delta\theta_\mathrm{step}} = \Delta\theta_u\, r_\mathrm{max},

a handful of steps, and the lowest-resolution spots move only n\,\delta\theta_\mathrm{step}\,r_\mathrm{min} = \Delta\theta_u\, r_\mathrm{min} \ll 2 px — i.e. the strong anchors stay put by construction.

Lesson learned. Setting the range from "2 px at low resolution" instead gives n = 2\,r_\mathrm{max}/r_\mathrm{min} = 2\,d_\mathrm{low}/d_\mathrm{high} steps — tens of pixels of high-resolution freedom — which lets the per-image CC overfit and degrades the merge. The range must be tied to the (small) orientation uncertainty, not the low-resolution cap.

The sweep is a coordinate descent over the three Rodrigues axes and the cell scale, run before the LSQ. It improves the high-resolution shells (CC$\mathrm{ref}$ +1 to +5 per shell) while preserving CC${1/2}\approx 90$ %. It is an alternative to the loose-\sigma orientation LSQ, not a complement — stacking the two double-moves the orientation and overfits. It is therefore available but off by default.


6. Background-estimator bias (the largest σ error; both integrators)

Symptom. Pushed past the true resolution limit (e.g. a 1.8 Å crystal merged to 1.3 Å), the no-signal shells still reported \langle I/\sigma\rangle \approx 4\text{}6 with \mathrm{CC}_{1/2}\approx 0 — i.e. confident "data" where there is none. Present in both PixelRefine and the classical BraggIntegrate2D.

Cause. Both estimated the local background as the median of the surrounding ring. For a Poisson / right-skewed background the median sits below the mean,


\operatorname{median}(B) < \mathbb{E}[B],

so subtracting it under-subtracts on every pixel. The leftover positive offset is tiny per pixel but coherent, so over an $n_\mathrm{pix}$-pixel peak and a multiplicity-m merge it grows to a fake signal


\langle I\rangle_\mathrm{bias} \;\approx\; n_\mathrm{pix}\,\bigl(\mathbb{E}[B]-\operatorname{median}(B)\bigr),
\qquad
\Bigl(\tfrac{I}{\sigma}\Bigr)_\mathrm{merged,\,bias} \propto \sqrt{m}.

It is worst where the real signal is weakest (high resolution), because there the offset is all that remains — which is exactly the observed signature. Nothing leaks into the high-resolution shells; the background is simply under-estimated.

Fix. Use the mean of the ring (outliers excluded by the existing spot-core mask and saturation sentinels). \langle I/\sigma\rangle then collapses to ~0 wherever \mathrm{CC}\approx0, tracks CC down the shells, and the honest resolution limit becomes visible. This was the single largest contributor to untrustworthy σ — a one-line change in each integrator, not a variance-model problem.


7. Error model (global a, b; XDS form)

Counting statistics under-estimate the variance of strong reflections, which carry systematic errors (scaling, partiality, detector) proportional to intensity, not to \sqrt{I}. After merging, this leaves CC$_{1/2}$ and \langle I/\sigma\rangle inconsistent. The standard correction (XDS / DIALS / AIMLESS) inflates the variance with a global two-parameter model:


\sigma'^{\,2} \;=\; a\,\sigma^{2} + \bigl(b\,\langle I\rangle\bigr)^{2},
\qquad
\mathrm{ISa} \;=\; \frac{1}{b}\;=\;\lim_{I\to\infty}\frac{I}{\sigma'} .

Two points matter for an unbiased fit:

  • The I^2 term uses the reflection mean \langle I\rangle (constant over its observations), not the per-observation I_i. Using I_i gives a down-fluctuated point a small \sigma' and hence a large 1/\sigma'^2 weight, biasing the merged mean — which collapses CC. (This was the decisive bug in the first attempt.)
  • a and b are fit from the spread of symmetry equivalents: for an observation in a group of n, \mathbb{E}[(I_i-\langle I\rangle)^2] = \sigma_i^2(1-h_i) with leverage h_i = w_i/\sum w. Binning by intensity and regressing the bin medians of (I_i-\langle I\rangle)^2/(1-h_i) on (\sigma^2, \langle I\rangle^2)weighted by 1/\mathrm{dev}^4 so the fit is relative — gives (a, b^2); the relative weight stops the strong bins (which fix b) from swamping the weak bins (which fix a).

It is applied at the merge level (MergeOnTheFly), so both integrators benefit, and jfjoch_process prints the model and ISa. Earlier per-resolution-shell variants were dropped: the standard tools use a single global a, b, and the per-shell version was partly masking the background bias of Section 6.


Results (lysozyme jet, 1.8 Å, identical input)

Configuration N_obs Compl. CC$_{1/2}$ CC$_\mathrm{ref}$
Classical integrator (box-sum) 421 k 100 % 81.4 % 60.9 %
PixelRefine — original 61 k 95 % 0.0 % 0.4 %
PixelRefine — consolidated (this work) 951 k 100 % 80.0 % 62.6 %

The consolidated integrator beats the classical one on the accuracy metric (CC$\mathrm{ref}$) with > 2× the multiplicity, and turns a previously unusable result (CC${1/2}=0$) into a competitive one.


Default recipe

Sections 15 are PixelRefine-specific; Sections 67 act at the integration/merge level and apply to the classical route too.

Field / behaviour Default Section
fit/extraction variance local background B 1
ewald_dist_cutoff 2\times10^{-3}\,\text{Å}^{-1} 2
scale_reg_sigma 2.0 3
orient_reg_sigma_deg 1.0 3
refine_R false 3
fit_signal_sigma_pix 1.5 4
sweep_orientation false (available) 5
local background estimator mean of the ring 6
merge error model global a,b (ISa printed) 7

orient_reg_sigma_deg (accuracy vs. precision) and ewald_dist_cutoff (multiplicity vs. cost) are the two knobs worth tuning per dataset.