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>
249 lines
8.0 KiB
C++
249 lines
8.0 KiB
C++
// Copyright 2018 Global Phasing Ltd.
|
|
//
|
|
// Representation of a small molecule or inorganic crystal.
|
|
// Flat list of atom sites. Minimal functionality.
|
|
|
|
#ifndef GEMMI_SMALL_HPP_
|
|
#define GEMMI_SMALL_HPP_
|
|
|
|
#include <cctype> // for isalpha
|
|
#include <algorithm> // for any_of
|
|
#include <bitset>
|
|
#include <string>
|
|
#include <vector>
|
|
#include "elem.hpp" // Element
|
|
#include "math.hpp" // SMat33
|
|
#include "symmetry.hpp" // find_spacegroup_by_name
|
|
#include "unitcell.hpp" // UnitCell, Fractional
|
|
#include "util.hpp" // vector_remove_if
|
|
|
|
namespace gemmi {
|
|
|
|
inline bool is_complete(const GroupOps& gops) {
|
|
for (Op op1 : gops.sym_ops)
|
|
for (Op op2 : gops.sym_ops)
|
|
if (gops.find_by_rotation((op1 * op2).rot) == nullptr)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
inline std::vector<Op> triplets_to_ops(const std::vector<std::string>& symops) {
|
|
std::vector<Op> ops;
|
|
ops.reserve(symops.size());
|
|
for (const std::string& xyz : symops)
|
|
ops.push_back(parse_triplet(xyz));
|
|
return ops;
|
|
}
|
|
|
|
struct SmallStructure {
|
|
struct Site {
|
|
std::string label;
|
|
std::string type_symbol;
|
|
Fractional fract;
|
|
double occ = 1.0;
|
|
double u_iso = 0.;
|
|
SMat33<double> aniso = {0, 0, 0, 0, 0, 0};
|
|
int disorder_group = 0;
|
|
Element element = El::X;
|
|
signed char charge = 0; // [-8, +8]
|
|
|
|
Position orth(const gemmi::UnitCell& cell_) const {
|
|
return cell_.orthogonalize(fract);
|
|
}
|
|
|
|
std::string element_and_charge_symbol() const {
|
|
std::string s = element.name();
|
|
if (charge != 0) {
|
|
s += std::to_string(std::abs(charge));
|
|
s += charge > 0 ? '+' : '-';
|
|
}
|
|
return s;
|
|
}
|
|
};
|
|
|
|
struct AtomType {
|
|
std::string symbol;
|
|
Element element = El::X;
|
|
signed char charge = 0; // [-8, +8]
|
|
double dispersion_real;
|
|
double dispersion_imag;
|
|
};
|
|
|
|
std::string name;
|
|
UnitCell cell;
|
|
const SpaceGroup* spacegroup = nullptr;
|
|
std::string spacegroup_hm;
|
|
std::string spacegroup_hall;
|
|
int spacegroup_number = 0;
|
|
std::vector<std::string> symops;
|
|
std::vector<Site> sites;
|
|
std::vector<AtomType> atom_types;
|
|
double wavelength = 0.; // the first wavelength if multiple
|
|
|
|
std::vector<Site> get_all_unit_cell_sites() const;
|
|
|
|
void determine_and_set_spacegroup(const char* order) {
|
|
spacegroup = nullptr;
|
|
if (order)
|
|
for (const char* c = order; *c != '\0' && spacegroup == nullptr; ++c) {
|
|
try {
|
|
GroupOps gops;
|
|
spacegroup = determine_spacegroup_from(*c, gops);
|
|
if (!spacegroup && *(c+1) == '.') {
|
|
// If symops don't correspond to tabulated settings,
|
|
// we can't set spacegroup, but we can set UnitCell::images.
|
|
if (gops.order() == (int) symops.size() && is_complete(gops)) {
|
|
cell.set_cell_images_from_groupops(gops);
|
|
return;
|
|
}
|
|
++c;
|
|
}
|
|
} catch (std::exception&) {}
|
|
}
|
|
setup_cell_images();
|
|
}
|
|
|
|
const SpaceGroup* determine_spacegroup_from(char c, GroupOps& gops) const {
|
|
switch (lower(c)) {
|
|
case 's':
|
|
if (symops.empty())
|
|
return nullptr;
|
|
gops = split_centering_vectors(triplets_to_ops(symops));
|
|
return find_spacegroup_by_ops(gops);
|
|
case 'h':
|
|
if (spacegroup_hall.empty())
|
|
return nullptr;
|
|
return find_spacegroup_by_ops(symops_from_hall(spacegroup_hall.c_str()));
|
|
case '1':
|
|
case '2': {
|
|
if (spacegroup_hm.empty())
|
|
return nullptr;
|
|
char prefer[] = {c, '\0'};
|
|
return find_spacegroup_by_name(spacegroup_hm, cell.alpha, cell.gamma, prefer);
|
|
}
|
|
case 'n':
|
|
if (spacegroup_number == 0)
|
|
return nullptr;
|
|
return find_spacegroup_by_number(spacegroup_number);
|
|
default:
|
|
throw std::invalid_argument("determine_and_set_spacegroup(): wrong character in 'order'");
|
|
}
|
|
}
|
|
|
|
std::string check_spacegroup() const {
|
|
std::string err;
|
|
if (!symops.empty())
|
|
try {
|
|
std::vector<Op> ops = triplets_to_ops(symops);
|
|
for (Op& op : ops)
|
|
op.wrap();
|
|
std::sort(ops.begin(), ops.end());
|
|
GroupOps gops = split_centering_vectors(ops);
|
|
if (!is_complete(gops))
|
|
cat_to(err, "symops list is incomplete or incorrect\n");
|
|
else if (gops.all_ops_sorted() != ops)
|
|
cat_to(err, "symops list is incorrect or incomplete or redundant\n");
|
|
const SpaceGroup* sg = find_spacegroup_by_ops(gops);
|
|
if (!sg)
|
|
cat_to(err, "space group from symops not found in the table\n");
|
|
else if (sg != spacegroup)
|
|
cat_to(err, "space group from symops differs: ", sg->xhm(), '\n');
|
|
} catch (std::exception& e) {
|
|
cat_to(err, "error while processing symops: ", e.what(), '\n');
|
|
}
|
|
if (!spacegroup_hall.empty())
|
|
try {
|
|
const SpaceGroup* sg = find_spacegroup_by_ops(symops_from_hall(spacegroup_hall.c_str()));
|
|
if (!sg)
|
|
cat_to(err, "space group from Hall symbol (", spacegroup_hall,
|
|
") not found in the table\n");
|
|
else if (spacegroup != sg)
|
|
cat_to(err, "space group from Hall symbol (", spacegroup_hall,
|
|
") differs: ", sg->xhm(), '\n');
|
|
} catch (std::exception& e) {
|
|
cat_to(err, "error while processing Hall symbol: ", e.what(), '\n');
|
|
}
|
|
if (!spacegroup_hm.empty()) {
|
|
const SpaceGroup* sg = find_spacegroup_by_name(spacegroup_hm, cell.alpha, cell.gamma, "2");
|
|
if (!sg)
|
|
cat_to(err, "H-M symbol (", spacegroup_hm, ") not found in the table\n");
|
|
else if (!spacegroup || strcmp(spacegroup->hm, sg->hm) != 0)
|
|
cat_to(err, "space group from H-M symbol (", spacegroup_hm,
|
|
") differs: ", sg->hm, '\n');
|
|
}
|
|
if (spacegroup_number != 0 && spacegroup && spacegroup->number != spacegroup_number)
|
|
cat_to(err, "space group number (", spacegroup_number, ") differs\n");
|
|
return err;
|
|
}
|
|
|
|
const AtomType* get_atom_type(const std::string& symbol) const {
|
|
for (const AtomType& at : atom_types)
|
|
if (at.symbol == symbol)
|
|
return &at;
|
|
return nullptr;
|
|
}
|
|
|
|
// similar to Model::present_elements() from model.hpp
|
|
std::bitset<(size_t)El::END> present_elements() const {
|
|
std::bitset<(size_t)El::END> table;
|
|
for (const Site& atom : sites)
|
|
table.set((size_t)atom.element.elem);
|
|
return table;
|
|
}
|
|
|
|
void remove_hydrogens() {
|
|
vector_remove_if(sites, [](const Site& a) { return a.element.is_hydrogen(); });
|
|
}
|
|
|
|
// pre: atoms on special positions have "chemical" occupancy (i.e. not divided
|
|
// by n for n-fold symmetry)
|
|
void change_occupancies_to_crystallographic(double max_dist=0.4) {
|
|
for (Site& site : sites) {
|
|
int n_mates = cell.is_special_position(site.fract, max_dist);
|
|
if (n_mates != 0)
|
|
site.occ /= (n_mates + 1);
|
|
}
|
|
}
|
|
|
|
void setup_cell_images() {
|
|
cell.set_cell_images_from_spacegroup(spacegroup);
|
|
}
|
|
};
|
|
|
|
template<typename T>
|
|
inline void split_element_and_charge(const std::string& label, T* dest) {
|
|
int len = label.size() > 1 && std::isalpha(label[1]) ? 2 : 1;
|
|
dest->element = len == 1 ? impl::find_single_letter_element(label[0] & ~0x20)
|
|
: find_element(label.c_str());
|
|
if (dest->element != El::X && (label.back() == '+' || label.back() == '-')) {
|
|
int sign = label.back() == '+' ? 1 : -1;
|
|
if (label.size() - len == 1)
|
|
dest->charge = sign;
|
|
else if (label.size() - len == 2 && label[len] >= '0' && label[len] <= '9')
|
|
dest->charge = sign * (label[len] - '0');
|
|
}
|
|
}
|
|
|
|
inline std::vector<SmallStructure::Site>
|
|
SmallStructure::get_all_unit_cell_sites() const {
|
|
const double SPECIAL_POS_TOL = 0.4;
|
|
std::vector<Site> all;
|
|
for (const Site& site : sites) {
|
|
size_t start = all.size();
|
|
all.push_back(site);
|
|
for (const FTransform& image : cell.images) {
|
|
Fractional fpos = image.apply(site.fract);
|
|
if (std::any_of(all.begin() + start, all.end(), [&](const Site& other) {
|
|
return cell.distance_sq(fpos, other.fract) < sq(SPECIAL_POS_TOL);
|
|
}))
|
|
continue;
|
|
all.push_back(site);
|
|
all.back().fract = fpos;
|
|
}
|
|
}
|
|
return all;
|
|
}
|
|
|
|
} // namespace gemmi
|
|
#endif
|