Files
leonarski_f 63f5f96132
Build Packages / Create release (push) Successful in 9s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m19s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 10m26s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 19m33s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 23m17s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 23m28s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 24m38s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 27m30s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 29m4s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 29m19s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 29m31s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 23m19s
Build Packages / Generate python client (push) Successful in 28s
Build Packages / Build documentation (push) Successful in 1m22s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 24m39s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 29m40s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 28m31s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 24m33s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 27m45s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 29m25s
Build Packages / Unit tests (push) Successful in 1h35m24s
Build Packages / Validation Battery (push) Successful in 35m45s
v1.0.0-rc.180 (#91)
* Rugnux: A placed model is refined by default - restrained maximum-likelihood refinement on the GPU (CPU without one), writing `<prefix>_refined_model.cif`, `<prefix>_refined_maps.mtz` and the difference-map blobs in `<prefix>_blobs.json`; `--no-refine` skips it.
* Rugnux: `--mode refine` refines a model against merged structure factors from an MTZ or mmCIF file.
* Rugnux: The embedded restraint dictionary covers amino acids, nucleotides including common modified RNA nucleotides, common cofactors, crystallisation additives, waters and ions, with metal sites restrained from the model's LINK records.
* Rugnux: A model or reference file can be given among the inputs in any order, recognised by its contents.
* Rugnux: The geometry refinement algorithm option is now `-r, --geom-refine` (was `--refine`).
* Rugnux: Serial stills resolve the indexing ambiguity from the data when no model or reference is given, reject merge outliers by default, follow the run's measured rocking curve in prediction and partiality (also in `--mode scale`), and index reproducibly on the GPU.
* Rugnux: Reflections touching the beam-stop shadow are not integrated, reflections coarser than the low-resolution limit are merged and written (but kept out of the scale fit), and the polarisation plane is measured more reliably.
* Rugnux: Small-molecule runs report unit-cell e.s.d.s, in a SHELX `.ins` beside the `.hkl` and in the mmCIF.
* jfjoch_broker: Live Rugnux refines the uploaded model after placement; the web frontend's Rugnux live page has a hero panel with resolution, ISa, space group, placement and refined R-work/R-free, and its Mol* viewer shows the refined model and maps.
* Documentation: A new front page with download pages for Rugnux and the viewer, and the technical reference reorganised below it.
* Build: The fast-feedback indexer is part of the source tree instead of being downloaded at build time.

Reviewed-on: #91
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-10-27 13:18:42 +01:00

1085 lines
43 KiB
C++

// Copyright 2018-2022 Global Phasing Ltd.
#include <gemmi/topo.hpp>
#include <cmath> // for sqrt, round
#include <map> // for multimap
#include <unordered_map> // for unordered_multimap
#include <gemmi/polyheur.hpp> // for get_or_check_polymer_type, ...
#include <gemmi/riding_h.hpp> // for place_hydrogens_on_all_atoms, ...
#include <gemmi/modify.hpp> // for remove_hydrogens
namespace gemmi {
std::unique_ptr<ChemComp> make_chemcomp_with_restraints(const Residue& res) {
std::unique_ptr<ChemComp> cc(new ChemComp());
cc->name = res.name;
cc->group = ChemComp::Group::Null;
cc->has_coordinates = true;
// add atoms
cc->atoms.reserve(res.atoms.size());
for (const Atom& a : res.atoms) {
if (a.altloc != '\0' && cc->has_atom(a.name))
continue;
Element el = a.element;
if (el == El::X)
el = El::N;
if (el == El::D)
el = El::H;
const std::string& chem_type = el.uname();
cc->atoms.push_back(ChemComp::Atom{a.name, "", el, float(a.charge), chem_type, "", a.pos});
}
// prepare pairs of atoms
struct Pair {
size_t n1, n2;
double dist;
};
std::vector<Pair> pairs;
// first heavy atoms only
for (size_t i = 0; i != res.atoms.size(); ++i) {
const Atom& at1 = res.atoms[i];
if (at1.is_hydrogen())
continue;
float r1 = at1.element.covalent_r();
for (size_t j = i+1; j != res.atoms.size(); ++j) {
const Atom& at2 = res.atoms[j];
if (at2.is_hydrogen() || !at2.same_conformer(at1))
continue;
double d2 = at1.pos.dist_sq(at2.pos);
float r2 = at2.element.covalent_r();
double dmax = std::max(2.0, 1.3 * std::max(r1, r2));
if (d2 < sq(dmax))
pairs.push_back(Pair{i, j, std::sqrt(d2)});
}
}
// now each hydrogen with the nearest heavy atom
for (size_t i = 0; i != res.atoms.size(); ++i) {
const Atom& at1 = res.atoms[i];
if (at1.is_hydrogen()) {
size_t nearest = (size_t)-1;
double min_d2 = sq(2.5);
for (size_t j = 0; j != res.atoms.size(); ++j) {
const Atom& at2 = res.atoms[j];
if (!at2.is_hydrogen() && at2.same_conformer(at1)) {
double d2 = at1.pos.dist_sq(at2.pos);
if (d2 < min_d2) {
min_d2 = d2;
nearest = j;
}
}
}
if (nearest != (size_t)-1) {
pairs.push_back(Pair{nearest, i, std::sqrt(min_d2)});
}
}
}
// add bonds
// note: duplicated bonds (from different conformations) are not a problem
for (const Pair& p : pairs) {
Restraints::Bond bond;
bond.id1 = Restraints::AtomId{1, res.atoms[p.n1].name};
bond.id2 = Restraints::AtomId{1, res.atoms[p.n2].name};
bond.type = BondType::Unspec;
bond.aromatic = false;
double rounded_dist = 0.001 * std::round(1000 * p.dist);
bond.value = bond.value_nucleus = rounded_dist;
bond.esd = bond.esd_nucleus = 0.02;
cc->rt.bonds.push_back(bond);
}
// add angles
struct Triple {
size_t n1, n2, n3;
};
std::vector<Triple> triples;
for (size_t i = 0; i != pairs.size(); ++i)
for (size_t j = i+1; j != pairs.size(); ++j) {
if (pairs[i].n1 == pairs[j].n1)
triples.push_back(Triple{pairs[i].n2, pairs[i].n1, pairs[j].n2});
else if (pairs[i].n1 == pairs[j].n2)
triples.push_back(Triple{pairs[i].n2, pairs[i].n1, pairs[j].n1});
else if (pairs[i].n2 == pairs[j].n1)
triples.push_back(Triple{pairs[i].n1, pairs[i].n2, pairs[j].n2});
else if (pairs[i].n2 == pairs[j].n2)
triples.push_back(Triple{pairs[i].n1, pairs[i].n2, pairs[j].n1});
}
for (const Triple& triple : triples) {
Restraints::Angle angle;
angle.id1 = Restraints::AtomId{1, res.atoms[triple.n1].name};
angle.id2 = Restraints::AtomId{1, res.atoms[triple.n2].name};
angle.id3 = Restraints::AtomId{1, res.atoms[triple.n3].name};
double angle_rad = calculate_angle(res.atoms[triple.n1].pos,
res.atoms[triple.n2].pos,
res.atoms[triple.n3].pos);
angle.value = 0.01 * std::round(100 * deg(angle_rad));
angle.esd = 3.0;
cc->rt.angles.push_back(angle);
}
return cc;
}
Topo::ChainInfo::ChainInfo(ResidueSpan& subchain, const Chain& chain, const Entity* ent)
: chain_ref(chain) {
subchain_name = subchain.at(0).subchain;
res_infos.reserve(subchain.size());
if (ent) {
entity_id = ent->name;
polymer = ent->entity_type == EntityType::Polymer;
polymer_type = get_or_check_polymer_type(ent, subchain);
} else {
polymer = false;
polymer_type = PolymerType::Unknown;
}
for (Residue& res : subchain)
res_infos.emplace_back(&res);
}
// Add a ChemLink that restraints only bond length.
static
const std::string& add_auto_chemlink(MonLib& monlib,
const std::string& resname1, const std::string& aname1,
const std::string& resname2, const std::string& aname2,
double ideal_dist, double esd) {
ChemLink cl;
cl.side1.comp = resname1;
cl.side2.comp = resname2;
cl.id = resname1 + resname2;
cl.name = "auto-" + cl.id;
cl.rt.bonds.push_back({Restraints::AtomId{1, aname1},
Restraints::AtomId{2, aname2},
BondType::Unspec, false,
ideal_dist, esd,
ideal_dist, esd});
// ensure unique link id
size_t orig_len = cl.id.size();
for (int n = 0; monlib.get_link(cl.id) != nullptr; ++n)
cl.id.replace(orig_len, cl.id.size(), std::to_string(n));
auto it = monlib.links.emplace(cl.id, cl);
return it.first->first;
}
static const ChemLink* setup_link_if_matches(Topo::Link& link, const MonLib& monlib,
const std::string& atom1,
const std::string& atom2) {
bool invert;
const ChemLink* match = nullptr;
std::tie(match, invert, link.aliasing1, link.aliasing2) =
monlib.match_link(*link.res1, atom1, link.alt1, *link.res2, atom2, link.alt2);
if (match) {
link.link_id = match->id;
if (invert) {
std::swap(link.res1, link.res2);
std::swap(link.alt1, link.alt2);
std::swap(link.atom1_name_id, link.atom2_name_id);
std::swap(link.aliasing1, link.aliasing2);
}
}
return match;
}
static int atom_name_id(const std::string& s) {
int id = 0;
if (s.size() > 0) // there is '\0' at the end
id = (s[0] << 24 | s[1] << 16);
if (s.size() > 2)
id |= (s[2] << 8 | s[3]);
return id | 0x20202020;
}
static void add_polymer_links(PolymerType polymer_type,
const Topo::ResInfo& ri1,
Topo::ResInfo& ri2,
MonLib* monlib) {
Topo::Link link;
link.res1 = ri1.res;
link.res2 = ri2.res;
assert(&ri1 - &ri2 == link.res_distance());
bool groups_ok = (ri1.orig_chemcomp != nullptr && ri2.orig_chemcomp != nullptr);
if (is_polypeptide(polymer_type)) {
std::string c = "C";
std::string ca1 = "CA";
std::string n = "N";
std::string ca2 = "CA";
if (ri1.orig_chemcomp && !ChemComp::is_peptide_group(ri1.orig_chemcomp->group)) {
for (const ChemComp::Aliasing& aliasing : ri1.orig_chemcomp->aliases)
if (ChemComp::is_peptide_group(aliasing.group)) {
link.aliasing1 = &aliasing;
if (const std::string* ptr = aliasing.name_from_alias(c))
c = *ptr;
if (const std::string* ptr = aliasing.name_from_alias(ca1))
ca1 = *ptr;
break;
}
if (!link.aliasing1)
groups_ok = false;
}
ChemComp::Group n_terminus_group = ri2.orig_chemcomp ? ri2.orig_chemcomp->group
: ChemComp::Group::Null;
if (ri2.orig_chemcomp && !ChemComp::is_peptide_group(ri2.orig_chemcomp->group)) {
for (const ChemComp::Aliasing& aliasing : ri2.orig_chemcomp->aliases)
if (ChemComp::is_peptide_group(aliasing.group)) {
link.aliasing2 = &aliasing;
n_terminus_group = aliasing.group;
if (const std::string* ptr = aliasing.name_from_alias(n))
n = *ptr;
if (const std::string* ptr = aliasing.name_from_alias(ca2))
ca2 = *ptr;
break;
}
if (!link.aliasing2)
groups_ok = false;
}
for (const Atom& a1 : ri1.res->atoms)
if (a1.name == c && a1.element == El::C) {
for (const Atom& a2 : ri2.res->atoms)
if (a2.name == n && a2.element == El::N &&
(a2.altloc == a1.altloc || a2.altloc == '\0' || a1.altloc == '\0') &&
in_peptide_bond_distance(&a1, &a2)) {
// One C-N pair of atoms can create here only one link.
// Ignoring artificial configuration of no-altloc C and N atoms,
// and CA atoms in 2+ conformations making both CIS and TRANS links.
link.alt1 = a1.altloc;
link.alt2 = a2.altloc;
link.atom1_name_id = atom_name_id(a1.name);
link.atom2_name_id = atom_name_id(a2.name);
if (groups_ok) {
// Deciding CIS/TRANS based on omega angle.
char alt = a1.altloc_or(a2.altloc_or('*'));
const Atom* ca1_atom = ri1.res->find_atom(ca1, alt, El::C);
const Atom* ca2_atom = ri2.res->find_atom(ca2, alt, El::C);
link.is_cis = is_peptide_bond_cis(ca1_atom, &a1, &a2, ca2_atom, 60.);
if (n_terminus_group == ChemComp::Group::PPeptide)
link.link_id = link.is_cis ? "PCIS" : "PTRANS";
else if (n_terminus_group == ChemComp::Group::MPeptide)
link.link_id = link.is_cis ? "NMCIS" : "NMTRANS";
else
link.link_id = link.is_cis ? "CIS" : "TRANS";
} else if (monlib) {
if (!setup_link_if_matches(link, *monlib, c, n))
link.link_id = add_auto_chemlink(*monlib,
ri1.res->name, c,
ri2.res->name, n,
1.34, 0.04);
}
ri2.prev.push_back(link);
}
}
} else if (is_polynucleotide(polymer_type)) {
std::string o3p = "O3'";
std::string p = "P";
if (ri1.orig_chemcomp && !ChemComp::is_nucleotide_group(ri1.orig_chemcomp->group)) {
for (const ChemComp::Aliasing& aliasing : ri1.orig_chemcomp->aliases)
if (ChemComp::is_nucleotide_group(aliasing.group)) {
link.aliasing1 = &aliasing;
if (const std::string* o3p_ptr = aliasing.name_from_alias(o3p))
o3p = *o3p_ptr;
break;
}
if (!link.aliasing1)
groups_ok = false;
}
if (ri2.orig_chemcomp && !ChemComp::is_nucleotide_group(ri2.orig_chemcomp->group)) {
for (const ChemComp::Aliasing& aliasing : ri2.orig_chemcomp->aliases)
if (ChemComp::is_nucleotide_group(aliasing.group)) {
link.aliasing2 = &aliasing;
if (const std::string* p_ptr = aliasing.name_from_alias(p))
p = *p_ptr;
break;
}
if (!link.aliasing2)
groups_ok = false;
}
for (const Atom& a1 : ri1.res->atoms)
if (a1.name == o3p && a1.element == El::O) {
for (const Atom& a2 : ri2.res->atoms)
if (a2.name == p && a2.element == El::P &&
(a2.altloc == a1.altloc || a2.altloc == '\0' || a1.altloc == '\0') &&
in_nucleotide_bond_distance(&a1, &a2)) {
link.alt1 = a1.altloc;
link.alt2 = a2.altloc;
link.atom1_name_id = atom_name_id(a1.name);
link.atom2_name_id = atom_name_id(a2.name);
if (groups_ok) {
link.link_id = "p";
} else if (monlib) {
if (!setup_link_if_matches(link, *monlib, o3p, p))
link.link_id = add_auto_chemlink(*monlib,
ri1.res->name, o3p,
ri2.res->name, p,
1.606, 0.02);
}
ri2.prev.push_back(link);
}
}
}
if (ri2.prev.empty()) {
link.link_id = "gap";
ri2.prev.push_back(link);
}
}
double Topo::ideal_chiral_abs_volume(const Chirality &ch) const {
const Restraints::Bond* bond_c1 = take_bond(ch.atoms[0], ch.atoms[1]);
const Restraints::Bond* bond_c2 = take_bond(ch.atoms[0], ch.atoms[2]);
const Restraints::Bond* bond_c3 = take_bond(ch.atoms[0], ch.atoms[3]);
const Restraints::Angle* angle_1c2 = take_angle(ch.atoms[1], ch.atoms[0], ch.atoms[2]);
const Restraints::Angle* angle_2c3 = take_angle(ch.atoms[2], ch.atoms[0], ch.atoms[3]);
const Restraints::Angle* angle_3c1 = take_angle(ch.atoms[3], ch.atoms[0], ch.atoms[1]);
if (bond_c1 && bond_c2 && bond_c3 && angle_1c2 && angle_2c3 && angle_3c1)
return chiral_abs_volume(bond_c1->value, bond_c2->value, bond_c3->value,
angle_1c2->value, angle_2c3->value, angle_3c1->value);
return std::numeric_limits<double>::quiet_NaN();
}
std::vector<Topo::Rule> Topo::apply_restraints(const Restraints& rt,
Residue& res, Residue* res2, Asu asu,
char altloc1, char altloc2, bool require_alt) {
std::string altlocs;
if (altloc1 == '\0' && altloc2 == '\0') {
add_distinct_altlocs(res, altlocs);
if (res2)
add_distinct_altlocs(*res2, altlocs);
}
if (altlocs.empty())
altlocs += altloc1 != 0 ? altloc1 : altloc2;
std::vector<Rule> rules;
for (const Restraints::Bond& bond : rt.bonds)
for (char alt : altlocs)
if (Atom* at1 = bond.id1.get_from(res, res2, alt, altloc2))
if (Atom* at2 = bond.id2.get_from(res, res2, alt, altloc2)) {
bool with_alt = at1->altloc || at2->altloc;
if (with_alt || !require_alt) {
rules.push_back({RKind::Bond, bonds.size()});
bonds.push_back({&bond, {{at1, at2}}, asu});
}
if (!with_alt)
break;
}
if (only_bonds)
return rules;
for (const Restraints::Angle& angle : rt.angles)
for (char alt : altlocs)
if (Atom* at1 = angle.id1.get_from(res, res2, alt, altloc2))
if (Atom* at2 = angle.id2.get_from(res, res2, alt, altloc2))
if (Atom* at3 = angle.id3.get_from(res, res2, alt, altloc2)) {
bool with_alt = at1->altloc || at2->altloc || at3->altloc;
if (with_alt || !require_alt) {
rules.push_back({RKind::Angle, angles.size()});
angles.push_back({&angle, {{at1, at2, at3}}});
}
if (!with_alt)
break;
}
for (const Restraints::Torsion& tor : rt.torsions)
for (char alt : altlocs)
if (Atom* at1 = tor.id1.get_from(res, res2, alt, altloc2))
if (Atom* at2 = tor.id2.get_from(res, res2, alt, altloc2))
if (Atom* at3 = tor.id3.get_from(res, res2, alt, altloc2))
if (Atom* at4 = tor.id4.get_from(res, res2, alt, altloc2)) {
bool with_alt = at1->altloc || at2->altloc || at3->altloc || at4->altloc;
if (with_alt || !require_alt) {
rules.push_back({RKind::Torsion, torsions.size()});
torsions.push_back({&tor, {{at1, at2, at3, at4}}});
}
if (!with_alt)
break;
}
for (const Restraints::Chirality& chir : rt.chirs)
for (char alt : altlocs)
if (Atom* at1 = chir.id_ctr.get_from(res, res2, alt, altloc2))
if (Atom* at2 = chir.id1.get_from(res, res2, alt, altloc2))
if (Atom* at3 = chir.id2.get_from(res, res2, alt, altloc2))
if (Atom* at4 = chir.id3.get_from(res, res2, alt, altloc2)) {
bool with_alt = at1->altloc || at2->altloc || at3->altloc || at4->altloc;
if (with_alt || !require_alt) {
rules.push_back({RKind::Chirality, chirs.size()});
chirs.push_back({&chir, {{at1, at2, at3, at4}}});
}
if (!with_alt)
break;
}
for (const Restraints::Plane& plane : rt.planes)
for (char alt : altlocs) {
std::vector<Atom*> atoms;
bool with_alt = false;
for (const Restraints::AtomId& id : plane.ids)
if (Atom* atom = id.get_from(res, res2, alt, altloc2)) {
with_alt = with_alt || atom->altloc;
atoms.push_back(atom);
}
if (atoms.size() >= 4 && (with_alt || !require_alt)) {
rules.push_back({RKind::Plane, planes.size()});
planes.push_back({&plane, atoms});
}
if (!with_alt)
break;
}
return rules;
}
void Topo::apply_restraints_from_link(Link& link, const MonLib& monlib) {
if (link.link_id.empty() || link.link_id == "gap")
return;
const ChemLink* chem_link = monlib.get_link(link.link_id);
if (!chem_link) {
logger.err("ignoring link '", link.link_id, "' as it is not in the monomer library");
return;
}
const Restraints* rt = &chem_link->rt;
if (link.alt1 && link.alt2 && link.alt1 != link.alt2)
logger.err("LINK between different conformers: ", link.alt1, " (in ",
link.res1->name, ") and ", link.alt2, " (in ", link.res2->name, ").");
// aliases are a new feature - introduced in 2022
if (link.aliasing1 || link.aliasing2) {
std::unique_ptr<Restraints> rt_copy(new Restraints(*rt));
if (link.aliasing1)
for (const auto& p : link.aliasing1->related)
rt_copy->rename_atom(Restraints::AtomId{1, p.second}, p.first);
if (link.aliasing2)
for (const auto& p : link.aliasing2->related)
rt_copy->rename_atom(Restraints::AtomId{2, p.second}, p.first);
rt = rt_copy.get();
rt_storage.push_back(std::move(rt_copy));
}
link.link_rules = apply_restraints(*rt, *link.res1, link.res2, link.asu,
link.alt1, link.alt2, false);
}
// see comments above the declaration
void Topo::initialize_refmac_topology(Structure& st, Model& model0,
MonLib& monlib, bool ignore_unknown_links) {
// initialize chain_infos
for (Chain& chain : model0.chains)
for (ResidueSpan& sub : chain.subchains()) {
// set Residue::group_idx which is used in Restraints::AtomId::get_from()
for (size_t i = 0; i != sub.size(); ++i) {
sub[i].group_idx = 0;
if (i != 0 && sub[i-1].seqid == sub[i].seqid)
sub[i].group_idx = sub[i-1].group_idx + 1;
}
const Entity* ent = st.get_entity_of(sub);
chain_infos.emplace_back(sub, chain, ent);
}
// add modifications from MODRES records (Refmac's extension)
for (const ModRes& modres : st.mod_residues)
if (!modres.mod_id.empty()) {
for (Topo::ChainInfo& chain_info : chain_infos)
if (chain_info.chain_ref.name == modres.chain_name) {
for (Topo::ResInfo& res_info : chain_info.res_infos)
if (*res_info.res == modres.res_id)
res_info.add_mod(modres.mod_id, nullptr, '\0');
}
}
// setup pointers to monomers and links in the polymer
for (ChainInfo& ci : chain_infos) {
for (ResInfo& ri : ci.res_infos) {
auto it = monlib.monomers.find(ri.res->name);
if (it != monlib.monomers.end())
ri.orig_chemcomp = &it->second;
// orig_chemcomp is left null for missing monomers
}
// setup polymer links
if (ci.polymer && !ci.res_infos.empty()) {
// handling of microheterogeneities makes it more complicated
// and it should be even more complex to handle partial bonding
auto prev_begin = ci.res_infos.begin();
auto prev_end = ci.group_end(prev_begin);
while (prev_end != ci.res_infos.end()) {
auto group_begin = prev_end;
auto group_end = ci.group_end(group_begin);
for (auto ri = group_begin; ri != group_end; ++ri)
for (auto prev_ri = prev_begin; prev_ri != prev_end; ++prev_ri) {
MonLib* monlib_ptr = ignore_unknown_links ? nullptr : &monlib;
add_polymer_links(ci.polymer_type, *prev_ri, *ri, monlib_ptr);
}
prev_begin = group_begin;
prev_end = group_end;
}
}
}
// add extra links
for (Connection& conn : st.connections)
if (conn.type != Connection::Hydrog) // ignoring hydrogen bonds
setup_connection(conn, model0, monlib, ignore_unknown_links);
// Add modifications from standard links. We do it here b/c polymer links
// could be disabled (link_id.clear()) in setup_connection().
for (ChainInfo& ci : chain_infos)
for (ResInfo& ri : ci.res_infos)
for (Link& prev : ri.prev)
if (const ChemLink* chem_link = monlib.get_link(prev.link_id)) {
ResInfo* ri_prev = &ri + prev.res_distance();
ri_prev->add_mod(chem_link->side1.mod, prev.aliasing1, prev.alt1);
ri.add_mod(chem_link->side2.mod, prev.aliasing2, prev.alt2);
}
// Apply modifications to monomer restraints
for (ChainInfo& chain_info : chain_infos)
for (ResInfo& ri : chain_info.res_infos) {
if (ri.orig_chemcomp) {
// The final ChemComp restraints that we'll use are made from
// original (_chem_comp) restraints with modifications (_chem_mod)
// applied. There is a corner case in which different conformations
// of the residue have different modifications applied.
bool has_mod_altlocs = false;
for (const Mod& mod : ri.mods)
if (mod.altloc != '\0')
has_mod_altlocs = true;
std::string altlocs;
if (has_mod_altlocs)
add_distinct_altlocs(*ri.res, altlocs); // cf. apply_restraints
if (altlocs.empty())
altlocs += '\0';
for (char altloc : altlocs) {
// key for caching in Topo::cc_cache: ChemComp::name + modifications
std::string key = ri.orig_chemcomp->name;
for (const Mod& mod : ri.mods)
if (!mod.altloc || altloc == mod.altloc) {
key += char(1 + static_cast<int>(mod.alias));
key += mod.id;
}
auto it = cc_cache.find(key);
if (it != cc_cache.end()) {
ri.chemcomps.push_back({altloc, it->second.get()});
} else { // it's not in cache yet - we need to add it
std::unique_ptr<ChemComp> cc_copy(new ChemComp(*ri.orig_chemcomp));
// apply modifications
for (const Mod& mod : ri.mods) {
if (!mod.altloc || altloc == mod.altloc) {
if (const ChemMod* chem_mod = monlib.get_mod(mod.id)) {
try {
chem_mod->apply_to(*cc_copy, mod.alias);
} catch(std::runtime_error& e) {
logger.err("failed to apply modification ", chem_mod->id,
" to ", ri.res->name, ": ", e.what());
}
} else {
logger.err("modification not found: ", mod.id);
}
}
}
ri.chemcomps.push_back({altloc, cc_copy.get()});
cc_cache.emplace(key, std::move(cc_copy));
}
}
// Usually the same modifications are applied to all conformers.
// In such case reduce chemcomps to a single value.
if (ri.chemcomps.size() > 0 &&
std::all_of(ri.chemcomps.begin() + 1, ri.chemcomps.end(),
[&](const FinalChemComp& f) { return f.cc == ri.chemcomps[0].cc; })) {
ri.chemcomps.resize(1);
ri.chemcomps[0].altloc = '\0';
}
} else { // orig_chemcomp not set - make ChemComp with ad-hoc restraints
// no cache - ad-hoc restraints are separate for each residue
cc_storage.emplace_back(make_chemcomp_with_restraints(*ri.res));
ri.chemcomps.push_back({'\0', cc_storage.back().get()});
}
}
}
void Topo::apply_all_restraints(const MonLib& monlib) {
bonds.clear();
angles.clear();
torsions.clear();
chirs.clear();
planes.clear();
rt_storage.clear();
for (ChainInfo& chain_info : chain_infos)
for (ResInfo& ri : chain_info.res_infos) {
// link restraints
for (Link& link : ri.prev)
apply_restraints_from_link(link, monlib);
// monomer restraints
auto it = ri.chemcomps.cbegin();
ri.monomer_rules = apply_restraints(it->cc->rt, *ri.res, nullptr, Asu::Same,
it->altloc, '\0', /*require_alt=*/false);
while (++it != ri.chemcomps.end()) {
auto rules = apply_restraints(it->cc->rt, *ri.res, nullptr, Asu::Same,
it->altloc, '\0', /*require_alt=*/true);
// calling reserve avoids bogus GCC warning -Wstringop-overflow
ri.monomer_rules.reserve(ri.monomer_rules.size() + rules.size());
vector_move_extend(ri.monomer_rules, std::move(rules));
}
}
for (Link& link : extras)
apply_restraints_from_link(link, monlib);
}
void Topo::create_indices() {
bond_index.clear();
for (Bond& bond : bonds) {
bond_index.emplace(bond.atoms[0], &bond);
if (bond.atoms[1] != bond.atoms[0])
bond_index.emplace(bond.atoms[1], &bond);
}
angle_index.clear();
for (Angle& ang : angles)
angle_index.emplace(ang.atoms[1], &ang);
torsion_index.clear();
for (Torsion& tor : torsions) {
torsion_index.emplace(tor.atoms[1], &tor);
if (tor.atoms[1] != tor.atoms[2])
torsion_index.emplace(tor.atoms[2], &tor);
}
plane_index.clear();
for (Plane& plane : planes)
for (Atom* atom : plane.atoms)
plane_index.emplace(atom, &plane);
}
Topo::Link* Topo::find_polymer_link(const AtomAddress& aa1, const AtomAddress& aa2) {
auto are_bonded = [&](const Link& link, const AtomAddress& a1, const AtomAddress& a2) {
return a1.res_id.matches_noseg(*link.res1) &&
a2.res_id.matches_noseg(*link.res2) &&
a1.altloc == link.alt1 &&
a2.altloc == link.alt2 &&
(a1.atom_name.empty() || atom_name_id(a1.atom_name) == link.atom1_name_id) &&
(a2.atom_name.empty() || atom_name_id(a2.atom_name) == link.atom2_name_id);
};
if (aa1.chain_name != aa2.chain_name)
return nullptr;
for (ChainInfo& ci : chain_infos)
if (aa1.chain_name == ci.chain_ref.name) {
for (ResInfo& ri : ci.res_infos)
for (Link& link : ri.prev) {
assert(link.res1 && link.res2);
if (are_bonded(link, aa1, aa2) || are_bonded(link, aa2, aa1))
return &link;
}
}
return nullptr;
}
// Tries to construct Topo::Link and append it to extras.
// Side-effects: it may modify conn.link_id and add ChemLink to monlib.links.
void Topo::setup_connection(Connection& conn, Model& model0, MonLib& monlib,
bool ignore_unknown_links) {
if (conn.link_id == "gap") {
if (Link* polymer_link = find_polymer_link(conn.partner1, conn.partner2))
polymer_link->link_id.clear(); // disable polymer link
return;
}
Link extra;
CRA cra1 = model0.find_cra(conn.partner1, true);
CRA cra2 = model0.find_cra(conn.partner2, true);
if (!cra1.atom || !cra2.atom)
return;
extra.res1 = cra1.residue;
extra.res2 = cra2.residue;
extra.alt1 = conn.partner1.altloc;
extra.alt2 = conn.partner2.altloc;
extra.asu = conn.asu;
const ChemLink* match = nullptr;
// If we have link_id find ChemLink by name (and check if it matches).
if (!conn.link_id.empty()) {
match = monlib.get_link(conn.link_id);
if (!match) {
logger.err("link not found in monomer library: ", conn.link_id);
return;
}
if (match->rt.bonds.empty() ||
!monlib.link_side_matches_residue(match->side1, extra.res1->name, &extra.aliasing1) ||
!monlib.link_side_matches_residue(match->side2, extra.res2->name, &extra.aliasing2) ||
!atom_match_with_alias(match->rt.bonds[0].id1.atom,
conn.partner1.atom_name, extra.aliasing1) ||
!atom_match_with_alias(match->rt.bonds[0].id2.atom,
conn.partner2.atom_name, extra.aliasing2)) {
logger.err("link from the monomer library does not match: ", conn.link_id);
return;
}
} else {
// we don't have link_id - use the best matching link (if any)
match = setup_link_if_matches(extra, monlib,
conn.partner1.atom_name, conn.partner2.atom_name);
if (match)
conn.link_id = match->id;
}
// If a polymer link is also given in LINK/struct_conn,
// use only one of them. If LINK has explicit name (ccp4_link_id),
// or if it matches residue-specific link from monomer library, use it;
// otherwise, LINK is repetition of TRANS/CIS, so ignore LINK.
if (Link* polymer_link = find_polymer_link(conn.partner1, conn.partner2)) {
if (!conn.link_id.empty() || // conn has explicit link name
// polymer link was not really identified
cif::is_null(polymer_link->link_id) || polymer_link->link_id == "gap" ||
// conn is residue-specific
(match && !(match->side1.comp.empty() && match->side2.comp.empty())))
polymer_link->link_id.clear(); // disable polymer link, use conn
else
return;
}
if (match) {
extra.link_id = match->id;
// add modifications from the link
find_resinfo(extra.res1)->add_mod(match->side1.mod, extra.aliasing1, extra.alt1);
find_resinfo(extra.res2)->add_mod(match->side2.mod, extra.aliasing2, extra.alt2);
} else {
if (ignore_unknown_links)
return;
// create a new ChemLink and add it to the monomer library
double ideal_dist = monlib.find_ideal_distance(cra1, cra2);
extra.link_id = add_auto_chemlink(monlib,
extra.res1->name, conn.partner1.atom_name,
extra.res2->name, conn.partner2.atom_name,
ideal_dist, 0.02);
}
extras.push_back(extra);
}
void Topo::set_cispeps_in_structure(Structure& st) {
st.cispeps.clear();
if (chain_infos.empty())
return;
// model is not stored in Topo, let's determine it from chain_infos[0]
int model_num = 0;
for (const Model& model : st.models)
if (!model.chains.empty() &&
&model.chains[0] == &chain_infos[0].chain_ref)
model_num = model.num;
for (ChainInfo& chain_info : chain_infos)
for (ResInfo& res_info : chain_info.res_infos)
for (Link& link : res_info.prev)
if (link.is_cis) {
CisPep cp;
cp.model_num = model_num;
cp.partner_c = AtomAddress(chain_info.chain_ref.name, *link.res1, "", link.alt1);
cp.partner_n = AtomAddress(chain_info.chain_ref.name, *link.res2, "", link.alt2);
cp.only_altloc = link.alt1 ? link.alt1 : link.alt2;
for (const Rule& rule : link.link_rules)
if (rule.rkind == RKind::Torsion) {
const Torsion& tor = torsions[rule.index];
if (tor.restr->label == "omega") {
cp.reported_angle = tor.calculate();
break;
}
}
st.cispeps.push_back(cp);
}
}
namespace {
void remove_hydrogens_from_atom(Topo::ResInfo* ri,
const std::string& atom_name, char alt) {
if (!ri)
return;
std::vector<Atom>& atoms = ri->res->atoms;
const Restraints& rt = ri->get_final_chemcomp(alt).rt;
for (auto it = atoms.end(); it-- != atoms.begin(); ) {
if (it->is_hydrogen() && is_same_conformer(it->altloc, alt)) {
const Restraints::AtomId* heavy = rt.first_bonded_atom(it->name);
if (heavy && heavy->atom == atom_name)
atoms.erase(it);
}
}
}
void set_cis_in_link(Topo::Link& link, bool is_cis) {
if (is_cis) {
if (ends_with(link.link_id, "TRANS"))
link.link_id.replace(link.link_id.size() - 5, 5, "CIS");
} else {
if (ends_with(link.link_id, "CIS"))
link.link_id.replace(link.link_id.size() - 3, 3, "TRANS");
}
link.is_cis = is_cis;
}
void force_cispeps(Topo& topo, bool single_model, const Model& model,
const std::vector<CisPep>& cispeps) {
std::multimap<const Residue*, const CisPep*> cispep_index;
for (const CisPep& cp : cispeps) {
if (single_model || model.num == cp.model_num)
if (const Residue* res_n = model.find_cra(cp.partner_n).residue)
cispep_index.emplace(res_n, &cp);
}
for (Topo::ChainInfo& chain_info : topo.chain_infos)
for (Topo::ResInfo& res_info : chain_info.res_infos) {
auto range = cispep_index.equal_range(res_info.res);
for (Topo::Link& link : res_info.prev) {
bool is_cis = false;
for (auto i = range.first; i != range.second; ++i) {
const CisPep& cp = *i->second;
if (cp.partner_c.res_id.matches_noseg(*link.res1) &&
(cp.only_altloc == '\0' || cp.only_altloc == link.alt1
|| cp.only_altloc == link.alt2))
is_cis = true;
}
if (is_cis != link.is_cis) {
set_cis_in_link(link, is_cis);
const std::string& ch_name = chain_info.chain_ref.name;
topo.logger.note("link between ", atom_str(ch_name, *link.res1, "", link.alt1),
" and ", atom_str(ch_name, *link.res2, "", link.alt2),
" forced to ", link.link_id);
}
}
}
}
struct Neigh {
char alt;
float occ;
};
using NeighMap = std::unordered_multimap<int, Neigh>;
// Assumes no hydrogens in the residue.
// Position and serial number are not assigned for new atoms.
void add_hydrogens_without_positions(Topo::ResInfo& ri, const NeighMap& neighbor_altlocs) {
std::vector<Atom>& atoms = ri.res->atoms;
// Add H atom for each conformation (altloc) of the parent atom and its
// first neighbors.
for (size_t i = 0, size = atoms.size(); i != size; ++i) {
if (atoms[i].calc_flag == CalcFlag::NoHydrogen)
continue;
char parent_alt = atoms[i].altloc;
float parent_occ = atoms[i].occ;
std::map<char, float> altlocs; // altloc + occupancy
if (parent_alt == '\0') {
// We could have neighbor 1 with altlocs A,B and neighbor 2 with C,D.
// Or A,B and A,B,C. Or, neighbor 1 could have different occupancy
// of altloc A than neighbor 2. We don't want to check for all possible
// weird cases. Only ensure that the added H has total occupancy <= 1.
float occ_sum = 0;
auto range = neighbor_altlocs.equal_range(atoms[i].serial);
for (auto it = range.first; it != range.second; ++it) {
const Neigh& neigh = it->second;
if (altlocs.count(neigh.alt) == 0 && neigh.occ < 1.0f && occ_sum + neigh.occ <= 1.001f) {
occ_sum += neigh.occ;
altlocs.emplace(neigh.alt, neigh.occ);
}
}
if (occ_sum - parent_occ > 0.001f)
for (auto& it : altlocs)
it.second *= parent_occ;
}
if (altlocs.empty())
altlocs.emplace(parent_alt, parent_occ);
const ChemComp& cc = ri.get_final_chemcomp(parent_alt);
for (const Restraints::Bond& bond : cc.rt.bonds) {
// atoms may get re-allocated, so we can't set parent earlier
const Atom& parent = atoms[i];
assert(!parent.is_hydrogen());
const Restraints::AtomId* atom_id = bond.other(parent.name);
if (!atom_id)
continue;
auto it = cc.find_atom(atom_id->atom);
if (it == cc.atoms.end())
fail("inconsistent _chem_comp " + cc.name);
if (it->is_hydrogen()) {
gemmi::Atom atom;
atom.name = it->id;
atom.element = it->el;
// calc_flag will be changed to Calculated when the position is set
atom.calc_flag = CalcFlag::Dummy;
atom.b_iso = parent.b_iso;
for (auto alt_occ : altlocs) {
atom.altloc = alt_occ.first;
atom.occ = alt_occ.second;
atoms.push_back(atom);
}
}
}
}
}
NeighMap prepare_neighbor_altlocs(Topo& topo, const MonLib& monlib) {
// disable warnings here, so they are not printed twice
topo.logger.suspend();
// Prepare bonds. Fills topo.bonds, monomer_rules/link_rules and rt_storage,
// but they are all reset when apply_all_restraints() is called again.
topo.only_bonds = true;
topo.apply_all_restraints(monlib);
topo.only_bonds = false;
topo.logger.resume(); // re-enable warnings
NeighMap neighbors;
for (const Topo::Bond& bond : topo.bonds) {
const Atom* a1 = bond.atoms[0];
const Atom* a2 = bond.atoms[1];
if (a2->altloc)
neighbors.emplace(a1->serial, Neigh{a2->altloc, a2->occ});
if (a1->altloc && a1 != a2)
neighbors.emplace(a2->serial, Neigh{a1->altloc, a1->occ});
}
return neighbors;
}
} // anonymous namespace
std::unique_ptr<Topo>
prepare_topology(Structure& st, MonLib& monlib, size_t model_index,
HydrogenChange h_change, bool reorder,
const Logger& logger, bool ignore_unknown_links, bool use_cispeps) {
std::unique_ptr<Topo> topo(new Topo);
topo->logger = logger;
if (model_index >= st.models.size())
fail("no such model index: " + std::to_string(model_index));
Model& model = st.models[model_index];
topo->initialize_refmac_topology(st, model, monlib, ignore_unknown_links);
if (use_cispeps)
force_cispeps(*topo, st.models.size() == 1, model, st.cispeps);
// remove hydrogens, or change deuterium to fraction, or nothing
// and then check atom names
for (Topo::ChainInfo& chain_info : topo->chain_infos)
for (Topo::ResInfo& ri : chain_info.res_infos) {
Residue& res = *ri.res;
if (h_change != HydrogenChange::NoChange && h_change != HydrogenChange::Shift
// don't re-add H's if we don't have chemical component description
&& (ri.orig_chemcomp != nullptr || h_change == HydrogenChange::Remove)) {
// remove/add hydrogens
remove_hydrogens(res);
} else {
// Special handling of Deuterium - mostly for Refmac.
// Note: if the model has deuterium, it gets modified.
if (replace_deuterium_with_fraction(res)) {
// deuterium names usually differ from the names in dictionary
for (Atom& atom : res.atoms)
if (atom.name[0] == 'D' && atom.fraction != 0) {
const ChemComp& cc = ri.get_final_chemcomp(atom.altloc);
if (cc.find_atom(atom.name) == cc.atoms.end())
atom.name[0] = 'H';
}
st.has_d_fraction = true;
}
}
// check atom names
for (Atom& atom : res.atoms) {
const ChemComp& cc = ri.get_final_chemcomp(atom.altloc);
if (!cc.has_atom(atom.name)) {
std::string msg = "definition not found for "
+ atom_str(chain_info.chain_ref, *ri.res, atom);
if (ri.orig_chemcomp && ri.orig_chemcomp->has_atom(atom.name)) {
msg += " (linkage should remove this atom)";
} else {
auto it = cc.find_atom_by_old_name(atom.name);
if (it != cc.atoms.end())
cat_to(msg, " (replace ", atom.name, " with ", it->id, ')');
}
topo->logger.err(msg);
}
}
}
// add hydrogens
if (h_change == HydrogenChange::ReAdd ||
h_change == HydrogenChange::ReAddButWater ||
h_change == HydrogenChange::ReAddKnown) {
NeighMap neighbor_altlocs = prepare_neighbor_altlocs(*topo, monlib);
for (Topo::ChainInfo& chain_info : topo->chain_infos)
for (Topo::ResInfo& ri : chain_info.res_infos) {
Residue& res = *ri.res;
if (ri.orig_chemcomp != nullptr &&
(h_change == HydrogenChange::ReAdd || !res.is_water())) {
add_hydrogens_without_positions(ri, neighbor_altlocs);
// a special handling of HIS for compatibility with Refmac
if (res.name == "HIS") {
for (gemmi::Atom& atom : ri.res->atoms)
if (atom.name == "HD1" || atom.name == "HE2")
atom.occ = 0;
}
}
}
}
// sort atoms in residues
for (Topo::ChainInfo& chain_info : topo->chain_infos)
for (Topo::ResInfo& ri : chain_info.res_infos)
if (reorder && ri.orig_chemcomp) {
Residue& res = *ri.res;
const ChemComp& cc = *ri.orig_chemcomp;
for (Atom& atom : res.atoms) {
auto it = cc.find_atom(atom.name);
// If atom.name is not found (b/c it was added in a modification),
// the atom will be put after original atoms.
atom.serial = int(it - cc.atoms.begin()); // temporary, for sorting only
}
std::sort(res.atoms.begin(), res.atoms.end(), [](const Atom& a, const Atom& b) {
return a.serial != b.serial ? a.serial < b.serial
: a.altloc < b.altloc;
});
// check for missing altloc
for (auto atom = res.atoms.begin(); atom + 1 < res.atoms.end(); ++atom)
if (atom->name == (atom + 1)->name && atom->altloc == '\0')
topo->logger.err("missing altloc in ", atom_str(chain_info.chain_ref, *ri.res, *atom));
}
// for atoms with ad-hoc links, for now we don't want hydrogens
if (!ignore_unknown_links && h_change != HydrogenChange::NoChange) {
auto remove_h_from_auto_links = [&](const Topo::Link& link) {
const ChemLink* cl = monlib.get_link(link.link_id);
if (cl && starts_with(cl->name, "auto-")) {
const Restraints::Bond& bond = cl->rt.bonds.at(0);
remove_hydrogens_from_atom(topo->find_resinfo(link.res1), bond.id1.atom, link.alt1);
remove_hydrogens_from_atom(topo->find_resinfo(link.res2), bond.id2.atom, link.alt2);
}
};
for (const Topo::ChainInfo& chain_info : topo->chain_infos)
for (const Topo::ResInfo& res_info : chain_info.res_infos)
for (const Topo::Link& link : res_info.prev)
remove_h_from_auto_links(link);
for (const Topo::Link& link : topo->extras)
remove_h_from_auto_links(link);
}
// fill Topo::bonds, angles, ... and ResInfo::monomer_rules, Links::link_rules
topo->apply_all_restraints(monlib);
// fill bond_index, angle_index, etc
topo->create_indices();
// the hydrogens added previously have positions not set
if (h_change != HydrogenChange::NoChange)
place_hydrogens_on_all_atoms(*topo);
if (h_change == HydrogenChange::ReAddKnown) {
// To leave only known hydrogens, we remove Hs with zero occupancy.
// As a side-effect, it removes any H atoms on zero-occupancy parents.
for (Chain& chain : model.chains)
for (Residue& res : chain.residues)
vector_remove_if(res.atoms, [](Atom& a) { return a.is_hydrogen() && a.occ == 0; });
// disable warnings here, so they are not printed twice
topo->logger.suspend();
// re-set restraints and indices
topo->apply_all_restraints(monlib);
topo->create_indices();
topo->logger.resume();
}
assign_serial_numbers(model);
return topo;
}
std::vector<AtomAddress> find_missing_atoms(const Topo& topo, bool including_hydrogen) {
std::vector<AtomAddress> ret;
for (const Topo::ChainInfo& chain_info : topo.chain_infos)
for (const Topo::ResInfo& ri : chain_info.res_infos) {
// check only the first conformation
const Topo::FinalChemComp& fcc = ri.chemcomps.at(0);
char altloc = fcc.altloc != '\0' ? fcc.altloc : '*';
for (const ChemComp::Atom& at : fcc.cc->atoms)
if ((including_hydrogen || !at.is_hydrogen()) &&
ri.res->find_atom(at.id, altloc) == nullptr)
ret.emplace_back(chain_info.chain_ref.name, *ri.res, at.id, fcc.altloc);
}
return ret;
}
}