Merge ncs-fix into rc181: NCS copies also within 2.5 A C-alpha rms after superposition

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQqN9qRD57vnShzTC37
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-10-28 03:36:53 +01:00
commit fee8ffacd9
5 files changed
+55 -20

No files matched your search

+2 -1
View File
@@ -631,7 +631,8 @@ what the data support rather than a rule by resolution: E. A. Merritt, "To B or
resolution?" (2012), Acta Cryst. D68, 468-477 [doi:10.1107/S0907444911028320](https://doi.org/10.1107/S0907444911028320). resolution?" (2012), Acta Cryst. D68, 468-477 [doi:10.1107/S0907444911028320](https://doi.org/10.1107/S0907444911028320).
**NCS restraints** — copies of a molecule in the model are found by sequence alignment with REFMAC5's criterion **NCS restraints** — copies of a molecule in the model are found by sequence alignment with REFMAC5's criterion
(more than 15 residues aligned, 80% identity - here of the longer chain; Murshudov et al. 2011, above) and restrained locally, on pairs of (more than 15 residues aligned, 80% identity - here of the longer chain - and 2.5 Å rms after superposition;
Murshudov et al. 2011, above) and restrained locally, on pairs of
corresponding interatomic distances under the Geman-McClure robust loss, as REFMAC5's local NCS restraints are, with corresponding interatomic distances under the Geman-McClure robust loss, as REFMAC5's local NCS restraints are, with
the distance range of R. A. Nicholls, F. Long and G. N. Murshudov, "Low-resolution refinement tools in REFMAC5" the distance range of R. A. Nicholls, F. Long and G. N. Murshudov, "Low-resolution refinement tools in REFMAC5"
(2012), Acta Cryst. D68, 404-417 [doi:10.1107/S090744491105606X](https://doi.org/10.1107/S090744491105606X), and (2012), Acta Cryst. D68, 404-417 [doi:10.1107/S090744491105606X](https://doi.org/10.1107/S090744491105606X), and
+7 -5
View File
@@ -236,11 +236,13 @@ the ceiling of §15.1.
**NCS restraints.** Where the model holds several copies of one molecule, they are found from the model alone: two **NCS restraints.** Where the model holds several copies of one molecule, they are found from the model alone: two
polymer chains of one kind (protein, or nucleic acid) are copies where their sequences align with more than 15 polymer chains of one kind (protein, or nucleic acid) are copies where their sequences align with more than 15
residues identical and identical in 80% of the longer chain or more - REFMAC5's criterion, taken on the longer chain. In the residues aligned to a residue of the same name, the residues identical and identical in 80% of the longer chain or more, and their C-alpha (P) atoms lie within 2.5 Å
atoms of the same name correspond - not a hydrogen, not an alternative conformation, both refinable. Each copy is rms of each other after superposition - REFMAC5's criterion, the identity taken on the longer chain. The same
paired with the next of its molecule (and the last with the first, where there are three or more), and each pair is sequence in a conformation further away than that, such as a quasi-equivalent subunit of a virus capsid, is another
restrained **locally**, on its interatomic distances rather than on a superposition: every distance within a chain molecule. In the residues aligned to a residue of the same name, the atoms of the same name correspond - not a
shorter than 4.2 Å in either copy, except those a bond or an angle of the dictionary already fixes, against the same hydrogen, not an alternative conformation, both refinable. Each copy is paired with the next of its molecule (and the
last with the first, where there are three or more), and each pair is restrained **locally**, on its interatomic
distances rather than on a superposition: every distance within a chain shorter than 4.2 Å in either copy, except those a bond or an angle of the dictionary already fixes, against the same
distance in the other copy, with the robust loss of Geman and McClure, distance in the other copy, with the robust loss of Geman and McClure,
$$ $$
@@ -5,6 +5,7 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <limits>
#include <map> #include <map>
#include <set> #include <set>
#include <unordered_map> #include <unordered_map>
@@ -26,6 +27,10 @@ constexpr float NCS_RELEASE = 0.5f; // A
// Following Murshudov et al. (2011) Acta Cryst. D67, 355-367 // Following Murshudov et al. (2011) Acta Cryst. D67, 355-367
constexpr int NCS_MIN_ALIGNED = 15; constexpr int NCS_MIN_ALIGNED = 15;
constexpr double NCS_MIN_IDENTITY = 80.0; // % constexpr double NCS_MIN_IDENTITY = 80.0; // %
// and their C-alpha (P) atoms no further apart than this after superposition: the same sequence further away (a
// quasi-equivalent subunit of a virus capsid) is another molecule.
// Following Murshudov et al. (2011) Acta Cryst. D67, 355-367
constexpr double NCS_MAX_RMSD = 2.5; // A
// One polymer chain: its residues and, per residue, its atoms' indices by name. // One polymer chain: its residues and, per residue, its atoms' indices by name.
struct NcsChain { struct NcsChain {
@@ -65,15 +70,30 @@ void Correspond(const NcsChain &a, const NcsChain &b, std::vector<std::array<int
} }
} }
bool AreCopies(const NcsChain &a, const NcsChain &b) { // The C-alpha (P) rmsd of two chains after superposition where their sequences make them copies of one molecule,
// infinity where they do not.
double CopyRmsd(const NcsChain &a, const NcsChain &b, const std::vector<gemmi::Position> &pos) {
const double none = std::numeric_limits<double>::infinity();
if (a.type != b.type || a.sequence.size() <= NCS_MIN_ALIGNED || b.sequence.size() <= NCS_MIN_ALIGNED) if (a.type != b.type || a.sequence.size() <= NCS_MIN_ALIGNED || b.sequence.size() <= NCS_MIN_ALIGNED)
return false; return none;
const gemmi::AlignmentResult al = gemmi::align_string_sequences(a.sequence, b.sequence, {}, const gemmi::AlignmentResult al = gemmi::align_string_sequences(a.sequence, b.sequence, {},
gemmi::AlignmentScoring::simple()); gemmi::AlignmentScoring::simple());
// identity over the longer chain: over the shorter one, a short RNA aligned with gaps into a long one finds most of // identity over the longer chain: over the shorter one, a short RNA aligned with gaps into a long one finds most of
// its four letters matched // its four letters matched
const size_t longer = std::max(a.sequence.size(), b.sequence.size()); const size_t longer = std::max(a.sequence.size(), b.sequence.size());
return al.match_count > NCS_MIN_ALIGNED && 100.0 * al.match_count >= NCS_MIN_IDENTITY * double(longer); if (al.match_count <= NCS_MIN_ALIGNED || 100.0 * al.match_count < NCS_MIN_IDENTITY * double(longer))
return none;
std::vector<std::array<int, 2>> atoms, anchors;
std::vector<int> residue_of;
Correspond(a, b, atoms, residue_of, anchors);
if (anchors.size() < 3)
return none;
std::vector<gemmi::Position> pa, pb;
for (const std::array<int, 2> &p : anchors) {
pa.push_back(pos[p[0]]);
pb.push_back(pos[p[1]]);
}
return gemmi::superpose_positions(pa.data(), pb.data(), pa.size(), nullptr).rmsd;
} }
// The pairs (i, j), i < j, of `atoms` closer than `reach` at `pos`, by a grid of cells of that size. // The pairs (i, j), i < j, of `atoms` closer than `reach` at `pos`, by a grid of cells of that size.
@@ -146,7 +166,7 @@ NcsCopies FindNcsCopies(const gemmi::Model &model, const RefinementTopology &top
for (size_t k = 0; k < chains.size(); k++) { for (size_t k = 0; k < chains.size(); k++) {
bool placed = false; bool placed = false;
for (std::vector<int> &g : groups) for (std::vector<int> &g : groups)
if (AreCopies(chains[g[0]], chains[k])) { if (CopyRmsd(chains[g[0]], chains[k], pos) <= NCS_MAX_RMSD) {
g.push_back(static_cast<int>(k)); g.push_back(static_cast<int>(k));
placed = true; placed = true;
break; break;
@@ -154,7 +174,6 @@ NcsCopies FindNcsCopies(const gemmi::Model &model, const RefinementTopology &top
if (!placed) if (!placed)
groups.push_back({static_cast<int>(k)}); groups.push_back({static_cast<int>(k)});
} }
// The distances the dictionary fixes: bonded atoms and those bonded to a common atom. // The distances the dictionary fixes: bonded atoms and those bonded to a common atom.
std::set<std::pair<int, int>> fixed; std::set<std::pair<int, int>> fixed;
for (const RestraintRecord &r : topo.geometry.records) for (const RestraintRecord &r : topo.geometry.records)
@@ -30,13 +30,15 @@ struct NcsCopies {
}; };
// The copies of `model` (whose entities are set up, as BuildRefinementTopology leaves them): polymer chains whose // The copies of `model` (whose entities are set up, as BuildRefinementTopology leaves them): polymer chains whose
// sequences align with more than 15 residues identical, and identical in 80% of the longer chain's residues, are // sequences align with more than 15 residues identical, identical in 80% of the longer chain's residues, and whose
// copies of one molecule (REFMAC's criterion, taken on the longer chain). Their residues of the same name aligned to // C-alpha (P) atoms are within 2.5 A rms of each other after superposition are copies of one molecule (REFMAC's
// each other correspond, and in them the atoms of the same name - neither a hydrogen nor an alternative conformation, // criterion, the identity taken on the longer chain); the same sequence in a conformation further away - a
// all refinable (topo.refinable). Each copy is paired with the next of its molecule (the last with the first, where // quasi-equivalent subunit of a virus capsid - is another molecule. Their residues of the same name aligned to each
// there are three or more), and each pair restrained on the distances within a chain shorter than NCS_DISTANCE_MAX in // other correspond, and in them the atoms of the same name - neither a hydrogen nor an alternative conformation, all
// either copy, those the dictionary's bonds and angles fix excepted. Atoms are numbered in model order, as // refinable (topo.refinable). Each copy is paired with the next of its molecule (the last with the first, where there
// ModelPositions() lists them. // are three or more), and each pair restrained on the distances within a chain shorter than NCS_DISTANCE_MAX in either
// copy, those the dictionary's bonds and angles fix excepted. Atoms are numbered in model order, as ModelPositions()
// lists them.
NcsCopies FindNcsCopies(const gemmi::Model &model, const RefinementTopology &topo); NcsCopies FindNcsCopies(const gemmi::Model &model, const RefinementTopology &topo);
// The restraints at `sigma` (A). // The restraints at `sigma` (A).
+13 -2
View File
@@ -997,8 +997,8 @@ TEST_CASE("Refinement_ion_form_factors", "[Refinement]") {
} }
} }
// Two copies of one RNA chain in the asymmetric unit, the second turned and moved, and a fragment of it: the two found // Two copies of one RNA chain in the asymmetric unit, the second turned and moved, a fragment of it and the chain in
// as copies of one molecule, the fragment not, held // another conformation: the two found as copies of one molecule, the fragment and the other conformation not, held
// to each other by local distance restraints whose gradient is right on the CPU and the GPU, and let go where the // to each other by local distance restraints whose gradient is right on the CPU and the GPU, and let go where the
// copies really differ. A single copy has none. // copies really differ. A single copy has none.
TEST_CASE("RefinementNcs_two_copies", "[Refinement]") { TEST_CASE("RefinementNcs_two_copies", "[Refinement]") {
@@ -1029,6 +1029,17 @@ TEST_CASE("RefinementNcs_two_copies", "[Refinement]") {
for (gemmi::Atom &at : r.atoms) for (gemmi::Atom &at : r.atoms)
at.pos += gemmi::Position(-30, 40, 20); at.pos += gemmi::Position(-30, 40, 20);
st.models[0].chains.push_back(fragment); st.models[0].chains.push_back(fragment);
// and the whole molecule in another conformation, each residue moved 3 A its own way: the same sequence, not a copy
gemmi::Chain other = st.models[0].chains.at(0);
other.name = "D";
std::mt19937 shift_rng(11);
std::uniform_real_distribution<double> shift(-1.0, 1.0);
for (gemmi::Residue &r : other.residues) {
const gemmi::Vec3 v = gemmi::Vec3(shift(shift_rng), shift(shift_rng), shift(shift_rng)).normalized() * 3.0;
for (gemmi::Atom &at : r.atoms)
at.pos += gemmi::Position(v) + gemmi::Position(30, -40, 25);
}
st.models[0].chains.push_back(other);
st.connections.clear(); // the LINK records name chain A only st.connections.clear(); // the LINK records name chain A only
const RefinementTopology topo = BuildRefinementTopology(st); const RefinementTopology topo = BuildRefinementTopology(st);
const NcsCopies ncs = FindNcsCopies(st.models[0], topo); const NcsCopies ncs = FindNcsCopies(st.models[0], topo);