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>
257 lines
8.5 KiB
C++
257 lines
8.5 KiB
C++
// Copyright 2018 Global Phasing Ltd.
|
|
//
|
|
// Monomer library - (Refmac) restraints dictionary,
|
|
// which consists of monomers (chemical components), links, and modifications.
|
|
|
|
#ifndef GEMMI_MONLIB_HPP_
|
|
#define GEMMI_MONLIB_HPP_
|
|
|
|
#include <map>
|
|
#include <string>
|
|
#include <vector>
|
|
#include "cifdoc.hpp"
|
|
#include "elem.hpp" // for Element
|
|
#include "fail.hpp" // for fail, unreachable
|
|
#include "model.hpp" // for Residue, Atom
|
|
#include "chemcomp.hpp" // for ChemComp
|
|
#include "logger.hpp" // for Logger
|
|
|
|
namespace gemmi {
|
|
|
|
inline bool atom_match_with_alias(const std::string& atom_id, const std::string& atom,
|
|
const ChemComp::Aliasing* aliasing) {
|
|
if (aliasing)
|
|
if (const std::string* real_id = aliasing->name_from_alias(atom_id))
|
|
return *real_id == atom;
|
|
return atom_id == atom;
|
|
}
|
|
|
|
struct GEMMI_DLL ChemLink {
|
|
struct Side {
|
|
using Group = ChemComp::Group;
|
|
std::string comp;
|
|
std::string mod;
|
|
Group group = Group::Null;
|
|
bool matches_group(Group res) const {
|
|
if (group == Group::Null)
|
|
return false;
|
|
return res == group || (group == Group::Peptide && ChemComp::is_peptide_group(res))
|
|
|| (group == Group::DnaRna && ChemComp::is_nucleotide_group(res));
|
|
}
|
|
int specificity() const {
|
|
if (!comp.empty())
|
|
return 3;
|
|
return group == Group::PPeptide || group == Group::MPeptide ? 1 : 0;
|
|
}
|
|
};
|
|
std::string id;
|
|
std::string name;
|
|
Side side1;
|
|
Side side2;
|
|
Restraints rt;
|
|
cif::Block block; // temporary, until we have ChemLink->Block function
|
|
|
|
/// If multiple ChemLinks match a bond, the one with highest scores should be used.
|
|
int calculate_score(const Residue& res1, const Residue* res2,
|
|
char alt, char alt2,
|
|
const ChemComp::Aliasing* aliasing1,
|
|
const ChemComp::Aliasing* aliasing2) const;
|
|
};
|
|
|
|
struct GEMMI_DLL ChemMod {
|
|
struct AtomMod {
|
|
int func;
|
|
std::string old_id;
|
|
std::string new_id;
|
|
Element el;
|
|
float charge;
|
|
std::string chem_type;
|
|
};
|
|
|
|
std::string id;
|
|
std::string name;
|
|
std::string comp_id;
|
|
std::string group_id;
|
|
std::vector<AtomMod> atom_mods;
|
|
Restraints rt;
|
|
cif::Block block; // temporary, until we have ChemMod->Block function
|
|
|
|
void apply_to(ChemComp& chemcomp, ChemComp::Group alias_group) const;
|
|
};
|
|
|
|
struct EnerLib {
|
|
enum class RadiusType {Vdw, Vdwh, Ion};
|
|
struct Atom {
|
|
Element element;
|
|
char hb_type;
|
|
double vdw_radius;
|
|
double vdwh_radius;
|
|
double ion_radius;
|
|
int valency;
|
|
int sp;
|
|
};
|
|
struct Bond {
|
|
std::string atom_type_2;
|
|
BondType type;
|
|
double length;
|
|
double value_esd;
|
|
};
|
|
|
|
EnerLib() {}
|
|
void read(const cif::Document& doc);
|
|
std::map<std::string, Atom> atoms; // type->Atom
|
|
std::multimap<std::string, Bond> bonds; // atom_type_1->Bond
|
|
};
|
|
|
|
struct GEMMI_DLL MonLib {
|
|
std::string monomer_dir;
|
|
std::map<std::string, ChemComp> monomers;
|
|
std::map<std::string, ChemLink> links;
|
|
std::map<std::string, ChemMod> modifications;
|
|
std::map<std::string, ChemComp::Group> cc_groups;
|
|
EnerLib ener_lib;
|
|
|
|
const ChemLink* get_link(const std::string& link_id) const {
|
|
auto link = links.find(link_id);
|
|
return link != links.end() ? &link->second : nullptr;
|
|
}
|
|
const ChemMod* get_mod(const std::string& name) const {
|
|
auto modif = modifications.find(name);
|
|
return modif != modifications.end() ? &modif->second : nullptr;
|
|
}
|
|
|
|
// Returns the most specific link and a flag that is true
|
|
// if the order is comp2-comp1 in the link definition.
|
|
std::tuple<const ChemLink*, bool, const ChemComp::Aliasing*, const ChemComp::Aliasing*>
|
|
match_link(const Residue& res1, const std::string& atom1, char alt1,
|
|
const Residue& res2, const std::string& atom2, char alt2,
|
|
double min_bond_sq=0) const {
|
|
const ChemLink* best_link = nullptr;
|
|
bool inverted = false;
|
|
const ChemComp::Aliasing* aliasing1 = nullptr;
|
|
const ChemComp::Aliasing* aliasing2 = nullptr;
|
|
const ChemComp::Aliasing* aliasing1_final = nullptr;
|
|
const ChemComp::Aliasing* aliasing2_final = nullptr;
|
|
int best_score = -1000;
|
|
for (const auto& ml : links) {
|
|
const ChemLink& link = ml.second;
|
|
if (link.rt.bonds.empty() || starts_with(link.name, "auto-"))
|
|
continue;
|
|
// for now we don't have link definitions with >1 bonds
|
|
const Restraints::Bond& bond = link.rt.bonds[0];
|
|
if (sq(bond.value) < min_bond_sq)
|
|
continue;
|
|
if (link_side_matches_residue(link.side1, res1.name, &aliasing1) &&
|
|
link_side_matches_residue(link.side2, res2.name, &aliasing2) &&
|
|
atom_match_with_alias(bond.id1.atom, atom1, aliasing1) &&
|
|
atom_match_with_alias(bond.id2.atom, atom2, aliasing2)) {
|
|
int score = link.calculate_score(res1, &res2, alt1, alt2, aliasing1, aliasing2);
|
|
if (score > best_score) {
|
|
best_link = &link;
|
|
best_score = score;
|
|
aliasing1_final = aliasing1;
|
|
aliasing2_final = aliasing2;
|
|
inverted = false;
|
|
}
|
|
}
|
|
if (link_side_matches_residue(link.side1, res2.name, &aliasing2) &&
|
|
link_side_matches_residue(link.side2, res1.name, &aliasing1) &&
|
|
atom_match_with_alias(bond.id1.atom, atom2, aliasing2) &&
|
|
atom_match_with_alias(bond.id2.atom, atom1, aliasing1)) {
|
|
// NOLINTNEXTLINE(readability-suspicious-call-argument)
|
|
int score = link.calculate_score(res2, &res1, alt2, alt1, aliasing2, aliasing1);
|
|
if (score > best_score) {
|
|
best_link = &link;
|
|
best_score = score;
|
|
aliasing1_final = aliasing1;
|
|
aliasing2_final = aliasing2;
|
|
inverted = true;
|
|
}
|
|
}
|
|
}
|
|
return std::make_tuple(best_link, inverted, aliasing1_final, aliasing2_final);
|
|
}
|
|
|
|
void add_monomer_if_present(const cif::Block& block) {
|
|
if (block.has_tag("_chem_comp_atom.atom_id")) {
|
|
ChemComp cc = make_chemcomp_from_block(block);
|
|
if (cc.group == ChemComp::Group::Null) {
|
|
auto it = cc_groups.find(cc.name);
|
|
if (it != cc_groups.end())
|
|
cc.group = it->second;
|
|
}
|
|
std::string name = cc.name;
|
|
monomers.emplace(name, std::move(cc));
|
|
}
|
|
}
|
|
|
|
bool link_side_matches_residue(const ChemLink::Side& side,
|
|
const std::string& res_name,
|
|
ChemComp::Aliasing const** aliasing) const {
|
|
assert(aliasing);
|
|
*aliasing = nullptr;
|
|
if (!side.comp.empty())
|
|
return side.comp == res_name;
|
|
auto it = monomers.find(res_name);
|
|
if (it != monomers.end()) {
|
|
if (side.matches_group(it->second.group))
|
|
return true;
|
|
for (const ChemComp::Aliasing& a : it->second.aliases)
|
|
if (side.matches_group(a.group)) {
|
|
*aliasing = &a;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Returns path to the monomer cif file (the file may not exist).
|
|
std::string path(const std::string& code) const {
|
|
return monomer_dir + relative_monomer_path(code);
|
|
}
|
|
|
|
static std::string relative_monomer_path(const std::string& code);
|
|
|
|
void read_monomer_doc(const cif::Document& doc);
|
|
|
|
void read_monomer_cif(const std::string& path_);
|
|
|
|
void set_monomer_dir(const std::string& monomer_dir_) {
|
|
monomer_dir = monomer_dir_;
|
|
if (!monomer_dir.empty() && monomer_dir.back() != '/' && monomer_dir.back() != '\\')
|
|
monomer_dir += '/';
|
|
}
|
|
|
|
/// Read mon_lib_list.cif, ener_lib.cif and required monomers.
|
|
/// Returns true if all requested monomers were added.
|
|
bool read_monomer_lib(const std::string& monomer_dir_,
|
|
const std::vector<std::string>& resnames,
|
|
const Logger& logger);
|
|
|
|
double find_ideal_distance(const const_CRA& cra1, const const_CRA& cra2) const;
|
|
void update_old_atom_names(Structure& st, const Logger& logger) const;
|
|
};
|
|
|
|
// to be deprecated
|
|
inline MonLib read_monomer_lib(const std::string& monomer_dir,
|
|
const std::vector<std::string>& resnames,
|
|
const std::string& libin="",
|
|
bool ignore_missing=false) {
|
|
MonLib monlib;
|
|
if (!libin.empty())
|
|
monlib.read_monomer_cif(libin);
|
|
std::string error;
|
|
Logger logger;
|
|
if (!ignore_missing)
|
|
logger.callback = [&error](const std::string& s) { cat_to(error, s, '\n'); };
|
|
bool ok = monlib.read_monomer_lib(monomer_dir, resnames, logger);
|
|
if (!ignore_missing && !ok) {
|
|
error += "Please create definitions for missing monomers.";
|
|
fail(error);
|
|
}
|
|
return monlib;
|
|
}
|
|
|
|
} // namespace gemmi
|
|
#endif
|