IndexAndRefine: unify cell setting across indexers, keep FFBIDX neutral

Follow-up to 40da1aab. Conventionalise the indexed lattice for BOTH indexers when
cell+SG are given, so every frame reports its cell in ONE consistent setting per
space group (mixed axis orders like [78,78,38] vs [38,78,78] index the same
reflection as different HKLs and cannot be merged).

Use LatticeSearch's conventional cell when its detected symmetry agrees with the
user's space group. On noisy frames LatticeSearch can pick an alternative Bravais
setting (e.g. the sqrt2 C-centred description of a primitive tetragonal cell,
[78,78,38]->[110,111,38]) whose system disagrees; there:
  - FFBIDX already returns the reference setting (c-last), consistent with the
    conventional frames, so its raw lattice is safe -> use it. This recovers the
    ~3% of frames the unconditional version lost: FFBIDX is byte-for-byte neutral.
  - de-novo (FFT/FFTW) return a Niggli-primitive cell with a DIFFERENT axis order
    (c-first); merging it with the conventional (c-last) frames would corrupt the
    data -> reject the frame instead.

Validation (lyso): FFBIDX c2 71.44% / jet 27.15% (both unchanged vs raw-lattice
baseline, CC1/2 94.6 / 91.9); FFT c2 +C+S 92.72% (CC1/2 95.8, all c-last,
consistent); FFT c2 de-novo 95.44% (unchanged). Verified per-frame that the c-axis
(~38A) is axis 2 in every merged frame - no axis-order mixing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 22:50:56 +02:00
co-authored by Claude Opus 4.8
parent 40da1aab13
commit e1e2ca8e49
+21 -10
View File
@@ -108,20 +108,31 @@ IndexAndRefine::IndexingOutcome IndexAndRefine::DetermineLatticeAndSymmetry(Data
// If space group and cell provided => enforce that symmetry in refinement.
// If not => detect the symmetry from the lattice.
if (sg && experiment.GetUnitCell()) {
// FFBIDX returns the lattice already in the reference setting, so use it
// as-is. De-novo indexers (FFT/FFTW) return an arbitrarily-oriented primitive
// cell that must first be reduced to the conventional setting, else enforcing
// the crystal system on mis-assigned axes rejects every frame. Centering is
// taken from the user's space group; niggli_class is left unassigned (0) - it
// is a property of the primitive cell incl. centering, which LatticeSearch
// cannot recover from a cell the user may have given centered (e.g. C2). A
// proper primitive-cell indexing path (CrystFEL-style) is deferred.
outcome.symmetry = LatticeMessage{
.centering = sg->centring_type(),
.niggli_class = 0,
.crystal_system = sg->crystal_system()
};
outcome.lattice_candidate = de_novo ? LatticeSearch(latt).conventional : latt;
// Place every frame's cell in ONE consistent setting for the whole dataset:
// mixed axis orders (e.g. [78,78,38] vs [38,78,78]) index the same reflection
// as different HKLs and cannot be merged. LatticeSearch gives the conventional
// setting when its detected symmetry agrees with the user's space group. On
// noisy frames it can instead pick an alternative Bravais setting (e.g. the
// sqrt2 C-centred description of a primitive tetragonal cell,
// [78,78,38]->[110,111,38]); there:
// - FFBIDX already returns the reference setting (c-last), consistent with the
// conventional frames, so its raw lattice is safe -> use it (FFBIDX neutral);
// - de-novo indexers (FFT/FFTW) return a Niggli-primitive cell with a DIFFERENT
// axis order (c-first) that would corrupt the merge -> reject the frame.
// niggli_class is left unassigned (0): it needs the primitive cell incl.
// centering, which LatticeSearch cannot recover from a (possibly centred, e.g.
// C2) user cell. A proper primitive-cell indexing path (CrystFEL-style) is deferred.
auto sym_result = LatticeSearch(latt);
if (sym_result.system == sg->crystal_system())
outcome.lattice_candidate = sym_result.conventional;
else if (!de_novo)
outcome.lattice_candidate = latt;
// else: de-novo + symmetry mismatch -> leave unset, frame is not indexed
} else {
auto sym_result = LatticeSearch(latt);
outcome.symmetry = LatticeMessage{
@@ -136,7 +147,7 @@ IndexAndRefine::IndexingOutcome IndexAndRefine::DetermineLatticeAndSymmetry(Data
// lattice to each accepted extra lattice. Candidates are materialized later
// in RefineGeometryIfNeeded so they're rooted in the refined (and, for
// monoclinic, reordered) main lattice.
if (indexer_result.lattice.size() > 1) {
if (outcome.lattice_candidate && indexer_result.lattice.size() > 1) {
auto ml_latt = MultiLatticeSearch(indexer_result.lattice);
for (auto &ml : ml_latt) {
if (outcome.extra_lattice_rotations.size() >= experiment.GetIndexingSettings().GetMaxExtraLattices())