Build Packages / Create release (push) Successful in 26s
Build Packages / Build documentation (push) Successful in 1m43s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m7s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 11m48s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 7m22s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 12m24s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m30s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 25m29s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 29m41s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 21m44s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 24m41s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 26m9s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 26m3s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 28m28s
Build Packages / Generate python client (push) Successful in 27s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 31m16s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 20m44s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 26m27s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 22m52s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 27m44s
Build Packages / Unit tests (push) Successful in 1h46m55s
Build Packages / Validation Battery (push) Successful in 52m50s
* Rugnux: Several rotation sweeps of one crystal are merged as one dataset (`rugnux a_master.h5 b_master.h5`), on a common indexing and cell, with a non-isomorphous sweep left out and reported; `--sweeps separate` processes each sweep on its own. Bruker `.sfrm` frames and imgCIF files with a full goniometer axis chain are read. * Rugnux: More robust rotation processing - a retry with a more sensitive spot finder when no lattice is found, a self-consistent detector distance, long axes and doubled cells tested as hypotheses, a `-C` supercell multiple stepped down, and each ice ring merged, down-weighted or rejected on its own. * Rugnux: Serial stills are merged with a partiality-aware error model, reported as `ERROR_MODEL_A/B/C` with no ISa or R_merge/R_meas/R_pim; runs without `-S` have an overall intensity scale about 0.86-0.88 times that of earlier versions, with unchanged statistics. A second crystal form is left out of the merge and reported, the indexing ambiguity is resolved more reliably, and `--model` no longer influences processing. * Rugnux: The reported CC1/2 is the standard unweighted one, which can be lower than before on sweeps with weak frames; ISa comes with a 95% interval (`ISA_LOW`/`ISA_HIGH`). * Rugnux: Model refinement adds anisotropic/TLS, local NCS and twin refinement where R-free supports them; building waters and relabelling ions is now off by default (`--refine-solvent` turns it on). * NXmx: A detector of several modules is written as `module_0` ... `module_N` only, so DIALS reads each module as its own panel. * Other: JPEG previews use a bundled encoder (libjpeg-turbo no longer needed), Rugnux prints how to cite it, and the release archives contain the HTML documentation. Reviewed-on: #92 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
320 lines
14 KiB
C++
320 lines
14 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "RefinementADP.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <limits>
|
|
|
|
#include <Eigen/Dense>
|
|
|
|
namespace {
|
|
|
|
constexpr double DEG = 3.14159265358979323846 / 180.0;
|
|
|
|
} // namespace
|
|
|
|
gemmi::SMat33<double> TlsBody::U(const gemmi::Position &pos) const {
|
|
const double x = pos.x - origin.x, y = pos.y - origin.y, z = pos.z - origin.z;
|
|
const gemmi::Mat33 a(0, z, -y, -z, 0, x, y, -x, 0);
|
|
const gemmi::Mat33 ala = a.multiply(L.as_mat33()).multiply(a.transpose());
|
|
const gemmi::Mat33 as = a.multiply(S);
|
|
gemmi::SMat33<double> u = T;
|
|
u.u11 += ala[0][0] + 2 * as[0][0];
|
|
u.u22 += ala[1][1] + 2 * as[1][1];
|
|
u.u33 += ala[2][2] + 2 * as[2][2];
|
|
u.u12 += ala[0][1] + as[0][1] + as[1][0];
|
|
u.u13 += ala[0][2] + as[0][2] + as[2][0];
|
|
u.u23 += ala[1][2] + as[1][2] + as[2][1];
|
|
return u;
|
|
}
|
|
|
|
std::array<double, TlsBody::PARAMETERS> TlsBody::Get() const {
|
|
return {T.u11, T.u22, T.u33, T.u12, T.u13, T.u23, L.u11, L.u22, L.u33, L.u12, L.u13, L.u23,
|
|
S[0][0], S[1][1], S[0][1], S[0][2], S[1][0], S[1][2], S[2][0], S[2][1]};
|
|
}
|
|
|
|
void TlsBody::Set(const std::array<double, PARAMETERS> &p) {
|
|
T = {p[0], p[1], p[2], p[3], p[4], p[5]};
|
|
L = {p[6], p[7], p[8], p[9], p[10], p[11]};
|
|
S = gemmi::Mat33(p[12], p[14], p[15], p[16], p[13], p[17], p[18], p[19], -(p[12] + p[13]));
|
|
}
|
|
|
|
std::array<std::array<double, 6>, TlsBody::PARAMETERS> TlsBody::Derivatives(const gemmi::Position &pos) const {
|
|
std::array<std::array<double, 6>, PARAMETERS> d{};
|
|
TlsBody unit;
|
|
unit.origin = origin;
|
|
for (int k = 0; k < PARAMETERS; k++) {
|
|
std::array<double, PARAMETERS> p{};
|
|
p[k] = 1;
|
|
unit.Set(p);
|
|
const gemmi::SMat33<double> u = unit.U(pos);
|
|
d[k] = {u.u11, u.u22, u.u33, u.u12, u.u13, u.u23};
|
|
}
|
|
return d;
|
|
}
|
|
|
|
std::array<double, TlsBody::PARAMETERS> TlsBody::Gradient(const std::vector<gemmi::Atom *> &atom_list,
|
|
const std::vector<std::array<double, 6>> &grad_u) const {
|
|
gemmi::Mat33 mt(0), ml(0), ms(0);
|
|
for (int i : atoms) {
|
|
const std::array<double, 6> &g = grad_u[i];
|
|
const gemmi::Mat33 gm(g[0], g[3] / 2, g[4] / 2, g[3] / 2, g[1], g[5] / 2, g[4] / 2, g[5] / 2, g[2]);
|
|
const gemmi::Position &pos = atom_list[i]->pos;
|
|
const double x = pos.x - origin.x, y = pos.y - origin.y, z = pos.z - origin.z;
|
|
const gemmi::Mat33 a(0, z, -y, -z, 0, x, y, -x, 0);
|
|
const gemmi::Mat33 atg = a.transpose().multiply(gm);
|
|
const gemmi::Mat33 atga = atg.multiply(a);
|
|
for (int u = 0; u < 3; u++)
|
|
for (int v = 0; v < 3; v++) {
|
|
mt[u][v] += gm[u][v];
|
|
ml[u][v] += atga[u][v];
|
|
ms[u][v] += 2 * atg[u][v];
|
|
}
|
|
}
|
|
// symmetric T and L: an off-diagonal parameter takes both of its elements; S33 = -(S11 + S22)
|
|
return {mt[0][0], mt[1][1], mt[2][2], 2 * mt[0][1], 2 * mt[0][2], 2 * mt[1][2],
|
|
ml[0][0], ml[1][1], ml[2][2], 2 * ml[0][1], 2 * ml[0][2], 2 * ml[1][2],
|
|
ms[0][0] - ms[2][2], ms[1][1] - ms[2][2], ms[0][1], ms[0][2], ms[1][0], ms[1][2], ms[2][0], ms[2][1]};
|
|
}
|
|
|
|
std::vector<TlsBody> TlsBodiesFromModel(const gemmi::Model &model, const std::vector<gemmi::TlsGroup> &groups) {
|
|
std::vector<TlsBody> bodies;
|
|
for (const gemmi::TlsGroup &g : groups) {
|
|
TlsBody b;
|
|
b.id = g.id;
|
|
b.origin = g.origin;
|
|
b.T = g.T;
|
|
b.L = g.L.scaled(DEG * DEG);
|
|
for (int i = 0; i < 3; i++)
|
|
for (int j = 0; j < 3; j++)
|
|
b.S[i][j] = g.S[i][j] * DEG;
|
|
// the trace of S does not enter U; S33 is taken as -(S11 + S22)
|
|
const double t = (b.S[0][0] + b.S[1][1] + b.S[2][2]) / 3.0;
|
|
for (int i = 0; i < 3; i++)
|
|
b.S[i][i] -= t;
|
|
bool finite = std::isfinite(b.origin.x) && std::isfinite(b.origin.y) && std::isfinite(b.origin.z);
|
|
for (double v : b.Get())
|
|
finite = finite && std::isfinite(v);
|
|
if (finite)
|
|
bodies.push_back(b);
|
|
}
|
|
// the atoms of each group's residues, an atom to the first group that names it
|
|
int i = 0;
|
|
for (const gemmi::Chain &ch : model.chains)
|
|
for (const gemmi::Residue &r : ch.residues) {
|
|
int body = -1;
|
|
if (!r.is_water())
|
|
for (size_t k = 0; k < groups.size() && body < 0; k++) {
|
|
for (const gemmi::TlsGroup::Selection &sel : groups[k].selections)
|
|
if (sel.chain == ch.name && !(r.seqid < sel.res_begin) && !(sel.res_end < r.seqid))
|
|
body = static_cast<int>(k);
|
|
}
|
|
// the group index among those kept
|
|
int kept = -1;
|
|
if (body >= 0)
|
|
for (size_t k = 0; k < bodies.size(); k++)
|
|
if (bodies[k].id == groups[body].id)
|
|
kept = static_cast<int>(k);
|
|
for (size_t a = 0; a < r.atoms.size(); a++, i++)
|
|
if (kept >= 0)
|
|
bodies[kept].atoms.push_back(i);
|
|
}
|
|
std::erase_if(bodies, [](const TlsBody &b) { return b.atoms.empty(); });
|
|
return bodies;
|
|
}
|
|
|
|
std::vector<gemmi::TlsGroup> TlsBodiesToGroups(const gemmi::Model &model, const std::vector<TlsBody> &bodies) {
|
|
// per atom its body, per residue the body of its first atom in one
|
|
std::vector<int> atom_body;
|
|
for (const gemmi::Chain &ch : model.chains)
|
|
for (const gemmi::Residue &r : ch.residues)
|
|
atom_body.insert(atom_body.end(), r.atoms.size(), -1);
|
|
for (size_t b = 0; b < bodies.size(); b++)
|
|
for (int a : bodies[b].atoms)
|
|
atom_body[a] = static_cast<int>(b);
|
|
std::vector<gemmi::TlsGroup> groups(bodies.size());
|
|
for (size_t b = 0; b < bodies.size(); b++) {
|
|
gemmi::TlsGroup &g = groups[b];
|
|
g.id = std::to_string(b + 1);
|
|
g.num_id = static_cast<short>(b + 1);
|
|
g.origin = bodies[b].origin;
|
|
g.T = bodies[b].T;
|
|
g.L = bodies[b].L.scaled(1.0 / (DEG * DEG));
|
|
for (int i = 0; i < 3; i++)
|
|
for (int j = 0; j < 3; j++)
|
|
g.S[i][j] = bodies[b].S[i][j] / DEG;
|
|
}
|
|
size_t i = 0;
|
|
for (const gemmi::Chain &ch : model.chains) {
|
|
int open = -1; // the body whose run of residues is open in this chain
|
|
for (const gemmi::Residue &r : ch.residues) {
|
|
int body = -1;
|
|
for (size_t a = 0; a < r.atoms.size(); a++)
|
|
if (atom_body[i + a] >= 0 && body < 0)
|
|
body = atom_body[i + a];
|
|
i += r.atoms.size();
|
|
if (body >= 0 && body == open)
|
|
groups[body].selections.back().res_end = r.seqid;
|
|
else if (body >= 0) {
|
|
groups[body].selections.push_back({ch.name, r.seqid, r.seqid, ""});
|
|
}
|
|
open = body;
|
|
}
|
|
}
|
|
return groups;
|
|
}
|
|
|
|
URestraints MakeURestraints(const std::vector<std::pair<int, int>> &b_similarity_pairs, const std::vector<char> &refines_u) {
|
|
URestraints r;
|
|
std::vector<char> paired(refines_u.size(), 0);
|
|
for (const auto &[i, j] : b_similarity_pairs)
|
|
if (refines_u[i] && refines_u[j]) {
|
|
r.pairs.emplace_back(i, j);
|
|
paired[i] = paired[j] = 1;
|
|
}
|
|
for (size_t i = 0; i < refines_u.size(); i++)
|
|
if (refines_u[i] && !paired[i])
|
|
r.isotropic.push_back(static_cast<int>(i));
|
|
return r;
|
|
}
|
|
|
|
double EvaluateURestraints(const URestraints &r, const std::vector<gemmi::Position> &pos,
|
|
const std::vector<std::array<double, 6>> &u, std::vector<std::array<double, 6>> *grad) {
|
|
// a symmetric matrix as its six elements: the off-diagonal ones count twice in a Frobenius norm, and a derivative
|
|
// with respect to U_ab (a != b) is the sum of the matrix elements ab and ba
|
|
double e = 0;
|
|
const double wr = 1.0 / (URestraints::SIGMA_RIGID * URestraints::SIGMA_RIGID);
|
|
const double ws = 1.0 / (URestraints::SIGMA_SIMILAR * URestraints::SIGMA_SIMILAR);
|
|
const double wi = 1.0 / (URestraints::SIGMA_ISOTROPIC * URestraints::SIGMA_ISOTROPIC);
|
|
for (const auto &[i, j] : r.pairs) {
|
|
std::array<double, 6> d;
|
|
for (int c = 0; c < 6; c++)
|
|
d[c] = u[i][c] - u[j][c];
|
|
gemmi::Vec3 n = pos[j] - pos[i];
|
|
const double len = n.length();
|
|
std::array<double, 6> g{};
|
|
if (len > 0) {
|
|
n = n / len;
|
|
// v = (U_i - U_j) n
|
|
const double v[3] = {d[0] * n.x + d[3] * n.y + d[4] * n.z, d[3] * n.x + d[1] * n.y + d[5] * n.z,
|
|
d[4] * n.x + d[5] * n.y + d[2] * n.z};
|
|
const double nn[3] = {n.x, n.y, n.z};
|
|
e += wr * (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
|
g[0] += 2 * wr * v[0] * nn[0];
|
|
g[1] += 2 * wr * v[1] * nn[1];
|
|
g[2] += 2 * wr * v[2] * nn[2];
|
|
g[3] += 2 * wr * (v[0] * nn[1] + v[1] * nn[0]);
|
|
g[4] += 2 * wr * (v[0] * nn[2] + v[2] * nn[0]);
|
|
g[5] += 2 * wr * (v[1] * nn[2] + v[2] * nn[1]);
|
|
}
|
|
e += ws * (d[0] * d[0] + d[1] * d[1] + d[2] * d[2] + 2 * (d[3] * d[3] + d[4] * d[4] + d[5] * d[5]));
|
|
for (int c = 0; c < 3; c++)
|
|
g[c] += 2 * ws * d[c];
|
|
for (int c = 3; c < 6; c++)
|
|
g[c] += 4 * ws * d[c];
|
|
if (grad != nullptr)
|
|
for (int c = 0; c < 6; c++) {
|
|
(*grad)[i][c] += g[c];
|
|
(*grad)[j][c] -= g[c];
|
|
}
|
|
}
|
|
for (int i : r.isotropic) {
|
|
const double t = (u[i][0] + u[i][1] + u[i][2]) / 3.0;
|
|
const double d[6] = {u[i][0] - t, u[i][1] - t, u[i][2] - t, u[i][3], u[i][4], u[i][5]};
|
|
e += wi * (d[0] * d[0] + d[1] * d[1] + d[2] * d[2] + 2 * (d[3] * d[3] + d[4] * d[4] + d[5] * d[5]));
|
|
if (grad != nullptr) {
|
|
// d/dU_aa of the traceless part's norm: 2 d_aa (the trace's own derivative sums to zero against d)
|
|
for (int c = 0; c < 3; c++)
|
|
(*grad)[i][c] += 2 * wi * d[c];
|
|
for (int c = 3; c < 6; c++)
|
|
(*grad)[i][c] += 4 * wi * d[c];
|
|
}
|
|
}
|
|
return e;
|
|
}
|
|
|
|
std::vector<int> TlsSegments(const std::vector<std::vector<int>> &residue_atoms, const std::vector<gemmi::Position> &pos,
|
|
const std::vector<double> &b, int k, int min_residues) {
|
|
using Mat = Eigen::Matrix<double, 10, 10>;
|
|
using Vec = Eigen::Matrix<double, 10, 1>;
|
|
const int n = static_cast<int>(residue_atoms.size());
|
|
k = std::min(k, n / std::max(min_residues, 1));
|
|
if (k <= 1)
|
|
return {0};
|
|
// The chain in blocks of consecutive residues (cuts between blocks only), so that a long chain stays a few hundred
|
|
// blocks: the dynamic programming is quadratic in them.
|
|
const int per_block = std::max(1, (n + 299) / 300);
|
|
const int min_blocks = std::max(1, (min_residues + per_block - 1) / per_block);
|
|
std::vector<int> block_first;
|
|
for (int r = 0; r < n; r += per_block)
|
|
block_first.push_back(r);
|
|
const int nb = static_cast<int>(block_first.size());
|
|
k = std::min(k, nb / min_blocks);
|
|
if (k <= 1)
|
|
return {0};
|
|
// the normal equations of each block, about the chain's centroid in units of 10 A (for their conditioning)
|
|
gemmi::Position c;
|
|
int count = 0;
|
|
for (const std::vector<int> &ra : residue_atoms)
|
|
for (int a : ra) {
|
|
c += pos[a];
|
|
count++;
|
|
}
|
|
if (count == 0)
|
|
return {0};
|
|
c /= count;
|
|
std::vector<Mat> ata(nb + 1, Mat::Zero());
|
|
std::vector<Vec> atb(nb + 1, Vec::Zero());
|
|
std::vector<double> bb(nb + 1, 0.0);
|
|
for (int q = 0; q < nb; q++) {
|
|
ata[q + 1] = ata[q];
|
|
atb[q + 1] = atb[q];
|
|
bb[q + 1] = bb[q];
|
|
const int last = q + 1 < nb ? block_first[q + 1] : n;
|
|
for (int r = block_first[q]; r < last; r++)
|
|
for (int a : residue_atoms[r]) {
|
|
const double x = (pos[a].x - c.x) / 10, y = (pos[a].y - c.y) / 10, z = (pos[a].z - c.z) / 10;
|
|
Vec f;
|
|
f << 1, x, y, z, x * x, y * y, z * z, x * y, x * z, y * z;
|
|
ata[q + 1] += f * f.transpose();
|
|
atb[q + 1] += b[a] * f;
|
|
bb[q + 1] += b[a] * b[a];
|
|
}
|
|
}
|
|
// the residual of the fit of blocks [i, j)
|
|
auto cost = [&](int i, int j) {
|
|
const Mat m = ata[j] - ata[i] + 1e-6 * Mat::Identity();
|
|
const Vec v = atb[j] - atb[i];
|
|
const Vec x = m.ldlt().solve(v);
|
|
return std::max(0.0, bb[j] - bb[i] - v.dot(x));
|
|
};
|
|
const double INF = std::numeric_limits<double>::infinity();
|
|
// best[s][j]: blocks [0, j) in s + 1 segments; from[s][j]: where the last of them starts
|
|
std::vector<std::vector<double>> best(k, std::vector<double>(nb + 1, INF));
|
|
std::vector<std::vector<int>> from(k, std::vector<int>(nb + 1, 0));
|
|
std::vector<std::vector<double>> seg(nb + 1, std::vector<double>(nb + 1, INF));
|
|
for (int i = 0; i < nb; i++)
|
|
for (int j = i + min_blocks; j <= nb; j++)
|
|
seg[i][j] = cost(i, j);
|
|
for (int j = min_blocks; j <= nb; j++)
|
|
best[0][j] = seg[0][j];
|
|
for (int s = 1; s < k; s++)
|
|
for (int j = (s + 1) * min_blocks; j <= nb; j++)
|
|
for (int i = s * min_blocks; i <= j - min_blocks; i++)
|
|
if (best[s - 1][i] + seg[i][j] < best[s][j]) {
|
|
best[s][j] = best[s - 1][i] + seg[i][j];
|
|
from[s][j] = i;
|
|
}
|
|
std::vector<int> starts;
|
|
int j = nb;
|
|
for (int s = k - 1; s > 0; s--) {
|
|
j = from[s][j];
|
|
starts.push_back(block_first[j]);
|
|
}
|
|
starts.push_back(0);
|
|
std::reverse(starts.begin(), starts.end());
|
|
return starts;
|
|
}
|