Space-group search: a screw threshold needs a row with controls behind it

The row-relative cut scales the "too strong to be absent" threshold by the axial
row's own median E^2, floored at the plain value - so it can only raise the bar,
and a row whose control class holds a single strong reflection sets it from that
one reflection. That direction invents screws: a genuine 4_2 whose 00l happen to
be observed only at l=4n reads its l=4n+2 reflections as absent and ranks
4_1/4_3 above the truth. Require three controls before the row may set the
scale; below that the row keeps the plain cut.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-30 10:52:32 +02:00
co-authored by Claude Opus 5
parent 72cd91ccc3
commit 1355d6b2aa
@@ -732,9 +732,16 @@ SearchSpaceGroupResult SearchSpaceGroup(
// control rows, and on the same crystal one row can be 20x an average reflection while another
// is half of one. Pooling them lets the weak rows set the threshold for the strong one and the
// rescue never fires (that very crystal pooled to a row median of 0.7 and stayed at P42_12).
// A row needs more than a reflection or two behind its median before that median may set the
// threshold. The scale only ever RAISES the cut, so a row whose control class holds one strong
// reflection would license a screw claim the row does not support - a genuine 4_2 whose 00l are
// observed only at l=4n, strongly, would read its l=4n+2 reflections as absent and be reported as
// 4_1/4_3. Below this count the row falls back to the plain cut, i.e. no rescue.
constexpr size_t MIN_ROW_CONTROL_REFLECTIONS = 3;
std::map<AxialRow, double> row_median;
for (auto& [row, esq] : row_present_esq)
row_median[row] = MedianOf(esq);
if (esq.size() >= MIN_ROW_CONTROL_REFLECTIONS)
row_median[row] = MedianOf(esq);
const int screw_absent = static_cast<int>(screw_absent_refl.size());
int screw_violations = 0;