Unmerged export: carry the sort key with the part here too

SumRockingEvents assembles rocking events for the unmerged MTZ through
the same indirect comparator the anisotropy diagnostic had, on a larger
array - it has neither the scaled-image test nor the usable() filter in
front of it. It is latent, since --export-unmerged is off by default, but
it is the same defect and it is a mechanical fix.

Same treatment: the key travels with the part, plus an exact reserve.
p_unmerged.mtz, p_unmerged_partials.mtz and p.hkl are md5-identical
across eight runs on two crystals. With the export on it is worth about
0.9 s on a seventeen-million-partial run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CHMmeM1d489zvNFT7ZMN2P
This commit is contained in:
2026-08-26 14:11:56 +02:00
co-authored by Claude Opus 5
parent 0e6ece3c38
commit abb61e2708
+24 -10
View File
@@ -617,28 +617,42 @@ std::vector<Reflection> SumRockingEvents(const std::vector<IntegrationOutcome> &
double min_partiality) {
constexpr float MAX_FRAME_GAP = 2.0f; // == RotationScaleMerge's: what makes one rocking event
std::vector<const Reflection *> parts;
// The sort key travels with the part instead of being read back through the pointer, the way the
// merge's own ingest sort carries it (RotationScaleMerge's SortKey): there are millions of parts
// and an indirect compare is a cache miss on every one of them. The keys are the same values in
// the same order, so introsort makes the same comparisons and the same swaps and leaves the same
// order - which matters, because two parts can genuinely share (h,k,l) and image_number and the
// event sums below are floating point.
struct Part {
int32_t h, k, l;
float image_number;
const Reflection *r;
};
std::vector<Part> parts;
size_t n_parts = 0;
for (const auto &outcome : outcomes)
n_parts += outcome.reflections.size();
parts.reserve(n_parts);
for (const auto &outcome : outcomes)
for (const auto &r : outcome.reflections)
parts.push_back(&r);
std::sort(parts.begin(), parts.end(), [](const Reflection *a, const Reflection *b) {
return std::tie(a->h, a->k, a->l, a->image_number)
< std::tie(b->h, b->k, b->l, b->image_number);
parts.push_back({r.h, r.k, r.l, r.image_number, &r});
std::sort(parts.begin(), parts.end(), [](const Part &a, const Part &b) {
return std::tie(a.h, a.k, a.l, a.image_number) < std::tie(b.h, b.k, b.l, b.image_number);
});
std::vector<Reflection> fulls;
for (size_t i = 0; i < parts.size(); ) {
size_t j = i + 1;
while (j < parts.size() && parts[j]->h == parts[i]->h && parts[j]->k == parts[i]->k
&& parts[j]->l == parts[i]->l
&& parts[j]->image_number - parts[j - 1]->image_number <= MAX_FRAME_GAP)
while (j < parts.size() && parts[j].h == parts[i].h && parts[j].k == parts[i].k
&& parts[j].l == parts[i].l
&& parts[j].image_number - parts[j - 1].image_number <= MAX_FRAME_GAP)
++j;
double sum_p = 0.0, sum_I = 0.0, sum_var = 0.0, sum_var_bkg = 0.0;
double p_rlp = 0.0, p_frame = 0.0, p_x = 0.0, p_y = 0.0, p_delta_phi = 0.0, p_zeta = 0.0,
p_bkg = 0.0;
for (size_t m = i; m < j; ++m) {
const Reflection &r = *parts[m];
const Reflection &r = *parts[m].r;
const double p = r.partiality;
sum_p += p;
sum_I += static_cast<double>(r.I) * r.rlp;
@@ -652,7 +666,7 @@ std::vector<Reflection> SumRockingEvents(const std::vector<IntegrationOutcome> &
p_zeta += p * r.zeta;
p_bkg += p * r.bkg;
}
Reflection full = *parts[i];
Reflection full = *parts[i].r;
i = j;
if (sum_p < min_partiality)
continue;