Rugnux report: warn only where the evidence supports the claim

Report-layer fixes from the warnings audit of the rc173 full battery. Processing is
unchanged (merged MTZ/HKL bit-identical on the sets checked).

- SUPERCELL_POSSIBLE stays a key and a summary line; a warning only where the rocking part
  reaches 20% (sub-cells that refine normally rocked at 2.1-16.2%, the accepted doubled cells
  at 4.2% and 29.1%). The summary says where further lattice domains may put spots on the
  half-integer nodes.
- LATTICE_TRANSLATION only where the Patterson at the vector reaches 90% of the origin
  (UNDECLARED_LATTICE_TRANSLATION_PCT, new); the two admitted vectors on the battery read 79%
  and 85% on crystals refining in the deposited cell, and are now reported as a very strong
  pseudo-translation. PSEUDO_TRANSLATION warns from a 20% peak (xtriage convention; detected
  peaks were 8.1% on one set, 24.8-63.2% on the rest).
- <|L|> outside 0.375-0.55 (on the adopted or the pre-search merge) is TWINNING_VERDICT=
  NOT_READABLE with no warning, instead of a twin or SYMMETRY_SUSPECT reading.
- NO_LATTICE no longer fires on a rotation run that indexed the sweep with no frame indexed
  on its own; INDEXING_RATE is --developer on rotation.
- A rotation run that finds no lattice writes a report (VERDICT= FAILED, NO_LATTICE) before
  exiting 1, instead of an input-parameter error and no report (NoLatticeFound).
- Powder and Ice summary lines, ICE_* keys, and a POWDER_RINGS flag from a 0.5 spot fraction.
- SWEEP_GAPS warns only where the degraded ranges cover 10% of the sweep (77 -> 48 sets).
- A single rotation sweep no longer carries an INDEXING_AMBIGUITY warning (one orientation
  matrix, consistent hand); INDEXING_AMBIGUITY_OPERATORS and an Indexing choice line instead.
- A DOMAIN more than 10 deg away is a second crystal; the MULTIPLE_LATTICES sum is documented.
- Mosaicity line and docs name XDS's suggested REFLECTING_RANGE_E.S.D. as the comparable
  number (0.90x median over 34 in-house sets; per-image SIGMAR runs ~1.35x higher).
- HARMONIC_CONTAMINATION, SCALING_NOT_CONVERGED, POWDER_RINGS in the documented vocabulary.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1G8gJVAy6gp1K5Dz3NE5C
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-09-26 14:02:05 +02:00
1 parent a1816e905e
commit 8a8c992cca
10 files changed
+606 -83

No files matched your search

