Scaling: take the detector modulation surface to a 24x24 grid

The per-detector-plane modulation surface was learned on a 16x16 grid. Fitting the same surface to
rugnux's own symmetry mates and cross-validating on held-out frames shows the grid was the binding
constraint, not the data: held-out R_meas improves monotonically to 24x24 and then stops -
none 12.64%, 8x8 11.13%, 16x16 10.71%, 24x24 10.58%, 32x32 10.60%, 48x48 10.58%, 64x64 10.60%.
At d > 4.4 A the same ladder reads 5.29 / 4.92 / 4.86 / 4.66 / 4.72 / 4.64 / 4.74%. Frame-parity,
random 50/50 and 4-fold splits agree.

The structure being fitted is ours, not a reference program's: the same surface fitted to the other
program's observations of the SAME events moves it 7.10 -> 7.07%, against 13.46 -> 12.81% for ours,
and its amplitude is 6.5% robust sd against 1.2%.

Measured across seven crystals spanning multiplicity 3.7-9.4, two detector types and 75-100%
completeness, 24 never clearly hurts and mildly helps six of them; 32 adds nothing beyond it. An
earlier in-sample ladder suggested 48x48 was worth twice as much - that was in-sample, and it
overstated the gain about threefold.

Nothing else needs adjusting: the Tikhonov shrinkage already adapts to thinly-populated cells, and the
cross-validation gate already refuses the surface outright where the finer grid is too fine for the
data - on the weakest crystal tested its held-out gain falls 4.3% -> 3.1% -> 1.7% and the surface is
skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-10 04:18:44 +02:00
co-authored by Claude Opus 5
parent 7523c67655
commit f766a342bb
@@ -1192,7 +1192,10 @@ void RotationScaleMerge::RefineModulation(int n_iter, int n_groups) {
}
if (!(pxmax > pxmin) || !(pymax > pymin))
return;
constexpr int NB = 16;
// 24 rather than 16: held out on unseen frames, 16 -> 24 is worth 0.1-0.2 pp of merged R_meas,
// while 24 -> 48 buys 0.02 and 64 is worse. The cross-validation gate below refuses the surface
// outright where 24 is too fine for the data, so the finer grid needs no separate guard.
constexpr int NB = 24;
const float sx = NB / (pxmax - pxmin), sy = NB / (pymax - pymin);
std::vector<int32_t> cell(fulls.size(), -1);
for (size_t i = 0; i < fulls.size(); ++i) {