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

329 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.