Build Packages / Unit tests (push) Skipped
Build Packages / build:windows:nocuda (push) Successful in 11m6s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 10m27s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 10m54s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 9m25s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 10m5s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 11m33s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 11m19s
Build Packages / build:rpm (rocky8) (push) Successful in 12m23s
Build Packages / build:rpm (rocky9) (push) Successful in 13m21s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 12m30s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 11m55s
Build Packages / DIALS test (push) Successful in 13m42s
Build Packages / XDS test (durin plugin) (push) Successful in 9m26s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 6m41s
Build Packages / XDS test (neggia plugin) (push) Successful in 6m12s
Build Packages / Generate python client (push) Successful in 19s
Build Packages / Build documentation (push) Successful in 52s
Build Packages / Create release (push) Skipped
Build Packages / build:viewer-tgz:cpu (push) Successful in 5m29s
Build Packages / build:viewer-tgz:cuda (push) Successful in 6m12s
Build Packages / build:windows:cuda (push) Successful in 18m36s
This is an UNSTABLE release. It includes many experimental features, as well as many AI generated fixes. We recommend using rc.152 for production use. * rugnux: Add `--model model.pdb` - score the merged data against an atomic model and compute initial maps. It reports R-work/R-free (scaling the model to the observed amplitudes with an overall scale, an anisotropic B and a flat bulk solvent - the standard few-parameter model, so a batch of maps stays directly comparable) and writes 2Fo-Fc / Fo-Fc electron-density maps (CCP4) plus a map-coefficient MTZ. The structure itself is not refined; the model is only re-fractionalised into the data cell. * rugnux: The merged reflection output now carries French-Wilson amplitudes (|F| and its sigma) next to the intensities - MTZ `F`/`SIGF`, mmCIF `_refln.F_meas_au`, and the text HKL - computed with the correct centric/acentric Wilson prior and epsilon multiplicity, so a downstream program (e.g. phenix.refine) can refine against amplitudes. The intensity columns are unchanged. * rugnux: R-free test-set flags are now assigned deterministically and consistently across symmetry - a Bijvoet pair I(+)/I(-) is never split between the work and free sets, and the assignment is a reproducible per-hkl hash that depends only on the reflection index, so every dataset of one crystal form gets the same ~5% free set (what a multi-dataset campaign such as PanDDA needs). On small data the fraction is floored so the test set stays large enough for a stable R-free (~500 reflections, capped at 10%); it stays flat at 5% on ordinary data. When a reference MTZ carries a `FreeR_flag` column its test set is imported instead, letting a whole campaign inherit one shared free set. * rugnux: A reference MTZ (`--reference-mtz`) can now fix the space group and cell for rotation data too (previously rejected), without being used to scale - the rotation merge stays self-consistent. When the crystal has an indexing (merohedral) ambiguity - a lattice symmetry higher than its Laue symmetry, e.g. P3/P4/P6/C2 - the reference also resolves it: each candidate reindexing (identity plus the twin-law cosets of the metric symmetry) is scored by its intensity correlation against the reference and the data are re-merged in the best-correlating one. This is a metric-preserving relabelling of hkl (the cell is unchanged) and a no-op for a holohedral crystal such as lysozyme. * rugnux: `--model` validation now aligns the data to the model before scoring - the observed reflections are reindexed into the model's enantiomorph when the two differ only by hand (indistinguishable from merged intensities). A merohedral indexing ambiguity is resolved against the reference MTZ when one is given (so a whole campaign shares one indexing convention); only with a model and no reference does validation fall back to fitting each candidate reindexing and keeping the lowest R-free. * rugnux: De-novo symmetry - recover a genuine high-symmetry group whose data are imperfectly scaled. Such a merge's within-orbit chi² lands just past the self-consistency bound (each real symmetry step adds a little systematic scatter), right where a merohedral twin also lands, so the chi² ratio alone cannot separate them. The candidate is now rescued when the extra intensity-proportional systematic error it invokes stays small relative to the confirmed subgroup - a genuine symmetry step gains multiplicity without inflating the merge error model's b, whereas a twin forces non-equivalent reflections together and b balloons. Fixes cubic insulin (I23 instead of I222) with no change to any other crystal in the test battery, including the twins that must stay in their lower symmetry. * Docs: Document the French-Wilson amplitude estimation, R-free flagging, reference-based space-group/ambiguity resolution, and model-based validation/maps in CPU_DATA_ANALYSIS.md. * Frontend: The status-bar pill now shows a progress bar during detector calibration (previously only during measurement), and the calibration state and its button are labelled "Calibration"/"CALIBRATE" (the internal `Pedestal` state name is unchanged for back-compatibility).Reviewed-on: #69 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
748 lines
27 KiB
C++
748 lines
27 KiB
C++
// Copyright 2018 Global Phasing Ltd.
|
|
//
|
|
// ChemComp - chemical component that represents a monomer from Refmac
|
|
// monomer library, or from PDB CCD.
|
|
|
|
#ifndef GEMMI_CHEMCOMP_HPP_
|
|
#define GEMMI_CHEMCOMP_HPP_
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
#include <cmath>
|
|
#include "cifdoc.hpp"
|
|
#include "elem.hpp" // for Element
|
|
#include "fail.hpp" // for fail, unreachable
|
|
#include "numb.hpp" // for as_number
|
|
#include "util.hpp" // for istarts_with, join_str
|
|
#include "model.hpp" // for Residue, Atom
|
|
|
|
namespace gemmi {
|
|
|
|
enum class BondType {
|
|
Unspec, Single, Double, Triple, Aromatic, Deloc, Metal
|
|
};
|
|
enum class ChiralityType { Positive, Negative, Both };
|
|
|
|
struct Restraints {
|
|
struct AtomId {
|
|
int comp;
|
|
std::string atom;
|
|
|
|
bool operator==(const AtomId& o) const {
|
|
return comp == o.comp && atom == o.atom;
|
|
}
|
|
bool operator!=(const AtomId& o) const { return !operator==(o); }
|
|
|
|
bool operator==(const std::string& name) const { return atom == name; }
|
|
bool operator!=(const std::string& name) const { return atom != name; }
|
|
|
|
bool operator<(const AtomId& o) const {
|
|
return comp == o.comp ? atom < o.atom : comp < o.comp;
|
|
}
|
|
|
|
// altloc2 is needed only in rare case when we have a link between
|
|
// atoms with different altloc (example: 2e7z).
|
|
Atom* get_from(Residue& res1, Residue* res2, char alt, char altloc2) const {
|
|
Residue* residue = &res1;
|
|
if (comp == 2 && res2 != nullptr) {
|
|
residue = res2;
|
|
if (altloc2 != '\0')
|
|
alt = altloc2;
|
|
}
|
|
Atom* a = residue->find_atom(atom, alt, El::X, false);
|
|
// Special case: microheterogeneity may have shared atoms only in
|
|
// the first residue. Example: in 1ejg N is shared between PRO and SER.
|
|
if (a == nullptr && alt != '\0' && residue->group_idx > 0)
|
|
a = (residue - residue->group_idx)->find_atom(atom, alt, El::X, false);
|
|
return a;
|
|
}
|
|
const Atom* get_from(const Residue& res1, const Residue* res2,
|
|
char alt, char alt2) const {
|
|
return get_from(const_cast<Residue&>(res1), const_cast<Residue*>(res2), alt, alt2);
|
|
}
|
|
};
|
|
|
|
static std::string lexicographic_str(const std::string& name1,
|
|
const std::string& name2) {
|
|
return name1 < name2 ? cat(name1, '-', name2) : cat(name2, '-', name1);
|
|
}
|
|
|
|
enum class DistanceOf { ElectronCloud, Nucleus };
|
|
|
|
struct Bond {
|
|
static const char* what() { return "bond"; }
|
|
AtomId id1, id2;
|
|
BondType type;
|
|
bool aromatic;
|
|
double value;
|
|
double esd;
|
|
double value_nucleus;
|
|
double esd_nucleus;
|
|
std::string str() const { return cat(id1.atom, '-', id2.atom); }
|
|
std::string lexicographic_str() const {
|
|
return Restraints::lexicographic_str(id1.atom, id2.atom);
|
|
}
|
|
double distance(DistanceOf of) const {
|
|
return of == DistanceOf::ElectronCloud ? value : value_nucleus;
|
|
}
|
|
template<typename T> const AtomId* other(const T& a) const {
|
|
if (id1 == a) return &id2;
|
|
if (id2 == a) return &id1;
|
|
return nullptr;
|
|
}
|
|
};
|
|
|
|
struct Angle {
|
|
static const char* what() { return "angle"; }
|
|
AtomId id1, id2, id3;
|
|
double value; // degrees
|
|
double esd;
|
|
double radians() const { return rad(value); }
|
|
std::string str() const {
|
|
return cat(id1.atom, '-', id2.atom, '-', id3.atom);
|
|
}
|
|
};
|
|
|
|
struct Torsion {
|
|
static const char* what() { return "torsion"; }
|
|
std::string label;
|
|
AtomId id1, id2, id3, id4;
|
|
double value;
|
|
double esd;
|
|
int period;
|
|
std::string str() const {
|
|
return cat(id1.atom, '-', id2.atom, '-', id3.atom, '-', id4.atom);
|
|
}
|
|
};
|
|
|
|
struct Chirality {
|
|
static const char* what() { return "chirality"; }
|
|
AtomId id_ctr, id1, id2, id3;
|
|
ChiralityType sign;
|
|
|
|
bool is_wrong(double volume) const {
|
|
return (sign == ChiralityType::Positive && volume < 0) ||
|
|
(sign == ChiralityType::Negative && volume > 0);
|
|
}
|
|
std::string str() const {
|
|
return cat(id_ctr.atom, ',', id1.atom, ',', id2.atom, ',', id3.atom);
|
|
}
|
|
};
|
|
|
|
struct Plane {
|
|
static const char* what() { return "plane"; }
|
|
std::string label;
|
|
std::vector<AtomId> ids;
|
|
double esd;
|
|
std::string str() const {
|
|
return join_str(ids, ',', [](const AtomId& a) { return a.atom; });
|
|
}
|
|
};
|
|
|
|
std::vector<Bond> bonds;
|
|
std::vector<Angle> angles;
|
|
std::vector<Torsion> torsions;
|
|
std::vector<Chirality> chirs;
|
|
std::vector<Plane> planes;
|
|
|
|
bool empty() const {
|
|
return bonds.empty() && angles.empty() && torsions.empty() &&
|
|
chirs.empty() && planes.empty();
|
|
}
|
|
|
|
template<typename T>
|
|
std::vector<Bond>::iterator find_bond(const T& a1, const T& a2) {
|
|
return std::find_if(bonds.begin(), bonds.end(), [&](const Bond& b) {
|
|
return (b.id1 == a1 && b.id2 == a2) || (b.id1 == a2 && b.id2 == a1);
|
|
});
|
|
}
|
|
template<typename T>
|
|
std::vector<Bond>::const_iterator find_bond(const T& a1, const T& a2) const {
|
|
return const_cast<Restraints*>(this)->find_bond(a1, a2);
|
|
}
|
|
const Bond& get_bond(const AtomId& a1, const AtomId& a2) const {
|
|
auto it = find_bond(a1, a2);
|
|
if (it == bonds.end())
|
|
fail("Bond restraint not found: ", a1.atom, '-', a2.atom);
|
|
return *it;
|
|
}
|
|
|
|
template<typename T>
|
|
bool are_bonded(const T& a1, const T& a2) const {
|
|
return find_bond(a1, a2) != bonds.end();
|
|
}
|
|
|
|
template<typename T>
|
|
const AtomId* first_bonded_atom(const T& a) const {
|
|
for (const Bond& bond : bonds)
|
|
if (const AtomId* other = bond.other(a))
|
|
return other;
|
|
return nullptr;
|
|
}
|
|
|
|
// BFS
|
|
std::vector<AtomId> find_shortest_path(const AtomId& a, const AtomId& b,
|
|
std::vector<AtomId> visited,
|
|
int min_length=1) const {
|
|
int start = (int) visited.size();
|
|
int end = -1;
|
|
visited.push_back(b);
|
|
std::vector<int> parent(visited.size(), -1);
|
|
std::vector<int> depth(visited.size(), -1);
|
|
depth[start] = 0;
|
|
for (int n = start; end == -1 && n != (int) visited.size(); ++n) {
|
|
for (const Bond& bond : bonds)
|
|
if (const AtomId* id = bond.other(visited[n])) {
|
|
if (*id == a) {
|
|
int cand_len = depth[n] + 1;
|
|
if (cand_len >= min_length) {
|
|
end = (int) visited.size();
|
|
if (!in_vector(*id, visited)) {
|
|
visited.push_back(*id);
|
|
parent.push_back(n);
|
|
depth.push_back(cand_len);
|
|
}
|
|
}
|
|
} else if (!in_vector(*id, visited)) {
|
|
visited.push_back(*id);
|
|
parent.push_back(n);
|
|
depth.push_back(depth[n] + 1);
|
|
}
|
|
}
|
|
}
|
|
std::vector<AtomId> path;
|
|
for (int n = end; n != -1; n = parent[n])
|
|
path.push_back(visited[n]);
|
|
return path;
|
|
}
|
|
|
|
template<typename T>
|
|
std::vector<Angle>::iterator find_angle(const T& a, const T& b, const T& c) {
|
|
return std::find_if(angles.begin(), angles.end(), [&](const Angle& ang) {
|
|
return ang.id2 == b && ((ang.id1 == a && ang.id3 == c) ||
|
|
(ang.id1 == c && ang.id3 == a));
|
|
});
|
|
}
|
|
template<typename T> std::vector<Angle>::const_iterator
|
|
find_angle(const T& a, const T& b, const T& c) const {
|
|
return const_cast<Restraints*>(this)->find_angle(a, b, c);
|
|
}
|
|
const Angle& get_angle(const AtomId& a, const AtomId& b, const AtomId& c) const {
|
|
auto it = const_cast<Restraints*>(this)->find_angle(a, b, c);
|
|
if (it == angles.end())
|
|
fail("Angle restraint not found: ", a.atom, '-', b.atom, '-', c.atom);
|
|
return *it;
|
|
}
|
|
|
|
template<typename T>
|
|
std::vector<Torsion>::iterator find_torsion(const T& a, const T& b,
|
|
const T& c, const T& d) {
|
|
return std::find_if(torsions.begin(), torsions.end(),
|
|
[&](const Torsion& t) {
|
|
return (t.id1 == a && t.id2 == b && t.id3 == c && t.id4 == d) ||
|
|
(t.id1 == d && t.id2 == c && t.id3 == b && t.id4 == a);
|
|
});
|
|
}
|
|
template<typename T> std::vector<Torsion>::const_iterator
|
|
find_torsion(const T& a, const T& b, const T& c, const T& d) const {
|
|
return const_cast<Restraints*>(this)->find_torsion(a, b, c, d);
|
|
}
|
|
|
|
template<typename T>
|
|
std::vector<Chirality>::iterator find_chir(const T& ctr, const T& a,
|
|
const T& b, const T& c) {
|
|
return std::find_if(chirs.begin(), chirs.end(), [&](const Chirality& t) {
|
|
return t.id_ctr == ctr && ((t.id1 == a && t.id2 == b && t.id3 == c) ||
|
|
(t.id1 == b && t.id2 == c && t.id3 == a) ||
|
|
(t.id1 == c && t.id2 == a && t.id3 == b));
|
|
});
|
|
}
|
|
template<typename T> std::vector<Chirality>::const_iterator
|
|
find_chir(const T& ctr, const T& a, const T& b, const T& c) const {
|
|
return const_cast<Restraints*>(this)->find_chir(ctr, a, b, c);
|
|
}
|
|
|
|
double chiral_abs_volume(const Restraints::Chirality& ch) const;
|
|
|
|
std::vector<Plane>::iterator get_plane(const std::string& label) {
|
|
return std::find_if(planes.begin(), planes.end(),
|
|
[&label](const Plane& p) { return p.label == label; });
|
|
}
|
|
|
|
Plane& get_or_add_plane(const std::string& label) {
|
|
std::vector<Plane>::iterator it = get_plane(label);
|
|
if (it != planes.end())
|
|
return *it;
|
|
planes.push_back(Plane{label, {}, 0.0});
|
|
return planes.back();
|
|
}
|
|
|
|
void rename_atom(const AtomId& atom_id, const std::string& new_name) {
|
|
auto rename_atom = [&](AtomId& id) {
|
|
if (id == atom_id)
|
|
id.atom = new_name;
|
|
};
|
|
for (Bond& bond : bonds) {
|
|
rename_atom(bond.id1);
|
|
rename_atom(bond.id2);
|
|
}
|
|
for (Angle& angle : angles) {
|
|
rename_atom(angle.id1);
|
|
rename_atom(angle.id2);
|
|
rename_atom(angle.id3);
|
|
}
|
|
for (Torsion& tor : torsions) {
|
|
rename_atom(tor.id1);
|
|
rename_atom(tor.id2);
|
|
rename_atom(tor.id3);
|
|
rename_atom(tor.id4);
|
|
}
|
|
for (Chirality& chir : chirs) {
|
|
rename_atom(chir.id_ctr);
|
|
rename_atom(chir.id1);
|
|
rename_atom(chir.id2);
|
|
rename_atom(chir.id3);
|
|
}
|
|
for (Plane& plane : planes)
|
|
for (AtomId& id : plane.ids)
|
|
rename_atom(id);
|
|
}
|
|
};
|
|
|
|
template<typename Restr>
|
|
double angle_z(double value_rad, const Restr& restr, double full=360.) {
|
|
return angle_abs_diff(deg(value_rad), restr.value, full) / restr.esd;
|
|
}
|
|
|
|
inline double chiral_abs_volume(double bond1, double bond2, double bond3,
|
|
double angle1, double angle2, double angle3) {
|
|
double mult = bond1 * bond2 * bond3;
|
|
double x = 1;
|
|
double y = 2;
|
|
for (double a : {angle1, angle2, angle3}) {
|
|
double cosine = a == 90. ? 0. : std::cos(rad(a));
|
|
x -= cosine * cosine;
|
|
y *= cosine;
|
|
}
|
|
return mult * std::sqrt(std::max(0., x + y));
|
|
}
|
|
|
|
inline double Restraints::chiral_abs_volume(const Restraints::Chirality& ch) const {
|
|
return gemmi::chiral_abs_volume(get_bond(ch.id_ctr, ch.id1).value,
|
|
get_bond(ch.id_ctr, ch.id2).value,
|
|
get_bond(ch.id_ctr, ch.id3).value,
|
|
get_angle(ch.id1, ch.id_ctr, ch.id2).value,
|
|
get_angle(ch.id2, ch.id_ctr, ch.id3).value,
|
|
get_angle(ch.id3, ch.id_ctr, ch.id1).value);
|
|
}
|
|
|
|
struct ChemComp {
|
|
// Items used in _chem_comp.group and _chem_link.group_comp_N in CCP4.
|
|
enum class Group {
|
|
Peptide, // "peptide"
|
|
PPeptide, // "P-peptide"
|
|
MPeptide, // "M-peptide"
|
|
Dna, // "DNA" - used in _chem_comp.group
|
|
Rna, // "RNA" - used in _chem_comp.group
|
|
DnaRna, // "DNA/RNA" - used in _chem_link.group_comp_N
|
|
Pyranose, // "pyranose"
|
|
Ketopyranose, // "ketopyranose"
|
|
Furanose, // "furanose"
|
|
NonPolymer, // "non-polymer"
|
|
Null
|
|
};
|
|
|
|
struct Atom {
|
|
std::string id;
|
|
std::string old_id; // read from _chem_comp_atom.alt_atom_id
|
|
Element el = El::X;
|
|
// _chem_comp_atom.partial_charge can be non-integer,
|
|
// _chem_comp_atom.charge is always integer (but sometimes has format
|
|
// '0.000' which is not correct but we ignore it).
|
|
float charge = 0;
|
|
std::string chem_type;
|
|
std::string acedrg_type; // read from _chem_comp_atom.atom_type
|
|
Position xyz{NAN, NAN, NAN};
|
|
|
|
bool is_hydrogen() const { return gemmi::is_hydrogen(el); }
|
|
};
|
|
|
|
struct Aliasing {
|
|
Group group;
|
|
// pairs of (name in chem_comp, usual name in this group)
|
|
std::vector<std::pair<std::string, std::string>> related;
|
|
|
|
const std::string* name_from_alias(const std::string& atom_id) const {
|
|
for (const auto& item : related)
|
|
if (item.second == atom_id)
|
|
return &item.first;
|
|
return nullptr;
|
|
}
|
|
};
|
|
|
|
std::string name;
|
|
std::string type_or_group; // _chem_comp.type or _chem_comp.group
|
|
Group group = Group::Null;
|
|
bool has_descriptor = false; // true if _pdbx_chem_comp_descriptor present
|
|
bool has_coordinates = false;
|
|
std::vector<Atom> atoms;
|
|
std::vector<Aliasing> aliases;
|
|
Restraints rt;
|
|
|
|
const Aliasing& get_aliasing(Group g) const {
|
|
for (const Aliasing& aliasing : aliases)
|
|
if (aliasing.group == g)
|
|
return aliasing;
|
|
fail("aliasing not found");
|
|
}
|
|
|
|
static Group read_group(const std::string& str) {
|
|
if (str.size() >= 3) {
|
|
const char* cstr = str.c_str();
|
|
if ((str[0] == '\'' || str[0] == '"') && str.size() >= 5)
|
|
++cstr;
|
|
switch (ialpha4_id(cstr)) {
|
|
case ialpha4_id("non-"): return Group::NonPolymer;
|
|
case ialpha4_id("pept"): return Group::Peptide;
|
|
case ialpha4_id("l-pe"): return Group::Peptide;
|
|
case ialpha4_id("p-pe"): return Group::PPeptide;
|
|
case ialpha4_id("m-pe"): return Group::MPeptide;
|
|
case ialpha4_id("dna"): return Group::Dna;
|
|
case ialpha4_id("rna"): return Group::Rna;
|
|
case ialpha4_id("dna/"): return Group::DnaRna;
|
|
case ialpha4_id("pyra"): return Group::Pyranose;
|
|
case ialpha4_id("keto"): return Group::Ketopyranose;
|
|
case ialpha4_id("fura"): return Group::Furanose;
|
|
}
|
|
}
|
|
return Group::Null;
|
|
}
|
|
|
|
static const char* group_str(Group g) {
|
|
switch (g) {
|
|
case Group::Peptide: return "peptide";
|
|
case Group::PPeptide: return "P-peptide";
|
|
case Group::MPeptide: return "M-peptide";
|
|
case Group::Dna: return "DNA";
|
|
case Group::Rna: return "RNA";
|
|
case Group::DnaRna: return "DNA/RNA";
|
|
case Group::Pyranose: return "pyranose";
|
|
case Group::Ketopyranose: return "ketopyranose";
|
|
case Group::Furanose: return "furanose";
|
|
case Group::NonPolymer: return "non-polymer";
|
|
case Group::Null: return ".";
|
|
}
|
|
unreachable();
|
|
}
|
|
|
|
void set_group(const std::string& s) {
|
|
type_or_group = s;
|
|
group = read_group(s);
|
|
}
|
|
|
|
std::vector<Atom>::iterator find_atom(const std::string& atom_id) {
|
|
return std::find_if(atoms.begin(), atoms.end(),
|
|
[&](const Atom& a) { return a.id == atom_id; });
|
|
}
|
|
std::vector<Atom>::const_iterator find_atom(const std::string& atom_id) const {
|
|
return const_cast<ChemComp*>(this)->find_atom(atom_id);
|
|
}
|
|
bool has_atom(const std::string& atom_id) const {
|
|
return find_atom(atom_id) != atoms.end();
|
|
}
|
|
|
|
std::vector<Atom>::iterator find_atom_by_old_name(const std::string& old_id) {
|
|
return std::find_if(atoms.begin(), atoms.end(),
|
|
[&](const Atom& a) { return a.old_id == old_id; });
|
|
}
|
|
std::vector<Atom>::const_iterator find_atom_by_old_name(const std::string& old_id) const {
|
|
return const_cast<ChemComp*>(this)->find_atom_by_old_name(old_id);
|
|
}
|
|
bool has_old_names() const {
|
|
return std::any_of(atoms.begin(), atoms.end(),
|
|
[&](const Atom& a) { return !a.old_id.empty() && a.old_id != a.id; });
|
|
}
|
|
|
|
int get_atom_index(const std::string& atom_id) const {
|
|
auto it = find_atom(atom_id);
|
|
if (it == atoms.end())
|
|
fail("Chemical component ", name, " has no atom ", atom_id);
|
|
return int(it - atoms.begin());
|
|
}
|
|
|
|
int find_atom_index(const std::string& atom_id) const {
|
|
auto it = find_atom(atom_id);
|
|
return it != atoms.end() ? int(it - atoms.begin()) : -1;
|
|
}
|
|
|
|
std::map<std::string, size_t> make_atom_index() const {
|
|
std::map<std::string, size_t> atom_index;
|
|
for (size_t i = 0; i < atoms.size(); ++i)
|
|
atom_index[atoms[i].id] = i;
|
|
return atom_index;
|
|
}
|
|
|
|
const Atom& get_atom(const std::string& atom_id) const {
|
|
return atoms[get_atom_index(atom_id)];
|
|
}
|
|
|
|
/// Check if the group (M-|P-)peptide
|
|
static bool is_peptide_group(Group g) {
|
|
return g == Group::Peptide || g == Group::PPeptide || g == Group::MPeptide;
|
|
}
|
|
|
|
/// Check if the group is DNA/RNA
|
|
static bool is_nucleotide_group(Group g) {
|
|
return g == Group::Dna || g == Group::Rna || g == Group::DnaRna;
|
|
}
|
|
|
|
void remove_nonmatching_restraints() {
|
|
vector_remove_if(rt.bonds, [&](const Restraints::Bond& x) {
|
|
return !has_atom(x.id1.atom) ||
|
|
!has_atom(x.id2.atom);
|
|
});
|
|
vector_remove_if(rt.angles, [&](const Restraints::Angle& x) {
|
|
return !has_atom(x.id1.atom) ||
|
|
!has_atom(x.id2.atom) ||
|
|
!has_atom(x.id3.atom);
|
|
});
|
|
vector_remove_if(rt.torsions, [&](const Restraints::Torsion& x) {
|
|
return !has_atom(x.id1.atom) ||
|
|
!has_atom(x.id2.atom) ||
|
|
!has_atom(x.id3.atom) ||
|
|
!has_atom(x.id4.atom);
|
|
});
|
|
vector_remove_if(rt.chirs, [&](const Restraints::Chirality& x) {
|
|
return !has_atom(x.id_ctr.atom) ||
|
|
!has_atom(x.id1.atom) ||
|
|
!has_atom(x.id2.atom) ||
|
|
!has_atom(x.id3.atom);
|
|
});
|
|
for (Restraints::Plane& plane : rt.planes)
|
|
vector_remove_if(plane.ids, [&](const Restraints::AtomId& x) {
|
|
return !has_atom(x.atom);
|
|
});
|
|
}
|
|
|
|
ChemComp& remove_hydrogens() {
|
|
vector_remove_if(atoms, [](const ChemComp::Atom& a) {
|
|
return a.is_hydrogen();
|
|
});
|
|
remove_nonmatching_restraints();
|
|
return *this;
|
|
}
|
|
};
|
|
|
|
inline BondType bond_type_from_string(const std::string& s) {
|
|
if (s.size() >= 3)
|
|
switch (ialpha4_id(s.c_str())) {
|
|
case ialpha4_id("sing"): return BondType::Single;
|
|
case ialpha4_id("doub"): return BondType::Double;
|
|
case ialpha4_id("trip"): return BondType::Triple;
|
|
case ialpha4_id("arom"): return BondType::Aromatic;
|
|
case ialpha4_id("meta"): return BondType::Metal;
|
|
case ialpha4_id("delo"): return BondType::Deloc;
|
|
case ialpha4_id("1.5"): return BondType::Deloc; // rarely used
|
|
// program PDB2TNT produces a restraint file with bond type 'coval'
|
|
case ialpha4_id("cova"): return BondType::Unspec;
|
|
}
|
|
if (cif::is_null(s))
|
|
return BondType::Unspec;
|
|
throw std::out_of_range("Unexpected bond type: " + s);
|
|
}
|
|
|
|
inline const char* bond_type_to_string(BondType btype) {
|
|
switch (btype) {
|
|
case BondType::Unspec: return ".";
|
|
case BondType::Single: return "single";
|
|
case BondType::Double: return "double";
|
|
case BondType::Triple: return "triple";
|
|
case BondType::Aromatic: return "aromatic";
|
|
case BondType::Deloc: return "deloc";
|
|
case BondType::Metal: return "metal";
|
|
}
|
|
unreachable();
|
|
}
|
|
|
|
inline float order_of_bond_type(BondType btype) {
|
|
switch (btype) {
|
|
case BondType::Single: return 1.0f;
|
|
case BondType::Double: return 2.0f;
|
|
case BondType::Triple: return 3.0f;
|
|
case BondType::Aromatic: return 1.5f;
|
|
case BondType::Deloc: return 1.5f;
|
|
case BondType::Metal: return 1.0f;
|
|
case BondType::Unspec: return 0.0f;
|
|
}
|
|
unreachable();
|
|
}
|
|
|
|
// it doesn't handle crossN types from the monomer library
|
|
inline ChiralityType chirality_from_string(const std::string& s) {
|
|
switch (s[0] | 0x20) {
|
|
case 'p': return ChiralityType::Positive;
|
|
case 'n': return ChiralityType::Negative;
|
|
case 'b': return ChiralityType::Both;
|
|
case '.': return ChiralityType::Both;
|
|
default: throw std::out_of_range("Unexpected chirality: " + s);
|
|
}
|
|
}
|
|
|
|
inline ChiralityType chirality_from_flag_and_volume(const std::string& s,
|
|
double volume) {
|
|
switch (s[0] | 0x20) {
|
|
case 's': return volume > 0 ? ChiralityType::Positive
|
|
: ChiralityType::Negative;
|
|
case 'n': return ChiralityType::Both;
|
|
default: throw std::out_of_range("Unexpected volume_flag: " + s);
|
|
}
|
|
}
|
|
|
|
inline const char* chirality_to_string(ChiralityType chir_type) {
|
|
switch (chir_type) {
|
|
case ChiralityType::Positive: return "positive";
|
|
case ChiralityType::Negative: return "negative";
|
|
case ChiralityType::Both: return "both";
|
|
}
|
|
unreachable();
|
|
}
|
|
|
|
inline ChemComp make_chemcomp_from_block(const cif::Block& block_) {
|
|
ChemComp cc;
|
|
cc.name = block_.name.substr(starts_with(block_.name, "comp_") ? 5 : 0);
|
|
cif::Block& block = const_cast<cif::Block&>(block_);
|
|
// CCD uses _chem_comp.type, monomer libraries use .group in a separate block
|
|
// named comp_list, but it'd be a better idea to have _chem_comp.group in the
|
|
// same block, so we try read it.
|
|
if (cif::Column group_col = block.find_values("_chem_comp.group"))
|
|
cc.set_group(group_col.str(0));
|
|
else if (cif::Column type_col = block.find_values("_chem_comp.type"))
|
|
cc.type_or_group = type_col.str(0);
|
|
// Presence of descriptor category (AceDRG uses it to gate torsion corrections)
|
|
cc.has_descriptor = block.find("_pdbx_chem_comp_descriptor.", {"comp_id"}).ok();
|
|
for (auto row : block.find("_chem_comp_atom.",
|
|
{"atom_id", "type_symbol", "?type_energy",
|
|
"?charge", "?partial_charge", "?alt_atom_id",
|
|
"?atom_type",
|
|
"?model_Cartn_x",
|
|
"?model_Cartn_y",
|
|
"?model_Cartn_z",
|
|
"?x",
|
|
"?y",
|
|
"?z",
|
|
"?pdbx_model_Cartn_x_ideal",
|
|
"?pdbx_model_Cartn_y_ideal",
|
|
"?pdbx_model_Cartn_z_ideal"})) {
|
|
ChemComp::Atom atom;
|
|
atom.id = row.str(0);
|
|
atom.old_id = row.has(5) ? row.str(5) : "";
|
|
atom.el = Element(row.str(1));
|
|
atom.charge = (float) cif::as_number(row.one_of(3, 4), 0.0);
|
|
atom.chem_type = row.has(2) ? row.str(2) : "";
|
|
atom.acedrg_type = row.has(6) ? row.str(6) : "";
|
|
auto set_xyz_if_finite = [&](int ix, int iy, int iz) {
|
|
if (!row.has(ix) || !row.has(iy) || !row.has(iz))
|
|
return false;
|
|
double x = cif::as_number(row[ix]);
|
|
double y = cif::as_number(row[iy]);
|
|
double z = cif::as_number(row[iz]);
|
|
if (!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z))
|
|
return false;
|
|
atom.xyz = Position(x, y, z);
|
|
return true;
|
|
};
|
|
if (!set_xyz_if_finite(7, 8, 9) &&
|
|
!set_xyz_if_finite(10, 11, 12))
|
|
set_xyz_if_finite(13, 14, 15);
|
|
cc.atoms.push_back(std::move(atom));
|
|
}
|
|
// Also check _chem_comp_acedrg table for atom types (used by acedrg output)
|
|
for (auto row : block.find("_chem_comp_acedrg.", {"atom_id", "atom_type"})) {
|
|
std::string atom_id = row.str(0);
|
|
std::string atom_type = row.str(1);
|
|
for (auto& atom : cc.atoms)
|
|
if (atom.id == atom_id && atom.acedrg_type.empty()) {
|
|
atom.acedrg_type = atom_type;
|
|
break;
|
|
}
|
|
}
|
|
for (auto row : block.find("_chem_comp_bond.",
|
|
{"atom_id_1", "atom_id_2", // 0, 1
|
|
"?type", "?value_order", // 2, 3
|
|
"?aromatic", "?pdbx_aromatic_flag", // 4, 5
|
|
"?value_dist", "?value_dist_esd", // 6, 7
|
|
"?value_dist_nucleus", "?value_dist_nucleus_esd"})) { // 8, 9
|
|
bool aromatic_flag = (row.one_of(4, 5)[0] | 0x20) == 'y';
|
|
double dist = row.has(6) ? cif::as_number(row[6]) : NAN;
|
|
double esd = row.has(7) ? cif::as_number(row[7]) : NAN;
|
|
double dist_nucl = row.has(8) ? cif::as_number(row[8]) : NAN;
|
|
double esd_nucl = row.has(9) ? cif::as_number(row[9]) : NAN;
|
|
cc.rt.bonds.push_back({{1, row.str(0)}, {1, row.str(1)},
|
|
bond_type_from_string(row.one_of(2, 3)),
|
|
aromatic_flag, dist, esd, dist_nucl, esd_nucl});
|
|
}
|
|
for (auto row : block.find("_chem_comp_angle.",
|
|
{"atom_id_1", "atom_id_2", "atom_id_3",
|
|
"value_angle", "value_angle_esd"}))
|
|
cc.rt.angles.push_back({{1, row.str(0)}, {1, row.str(1)}, {1, row.str(2)},
|
|
cif::as_number(row[3]), cif::as_number(row[4])});
|
|
for (auto row : block.find("_chem_comp_tor.",
|
|
{"id",
|
|
"atom_id_1", "atom_id_2",
|
|
"atom_id_3", "atom_id_4",
|
|
"value_angle", "value_angle_esd",
|
|
"period"}))
|
|
cc.rt.torsions.push_back({row.str(0),
|
|
{1, row.str(1)}, {1, row.str(2)},
|
|
{1, row.str(3)}, {1, row.str(4)},
|
|
cif::as_number(row[5]), cif::as_number(row[6]),
|
|
cif::as_int(row[7])});
|
|
for (auto row : block.find("_chem_comp_chir.",
|
|
{"atom_id_centre",
|
|
"atom_id_1", "atom_id_2", "atom_id_3",
|
|
"volume_sign"}))
|
|
if (row[4][0] != 'c') // ignore crossN
|
|
cc.rt.chirs.push_back({{1, row.str(0)},
|
|
{1, row.str(1)}, {1, row.str(2)}, {1, row.str(3)},
|
|
chirality_from_string(row[4])});
|
|
// mmCIF compliant
|
|
cif::Table chir_tab = block.find("_chem_comp_chir_atom.",
|
|
{"chir_id", "atom_id"});
|
|
if (chir_tab.ok()) {
|
|
std::map<std::string, std::vector<std::string>> chir_atoms;
|
|
for (auto chir : chir_tab)
|
|
chir_atoms[chir[0]].push_back(chir[1]);
|
|
for (auto row : block.find("_chem_comp_chir.",
|
|
{"id", "atom_id", "volume_flag", "volume_three"})) {
|
|
auto atoms = chir_atoms.find(row.str(0));
|
|
if (atoms != chir_atoms.end() && atoms->second.size() == 3)
|
|
cc.rt.chirs.push_back({{1, row.str(1)},
|
|
{1, atoms->second[0]}, {1, atoms->second[1]},
|
|
{1, atoms->second[2]},
|
|
chirality_from_flag_and_volume(row[2],
|
|
cif::as_number(row[3]))});
|
|
}
|
|
}
|
|
for (auto row : block.find("_chem_comp_plane_atom.",
|
|
{"plane_id", "atom_id" , "dist_esd"})) {
|
|
Restraints::Plane& plane = cc.rt.get_or_add_plane(row.str(0));
|
|
if (plane.esd == 0.0)
|
|
plane.esd = cif::as_number(row[2]);
|
|
plane.ids.push_back({1, row.str(1)});
|
|
}
|
|
for (auto row : block.find("_chem_comp_alias.",
|
|
{"group", "atom_id", "atom_id_standard"})) {
|
|
ChemComp::Group group = ChemComp::read_group(row.str(0));
|
|
if (cc.aliases.empty() || cc.aliases.back().group != group) {
|
|
cc.aliases.emplace_back();
|
|
cc.aliases.back().group = group;
|
|
}
|
|
cc.aliases.back().related.emplace_back(row.str(1), row.str(2));
|
|
}
|
|
|
|
return cc;
|
|
}
|
|
|
|
} // namespace gemmi
|
|
#endif
|