Map an FFT candidate to its own Bravais lattice, not to the pinned group's

A user-fixed space group reached indexing in one place, and what it did there was
relabel a cell rather than re-express it. build_sr took the conventional cell that
LatticeSearch had reduced for whatever Bravais class the METRIC matched, then
overwrote its system and centring with the pinned group's - without transforming
the cell. The constrained refine then snapped that cell's real angles onto the
pinned class's ideal ones. Measured: a C-centred orthorhombic cell relabelled
primitive monoclinic indexed 1 of 60 validation frames, and an F-cubic one
relabelled trigonal indexed 0 of 60, where the same frames index 36/60 and 51/60
with no group given.

The pseudo-symmetry guard was withheld at the same time - has_tri required no
group - so the unconstrained cell did not exist, which also disabled the
false-promotion rescue in pick_best. The only remaining outcome for a bad
constrained cell was the throw. That is what decided the two centred-monoclinic
cases, where the relabelling is a no-op and the metric really is the pinned class:
the constrained solve runs out to the length bound at a fraction of 0.005 while the
unconstrained solve on the same candidate reaches 0.7.

The group names the symmetry; it does not say which basis the candidate came back
in. It is applied where it belongs, to the scaling and the merge.

Pinning each rotation test dataset to its reference group: 5 hard failures of 38
become none, and no dataset is worse than before. The de-novo path is unchanged by
construction - with no group the deleted branch never ran, and has_tri's condition
reduces to its old form - and a ten-dataset de-novo control reproduces the baseline
exactly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016NNnL26LAvruQ9eLUUWvrJ
This commit is contained in:
2026-08-24 18:50:01 +02:00
co-authored by Claude Opus 5
parent bd2179b08f
commit baf302f813
2 changed files with 19 additions and 24 deletions
+1
View File
@@ -18,6 +18,7 @@ This is an UNSTABLE release. It includes many experimental features, as well as
* HDF5 and image stream: `mirror_y` records whether the assembled image is mirrored in Y relative to the detector's raw readout.
* rugnux: an image integrated in pyFAI through the `.poni` file written by `--mode calibration` now comes out with the correct azimuth. Radial integration is unchanged.
* rugnux: the `.poni` file declares pyFAI's `orientation`, which needs pyFAI 2024.01 or newer.
* rugnux: fixing the space group with `-S` no longer prevents the lattice from being found; the group is applied to scaling and merging rather than to the indexing search.
* rugnux: the detector geometry is also logged in XDS's convention (`ORGX`/`ORGY`, detector axis vectors, rotation axis), so it can be compared with an XDS refinement.
* rugnux: `_process.h5` describes the pixel format of the images it links to.
* A DECTRIS detector sending signed images is no longer declared unsigned in the image stream and in HDF5.
@@ -143,33 +143,24 @@ void RotationIndexer::RunIndexing() {
const auto indexer_result = indexer_.Run(experiment, coords);
if (!indexer_result.lattice.empty() && indexer_result.lattice[0].CalcVolume() > 1.0) {
auto sg = experiment.GetGemmiSpaceGroup();
DiffractionExperiment experiment_copy(experiment);
const float index_tol = experiment.GetIndexingSettings().GetTolerance();
const auto orig_axis = axis_;
// Map an FFT candidate cell to a (metric) space-group setting: the user-fixed SG's conventional
// cell, or the de-novo Bravais lattice. Re-express a metrically-hexagonal cell in conventional
// hexagonal axes (LatticeSearch can land on the ortho-hexagonal C setting) so the 3-fold is not
// hidden from scaling.
// Map an FFT candidate cell to its Bravais lattice. Re-express a metrically-hexagonal cell in
// conventional hexagonal axes (LatticeSearch can land on the ortho-hexagonal C setting) so the
// 3-fold is not hidden from scaling.
//
// A user-fixed space group is deliberately NOT stamped on here. The group names the symmetry;
// it does not say which basis the FFT candidate came back in, and the conventional cell above
// was reduced for whatever class the METRIC matched. Relabelling that cell with the group's
// system and centring leaves the constrained refine snapping the wrong angles to the ideal
// ones: measured, a C-centred orthorhombic cell relabelled primitive monoclinic indexed 1 of
// 60 validation frames and an F-cubic one relabelled trigonal indexed 0 of 60, where the same
// frames index at 36/60 and 51/60 without a group. The group is applied where it belongs - to
// the scaling and the merge.
auto build_sr = [&](const CrystalLattice &cand) -> LatticeSearchResult {
auto ls = LatticeSearch(cand);
if (sg) {
const auto is_hexagonal = [](gemmi::CrystalSystem s) {
return s == gemmi::CrystalSystem::Trigonal || s == gemmi::CrystalSystem::Hexagonal;
};
CrystalLattice conventional = ls.conventional;
if (is_hexagonal(sg->crystal_system()) && !is_hexagonal(ls.system))
conventional = HexagonalConventional(ls.primitive_reduced);
return LatticeSearchResult{
.niggli_class = ls.niggli_class,
.primitive_reduced = ls.primitive_reduced,
.conventional = conventional,
.system = sg->crystal_system(),
.centering = sg->centring_type(),
.reindex = ls.reindex,
};
}
if (!IsHexagonalSystem(ls.system) && IsMetricallyHexagonal(ls.primitive_reduced)) {
ls.conventional = HexagonalConventional(ls.primitive_reduced);
ls.system = gemmi::CrystalSystem::Hexagonal;
@@ -250,9 +241,12 @@ void RotationIndexer::RunIndexing() {
const auto conv_uc = w.sr.conventional.GetUnitCell();
const float length_bound_A = 1.2f * static_cast<float>(std::max({conv_uc.a, conv_uc.b, conv_uc.c}));
w.constrained = make_data(w.sr.conventional, w.sr.system, length_bound_A);
// Pseudo-symmetry guard (de-novo only - never override a user-fixed space group): also refine
// unconstrained (triclinic) on the primitive cell.
w.has_tri = (!sg && w.sr.system != gemmi::CrystalSystem::Triclinic);
// Pseudo-symmetry guard: also refine unconstrained (triclinic) on the primitive cell. Run it
// with a user-fixed space group too. The metric promotion the constrained refine acts on is
// decided by the geometry, not by the group, so it can be false whether or not a group was
// given - and without this cell there is nothing to catch it with, which is how a fixed
// group turned crystals the de-novo path indexes at 60/60 into runs that index none.
w.has_tri = (w.sr.system != gemmi::CrystalSystem::Triclinic);
if (w.has_tri)
w.tri = make_data(w.sr.primitive_reduced, gemmi::CrystalSystem::Triclinic, length_bound_A);
w.viable = true;