Stop making the first pass write the files the second pass replaces

Pass 1 exists to choose the space group and post-refine the geometry. Its merged intensities are
discarded - pass 2 remakes them seconds later at the refined geometry, and that is the answer anyone
reads. It was nonetheless writing the full set of merged files at the end of every pass 1: a mmCIF
of every unique reflection (22 MB on an ordinary crystal, 48 MB on a crowded one), an .hkl, an .mtz
and the per-image scaling table, all through one thread.

Measured on an ordinary rotation set: 0.60 s of a 15 s run, and pass 2's identical block right
after it takes another 0.585 s to write the files that are kept.

The pass-2 quality guard is untouched, which is what disqualified an earlier attempt at this:
has_merge_statistics is set at the merge, well above the write, so pass 1 still reports the
completeness and CC1/2 the guard compares against. Nothing numeric moves - the same run measures
38.5 s before and 36.6 s after with a byte-identical .hkl.

Also hoist the pixel-mask accessor out of the preprocessor's per-pixel loop. It called .at() on
every pixel of the detector - 18 million bounds checks per engine, and an engine is built per worker
per pass - for a bound the loop already respects, which stopped it vectorising.

The <prefix>_01.mtz/.cif/.hkl are documented output, so this is a deliberate behaviour change: the
pre-pass result is no longer written. If it is wanted for comparison it should come back behind a
flag rather than by default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011n8riB6X59oRjkrSHzNPAU
This commit is contained in:
jungfrau
2026-08-23 11:52:37 -04:00
co-authored by Claude Opus 5
parent 16639e9de2
commit c9fc46e6e2
2 changed files with 13 additions and 3 deletions
@@ -237,9 +237,14 @@ ImagePreprocessorGPU::ImagePreprocessorGPU(const DiffractionExperiment &experime
cpu_stats_reg(cpu_stats) {
// Setup mask. The same for every worker, so it is uploaded once per GPU and shared; keyed on the
// PixelMask's own vector, which the derived table is a pure function of.
// Hoist the accessor and index without the bounds check: this runs once per worker over every
// pixel of the detector - 18 million times per engine on a 16 Mpx one, and an engine is built per
// worker per pass - and .at() on each of them stops the loop vectorising for a bound the loop
// itself already respects.
const std::vector<uint32_t> &mask_raw = mask.GetMask();
std::vector<uint8_t> mask_vec(npixels);
for (int i = 0; i < npixels; i++)
mask_vec[i] = (mask.GetMask().at(i) != 0);
mask_vec[i] = (mask_raw[i] != 0);
gpu_mask = SharedDeviceTable(mask.GetMask().data(), npixels, mask_vec.data(), *stream);
// Setup GPU settings. The current device, not device 0: workers are pinned round-robin across GPUs,