Indexing: a coplanar cell leaves the frame unindexed instead of throwing
The online broker cancelled a collection with "Crystal lattice is coplanar and has no reciprocal cell". The candidate filter in PostIndexingRefinement (used by FFT, FFTW and ffbidx) tests lengths, angles and spot count only; three rows 120 deg apart in one plane pass all of them, and spots along the plane normal index as (0,0,0). Such a candidate - from ffbidx, or one the least-squares refinement walked flat - went on to LatticeSearch and XtalOptimizer, which refuses it and leaves it in place, and then reached Astar() in AnalyzeIndexing/prediction, which throws; the receiver turns that into a cancelled acquisition. - Refine() rejects a candidate below MIN_BASIS_VOLUME_FRACTION, like any other bad candidate. - XtalOptimizerInternal builds the refined lattice first and counts a coplanar result as a failed refinement, writing nothing back (the residual's volume clamp lets a solve end there and report success). Test: PostIndexingRefinement_CoplanarCandidateIsRejected threw the exact message before the fix. rugnux p.mtz unchanged on the three in-house reference sets at --prepass-fraction 1. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
This commit is contained in:
@@ -785,3 +785,43 @@ TEST_CASE("FitSupercellProbe", "[SupercellProbe]") {
|
||||
// Too few reflections to fit a line: nothing.
|
||||
CHECK(FitSupercellProbe(SupercellProbeClass{}).b == 0.0);
|
||||
}
|
||||
|
||||
TEST_CASE("PostIndexingRefinement_CoplanarCandidateIsRejected", "[Indexing]") {
|
||||
// A candidate whose three rows lie in one plane passes every length and angle test - three rows
|
||||
// 120 deg apart in a plane meet the 30-150 deg window - and every spot along the plane normal
|
||||
// indexes as (0,0,0), so the spot count passes too. Such a cell has no reciprocal basis; handed
|
||||
// on, it made the online analysis throw "Crystal lattice is coplanar" for the frame instead of
|
||||
// leaving the frame unindexed.
|
||||
const Coord a(50, 0, 0);
|
||||
const Coord b(-25, 25.0f * std::sqrt(3.0f), 0);
|
||||
const Coord c(-25, -25.0f * std::sqrt(3.0f), 0);
|
||||
REQUIRE(CrystalLattice(a, b, c).VolumeFraction() < MIN_BASIS_VOLUME_FRACTION);
|
||||
|
||||
std::vector<Coord> spots;
|
||||
for (int i = 1; i <= 40; i++)
|
||||
spots.emplace_back(0.0f, 0.0f, 0.01f * static_cast<float>(i));
|
||||
|
||||
Eigen::MatrixX3<float> cells(3, 3);
|
||||
cells << a.x, a.y, a.z,
|
||||
b.x, b.y, b.z,
|
||||
c.x, c.y, c.z;
|
||||
// A score below the refinement's distance floor, so the candidate reaches the filters as given.
|
||||
Eigen::VectorX<float> scores(1);
|
||||
scores << -10.99f;
|
||||
|
||||
RefineParameters parameters{
|
||||
.viable_cell_min_spots = 9,
|
||||
.dist_tolerance_vs_reference = 0.05,
|
||||
.reference_unit_cell = std::nullopt,
|
||||
.min_length_A = 5,
|
||||
.max_length_A = 500,
|
||||
.min_angle_deg = 30,
|
||||
.max_angle_deg = 150,
|
||||
.indexing_tolerance = 0.1
|
||||
};
|
||||
|
||||
const auto lattices = Refine(spots, spots.size(), cells, scores, parameters);
|
||||
CHECK(lattices.empty());
|
||||
for (const auto &l: lattices)
|
||||
CHECK_NOTHROW(l.GetUBMatrix());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user