Files
Jungfraujoch/gemmi_gph/gemmi/polyheur.hpp
T
leonarski_f dd0bffb283
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
v1.0.0-rc.159 (#69)
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>
2026-07-13 13:54:03 +02:00

204 lines
7.8 KiB
C++

// Copyright 2017-2018 Global Phasing Ltd.
//
// Heuristic methods for working with chains and polymers.
// Also includes a few well-defined functions, such as removal of waters.
#ifndef GEMMI_POLYHEUR_HPP_
#define GEMMI_POLYHEUR_HPP_
#include <vector>
#include "model.hpp"
#include "util.hpp" // for vector_remove_if
namespace gemmi {
// A simplistic classification. It may change in the future.
// It returns PolymerType which corresponds to _entity_poly.type,
// but here we use only PeptideL, Rna, Dna, DnaRnaHybrid and Unknown.
GEMMI_DLL PolymerType check_polymer_type(const ConstResidueSpan& span,
bool ignore_entity_type=false);
inline PolymerType get_or_check_polymer_type(const Entity* ent,
const ConstResidueSpan& polymer) {
if (ent && ent->polymer_type != PolymerType::Unknown)
return ent->polymer_type;
return check_polymer_type(polymer);
}
struct AtomNameElement { std::string atom_name; El el; };
inline std::vector<AtomNameElement> get_mainchain_atoms(PolymerType ptype) {
if (is_polynucleotide(ptype))
return {{"P", El::P}, {"O5'", El::O}, {"C5'", El::C},
{"C4'", El::C}, {"O4'", El::O}, {"C3'", El::C}, {"O3'", El::O},
{"C2'", El::C}, {"O2'", El::O}, {"C1'", El::C}};
return {{"N", El::N}, {"CA", El::C}, {"C", El::C}, {"O", El::O}};
}
/// distance-based check for peptide bond
inline bool in_peptide_bond_distance(const Atom* a1, const Atom* a2) {
return a1 && a2 && a1->pos.dist_sq(a2->pos) < sq(1.341 * 1.5);
}
inline bool have_peptide_bond(const Residue& r1, const Residue& r2) {
return in_peptide_bond_distance(r1.get_c(), r2.get_n());
}
/// distance-based check for phosphodiester bond between nucleotide
inline bool in_nucleotide_bond_distance(const Atom* a1, const Atom* a2) {
return a1 && a2 && a1->pos.dist_sq(a2->pos) < sq(1.6 * 1.5);
}
inline bool have_nucleotide_bond(const Residue& r1, const Residue& r2) {
return in_nucleotide_bond_distance(r1.get_o3prim(), r2.get_p());
}
/// check C-N or O3'-P distance
inline bool are_connected(const Residue& r1, const Residue& r2, PolymerType ptype) {
if (is_polypeptide(ptype))
return have_peptide_bond(r1, r2);
if (is_polynucleotide(ptype))
return have_nucleotide_bond(r1, r2);
return false;
}
/// are_connected2() is less exact, but requires only CA (or P) atoms.
inline bool are_connected2(const Residue& r1, const Residue& r2, PolymerType ptype) {
auto this_or_first = [](const Atom* a, const Residue& r, El el) -> const Atom* {
if (a || r.atoms.empty())
return a;
if (const Atom* b = r.find_by_element(el))
return b;
return &r.atoms.front();
};
if (is_polypeptide(ptype)) {
const Atom* a1 = this_or_first(r1.get_ca(), r1, El::C);
const Atom* a2 = this_or_first(r2.get_ca(), r2, El::C);
return a1 && a2 && a1->pos.dist_sq(a2->pos) < sq(5.0);
}
if (is_polynucleotide(ptype)) {
const Atom* a1 = this_or_first(r1.get_p(), r1, El::P);
const Atom* a2 = this_or_first(r2.get_p(), r2, El::P);
return a1 && a2 && a1->pos.dist_sq(a2->pos) < sq(7.5);
}
return false;
}
/// are_connected3() = are_connected() + fallback to are_connected2()
inline bool are_connected3(const Residue& r1, const Residue& r2, PolymerType ptype) {
if (is_polypeptide(ptype)) {
if (const Atom* a1 = r1.get_c())
if (const Atom* a2 = r2.get_n())
return a1->pos.dist_sq(a2->pos) < sq(1.341 * 1.5);
if (const Atom* a1 = r1.get_ca())
if (const Atom* a2 = r2.get_ca())
return a1->pos.dist_sq(a2->pos) < sq(5.0);
} else if (is_polynucleotide(ptype)) {
if (const Atom* a1 = r1.get_o3prim())
if (const Atom* a2 = r2.get_p())
return a1->pos.dist_sq(a2->pos) < sq(1.6 * 1.5);
if (const Atom* a1 = r1.get_p())
if (const Atom* a2 = r2.get_p())
return a1->pos.dist_sq(a2->pos) < sq(7.5);
}
return false;
}
GEMMI_DLL std::string make_one_letter_sequence(const ConstResidueSpan& polymer);
/// Assigns entity_type=Polymer|NonPolymer|Water for each Residue (only
/// for residues with entity_type==Unknown, unless overwrite=true).
/// Determining where the polymer ends and ligands start is sometimes
/// arbitrary -- there can be a non-standard residue at the end that can
/// be regarded as as either the last residue or a linked ligand.
GEMMI_DLL void add_entity_types(Chain& chain, bool overwrite);
GEMMI_DLL void add_entity_types(Structure& st, bool overwrite);
/// Assigns entity_type=Unknown for all residues.
GEMMI_DLL void remove_entity_types(Structure& st);
/// Assigns Residue::entity_id based on Residue::subchain and Entity::subchains.
GEMMI_DLL void add_entity_ids(Structure& st, bool overwrite);
/// The subchain field in the residue is where we store_atom_site.label_asym_id
/// from mmCIF files. As of 2018 wwPDB software splits author's chains
/// (auth_asym_id) into label_asym_id units:
/// * linear polymer,
/// * non-polymers (each residue has different separate label_asym_id),
/// * and waters.
/// Refmac/makecif is doing similar thing but using different naming and
/// somewhat different rules (it was written in 1990's before PDBx/mmCIF).
///
/// Here we use naming and rules different from both wwPDB and makecif.
/// Note: call add_entity_types() first.
GEMMI_DLL void assign_subchain_names(Chain& chain, int& nonpolymer_counter);
GEMMI_DLL void assign_subchains(Structure& st, bool force, bool fail_if_unknown=true);
GEMMI_DLL void ensure_entities(Structure& st);
GEMMI_DLL void deduplicate_entities(Structure& st);
inline void setup_entities(Structure& st) {
add_entity_types(st, /*overwrite=*/false);
assign_subchains(st, /*force=*/false);
ensure_entities(st);
deduplicate_entities(st);
}
/// Determine ATOM/HETATM record type, based on Residue::entity_type
GEMMI_DLL char recommended_het_flag(const Residue& res);
/// R = recommended_het_flag(), other valid values are A, H and '\0'
template<class T> void assign_het_flags(T& obj, char flag='R') {
for (auto& child : obj.children())
assign_het_flags(child, flag);
}
template<> inline void assign_het_flags(Residue& res, char flag) {
flag &= ~0x20; // uppercase letters, ' ' -> \0
if (flag != 'R' && flag != '\0' && flag != 'A' && flag != 'H')
fail("assign_het_flags(): the only allowed values are A, H, ' ' and R");
res.het_flag = flag == 'R' ? recommended_het_flag(res) : flag;
}
// Remove waters. It may leave empty chains.
template<class T> void remove_waters(T& obj) {
for (auto& child : obj.children())
remove_waters(child);
}
template<> inline void remove_waters(Chain& ch) {
vector_remove_if(ch.residues,
[](const Residue& res) { return res.is_water(); });
}
// Remove ligands and waters. It may leave empty chains.
template<class T> void remove_ligands_and_waters(T& obj) {
for (auto& child : obj.children())
remove_ligands_and_waters(child);
}
template<> inline void remove_ligands_and_waters(Chain& ch) {
vector_remove_if(ch.residues, [&](const Residue& res) {
if (res.entity_type == EntityType::Unknown)
fail("remove_ligands_and_waters(): missing entity_type in chain ", ch.name);
return res.entity_type != EntityType::Polymer;
});
}
// Trim to alanine. Returns true if trimmed, false if it's (likely) not AA.
GEMMI_DLL bool trim_to_alanine(Residue& res);
inline void trim_to_alanine(Chain& chain) {
for (Residue& res : chain.residues)
trim_to_alanine(res);
}
// Functions for switching between long (>3 chars) residue names (CCD codes)
// and shortened ones that are compatible with the PDB format.
GEMMI_DLL void shorten_ccd_codes(Structure& st);
GEMMI_DLL void restore_full_ccd_codes(Structure& st);
/// Modifies Entity::full_sequence. Uses only the first chain for each Entity.
GEMMI_DLL void add_microhetero_to_sequences(Structure& st, bool overwrite=false);
GEMMI_DLL void add_tls_group_ids(Structure& st);
} // namespace gemmi
#endif