rugnux --af-model: tNCS half-vector moves checked, the merge's own group not ruled out beside tNCS, P1's first copy judged by its orientation (WIP, validation pending)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -102,6 +102,14 @@ constexpr double NULL_SIGMA_MIN = 4.51;
|
||||
// A model whose expected log-likelihood gain (ExpectedLogLikelihoodGain) is below this is not searched for. The
|
||||
// lowest of a model the search did place, over 193 survey sets: 26; every PAE domain below 24 placed nowhere.
|
||||
constexpr double ELLG_MIN = 10.0;
|
||||
// tNCS by half a lattice vector v: copies v apart make the reflections with h.v half-integral weak, and a
|
||||
// placement moved by (v + o) / 2, o an origin shift of the group, fits the strong ones as well as the right one
|
||||
// does; only the weak ones tell them apart. A placement is compared with every such move of itself on those
|
||||
// reflections (R_tncs_weak), and one of them that fits them better by TNCS_SHIFT_MARGIN is taken instead; where the
|
||||
// two best are closer than that, the placement is not decided on. A vector is taken as half-integral within
|
||||
// TNCS_HALF_TOLERANCE.
|
||||
constexpr double TNCS_SHIFT_MARGIN = 0.01;
|
||||
constexpr double TNCS_HALF_TOLERANCE = 0.05;
|
||||
|
||||
double seconds_since(std::chrono::steady_clock::time_point t0) {
|
||||
return std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
|
||||
@@ -161,6 +169,9 @@ struct PlacementScore {
|
||||
// Against the data as twinned by a law (score_placement's `law`): at the fraction that gives the lowest
|
||||
// R-work.
|
||||
double r_work_twin = NAN, r_free_twin = NAN, twin_fraction = NAN;
|
||||
// Over the reflections a tNCS vector v weakens - h.v half-integral - work and free together (score_placement's
|
||||
// `tncs`).
|
||||
double r_tncs_weak = NAN;
|
||||
};
|
||||
|
||||
gemmi::AsuData<gemmi::ValueSigma<float>> working_amplitudes(const std::vector<MergedReflection> &merged,
|
||||
@@ -205,37 +216,54 @@ void model_factors(const gemmi::Structure &st, double d_min, gemmi::AsuData<std:
|
||||
|
||||
// R-work and R-free of the scaled Fcalc over the reflections to d_min.
|
||||
PlacementScore r_factors(const gemmi::AsuData<std::complex<float>> &fmodel, const gemmi::Scaling<float> &scaling,
|
||||
const gemmi::SpaceGroup &sg, const std::vector<MergedReflection> &merged, double d_min) {
|
||||
const gemmi::SpaceGroup &sg, const std::vector<MergedReflection> &merged, double d_min,
|
||||
const gemmi::Fractional *tncs = nullptr) {
|
||||
const gemmi::GroupOps gops = sg.operations();
|
||||
const gemmi::ReciprocalAsu asu(&sg);
|
||||
std::unordered_map<long, std::pair<double, bool>> obs;
|
||||
struct Obs {
|
||||
double f;
|
||||
bool free;
|
||||
bool weak; // h.v half-integral
|
||||
};
|
||||
std::unordered_map<long, Obs> obs;
|
||||
for (const MergedReflection &r : merged) {
|
||||
if (std::isnan(r.F) || r.d < d_min)
|
||||
continue;
|
||||
gemmi::Miller h{{r.h, r.k, r.l}};
|
||||
if (!asu.is_in(h))
|
||||
h = asu.to_asu(h, gops).first;
|
||||
obs[hkl_key(h)] = {r.F, r.rfree_flag};
|
||||
bool weak = false;
|
||||
if (tncs != nullptr) {
|
||||
const double hv = h[0] * tncs->x + h[1] * tncs->y + h[2] * tncs->z;
|
||||
weak = std::fabs(hv - std::round(hv)) > 0.25;
|
||||
}
|
||||
obs[hkl_key(h)] = {r.F, r.rfree_flag, weak};
|
||||
}
|
||||
double num_w = 0, den_w = 0, num_f = 0, den_f = 0;
|
||||
double num_w = 0, den_w = 0, num_f = 0, den_f = 0, num_t = 0, den_t = 0;
|
||||
for (const auto &hv : fmodel.v) {
|
||||
const auto it = obs.find(hkl_key(hv.hkl));
|
||||
if (it == obs.end())
|
||||
continue;
|
||||
const double fo = it->second.first, fc = std::abs(hv.value);
|
||||
if (it->second.second) {
|
||||
const double fo = it->second.f, fc = std::abs(hv.value);
|
||||
if (it->second.free) {
|
||||
num_f += std::fabs(fo - fc);
|
||||
den_f += fo;
|
||||
} else {
|
||||
num_w += std::fabs(fo - fc);
|
||||
den_w += fo;
|
||||
}
|
||||
if (it->second.weak) {
|
||||
num_t += std::fabs(fo - fc);
|
||||
den_t += fo;
|
||||
}
|
||||
}
|
||||
PlacementScore out;
|
||||
out.k_sol = scaling.k_sol;
|
||||
out.b_sol = scaling.b_sol;
|
||||
out.r_work = den_w > 0 ? num_w / den_w : 1;
|
||||
out.r_free = den_f > 0 ? num_f / den_f : 1;
|
||||
if (tncs != nullptr)
|
||||
out.r_tncs_weak = den_t > 0 ? num_t / den_t : 1;
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -423,7 +451,7 @@ void twin_r_factors(const gemmi::AsuData<std::complex<float>> &fmodel, const gem
|
||||
PlacementScore score_placement(const gemmi::Structure &st, const gemmi::SpaceGroup &sg,
|
||||
const std::vector<MergedReflection> &merged, double d_min, size_t nthreads,
|
||||
ScoreEngines *engines = nullptr, const PlacementScore *solvent = nullptr,
|
||||
const gemmi::Op *law = nullptr) {
|
||||
const gemmi::Op *law = nullptr, const gemmi::Fractional *tncs = nullptr) {
|
||||
gemmi::AsuData<std::complex<float>> fmask, fmodel;
|
||||
#ifdef JFJOCH_USE_CUDA
|
||||
ScoreEngines::Engine *gpu = engines != nullptr ? engines->get(st, sg, d_min) : nullptr;
|
||||
@@ -454,7 +482,7 @@ PlacementScore score_placement(const gemmi::Structure &st, const gemmi::SpaceGro
|
||||
FitModelScale(scaling, {.b_hi = SolventBMax(st.models[0])}, nthreads);
|
||||
}
|
||||
scaling.scale_data(fmodel, &fmask);
|
||||
PlacementScore out = r_factors(fmodel, scaling, sg, merged, d_min);
|
||||
PlacementScore out = r_factors(fmodel, scaling, sg, merged, d_min, tncs);
|
||||
if (law != nullptr)
|
||||
twin_r_factors(fmodel, sg, merged, d_min, *law, out);
|
||||
return out;
|
||||
@@ -651,6 +679,33 @@ bool enantiomorphs(const gemmi::SpaceGroup &a, const gemmi::SpaceGroup &b) {
|
||||
return other != nullptr && other->number == b.number;
|
||||
}
|
||||
|
||||
// The tNCS vector where it is half a lattice vector - every component within TNCS_HALF_TOLERANCE of 0 or 1/2 -
|
||||
// on those halves; empty otherwise.
|
||||
std::optional<gemmi::Fractional> half_tncs_vector(const TranslationalNcs &tncs) {
|
||||
if (!tncs.present)
|
||||
return std::nullopt;
|
||||
double v[3] = {tncs.vector.x, tncs.vector.y, tncs.vector.z};
|
||||
for (double &x : v) {
|
||||
const double h = std::round(2 * x) / 2;
|
||||
if (std::fabs(x - h) > TNCS_HALF_TOLERANCE)
|
||||
return std::nullopt;
|
||||
x = h - std::floor(h);
|
||||
}
|
||||
return gemmi::Fractional(v[0], v[1], v[2]);
|
||||
}
|
||||
|
||||
// Whether moving the origin by `s` (a multiple of 1/24 in each component) leaves `sg`'s operators as they are:
|
||||
// a placement moved by s is then the same crystal.
|
||||
bool is_origin_shift(const gemmi::SpaceGroup &sg, const gemmi::Fractional &s) {
|
||||
gemmi::Op shift = gemmi::Op::identity();
|
||||
const double x[3] = {s.x, s.y, s.z};
|
||||
for (int a = 0; a < 3; a++)
|
||||
shift.tran[a] = static_cast<int>(std::lround(x[a] * gemmi::Op::DEN)) % gemmi::Op::DEN;
|
||||
gemmi::GroupOps ops = sg.operations();
|
||||
ops.change_basis_forward(shift);
|
||||
return ops.is_same_as(sg.operations());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// --- the model ---
|
||||
@@ -2531,8 +2586,11 @@ MolecularReplacementResult search_model(const SearchModel &model, const std::vec
|
||||
cand.verdict = "CLASH";
|
||||
// The likelihood's Z-score stands in for the translation function's only where the copies placed
|
||||
// are as many as the cell most probably holds: beside content the search has not found, a copy's
|
||||
// likelihood over random positions of its own is no measure of the placement.
|
||||
else if (!(cand.tf_z >= TF_Z_PLACED || (cand.llg_z >= TF_Z_PLACED && cand.copies >= cand.copies_likely)))
|
||||
// likelihood over random positions of its own is no measure of the placement. In P1 the first copy sits
|
||||
// at the origin, which is arbitrary there and which no function of its position can single out: one copy
|
||||
// is judged by its orientation alone, against the null.
|
||||
else if (!(cand.tf_z >= TF_Z_PLACED || (cand.llg_z >= TF_Z_PLACED && cand.copies >= cand.copies_likely)
|
||||
|| (gops.order() == 1 && cand.copies == 1)))
|
||||
cand.verdict = "NOT_PLACED";
|
||||
else
|
||||
cand.verdict = "PLACED";
|
||||
@@ -2630,6 +2688,79 @@ MolecularReplacementResult search_model(const SearchModel &model, const std::vec
|
||||
// by side, each taken where the choice comes to it.
|
||||
std::vector<double> nulls(result.candidates.size(), NAN);
|
||||
bool first = true;
|
||||
// With tNCS by half a lattice vector, candidate i against its moves by half that vector (TNCS_SHIFT_MARGIN):
|
||||
// the better taken into it, and whether the two best are too close to tell apart.
|
||||
// The tNCS vector is the answer's merge's, in its cell.
|
||||
const std::optional<gemmi::Fractional> half = merge == 0 ? half_tncs_vector(result.tncs) : std::nullopt;
|
||||
std::vector<char> shift_checked(result.candidates.size(), 0);
|
||||
const auto check_shifts = [&](int i) -> bool {
|
||||
MRCandidate &c = result.candidates[i];
|
||||
const gemmi::Fractional v = *half;
|
||||
const std::vector<MergedReflection> &merged = *merges[merge].merged;
|
||||
const double d_min = result.d_min_score;
|
||||
struct Option {
|
||||
gemmi::Fractional shift;
|
||||
std::shared_ptr<gemmi::Structure> placed;
|
||||
PlacementScore score;
|
||||
double clash = 0;
|
||||
};
|
||||
std::vector<Option> options{{gemmi::Fractional(0, 0, 0), c.placed,
|
||||
score_placement(*c.placed, c.space_group, merged, d_min, nthreads, &scorers,
|
||||
nullptr, nullptr, &v), c.clash_fraction}};
|
||||
RigidBodyEngines engines;
|
||||
for (int o = 0; o < 8; o++) {
|
||||
const gemmi::Fractional shift((v.x + 0.5 * (o & 1)) / 2, (v.y + 0.5 * ((o >> 1) & 1)) / 2,
|
||||
(v.z + 0.5 * ((o >> 2) & 1)) / 2);
|
||||
// The same crystal as one already tried, or as no move at all.
|
||||
if (std::any_of(options.begin(), options.end(), [&](const Option &p) {
|
||||
return is_origin_shift(c.space_group, gemmi::Fractional(shift.x - p.shift.x, shift.y - p.shift.y,
|
||||
shift.z - p.shift.z));
|
||||
}))
|
||||
continue;
|
||||
auto moved = std::make_shared<gemmi::Structure>(*c.placed);
|
||||
const gemmi::Position by = moved->cell.orthogonalize(shift);
|
||||
std::vector<gemmi::Position> pos = ModelPositions(moved->models[0]);
|
||||
for (gemmi::Position &p : pos)
|
||||
p = gemmi::Position(p + by);
|
||||
SetModelPositions(moved->models[0], pos);
|
||||
Option opt{shift, moved, {}, ClashFraction(*moved, c.copies)};
|
||||
if (opt.clash <= 2 * CLASH_MAX) {
|
||||
if (engines.get() == nullptr)
|
||||
engines = make_engines(*moved, c.space_group, merged, d_min, 1, logger);
|
||||
refine_rigid_body(*moved, c.space_group, merged, d_min, engines.get(), nthreads);
|
||||
opt.clash = ClashFraction(*moved, c.copies);
|
||||
opt.score = score_placement(*moved, c.space_group, merged, d_min, nthreads, &scorers, nullptr,
|
||||
nullptr, &v);
|
||||
}
|
||||
logger.Info("Model prior: {} moved by {:.2f} {:.2f} {:.2f}: clash {:.1f}%, R-free {:.3f}, R on the "
|
||||
"reflections tNCS weakens {:.3f} against {:.3f}", c.space_group.xhm(), shift.x, shift.y,
|
||||
shift.z, 100 * opt.clash, opt.score.r_free, opt.score.r_tncs_weak,
|
||||
options[0].score.r_tncs_weak);
|
||||
options.push_back(std::move(opt));
|
||||
}
|
||||
std::vector<const Option *> packing;
|
||||
for (const Option &p : options)
|
||||
if (p.clash <= CLASH_MAX && std::isfinite(p.score.r_tncs_weak))
|
||||
packing.push_back(&p);
|
||||
std::stable_sort(packing.begin(), packing.end(), [](const Option *a, const Option *b) {
|
||||
return a->score.r_tncs_weak < b->score.r_tncs_weak;
|
||||
});
|
||||
if (packing.empty() || packing[0] == &options[0])
|
||||
return packing.size() > 1 && packing[1]->score.r_tncs_weak - packing[0]->score.r_tncs_weak < TNCS_SHIFT_MARGIN;
|
||||
const Option &b = *packing[0];
|
||||
if (options[0].score.r_tncs_weak - b.score.r_tncs_weak < TNCS_SHIFT_MARGIN
|
||||
|| (packing.size() > 1 && packing[1]->score.r_tncs_weak - b.score.r_tncs_weak < TNCS_SHIFT_MARGIN))
|
||||
return true;
|
||||
logger.Info("Model prior: {} taken moved by {:.2f} {:.2f} {:.2f}", c.space_group.xhm(), b.shift.x,
|
||||
b.shift.y, b.shift.z);
|
||||
c.placed = b.placed;
|
||||
c.clash_fraction = b.clash;
|
||||
c.r_work = b.score.r_work;
|
||||
c.r_free = b.score.r_free;
|
||||
c.k_sol = b.score.k_sol;
|
||||
c.b_sol = b.score.b_sol;
|
||||
return false;
|
||||
};
|
||||
int best = -1;
|
||||
while (true) {
|
||||
best = -1;
|
||||
@@ -2642,6 +2773,15 @@ MolecularReplacementResult search_model(const SearchModel &model, const std::vec
|
||||
if (best < 0)
|
||||
return -1;
|
||||
MRCandidate &c = result.candidates[best];
|
||||
if (half && !shift_checked[best]) {
|
||||
shift_checked[best] = 1;
|
||||
if (check_shifts(best)) {
|
||||
result.decision = fmt::format("UNDECIDED: {}'s placement and its move by half the tNCS vector fit "
|
||||
"alike", c.space_group.xhm());
|
||||
return -2;
|
||||
}
|
||||
continue; // its R-free may have changed
|
||||
}
|
||||
if (std::isnan(nulls[best])) {
|
||||
const auto t_null = std::chrono::steady_clock::now();
|
||||
std::vector<int> todo{best};
|
||||
@@ -2665,6 +2805,18 @@ MolecularReplacementResult search_model(const SearchModel &model, const std::vec
|
||||
}
|
||||
for (size_t i = 0; i < result.candidates.size(); i++) {
|
||||
const MRCandidate &c = result.candidates[i];
|
||||
// With tNCS, the group the merge was made in, searched to fewer copies than the chosen one - whether it
|
||||
// packs or not: its first copy alone can sit where half the tNCS vector puts it - is not ruled out by
|
||||
// any placement of the chosen one, which may sit there itself. (Its enantiomorph is the hand check's.)
|
||||
if (result.tncs.present && static_cast<int>(i) != best && c.merge == merge
|
||||
&& c.space_group.xhm() == merges[merge].space_group.xhm()
|
||||
&& result.candidates[best].space_group.xhm() != c.space_group.xhm()
|
||||
&& !enantiomorphs(c.space_group, result.candidates[best].space_group)
|
||||
&& c.copies < result.candidates[best].copies) {
|
||||
result.decision = fmt::format("UNDECIDED: {} placed fewer copies than {} beside tNCS",
|
||||
c.space_group.xhm(), result.candidates[best].space_group.xhm());
|
||||
return -2;
|
||||
}
|
||||
if (static_cast<int>(i) == best || c.merge != merge || c.clash_fraction > CLASH_MAX)
|
||||
continue;
|
||||
// With tNCS one copy alone fits no better than at random, so a group whose search stopped at
|
||||
|
||||
Reference in New Issue
Block a user