Files
Jungfraujoch/gemmi_gph/gemmi/cellred.hpp
T
leonarski_f fc68a9baed
Build Packages / Unit tests (push) Skipped
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 8m34s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 10m0s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 10m23s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 10m23s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 11m16s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 11m49s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 8m32s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 9m15s
Build Packages / XDS test (durin plugin) (push) Successful in 7m16s
Build Packages / Generate python client (push) Successful in 16s
Build Packages / build:rpm (rocky9) (push) Successful in 10m12s
Build Packages / Create release (push) Skipped
Build Packages / Build documentation (push) Successful in 47s
Build Packages / DIALS test (push) Successful in 10m18s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 5m46s
Build Packages / build:rpm (rocky8) (push) Successful in 1h41m2s
Build Packages / XDS test (neggia plugin) (push) Successful in 1h59m18s
v1.0.0-rc.146 (#56)
This is an UNSTABLE release. The release has significant modifications for data processing - in case of troubles go back to 1.0.0-rc.144.

jfjoch_process: Generate a dedicated file (_process.h5), which can be used as a replacement for the _master.h5 file for a reanalyzed dataset.
jfjoch_process: Improve the performance of scaling and merging, implement on the fly scaling.
jfjoch_writer: All final data analysis results are repopulated in the _master.h5 file.
jfjoch_scale: Dedicated tool for rescaling/merging existing data.
jfjoch_viewer: Fix bugs where pixel labels where displayed on a wrong pixel.

WARNING! Scaling and merging are experimental at the moment, and may not provide reasonable results for the time being.

Reviewed-on: #56
2026-05-28 18:48:35 +02:00

407 lines
13 KiB
C++

// Copyright 2021 Global Phasing Ltd.
//
// Unit cell reductions: Buerger, Niggli, Selling-Delaunay.
#ifndef GEMMI_CELLRED_HPP_
#define GEMMI_CELLRED_HPP_
#include <cmath>
#include <array>
#include <memory> // for unique_ptr
#include "math.hpp" // for deg
#include "symmetry.hpp" // for Op
#include "unitcell.hpp" // for UnitCell
namespace gemmi {
struct SellingVector;
// GruberVector contains G6 vector (G for Gruber) and cell reduction algorithms.
// Originally, in B. Gruber, Acta Cryst. A29, 433 (1973), the vector was called
// "characteristic" of a lattice/cell.
// Functions that take epsilon as a parameter use it for comparisons,
// as proposed in Grosse-Kunstleve et al, Acta Cryst. (2004) A60, 1.
struct GruberVector {
// a.a b.b c.c 2b.c 2a.c 2a.b
double A, B, C, xi, eta, zeta; // the 1973 paper uses names A B C ξ η ζ
std::unique_ptr<Op> change_of_basis; // we use only Op::Rot
// m - orthogonalization matrix of a primitive cell
explicit GruberVector(const Mat33& m)
: A(m.column_dot(0,0)),
B(m.column_dot(1,1)),
C(m.column_dot(2,2)),
xi(2 * m.column_dot(1,2)),
eta(2 * m.column_dot(0,2)),
zeta(2 * m.column_dot(0,1)) {}
explicit GruberVector(const std::array<double,6>& g6)
: A(g6[0]), B(g6[1]), C(g6[2]), xi(g6[3]), eta(g6[4]), zeta(g6[5]) {}
GruberVector(const UnitCell& u, char centring, bool track_change_of_basis=false)
: GruberVector(u.primitive_orth_matrix(centring)) {
if (track_change_of_basis)
set_change_of_basis(Op{centred_to_primitive(centring), {0,0,0}, 'x'});
}
GruberVector(const UnitCell& u, const SpaceGroup* sg, bool track_change_of_basis=false)
: GruberVector(u, sg ? sg->centring_type() : 'P', track_change_of_basis) {}
void set_change_of_basis(const Op& op) { change_of_basis.reset(new Op(op)); }
std::array<double,6> parameters() const { return {A, B, C, xi, eta, zeta}; }
std::array<double,6> cell_parameters() const {
// inverse of UnitCell::g6()
double a = std::sqrt(A);
double b = std::sqrt(B);
double c = std::sqrt(C);
return {a, b, c,
deg(std::acos(xi/(2*b*c))),
deg(std::acos(eta/(2*a*c))),
deg(std::acos(zeta/(2*a*b)))};
}
UnitCell get_cell() const { return UnitCell(cell_parameters()); }
SellingVector selling() const;
bool is_normalized() const {
// eq(3) from Gruber 1973
return A <= B && B <= C &&
(A != B || std::abs(xi) <= std::abs(eta)) &&
(B != C || std::abs(eta) <= std::abs(zeta)) &&
(xi > 0) == (eta > 0) && (xi > 0) == (zeta > 0);
}
bool is_buerger(double epsilon=1e-9) const {
return is_normalized() &&
// eq (4) from Gruber 1973
std::abs(xi) <= B + epsilon &&
std::abs(eta) <= A + epsilon &&
std::abs(zeta) <= A + epsilon;
}
// Algorithm N from Gruber (1973).
// Returns branch taken in N3.
void normalize(double eps=1e-9) {
auto step_N1 = [&]() {
if (A - B > eps || (A - B >= -eps && std::abs(xi) > std::abs(eta) + eps)) { // N1
std::swap(A, B);
std::swap(xi, eta);
if (change_of_basis)
swap_columns_and_negate(0, 1);
}
};
step_N1();
if (B - C > eps || (B - C >= -eps && std::abs(eta) > std::abs(zeta) + eps)) { // N2
std::swap(B, C);
std::swap(eta, zeta);
if (change_of_basis)
swap_columns_and_negate(1, 2);
// To make it faster, instead of "go to the point N1" we repeat N1 once
// (which is equivalent - three swaps are sufficient to reorder ABC).
step_N1();
}
// N3
// xi * eta * zeta > 0 <=> positive count is 1 or 3 and no zeros
int pos_count = (xi > eps) + (eta > eps) + (zeta > eps);
int nonneg_count = (xi >= -eps) + (eta >= -eps) + (zeta >= -eps);
double sgn = (pos_count == nonneg_count && pos_count % 2 == 1) ? 1 : -1;
if (change_of_basis) {
if (sgn * xi < -eps) negate_column(0);
if (sgn * eta < -eps) negate_column(1);
if (sgn * zeta < -eps) negate_column(2);
if (pos_count != nonneg_count && pos_count % 2 == 1)
negate_column(std::fabs(zeta) <= eps ? 2 :
std::fabs(eta) <= eps ? 1 : 0);
}
xi = std::copysign(xi, sgn);
eta = std::copysign(eta, sgn);
zeta = std::copysign(zeta, sgn);
}
// Algorithm B from Gruber (1973).
// Returns true if no change was needed.
bool buerger_step() {
if (std::abs(xi) > B) { // B2
double j = std::floor(0.5*xi/B + 0.5);
C += j * (j*B - xi);
xi -= 2 * j * B;
eta -= j * zeta;
} else if (std::abs(eta) > A) { // B3
double j = std::floor(0.5*eta/A + 0.5);
C += j * (j*A - eta);
xi -= j * zeta;
eta -= 2 * j * A;
} else if (std::abs(zeta) > A) { // B4
double j = std::floor(0.5*zeta/A + 0.5);
B += j * (j*A - zeta);
xi -= j * eta;
zeta -= 2 * j * A;
} else if (xi + eta + zeta + A + B < 0) { // B5
double j = std::floor(0.5 * (xi + eta) / (A + B + zeta) + 0.5);
C += j * (j * (A + B + zeta) - (xi + eta));
xi -= j * (2*B + zeta);
eta -= j * (2*A + zeta);
} else {
return true;
}
return false;
}
// Returns number of iterations.
int buerger_reduce() {
int n = 0;
double prev_sum = -1;
int stall_count = 0;
for (;;) {
normalize();
// In rare cases numerical errors push the algorithm into infinite loop,
// as described in Grosse-Kunstleve et al, Acta Cryst. (2004) A60, 1.
// Ad-hoc solution: stop if a+b+c is stalled for 5 iterations.
if (++n > 8) { // don't waste time during the first few iterations
double sum = std::sqrt(A) + std::sqrt(B) + std::sqrt(C);
if (std::abs(sum - prev_sum) < sum * 1e-6) {
if (++stall_count == 5)
break;
} else {
stall_count = 0;
}
prev_sum = sum;
}
if (buerger_step())
break;
}
return n;
}
// To be called after normalize() or is_normalized().
// Returns true if it already was Niggli cell.
// Algorithm from Krivy & Gruber, Acta Cryst. (1976) A32, 297.
bool niggli_step(double epsilon=1e-9) {
if (std::abs(xi) > B + epsilon || // step 5. from Krivy-Gruber (1976)
(xi >= B - epsilon && 2 * eta < zeta - epsilon) ||
(xi <= -(B - epsilon) && zeta < -epsilon)) {
double sign_xi = xi >= 0 ? 1 : -1;
C += B - xi * sign_xi;
eta -= zeta * sign_xi;
xi -= 2 * B * sign_xi;
if (change_of_basis)
add_column(1, 2, -int(sign_xi));
} else if (std::abs(eta) > A + epsilon || // step 6.
(eta >= A - epsilon && 2 * xi < zeta - epsilon) ||
(eta <= -(A - epsilon) && zeta < -epsilon)) {
double sign_eta = eta >= 0 ? 1 : -1;
C += A - eta * sign_eta;
xi -= zeta * sign_eta;
eta -= 2 * A * sign_eta;
if (change_of_basis)
add_column(0, 2, -int(sign_eta));
} else if (std::abs(zeta) > A + epsilon || // step 7.
(zeta >= A - epsilon && 2 * xi < eta - epsilon) ||
(zeta <= -(A - epsilon) && eta < -epsilon)) {
double sign_zeta = zeta >= 0 ? 1 : -1;
B += A - zeta * sign_zeta;
xi -= eta * sign_zeta;
zeta -= 2 * A * sign_zeta;
if (change_of_basis)
add_column(0, 1, -int(sign_zeta));
} else if (xi + eta + zeta + A + B < -epsilon || // step 8.
(xi + eta + zeta + A + B <= epsilon && 2 * (A + eta) + zeta > epsilon)) {
C += A + B + xi + eta + zeta;
xi += 2 * B + zeta;
eta += 2 * A + zeta;
if (change_of_basis) {
add_column(0, 2, 1);
add_column(1, 2, 1);
}
} else {
return true;
}
return false;
}
// Returns number of iterations.
int niggli_reduce(double epsilon=1e-9, int iteration_limit=100) {
int n = 0;
for (;;) {
normalize(epsilon);
if (++n == iteration_limit || niggli_step(epsilon))
break;
}
return n;
}
bool is_niggli(double epsilon=1e-9) const {
return is_normalized() && GruberVector(parameters()).niggli_step(epsilon);
}
private:
void swap_columns_and_negate(int i, int j) {
for (auto& r : change_of_basis->rot)
std::swap(r[i], r[j]);
for (auto& r : change_of_basis->rot)
for (auto& v : r)
v = -v;
}
void negate_column(int i) {
for (auto& r : change_of_basis->rot)
r[i] = -r[i];
}
void add_column(int pos, int dest, int sign) {
for (auto& r : change_of_basis->rot)
r[dest] += sign * r[pos];
}
};
// Selling-Delaunay reduction. Based on:
// - chapter "Delaunay reduction and standardization" in
// International Tables for Crystallography vol. A (2016), sec. 3.1.2.3.
// https://onlinelibrary.wiley.com/iucr/itc/Ac/ch3o1v0001/
// - Patterson & Love (1957), Acta Cryst. 10, 111,
// "Remarks on the Delaunay reduction", doi:10.1107/s0365110x57000328
// - Andrews et al (2019), Acta Cryst. A75, 115,
// "Selling reduction versus Niggli reduction for crystallographic lattices".
struct SellingVector {
// b.c a.c a.b a.d b.d c.d
std::array<double,6> s;
explicit SellingVector(const std::array<double,6>& s_) : s(s_) {}
explicit SellingVector(const Mat33& orth) {
Vec3 b[4];
for (int i = 0; i < 3; ++i)
b[i] = orth.column_copy(i);
b[3]= -b[0] - b[1] - b[2];
s[0] = b[1].dot(b[2]);
s[1] = b[0].dot(b[2]);
s[2] = b[0].dot(b[1]);
s[3] = b[0].dot(b[3]);
s[4] = b[1].dot(b[3]);
s[5] = b[2].dot(b[3]);
}
SellingVector(const UnitCell& u, char centring)
: SellingVector(u.primitive_orth_matrix(centring)) {}
SellingVector(const UnitCell& u, const SpaceGroup* sg)
: SellingVector(u, sg ? sg->centring_type() : 'P') {}
// The reduction minimizes the sum b_i^2 which is equal to -2 sum s_i.
double sum_b_squared() const {
return -2 * (s[0] + s[1] + s[2] + s[3] + s[4] + s[5]);
}
bool is_reduced(double eps=1e-9) const {
return std::all_of(s.begin(), s.end(), [eps](double x) { return x <= eps; });
}
bool reduce_step(double eps=1e-9) {
//printf(" s = %g %g %g %g %g %g sum=%g\n",
// s[0], s[1], s[2], s[3], s[4], s[5], sum_b_squared());
const int table[6][5] = {
// When negating s[n] we need to apply operations from table[n]:
// 2 x add, subtract, 2 x swap&add
{2, 4, 3, 1, 5}, // 0
{2, 3, 4, 0, 5}, // 1
{1, 3, 5, 0, 4}, // 2
{1, 2, 0, 4, 5}, // 3
{0, 2, 1, 3, 5}, // 4
{0, 1, 2, 3, 4}, // 5
};
double max_s = eps;
int max_s_pos = -1;
for (int i = 0; i < 6; ++i)
if (s[i] > max_s) {
max_s = s[i];
max_s_pos = i;
}
if (max_s_pos < 0)
return false;
const int (&indices)[5] = table[max_s_pos];
s[max_s_pos] = -max_s;
s[indices[0]] += max_s;
s[indices[1]] += max_s;
s[indices[2]] -= max_s;
std::swap(s[indices[3]], s[indices[4]]);
s[indices[3]] += max_s;
s[indices[4]] += max_s;
//printf(" s[%d]=%g sum: %g\n", max_s_pos, max_s, sum_b_squared());
return true;
}
// Returns number of iterations.
int reduce(double eps=1e-9, int iteration_limit=100) {
int n = 0;
while (++n != iteration_limit)
if (!reduce_step(eps))
break;
return n;
}
std::array<double,6> g6_parameters() const {
return {-s[1]-s[2]-s[3], -s[0]-s[2]-s[4], -s[0]-s[1]-s[5], 2*s[0], 2*s[1], 2*s[2]};
}
GruberVector gruber() const { return GruberVector(g6_parameters()); }
// Swap values to make a <= b <= c <= d
void sort(double eps=1e-9) {
double abcd_sq_neg[4] = {
// -a^2, -b^2, -c^2, -d^2 (negated - to be sorted in descending order)
s[1]+s[2]+s[3], s[0]+s[2]+s[4], s[0]+s[1]+s[5], s[3]+s[4]+s[5]
};
// First, make sure that d >= a,b,c (therefore -d^2 <= -a^2,...).
int min_idx = 3;
for (int i = 0; i < 3; ++i)
if (abcd_sq_neg[i] < abcd_sq_neg[min_idx] - eps)
min_idx = i;
switch (min_idx) {
case 0: // a <-> d
std::swap(s[1], s[5]);
std::swap(s[2], s[4]);
break;
case 1: // b <-> d
std::swap(s[0], s[5]);
std::swap(s[2], s[3]);
break;
case 2: // c <-> d
std::swap(s[0], s[4]);
std::swap(s[1], s[3]);
break;
}
// we could stop here and not care about the order of a,b,c.
std::swap(abcd_sq_neg[min_idx], abcd_sq_neg[3]);
if (abcd_sq_neg[0] < abcd_sq_neg[1] - eps) { // a <-> b
std::swap(s[0], s[1]);
std::swap(s[3], s[4]);
std::swap(abcd_sq_neg[0], abcd_sq_neg[1]);
}
if (abcd_sq_neg[1] < abcd_sq_neg[2] - eps) { // b <-> c
std::swap(s[1], s[2]);
std::swap(s[4], s[5]);
std::swap(abcd_sq_neg[1], abcd_sq_neg[2]);
}
if (abcd_sq_neg[0] < abcd_sq_neg[1] - eps) { // a <-> b
std::swap(s[0], s[1]);
std::swap(s[3], s[4]);
//std::swap(abcd_sq_neg[0], abcd_sq_neg[1]);
}
}
std::array<double,6> cell_parameters() const {
return gruber().cell_parameters();
}
UnitCell get_cell() const { return UnitCell(cell_parameters()); }
};
inline SellingVector GruberVector::selling() const {
double s0 = 0.5 * xi;
double s1 = 0.5 * eta;
double s2 = 0.5 * zeta;
return SellingVector({s0, s1, s2, -A - s1 - s2, -B - s0 - s2, -C - s0 - s1});
}
} // namespace gemmi
#endif