rotation: deterministic frame-order mosaicity smoothing + partiality recompute
Prediction applied a mosaicity/profile-radius moving average (RotationParameters) over the last N *processed* frames. Under the parallel per-image loop that window is thread-arrival order, so the smoothed value - and hence which reflections are predicted/integrated - was non-deterministic run-to-run, swinging CC1/2 (and even the space group) on marginal crystals. `-N 1` was deterministic; `-N 32` was not. Fix (as designed with FL): prediction now uses each frame's OWN mosaicity/profile-radius (image-local, deterministic membership - a reflection on the cutoff contributes ~nothing). The smoothing that actually matters is moved into RotationScaleMerge and done in FRAME order (deterministic): per-frame mosaicity is smoothed with the same window as smooth-G, then every partial's partiality is recomputed from it BEFORE the 3D combine. This is the mosaicity analogue of smooth-G: combining a reflection's per-frame partials only tiles the rocking curve correctly (captured fractions summing toward 1) if neighbouring frames share a consistent mosaicity. Battery (18 crystals, /data/rotation_test, 2 runs each): 15/18 now bit-identical run-to-run (the good crystals unchanged - lyso P41212 ISa 7.8 CC1/2 99.7%). The 3 residual crystals (EcwtAL500, EcwtCQ066S, pding4_003 - all large/triclinic cells) still jitter ~0.002%, traced to a SEPARATE, benign cause: the GPU prediction buffer overflow (BraggPredictionRotGPU max_reflections=10000 with a racy atomicAdd/atomicSub) on dense frames - cell/space group stay stable; to be addressed in the GPU prediction/integration rework (naively raising the cap also changes prediction quality, so it is not a one-line bump). Minor label refinements from the recomputed partiality: cytC_2 P321 -> P3121 (now consistent with cytC_3), Ins_I_2/3 report the honest I23/I213 screw-axis ambiguity. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -272,14 +272,14 @@ void IndexAndRefine::QuickPredictAndIntegrate(DataMessage &msg,
|
||||
|
||||
CrystalLattice latt = outcome.lattice_candidate.value();
|
||||
|
||||
if (rotation_indexer) {
|
||||
// Use moving average for mosaicity and profile_radius (also add beam center later)
|
||||
if (msg.mosaicity_deg)
|
||||
msg.mosaicity_deg = rotation_parameters.Mosaicity(msg.mosaicity_deg.value());
|
||||
if (msg.profile_radius) {
|
||||
msg.profile_radius = rotation_parameters.ProfileRadius(msg.profile_radius.value());
|
||||
}
|
||||
}
|
||||
// Prediction uses each frame's OWN mosaicity/profile_radius (image-local). We deliberately do NOT
|
||||
// smooth them here with a running moving average: it averaged the last N *processed* frames, whose
|
||||
// order under the parallel per-image loop is thread-arrival order, making the predicted rocking
|
||||
// width - and hence which reflections are integrated - non-deterministic run-to-run. Prediction only
|
||||
// decides membership (a reflection on the cutoff contributes ~nothing), so the per-frame value is
|
||||
// fine here. The mosaicity smoothing that actually matters - keeping the partialities of one rocking
|
||||
// event consistent so they tile the curve and sum toward 1 - is done deterministically in frame
|
||||
// order before the 3D combine (RotationScaleMerge), where partiality is recomputed from it.
|
||||
|
||||
float ewald_dist_cutoff = 0.001f;
|
||||
if (msg.profile_radius)
|
||||
|
||||
Reference in New Issue
Block a user