diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b5a4e5b3f..809fcf766 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -11,6 +11,7 @@ * `rugnux` leaves an event out of the unmerged MTZ when less of its rocking curve was captured than `--min-captured-fraction`, as the merge does, since the exported rows are declared full. * `rugnux` writes `FreeR_flag` with 0 for the test set and 1 for the working set, the CCP4 convention REFMAC5 defaults to; it was the other way round. * `rugnux -S` refuses or re-seats a fixed space group whose symmetry axes the indexed cell does not carry, not only one whose centring differs; `--mode scale` refuses it too. +* The rugnux results report names the groups the data cannot separate, the enantiomorph state and any refused higher point group as `SPACE_GROUP_ALTERNATIVES`, `SPACE_GROUP_ENANTIOMORPH`, `SPACE_GROUP_REFUSED_POINT_GROUP` and `SPACE_GROUP_REFUSED_REASON`; `REPORT_VERSION` is 5. * `rugnux` handles symmetry better: the lattice, the point group, the setting and the systematic absences. * `rugnux` fits the direction of the goniometer axis and not its length, so the cell chosen by the first pass is the one its own refinement scored. * `rugnux` reports the detector geometry it measured - the direct beam, the tilt and the beam centre - and what a single sweep can and cannot determine. diff --git a/rugnux/ResultReport.cpp b/rugnux/ResultReport.cpp index aaeb18871..ccb34fcc9 100644 --- a/rugnux/ResultReport.cpp +++ b/rugnux/ResultReport.cpp @@ -20,7 +20,7 @@ namespace { // The version of this file format. Bumped when a key is renamed or removed, a table column moves, // or a reason code changes meaning - a consumer can gate on it. - constexpr int REPORT_VERSION = 4; + constexpr int REPORT_VERSION = 5; const char *BANNER = " ******************************************************************************"; @@ -231,6 +231,53 @@ std::string RenderResultReport(const std::string &output_prefix, // ---------------------------------------------- 4. SPACE GROUP DETERMINATION Section(os, "4. SPACE GROUP DETERMINATION"); + + // What the data could NOT decide, as keys rather than only as prose. SPACE_GROUP_NAME above is a + // scalar and reads like a determination; where several groups predict the same absences it is one + // of them, chosen by convention. A script that greps only the name records a coin-flip as an + // answer - measured, refining against the deposited model in the wrong enantiomorph gives + // R = 0.549 - so the ambiguity has to survive the same grep. + { + std::string alts; + if (result.space_group_search.has_value() && result.space_group.has_value()) + for (const auto &alt : result.space_group_search->alternatives) + if (alt.number != result.space_group->number) + alts += (alts.empty() ? "" : " | ") + alt.xhm(); + Key(os, "SPACE_GROUP_ALTERNATIVES", alts.empty() ? "NONE" : alts); + + // The hand is a separate question from the alternatives list, and it is decidable from the + // 22 groups' numbers alone, so it is answered even where the search did not run. Merged + // intensities never decide it: an enantiomorphic pair has the same absences and the same + // Laue class, and only a model, a substructure or an anomalous signal names the hand. + const char *enantiomorph = "NOT_APPLICABLE"; + if (result.space_group.has_value() && result.space_group->is_enantiomorphic()) { + if (result.model_validation.has_value() && result.model_validation->ok) + enantiomorph = "DETERMINED_FROM_MODEL"; + else if (!result.space_group_search.has_value()) + enantiomorph = "GIVEN"; // -S, or a reference MTZ: the user's assertion + else + enantiomorph = "UNDETERMINED"; + } + Key(os, "SPACE_GROUP_ENANTIOMORPH", enantiomorph); + + // A higher point group whose operators the intensities confirmed and whose promotion was + // refused. It exists only as prose in the search text below, so nothing can act on it - and + // a refusal is exactly the case where a user might want to try the higher group as well. + const bool refused = result.space_group_search.has_value() + && !result.space_group_search->refused_point_group_hm.empty(); + Key(os, "SPACE_GROUP_REFUSED_POINT_GROUP", + refused ? result.space_group_search->refused_point_group_hm : std::string("NONE")); + if (refused) + Key(os, "SPACE_GROUP_REFUSED_REASON", result.space_group_search->refused_reason); + os << "\n SPACE_GROUP_ALTERNATIVES names every group these data cannot separate from the one\n" + << " adopted (enantiomorphic partners, origin-ambiguous pairs, groups a gap in the data\n" + << " leaves untested); NONE means the absences single the answer out. SPACE_GROUP_ENANTIOMORPH\n" + << " is UNDETERMINED whenever the group is one of the 22 that come in enantiomorphic pairs and\n" + << " nothing outside the merged intensities named the hand - the intensities cannot, so this\n" + << " is the normal outcome, not a failure. SPACE_GROUP_REFUSED_POINT_GROUP is a higher point\n" + << " group the operator correlations supported and the consistency tests would not take.\n"; + } + if (result.space_group_search.has_value()) { Key(os, "SPACE_GROUP_SEARCH", "DE_NOVO"); os << "\n" << SearchSpaceGroupResultToText(*result.space_group_search) << "\n"; diff --git a/tests/ResultReportTest.cpp b/tests/ResultReportTest.cpp index bc27855c1..fbc507fe3 100644 --- a/tests/ResultReportTest.cpp +++ b/tests/ResultReportTest.cpp @@ -69,10 +69,15 @@ TEST_CASE("ResultReport_Render", "[Diagnostics]") { const auto text = RenderResultReport("prefix", "in.h5", x, result); // The stable keys a consumer greps for. - CHECK(text.find("\nREPORT_VERSION= 4\n") != std::string::npos); + CHECK(text.find("\nREPORT_VERSION= 5\n") != std::string::npos); CHECK(text.find("\nOUTPUT_PREFIX= prefix\n") != std::string::npos); CHECK(text.find("\nINDEXING_RATE= 0.8700\n") != std::string::npos); CHECK(text.find("\nSPACE_GROUP_NUMBER= 96\n") != std::string::npos); + // What the data could NOT decide, beside what they did. The group here was given rather + // than searched for, and it is one of the 22 that come in enantiomorphic pairs. + CHECK(text.find("\nSPACE_GROUP_ALTERNATIVES= NONE\n") != std::string::npos); + CHECK(text.find("\nSPACE_GROUP_ENANTIOMORPH= GIVEN\n") != std::string::npos); + CHECK(text.find("\nSPACE_GROUP_REFUSED_POINT_GROUP= NONE\n") != std::string::npos); CHECK(text.find("\nSWEEP_QUALITY_STATUS= COMPUTED\n") != std::string::npos); CHECK(text.find("\nSWEEP_QUALITY_COUNT= 2\n") != std::string::npos); CHECK(text.find("\nSWEEP_QUALITY_REASONS= no_diffraction crystal_out_of_beam weak_diffraction "