Refinement: B restraints in proportion to B (Kullback-Leibler form) with contacts, an own ADP weight, overall B into the atoms

- B similarity: E = (B_i - B_j)^2 / (kappa^2 B_i B_j), the symmetric Kullback-Leibler divergence of two
  isotropic Gaussians (Murshudov et al. 2011), so sigma = kappa sqrt(B_i B_j) instead of a fixed 1.5-4.5 A^2.
  kappa 0.1 / 0.133 / 0.2 / 0.3 for main-chain bonded / angle-related and side-chain pairs (REFMAC's logs list
  sigma ~0.1 B for main-chain bonded pairs at B 11-140; the ratios are those of the old sigmas, which these
  equal at B = 15). New class: atoms within 4 A that no bond or angle relates, waters and symmetry mates
  included, kappa 0.5 (BuildBContacts, sharing the neighbour search with BuildRepulsions). CPU and GPU.
- The B factors' X-ray weight is balanced on its own (BalancedADPWeight: B-restraint over X-ray B-gradient on
  the model with B shaken by 20%) instead of being a multiple of the positional weight; the ladder tries B held
  and refined at that weight, the trajectory's B steps divide and multiply it by four, positional steps and the
  final ladder leave it.
- Before each B-refining macro-cycle the overall isotropic B of the scale moves into the atoms and the
  solvent's B (F_model unchanged), so the two do not drift against each other.
Tried and not kept: xyz and B as one L-BFGS vector (the B block scaled by its curvature, the positions by the
median X-ray curvature): no better than alternation with a memory per block on 12 starts (mean R-free 0.265
vs 0.262), worse on 7arr/7d98; a refit before each B step: no gain for +20-40% time.

