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
* 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>
481 lines
20 KiB
C++
481 lines
20 KiB
C++
// Copyright 2018-2022 Global Phasing Ltd.
|
|
|
|
#include <gemmi/riding_h.hpp>
|
|
|
|
namespace gemmi {
|
|
|
|
namespace {
|
|
|
|
struct BondedAtom {
|
|
Atom* ptr;
|
|
Position& pos; // == ptr->pos;
|
|
double dist;
|
|
};
|
|
|
|
// Calculate position using one angle (theta) and one dihedral angle (tau).
|
|
// Returns position of x4 in x1-x2-x3-x4, where dist=|x3-x4| and
|
|
// theta is angle(x2, x3, x4).
|
|
// Based on section 3.3 of Paciorek et al, Acta Cryst. A52, 349 (1996).
|
|
Position position_from_angle_and_torsion(const Position& x1,
|
|
const Position& x2,
|
|
const Position& x3,
|
|
double dist, // |x3-x4|
|
|
double theta, // angle x2-x3-x4
|
|
double tau) { // dihedral angle
|
|
Vec3 u = x2 - x1;
|
|
Vec3 v = x3 - x2;
|
|
Vec3 e1 = v.normalized();
|
|
Vec3 e2 = -(u - u.dot(e1) * e1).normalized();
|
|
Vec3 e3 = e1.cross(e2);
|
|
Vec3 e23 = std::cos(tau) * e2 + std::sin(tau) * e3;
|
|
return x3 + Position(dist * (-std::cos(theta) * e1 + std::sin(theta) * e23));
|
|
}
|
|
|
|
// Similar to position_from_angle_and_torsion(), but x1 and tau are not given.
|
|
Position arbitrary_position_from_angle(const Position& x2,
|
|
const Position& x3,
|
|
double dist, // |x3-x4|
|
|
double theta) { // angle x2-x3-x4
|
|
Vec3 u(1, 0, 0);
|
|
Vec3 v = x3 - x2;
|
|
Vec3 e1 = v.normalized();
|
|
Vec3 e2_ = -(u - u.dot(e1) * e1);
|
|
if (e2_.length_sq() < 1e-6) {
|
|
// u || v, let's take any non-parallel u
|
|
u = Vec3(0, 1, 0);
|
|
e2_ = -(u - u.dot(e1) * e1);
|
|
}
|
|
Vec3 e2 = e2_.normalized();
|
|
return x3 + Position(dist * (-std::cos(theta) * e1 + std::sin(theta) * e2));
|
|
}
|
|
|
|
|
|
Vec3 get_vector_to_line(const Position& point,
|
|
const Position& point_on_the_line,
|
|
const Vec3& unit_vector) {
|
|
// en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Vector_formulation
|
|
// the component of a - p perpendicular to the line is: (a-p) - ((a-p).n)n
|
|
Vec3 ap = point_on_the_line - point;
|
|
return ap - ap.dot(unit_vector) * unit_vector;
|
|
}
|
|
|
|
// If no points satisfy the distances returns a pair of NaNs
|
|
std::pair<Position, Position> trilaterate(const Position& p1, double r1sq,
|
|
const Position& p2, double r2sq,
|
|
const Position& p3, double r3sq) {
|
|
// It was based on https://en.wikipedia.org/wiki/Trilateration
|
|
// but apparently that page has changed in the meantime.
|
|
Vec3 ex = (p2 - p1).normalized();
|
|
double i = ex.dot(p3-p1);
|
|
Vec3 ey = (Vec3(p3) - p1 - i*ex).normalized();
|
|
Vec3 ez = ex.cross(ey);
|
|
double d = (p2-p1).length();
|
|
double j = ey.dot(p3-p1);
|
|
double x = (r1sq - r2sq + d*d) / (2*d);
|
|
double y = (r1sq - r3sq + i*i + j*j) / (2*j) - x*i/j;
|
|
double z2 = r1sq - x*x - y*y;
|
|
double z = std::sqrt(z2); // may result in NaN
|
|
return std::make_pair(p1 + Position(x*ex + y*ey + z*ez),
|
|
p1 + Position(x*ex + y*ey - z*ez));
|
|
}
|
|
|
|
// Calculate position using two angles.
|
|
// Returns p4. Topology: p1 is bonded to p2, p3 and p4.
|
|
std::pair<Position, Position>
|
|
position_from_two_angles(const Position& p1,
|
|
const Position& p2,
|
|
const Position& p3,
|
|
double dist14, // |p4-p1|
|
|
double theta214, // angle p2-p1-p4
|
|
double theta314) { // angle p3-p1-p4
|
|
double d12sq = p1.dist_sq(p2);
|
|
double d13sq = p1.dist_sq(p3);
|
|
double d14sq = dist14 * dist14;
|
|
// the law of cosines
|
|
double d24sq = d14sq + d12sq - 2 * std::sqrt(d14sq * d12sq) * cos(theta214);
|
|
double d34sq = d14sq + d13sq - 2 * std::sqrt(d14sq * d13sq) * cos(theta314);
|
|
return trilaterate(p1, d14sq, p2, d24sq, p3, d34sq);
|
|
}
|
|
|
|
// Returns angle between hydrogen and the plane of heavy atoms
|
|
// in 2H tetrahedral configuration. theta0 is the angle between heavy atoms.
|
|
double calculate_tetrahedral_delta(double theta0, double theta1, double theta2) {
|
|
// simplified trilateration:
|
|
// auto r = trilaterate(Position(0, 0, 0), 1,
|
|
// Position(1, 0, 0), 2 - 2 * std::cos(theta1),
|
|
// Position(std::cos(theta0), std::sin(theta0), 0),
|
|
// 2 - 2 * std::cos(theta2));
|
|
// return std::asin(std::fabs(r.first.z));
|
|
double x = std::cos(theta1);
|
|
double y = (std::cos(theta2) - x * std::cos(theta0)) / std::sin(theta0);
|
|
double z2 = 1 - x*x - y*y;
|
|
double z = std::sqrt(z2); // may result in NaN
|
|
return std::asin(z);
|
|
}
|
|
|
|
double angle_in_triangle(double a, double b, double c) {
|
|
return std::acos((a * a + c * c - b * b) / (2 * a * c));
|
|
}
|
|
|
|
// Used in rare cases when one angle is missing in 2H-tetrahedral configuration.
|
|
// Currently it can happen only with metal sites.
|
|
// b c(metal)
|
|
// \ /
|
|
// atom -- h (possibly two H atoms, symmetric wrt plane abc)
|
|
// Angle c-atom-h is missing, b-atom-h is equal alpha, b-atom-c is theta.
|
|
// For two H atoms we also need angle h-atom-h.
|
|
double missing_angle_2H_tetrahedral(const Topo& topo, const Atom& atom,
|
|
const std::vector<BondedAtom>& hs,
|
|
double alpha, double theta) {
|
|
if (hs.size() == 1)
|
|
return 2 * pi() - alpha - theta; // make it coplanar
|
|
if (hs.size() == 2) {
|
|
if (const Restraints::Angle* hh = topo.take_angle(hs[0].ptr, &atom, hs[1].ptr)) {
|
|
double xh = std::cos(alpha);
|
|
double zh = std::sin(0.5 * hh->radians());
|
|
double yh = std::sqrt(1 - xh*xh - zh*zh);
|
|
double st = std::sin(theta);
|
|
double ct = std::cos(theta);
|
|
Vec3 ah = Vec3(xh, -yh * st, zh * st);
|
|
double angle = std::acos((ct * ah.x + st * ah.y) / ah.length());
|
|
//printf("missing_angle: %s %g\n", atom.name.c_str(), deg(angle));
|
|
return angle;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[noreturn]]
|
|
void giveup(const std::string& message, const std::vector<BondedAtom>& hs) {
|
|
for (const BondedAtom& bonded_h : hs) {
|
|
bonded_h.ptr->occ = 0;
|
|
bonded_h.ptr->calc_flag = CalcFlag::Dummy;
|
|
}
|
|
fail(message);
|
|
}
|
|
|
|
// known and hs are lists of heavy atoms and hydrogens bonded to atom.
|
|
// hs is const, but nevertheless atoms it points to are modified.
|
|
void place_hydrogens(const Topo& topo, const Atom& atom,
|
|
const std::vector<BondedAtom>& known,
|
|
const std::vector<BondedAtom>& hs) {
|
|
using Angle = Restraints::Angle;
|
|
assert(!hs.empty());
|
|
|
|
for (const BondedAtom& bonded_h : hs)
|
|
bonded_h.ptr->calc_flag = CalcFlag::Calculated;
|
|
|
|
// ==== only hydrogens ====
|
|
if (known.size() == 0) {
|
|
// we can only arbitrarily pick directions of atoms
|
|
for (const BondedAtom& bonded_h : hs)
|
|
bonded_h.ptr->occ = 0;
|
|
hs[0].pos = atom.pos + Position(hs[0].dist, 0, 0);
|
|
if (hs.size() > 1) {
|
|
double theta = pi();
|
|
if (const Angle* ang = topo.take_angle(hs[1].ptr, &atom, hs[0].ptr))
|
|
theta = ang->radians();
|
|
hs[1].pos = atom.pos + Position(hs[1].dist * cos(theta),
|
|
hs[1].dist * sin(theta), 0);
|
|
}
|
|
if (hs.size() > 2) {
|
|
const Angle* ang0 = topo.take_angle(hs[2].ptr, &atom, hs[0].ptr);
|
|
const Angle* ang1 = topo.take_angle(hs[2].ptr, &atom, hs[1].ptr);
|
|
double theta0 = rad(ang0 ? ang0->value : 109.47122);
|
|
double theta1 = rad(ang1 ? ang1->value : 109.47122);
|
|
auto pos = position_from_two_angles(atom.pos, hs[0].pos, hs[1].pos,
|
|
hs[2].dist, theta0, theta1);
|
|
hs[2].pos = pos.first;
|
|
// 3 hydrogens: for now happens only NH3 (NH2.cif and NH3.cif)
|
|
// 4 hydrogens: CH4 (CH2.cif) and NH4 (NH4.cif)
|
|
if (hs.size() == 4)
|
|
hs[3].pos = pos.second;
|
|
}
|
|
|
|
// ==== one heavy atom and hydrogens ====
|
|
} else if (known.size() == 1) {
|
|
const BondedAtom& h = hs[0];
|
|
const BondedAtom& heavy = known[0];
|
|
const Restraints::Angle* angle = topo.take_angle(h.ptr, &atom, heavy.ptr);
|
|
if (!angle)
|
|
giveup("No angle restraint for " + h.ptr->name + ".\n", hs);
|
|
if (std::abs(angle->value - 180.0) < 0.5) {
|
|
Vec3 u = atom.pos - h.pos;
|
|
h.pos = atom.pos + Position(u * (h.dist / u.length()));
|
|
if (hs.size() > 1)
|
|
giveup("Unusual: one of two H atoms has angle restraint 180 deg.", hs);
|
|
return;
|
|
}
|
|
double theta = angle->radians();
|
|
double tau = 0.0;
|
|
int period = 0;
|
|
const Atom* tau_end = nullptr;
|
|
auto plane_range = topo.plane_index.equal_range(&atom);
|
|
for (auto i = plane_range.first; i != plane_range.second; ++i) {
|
|
const Topo::Plane& plane = *i->second;
|
|
// only Topo::Plane with atoms.size() >= 4 is put into planes
|
|
if (plane.has(h.ptr) && plane.has(heavy.ptr)) {
|
|
for (const Atom* a : plane.atoms) {
|
|
if (!a->is_hydrogen() && a != &atom && a != heavy.ptr) {
|
|
tau_end = a;
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
Atom* torsion_h = nullptr;
|
|
if (!tau_end) {
|
|
// Using one dihedral angle.
|
|
// We don't check here for which hydrogen the torsion angle is defined.
|
|
// If an atom has 2 or 3 hydrogens, the torsion angle may not be given
|
|
// for the first one, but only for the 2nd or 3rd (e.g. HD22 in ASN).
|
|
auto tor_range = topo.torsion_index.equal_range(&atom);
|
|
for (auto i = tor_range.first; i != tor_range.second; ++i) {
|
|
const Topo::Torsion& tor = *i->second;
|
|
if (tor.atoms[1] == &atom && tor.atoms[2] == heavy.ptr &&
|
|
tor.atoms[0]->is_hydrogen() && !tor.atoms[3]->is_hydrogen()) {
|
|
tau = rad(tor.restr->value);
|
|
torsion_h = tor.atoms[0];
|
|
tau_end = tor.atoms[3];
|
|
period = tor.restr->period;
|
|
break;
|
|
}
|
|
if (tor.atoms[2] == &atom && tor.atoms[1] == heavy.ptr &&
|
|
tor.atoms[3]->is_hydrogen() && !tor.atoms[0]->is_hydrogen()) {
|
|
tau = rad(tor.restr->value);
|
|
torsion_h = tor.atoms[3];
|
|
tau_end = tor.atoms[0];
|
|
period = tor.restr->period;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (tau_end)
|
|
h.pos = position_from_angle_and_torsion(tau_end->pos, heavy.pos, atom.pos,
|
|
h.dist, theta, tau);
|
|
else
|
|
h.pos = arbitrary_position_from_angle(heavy.pos, atom.pos, h.dist, theta);
|
|
if (std::isnan(h.pos.x)) {
|
|
h.pos = Position(0, 0, 0);
|
|
giveup("bonded atoms are exactly overlapping (case 1).", hs);
|
|
}
|
|
if (hs.size() == 2) {
|
|
// I think we can assume the two hydrogens are symmetric.
|
|
Vec3 axis = (heavy.pos - atom.pos).normalized();
|
|
Vec3 perpendicular = get_vector_to_line(hs[0].pos, atom.pos, axis);
|
|
hs[1].pos = hs[0].pos + Position(2 * perpendicular);
|
|
} else if (hs.size() == 3) {
|
|
// Here we assume the three hydrogens are in the same distance from
|
|
// the parent atom and that they make an equilateral triangle.
|
|
// First check for which hydrogen was the torsion restraint (if any).
|
|
int idx = 0;
|
|
for (int i : {1, 2})
|
|
if (torsion_h == hs[i].ptr) {
|
|
idx = i;
|
|
hs[i].pos = h.pos;
|
|
}
|
|
// Now get the other positions by rotation
|
|
Vec3 axis = (heavy.pos - atom.pos).normalized();
|
|
Vec3 v1 = h.pos - atom.pos;
|
|
Vec3 v2 = rotate_about_axis(v1, axis, rad(120));
|
|
Vec3 v3 = rotate_about_axis(v1, axis, rad(-120));
|
|
hs[(idx+1) % 3].pos = atom.pos + Position(v2);
|
|
hs[(idx+2) % 3].pos = atom.pos + Position(v3);
|
|
} else if (hs.size() >= 4) {
|
|
giveup("Unusual: atom bonded to one heavy atoms and 4+ hydrogens.", hs);
|
|
}
|
|
if (!tau_end || period > (int)hs.size())
|
|
for (const BondedAtom& bonded_h : hs)
|
|
bonded_h.ptr->occ = 0;
|
|
// ==== two heavy atoms and hydrogens ====
|
|
} else if (known.size() == 2) {
|
|
if (hs.size() >= 3)
|
|
giveup("Unusual: atom bonded to 2+ heavy atoms and 3+ hydrogens.", hs);
|
|
const Angle* ang1 = topo.take_angle(hs[0].ptr, &atom, known[0].ptr);
|
|
const Angle* ang2 = topo.take_angle(hs[0].ptr, &atom, known[1].ptr);
|
|
const Angle* ang3 = topo.take_angle(known[0].ptr, &atom, known[1].ptr);
|
|
|
|
double theta1 = ang1 ? ang1->radians() : 0;
|
|
double theta2 = ang2 ? ang2->radians() : 0;
|
|
double theta3 = ang3 ? ang3->radians() : 0;
|
|
if (!ang3) {
|
|
// If ang3 atoms form a bonded triangle (e.g. C11-C12-PT in DVW),
|
|
// we can calculate theta3 from the bond lengths.
|
|
const Restraints::Bond* aptr = topo.take_bond(&atom, known[1].ptr);
|
|
const Restraints::Bond* bptr = topo.take_bond(known[0].ptr, known[1].ptr);
|
|
const Restraints::Bond* cptr = topo.take_bond(known[0].ptr, &atom);
|
|
if (!aptr || !bptr || !cptr)
|
|
giveup(cat("Missing angle restraint ", known[0].ptr->name, '-', atom.name,
|
|
'-', known[1].ptr->name, ".\n"), hs);
|
|
theta3 = angle_in_triangle(aptr->value, bptr->value, cptr->value);
|
|
}
|
|
// Some configurations with metals have only distance restraints.
|
|
// In such cases, we assume that hydrogens are as far as possible from the metal.
|
|
if (!ang1 && ang2 && known[0].ptr->element.is_metal())
|
|
theta1 = missing_angle_2H_tetrahedral(topo, atom, hs, theta2, theta3);
|
|
if (!ang2 && ang1 && known[1].ptr->element.is_metal())
|
|
theta2 = missing_angle_2H_tetrahedral(topo, atom, hs, theta1, theta3);
|
|
if (theta1 == 0 || theta2 == 0)
|
|
giveup(cat("Missing angle restraint ", hs[0].ptr->name, '-', atom.name,
|
|
'-', known[theta1 == 0 ? 0 : 1].ptr->name, ".\n"), hs);
|
|
|
|
// Co-planar case. The sum of angles should be 360 degrees,
|
|
// but in some cif files it differs slightly.
|
|
if (theta1 + theta2 + theta3 > rad(360 - 3)) {
|
|
Vec3 v12 = known[0].pos - atom.pos;
|
|
Vec3 v13 = known[1].pos - atom.pos;
|
|
// theta3 is the ideal restraint value, cur_theta3 is the current value
|
|
double cur_theta3 = v12.angle(v13);
|
|
double ratio = (2 * pi() - cur_theta3) / (theta1 + theta2);
|
|
Vec3 axis = v13.cross(v12).normalized();
|
|
Vec3 v14 = rotate_about_axis(v12, axis, theta1 * ratio);
|
|
hs[0].pos = atom.pos + Position(hs[0].dist / v14.length() * v14);
|
|
if (hs.size() > 1) {
|
|
topo.logger.err("Unhandled topology of ", hs.size(), " hydrogens bonded to ", atom.name);
|
|
for (size_t i = 1; i < hs.size(); ++i) {
|
|
hs[i].ptr->occ = 0;
|
|
hs[i].ptr->calc_flag = CalcFlag::Dummy;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Tetrahedral or similar configuration.
|
|
double hh_half = 0;
|
|
if (hs.size() == 2)
|
|
if (const Angle* hh = topo.take_angle(hs[0].ptr, &atom, hs[1].ptr))
|
|
hh_half = 0.5 * hh->radians();
|
|
if (hh_half == 0)
|
|
hh_half = calculate_tetrahedral_delta(theta3, theta1, theta2);
|
|
|
|
// Based on Liebschner et al (2020) doi:10.1016/bs.mie.2020.01.007
|
|
// sec. 2.3. 2H-tetrahedral configuration
|
|
double c0 = std::cos(theta3);
|
|
double c1 = std::cos(theta1);
|
|
double c2 = std::cos(theta2);
|
|
double den = 1 / (1 - c0*c0);
|
|
double a = den * (c1 - c0 * c2);
|
|
double b = den * (c2 - c0 * c1);
|
|
// I think the paper defines u10 and u20 in the opposite direction,
|
|
// but I had to reverse it somewhere to make it work.
|
|
Vec3 u10 = (known[0].pos - atom.pos).normalized();
|
|
Vec3 u20 = (known[1].pos - atom.pos).normalized();
|
|
Vec3 v = u10.cross(u20);
|
|
if (std::isnan(v.x))
|
|
giveup("bonded atoms are exactly overlapping (case 2).", hs);
|
|
Vec3 d = a * u10 + b * u20;
|
|
double dist_sin = hs[0].dist * std::sin(hh_half);
|
|
double dist_cos = hs[0].dist * std::cos(hh_half);
|
|
Vec3 v0s = v.changed_magnitude(dist_sin);
|
|
Vec3 d0c = d.changed_magnitude(dist_cos);
|
|
hs[0].pos = atom.pos + Position(d0c + v0s);
|
|
Position other_pos = atom.pos + Position(d0c - v0s);
|
|
|
|
if (hs.size() == 1) {
|
|
const Topo::Chirality* chir = topo.get_chirality(&atom);
|
|
if (chir && chir->restr->sign != ChiralityType::Both) {
|
|
if (!chir->check())
|
|
hs[0].pos = other_pos;
|
|
} else {
|
|
hs[0].ptr->occ = 0;
|
|
}
|
|
} else { // hs.size() == 2
|
|
hs[1].pos = other_pos;
|
|
}
|
|
|
|
} else { // known.size() >= 3
|
|
if (hs.size() > 1)
|
|
giveup("Unusual: atom bonded to 3+ heavy atoms and 2+ hydrogens.", hs);
|
|
// Based on Liebschner et al (2020) doi:10.1016/bs.mie.2020.01.007
|
|
// sec. 2.4. 1H-tetrahedral configuration
|
|
Vec3 u10 = (known[0].pos - atom.pos).normalized();
|
|
Vec3 u20 = (known[1].pos - atom.pos).normalized();
|
|
Vec3 u30 = (known[2].pos - atom.pos).normalized();
|
|
auto cos_tetrahedral = [&](size_t n) {
|
|
const Angle* angle = topo.take_angle(known[n].ptr, &atom, hs[0].ptr);
|
|
return angle ? std::cos(angle->radians()) : -1./3.;
|
|
};
|
|
SMat33<double> m{1., 1., 1., u10.dot(u20), u10.dot(u30), u20.dot(u30)};
|
|
Vec3 rhs(cos_tetrahedral(0), cos_tetrahedral(1), cos_tetrahedral(2));
|
|
double det = m.determinant();
|
|
if (std::fabs(det) < 1e-12)
|
|
giveup("tetrahedral configuration with four co-planar atoms.", hs);
|
|
Vec3 abc = m.inverse_(det).multiply(rhs);
|
|
Vec3 h_dir = abc.x * u10 + abc.y * u20 + abc.z * u30;
|
|
hs[0].pos = atom.pos + Position(h_dir.changed_magnitude(hs[0].dist));
|
|
}
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
void place_hydrogens_on_all_atoms(Topo& topo) {
|
|
std::vector<BondedAtom> known;
|
|
std::vector<BondedAtom> hs;
|
|
auto filter = [](char alt, const std::vector<BondedAtom>& v) {
|
|
std::vector<BondedAtom> out;
|
|
out.reserve(v.size());
|
|
for (const BondedAtom& ba : v)
|
|
if (ba.ptr->altloc_matches(alt))
|
|
out.push_back(ba);
|
|
return out;
|
|
};
|
|
for (Topo::ChainInfo& chain_info : topo.chain_infos)
|
|
for (Topo::ResInfo& ri : chain_info.res_infos) {
|
|
// If we don't have monomer description from a cif file,
|
|
// only ad-hoc restraints, don't try to place hydrogens.
|
|
if (ri.orig_chemcomp == nullptr)
|
|
continue;
|
|
for (Atom& atom : ri.res->atoms) {
|
|
if (atom.is_hydrogen())
|
|
continue;
|
|
try {
|
|
// gather bonded atoms
|
|
known.clear();
|
|
hs.clear();
|
|
auto range = topo.bond_index.equal_range(&atom);
|
|
for (auto i = range.first; i != range.second; ++i) {
|
|
const Topo::Bond* t = i->second;
|
|
Atom* other = t->atoms[t->atoms[0] == &atom ? 1 : 0];
|
|
if (other->altloc && atom.altloc) {
|
|
// We support links between different altlocs in Topo (e.g. link A-B),
|
|
// although these are rare, special cases.
|
|
// But if we had bonds between atom 1 (A/B) and atom 2 (A/B/C),
|
|
// and we had bonds B-B and B-C, we'd want to use only one of them (B-B).
|
|
// Checking atom's name is not robust, but should suffice here.
|
|
if (atom.altloc != other->altloc &&
|
|
in_vector_f([&](const BondedAtom& a) { return a.ptr->name == other->name; },
|
|
known))
|
|
continue;
|
|
}
|
|
auto& atom_list = other->is_hydrogen() ? hs : known;
|
|
atom_list.push_back({other, other->pos, t->restr->value});
|
|
}
|
|
if (hs.size() == 0)
|
|
continue;
|
|
std::string altlocs;
|
|
// In a special case: Hs with altlocs on a parent without altloc,
|
|
// we need to process conformations one by one.
|
|
if (atom.altloc == '\0')
|
|
for (const auto& h : hs) {
|
|
char alt = h.ptr->altloc;
|
|
if (alt && altlocs.find(alt) == std::string::npos) {
|
|
altlocs += alt;
|
|
place_hydrogens(topo, atom, filter(alt, known), filter(alt, hs));
|
|
}
|
|
}
|
|
// In all other cases, all bonded atoms are from the same conformation.
|
|
if (altlocs.empty())
|
|
place_hydrogens(topo, atom, known, hs);
|
|
} catch (const std::runtime_error& e) {
|
|
topo.logger.err("Placing of hydrogen bonded to ",
|
|
atom_str(chain_info.chain_ref, *ri.res, atom),
|
|
" failed:\n ", e.what());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace gemmi
|