rugnux: the two-pass supercell guard compares primitive cells, not settings

The guard that catches the bistable supercell collapse between the two rotation passes took the
volume of each pass's CONVENTIONAL cell. A centred conventional cell is an exact integer multiple
of its primitive one - C and I twice, R three times, F four times - so two settings of the same
lattice differ by exactly that factor, and the guard read a change of setting as a supercell. It
then forced pass 1's result, and with it pass 1's lower symmetry, on a pass that had found the
same lattice in a better one. Ten of 116 rotation datasets tripped it, every one at an exact
centring multiplicity: seven at 2.00x, one at 3.06x, one at 3.99x. The last is an F-centred
lattice; it was held in P1 where the second pass had found it centred orthorhombic.

The two structurally identical guards inside RunPipeline already convert with ToPrimitive first,
and their comments say why. This one could not: ProcessResult carried the consensus cell with no
centring beside it, so at the comparison there was nothing to convert with. The centring is now
carried alongside the cell, set at each of the four places the cell is - the finalized rotation
lattice, the reindex into a user-fixed group's setting, the reduction of a doubled cell, and the
committed higher-symmetry reindex - and the guard converts both sides before comparing. Nothing
else reads it; every other consumer of the cell is unchanged.

On the F-centred dataset the run goes from P1 at 2.232 A to I 2 2 2 at 2.077 A, multiplicity
1.86 to 7.07.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 23:20:32 +02:00
co-authored by Claude Opus 5
parent ecb0571d8d
commit ffce27ac33
2 changed files with 27 additions and 6 deletions
+22 -6
View File
@@ -1577,11 +1577,21 @@ ProcessResult Rugnux::RunAllPasses(RugnuxObserver *observer) {
// genuine de-novo demotion to a smaller primitive (the pseudo-symmetry case the second pass re-indexes
// de novo to catch) has pass2 <= pass1 and is left untouched.
if (!cancelled_ && prepass_result_ && pass1.consensus_cell && pass2.consensus_cell) {
const double v1 = gemmi::UnitCell(*pass1.consensus_cell).volume;
const double v2 = gemmi::UnitCell(*pass2.consensus_cell).volume;
// PRIMITIVE volumes, like the two guards in RunPipeline: a centred conventional cell is an
// exact integer multiple of its primitive one (C/I 2x, R 3x, F 4x), so two settings of the
// SAME lattice differ by that factor and comparing the conventional cells reads a mere
// change of setting as a supercell - which then forces pass 1's setting and, with it, its
// lower symmetry. Measured: every dataset this guard fired on was at an exact centring
// multiplicity, one of them an F-centred lattice held down to P1 at 3.99x.
const auto primitive_volume = [](const UnitCell &cell, std::optional<char> centering) {
return std::abs(CrystalLattice(cell).ToPrimitive(centering.value_or('P')).CalcVolume());
};
const double v1 = primitive_volume(*pass1.consensus_cell, pass1.consensus_centering);
const double v2 = primitive_volume(*pass2.consensus_cell, pass2.consensus_centering);
if (v1 > 0.0 && v2 > 1.5 * v1) {
logger.Info("Two-pass: second-pass cell volume {:.0f} A^3 is {:.2f}x pass-1 ({:.0f} A^3) - a "
"spurious supercell; re-running the second pass with pass-1's result forced",
logger.Info("Two-pass: second-pass primitive cell volume {:.0f} A^3 is {:.2f}x pass-1's "
"({:.0f} A^3) - a spurious supercell; re-running the second pass with "
"pass-1's result forced",
v2, v2 / v1, v1);
force_rotation_result_ = *prepass_result_;
// Pass 1's result carries pass 1's goniometer, and the per-image path takes its angles
@@ -1596,8 +1606,8 @@ ProcessResult Rugnux::RunAllPasses(RugnuxObserver *observer) {
pass2.pass_count = 3;
pass2.pass_decision = fmt::format(
"post-refined geometry adopted, re-run with pass-1's lattice forced: the de-novo "
"cell volume {:.0f} A^3 was {:.2f}x pass-1's ({:.0f} A^3), a spurious supercell",
v2, v2 / v1, v1);
"primitive cell volume {:.0f} A^3 was {:.2f}x pass-1's ({:.0f} A^3), a spurious "
"supercell", v2, v2 / v1, v1);
force_rotation_result_.reset();
}
}
@@ -3162,6 +3172,9 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
.crystal_system = rot->search_result.system
};
result.rotation_lattice_found = true;
// The consensus cell below is this lattice's conventional cell, so this is the centring
// it is written in - the pair the volume comparisons in Run() need.
result.consensus_centering = rot->search_result.centering;
}
result.consensus_cell = indexer->GetConsensusUnitCell();
end_msg.unit_cell = result.consensus_cell;
@@ -3293,6 +3306,7 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
}
}
result.consensus_cell = cand.conventional.GetUnitCell();
result.consensus_centering = cand.centering;
end_msg.unit_cell = result.consensus_cell;
end_msg.rotation_lattice = end_msg.rotation_lattice->Multiply(reindex);
end_msg.rotation_lattice_type = LatticeMessage{ .centering = cand.centering,
@@ -4025,6 +4039,7 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
}
const size_t dropped = sm.merged.size() - merged_p.size();
result.consensus_cell = cand->conventional.GetUnitCell();
result.consensus_centering = cand->centering;
end_msg.unit_cell = result.consensus_cell;
end_msg.rotation_lattice = end_msg.rotation_lattice->Multiply(reduce);
end_msg.rotation_lattice_type = LatticeMessage{ .centering = cand->centering,
@@ -4089,6 +4104,7 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
reindex_hkl(r, *commit_reindex);
}
result.consensus_cell = commit_cell;
result.consensus_centering = commit_lattice_type->centering;
end_msg.unit_cell = commit_cell;
// Keep the lattice metadata (hence the master-file UB matrix and indexed-lattice
// vectors) in the same conventional setting as the cell, reflections and space group.
+5
View File
@@ -218,6 +218,11 @@ struct ProcessResult {
std::array<double, 3> used_detector_tilt_deg{};
double used_direct_beam_x_pxl = 0.0, used_direct_beam_y_pxl = 0.0;
std::optional<UnitCell> consensus_cell;
// The centring `consensus_cell` is written in; empty when nothing named one (the stills path
// averages per-image cells and has no lattice search behind them). A conventional cell says
// nothing about size on its own - a centred one is an exact integer multiple of its primitive
// cell - so two cells can only be compared for volume once this has brought both to primitive.
std::optional<char> consensus_centering;
bool rotation_lattice_found = false;
MeanProcessingTime mean_processing_time{};
std::optional<std::string> written_master_path;