Files
Jungfraujoch/gemmi_gph/polyheur.cpp
T
leonarski_fandClaude Opus 4.8 5addb4483c gemmi_gph: vendor the model / structure-factor headers into the single gemmi target
Extend the vendored GEMMI subset (v0.7.5) with the atomic-model, structure-factor,
bulk-solvent and map machinery so the whole thing builds as one static `gemmi`
library instead of a separate target:

  - add the model/SF/map sources compiled into `gemmi`:
    pdb, resinfo, polyheur, calculate, eig3, ccp4
  - add the v0.7.5 headers these pull in (model.hpp, dencalc.hpp, sfcalc.hpp,
    solmask.hpp, scaling.hpp, fourier.hpp, grid.hpp, ccp4.hpp, it92.hpp, ...)
    plus third_party/pocketfft (FFT), half, tinydir

Only the low-level string/math/symmetry headers were present before; this makes
the vendored copy a complete, self-consistent gemmi that can read a PDB and do
density / structure-factor / map calculations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:27:44 +02:00

464 lines
16 KiB
C++

// Copyright 2017-2022 Global Phasing Ltd.
#include <gemmi/polyheur.hpp>
#include <gemmi/resinfo.hpp> // for find_tabulated_residue
#include <gemmi/modify.hpp> // for rename_residues
#include <gemmi/atox.hpp> // for no_sign_atoi
namespace gemmi {
PolymerType check_polymer_type(const ConstResidueSpan& span, bool ignore_entity_type) {
if (span.empty())
return PolymerType::Unknown;
size_t counts[(int)ResidueKind::ELS+1] = {0};
size_t aa = 0;
size_t na = 0;
size_t total = 0;
bool has_atom_record = false;
for (const Residue& r : span)
if (ignore_entity_type ||
r.entity_type == EntityType::Unknown ||
r.entity_type == EntityType::Polymer) {
if (r.het_flag == 'A')
has_atom_record = true;
const ResidueInfo& info = find_tabulated_residue(r.name);
if (info.found()) {
// Exclude water and ions - it can make difference
// if this function is called for the whole chain.
// Components w/o hydrogens are often ions and always non-polymers
// (and almost never are in a polymer - except PO4, PO2 and AZI (N3)
// which in a few PDB entries are included in polymers - but it
// doesn't matter here).
if (info.kind == ResidueKind::HOH || info.hydrogen_count == 0)
continue;
if (info.is_peptide_linking())
++aa;
if (info.is_na_linking())
++na;
counts[(int)info.kind]++;
} else if (r.get_ca()) {
++aa;
} else if (r.get_p()) {
++na;
}
++total;
}
if (total == 0)
return PolymerType::Unknown;
// One residue is not a polymer, but it may happen that only a single residue
// of a chain is modelled. OTOH, a single non-standard residue is usually
// a ligand.
if (total == 1 && !has_atom_record)
return PolymerType::Unknown;
// ATOM records suggest a polymer, so weaken the condition for AA/NA polymers.
size_t bonus = has_atom_record ? 1 : 0;
if (2 * aa + bonus > total)
return counts[(int)ResidueKind::AA] >= counts[(int)ResidueKind::AAD]
? PolymerType::PeptideL : PolymerType::PeptideD;
if (2 * na + bonus > total) {
if (counts[(int)ResidueKind::DNA] == 0)
return PolymerType::Rna;
if (counts[(int)ResidueKind::RNA] == 0)
return PolymerType::Dna;
return PolymerType::DnaRnaHybrid;
}
return PolymerType::Unknown;
}
std::string make_one_letter_sequence(const ConstResidueSpan& polymer) {
std::string seq;
const Residue* prev = nullptr;
PolymerType ptype = check_polymer_type(polymer);
for (const Residue& residue : polymer.first_conformer()) {
ResidueInfo info = find_tabulated_residue(residue.name);
if (prev && !are_connected2(*prev, residue, ptype))
seq += '-';
seq += (info.one_letter_code != ' ' ? info.one_letter_code : 'X');
prev = &residue;
}
return seq;
}
static std::vector<Residue>::iterator infer_polymer_end(Chain& chain, PolymerType ptype) {
auto b = chain.residues.begin();
auto e = chain.residues.end();
// find the last residue w/ record type ATOM
auto last_a = e;
for (auto it = e; it-- != b;)
if (it->het_flag == 'A')
last_a = it;
for (auto it = b; it != e; ++it) {
ResidueInfo info = find_tabulated_residue(it->name);
if (info.found()) {
if (info.is_water()) {
e = it;
break;
}
bool maybe_linking = (is_polypeptide(ptype) && info.is_peptide_linking())
|| (is_polynucleotide(ptype) && info.is_na_linking());
// The first residue could be non-polymer.
if (!maybe_linking && b != chain.residues.begin()) {
e = it;
break;
}
// If a standard residue is HETATM, it should be in the buffer. Although
// it could happen that a non-standard residue was mutated to a standard
// one, but the record type was not updated, so it's not 100% reliable.
if (info.is_standard() && it->het_flag == 'H' && it > last_a) {
e = it;
break;
}
if (maybe_linking && info.is_standard())
b = it;
}
}
if (b == e || b + 1 == e)
return e;
// Ligands are often separated by a significant gap in the sequence ID numeration.
// But such gap can also mean that part of the chain is not modelled.
auto last = std::min(e, chain.residues.end() - 1);
for (auto it = b; it < last; ++it) {
int gap = *(it+1)->seqid.num - *it->seqid.num;
// The gap should be non-negative, but you can find exceptions in the PDB.
if (gap < -1 || gap > 10)
return it+1;
// Usually polymers are longer than 1-2 residues, although there are
// exceptions (example: 1-residue polymers in 5N22), so we can't be sure.
// OTOH a protein can be capped with monomers different from amino-acid
// and are_connected2() may return false negative. So if there is no gap
// in numbering, it seems better to assume the polymer didn't end yet.
if (gap == 1 && it - chain.residues.begin() < 2)
continue;
if (!are_connected2(*it, *(it+1), ptype))
return it+1;
}
return e;
}
void add_entity_types(Chain& chain, bool overwrite) {
if (!overwrite &&
std::all_of(chain.residues.begin(), chain.residues.end(),
[](const Residue& r) { return r.entity_type != EntityType::Unknown; }))
return;
PolymerType ptype = check_polymer_type(chain.whole(), /*ignore_entity_type=*/overwrite);
auto it = chain.residues.begin();
if (ptype != PolymerType::Unknown) {
auto polymer_end = infer_polymer_end(chain, ptype);
for (; it != polymer_end; ++it)
if (overwrite || it->entity_type == EntityType::Unknown)
it->entity_type = EntityType::Polymer;
}
for (; it != chain.residues.end(); ++it)
if (overwrite || it->entity_type == EntityType::Unknown)
it->entity_type = it->is_water() ? EntityType::Water
: EntityType::NonPolymer;
}
void add_entity_types(Structure& st, bool overwrite) {
for (Model& model : st.models)
for (Chain& chain : model.chains)
add_entity_types(chain, overwrite);
}
void remove_entity_types(Structure& st) {
for (Model& model : st.models)
for (Chain& chain : model.chains)
for (Residue& res : chain.residues)
res.entity_type = EntityType::Unknown;
}
void add_entity_ids(Structure& st, bool overwrite) {
for (Model& model : st.models)
for (Chain& chain : model.chains)
for (ResidueSpan& sub : chain.subchains()) {
if (Entity* ent = st.get_entity_of(sub)) {
for (Residue& res : sub)
if (overwrite || res.entity_id.empty())
res.entity_id = ent->name;
} else if (overwrite) {
for (Residue& res : sub)
res.entity_id.clear();
}
}
}
void assign_subchain_names(Chain& chain, int& nonpolymer_counter) {
for (Residue& res : chain.residues) {
res.subchain = chain.name;
// We'd use '-' as a separator (A-p or B-4 is more clear), but although
// such names are valid in mmCIF, OneDep refuses to accept them.
res.subchain += "x";
switch (res.entity_type) {
case EntityType::Polymer:
res.subchain += 'p';
break;
case EntityType::NonPolymer:
++nonpolymer_counter;
// to keep the name short use base36 for 2+ digit numbers:
// 1, 2, ..., 9, 00, 01, ..., 09, 0A, 0B, ..., 0Z, 10, ...
if (nonpolymer_counter < 10) {
res.subchain += char('0' + nonpolymer_counter);
} else {
const char base36[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
int n = nonpolymer_counter - 10;
if (n < 36)
res.subchain += '0';
size_t pos = res.subchain.size();
while (n != 0) {
res.subchain.insert(res.subchain.begin() + pos, base36[n % 36]);
n /= 36;
}
}
break;
case EntityType::Water:
res.subchain += 'w';
break;
// In the wwPDB branched are kept each in separate auth/label chain.
// So we have one subchain in chain.
case EntityType::Branched:
res.subchain += 'b';
break;
case EntityType::Unknown:
break;
}
}
}
static std::pair<bool,bool> has_entity_types_and_subchains(const Chain& chain) {
bool has_entity_types = true;
bool has_subchains = true;
for (const Residue& res : chain.residues) {
if (res.subchain.empty())
has_subchains = false;
if (res.entity_type == EntityType::Unknown)
has_entity_types = false;
}
return {has_entity_types, has_subchains};
}
void assign_subchains(Structure& st, bool force, bool fail_if_unknown) {
for (Model& model : st.models) {
std::map<std::string, int> counters;
for (Chain& chain : model.chains) {
auto has = has_entity_types_and_subchains(chain);
if (force || !has.second) {
if (has.first) // all chain's residues have known entity_type
assign_subchain_names(chain, counters[chain.name]);
else if (fail_if_unknown)
fail("assign_subchains(): missing entity_type in chain " + chain.name);
}
}
}
}
void ensure_entities(Structure& st) {
for (Model& model : st.models)
for (Chain& chain : model.chains)
for (ResidueSpan& sub : chain.subchains()) {
Entity* ent = st.get_entity_of(sub);
if (!ent) {
EntityType etype = sub[0].entity_type;
std::string name = sub[0].entity_id;
if (name.empty()) {
if (etype == EntityType::Polymer || etype == EntityType::Branched)
name = chain.name;
else if (etype == EntityType::NonPolymer)
name = sub[0].name + "!";
else if (etype == EntityType::Water)
name = "water";
}
if (!name.empty()) {
ent = &impl::find_or_add(st.entities, name);
ent->entity_type = etype;
ent->subchains.push_back(sub.subchain_id());
}
}
// ensure we have polymer_type set where needed
if (ent && ent->entity_type == EntityType::Polymer &&
ent->polymer_type == PolymerType::Unknown)
ent->polymer_type = check_polymer_type(sub);
}
}
static bool operator==(const Entity::DbRef& a, const Entity::DbRef& b) {
return a.db_name == b.db_name &&
a.id_code == b.id_code &&
a.isoform == b.isoform &&
a.seq_begin == b.seq_begin && a.seq_end == b.seq_end &&
a.db_begin == b.db_begin && a.db_end == b.db_end;
}
void deduplicate_entities(Structure& st) {
for (auto i = st.entities.begin(); i != st.entities.end(); ++i)
if (!i->full_sequence.empty())
for (auto j = i + 1; j != st.entities.end(); ++j)
if (j->polymer_type == i->polymer_type &&
j->full_sequence == i->full_sequence &&
j->dbrefs == i->dbrefs) {
vector_move_extend(i->subchains, std::move(j->subchains));
st.entities.erase(j--);
}
}
char recommended_het_flag(const Residue& res) {
if (res.entity_type == EntityType::Unknown)
return '\0';
if (res.entity_type == EntityType::Polymer &&
find_tabulated_residue(res.name).is_standard())
return 'A';
return 'H';
}
bool trim_to_alanine(Residue& res) {
static const std::pair<std::string, El> ala_atoms[6] = {
{"N", El::N}, {"CA", El::C}, {"C", El::C}, {"O", El::O}, {"CB", El::C},
{"OXT", El::O}
};
if (res.get_ca() == nullptr)
return false;
vector_remove_if(res.atoms, [](const Atom& a) {
for (const auto& name_el : ala_atoms)
if (a.name == name_el.first && a.element == name_el.second)
return false;
return true;
});
// if non-standard polymer residue was mutated, update het_flag
if (res.entity_type == EntityType::Polymer && res.het_flag == 'H')
res.het_flag = 'A';
return true;
}
template <size_t I, typename T1, typename T2>
static bool in_vector_at(T1& x, std::vector<T2>& v) {
for (const auto& el : v)
if (std::get<I>(el) == x)
return true;
return false;
}
void shorten_ccd_codes(Structure& st) {
// find all long residue names in both models and sequences
for (Model& model : st.models)
for (Chain& chain : model.chains)
for (Residue& res : chain.residues)
if (res.name.size() > 3 && !in_vector_at<0>(res.name, st.shortened_ccd_codes))
st.shortened_ccd_codes.emplace_back(res.name, "");
for (const Entity& ent : st.entities)
for (const std::string& mon_ids : ent.full_sequence) {
for (size_t start = 0;;) {
size_t end = mon_ids.find(',', start);
size_t len = std::min(end, mon_ids.size()) - start;
if (len > 3) {
std::string s(mon_ids, start, len);
if (!in_vector_at<0>(s, st.shortened_ccd_codes))
st.shortened_ccd_codes.emplace_back(s, "");
}
if (end == std::string::npos)
break;
start = end + 1;
}
}
// the first try on renaming: ABCDE -> ~DE
for (auto& old_new : st.shortened_ccd_codes) {
const std::string& old = old_new.first;
char short_code[4] = {'~', *(old.end()-2), *(old.end()-1), '\0'};
if (!in_vector_at<1>(short_code, st.shortened_ccd_codes))
old_new.second = short_code;
}
// pick a new residue name and call rename_residues()
int i = -1;
for (auto& old_new : st.shortened_ccd_codes) {
// If ~DE was not unique, use ~00, ~01, ...
// After ~99, the middle character will be punctuation or letter.
// After ~Z9 (430+ names), we give up and the names will be empty.
while (old_new.second.empty() && ++i < 'Z'*10) {
char short_code[4] = {'~', char('0' + i/10), char('0' + i%10), '\0'};
if (!in_vector_at<1>(short_code, st.shortened_ccd_codes))
old_new.second = short_code;
}
rename_residues(st, old_new.first, old_new.second);
}
}
void restore_full_ccd_codes(Structure& st) {
for (const auto& item : st.shortened_ccd_codes)
rename_residues(st, item.second, item.first);
st.shortened_ccd_codes.clear();
}
// Unlike _entity_poly_seq, SEQRES doesn't contain alternative residue names.
// This function adds the alternative names to full_sequence.
static void add_microhetero_to_sequence(Entity& ent, ConstResidueSpan polymer) {
ent.reflects_microhetero = false;
if (ent.full_sequence.empty())
return;
int max_n = -1; // max label_seq seen so far
for (const Residue& res : polymer) {
int n = *res.label_seq;
if (size_t(n-1) > ent.full_sequence.size())
return;
std::string& seq_item = ent.full_sequence[n-1];
if (n > max_n) {
if (!is_in_list(res.name, seq_item))
return;
max_n = n;
} else { // n < max_n shouldn't happen
if (!is_in_list(res.name, seq_item))
cat_to(seq_item, ',', res.name);
}
}
ent.reflects_microhetero = true;
}
void add_microhetero_to_sequences(Structure& st, bool overwrite) {
if (st.models.empty())
return;
for (Entity& ent : st.entities) {
if (ent.subchains.empty())
continue;
ConstResidueSpan polymer = st.models[0].get_subchain(ent.subchains[0]);
if (!polymer || !polymer.front().label_seq)
continue;
if (overwrite || !ent.reflects_microhetero)
add_microhetero_to_sequence(ent, polymer);
}
}
void add_tls_group_ids(Structure& st) {
std::vector<TlsGroup>* tls_groups = st.meta.get_tls_groups();
if (!tls_groups)
return;
bool has_ids = false;
for (const Model& model : st.models)
for (const Chain& chain : model.chains)
for (const Residue& res : chain.residues)
for (const Atom& atom : res.atoms)
if (atom.tls_group_id >= 0)
has_ids = true;
if (has_ids)
return;
for (const TlsGroup& tls : *tls_groups) {
// assuming that _pdbx_refine_tls.id is a non-negative number
const char* endptr;
short tls_id = (short) no_sign_atoi(tls.id.c_str(), &endptr);
if (endptr == tls.id.c_str() || *endptr != '\0')
continue;
for (const TlsGroup::Selection& sel : tls.selections) {
// for now we don't use selection_details, only chains and sequence ids
for (Model& model : st.models)
for (Chain& chain : model.chains)
if (chain.name == sel.chain) {
for (Residue& res : chain.residues)
if (sel.res_begin <= res.seqid && res.seqid <= sel.res_end) {
for (Atom& atom : res.atoms)
atom.tls_group_id = tls_id;
}
}
}
}
}
} // namespace gemmi