rugnux: decide the space group after the second pass, not the first
Pass 1 searched for the space group on the geometry the run started with, stashed the answer, and reinstated it before pass 2's merge - so the decision was made on the worse of the two passes and then fed forward as a constraint. Four guards existed to reconcile it with what pass 2 later found. Now pass 1 does not search at all. It keeps its first merge, which is where mosaicity smoothing and geometry post-refinement already happen, and skips the in-symmetry re-merge that existed only to feed the search. prepass_merge_sg_, prepass_promoted_point_group_, the reinstatement, the primitive-to-conventional reindex it needed, and lattice_conflicts_with_prepass_sg all go. The obstacle was the pass-adoption guard, which compared completeness and CC1/2 across what could now be two different space groups. It compares each pass's FIRST merge instead - P1 on both sides, full range, before correction surfaces - which pass 2 produces anyway. One guard had to be replaced rather than removed. The index-time veto is re-keyed from pass 1's GROUP to its LATTICE and made one-directional: a centred pass-1 lattice against a primitive pass-2 one. Without it a C2 crystal fell to P1, and the obvious narrower fix - exempting a pass-1 P1 - would not have caught it, because that crystal's pass-2 lattice is monoclinic-P rather than triclinic. A centred lattice and its primitive sub-cell share a primitive volume, so the supercell arm cannot see that demotion. Corpus of 93 datasets, both arms at -N 6, every log validated: 78 comparable, 65 with a byte-identical p.hkl. Two crystals gain their reference point group, one gains its screw axis, one is lost. Per shell, over the 72 unchanged crystals, the median move is +0.25 points of R_meas and zero in <I/sigma>, CC1/2 and completeness. Load-matched compute is +0.4%, and the header-geometry fallback fires three times in the old arm against once in the new. What is lost is one crystal whose 222 the operator correlations still confirm: the merge chi-squared ratio moves 3% across a fixed bound at the refined geometry. A second crystal in the XDS harness crosses the H bound the same way. Both bounds are in SearchSpaceGroup and are recorded elsewhere as miscalibrated; recalibrating them is deliberately left to its own change. The comment at the long-axis rescue is corrected while here: the resolution setting only sizes the FFT histogram, and coarsening moves the true axis DOWN the direction ranking - 1996 to 16361 of 16384 - rather than making it more robust as the comment claimed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lc5JG6kJqZoCWaoZ43JGTW
This commit is contained in:
+109
-164
@@ -1052,8 +1052,6 @@ ProcessResult Rugnux::RunAllPasses(RugnuxObserver *observer) {
|
||||
prepass_rotation_scale_.reset();
|
||||
prepass_result_.reset();
|
||||
force_rotation_result_.reset();
|
||||
prepass_merge_sg_.reset();
|
||||
prepass_promoted_point_group_ = false;
|
||||
bragg_adaptive_.reset();
|
||||
|
||||
logger.Info("Rotation two-pass geometry post-refinement: first pass (header geometry) -> {}_01_*",
|
||||
@@ -1106,24 +1104,6 @@ ProcessResult Rugnux::RunAllPasses(RugnuxObserver *observer) {
|
||||
bragg_adaptive_->GetR2(), bragg_adaptive_->GetR3());
|
||||
}
|
||||
|
||||
// Space group: the second pass RE-INDEXES DE NOVO (clear the group here) so the indexer's pseudo-
|
||||
// symmetry safeguards recover the true cell - reusing pass-1's group in the indexer forces build_sr
|
||||
// onto a wrong / doubled cell (a huge oblique cell collapses; a pseudo-centred cell doubles). Pass-1's
|
||||
// group is instead reinstated for the MERGE ONLY (RunPipeline, before RotationScaleMerge), so the
|
||||
// symmetry stays fixed with no re-search / no flip.
|
||||
//
|
||||
// This holds for a group the centred-lattice test determined too, even though its conventional
|
||||
// setting is not the one the de-novo primitive frame comes back in. That is precisely what the
|
||||
// primitive->conventional reindex below does, and when the reindex declines - pass 2's metric is
|
||||
// not the group's - the centring check right after it stops the pass being adopted at all. Letting
|
||||
// such a group be re-searched instead put the second pass's determination back in play at a
|
||||
// slightly different geometry, which on a lattice whose centring is pseudo-symmetric to a couple of
|
||||
// tenths of a percent is a coin toss: measured on a C-centred monoclinic crystal, pass 1 confirmed
|
||||
// the centring and pass 2 dropped it, and the run merged in P1.
|
||||
experiment_.SpaceGroupNumber(std::nullopt);
|
||||
prepass_merge_sg_ = pass1.space_group_number;
|
||||
prepass_promoted_point_group_ = pass1.twinning.laue_class_was_chosen_by_promotion;
|
||||
|
||||
logger.Info("Rotation two-pass geometry post-refinement: second pass (refined geometry, canonical) -> {}_*",
|
||||
base_prefix);
|
||||
config_.output_prefix = base_prefix; // the refined pass is the canonical result (no _02 suffix)
|
||||
@@ -1202,43 +1182,31 @@ ProcessResult Rugnux::RunAllPasses(RugnuxObserver *observer) {
|
||||
force_rotation_result_.reset();
|
||||
}
|
||||
}
|
||||
// The space group was DECIDED in pass 1 and only reused by pass 2, which therefore has no search
|
||||
// of its own to report. Carry pass 1's over, or the evidence behind the decision (operator CCs,
|
||||
// absence counts, the refused higher symmetry) is never shown on the default rotation path.
|
||||
if (!pass2.space_group_search.has_value())
|
||||
pass2.space_group_search = pass1.space_group_search;
|
||||
|
||||
// Pass 2 is normally the better answer, which is why it is the canonical output - but it is not
|
||||
// guaranteed to be, and until now it was adopted whatever it produced. Two ways it can be wrong:
|
||||
// it merges more unique reflections than the cell it settled on can hold (completeness above
|
||||
// 100% is arithmetically impossible and means the cell is wrong), or its CC1/2 collapses
|
||||
// relative to pass 1. Measured on a large-cell crystal: pass 1 195538 unique at 92.6% and
|
||||
// CC1/2 0.98, pass 2 134667 at "117%" and CC1/2 0.62. Both bounds are set where only a failure
|
||||
// guaranteed to be. Two ways it can be wrong: it merges more unique reflections than the cell it
|
||||
// settled on can hold (completeness above 100% is arithmetically impossible and means the cell is
|
||||
// wrong), or its CC1/2 collapses relative to pass 1. Both bounds are set where only a failure
|
||||
// reaches them, so a normal run - where pass 2 is a little better - keeps pass 2.
|
||||
//
|
||||
// The CC1/2 compared is the one measured BEFORE the scaling correction surfaces, on both sides.
|
||||
// Pass 1 fits no surfaces (its intensities are discarded, so it has nowhere to put one) and its
|
||||
// reported CC1/2 is therefore already the uncorrected number; setting pass 2's CORRECTED CC1/2
|
||||
// against it would credit the refined geometry with whatever the surfaces did and blunt the
|
||||
// comparison in the process.
|
||||
// The comparison is made on each pass's SEARCH merge, not on the final one. Each pass
|
||||
// determines its own space group, so the two final merges can be in different groups, and
|
||||
// completeness and CC1/2 in different groups are not the same measurement. The search merge is
|
||||
// P1 on both sides, over the full resolution range and before any correction surface is fitted
|
||||
// - the one merge the two passes make in the same terms.
|
||||
if (!cancelled_ && pass1.has_merge_statistics && pass2.has_merge_statistics) {
|
||||
const auto completeness = [](const ProcessResult &r) {
|
||||
const auto &o = r.merge_statistics.overall;
|
||||
return o.possible_unique_reflections > 0
|
||||
? 100.0 * o.unique_reflections / o.possible_unique_reflections : 0.0;
|
||||
};
|
||||
const double compl2 = completeness(pass2);
|
||||
const double cc1 = pass1.cc_half_before_corrections;
|
||||
const double cc2 = pass2.cc_half_before_corrections;
|
||||
const double compl1 = pass1.search_merge_completeness;
|
||||
const double compl2 = pass2.search_merge_completeness;
|
||||
const double cc1 = pass1.search_merge_cc_half;
|
||||
const double cc2 = pass2.search_merge_cc_half;
|
||||
constexpr double MAX_CREDIBLE_COMPLETENESS = 100.5; // rounding headroom, nothing more
|
||||
constexpr double MAX_CC_HALF_LOSS = 0.05;
|
||||
if (pass2.lattice_conflicts_with_prepass_sg
|
||||
if (pass2.lattice_conflicts_with_fixed_sg
|
||||
|| compl2 > MAX_CREDIBLE_COMPLETENESS || cc2 < cc1 - MAX_CC_HALF_LOSS) {
|
||||
logger.Warning("Two-pass: the refined pass is worse than the header-geometry pass "
|
||||
"(completeness {:.1f}% vs {:.1f}%, CC1/2 before corrections {:.3f} vs "
|
||||
"{:.3f}) - going back to the header geometry. The refined geometry did "
|
||||
"not help this crystal.",
|
||||
compl2, completeness(pass1), cc2, cc1);
|
||||
compl2, compl1, cc2, cc1);
|
||||
// The refined pass has already written the canonical files, so re-run at the header
|
||||
// geometry to replace them - the same remedy the supercell collapse above uses, and it
|
||||
// only costs a pass on a crystal that was going to be wrong otherwise.
|
||||
@@ -1246,25 +1214,22 @@ ProcessResult Rugnux::RunAllPasses(RugnuxObserver *observer) {
|
||||
.DetectorDistance_mm(header_geometry[2]);
|
||||
if (prepass_rotation_scale_ && gonio_snapshot)
|
||||
experiment_.Goniometer(*gonio_snapshot); // and the header rotation angles with it
|
||||
experiment_.SpaceGroupNumber(std::nullopt);
|
||||
config_.output_prefix = base_prefix;
|
||||
auto redo = RunPipeline(observer, /*write_output=*/true, /*geometry_prepass=*/false);
|
||||
if (!redo.space_group_search.has_value())
|
||||
redo.space_group_search = pass1.space_group_search;
|
||||
redo.post_refine = pass1.post_refine;
|
||||
redo.pass_number = pass2.pass_count + 1;
|
||||
redo.pass_count = pass2.pass_count + 1;
|
||||
redo.pass_decision = fmt::format(
|
||||
"header geometry re-adopted: the post-refined pass was worse "
|
||||
"(completeness {:.1f}% vs {:.1f}%, CC1/2 before corrections {:.3f} vs {:.3f})",
|
||||
compl2, completeness(pass1), cc2, cc1);
|
||||
compl2, compl1, cc2, cc1);
|
||||
return redo;
|
||||
}
|
||||
if (pass2.pass_decision.empty())
|
||||
pass2.pass_decision = fmt::format(
|
||||
"post-refined geometry adopted (completeness {:.1f}% vs {:.1f}%, "
|
||||
"CC1/2 before corrections {:.3f} vs {:.3f})",
|
||||
compl2, completeness(pass1), cc2, cc1);
|
||||
compl2, compl1, cc2, cc1);
|
||||
}
|
||||
if (pass2.pass_decision.empty())
|
||||
pass2.pass_decision = (prepass_detector_geometry_ || prepass_rotation_scale_)
|
||||
@@ -1333,6 +1298,12 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
|
||||
Logger logger("Rugnux");
|
||||
ProcessResult result;
|
||||
|
||||
// Each pass determines its own space group, so start every one of them from the group the run was
|
||||
// ASKED for. A pass that determined one leaves it on experiment_, and the pass after it - the
|
||||
// re-runs in Run(), or a second Run() on the same object - would otherwise inherit that answer and
|
||||
// skip the search that is the whole point of running again.
|
||||
experiment_.SpaceGroupNumber(user_fixed_sg_);
|
||||
|
||||
const auto dataset = reader_.GetDataset();
|
||||
if (!dataset)
|
||||
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
||||
@@ -1965,9 +1936,9 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
|
||||
// axis was likely lost: the unconstrained FFT either collapsed it to a short sub-multiple or let
|
||||
// a denser supercell over-fit the accumulated cloud (a small global-orientation error throws the
|
||||
// many high-order reflections off along the fine axis, so the true cell scores worst on the raw
|
||||
// cloud). Recover the true metric with a COARSE-resolution pass - only low-order reflections,
|
||||
// where the fine axis stays robust - then RE-INDEX at full resolution constrained by that cell as
|
||||
// a reference (the -C path): the reference filter drops the collapsed/supercell candidates and
|
||||
// cloud). Recover the true metric with a second search whose FFT unit-cell bound is widened past
|
||||
// the default, then RE-INDEX at full resolution constrained by the cell that comes back, as a
|
||||
// reference (the -C path): the reference filter drops the collapsed/supercell candidates and
|
||||
// refines an accurate global lattice. Only runs after a poor standard pass, so well-indexing
|
||||
// crystals never pay for it and their result is untouched.
|
||||
auto max_axis = [](const RotationIndexerResult &r) {
|
||||
@@ -1977,8 +1948,12 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
|
||||
if (!cancelled_ && best.result.has_value()
|
||||
&& best.score < 0.5 * static_cast<double>(validation.size())) {
|
||||
auto coarse_settings = experiment_.GetIndexingSettings();
|
||||
coarse_settings.FFT_HighResolution_A(3.5f); // low-order reflections only -> robust long axis
|
||||
// Widen the search bound too, and note this costs nothing: the histogram length is
|
||||
// The resolution setting only sizes the FFT histogram - it does not filter which spots go
|
||||
// into it - so this is here to pay for the widened bound below, not to make the long axis
|
||||
// easier to pick out. Measured, coarsening does the opposite: the acceptance angle widens
|
||||
// (0.159 -> 0.579 deg) but the true axis falls from rank 1996 to 16361 of 16384 directions.
|
||||
coarse_settings.FFT_HighResolution_A(3.5f);
|
||||
// The widened bound is what the rescue turns on, and it costs nothing: the histogram length is
|
||||
// 4*pi*max_length/high_resolution, so coarsening the resolution to 3.5 A has already
|
||||
// SHRUNK the transform by 1.75x - spending that back on reach leaves the coarse pass at
|
||||
// 1.14x the standard one. Without it the rescue cannot recover what it exists to recover:
|
||||
@@ -2108,16 +2083,11 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
|
||||
}
|
||||
|
||||
// Second pass: compare the de-novo lattice with pass 1's HERE, before integrating every image
|
||||
// with it. A markedly larger cell is the bistable supercell collapse, and a centring the group
|
||||
// carried over from pass 1 cannot describe is the same disagreement seen from the symmetry side.
|
||||
// Both were already rejected - the first in Run(), the second before the merge - but only after
|
||||
// the whole pass had run, so each cost an entire extra pass on a dataset that ended up on pass-1's
|
||||
// lattice anyway. Take pass-1's result now instead: the same remedy, one pass earlier. A triclinic
|
||||
// de-novo cell is left alone, since that is the demotion the merge reindexes into the group's
|
||||
// conventional setting.
|
||||
// with it. A markedly larger cell is the bistable supercell collapse, which was already
|
||||
// rejected in Run() - but only after the whole pass had run, so it cost an entire extra pass
|
||||
// on a dataset that ended up on pass-1's lattice anyway. Take pass-1's result now instead:
|
||||
// the same remedy, one pass earlier.
|
||||
if (!geometry_prepass && prepass_result_.has_value()) {
|
||||
const auto *reuse_sg = prepass_merge_sg_.has_value()
|
||||
? gemmi::find_spacegroup_by_number(static_cast<int>(*prepass_merge_sg_)) : nullptr;
|
||||
// Primitive volumes, like the scheme comparison above: a centred conventional cell is an
|
||||
// exact integer multiple of its primitive one, so two settings of the SAME lattice differ
|
||||
// by that factor and comparing the conventional cells reads a mere change of setting as a
|
||||
@@ -2127,12 +2097,17 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
|
||||
const double v2 = std::abs(best.result->lattice
|
||||
.ToPrimitive(best.result->search_result.centering).CalcVolume());
|
||||
const bool supercell = v1 > 1.0 && v2 > 1.5 * v1;
|
||||
const bool wrong_centering = reuse_sg != nullptr
|
||||
&& best.result->search_result.system != gemmi::CrystalSystem::Triclinic
|
||||
&& best.result->search_result.centering != reuse_sg->centring_type();
|
||||
if (supercell || wrong_centering) {
|
||||
logger.Info("Two-pass: that lattice ({}-centred, volume {:.0f} A^3) disagrees with pass 1 "
|
||||
"({}-centred, {:.0f} A^3) - integrating with pass-1's lattice instead",
|
||||
// Dropping to the primitive sub-cell of pass-1's centred lattice is the same
|
||||
// disagreement seen from the other side, and the volumes above cannot see it: the two
|
||||
// have the SAME primitive metric. Integrating there never predicts the reflections the
|
||||
// centring implies, and the pass then merges in P1 (measured on a C-centred monoclinic
|
||||
// crystal). Only this direction - a centring pass 2 finds where pass 1 had none is the
|
||||
// refined geometry doing its job, and the intensity test in the search confirms it.
|
||||
const bool lost_centring = prepass_result_->search_result.centering != 'P'
|
||||
&& best.result->search_result.centering == 'P';
|
||||
if (supercell || lost_centring) {
|
||||
logger.Info("Two-pass: that lattice ({}-centred, volume {:.0f} A^3) disagrees with "
|
||||
"pass 1 ({}-centred, {:.0f} A^3) - integrating with pass-1's lattice instead",
|
||||
best.result->search_result.centering, v2,
|
||||
prepass_result_->search_result.centering, v1);
|
||||
indexer->ForceRotationIndexerResult(*prepass_result_);
|
||||
@@ -2564,26 +2539,6 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
|
||||
return true;
|
||||
};
|
||||
|
||||
// Two-pass second pass: if the de-novo indexer DEMOTED to a triclinic (primitive) cell while the reused
|
||||
// merge space group is higher-symmetry (a huge oblique cell whose constrained centred refine was
|
||||
// ill-posed, so the pseudo-symmetry guard kept the primitive), the reflections are in the primitive
|
||||
// frame but the merge expects the group's CONVENTIONAL setting - reindex them into it (the same
|
||||
// primitive->conventional change of basis the space-group search applies to a demoted result), else the
|
||||
// merge folds the wrong equivalents. No-op when the indexer already produced the group's setting.
|
||||
if (prepass_merge_sg_ && end_msg.rotation_lattice.has_value() && end_msg.rotation_lattice_type.has_value()
|
||||
&& end_msg.rotation_lattice_type->crystal_system == gemmi::CrystalSystem::Triclinic) {
|
||||
const auto *msg_sg = gemmi::find_spacegroup_by_number(static_cast<int>(*prepass_merge_sg_));
|
||||
if (msg_sg && msg_sg->crystal_system() != gemmi::CrystalSystem::Triclinic) {
|
||||
const auto cand = LatticeSearch(*end_msg.rotation_lattice);
|
||||
// Only when LatticeSearch recovers the reused group's own metric (system + centring): otherwise
|
||||
// the reused group and the indexed metric disagree and reindexing would be wrong - leave it.
|
||||
if (cand.system == msg_sg->crystal_system() && cand.centering == msg_sg->centring_type()
|
||||
&& reindex_into(cand))
|
||||
logger.Info("Two-pass: reindexed the de-novo primitive cell into the space-group {} "
|
||||
"conventional setting for the merge", static_cast<int>(*prepass_merge_sg_));
|
||||
}
|
||||
}
|
||||
|
||||
// A space group the USER fixed carries its own Bravais lattice, and that is the setting its
|
||||
// reflections have to be indexed in - but the indexer answers to the metric, not to the group, and
|
||||
// LatticeSearch hands back the MOST symmetric setting the metric supports. A tetragonal-P lattice
|
||||
@@ -2627,65 +2582,45 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
|
||||
}
|
||||
}
|
||||
|
||||
// Two-pass second pass only: the indexer above ran DE NOVO (Run() cleared the space group so its
|
||||
// pseudo-symmetry safeguards find the true cell); now reinstate pass-1's space group for the merge,
|
||||
// exactly as a user -S would, so the symmetry is fixed without a re-search. No-op on pass 1 / single pass.
|
||||
// The lattice a pass indexed has to carry the Bravais lattice of the group it merges in. Only a
|
||||
// group the USER fixed can fail that - one the pass determined for itself was determined FROM
|
||||
// this lattice - and the re-seating above has already had its chance to find a setting that
|
||||
// does. What comes out otherwise is not a merely suboptimal answer: the absence rule is applied
|
||||
// across a frame the reflections are not in, and the statistics stop being arithmetic (measured:
|
||||
// 173.5% complete on a triclinic-P lattice merged in C2, and an undefined R_meas on an F-centred
|
||||
// cubic one merged in a trigonal-P group).
|
||||
//
|
||||
// Only when the lattice pass 2 actually found HAS that group's centring. Re-indexing de novo can
|
||||
// land in a different setting from pass 1 - most often the primitive sub-cell of a centred
|
||||
// lattice, which is what the reindex above exists to undo, and which it declines to do when the
|
||||
// metric does not match. Stamping a centred group onto a primitive cell is not a small error:
|
||||
// the centring absence rule then removes half the reflections that genuinely exist, so the merge
|
||||
// reports more unique reflections than its own cell can hold (measured: a C2 group on a cell of
|
||||
// exactly half the C-centred volume, reported as 117% complete with CC1/2 0.62 against the first
|
||||
// pass's 92.6% and 0.98). When they disagree, let pass 2 search for itself rather than trust a
|
||||
// group that belongs to a different lattice.
|
||||
if (prepass_merge_sg_ && end_msg.rotation_lattice_type.has_value()) {
|
||||
const auto *reuse_sg = gemmi::find_spacegroup_by_number(static_cast<int>(*prepass_merge_sg_));
|
||||
if (reuse_sg && end_msg.rotation_lattice_type->centering != reuse_sg->centring_type()) {
|
||||
logger.Warning("Two-pass: the second pass indexed a {}-centred lattice, but the first pass "
|
||||
"determined {}, whose lattice is {}-centred - the group cannot describe this "
|
||||
"lattice, so this pass will not be adopted",
|
||||
end_msg.rotation_lattice_type->centering, reuse_sg->xhm(),
|
||||
reuse_sg->centring_type());
|
||||
result.lattice_conflicts_with_prepass_sg = true;
|
||||
}
|
||||
}
|
||||
|
||||
// The same conflict, but for a group the USER fixed, and on a pass that has nothing to fall back
|
||||
// on - the first of the two, or a single pass. The re-seating above has had its chance and no
|
||||
// setting of the lattice this crystal indexes as carries the group's Bravais lattice, so there is
|
||||
// no frame in which the merge means anything. What comes out is not a merely suboptimal answer:
|
||||
// the absence rule is applied across a frame the reflections are not in, and the statistics stop
|
||||
// being arithmetic (measured: 173.5% complete on a triclinic-P lattice merged in C2, and an
|
||||
// undefined R_meas on an F-centred cubic one merged in a trigonal-P group). Nor is there anything
|
||||
// to fall back ON - the group is the user's assertion, and quietly determining a different one
|
||||
// would answer a question that was not asked. Refuse, and name the cell that WAS indexed so the
|
||||
// user can act on it.
|
||||
//
|
||||
// The SECOND pass keeps the flag-and-do-not-adopt handling just above instead: its lattice comes
|
||||
// from a de-novo re-index at the post-refined geometry, and the first pass - whose lattice did
|
||||
// carry the group - is still there to go back to. Refusing there would throw away a good answer.
|
||||
if (user_fixed_sg_ && !prepass_merge_sg_
|
||||
&& end_msg.rotation_lattice_type.has_value() && end_msg.unit_cell.has_value()) {
|
||||
// The two-pass second pass is flagged and not adopted rather than refused: its lattice comes from
|
||||
// a de-novo re-index at the post-refined geometry, and the first pass - whose lattice did carry
|
||||
// the group - is still there to go back to. A pass with nothing to fall back on refuses instead;
|
||||
// the group is the user's assertion, and quietly determining a different one would answer a
|
||||
// question that was not asked. Name the cell that WAS indexed so the user can act on it.
|
||||
if (user_fixed_sg_ && end_msg.rotation_lattice_type.has_value() && end_msg.unit_cell.has_value()) {
|
||||
const auto *fixed_sg = gemmi::find_spacegroup_by_number(static_cast<int>(*user_fixed_sg_));
|
||||
if (fixed_sg && end_msg.rotation_lattice_type->centering != fixed_sg->centring_type()) {
|
||||
const auto &uc = *end_msg.unit_cell;
|
||||
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, fmt::format(
|
||||
"The space group {} was fixed for this run, and its lattice is {}-centred - but "
|
||||
"this crystal indexes as a {}-centred {} lattice (a={:.3f} b={:.3f} c={:.3f} "
|
||||
"alpha={:.2f} beta={:.2f} gamma={:.2f}), and no setting of that lattice carries the "
|
||||
"group's. Merging in {} would apply its absence rule to reflections that are not in "
|
||||
"its frame, so nothing it reported would describe this crystal. Re-run without a fixed "
|
||||
"space group to have it determined from the data, or fix one whose lattice this "
|
||||
"crystal has.",
|
||||
fixed_sg->xhm(), fixed_sg->centring_type(), end_msg.rotation_lattice_type->centering,
|
||||
gemmi::crystal_system_str(end_msg.rotation_lattice_type->crystal_system),
|
||||
uc.a, uc.b, uc.c, uc.alpha, uc.beta, uc.gamma, fixed_sg->xhm()));
|
||||
if (prepass_result_) {
|
||||
logger.Warning("Two-pass: the second pass indexed a {}-centred lattice, but the fixed "
|
||||
"space group {} has a {}-centred lattice - the group cannot describe "
|
||||
"this lattice, so this pass will not be adopted",
|
||||
end_msg.rotation_lattice_type->centering, fixed_sg->xhm(),
|
||||
fixed_sg->centring_type());
|
||||
result.lattice_conflicts_with_fixed_sg = true;
|
||||
} else {
|
||||
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, fmt::format(
|
||||
"The space group {} was fixed for this run, and its lattice is {}-centred - but "
|
||||
"this crystal indexes as a {}-centred {} lattice (a={:.3f} b={:.3f} c={:.3f} "
|
||||
"alpha={:.2f} beta={:.2f} gamma={:.2f}), and no setting of that lattice carries the "
|
||||
"group's. Merging in {} would apply its absence rule to reflections that are not in "
|
||||
"its frame, so nothing it reported would describe this crystal. Re-run without a fixed "
|
||||
"space group to have it determined from the data, or fix one whose lattice this "
|
||||
"crystal has.",
|
||||
fixed_sg->xhm(), fixed_sg->centring_type(), end_msg.rotation_lattice_type->centering,
|
||||
gemmi::crystal_system_str(end_msg.rotation_lattice_type->crystal_system),
|
||||
uc.a, uc.b, uc.c, uc.alpha, uc.beta, uc.gamma, fixed_sg->xhm()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (prepass_merge_sg_)
|
||||
experiment_.SpaceGroupNumber(*prepass_merge_sg_);
|
||||
|
||||
const auto &rot_ss = experiment_.GetScalingSettings();
|
||||
const bool is_rotation = experiment_.IsRotationIndexing(); // rotation indexing -> rotation scaling/merge
|
||||
@@ -2809,12 +2744,22 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
|
||||
const auto initial_sg = experiment_.GetGemmiSpaceGroup();
|
||||
auto sm = scale_and_merge(initial_sg ? initial_sg->short_name() : "P1", !initial_sg.has_value());
|
||||
|
||||
// The two-pass quality guard judges the second pass against the first on THIS merge, which both
|
||||
// passes make in the same terms - P1 (or the group the user fixed), full range, no correction
|
||||
// surfaces - where their final merges need not even be in the same group. See RunAllPasses.
|
||||
{
|
||||
const auto &o = sm.statistics.overall;
|
||||
result.search_merge_completeness = o.possible_unique_reflections > 0
|
||||
? 100.0 * o.unique_reflections / o.possible_unique_reflections : 0.0;
|
||||
result.search_merge_cc_half = sm.cc_half_before_corrections;
|
||||
}
|
||||
|
||||
// Rotation two-pass geometry pre-pass: this first scale/merge has now fitted a frame-order-smoothed
|
||||
// mosaicity and written it back onto the per-frame outcomes. Capture it for the second pass's Bragg
|
||||
// prediction (so prediction uses the smoothed value, not a fresh per-image estimate) and post-refine +
|
||||
// apply the detector geometry from this integration. The pre-pass then CONTINUES through the space-
|
||||
// group search (below) so the second pass can reuse the same space group and lattice - re-searching at
|
||||
// the slightly changed geometry could flip a borderline determination and mix the two passes.
|
||||
// apply the detector geometry from this integration. That is all the pre-pass is for: everything
|
||||
// the second pass consumes from it is fitted here, and the space group is left to the second pass
|
||||
// (see the search gate below).
|
||||
if (geometry_prepass && rsm && experiment_.IsRotationIndexing()) {
|
||||
const auto &outcomes = indexer->GetIntegrationOutcome();
|
||||
prepass_mosaicity_.assign(outcomes.size(), NAN);
|
||||
@@ -2864,8 +2809,20 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
|
||||
// weak-reflection profile-fit runaway) has an astronomical resolution-normalised E and wrecks the
|
||||
// intensity-correlation / second-moment statistics. The final in-symmetry merge keeps the full
|
||||
// range (only this P1 search pass is cut); a manual --scaling-high-resolution, if coarser, wins.
|
||||
// The geometry pre-pass does not determine the space group. Its integration is at the header
|
||||
// geometry, which is the thing the second pass exists to improve on, and the symmetry evidence
|
||||
// moves with the geometry: measured on a rotation crystal, 0.33 mm of distance and 0.65 px of
|
||||
// beam centre took the screw-axis evidence from -19 to +311 nats, where merging the same
|
||||
// integration in symmetry instead of P1 moved it 2.5. Deciding on the worse integration also
|
||||
// fails in one direction only - every statistic the search weighs is a ratio of the candidate
|
||||
// merge's scatter to the subgroup's, so poorly-integrated data inflate them and REFUSE real
|
||||
// symmetry (corpus-wide the misses run ten under-calls to one over-call). So the search runs on
|
||||
// the canonical pass, at the refined geometry, and the pre-pass skips it along with the
|
||||
// in-symmetry re-merge that only exists to serve it.
|
||||
const bool search_space_group = !geometry_prepass && !initial_sg.has_value();
|
||||
|
||||
double d_min_search = 0.0;
|
||||
if (!initial_sg.has_value()) {
|
||||
if (search_space_group) {
|
||||
std::vector<std::pair<float, float>> rs; // (d, I/sigma) over the full P1 merge
|
||||
rs.reserve(sm.merged.size());
|
||||
for (const auto &m : sm.merged)
|
||||
@@ -2911,10 +2868,8 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
|
||||
// True when the point group below was chosen BY THE SEARCH (rather than given by the user) and is
|
||||
// above triclinic - i.e. a promotion was made, which is what makes a later "the Laue class is
|
||||
// holohedral so there is no twin law" conclusion circular.
|
||||
// Pass 2 reuses pass-1's group instead of searching, so the promotion it needs to report was
|
||||
// made there; a first / single pass has no carried-over group and decides this below.
|
||||
bool promoted_point_group = prepass_merge_sg_.has_value() && prepass_promoted_point_group_;
|
||||
if (!experiment_.GetGemmiSpaceGroup().has_value()) {
|
||||
bool promoted_point_group = false;
|
||||
if (search_space_group) {
|
||||
SearchSpaceGroupOptions sg_opts;
|
||||
sg_opts.nthreads = static_cast<size_t>(std::max(1, config_.nthreads));
|
||||
sg_opts.merge_friedel = experiment_.GetScalingSettings().GetMergeFriedel();
|
||||
@@ -3179,22 +3134,12 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
|
||||
} else {
|
||||
// A space group was fixed by the user; surface it so the viewer/CLI can still show it.
|
||||
result.space_group_number = experiment_.GetSpaceGroupNumber();
|
||||
// ...and record it, exactly as the search arm above does with the group it determined.
|
||||
// This arm also carries the two-pass second pass, which REUSES the first pass's group
|
||||
// rather than re-searching, so without this the _process.h5 of every two-pass rotation run
|
||||
// came out with a cell but no group - and re-merging it defaulted to P1.
|
||||
// ...and record it, exactly as the search arm above does with the group it determined, or
|
||||
// the _process.h5 comes out with a cell but no group and re-merging it defaults to P1.
|
||||
if (const auto sg = experiment_.GetSpaceGroupNumber(); sg.has_value())
|
||||
end_msg.space_group_number = static_cast<uint64_t>(*sg);
|
||||
}
|
||||
|
||||
// Rotation two-pass geometry pre-pass: the determined space group is now fixed on experiment_, so the
|
||||
// second pass REUSES it instead of re-searching at the slightly changed geometry (which could flip a
|
||||
// borderline determination and mix the two passes). The lattice is deliberately NOT forced - the second
|
||||
// pass re-indexes at the refined geometry so the CELL comes out self-consistent with it (forcing the
|
||||
// pre-pass lattice would pin the nominal cell and undo the geometry refinement). Unlike a throwaway
|
||||
// pre-pass, this one now CONTINUES to the write below so its own (header-geometry) result is saved
|
||||
// as the first pass.
|
||||
|
||||
// Reference-based indexing-ambiguity resolution (rotation). When a reference MTZ is supplied and
|
||||
// the crystal has an indexing ambiguity (merohedral: lattice symmetry higher than the Laue group),
|
||||
// pick the reindexing that best correlates with the reference intensities and re-merge in it. The
|
||||
|
||||
+17
-23
@@ -195,17 +195,24 @@ struct ProcessResult {
|
||||
// measurement on both sides of the comparison.
|
||||
double cc_half_before_corrections = NAN;
|
||||
|
||||
// The same two numbers for the pass's FIRST merge - P1 when the pass searches for a space group,
|
||||
// otherwise the group the user fixed - which is what the rotation two-pass quality guard compares.
|
||||
// The final merges of the two passes can be in different groups, where completeness and CC1/2 are
|
||||
// no longer the same measurement; this merge is in the same terms on both sides.
|
||||
double search_merge_completeness = 0.0;
|
||||
double search_merge_cc_half = NAN;
|
||||
|
||||
// Per-reflection (I, sigma) of the final merged reflections, for the ISa diagnostic: I/sigma
|
||||
// plotted against I flattens off at the asymptote the error model reports as ISa, so the plot shows
|
||||
// whether that number describes the data. Strided down to a few thousand points - this is a shape,
|
||||
// not a reflection list, and the reflections themselves are in the .mtz/.cif. Empty when no merge ran.
|
||||
std::vector<std::pair<float, float>> merged_i_sigma;
|
||||
|
||||
// Two-pass second pass: the lattice this pass indexed does not have the centring of the space
|
||||
// group the first pass determined - most often because it landed on the primitive sub-cell of a
|
||||
// centred lattice. The group cannot describe this lattice, so the pass is not trustworthy whatever
|
||||
// its statistics say. Always false on a first / single pass.
|
||||
bool lattice_conflicts_with_prepass_sg = false;
|
||||
// Two-pass second pass: the lattice this pass indexed does not have the centring of the space group
|
||||
// the USER fixed - most often because it landed on the primitive sub-cell of a centred lattice. The
|
||||
// group cannot describe this lattice, so the pass is not trustworthy whatever its statistics say.
|
||||
// Always false on a first / single pass (which refuses outright) and when no group was fixed.
|
||||
bool lattice_conflicts_with_fixed_sg = false;
|
||||
|
||||
// Twinning analysis of the final merged intensities (l_test_pairs == 0 when not computed).
|
||||
TwinningAnalysisResult twinning;
|
||||
@@ -299,27 +306,14 @@ class Rugnux {
|
||||
// (RunPipeline calls indexer->ForceRotationIndexerResult when this is set); reset right after that re-run.
|
||||
std::optional<RotationIndexerResult> force_rotation_result_;
|
||||
|
||||
// Two-pass second pass: pass-1's space group, reinstated for the MERGE ONLY (RotationScaleMerge) after the
|
||||
// indexer has run de novo. The indexer runs with no fixed group so its pseudo-symmetry safeguards recover
|
||||
// the true cell (reusing the group in the indexer forces build_sr onto a wrong / doubled cell); the merge
|
||||
// then folds equivalents in pass-1's group so the symmetry stays fixed without a re-search. Empty outside
|
||||
// the second pass, and for a reindex-derived group (left to re-search).
|
||||
std::optional<int64_t> prepass_merge_sg_;
|
||||
|
||||
// The space group the USER fixed (-S on the CLI, the settings panel in the viewer), captured before
|
||||
// anything can clear it - the two-pass driver blanks the experiment's group between the passes so the
|
||||
// second pass re-indexes de novo, so after that point the experiment can no longer say whether the
|
||||
// group in force was asserted or determined. A group the user asserted is treated differently from
|
||||
// one the run determined: it is re-seated onto its own Bravais lattice if the metric has that
|
||||
// setting, and the run refuses rather than merging in it if the metric does not.
|
||||
// anything can clear it - a pass that determines a group of its own writes it onto the experiment,
|
||||
// so after that point the experiment can no longer say whether the group in force was asserted or
|
||||
// determined. A group the user asserted is treated differently from one the run determined: it is
|
||||
// re-seated onto its own Bravais lattice if the metric has that setting, and the run refuses rather
|
||||
// than merging in it if the metric does not. Each pass is also restored to it before it starts.
|
||||
const std::optional<int64_t> user_fixed_sg_;
|
||||
|
||||
// Whether the group in prepass_merge_sg_ was PROMOTED by pass-1's search rather than given. Pass 2
|
||||
// reuses the group without searching, so it cannot work this out for itself - and without it the
|
||||
// canonical output reports "the Laue class is holohedral, so no twin law exists" about a Laue class
|
||||
// the search chose, which is the circular claim that reasoning exists to flag.
|
||||
bool prepass_promoted_point_group_ = false;
|
||||
|
||||
|
||||
// Whether the beam centre has already been placed by a measurement the pre-scan cannot better:
|
||||
// a stills bundle adjustment, or an earlier pre-scan of this same run. The pre-scan estimate is
|
||||
|
||||
Reference in New Issue
Block a user