+195
View File
@@ -816,11 +816,31 @@ TEST_CASE("ResultReport_TranslationalNCS", "[Diagnostics]") {
result.tncs.detected = false;
result.tncs.near_extinct_class = false;
result.tncs.undeclared_lattice_translations.push_back({0.5, 0.5, 0.0});
result.tncs.undeclared_lattice_translation_pct.push_back(98.0);
const auto lat = RenderResultReport("prefix", "in.h5", x, result);
CHECK(lat.find("\nTNCS_DETECTED= FALSE\n") != std::string::npos);
CHECK(lat.find("\nUNDECLARED_LATTICE_TRANSLATION= 0.500 0.500 0.000\n") != std::string::npos);
CHECK(lat.find("\nUNDECLARED_LATTICE_TRANSLATION_PCT= 98.0\n") != std::string::npos);
CHECK(lat.find("\nPATHOLOGY_FLAGS= LATTICE_TRANSLATION\n") != std::string::npos);
CHECK(lat.find("\nTNCS_SUBLATTICE=") == std::string::npos);
// Admitted at 75% but short of a lattice translation's height: the claim is a very strong
// pseudo-translation, which is what molecular replacement has to be told.
result.tncs.undeclared_lattice_translation_pct = {80.0};
const auto strong = RenderResultReport("prefix", "in.h5", x, result);
CHECK(strong.find("\nPATHOLOGY_FLAGS= PSEUDO_TRANSLATION\n") != std::string::npos);
CHECK(strong.find("VERY STRONG, nearly a lattice translation") != std::string::npos);
CHECK(strong.find("invariant under the translation") == std::string::npos);
// Significant, but a peak this weak is not one molecular replacement needs declared.
result.tncs.undeclared_lattice_translations.clear();
result.tncs.undeclared_lattice_translation_pct.clear();
result.tncs.detected = true;
result.tncs.peak_percent = 8.1;
const auto weak = RenderResultReport("prefix", "in.h5", x, result);
CHECK(weak.find("\nTNCS_DETECTED= TRUE\n") != std::string::npos);
CHECK(weak.find("PSEUDO_TRANSLATION") == std::string::npos);
CHECK(weak.find("weak - below the 20% that matters for molecular replacement") != std::string::npos);
}
TEST_CASE("ResultReport_GeometryNotConverged", "[Diagnostics]") {
@@ -961,3 +981,178 @@ TEST_CASE("ResultReport_Mosaicity", "[Diagnostics]") {
CHECK(text.find("MOSAICITY_DEG_P90= 0.105") != std::string::npos);
CHECK(text.find("Mosaicity 0.091 deg") != std::string::npos);
}
TEST_CASE("ResultReport_SupercellDemoted", "[Diagnostics]") {
DiffractionExperiment x(DetJF(1));
ProcessResult result;
result.consensus_cell = UnitCell{.a = 50.0f, .b = 60.0f, .c = 70.0f,
.alpha = 90.0f, .beta = 90.0f, .gamma = 90.0f};
result.has_merge_statistics = true;
ProcessResult::SupercellClass sc;
sc.h = 0; sc.k = 0; sc.l = 1;
sc.occupancy_pct = 18.0;
sc.rock_pct = 5.0;
sc.rock_se_pct = 0.4;
sc.i_over_sigma = 1.2;
sc.doubled_cell = UnitCell{.a = 50.0f, .b = 60.0f, .c = 140.0f, .alpha = 90.0f, .beta = 90.0f, .gamma = 90.0f};
result.supercell = sc;
// Possible, as correct sub-cells also read: stated, not warned about.
auto text = RenderResultReport("p", "in.h5", x, result);
CHECK(text.find("\nSUPERCELL_POSSIBLE= TRUE\n") != std::string::npos);
CHECK(text.find("\nPATHOLOGY_FLAGS= NONE\n") != std::string::npos);
CHECK(text.find("weak Bragg-like intensity in class 0 0 1") != std::string::npos);
CHECK(text.find("may put spots there") == std::string::npos);
// Further domains of the crystal can put spots on the half-integer nodes, and the line says so.
LeftoverLatticeCensus census;
census.primary_intensity_pct = 70.0;
census.lattices.push_back(LeftoverLattice{.kind = LeftoverLatticeKind::TwinDomain, .cell = *result.consensus_cell,
.volume_ratio = 1.0, .misorientation_deg = 179.5, .z = 50.0,
.spots_pct = 8.0, .intensity_pct = 7.0, .sweep_concentration = 0.3});
result.leftover_lattices = census;
text = RenderResultReport("p", "in.h5", x, result);
CHECK(text.find("the further lattice domains found may put spots there") != std::string::npos);
// A rocking part no correct sub-cell reached is a condition.
result.supercell->rock_pct = 29.0;
text = RenderResultReport("p", "in.h5", x, result);
CHECK(text.find("\nPATHOLOGY_FLAGS= SUPERCELL_POSSIBLE\n") != std::string::npos);
CHECK(text.find("POSSIBLE - class 0 0 1 rocks like Bragg reflections (29.0%)") != std::string::npos);
}
TEST_CASE("ResultReport_TwinningOutsidePhysicalRange", "[Diagnostics]") {
DiffractionExperiment x(DetJF(1));
ProcessResult result;
result.consensus_cell = UnitCell{.a = 70.0f, .b = 70.0f, .c = 70.0f,
.alpha = 90.0f, .beta = 90.0f, .gamma = 90.0f};
result.has_merge_statistics = true;
result.twinning.l_test_pairs = 5000;
result.twinning.mean_abs_l = 0.355;
result.twinning.second_moment = 1.60;
result.twinning.twinning_suspected = true;
result.twinning.adopted_operators_suspect = true;
// Below the 0.375 of a perfect twin: no twin fraction can give it, so neither a twin nor a false
// operator is read from it.
auto text = RenderResultReport("p", "in.h5", x, result);
CHECK(text.find("\nTWINNING_VERDICT= NOT_READABLE\n") != std::string::npos);
CHECK(text.find("TWINNING ") == std::string::npos);
CHECK(text.find("\nPATHOLOGY_FLAGS= NONE\n") != std::string::npos);
CHECK(text.find("Twinning not readable (<|L|> 0.355") != std::string::npos);
CHECK(text.find("SYMMETRY SUSPECT") == std::string::npos);
// Inside the range on the adopted merge but far above 0.5 on the merge before the search: the
// statistic is distorted on these data, and the low adopted reading is not evidence either.
result.twinning.mean_abs_l = 0.395;
TwinningAnalysisResult pre;
pre.l_test_pairs = 5000;
pre.mean_abs_l = 0.590;
result.pre_promotion_twinning = pre;
text = RenderResultReport("p", "in.h5", x, result);
CHECK(text.find("\nTWINNING_VERDICT= NOT_READABLE\n") != std::string::npos);
CHECK(text.find("0.590 before the space-group search") != std::string::npos);
// An ordinary reading keeps its verdict.
result.pre_promotion_twinning.reset();
result.twinning.adopted_operators_suspect = false;
result.twinning.mean_abs_l = 0.419;
text = RenderResultReport("p", "in.h5", x, result);
CHECK(text.find("\nTWINNING_VERDICT= INDICATED\n") != std::string::npos);
CHECK(text.find("\nPATHOLOGY_FLAGS= TWINNING\n") != std::string::npos);
}
TEST_CASE("ResultReport_RotationLatticeWithoutIndexedFrames", "[Diagnostics]") {
DiffractionExperiment x(DetJF(1));
// A rotation run that indexed the sweep as a whole while no frame indexed on its own.
ProcessResult result;
result.images_processed = 1800;
result.indexing_rate = 0.0f;
result.rotation_lattice_found = true;
result.consensus_cell = UnitCell{.a = 50.0f, .b = 60.0f, .c = 70.0f,
.alpha = 90.0f, .beta = 100.0f, .gamma = 90.0f};
const auto text = RenderResultReport("p", "in.h5", x, result);
CHECK(text.find("NO_LATTICE") == std::string::npos);
CHECK(text.find("No image indexed") == std::string::npos);
CHECK(text.find("\nLATTICE_FOUND= TRUE\n") != std::string::npos);
// The no-crystal exit: the CLI hands over only the reason. The verdict has no image count to quote.
ProcessResult none;
none.warnings.push_back({PathologyCode::NO_LATTICE, "No crystal lattice was found: none"});
const auto failed = RenderResultReport("p", "in.h5", x, none);
CHECK(failed.find("\nVERDICT= FAILED\n") != std::string::npos);
CHECK(failed.find("\nPATHOLOGY_FLAGS= NO_LATTICE\n") != std::string::npos);
CHECK(failed.find("No crystal lattice was found in these frames") != std::string::npos);
CHECK(failed.find("UNIT_CELL_CONSTANTS=") == std::string::npos);
CHECK(failed.find("SPACE_GROUP_NUMBER=") == std::string::npos);
}
TEST_CASE("ResultReport_PowderAndIce", "[Diagnostics]") {
DiffractionExperiment x(DetJF(1));
ProcessResult result;
result.consensus_cell = UnitCell{.a = 79.0f, .b = 79.0f, .c = 38.0f,
.alpha = 90.0f, .beta = 90.0f, .gamma = 90.0f};
result.has_merge_statistics = true;
auto text = RenderResultReport("p", "in.h5", x, result);
CHECK(text.find("Powder no rings detected") != std::string::npos);
CHECK(text.find("ICE_RINGS_DETECTED") == std::string::npos);
result.powder.rings_q_recipA = {1.61f, 1.71f, 1.82f};
result.powder.spot_fraction = 0.20f;
result.ice_ring_score = 2.69f;
result.ice_spot_ratio = 1.08f;
result.ice_rings_detected = true;
result.ice_flagged_fraction = 0.257;
text = RenderResultReport("p", "in.h5", x, result);
CHECK(text.find("Powder 3 rings holding 20% of the spots found") != std::string::npos);
CHECK(text.find("\nICE_RINGS_DETECTED= TRUE\n") != std::string::npos);
CHECK(text.find("\nICE_REFLECTIONS_ON_RINGS_PCT= 25.7\n") != std::string::npos);
CHECK(text.find("Ice rings DETECTED: the 25.7% of the reflections") != std::string::npos);
CHECK(text.find("POWDER_RINGS ") == std::string::npos);
CHECK(text.find("\nPATHOLOGY_FLAGS= NONE\n") != std::string::npos);
// Most of the spots on rings is a condition.
result.powder.spot_fraction = 0.74f;
text = RenderResultReport("p", "in.h5", x, result);
CHECK(text.find("\nPATHOLOGY_FLAGS= POWDER_RINGS\n") != std::string::npos);
}
TEST_CASE("ResultReport_SweepGapsMaterial", "[Diagnostics]") {
DiffractionExperiment x(DetJF(1));
ProcessResult result;
result.consensus_cell = UnitCell{.a = 79.0f, .b = 79.0f, .c = 38.0f,
.alpha = 90.0f, .beta = 90.0f, .gamma = 90.0f};
result.has_merge_statistics = true;
result.merge_statistics.sweep_quality = TestSweepQuality();
// Two frames of a long sweep: in the table and the summary, not a condition of the data set.
result.merge_statistics.sweep_quality.sweep_deg = 360.0f;
result.merge_statistics.sweep_quality.ranges.resize(1);
result.merge_statistics.sweep_quality.ranges[0].rotation_deg = 1.0f;
auto text = RenderResultReport("p", "in.h5", x, result);
CHECK(text.find("SWEEP_GAPS") == std::string::npos);
CHECK(text.find("Frames 100-149") == std::string::npos);
RunProvenance dev;
dev.developer = true;
CHECK(RenderResultReport("p", "in.h5", x, result, dev).find("Frames 100-149") != std::string::npos);
result.merge_statistics.sweep_quality.ranges[0].rotation_deg = 40.0f;
text = RenderResultReport("p", "in.h5", x, result);
CHECK(text.find("\nPATHOLOGY_FLAGS= SWEEP_GAPS\n") != std::string::npos);
}
TEST_CASE("ResultReport_SecondCrystal", "[Diagnostics]") {
DiffractionExperiment x(DetJF(1));
ProcessResult result;
result.consensus_cell = UnitCell{.a = 79.0f, .b = 79.0f, .c = 38.0f,
.alpha = 90.0f, .beta = 90.0f, .gamma = 90.0f};
LeftoverLatticeCensus census;
census.primary_intensity_pct = 80.0;
census.lattices.push_back(LeftoverLattice{.kind = LeftoverLatticeKind::Domain, .cell = *result.consensus_cell,
.volume_ratio = 1.0, .misorientation_deg = 38.9, .z = 50.0,
.spots_pct = 12.0, .intensity_pct = 14.0, .sweep_concentration = 0.3});
result.leftover_lattices = census;
const auto text = RenderResultReport("p", "in.h5", x, result);
CHECK(text.find("second crystal, 38.9 deg from the main lattice") != std::string::npos);
CHECK(text.find("split crystal, ") == std::string::npos);
}