Merge branch 'mr-7qij' into rc175: --mr (predicted and experimental models), MR tNCS half-shift check, P1 single copy, carry rule, MR 2-4x faster
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -438,7 +438,7 @@ Problems Whose Variables Separate" (1973), SIAM J. Numer. Anal. 10, 413-432
|
||||
for solving separable nonlinear least squares problems" (1975), BIT 15, 49-57
|
||||
[doi:10.1007/BF01932995](https://doi.org/10.1007/BF01932995).
|
||||
|
||||
**Predicted models** — `--af-model` (experimental) places an AlphaFold prediction by molecular
|
||||
**Predicted models** — `--mr` (experimental) places an AlphaFold prediction (or any model) by molecular
|
||||
replacement. J. Jumper, R. Evans, A. Pritzel et al., "Highly accurate protein structure prediction
|
||||
with AlphaFold" (2021), Nature 596, 583-589
|
||||
[doi:10.1038/s41586-021-03819-2](https://doi.org/10.1038/s41586-021-03819-2); the models are those of
|
||||
@@ -481,7 +481,12 @@ boundaries of molecular replacement with maximum likelihood" (2001), Acta Cryst.
|
||||
Z-score over the same orientation at other positions, as the likelihood-enhanced translation function's
|
||||
is: A. J. McCoy, R. W. Grosse-Kunstleve, L. C. Storoni and R. J. Read, "Likelihood-enhanced fast
|
||||
translation functions" (2005), Acta Cryst. D61, 458-464
|
||||
[doi:10.1107/S0907444905001617](https://doi.org/10.1107/S0907444905001617). The logarithm of the Bessel
|
||||
[doi:10.1107/S0907444905001617](https://doi.org/10.1107/S0907444905001617). A model whose expected
|
||||
log-likelihood gain - the sum of sigma_A^4 / 2 over the reflections - is too small for a placement to stand
|
||||
out is not searched for: A. J. McCoy, R. D. Oeffner, A. G. Wrobel, J. R. M. Ojala, K. Tryggvason,
|
||||
B. Lohkamp and R. J. Read, "Ab initio solution of macromolecular crystal structures without direct
|
||||
methods" (2017), Proc. Natl. Acad. Sci. USA 114, 3637-3641
|
||||
[doi:10.1073/pnas.1701640114](https://doi.org/10.1073/pnas.1701640114). The logarithm of the Bessel
|
||||
function I0 in it is evaluated with the polynomial approximations of M. Abramowitz and I. A. Stegun,
|
||||
"Handbook of Mathematical Functions" (1964), National Bureau of Standards, 9.8.1-9.8.2. A predicted
|
||||
model is cut into rigid domains by its predicted aligned error, as Oeffner et al. (above) do before
|
||||
|
||||
+30
-16
@@ -110,7 +110,7 @@ Rates, per-image costs and progress remain on stdout only.
|
||||
|
||||
Sections, in order: the `SUMMARY`, then `1. DATA SET AND GEOMETRY`, `2. CRYSTAL`,
|
||||
`3. MERGED DATA`, `4. DIAGNOSTICS`, `5. MODEL VALIDATION` only with `--model`, and the unnumbered
|
||||
`MODEL PRIOR (EXPERIMENTAL)` only with `--af-model` ([below](#model-prior---af-model-experimental))
|
||||
`MOLECULAR REPLACEMENT (EXPERIMENTAL)` only with `--mr` ([below](#molecular-replacement---mr-experimental))
|
||||
(see [The summary](#the-summary-and-what-the-run-decided) below). The numbering is contiguous, and
|
||||
the fifth section appearing renumbers nothing; a stage that did not run states that inside its
|
||||
section — `MERGE= NOT_PERFORMED` — rather than the section disappearing.
|
||||
@@ -970,11 +970,16 @@ files are **byte for byte** what a run with no model would have written — same
|
||||
`_unmerged.mtz`. A rejected model is therefore safe to try: it costs the null's compute and changes
|
||||
nothing else.
|
||||
|
||||
## Model prior (`--af-model`, experimental)
|
||||
## Molecular replacement (`--mr`, experimental)
|
||||
|
||||
`--af-model <file>` takes a predicted model - an AlphaFold mmCIF, with the pLDDT in the B-factor
|
||||
column - and places it by molecular replacement: residues below pLDDT 70 removed, B-factors from the
|
||||
pLDDT, then a rotation function over the point group (6 deg sampling, 4-12 A, each of the 50 best
|
||||
`--mr <file>` takes a model and places it by molecular replacement. What kind of model it is is read
|
||||
from the file (`MR_MODEL_KIND=`): a prediction - ModelCIF's `_ma_qa_metric`, or no cell and one B value
|
||||
per residue from 0 to 100 (an AlphaFold PDB or mmCIF) - has its residues below pLDDT 70 removed and its
|
||||
B-factors made from the pLDDT; anything else is an experimental structure, taken as its polymer only (no
|
||||
waters, ligands, hydrogens or alternative conformers) with its own B-factors, and a coordinate error of
|
||||
0.67 A (a prediction's at pLDDT 90) for sigma_A. A model whose expected log-likelihood gain - the sum of
|
||||
sigma_A^4 / 2 over the search's reflections, one copy being the part of the scattering the most probable
|
||||
copy number by Matthews makes it - is below 10 is not searched for (`NOT_PLACED`). Then a rotation function over the point group (6 deg sampling, 4-12 A, each of the 50 best
|
||||
peaks refined off the sampling), a translation function for those orientations in every space group the
|
||||
merge cannot tell apart, and the best three placements per group that pack through the rigid body, bulk
|
||||
solvent and R-free (to `SCORE_D_MIN`, 3 A or the data's limit). Further copies follow one at a time, up
|
||||
@@ -982,7 +987,7 @@ to the largest copy number with a Matthews probability of 1% (`COPIES_LIKELY`: t
|
||||
translation function again with the copies already placed as a known partial structure, its peaks
|
||||
ranked by the likelihood gain of the new copy beside them (peaks that lower the likelihood dropped),
|
||||
each new copy refined beside the others held fixed, and kept where it stands out (below), it packs and
|
||||
R-free drops by 0.01 (`COPIES`, and `MODEL_PRIOR_COPIES=` for the group chosen). The groups are searched
|
||||
R-free drops by 0.01 (`COPIES`, and `MR_COPIES=` for the group chosen). The groups are searched
|
||||
side by side; with tNCS, every group that placed fewer copies than the best one is searched once more
|
||||
with the best one's copies' orientations tried first, and the better search kept.
|
||||
|
||||
@@ -993,20 +998,22 @@ stands out where its translation-function Z-score reaches 8, or - only where the
|
||||
many as the cell most probably holds - where its likelihood after the rigid body reaches a Z-score of 8
|
||||
over the same copy in the same orientation at random positions (`LLG_Z`; taken only where `TF_Z` is
|
||||
below 8). That second way in is what places a single copy in a small cell, whose few reflections keep
|
||||
the translation function's own Z-score low.
|
||||
the translation function's own Z-score low. In P1 the first copy sits at the origin, which no function
|
||||
of its position can single out; it stands out where its rotation-function Z-score (`RF_Z`) reaches 8.
|
||||
|
||||
Where the AlphaFold DB's predicted-aligned-error file sits beside the model under its own name
|
||||
(`AF-<id>-F1-model_v<n>.cif` and `AF-<id>-F1-predicted_aligned_error_v<n>.json`), the prediction is cut
|
||||
into rigid domains by average linkage on the PAE (domains a mean 10 A apart, of 40 residues or more;
|
||||
`AF_PAE_DOMAINS=`). Where the whole model is placed nowhere, each domain is searched by itself, side by
|
||||
`MR_PAE_DOMAINS=`; a prediction only). Where the whole model is placed nowhere, each domain is searched by itself, side by
|
||||
side; the domains placed have to agree, and a domain alone can only confirm the answer's group
|
||||
(`AGREES`), never change it (`UNDECIDED` otherwise). `MODEL_PRIOR_SEARCH_MODEL=` names what the decision
|
||||
(`AGREES`), never change it (`UNDECIDED` otherwise). `MR_SEARCH_MODEL=` names what the decision
|
||||
was made with.
|
||||
|
||||
The table has one row per group and merge. A row is `PLACED` where one of its copies stands out, at most 5% of the C-alpha atoms clash with another
|
||||
copy or a symmetry mate (`CLASH`), and one copy fits the cell (`MATTHEWS`: Vm below 1.5 A^3/Da). The
|
||||
null - the same model refitted from random orientations, each placed by the translation function as the
|
||||
search placed the real one (all copies turned together about their centroid where there are several) -
|
||||
search placed the real one (each copy turned about its own centroid where there are several, so that the
|
||||
copies keep their arrangement) -
|
||||
is built for the candidate the choice falls on; its R-work has to beat the null's by 4.51 sd
|
||||
(`NULL_SIGMA`: 3 sd allowing for the null being nine draws, as `MODEL_FIT_SIGMA`), and one that does not
|
||||
is marked `NULL` and the choice falls on the next. A placement clashing over twice the limit is not
|
||||
@@ -1016,9 +1023,15 @@ the chosen group's enantiomorph) and whose search placed fewer copies leaves the
|
||||
since one copy beside a translated twin of itself fits the data no better than at random - unless the
|
||||
chosen copies, carried into that group as they are and refined there, fit worse by 0.03 in R-free:
|
||||
where the translation does make up the difference between the groups, the copies build the same crystal
|
||||
in both. The rotation and translation functions, the rigid
|
||||
in both. The group the merge was made in, searched to fewer copies than the chosen one, leaves the choice
|
||||
`UNDECIDED` too, whether it packs or not (its enantiomorph excepted, which the hand check decides). Where
|
||||
the tNCS vector v is half a lattice vector, the reflections with h.v half-integral are weak, and a
|
||||
placement moved by (v + o) / 2 (o an origin shift of the group) fits the strong ones exactly as well:
|
||||
the candidate the choice falls on is compared with every such move of itself, refined and packing, by
|
||||
R-work and R-free together over the weak reflections alone; a move better by 0.01 there is taken
|
||||
instead, and where the two best are within 0.01 the choice is `UNDECIDED`. The rotation and translation functions, the rigid
|
||||
body and the structure factors the R-factors are taken on run on the
|
||||
GPU where there is one (`MODEL_PRIOR_SEARCH_DEVICE=`). `MODEL_PRIOR_DECISION=` is then:
|
||||
GPU where there is one (`MR_SEARCH_DEVICE=`). `MR_DECISION=` is then:
|
||||
|
||||
- `AGREES` - the placed group with the lowest R-free is the answer's, ahead of every other group of
|
||||
the merge that packs by 0.03 in R-free (with tNCS, see above);
|
||||
@@ -1026,8 +1039,8 @@ GPU where there is one (`MODEL_PRIOR_SEARCH_DEVICE=`). `MODEL_PRIOR_DECISION=` i
|
||||
only, as with `--model`; no reflection moves). It is taken only by the rule a `--model`'s
|
||||
enantiomorph is taken by: the model has to fit against its null (the choice already required that)
|
||||
and model validation's anomalous difference map, read at the placed model's atoms, must not be
|
||||
inverted (`MODEL_PRIOR_HAND_CHECK=`,
|
||||
`MODEL_PRIOR_HAND_FIT_SIGMA=`, `MODEL_PRIOR_ANOMALOUS_INVERTED=`). Where it is not, the decision reads
|
||||
inverted (`MR_HAND_CHECK=`,
|
||||
`MR_HAND_FIT_SIGMA=`, `MR_ANOMALOUS_INVERTED=`). Where it is not, the decision reads
|
||||
`ENANTIOMORPH_VETOED` and the label stays;
|
||||
- `SPACE_GROUP` - another group with the answer's rotations; written as `<prefix>_mr.mtz`/`.cif` from
|
||||
the answer's merge, the reflections that group extinguishes left out;
|
||||
@@ -1039,8 +1052,9 @@ GPU where there is one (`MODEL_PRIOR_SEARCH_DEVICE=`). `MODEL_PRIOR_DECISION=` i
|
||||
is scored against the model searched for afresh in the subgroup; the subgroup is taken where its own
|
||||
placement leads by 0.03 in R-free and passes the subgroup's own choice (its null, its lead over the
|
||||
subgroup's other groups). Written as `<prefix>_mr.mtz`/`.cif` from the twin hypothesis' merge, with
|
||||
its law and fraction. With `--af-model`, the twin hypothesis' merge is made wherever a metric re-ask
|
||||
promoted the indexed lattice's group, so that the comparison can run;
|
||||
its law and fraction. With `--mr`, the twin hypothesis' merge is made wherever a metric re-ask
|
||||
promoted the indexed lattice's group, so that the comparison can run - and, where that is the only
|
||||
reason it was made, written and reported only where the decision is `TWIN_SUBGROUP`;
|
||||
- `UNDECIDED` or `NOT_PLACED` - nothing is written.
|
||||
|
||||
With `SPACE_GROUP` and `TWIN_SUBGROUP` the placed model is written as `<prefix>_mr_model.pdb`/`.cif`.
|
||||
|
||||
@@ -58,6 +58,8 @@ public:
|
||||
// work area and the reflections. The largest map Map() can be given shares the same buffers.
|
||||
size_t DeviceBytes() const { return device_bytes_; }
|
||||
double DMin() const { return d_min_; }
|
||||
// The reflections Compute() gives F_calc and F_mask at.
|
||||
size_t Reflections() const { return rows_.size(); }
|
||||
int Device() const { return device_; }
|
||||
// The device's total memory, which DeviceBytes() is to be judged against.
|
||||
size_t DeviceTotalMemory() const { return device_total_; }
|
||||
|
||||
@@ -308,7 +308,7 @@ void KeepModelVerdict(ModelValidationResult &remade, const ModelValidationResult
|
||||
// F(+) - F(-) on the phases of F_calc + solvent, and its reading at the model's atoms - anomalous_pairs,
|
||||
// the scatterers' mean, the sites and anomalous_hands_disagree, by the same code and the same rule as the
|
||||
// validation's. No placement, no scale fit, no null, no file. For a caller that has already placed the
|
||||
// model and judged its fit (the --af-model hand check). On the GPU where there is one.
|
||||
// model and judged its fit (the --mr hand check). On the GPU where there is one.
|
||||
ModelValidationResult ReadModelAnomalousMap(const std::vector<MergedReflection> &merged,
|
||||
const gemmi::Structure &placed, const gemmi::SpaceGroup &data_space_group,
|
||||
double k_sol, double b_sol, Logger &logger, size_t nthreads);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,12 +3,13 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
// Molecular replacement of a predicted model (rugnux --af-model, experimental): a global
|
||||
// search for the model's orientation and position in the cell, so that a model with no placement of
|
||||
// its own - an AlphaFold prediction - can be scored against the data, and the space groups the data
|
||||
// cannot tell apart can be scored against it.
|
||||
// Molecular replacement (rugnux --mr, experimental): a global search for a model's orientation and
|
||||
// position in the cell, so that a model with no placement of its own - an AlphaFold prediction, or an
|
||||
// experimental structure from another crystal - can be scored against the data, and the space groups the
|
||||
// data cannot tell apart can be scored against it.
|
||||
//
|
||||
// - The model: residues with pLDDT < 70 removed, B-factors from pLDDT, hydrogens dropped.
|
||||
// - The model: a prediction with residues of pLDDT < 70 removed and B-factors from pLDDT; an experimental
|
||||
// structure as its polymer, B-factors kept. Hydrogens dropped either way.
|
||||
// - Rotation: a uniform sampling of the point group's rotation ASU, each orientation scored by the
|
||||
// correlation of the observed E^2 with the model's own symmetry-summed |F|^2 - read off the
|
||||
// model's molecular transform, computed once by FFT and interpolated per orientation.
|
||||
@@ -37,18 +38,23 @@
|
||||
|
||||
class Logger;
|
||||
|
||||
// The predicted model as the search uses it: trimmed, B-factors from pLDDT, centroid at the origin.
|
||||
// The model as the search uses it, centroid at the origin. A prediction (AlphaFold, ModelCIF) trimmed to the
|
||||
// residues it is confident of, B-factors from pLDDT; an experimental structure as it is, its polymer only.
|
||||
struct SearchModel {
|
||||
gemmi::Structure st;
|
||||
bool predicted = true; // a prediction, its B column a pLDDT; an experimental structure otherwise
|
||||
int residues_read = 0;
|
||||
int residues_kept = 0;
|
||||
double mass_da = 0; // of the residues kept, 110 Da each
|
||||
double radius_A = 0; // the farthest atom from the centroid
|
||||
double error_A = 0; // rms of the coordinate errors the pLDDTs predict, over the atoms kept
|
||||
double error_A = 0; // rms of the coordinate errors the pLDDTs predict, over the atoms kept (fixed for an
|
||||
// experimental structure)
|
||||
std::vector<int> residue_index; // of each residue kept, in model order: its position in the prediction
|
||||
std::string description; // "the whole model", or which domain
|
||||
std::string failure_reason;
|
||||
};
|
||||
// A prediction where the file says so - ModelCIF's _ma_qa_metric - or where it has no cell and its B column
|
||||
// holds one value per residue, from 0 to 100; an experimental structure otherwise.
|
||||
SearchModel PrepareSearchModel(const std::string &path);
|
||||
|
||||
// The rigid domains of a prediction from its predicted aligned error (PAE, the AlphaFold DB's JSON: a matrix
|
||||
@@ -159,7 +165,8 @@ TranslationSetup MakeTranslationSetup(const SearchReflections &refl, const gemmi
|
||||
std::vector<std::complex<double>> KnownStructureFactors(const MolecularTransform &ft, const TranslationSetup &setup,
|
||||
const gemmi::UnitCell &cell,
|
||||
const std::vector<gemmi::Mat33> &rotations,
|
||||
const std::vector<gemmi::Fractional> &positions);
|
||||
const std::vector<gemmi::Fractional> &positions,
|
||||
size_t nthreads = 1);
|
||||
|
||||
// The translation function of the model in orientation `rotation`: its highest point as a Z-score over
|
||||
// every point of the function, and where it is (the fractional position of the model's centroid).
|
||||
@@ -333,6 +340,7 @@ struct MolecularReplacementResult {
|
||||
int residues_read = 0, residues_kept = 0;
|
||||
double mass_da = 0;
|
||||
std::string search_model; // the whole model, or the PAE domain the decision was made with
|
||||
bool predicted_model = true; // SearchModel::predicted
|
||||
int pae_domains = 0; // the rigid domains of the prediction's PAE; 0 without a PAE file
|
||||
double d_min_search = 0, d_max_search = 0;
|
||||
double d_min_score = 0; // the limit R-work / R-free are computed to
|
||||
|
||||
@@ -47,8 +47,9 @@ __device__ __forceinline__ float2 transform_at(const Transform &g, int i, int j,
|
||||
i = -i; j = -j; l = -l;
|
||||
conj = true;
|
||||
}
|
||||
i %= g.nu; if (i < 0) i += g.nu;
|
||||
j %= g.nv; if (j < 0) j += g.nv;
|
||||
// Wrapped into the grid; the division only for an index outside it.
|
||||
if (i < 0 || i >= g.nu) { i %= g.nu; if (i < 0) i += g.nu; }
|
||||
if (j < 0 || j >= g.nv) { j %= g.nv; if (j < 0) j += g.nv; }
|
||||
float2 v = g.data[i + static_cast<size_t>(g.nu) * (j + static_cast<size_t>(g.nv) * l)];
|
||||
if (conj)
|
||||
v.y = -v.y;
|
||||
@@ -416,20 +417,29 @@ std::vector<double> MolecularReplacementGPU::RotationFunction(const SearchReflec
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<TranslationPeak> MolecularReplacementGPU::TranslationFunction(
|
||||
const TranslationSetup &setup, const std::vector<gemmi::Mat33> &rotations,
|
||||
const std::vector<std::complex<double>> *known) {
|
||||
std::vector<TranslationPeak> out(rotations.size());
|
||||
const int terms = static_cast<int>(setup.Terms()), nops = setup.ops;
|
||||
const int npairs = static_cast<int>(setup.pair_j.size());
|
||||
if (terms == 0 || rotations.empty() || (nops < 2 && known == nullptr))
|
||||
return out;
|
||||
cudaStream_t stream = state_->stream;
|
||||
const int nu = setup.nu, nv = setup.nv, nw = setup.nw, nwh = nw / 2 + 1;
|
||||
const size_t cells = static_cast<size_t>(nu) * nv * nwh, points = static_cast<size_t>(nu) * nv * nw;
|
||||
// What the translation function needs of one setup, on the device: the gather's lists - for every point of the
|
||||
// half grid in cuFFT's layout ((u nv + v) nwh + l), the (term, pair) entries landing on it, in entry order, and
|
||||
// the same for the cross terms with a known partial structure - the terms' vectors, phases, weights and bins,
|
||||
// and the transform's plan. Made once where a setup is used for several functions in a row.
|
||||
struct TranslationSetupGPU {
|
||||
CudaDevicePtr<int> cross_offset, cross_entry, cross_term, cross_op, bin, bin_n, pair_j, pair_k, offset, entry;
|
||||
CudaDevicePtr<char> cross_conj;
|
||||
CudaDevicePtr<float3> s;
|
||||
CudaDevicePtr<float2> phase;
|
||||
CudaDevicePtr<float> weight;
|
||||
cufftHandle plan = 0;
|
||||
~TranslationSetupGPU() {
|
||||
if (plan != 0)
|
||||
cufftDestroy(plan);
|
||||
}
|
||||
};
|
||||
|
||||
// The gather's lists: for every point of the half grid in cuFFT's layout ((u nv + v) nwh + l), the
|
||||
// (term, pair) entries landing on it, in entry order.
|
||||
namespace {
|
||||
|
||||
std::unique_ptr<TranslationSetupGPU> prepare_translation(const TranslationSetup &setup, bool cross, cudaStream_t stream) {
|
||||
auto out = std::make_unique<TranslationSetupGPU>();
|
||||
const int nu = setup.nu, nv = setup.nv, nw = setup.nw, nwh = nw / 2 + 1;
|
||||
const size_t cells = static_cast<size_t>(nu) * nv * nwh;
|
||||
std::vector<int> offset(cells + 1, 0);
|
||||
for (size_t e = 0; e < setup.target_u.size(); e++)
|
||||
if (setup.target_u[e] >= 0)
|
||||
@@ -444,10 +454,8 @@ std::vector<TranslationPeak> MolecularReplacementGPU::TranslationFunction(
|
||||
entry[fill[(static_cast<size_t>(setup.target_u[e]) * nv + setup.target_v[e]) * nwh
|
||||
+ setup.target_l[e]]++] = static_cast<int>(e);
|
||||
}
|
||||
// The same for the cross terms.
|
||||
std::vector<int> cross_offset(cells + 1, 0), cross_entry;
|
||||
std::vector<float2> known_f;
|
||||
if (known != nullptr) {
|
||||
if (cross) {
|
||||
for (size_t e = 0; e < setup.cross_term.size(); e++)
|
||||
cross_offset[(static_cast<size_t>(setup.cross_u[e]) * nv + setup.cross_v[e]) * nwh + setup.cross_l[e] + 1]++;
|
||||
for (size_t c = 0; c < cells; c++)
|
||||
@@ -457,28 +465,60 @@ std::vector<TranslationPeak> MolecularReplacementGPU::TranslationFunction(
|
||||
for (size_t e = 0; e < setup.cross_term.size(); e++)
|
||||
cross_entry[fill[(static_cast<size_t>(setup.cross_u[e]) * nv + setup.cross_v[e]) * nwh
|
||||
+ setup.cross_l[e]]++] = static_cast<int>(e);
|
||||
for (const std::complex<double> &k : *known)
|
||||
known_f.push_back(make_float2(static_cast<float>(k.real()), static_cast<float>(k.imag())));
|
||||
}
|
||||
auto d_cross_offset = upload(cross_offset, stream);
|
||||
auto d_cross_entry = upload(cross_entry, stream);
|
||||
auto d_known = upload(known_f, stream);
|
||||
auto d_cross_term = upload(setup.cross_term, stream);
|
||||
auto d_cross_op = upload(setup.cross_op, stream);
|
||||
auto d_cross_conj = upload(setup.cross_conj, stream);
|
||||
out->cross_offset = upload(cross_offset, stream);
|
||||
out->cross_entry = upload(cross_entry, stream);
|
||||
out->cross_term = upload(setup.cross_term, stream);
|
||||
out->cross_op = upload(setup.cross_op, stream);
|
||||
out->cross_conj = upload(setup.cross_conj, stream);
|
||||
std::vector<float2> phase(setup.phase.size());
|
||||
for (size_t i = 0; i < phase.size(); i++)
|
||||
phase[i] = make_float2(static_cast<float>(setup.phase[i].real()), static_cast<float>(setup.phase[i].imag()));
|
||||
const std::vector<float> weight(setup.weight.begin(), setup.weight.end());
|
||||
auto d_s = upload(float3s(setup.s), stream);
|
||||
auto d_phase = upload(phase, stream);
|
||||
auto d_weight = upload(weight, stream);
|
||||
auto d_bin = upload(setup.bin, stream);
|
||||
auto d_bin_n = upload(bin_counts(setup.bin, setup.bins), stream);
|
||||
auto d_pair_j = upload(setup.pair_j, stream);
|
||||
auto d_pair_k = upload(setup.pair_k, stream);
|
||||
auto d_offset = upload(offset, stream);
|
||||
auto d_entry = upload(entry, stream);
|
||||
out->s = upload(float3s(setup.s), stream);
|
||||
out->phase = upload(phase, stream);
|
||||
out->weight = upload(weight, stream);
|
||||
out->bin = upload(setup.bin, stream);
|
||||
out->bin_n = upload(bin_counts(setup.bin, setup.bins), stream);
|
||||
out->pair_j = upload(setup.pair_j, stream);
|
||||
out->pair_k = upload(setup.pair_k, stream);
|
||||
out->offset = upload(offset, stream);
|
||||
out->entry = upload(entry, stream);
|
||||
cufft_err(cufftPlan3d(&out->plan, nu, nv, nw, CUFFT_C2R));
|
||||
cuda_err(cudaStreamSynchronize(stream)); // the host vectors go out of scope
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<TranslationSetupGPU, void (*)(TranslationSetupGPU *)> MolecularReplacementGPU::Prepare(
|
||||
const TranslationSetup &setup, bool known_part) {
|
||||
return {prepare_translation(setup, known_part, state_->stream).release(),
|
||||
[](TranslationSetupGPU *p) { delete p; }};
|
||||
}
|
||||
|
||||
std::vector<TranslationPeak> MolecularReplacementGPU::TranslationFunction(
|
||||
const TranslationSetup &setup, const std::vector<gemmi::Mat33> &rotations,
|
||||
const std::vector<std::complex<double>> *known, TranslationSetupGPU *prepared) {
|
||||
std::vector<TranslationPeak> out(rotations.size());
|
||||
const int terms = static_cast<int>(setup.Terms()), nops = setup.ops;
|
||||
const int npairs = static_cast<int>(setup.pair_j.size());
|
||||
if (terms == 0 || rotations.empty() || (nops < 2 && known == nullptr))
|
||||
return out;
|
||||
cudaStream_t stream = state_->stream;
|
||||
const int nu = setup.nu, nv = setup.nv, nw = setup.nw, nwh = nw / 2 + 1;
|
||||
const size_t cells = static_cast<size_t>(nu) * nv * nwh, points = static_cast<size_t>(nu) * nv * nw;
|
||||
std::unique_ptr<TranslationSetupGPU> made;
|
||||
if (prepared == nullptr) {
|
||||
made = prepare_translation(setup, known != nullptr, stream);
|
||||
prepared = made.get();
|
||||
}
|
||||
TranslationSetupGPU &p = *prepared;
|
||||
std::vector<float2> known_f;
|
||||
if (known != nullptr)
|
||||
for (const std::complex<double> &k : *known)
|
||||
known_f.push_back(make_float2(static_cast<float>(k.real()), static_cast<float>(k.imag())));
|
||||
auto d_known = upload(known_f, stream);
|
||||
CudaDevicePtr<float2> copies(static_cast<size_t>(terms) * nops);
|
||||
CudaDevicePtr<float> term_sum(terms), scaled(terms);
|
||||
CudaDevicePtr<float2> coef(cells);
|
||||
@@ -489,21 +529,20 @@ std::vector<TranslationPeak> MolecularReplacementGPU::TranslationFunction(
|
||||
CudaDevicePtr<float> d_best(nstat);
|
||||
CudaDevicePtr<long long> d_best_index(nstat);
|
||||
|
||||
cufftHandle plan;
|
||||
cufft_err(cufftPlan3d(&plan, nu, nv, nw, CUFFT_C2R));
|
||||
try {
|
||||
const cufftHandle plan = p.plan;
|
||||
{
|
||||
cufft_err(cufftSetStream(plan, stream));
|
||||
for (size_t r = 0; r < rotations.size(); r++) {
|
||||
const Rot rot = transposed(rotations[r]);
|
||||
translation_copies<<<(terms + THREADS - 1) / THREADS, THREADS, 0, stream>>>(
|
||||
state_->transform, rot, d_s, d_phase, terms, nops, copies, term_sum);
|
||||
translation_weights<<<1, THREADS, 0, stream>>>(term_sum, d_weight, d_bin, d_bin_n, terms, setup.bins,
|
||||
state_->transform, rot, p.s, p.phase, terms, nops, copies, term_sum);
|
||||
translation_weights<<<1, THREADS, 0, stream>>>(term_sum, p.weight, p.bin, p.bin_n, terms, setup.bins,
|
||||
scaled);
|
||||
translation_gather<<<static_cast<unsigned>((cells + THREADS - 1) / THREADS), THREADS, 0, stream>>>(
|
||||
d_offset, d_entry, copies, scaled, d_pair_j, d_pair_k, nops, npairs, cells, coef);
|
||||
p.offset, p.entry, copies, scaled, p.pair_j, p.pair_k, nops, npairs, cells, coef);
|
||||
if (known != nullptr)
|
||||
translation_gather_cross<<<static_cast<unsigned>((cells + THREADS - 1) / THREADS), THREADS, 0, stream>>>(
|
||||
d_cross_offset, d_cross_entry, copies, scaled, d_known, d_cross_term, d_cross_op, d_cross_conj,
|
||||
p.cross_offset, p.cross_entry, copies, scaled, d_known, p.cross_term, p.cross_op, p.cross_conj,
|
||||
nops, cells, coef);
|
||||
cufft_err(cufftExecC2R(plan, reinterpret_cast<cufftComplex *>(coef.get()), map.get()));
|
||||
translation_stats<<<stat_blocks, THREADS, 0, stream>>>(
|
||||
@@ -511,9 +550,6 @@ std::vector<TranslationPeak> MolecularReplacementGPU::TranslationFunction(
|
||||
d_best.get() + r * stat_blocks, d_best_index.get() + r * stat_blocks);
|
||||
cuda_err(cudaGetLastError());
|
||||
}
|
||||
} catch (...) {
|
||||
cufftDestroy(plan);
|
||||
throw;
|
||||
}
|
||||
std::vector<double> sum(nstat), sum2(nstat);
|
||||
std::vector<float> best(nstat);
|
||||
@@ -524,7 +560,6 @@ std::vector<TranslationPeak> MolecularReplacementGPU::TranslationFunction(
|
||||
cuda_err(cudaMemcpyAsync(best_index.data(), d_best_index.get(), nstat * sizeof(long long),
|
||||
cudaMemcpyDeviceToHost, stream));
|
||||
cuda_err(cudaStreamSynchronize(stream));
|
||||
cufftDestroy(plan);
|
||||
|
||||
for (size_t r = 0; r < rotations.size(); r++) {
|
||||
double s = 0, s2 = 0;
|
||||
|
||||
@@ -25,6 +25,8 @@ gemmi::FPhiGrid<float> MapToFPhiGPU(const gemmi::Grid<float> &map);
|
||||
struct MolecularReplacementGPUState;
|
||||
struct MolecularReplacementGPUGroup;
|
||||
|
||||
struct TranslationSetupGPU;
|
||||
|
||||
class MolecularReplacementGPU {
|
||||
public:
|
||||
// Uploads the molecular transform, on the calling thread's current device. Throws JFJochException on
|
||||
@@ -36,10 +38,16 @@ public:
|
||||
const gemmi::SpaceGroup &sg, const std::vector<gemmi::Mat33> &rotations);
|
||||
|
||||
// The translation function of one group at each of `rotations`; with `known`, beside a known partial
|
||||
// structure (KnownStructureFactors).
|
||||
// structure (KnownStructureFactors). `prepared`: Prepare() of this setup, for a setup used again and again;
|
||||
// made for this call otherwise.
|
||||
std::vector<TranslationPeak> TranslationFunction(const TranslationSetup &setup,
|
||||
const std::vector<gemmi::Mat33> &rotations,
|
||||
const std::vector<std::complex<double>> *known = nullptr);
|
||||
const std::vector<std::complex<double>> *known = nullptr,
|
||||
TranslationSetupGPU *prepared = nullptr);
|
||||
// What the translation function needs of `setup` on the device, made once; with `known_part`, for the
|
||||
// function beside a known partial structure.
|
||||
std::unique_ptr<TranslationSetupGPU, void (*)(TranslationSetupGPU *)> Prepare(const TranslationSetup &setup,
|
||||
bool known_part);
|
||||
|
||||
private:
|
||||
std::unique_ptr<MolecularReplacementGPUState> state_;
|
||||
|
||||
+33
-29
@@ -2574,46 +2574,48 @@ ReportDocument BuildReportDocument(const std::string &output_prefix,
|
||||
doc.sections.push_back(std::move(s));
|
||||
}
|
||||
|
||||
// ------------------------------------------------- MODEL PRIOR (--af-model, experimental)
|
||||
// ------------------------------------------------- MOLECULAR REPLACEMENT (--mr, experimental)
|
||||
if (result.model_prior.has_value()) {
|
||||
const auto &mp = *result.model_prior;
|
||||
ReportSection s;
|
||||
s.title = "MODEL PRIOR (EXPERIMENTAL)";
|
||||
Add(s, KeyText("AF_MODEL_FILE", mp.model_path));
|
||||
s.title = "MOLECULAR REPLACEMENT (EXPERIMENTAL)";
|
||||
Add(s, KeyText("MR_MODEL_FILE", mp.model_path));
|
||||
if (!mp.ok) {
|
||||
Add(s, KeyEnum("MODEL_PRIOR", "NOT_PERFORMED", {"PERFORMED", "NOT_PERFORMED"}));
|
||||
Add(s, KeyText("MODEL_PRIOR_REASON", mp.failure_reason));
|
||||
Add(s, KeyEnum("MR", "NOT_PERFORMED", {"PERFORMED", "NOT_PERFORMED"}));
|
||||
Add(s, KeyText("MR_REASON", mp.failure_reason));
|
||||
} else {
|
||||
Add(s, KeyEnum("MODEL_PRIOR", "PERFORMED", {"PERFORMED", "NOT_PERFORMED"}));
|
||||
Add(s, KeyInt("AF_RESIDUES_READ", mp.residues_read));
|
||||
Add(s, KeyInt("AF_RESIDUES_KEPT", mp.residues_kept));
|
||||
Add(s, KeyInt("AF_PAE_DOMAINS", mp.pae_domains));
|
||||
Add(s, KeyText("MODEL_PRIOR_SEARCH_MODEL", mp.search_model));
|
||||
Add(s, KeyReal("SEARCH_D_MIN", mp.d_min_search, "{:.2f}"));
|
||||
Add(s, KeyReal("SCORE_D_MIN", mp.d_min_score, "{:.2f}"));
|
||||
Add(s, KeyText("MODEL_PRIOR_DECISION", mp.decision));
|
||||
Add(s, KeyEnum("MR", "PERFORMED", {"PERFORMED", "NOT_PERFORMED"}));
|
||||
Add(s, KeyEnum("MR_MODEL_KIND", mp.predicted_model ? "PREDICTED" : "EXPERIMENTAL",
|
||||
{"PREDICTED", "EXPERIMENTAL"}));
|
||||
Add(s, KeyInt("MR_RESIDUES_READ", mp.residues_read));
|
||||
Add(s, KeyInt("MR_RESIDUES_KEPT", mp.residues_kept));
|
||||
Add(s, KeyInt("MR_PAE_DOMAINS", mp.pae_domains));
|
||||
Add(s, KeyText("MR_SEARCH_MODEL", mp.search_model));
|
||||
Add(s, KeyReal("MR_SEARCH_D_MIN", mp.d_min_search, "{:.2f}"));
|
||||
Add(s, KeyReal("MR_SCORE_D_MIN", mp.d_min_score, "{:.2f}"));
|
||||
Add(s, KeyText("MR_DECISION", mp.decision));
|
||||
if (mp.chosen >= 0)
|
||||
{
|
||||
Add(s, KeyText("MODEL_PRIOR_SPACE_GROUP", mp.candidates[mp.chosen].space_group.xhm()));
|
||||
Add(s, KeyInt("MODEL_PRIOR_COPIES", mp.candidates[mp.chosen].copies));
|
||||
Add(s, KeyText("MR_SPACE_GROUP", mp.candidates[mp.chosen].space_group.xhm()));
|
||||
Add(s, KeyInt("MR_COPIES", mp.candidates[mp.chosen].copies));
|
||||
}
|
||||
if (mp.hand_check.performed) {
|
||||
const auto &h = mp.hand_check;
|
||||
Add(s, KeyEnum("MODEL_PRIOR_HAND_CHECK", h.adopted ? "ADOPTED" : "VETOED", {"ADOPTED", "VETOED"}));
|
||||
Add(s, KeyReal("MODEL_PRIOR_HAND_FIT_SIGMA", h.fit_sigma, "{:+.2f}"));
|
||||
Add(s, KeyBool("MODEL_PRIOR_ANOMALOUS_INVERTED", h.anomalous_inverted));
|
||||
Add(s, KeyInt("MODEL_PRIOR_ANOMALOUS_PAIRS", h.anomalous_pairs));
|
||||
Add(s, KeyEnum("MR_HAND_CHECK", h.adopted ? "ADOPTED" : "VETOED", {"ADOPTED", "VETOED"}));
|
||||
Add(s, KeyReal("MR_HAND_FIT_SIGMA", h.fit_sigma, "{:+.2f}"));
|
||||
Add(s, KeyBool("MR_ANOMALOUS_INVERTED", h.anomalous_inverted));
|
||||
Add(s, KeyInt("MR_ANOMALOUS_PAIRS", h.anomalous_pairs));
|
||||
if (std::isfinite(h.anomalous_scatterer_mean_sigma))
|
||||
Add(s, KeyReal("MODEL_PRIOR_ANOMALOUS_SCATTERER_MEAN_SIGMA", h.anomalous_scatterer_mean_sigma,
|
||||
Add(s, KeyReal("MR_ANOMALOUS_SCATTERER_MEAN_SIGMA", h.anomalous_scatterer_mean_sigma,
|
||||
"{:+.2f}"));
|
||||
if (h.anomalous_inverted)
|
||||
Add(s, KeyText("MODEL_PRIOR_ANOMALOUS_DEEPEST_SITE",
|
||||
Add(s, KeyText("MR_ANOMALOUS_DEEPEST_SITE",
|
||||
fmt::format("{} {:.1f}", h.deepest_site, h.deepest_sigma)));
|
||||
}
|
||||
if (!result.model_prior_file_prefix.empty())
|
||||
Add(s, KeyText("MODEL_PRIOR_FILE_PREFIX", result.model_prior_file_prefix));
|
||||
Add(s, KeyEnum("MODEL_PRIOR_SEARCH_DEVICE", mp.gpu_search ? "GPU" : "CPU", {"GPU", "CPU"}));
|
||||
Add(s, KeyReal("MODEL_PRIOR_SECONDS", mp.t_model + mp.t_transform + mp.t_rotation + mp.t_translation
|
||||
Add(s, KeyText("MR_FILE_PREFIX", result.model_prior_file_prefix));
|
||||
Add(s, KeyEnum("MR_SEARCH_DEVICE", mp.gpu_search ? "GPU" : "CPU", {"GPU", "CPU"}));
|
||||
Add(s, KeyReal("MR_SECONDS", mp.t_model + mp.t_transform + mp.t_rotation + mp.t_translation
|
||||
+ mp.t_rescore + mp.t_null + mp.t_domains + mp.hand_check.seconds, "{:.2f}"));
|
||||
Add(s, Blank());
|
||||
ReportEntry e;
|
||||
@@ -2652,14 +2654,16 @@ ReportDocument BuildReportDocument(const std::string &output_prefix,
|
||||
Add(s, std::move(e));
|
||||
Add(s, Blank());
|
||||
Add(s, Prose(fmt::format(
|
||||
" The predicted model, trimmed to pLDDT >= 70, was searched for in every space group each merge\n"
|
||||
" The model - a prediction trimmed to pLDDT >= 70, or an experimental structure\n"
|
||||
" (MR_MODEL_KIND) - was searched for in every space group each merge\n"
|
||||
" cannot tell apart (rotation function on a 6 deg sampling, translation function, {:.1f}-{:.1f} A),\n"
|
||||
" and the best placements taken through the rigid body; then further copies, one at a time, up to\n"
|
||||
" the largest copy number with a Matthews probability of 1% (COPIES_LIKELY: the most probable),\n"
|
||||
" each with the translation function beside the copies already placed, its peaks ranked by the\n"
|
||||
" likelihood gain of the copy (sigma_A from the pLDDT's coordinate error, its scale refined), kept\n"
|
||||
" likelihood gain of the copy (sigma_A from the pLDDT's coordinate error - 0.67 A for an\n"
|
||||
" experimental structure - its scale refined), kept\n"
|
||||
" where the copy stands out, packs and R-free drops by 0.01. R_WORK and R_FREE are of all COPIES,\n"
|
||||
" to SCORE_D_MIN; TF_Z is the highest of the copies'; LLG_Z the highest Z-score of a copy's refined\n"
|
||||
" to MR_SCORE_D_MIN; TF_Z is the highest of the copies'; LLG_Z the highest Z-score of a copy's refined\n"
|
||||
" likelihood gain over the same copy at random positions, where its TF_Z was below 8. A copy stands\n"
|
||||
" out where TF_Z or LLG_Z is 8 or more. PLACED needs a copy that stands out, at most 5% of the\n"
|
||||
" C-alpha atoms clashing with another copy or a symmetry mate, one copy fitting the cell\n"
|
||||
@@ -2671,9 +2675,9 @@ ReportDocument BuildReportDocument(const std::string &output_prefix,
|
||||
" its law, the model searched for in the subgroup leads the chosen placement read as a crystal\n"
|
||||
" there by 0.03 in R-free (R_FREE_TWIN). A group one copy cannot fit is checked for\n"
|
||||
" packing only, not refined. Where the AlphaFold DB's predicted aligned error sits beside the\n"
|
||||
" model, the prediction is cut into rigid domains (AF_PAE_DOMAINS), residues in none left out;\n"
|
||||
" model, the prediction is cut into rigid domains (MR_PAE_DOMAINS), residues in none left out;\n"
|
||||
" where the whole model is placed nowhere, each domain is searched by itself, and the domains\n"
|
||||
" placed have to agree (MODEL_PRIOR_SEARCH_MODEL: what the decision was made with).\n"
|
||||
" placed have to agree (MR_SEARCH_MODEL: what the decision was made with).\n"
|
||||
" The answer's files take the enantiomorph from it - only where model validation of the placed\n"
|
||||
" model accepts it as it accepts a --model's: a fit over its null and an anomalous map at its\n"
|
||||
" atoms that is not inverted (ENANTIOMORPH_VETOED otherwise) - and nothing else; another group it\n"
|
||||
|
||||
+7
-6
@@ -200,11 +200,12 @@ struct ProcessConfig {
|
||||
// Fo-Fc maps (--model).
|
||||
std::string model_path;
|
||||
|
||||
// Experimental (--af-model): a predicted model (AlphaFold mmCIF, pLDDT in the B column) placed by
|
||||
// molecular replacement, one copy, in every space group the merge cannot tell apart and in the twin
|
||||
// hypothesis' subgroup. It never changes the answer's files beyond the enantiomorph label; a
|
||||
// different group it chooses is written beside them as <prefix>_mr.
|
||||
std::string af_model_path;
|
||||
// Experimental (--mr): a model - a prediction (AlphaFold, pLDDT in the B column) or an experimental
|
||||
// structure, told apart from the file - placed by molecular replacement, as many copies as the cell holds,
|
||||
// in every space group the merge cannot tell apart and in the twin hypothesis' subgroup. It never changes
|
||||
// the answer's files beyond the enantiomorph label; a different group it chooses is written beside them
|
||||
// as <prefix>_mr.
|
||||
std::string mr_model_path;
|
||||
|
||||
// --finalist-ledger: report-only symmetry evidence table (and the twin-immune zones), folded from
|
||||
// a merge of its own in P1 that only this flag makes.
|
||||
@@ -572,7 +573,7 @@ struct ProcessResult {
|
||||
// that was asked to validate looking like one that was not. Unset when no model was asked for.
|
||||
std::optional<ModelValidationResult> model_validation;
|
||||
|
||||
// What --af-model found, when one was given; failure_reason says why where it could not run.
|
||||
// What --mr found, when one was given; failure_reason says why where it could not run.
|
||||
std::optional<MolecularReplacementResult> model_prior;
|
||||
// The file prefix the model's choice was written under (<prefix>_mr), empty where nothing was.
|
||||
std::string model_prior_file_prefix;
|
||||
|
||||
@@ -82,8 +82,8 @@ std::string RugnuxCommandLine(const ProcessConfig &config,
|
||||
}
|
||||
if (!config.model_path.empty())
|
||||
add("--model", config.model_path);
|
||||
if (!config.af_model_path.empty())
|
||||
add("--af-model", config.af_model_path);
|
||||
if (!config.mr_model_path.empty())
|
||||
add("--mr", config.mr_model_path);
|
||||
}
|
||||
if (config.start_image != 0)
|
||||
add("-s", std::to_string(config.start_image));
|
||||
|
||||
@@ -1425,6 +1425,8 @@ bool Rugnux::ScaleMergeAndSymmetry(PipelineLocals &p) {
|
||||
std::string twin_overruled_reason;
|
||||
std::optional<TwinSubgroupChoice> twin_choice;
|
||||
std::string twin_choice_reason;
|
||||
// The twin hypothesis' merge made only for --mr to test: written, and reported, only where it chooses it.
|
||||
bool twin_for_mr_only = false;
|
||||
int twin_choice_group_number = 0; // the adopted group's, when the choice was made
|
||||
// A canonical pass the geometry walk is going to replace stops here, where the answer is
|
||||
// already in (GeometryWalkTakesRound); it is asked again below, before the analyses. Not with a
|
||||
@@ -2874,15 +2876,16 @@ bool Rugnux::ScaleMergeAndSymmetry(PipelineLocals &p) {
|
||||
// too, with the law - so a refinement program can take the twin hypothesis without a re-run.
|
||||
// The answer itself is not touched by any of this.
|
||||
const bool pre_twin_like = result.pre_promotion_twinning && result.pre_promotion_twinning->twinning_suspected;
|
||||
// A predicted model can test the twin hypothesis itself (--af-model's twin comparison), so with one the
|
||||
// A predicted model can test the twin hypothesis itself (--mr's twin comparison), so with one the
|
||||
// merge in the indexed lattice's group is made wherever a metric re-ask promoted from it.
|
||||
const bool model_tests_twin = !config_.af_model_path.empty() && twin_metric_subgroup.has_value();
|
||||
const bool model_tests_twin = !config_.mr_model_path.empty() && twin_metric_subgroup.has_value();
|
||||
if (!geometry_prepass && result.space_group && sg_search.point_group_order > 1
|
||||
&& (twin_overruled_subgroup || pre_twin_like || model_tests_twin)) {
|
||||
twin_choice = ChooseTwinSubgroup(*result.space_group, sg_search,
|
||||
twin_overruled_subgroup ? twin_overruled_subgroup
|
||||
: twin_metric_subgroup);
|
||||
twin_choice_group_number = result.space_group->number;
|
||||
twin_for_mr_only = !twin_overruled_subgroup && !pre_twin_like;
|
||||
twin_choice_reason = twin_overruled_subgroup
|
||||
? fmt::format("a twin gate refused it ({}) and was overruled", twin_overruled_reason)
|
||||
: pre_twin_like
|
||||
@@ -3978,7 +3981,7 @@ bool Rugnux::ScaleMergeAndSymmetry(PipelineLocals &p) {
|
||||
}
|
||||
prepare_twin_file();
|
||||
|
||||
// --af-model (experimental): the predicted model placed by molecular replacement, one copy, in the
|
||||
// --mr (experimental): the model placed by molecular replacement, as many copies as the cell holds, in the
|
||||
// groups the answer's merge cannot tell apart and in the twin hypothesis' subgroup. It takes the
|
||||
// enantiomorph label into the answer, as --model does, and nothing else: a different group it
|
||||
// chooses goes to files of its own beside the answer's, <prefix>_mr, from the merge it was chosen on.
|
||||
@@ -3991,20 +3994,20 @@ bool Rugnux::ScaleMergeAndSymmetry(PipelineLocals &p) {
|
||||
std::shared_ptr<gemmi::Structure> placed;
|
||||
};
|
||||
std::optional<ModelPriorFile> model_prior_file;
|
||||
if (result.consensus_cell && write_files && !config_.af_model_path.empty() && !geometry_prepass
|
||||
if (result.consensus_cell && write_files && !config_.mr_model_path.empty() && !geometry_prepass
|
||||
&& !superseded) {
|
||||
phase("Placing the predicted model");
|
||||
Step("Model prior");
|
||||
Step("Molecular replacement");
|
||||
TimingMark mark(logger, "model prior");
|
||||
std::vector<MRMerge> merges{{"MERGE", &sm.merged, *result.consensus_cell, experiment_.GetSpaceGroupOrP1()}};
|
||||
if (twin_file)
|
||||
merges.push_back({"TWIN_SUBGROUP_MERGE", &twin_file->merged, twin_file->cell, twin_file->space_group,
|
||||
twin_file->law});
|
||||
MolecularReplacementResult mr = RunMolecularReplacement(config_.af_model_path, merges, logger,
|
||||
MolecularReplacementResult mr = RunMolecularReplacement(config_.mr_model_path, merges, logger,
|
||||
static_cast<size_t>(config_.nthreads));
|
||||
if (!mr.ok)
|
||||
result.warnings.push_back({PathologyCode::MODEL_NOT_VALIDATED,
|
||||
"The predicted model was not placed: " + mr.failure_reason});
|
||||
"The --mr model could not be searched for: " + mr.failure_reason});
|
||||
if (mr.chosen >= 0) {
|
||||
const MRCandidate &c = mr.candidates[mr.chosen];
|
||||
if (mr.decision == "ENANTIOMORPH")
|
||||
@@ -4020,7 +4023,7 @@ bool Rugnux::ScaleMergeAndSymmetry(PipelineLocals &p) {
|
||||
if (const gemmi::SpaceGroup *other = gemmi::find_spacegroup_by_ops(e))
|
||||
twin_file->space_group = *other;
|
||||
}
|
||||
logger.Info("Model prior: the answer is relabelled {}, the enantiomorph the placed model chose "
|
||||
logger.Info("Molecular replacement: the answer is relabelled {}, the enantiomorph the placed model chose "
|
||||
"(fit {:+.1f} sd over its null, anomalous map at its atoms not inverted, {:.3f} s)",
|
||||
c.space_group.xhm(), mr.hand_check.fit_sigma, mr.hand_check.seconds);
|
||||
} else if (mr.decision == "SPACE_GROUP") {
|
||||
@@ -4037,6 +4040,10 @@ bool Rugnux::ScaleMergeAndSymmetry(PipelineLocals &p) {
|
||||
}
|
||||
if (model_prior_file)
|
||||
result.model_prior_file_prefix = config_.output_prefix + "_mr";
|
||||
if (twin_for_mr_only && mr.decision != "TWIN_SUBGROUP") {
|
||||
twin_file.reset();
|
||||
result.twin_subgroup_merge.reset();
|
||||
}
|
||||
result.model_prior = std::move(mr);
|
||||
}
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ void print_usage() {
|
||||
std::cout << " -z, --reference <file> Reference data of the same crystal form: an MTZ, or a PDB structure-factor mmCIF (e.g. 1abc-sf.cif, first merged block with a usable column; R-free set from _refln.status f/o, else pdbx_r_free_flag). Gzipped or not, recognised by content. --reference-mtz is the same option" << std::endl;
|
||||
std::cout << " --reference-column <label> Reference column to use, by MTZ label; an mmCIF is read under the labels gemmi cif2mtz gives it (IMEAN, FP, I(+), FC, F-model, ...) (default: auto - F-model, else IMEAN/I, else FP/FOBS/F)" << std::endl;
|
||||
std::cout << " --model <file> After merging, validate vs this atomic model: R-free, sigma_A-weighted 2mFo-DFc/mFo-DFc maps, the model as placed (written beside them), and - where the merge kept the Bijvoet split - an anomalous difference map with the strongest anomalous scatterers named by the atom they sit on. PDB or mmCIF, gzipped or not, recognised by content rather than by extension" << std::endl;
|
||||
std::cout << " --af-model <file> Experimental: place this predicted model (AlphaFold mmCIF, pLDDT in the B column; residues below pLDDT 70 removed; where the AlphaFold DB's predicted-aligned-error JSON sits beside it under its own name, the model is cut into rigid domains) by molecular replacement, as many copies as the cell holds, in every space group the merge cannot tell apart and in the twin hypothesis' subgroup. Reported in the MODEL PRIOR section; it may relabel the enantiomorph of the answer, and a different space group it chooses is written beside the answer as <prefix>_mr.mtz/.cif with the placed model <prefix>_mr_model.pdb. --mode mx only" << std::endl;
|
||||
std::cout << " --mr <file> Experimental: place this model by molecular replacement, as many copies as the cell holds, in every space group the merge cannot tell apart and in the twin hypothesis' subgroup. A prediction (AlphaFold or ModelCIF, pLDDT in the B column) is trimmed to the residues of pLDDT 70 and above, and where the AlphaFold DB's predicted-aligned-error JSON sits beside it under its own name it is cut into rigid domains; any other PDB or mmCIF is taken as an experimental structure, its polymer only (no waters, ligands, hydrogens or alternative conformers). Which of the two it is is read from the file. Reported in the MOLECULAR REPLACEMENT section; it may relabel the enantiomorph of the answer, and a different space group it chooses is written beside the answer as <prefix>_mr.mtz/.cif with the placed model <prefix>_mr_model.pdb. --mode mx only" << std::endl;
|
||||
std::cout << " --export-unmerged Write <prefix>_unmerged.mtz, an unmerged MTZ (POINTLESS column layout) of the integrated observations, for aimless / pointless / careless. On by default, whenever there is an output prefix. On a rotation run the partials of each reflection are summed into one full, written at the batch its rocking curve is centred on, with the summed rocking-curve fraction in FRACTIONCALC; an event that caught less of its rocking curve than --min-captured-fraction (or --min-partiality) is not written, exactly as in the merge, since a full is what the reading program is told these rows are. Intensities carry the deterministic per-reflection corrections and nothing else - Lorentz and polarization in the LP column, the sensor's angle-dependent efficiency in QE and the flight path's attenuation in FLIGHT, so raw counts are I / LP * QE * FLIGHT - while the partiality is not divided out and the per-image scale is not applied at all, since those programs scale the data themselves. Written in --mode mx and --mode scale, and with --no-merge" << std::endl;
|
||||
std::cout << " --no-export-unmerged Do not write <prefix>_unmerged.mtz. It is the largest file a run produces, so a run whose observations are not going to another scaling program - a regression battery, say - can skip writing it" << std::endl;
|
||||
std::cout << " --export-unmerged-partials Write <prefix>_unmerged_partials.mtz, the same observations with each partial as its own row (one batch per image) flagged for the reading program to sum, instead of summed here. Off by default and independent of --export-unmerged; both can be written in one run" << std::endl;
|
||||
@@ -287,7 +287,7 @@ enum {
|
||||
OPT_REJECT_OUTLIERS,
|
||||
OPT_REFERENCE_COLUMN,
|
||||
OPT_MODEL,
|
||||
OPT_AF_MODEL,
|
||||
OPT_MR,
|
||||
OPT_EXPORT_UNMERGED,
|
||||
OPT_NO_EXPORT_UNMERGED,
|
||||
OPT_EXPORT_UNMERGED_PARTIALS,
|
||||
@@ -350,7 +350,7 @@ static option long_options[] = {
|
||||
{"reference-mtz", required_argument, nullptr, 'z'},
|
||||
{"reference-column", required_argument, nullptr, OPT_REFERENCE_COLUMN},
|
||||
{"model", required_argument, nullptr, OPT_MODEL},
|
||||
{"af-model", required_argument, nullptr, OPT_AF_MODEL},
|
||||
{"mr", required_argument, nullptr, OPT_MR},
|
||||
{"export-unmerged", no_argument, nullptr, OPT_EXPORT_UNMERGED},
|
||||
{"no-export-unmerged", no_argument, nullptr, OPT_NO_EXPORT_UNMERGED},
|
||||
{"export-unmerged-partials", no_argument, nullptr, OPT_EXPORT_UNMERGED_PARTIALS},
|
||||
@@ -939,7 +939,7 @@ static int RunRugnux(int argc, char **argv) {
|
||||
std::string ref_mtz;
|
||||
std::string ref_column;
|
||||
std::string model_pdb; // --model: PDB to validate merged intensities against (R-free + maps)
|
||||
std::string af_model; // --af-model: predicted model placed by molecular replacement (experimental)
|
||||
std::string mr_model; // --mr: model placed by molecular replacement (experimental)
|
||||
bool export_unmerged = true; // on by default; --no-export-unmerged turns it off
|
||||
bool export_unmerged_partials = false; // --export-unmerged-partials: the same, one row per image
|
||||
std::string dump_observations; // diagnostic: dump unmerged -P rot3d fulls to this path
|
||||
@@ -1164,8 +1164,8 @@ static int RunRugnux(int argc, char **argv) {
|
||||
case OPT_MODEL:
|
||||
model_pdb = optarg;
|
||||
break;
|
||||
case OPT_AF_MODEL:
|
||||
af_model = optarg;
|
||||
case OPT_MR:
|
||||
mr_model = optarg;
|
||||
break;
|
||||
case OPT_EXPORT_UNMERGED:
|
||||
export_unmerged = true;
|
||||
@@ -1551,8 +1551,8 @@ static int RunRugnux(int argc, char **argv) {
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
if (!af_model.empty() && mode != RugnuxMode::MX) {
|
||||
logger.Error("--af-model is for --mode mx only");
|
||||
if (!mr_model.empty() && mode != RugnuxMode::MX) {
|
||||
logger.Error("--mr is for --mode mx only");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
@@ -3094,7 +3094,7 @@ static int RunRugnux(int argc, char **argv) {
|
||||
config.export_unmerged = export_unmerged;
|
||||
config.export_unmerged_partials = export_unmerged_partials;
|
||||
config.model_path = model_pdb;
|
||||
config.af_model_path = af_model;
|
||||
config.mr_model_path = mr_model;
|
||||
// When merging, the merged reflections (.mtz/.cif) are the wanted output; skip the large
|
||||
// _process.h5 unless explicitly requested. Without merging, the _process.h5 is the only output.
|
||||
config.write_process_h5 = run_scaling ? write_process_h5_flag : true;
|
||||
|
||||
@@ -229,6 +229,56 @@ TEST_CASE("MolecularReplacement_LikelihoodPlacement", "[MolecularReplacement]")
|
||||
CHECK(LogLikelihoodGain(setup, f, 0.3, 0.0) == Catch::Approx(0.0).margin(1e-9));
|
||||
}
|
||||
|
||||
TEST_CASE("MolecularReplacement_ModelKind", "[MolecularReplacement]") {
|
||||
// Three alanines along x, written as PDB with a CRYST1 line and the B column given per atom.
|
||||
const auto write = [](const std::string &path, const char *cryst1, const float b[3], bool water, bool altloc) {
|
||||
std::ofstream out(path);
|
||||
out << cryst1 << "\n";
|
||||
int serial = 1;
|
||||
for (int r = 0; r < 3; r++)
|
||||
for (const char *name : {" N ", " CA ", " C ", " O "}) {
|
||||
char line[128];
|
||||
const char alt = altloc && r == 1 ? 'A' : ' ';
|
||||
std::snprintf(line, sizeof line, "ATOM %5d %4s%cALA A%4d %8.3f%8.3f%8.3f%6.2f%6.2f %c",
|
||||
serial++, name, alt, r + 1, 3.8 * r + (name[1] == 'C' && name[2] == ' ' ? 1.5 : 0.0),
|
||||
name[1] == 'O' ? 1.2 : 0.0, 0.0, 1.0, b[r], name[1]);
|
||||
out << line << "\n";
|
||||
if (alt == 'A') {
|
||||
std::snprintf(line, sizeof line, "ATOM %5d %4sBALA A%4d %8.3f%8.3f%8.3f%6.2f%6.2f %c",
|
||||
serial++, name, r + 1, 3.8 * r + 0.3, 0.3, 0.0, 0.5, b[r], name[1]);
|
||||
out << line << "\n";
|
||||
}
|
||||
}
|
||||
if (water)
|
||||
out << "HETATM 100 O HOH A 101 10.000 10.000 10.000 1.00 30.00 O\n";
|
||||
out << "END\n";
|
||||
};
|
||||
const float plddt[3] = {90, 85, 95};
|
||||
write("MolecularReplacement_ModelKind_af.pdb", "CRYST1 1.000 1.000 1.000 90.00 90.00 90.00 P 1 1",
|
||||
plddt, false, false);
|
||||
const SearchModel af = PrepareSearchModel("MolecularReplacement_ModelKind_af.pdb");
|
||||
CHECK(af.failure_reason.empty());
|
||||
CHECK(af.predicted);
|
||||
CHECK(af.residues_kept == 3);
|
||||
|
||||
// In a cell, with refined B-factors (one above 100), a water and an alternative conformer.
|
||||
const float refined[3] = {25, 112, 40};
|
||||
write("MolecularReplacement_ModelKind_xtal.pdb", "CRYST1 50.000 60.000 70.000 90.00 90.00 90.00 P 21 21 21 4",
|
||||
refined, true, true);
|
||||
const SearchModel x = PrepareSearchModel("MolecularReplacement_ModelKind_xtal.pdb");
|
||||
CHECK(x.failure_reason.empty());
|
||||
CHECK(!x.predicted);
|
||||
CHECK(x.residues_kept == 3);
|
||||
size_t atoms = 0;
|
||||
for (const gemmi::Chain &ch : x.st.models[0].chains)
|
||||
for (const gemmi::Residue &r : ch.residues)
|
||||
atoms += r.atoms.size();
|
||||
CHECK(atoms == 12); // no water, one conformer
|
||||
CHECK(x.error_A == Catch::Approx(0.67).margin(0.01));
|
||||
std::remove("MolecularReplacement_ModelKind_af.pdb");
|
||||
std::remove("MolecularReplacement_ModelKind_xtal.pdb");
|
||||
}
|
||||
|
||||
TEST_CASE("MolecularReplacement_PaeDomains", "[MolecularReplacement]") {
|
||||
CHECK(PaePathFor("/x/AF-P12345-F1-model_v6.cif") == "/x/AF-P12345-F1-predicted_aligned_error_v6.json");
|
||||
CHECK(PaePathFor("AF-P12345-F1-model_v4.pdb.gz") == "AF-P12345-F1-predicted_aligned_error_v4.json");
|
||||
|
||||
Reference in New Issue
Block a user