Refinement battery MD starts, median R-free by the REFMAC 0-cycle scorer, previous commit -> this (REFMAC to
convergence): 5f6m x8 0.167 -> 0.167 (0.168), 8dqb x8 0.291 -> 0.294 (0.284), 7arr x4 0.270 -> 0.278 (0.311),
11if x4 0.245 -> 0.244 (0.251), 5reo x4 0.312 -> 0.258 (0.261: the ladder no longer keeps B held where B is the
model's largest error), 7d98 x4 0.342 -> 0.335 (0.361). Deposited models: unchanged (B stays held there).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
(cherry picked from commit ed292f35b482b696d81f71f1d97e0016ca05b144)
This commit is contained in:
leonarski_f committed 2026-10-26 01:38:24 +01:00
1 parent 6369a263b1
commit bda90dbdd5
7 files changed
+242 -130

No files matched your search

@@ -5,6 +5,7 @@
#include <algorithm>
#include <cmath>
#include <functional>
#include <set>
#include <unordered_map>
@@ -25,9 +26,14 @@ constexpr float CHIRAL_SIGMA = 0.2f; // A^3
constexpr float REPULSION_SIGMA = 0.2f; // A
constexpr float HBOND_CONTACT = 2.6f; // A
constexpr float METAL_CONTACT = 1.9f; // A, to N, O or S
// B similarity: main chain and the rest, for bonded pairs and pairs bonded to a common atom.
constexpr float B_SIGMA_MAIN_BOND = 1.5f, B_SIGMA_MAIN_ANGLE = 2.0f;
constexpr float B_SIGMA_SIDE_BOND = 3.0f, B_SIGMA_SIDE_ANGLE = 4.5f;
// B similarity (BSimilarityTerm's kappa: sigma = kappa sqrt(B_i B_j)): main chain and the rest, for bonded pairs and
// pairs bonded to a common atom - REFMAC's logs list sigma near 0.1 B for main-chain bonded pairs from B 11 to 140
// A^2, the ratios between the classes are those of the fixed sigmas this replaced (1.5, 2.0, 3.0, 4.5 A^2, i.e. these
// at B = 15) - and atoms in contact, waters among them, that no bond or angle relates.
constexpr float B_KAPPA_MAIN_BOND = 0.1f, B_KAPPA_MAIN_ANGLE = 0.133f;
constexpr float B_KAPPA_SIDE_BOND = 0.2f, B_KAPPA_SIDE_ANGLE = 0.3f;
constexpr float B_KAPPA_CONTACT = 0.5f;
constexpr double B_CONTACT_DISTANCE = 4.0; // A
bool IsMainChain(const gemmi::Atom &a) {
static const std::set<std::string> names = {"N", "CA", "C", "O", "OXT", "P", "OP1", "OP2", "O5'", "C5'",
@@ -303,11 +309,11 @@ double EvaluateRestraints(const RestraintSet &set, const std::vector<std::array<
break;
}
case RestraintKind::BSimilarity: {
const double rr = (xyzb[r.atom[0]][3] - xyzb[r.atom[1]][3]) / sigma;
e = rr * rr;
double gi = 0, gj = 0;
e = refine_math::BSimilarityTerm(xyzb[r.atom[0]][3], xyzb[r.atom[1]][3], sigma, gi, gj);
if (grad != nullptr) {
(*grad)[r.atom[0]][3] += 2 * rr / sigma;
(*grad)[r.atom[1]][3] -= 2 * rr / sigma;
(*grad)[r.atom[0]][3] += gi;
(*grad)[r.atom[1]][3] += gj;
}
break;
}
@@ -328,8 +334,10 @@ std::vector<double> BRestraintCurvature(const RestraintSet &set, const std::vect
std::vector<double> h(xyzb.size(), 0.0);
for (const RestraintRecord &r : set.records)
if (r.kind == static_cast<int>(RestraintKind::BSimilarity)) {
h[r.atom[0]] += 2.0 / (r.sigma * r.sigma);
h[r.atom[1]] += 2.0 / (r.sigma * r.sigma);
// d2E/dB_i^2 of BSimilarityTerm = 2 B_j / (kappa^2 B_i^3)
const double bi = xyzb[r.atom[0]][3], bj = xyzb[r.atom[1]][3], k2 = double(r.sigma) * r.sigma;
h[r.atom[0]] += 2.0 * bj / (k2 * bi * bi * bi);
h[r.atom[1]] += 2.0 * bi / (k2 * bj * bj * bj);
}
return h;
}
@@ -454,7 +462,7 @@ RefinementTopology BuildRefinementTopology(gemmi::Structure &st, bool hydrogens)
rb.atom[0] = i;
rb.atom[1] = j;
const bool main = IsMainChain(*b.atoms[0]) && IsMainChain(*b.atoms[1]);
rb.sigma = main ? B_SIGMA_MAIN_BOND : B_SIGMA_SIDE_BOND;
rb.sigma = main ? B_KAPPA_MAIN_BOND : B_KAPPA_SIDE_BOND;
out.adp.Add(rb, 2);
}
for (const gemmi::Topo::Angle &an : topo->angles) {
@@ -479,7 +487,7 @@ RefinementTopology BuildRefinementTopology(gemmi::Structure &st, bool hydrogens)
rb.atom[0] = i;
rb.atom[1] = k;
const bool main = IsMainChain(*an.atoms[0]) && IsMainChain(*an.atoms[2]);
rb.sigma = main ? B_SIGMA_MAIN_ANGLE : B_SIGMA_SIDE_ANGLE;
rb.sigma = main ? B_KAPPA_MAIN_ANGLE : B_KAPPA_SIDE_ANGLE;
out.adp.Add(rb, 2);
}
for (const gemmi::Topo::Torsion &t : topo->torsions) {
@@ -557,14 +565,17 @@ RefinementTopology BuildRefinementTopology(gemmi::Structure &st, bool hydrogens)
return out;
}
RestraintSet BuildRepulsions(const RefinementTopology &topo, gemmi::Model &model, const gemmi::UnitCell &cell_in,
const gemmi::SpaceGroup &sg, double margin, size_t nthreads) {
namespace {
// The pairs of atoms of `model`, symmetry mates included, closer than `reach` and not excluded, where at least one of
// the two is refinable, neither is a hydrogen and the two are not alternative conformations of each other: `make`
// turns each into a record (the image of j as x -> rot x + tran, Cartesian, and the distance), or says to leave it out.
// The search runs on `nthreads` threads; the list is in atom order whatever their number.
RestraintSet Contacts(const RefinementTopology &topo, gemmi::Model &model, const gemmi::UnitCell &cell_in,
const gemmi::SpaceGroup &sg, double reach, size_t nthreads,
const std::function<bool(int i, int j, double d, RestraintRecord &r)> &make) {
gemmi::UnitCell cell = cell_in;
cell.set_cell_images_from_spacegroup(&sg);
float max_radius = 0;
for (float r : topo.vdw_radius)
max_radius = std::max(max_radius, r);
const double reach = 2 * max_radius + margin;
gemmi::NeighborSearch ns(model, cell, std::max(reach, 5.0));
ns.populate(false);
@@ -609,29 +620,16 @@ RestraintSet BuildRepulsions(const RefinementTopology &topo, gemmi::Model &model
const gemmi::Mat33 rot = cell.orth.mat.multiply(ft.mat).multiply(cell.frac.mat);
const gemmi::Vec3 tran = cell.orth.mat.multiply(ft.vec + shift);
const gemmi::Position image(rot.multiply(pos[j]) + tran);
const double d = pos[i].dist(image);
float d0 = topo.vdw_radius[i] + topo.vdw_radius[j];
if (topo.metal[i] || topo.metal[j]) {
const gemmi::El other = topo.element[topo.metal[i] ? j : static_cast<int>(i)];
if (other == gemmi::El::N || other == gemmi::El::O || other == gemmi::El::S)
d0 = METAL_CONTACT;
} else if (topo.hbond[i] && topo.hbond[j])
d0 = HBOND_CONTACT;
if (!(d < d0 + margin))
return;
RestraintRecord r;
r.kind = static_cast<int>(RestraintKind::Repulsion);
r.atom[0] = static_cast<int>(i);
r.atom[1] = j;
r.ideal = d0;
r.sigma = REPULSION_SIGMA;
for (int a = 0; a < 3; a++) {
for (int b = 0; b < 3; b++)
r.rot[3 * a + b] = static_cast<float>(rot[a][b]);
r.tran[a] = static_cast<float>(tran.at(a));
}
found[block].push_back(r);
if (make(static_cast<int>(i), j, pos[i].dist(image), r))
found[block].push_back(r);
});
}
});
@@ -643,6 +641,42 @@ RestraintSet BuildRepulsions(const RefinementTopology &topo, gemmi::Model &model
return out;
}
} // namespace
RestraintSet BuildRepulsions(const RefinementTopology &topo, gemmi::Model &model, const gemmi::UnitCell &cell,
const gemmi::SpaceGroup &sg, double margin, size_t nthreads) {
float max_radius = 0;
for (float r : topo.vdw_radius)
max_radius = std::max(max_radius, r);
return Contacts(topo, model, cell, sg, 2 * max_radius + margin, nthreads,
[&](int i, int j, double d, RestraintRecord &r) {
float d0 = topo.vdw_radius[i] + topo.vdw_radius[j];
if (topo.metal[i] || topo.metal[j]) {
const gemmi::El other = topo.element[topo.metal[i] ? j : i];
if (other == gemmi::El::N || other == gemmi::El::O || other == gemmi::El::S)
d0 = METAL_CONTACT;
} else if (topo.hbond[i] && topo.hbond[j])
d0 = HBOND_CONTACT;
if (!(d < d0 + margin))
return false;
r.kind = static_cast<int>(RestraintKind::Repulsion);
r.ideal = d0;
r.sigma = REPULSION_SIGMA;
return true;
});
}
RestraintSet BuildBContacts(const RefinementTopology &topo, gemmi::Model &model, const gemmi::UnitCell &cell,
const gemmi::SpaceGroup &sg, size_t nthreads) {
return Contacts(topo, model, cell, sg, B_CONTACT_DISTANCE, nthreads, [&](int i, int j, double d, RestraintRecord &r) {
if (i == j || !(d < B_CONTACT_DISTANCE))
return false;
r.kind = static_cast<int>(RestraintKind::BSimilarity);
r.sigma = B_KAPPA_CONTACT;
return true;
});
}
void PlaceRidingHydrogens(RefinementTopology &topo) {
gemmi::place_hydrogens_on_all_atoms(*topo.topo);
}