Files
Jungfraujoch/gemmi_gph/gemmi/recgrid.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

176 lines
6.5 KiB
C++

// Copyright 2020 Global Phasing Ltd.
//
// ReciprocalGrid -- grid for reciprocal space data.
#ifndef GEMMI_RECGRID_HPP_
#define GEMMI_RECGRID_HPP_
#include <memory> // for unique_ptr
#include "asudata.hpp"
#include "grid.hpp"
namespace gemmi {
template<typename T> T friedel_mate_value(T v) { return v; }
template<typename T>
std::complex<T> friedel_mate_value(const std::complex<T>& v) {
return std::conj(v);
}
template<typename T>
struct ReciprocalGrid : GridBase<T> {
bool half_l = false; // hkl grid that stores only l>=0
bool has_index(int u, int v, int w) const {
bool half_u = (half_l && this->axis_order == AxisOrder::ZYX);
bool half_w = (half_l && this->axis_order != AxisOrder::ZYX);
return std::abs(half_u ? u : 2 * u) < this->nu &&
std::abs(2 * v) < this->nv &&
std::abs(half_w ? w : 2 * w) < this->nw;
}
void check_index(int u, int v, int w) const {
if (!has_index(u, v, w))
throw std::out_of_range("ReciprocalGrid: index out of grid.");
}
// Similar to Grid::index_n(), but works only for -nu <= u < nu, etc.
size_t index_n(int u, int v, int w) const {
return this->index_q(u >= 0 ? u : u + this->nu,
v >= 0 ? v : v + this->nv,
w >= 0 ? w : w + this->nw);
}
size_t index_checked(int u, int v, int w) const {
check_index(u, v, w);
return index_n(u, v, w);
}
T get_value(int u, int v, int w) const {
return this->data[index_checked(u, v, w)];
}
T get_value_or_zero(int u, int v, int w) const {
return has_index(u, v, w) ? this->data[index_n(u, v, w)] : T{};
}
void set_value(int u, int v, int w, T x) {
this->data[index_checked(u, v, w)] = x;
}
Miller to_hkl(const typename GridBase<T>::Point& point) const {
Miller hkl{{point.u, point.v, point.w}};
if (2 * point.u >= this->nu &&
!(half_l && this->axis_order == AxisOrder::ZYX))
hkl[0] -= this->nu;
if (2 * point.v >= this->nv)
hkl[1] -= this->nv;
if (2 * point.w >= this->nw &&
!(half_l && this->axis_order != AxisOrder::ZYX))
hkl[2] -= this->nw;
if (this->axis_order == AxisOrder::ZYX)
std::swap(hkl[0], hkl[2]);
return hkl;
}
double calculate_1_d2(const typename GridBase<T>::Point& point) const {
return this->unit_cell.calculate_1_d2(to_hkl(point));
}
double calculate_d(const typename GridBase<T>::Point& point) const {
return this->unit_cell.calculate_d(to_hkl(point));
}
T get_value_by_hkl(Miller hkl, double unblur=0,
bool mott_bethe=false) const {
if (this->axis_order == AxisOrder::ZYX)
fail("get_value_by_hkl(): ZYX order is not supported yet");
T value;
if (half_l && hkl[2] < 0)
value = friedel_mate_value(this->get_value(-hkl[0], -hkl[1], -hkl[2]));
else
value = this->get_value(hkl[0], hkl[1], hkl[2]);
if (unblur != 0. || mott_bethe) {
double inv_d2 = this->unit_cell.calculate_1_d2(hkl);
double mult = 1;
if (unblur != 0)
// cf. reciprocal_space_multiplier()
mult = std::exp(unblur * 0.25 * inv_d2);
if (mott_bethe)
// cf. mott_bethe_factor
mult *= -mott_bethe_const() / inv_d2;
value *= static_cast<decltype(std::abs(value))>(mult);
}
return value;
}
// the result is always sorted by h,k,l
template <typename R=T>
AsuData<R> prepare_asu_data(double dmin=0, double unblur=0,
bool with_000=false, bool with_sys_abs=false,
bool mott_bethe=false) {
AsuData<R> asu_data;
if (this->axis_order == AxisOrder::ZYX)
fail("get_asu_values(): ZYX order is not supported yet");
// Why "- 1" below? To skip the value at Nyquist frequency (±n/2).
// For even lengths of DFT (real -> reciprocal space) the resulting
// h=+nu/2 and h=-nu/2 are both represented by one (strictly real) value.
// The grid should be big enough so that these values are not needed.
int max_h = (this->nu - 1) / 2;
int max_k = (this->nv - 1) / 2;
int max_l = half_l ? this->nw - 1 : (this->nw - 1) / 2;
double max_1_d2 = 0.;
if (dmin != 0.) {
max_1_d2 = 1. / (dmin * dmin);
Miller lim = this->unit_cell.get_hkl_limits(dmin);
max_h = std::min(max_h, lim[0]);
max_k = std::min(max_k, lim[1]);
max_l = std::min(max_l, lim[2]);
}
gemmi::ReciprocalAsu asu(this->spacegroup);
std::unique_ptr<GroupOps> gops;
if (!with_sys_abs && this->spacegroup)
gops.reset(new GroupOps(this->spacegroup->operations()));
Miller hkl;
for (hkl[0] = -max_h; hkl[0] <= max_h; ++hkl[0]) {
int hi = hkl[0] >= 0 ? hkl[0] : hkl[0] + this->nu;
int hi_ = -hkl[0] >= 0 ? -hkl[0] : -hkl[0] + this->nu;
for (hkl[1] = -max_k; hkl[1] <= max_k; ++hkl[1]) {
hkl[2] = -max_l;
// (hkl)s with l<0 might be needed to get complete asu.
// If they are absent in the data (Hermitian FFT), use Friedel's pairs.
if (half_l) {
int ki_ = -hkl[1] >= 0 ? -hkl[1] : -hkl[1] + this->nv;
for (; hkl[2] < 0; ++hkl[2])
if (asu.is_in(hkl) &&
(max_1_d2 == 0. || this->unit_cell.calculate_1_d2(hkl) < max_1_d2) &&
(with_sys_abs || !gops->is_systematically_absent(hkl)))
asu_data.v.push_back({hkl,
friedel_mate_value(this->get_value_q(hi_, ki_, -hkl[2]))});
}
int ki = hkl[1] >= 0 ? hkl[1] : hkl[1] + this->nv;
for (; hkl[2] <= max_l; ++hkl[2])
if (asu.is_in(hkl) &&
(max_1_d2 == 0. || this->unit_cell.calculate_1_d2(hkl) < max_1_d2) &&
(with_sys_abs || !gops->is_systematically_absent(hkl)) &&
(with_000 || !(hkl[0] == 0 && hkl[1] == 0 && hkl[2] == 0))) {
int li = hkl[2] >= 0 ? hkl[2] : hkl[2] + this->nw;
asu_data.v.push_back({hkl, this->get_value_q(hi, ki, li)});
}
}
}
if (unblur != 0. || mott_bethe)
for (HklValue<R>& hv : asu_data.v) {
double inv_d2 = this->unit_cell.calculate_1_d2(hv.hkl);
double mult = 1;
if (unblur != 0)
// cf. reciprocal_space_multiplier()
mult = std::exp(unblur * 0.25 * inv_d2);
if (mott_bethe)
// cf. mott_bethe_factor
mult *= -mott_bethe_const() / inv_d2;
hv.value *= static_cast<decltype(std::abs(hv.value))>(mult);
}
asu_data.unit_cell_ = this->unit_cell;
asu_data.spacegroup_ = this->spacegroup;
return asu_data;
}
};
template<typename T> using FPhiGrid = ReciprocalGrid<std::complex<T>>;
} // namespace gemmi
#endif