Build Packages / Create release (push) Successful in 26s
Build Packages / Build documentation (push) Successful in 1m43s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m7s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 11m48s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 7m22s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 12m24s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m30s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 25m29s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 29m41s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 21m44s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 24m41s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 26m9s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 26m3s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 28m28s
Build Packages / Generate python client (push) Successful in 27s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 31m16s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 20m44s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 26m27s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 22m52s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 27m44s
Build Packages / Unit tests (push) Successful in 1h46m55s
Build Packages / Validation Battery (push) Successful in 52m50s
* Rugnux: Several rotation sweeps of one crystal are merged as one dataset (`rugnux a_master.h5 b_master.h5`), on a common indexing and cell, with a non-isomorphous sweep left out and reported; `--sweeps separate` processes each sweep on its own. Bruker `.sfrm` frames and imgCIF files with a full goniometer axis chain are read. * Rugnux: More robust rotation processing - a retry with a more sensitive spot finder when no lattice is found, a self-consistent detector distance, long axes and doubled cells tested as hypotheses, a `-C` supercell multiple stepped down, and each ice ring merged, down-weighted or rejected on its own. * Rugnux: Serial stills are merged with a partiality-aware error model, reported as `ERROR_MODEL_A/B/C` with no ISa or R_merge/R_meas/R_pim; runs without `-S` have an overall intensity scale about 0.86-0.88 times that of earlier versions, with unchanged statistics. A second crystal form is left out of the merge and reported, the indexing ambiguity is resolved more reliably, and `--model` no longer influences processing. * Rugnux: The reported CC1/2 is the standard unweighted one, which can be lower than before on sweeps with weak frames; ISa comes with a 95% interval (`ISA_LOW`/`ISA_HIGH`). * Rugnux: Model refinement adds anisotropic/TLS, local NCS and twin refinement where R-free supports them; building waters and relabelling ions is now off by default (`--refine-solvent` turns it on). * NXmx: A detector of several modules is written as `module_0` ... `module_N` only, so DIALS reads each module as its own panel. * Other: JPEG previews use a bundled encoder (libjpeg-turbo no longer needed), Rugnux prints how to cite it, and the release archives contain the HTML documentation. Reviewed-on: #92 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
273 lines
12 KiB
C++
273 lines
12 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "RefinementNcs.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <set>
|
|
#include <unordered_map>
|
|
|
|
#include "gemmi/polyheur.hpp" // check_polymer_type
|
|
#include "gemmi/qcp.hpp" // superpose_positions
|
|
#include "gemmi/seqalign.hpp" // align_string_sequences
|
|
|
|
#include "RefinementMath.h"
|
|
|
|
namespace {
|
|
|
|
// The distances restrained: those shorter than this in either copy, the bonded and angle-related pairs left out.
|
|
// Following Nicholls, Long & Murshudov (2012) Acta Cryst. D68, 404-417
|
|
constexpr double NCS_DISTANCE_MAX = 4.2; // A
|
|
// Where a pair's restraint lets go (NcsDistanceTerm): copies whose distance differs by this much are taken to differ.
|
|
constexpr float NCS_RELEASE = 0.5f; // A
|
|
// Copies: more than this many residues aligned, at this identity at least (of the longer chain).
|
|
// Following Murshudov et al. (2011) Acta Cryst. D67, 355-367
|
|
constexpr int NCS_MIN_ALIGNED = 15;
|
|
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.
|
|
struct NcsChain {
|
|
std::string name;
|
|
gemmi::PolymerType type = gemmi::PolymerType::Unknown;
|
|
std::vector<std::string> sequence;
|
|
std::vector<int> residue; // numbered over the model
|
|
std::vector<std::map<std::string, int>> atoms; // name -> atom index
|
|
std::vector<int> anchor; // CA or P, -1 for none
|
|
};
|
|
|
|
// Two copies' corresponding atoms (aligned residues of the same name, atoms of the same name), the residue of each in
|
|
// the first copy, and their anchors.
|
|
void Correspond(const NcsChain &a, const NcsChain &b, std::vector<std::array<int, 2>> &atoms,
|
|
std::vector<int> &residue_of, std::vector<std::array<int, 2>> &anchors) {
|
|
const gemmi::AlignmentResult al = gemmi::align_string_sequences(a.sequence, b.sequence, {},
|
|
gemmi::AlignmentScoring::simple());
|
|
size_t i = 0, j = 0;
|
|
for (const gemmi::AlignmentResult::Item &item : al.cigar)
|
|
for (uint32_t k = 0; k < item.len(); k++) {
|
|
const char op = item.op();
|
|
if (op == 'M' && a.sequence[i] == b.sequence[j]) {
|
|
for (const auto &[name, ia] : a.atoms[i]) {
|
|
auto it = b.atoms[j].find(name);
|
|
if (it != b.atoms[j].end()) {
|
|
atoms.push_back({ia, it->second});
|
|
residue_of.push_back(a.residue[i]);
|
|
}
|
|
}
|
|
if (a.anchor[i] >= 0 && b.anchor[j] >= 0)
|
|
anchors.push_back({a.anchor[i], b.anchor[j]});
|
|
}
|
|
if (op == 'M' || op == 'I')
|
|
i++;
|
|
if (op == 'M' || op == 'D')
|
|
j++;
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
return none;
|
|
const gemmi::AlignmentResult al = gemmi::align_string_sequences(a.sequence, b.sequence, {},
|
|
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
|
|
// its four letters matched
|
|
const size_t longer = std::max(a.sequence.size(), b.sequence.size());
|
|
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.
|
|
std::set<std::pair<int, int>> PairsWithin(const std::vector<int> &atoms, const std::vector<gemmi::Position> &pos,
|
|
double reach) {
|
|
std::map<std::array<int, 3>, std::vector<int>> cells; // cell -> positions in `atoms`
|
|
auto cell_of = [&](const gemmi::Position &p) {
|
|
return std::array<int, 3>{static_cast<int>(std::floor(p.x / reach)), static_cast<int>(std::floor(p.y / reach)),
|
|
static_cast<int>(std::floor(p.z / reach))};
|
|
};
|
|
for (size_t k = 0; k < atoms.size(); k++)
|
|
cells[cell_of(pos[atoms[k]])].push_back(static_cast<int>(k));
|
|
std::set<std::pair<int, int>> out;
|
|
for (size_t k = 0; k < atoms.size(); k++) {
|
|
const std::array<int, 3> c = cell_of(pos[atoms[k]]);
|
|
for (int dx = -1; dx <= 1; dx++)
|
|
for (int dy = -1; dy <= 1; dy++)
|
|
for (int dz = -1; dz <= 1; dz++) {
|
|
auto it = cells.find({c[0] + dx, c[1] + dy, c[2] + dz});
|
|
if (it == cells.end())
|
|
continue;
|
|
for (int l : it->second)
|
|
if (atoms[l] > atoms[k] && pos[atoms[k]].dist(pos[atoms[l]]) < reach)
|
|
out.insert({atoms[k], atoms[l]});
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
NcsCopies FindNcsCopies(const gemmi::Model &model, const RefinementTopology &topo) {
|
|
NcsCopies out;
|
|
std::vector<gemmi::Position> pos;
|
|
std::vector<NcsChain> chains;
|
|
int atom = 0, residue = 0;
|
|
for (const gemmi::Chain &ch : model.chains) {
|
|
NcsChain c;
|
|
c.name = ch.name;
|
|
const gemmi::ConstResidueSpan polymer = ch.get_polymer();
|
|
c.type = gemmi::check_polymer_type(polymer);
|
|
const bool na = gemmi::is_polynucleotide(c.type);
|
|
for (const gemmi::Residue &r : ch.residues) {
|
|
const bool in_polymer = !polymer.empty() && &r >= &polymer.front() && &r <= &polymer.back();
|
|
std::map<std::string, int> names;
|
|
int anchor = -1;
|
|
for (const gemmi::Atom &a : r.atoms) {
|
|
pos.push_back(a.pos);
|
|
if (in_polymer && !a.is_hydrogen() && a.altloc == '\0' && a.occ > 0 && topo.refinable[atom]) {
|
|
names[a.name] = atom;
|
|
if (a.name == (na ? "P" : "CA"))
|
|
anchor = atom;
|
|
}
|
|
atom++;
|
|
}
|
|
if (in_polymer) {
|
|
c.sequence.push_back(r.name);
|
|
c.residue.push_back(residue);
|
|
c.atoms.push_back(std::move(names));
|
|
c.anchor.push_back(anchor);
|
|
}
|
|
residue++;
|
|
}
|
|
if (gemmi::is_polypeptide(c.type) || gemmi::is_polynucleotide(c.type))
|
|
chains.push_back(std::move(c));
|
|
}
|
|
|
|
// The molecules: each chain joins the first group whose first chain it is a copy of.
|
|
std::vector<std::vector<int>> groups;
|
|
for (size_t k = 0; k < chains.size(); k++) {
|
|
bool placed = false;
|
|
for (std::vector<int> &g : groups)
|
|
if (CopyRmsd(chains[g[0]], chains[k], pos) <= NCS_MAX_RMSD) {
|
|
g.push_back(static_cast<int>(k));
|
|
placed = true;
|
|
break;
|
|
}
|
|
if (!placed)
|
|
groups.push_back({static_cast<int>(k)});
|
|
}
|
|
// The distances the dictionary fixes: bonded atoms and those bonded to a common atom.
|
|
std::set<std::pair<int, int>> fixed;
|
|
for (const RestraintRecord &r : topo.geometry.records)
|
|
if (r.kind == static_cast<int>(RestraintKind::Bond))
|
|
fixed.insert(std::minmax(r.atom[0], r.atom[1]));
|
|
else if (r.kind == static_cast<int>(RestraintKind::Angle))
|
|
fixed.insert(std::minmax(r.atom[0], r.atom[2]));
|
|
|
|
for (const std::vector<int> &g : groups) {
|
|
if (g.size() < 2)
|
|
continue;
|
|
out.groups++;
|
|
out.copies += static_cast<int>(g.size());
|
|
std::string names;
|
|
for (int k : g)
|
|
names += (names.empty() ? "" : "=") + chains[k].name;
|
|
out.description += (out.description.empty() ? "" : " ") + names;
|
|
// each copy with the next, and the last with the first where that is a pair not made already
|
|
const size_t n_pairs = g.size() == 2 ? 1 : g.size();
|
|
for (size_t p = 0; p < n_pairs; p++) {
|
|
const NcsChain &a = chains[g[p]], &b = chains[g[(p + 1) % g.size()]];
|
|
std::vector<std::array<int, 2>> corr, anchors;
|
|
std::vector<int> residue_of;
|
|
Correspond(a, b, corr, residue_of, anchors);
|
|
out.anchors.push_back(std::move(anchors));
|
|
std::unordered_map<int, int> to_b, res_of;
|
|
std::vector<int> atoms_a, atoms_b;
|
|
for (size_t k = 0; k < corr.size(); k++) {
|
|
to_b[corr[k][0]] = corr[k][1];
|
|
res_of[corr[k][0]] = residue_of[k];
|
|
atoms_a.push_back(corr[k][0]);
|
|
atoms_b.push_back(corr[k][1]);
|
|
}
|
|
std::set<std::pair<int, int>> pairs = PairsWithin(atoms_a, pos, NCS_DISTANCE_MAX);
|
|
std::unordered_map<int, int> to_a;
|
|
for (const auto &[ia, ib] : to_b)
|
|
to_a[ib] = ia;
|
|
for (const auto &[i, j] : PairsWithin(atoms_b, pos, NCS_DISTANCE_MAX))
|
|
pairs.insert(std::minmax(to_a[i], to_a[j]));
|
|
for (const auto &[i, j] : pairs) {
|
|
if (fixed.count({i, j}) > 0)
|
|
continue;
|
|
RestraintRecord r;
|
|
r.kind = static_cast<int>(RestraintKind::NcsDistance);
|
|
r.atom[0] = i;
|
|
r.atom[1] = j;
|
|
r.atom[2] = to_b[i];
|
|
r.atom[3] = to_b[j];
|
|
r.ideal = NCS_RELEASE;
|
|
r.sigma = 1.0f;
|
|
out.distances.Add(r, 4);
|
|
out.record_residue.push_back(res_of[i]);
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
RestraintSet NcsRestraintsAt(const NcsCopies &ncs, double sigma) {
|
|
RestraintSet set = ncs.distances;
|
|
for (RestraintRecord &r : set.records)
|
|
r.sigma = static_cast<float>(sigma);
|
|
return set;
|
|
}
|
|
|
|
double NcsRmsd(const NcsCopies &ncs, const std::vector<std::array<double, 4>> &xyzb) {
|
|
double sum = 0;
|
|
int n = 0;
|
|
for (const std::vector<std::array<int, 2>> &anchors : ncs.anchors) {
|
|
if (anchors.size() < 3)
|
|
continue;
|
|
std::vector<gemmi::Position> pa, pb;
|
|
for (const std::array<int, 2> &p : anchors) {
|
|
pa.emplace_back(xyzb[p[0]][0], xyzb[p[0]][1], xyzb[p[0]][2]);
|
|
pb.emplace_back(xyzb[p[1]][0], xyzb[p[1]][1], xyzb[p[1]][2]);
|
|
}
|
|
sum += gemmi::superpose_positions(pa.data(), pb.data(), pa.size(), nullptr).rmsd;
|
|
n++;
|
|
}
|
|
return n > 0 ? sum / n : NAN;
|
|
}
|
|
|
|
int NcsReleasedResidues(const NcsCopies &ncs, const std::vector<std::array<double, 4>> &xyzb) {
|
|
std::map<int, std::array<int, 2>> count; // residue -> restraints released, all
|
|
for (size_t k = 0; k < ncs.distances.records.size(); k++) {
|
|
const RestraintRecord &r = ncs.distances.records[k];
|
|
const double delta = RestraintValue(ncs.distances, r, xyzb);
|
|
std::array<int, 2> &c = count[ncs.record_residue[k]];
|
|
c[0] += std::fabs(delta) > r.ideal;
|
|
c[1]++;
|
|
}
|
|
int released = 0;
|
|
for (const auto &[res, c] : count)
|
|
released += 2 * c[0] > c[1];
|
|
return released;
|
|
}
|