Merge branch 'c-screw-undetermined' into pool-C
This commit is contained in:
@@ -484,6 +484,15 @@ RowModulation FindRowModulation(const gemmi::UnitCell& cell, const std::array<in
|
||||
return out;
|
||||
}
|
||||
|
||||
// The crystal axis a screw on this row would run along: h00 -> a, 0k0 -> b, 00l -> c. ' ' for a
|
||||
// row that is not a principal one, which no screw extinguishes.
|
||||
char AxisOf(std::array<int, 3> row) {
|
||||
if (row[1] == 0 && row[2] == 0) return 'a';
|
||||
if (row[0] == 0 && row[2] == 0) return 'b';
|
||||
if (row[0] == 0 && row[1] == 0) return 'c';
|
||||
return ' ';
|
||||
}
|
||||
|
||||
std::string FormatDouble(double v, int decimals) {
|
||||
std::ostringstream o;
|
||||
o << std::fixed << std::setprecision(decimals) << v;
|
||||
@@ -1039,6 +1048,17 @@ SearchSpaceGroupResult SearchSpaceGroup(
|
||||
row_modulation.emplace(row, std::move(m));
|
||||
}
|
||||
}
|
||||
// How many reflections of each principal axial row the merge holds at all. A screw lives on one
|
||||
// row, so a row the sweep never recorded - the spindle's blind cone points down it, or its
|
||||
// reflections fall outside the resolution range - is a screw nobody could have tested, and the
|
||||
// difference between "tested and found absent" and "never looked at" is invisible in every other
|
||||
// number here. Counted once over the merge, because it is a property of the data and not of a
|
||||
// candidate. Only the three principal rows: a screw extinguishes no other.
|
||||
std::map<AxialRow, int> axial_row_observed;
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
if (pass_absence[i] && (H[i] == 0) + (K[i] == 0) + (L[i] == 0) == 2)
|
||||
++axial_row_observed[RowOf(H[i], K[i], L[i])];
|
||||
|
||||
std::vector<double> tncs_expected(n, 1.0);
|
||||
for (size_t i = 0; i < n && !row_modulation.empty(); ++i) {
|
||||
if (!pass_absence[i] || absence_shell[i] < 0)
|
||||
@@ -2355,6 +2375,46 @@ SearchSpaceGroupResult SearchSpaceGroup(
|
||||
}
|
||||
}
|
||||
|
||||
// Which axis, if any, these data could not decide the screw on. The ambiguity itself is already
|
||||
// in `alternatives` - the selected set holds groups predicting different axial absences - but a
|
||||
// list of names does not say WHICH axis is open, nor why, and the scalar best_space_group reads
|
||||
// as a determination either way. So name it per row: a row is undetermined when two SELECTED
|
||||
// candidates disagree about whether it carries screw absences at all, which is exactly the case
|
||||
// where nothing in this merge separated them. An enantiomorphic or origin-ambiguous pair
|
||||
// (P4_1/P4_3, I222/I2_12_12_1) predicts the SAME absences on every row and is not named here -
|
||||
// that ambiguity is the hand, or the origin, and is reported as itself.
|
||||
if (result.best_space_group.has_value()) {
|
||||
for (const AxialRow& row : {RowOf(1, 0, 0), RowOf(0, 1, 0), RowOf(0, 0, 1)}) {
|
||||
const gemmi::Op::Miller hkl{{row[0], row[1], row[2]}};
|
||||
bool claimed = false, unclaimed = false;
|
||||
int n_absent = 0, n_control = 0;
|
||||
for (const auto& s : result.candidates) {
|
||||
if (!s.selected)
|
||||
continue;
|
||||
const gemmi::GroupOps g = s.space_group.operations();
|
||||
// The row's absences must come from a SCREW: in a centred group the axial condition
|
||||
// is the centring's own (I222 extinguishes h00 with h odd because h+k+l is odd), and
|
||||
// that is decided elsewhere, on the whole centring class.
|
||||
if (g.is_systematically_absent(hkl) && !CenteringAbsent(g, hkl)) {
|
||||
claimed = true;
|
||||
for (const auto& z : s.screw_zones)
|
||||
if (z.row == row) {
|
||||
n_absent = std::max(n_absent, z.n_absent);
|
||||
n_control = std::max(n_control, z.n_control);
|
||||
}
|
||||
} else {
|
||||
unclaimed = true;
|
||||
}
|
||||
}
|
||||
if (claimed && unclaimed) {
|
||||
const auto it = axial_row_observed.find(row);
|
||||
result.undetermined_screws.push_back(
|
||||
{row, RowLabel(row), AxisOf(row),
|
||||
it == axial_row_observed.end() ? 0 : it->second, n_absent, n_control});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Both answers, on every run. The winner above may be a non-Sohncke group, which is right for a
|
||||
// small molecule and impossible for a protein; a user who knows their sample is chiral has to be
|
||||
// able to read the Sohncke answer off the same run, and a user who is not sure has to be able to
|
||||
@@ -2647,6 +2707,31 @@ std::string SearchSpaceGroupResultToText(const SearchSpaceGroupResult& result,
|
||||
<< SettingName(*result.sohncke_space_group) << ".\n";
|
||||
}
|
||||
|
||||
// The axes the data left open, said in words. The zone table above is printed for the SELECTED
|
||||
// candidate only, and the selected candidate in exactly this case is the one with NO screw zones
|
||||
// - so without this the run's own account of an undecidable screw is a blank where the table
|
||||
// would have been.
|
||||
if (!result.undetermined_screws.empty()) {
|
||||
os << "Screw axes these data cannot decide\n";
|
||||
for (const auto& u : result.undetermined_screws) {
|
||||
os << " " << u.axis << ": ";
|
||||
if (u.n_observed == 0)
|
||||
os << "the " << u.row_label << " row holds no reflections in this merge at all, so "
|
||||
"nothing here\n could test a screw along " << u.axis
|
||||
<< " - the row was not recorded (it can lie in the spindle's\n blind cone, or "
|
||||
"outside the resolution range).\n";
|
||||
else if (u.n_control == 0)
|
||||
os << "the " << u.row_label << " row holds " << u.n_observed << " reflection(s) but no "
|
||||
"control class for its absences,\n so how weak they are cannot be judged.\n";
|
||||
else
|
||||
os << "the " << u.row_label << " row does not separate the candidates (" << u.n_absent
|
||||
<< " absent, " << u.n_control << " control).\n";
|
||||
}
|
||||
os << " A reflection file must carry ONE group, so the groups below are reported with the\n"
|
||||
" lowest-numbered - the one claiming no screw - standing for the set. That is a\n"
|
||||
" convention, not a measurement: on the axes named here the answer is all of them.\n";
|
||||
}
|
||||
|
||||
if (result.best_space_group.has_value()) {
|
||||
os << "Best space group: " << SettingName(*result.best_space_group);
|
||||
for (const auto& alt : result.alternatives)
|
||||
|
||||
@@ -723,6 +723,24 @@ struct SearchSpaceGroupResult {
|
||||
double r_over_best_bound = 0.0;
|
||||
double r_over_floor_bound = 0.0;
|
||||
|
||||
// An axial row whose SCREW these data could not decide: the selected group and at least one of
|
||||
// its `alternatives` disagree about whether that row carries screw absences, and nothing in this
|
||||
// merge separated them. The adopted group is then the lowest-numbered member of that set - the
|
||||
// symmorphic one, by convention and not by measurement - so a report printing only
|
||||
// best_space_group reads as a determination where the honest answer is "P2 or P2_1, no answer
|
||||
// possible". The written files still carry one group, because a reflection file cannot hold
|
||||
// "maybe a screw"; this is what lets the report, the log and a scorer say which axis is open.
|
||||
// Empty on the usual run, where every screw the data could show was judged.
|
||||
struct UndeterminedScrew {
|
||||
std::array<int, 3> row{}; // sign-canonicalised as everywhere else, e.g. {0,0,-1} for 00l
|
||||
std::string row_label; // the row as a crystallographer names it: "h00", "0k0", "00l"
|
||||
char axis = ' '; // the axis a screw would run along: 'a', 'b' or 'c'
|
||||
int n_observed = 0; // reflections of the row in this merge at all - 0 = never recorded
|
||||
int n_absent = 0; // of those, the ones a claiming candidate predicts absent
|
||||
int n_control = 0; // ...and the rest of the row, which an absence is judged against
|
||||
};
|
||||
std::vector<UndeterminedScrew> undetermined_screws;
|
||||
|
||||
// The best SOHNCKE candidate, always, even when a glide plane was confirmed and best_space_group
|
||||
// therefore names a non-Sohncke group. Both are reported on every run: a chiral (protein) crystal
|
||||
// cannot have a glide, so a user who knows their sample is a protein must be able to see the
|
||||
|
||||
Reference in New Issue
Block a user