// Copyright 2017 Global Phasing Ltd. // // Unit cell. #ifndef GEMMI_UNITCELL_HPP_ #define GEMMI_UNITCELL_HPP_ #include #include // for cos, sin, sqrt, floor, NAN #include #include "math.hpp" #include "fail.hpp" // for fail #include "symmetry.hpp" // for Op, SpaceGroup namespace gemmi { inline Mat33 rot_as_mat33(const Op::Rot& rot) { double mult = 1.0 / Op::DEN; return Mat33(mult * rot[0][0], mult * rot[0][1], mult * rot[0][2], mult * rot[1][0], mult * rot[1][1], mult * rot[1][2], mult * rot[2][0], mult * rot[2][1], mult * rot[2][2]); } inline Mat33 rot_as_mat33(const Op& op) { return rot_as_mat33(op.rot); } inline Vec3 tran_as_vec3(const Op& op) { double mult = 1.0 / Op::DEN; return Vec3(mult * op.tran[0], mult * op.tran[1], mult * op.tran[2]); } /// Coordinates in Angstroms - orthogonal (Cartesian) coordinates. struct Position : Vec3 { using Vec3::Vec3; Position() = default; explicit Position(const Vec3& v) : Vec3(v) {} Position operator-() const { return Position(Vec3::operator-()); } Position operator-(const Position& o) const { return Position(Vec3::operator-(o)); } Position operator+(const Position& o) const { return Position(Vec3::operator+(o)); } Position operator*(double d) const { return Position(Vec3::operator*(d)); } Position operator/(double d) const { return Position(Vec3::operator/(d)); } Position& operator-=(const Position& o) { *this = *this - o; return *this; } Position& operator+=(const Position& o) { *this = *this + o; return *this; } Position& operator*=(double d) { *this = *this * d; return *this; } Position& operator/=(double d) { return operator*=(1.0/d); } }; inline Position operator*(double d, const Position& v) { return v * d; } /// Fractional coordinates. struct Fractional : Vec3 { using Vec3::Vec3; Fractional() = default; explicit Fractional(const Vec3& v) : Vec3(v) {} Fractional operator-(const Fractional& o) const { return Fractional(Vec3::operator-(o)); } Fractional operator+(const Fractional& o) const { return Fractional(Vec3::operator+(o)); } Fractional wrap_to_unit() const { return {x - std::floor(x), y - std::floor(y), z - std::floor(z)}; } Fractional wrap_to_zero() const { return {x - std::round(x), y - std::round(y), z - std::round(z)}; } Fractional round() const { return {std::round(x), std::round(y), std::round(z)}; } void move_toward_zero_by_one() { if (x > 0.5) x -= 1.0; else if (x < -0.5) x += 1.0; if (y > 0.5) y -= 1.0; else if (y < -0.5) y += 1.0; if (z > 0.5) z -= 1.0; else if (z < -0.5) z += 1.0; } }; enum class Asu : unsigned char { Same, Different, Any }; /// Result of find_nearest_image struct NearestImage { double dist_sq; int pbc_shift[3] = { 0, 0, 0 }; int sym_idx = 0; double dist() const { return std::sqrt(dist_sq); } bool same_asu() const { return pbc_shift[0] == 0 && pbc_shift[1] == 0 && pbc_shift[2] == 0 && sym_idx == 0; } /// Returns a string such as 1555 or 1_555. std::string symmetry_code(bool underscore) const { std::string s = std::to_string(sym_idx + 1); if (underscore) s += '_'; if (unsigned(5 + pbc_shift[0]) <= 9 && unsigned(5 + pbc_shift[1]) <= 9 && unsigned(5 + pbc_shift[2]) <= 9) { // normal, quick path for (int shift : pbc_shift) s += char('5' + shift); } else { // problematic, non-standard path for (int i = 0; i < 3; ++i) { if (i != 0 && underscore) s += '_'; s += std::to_string(5 + pbc_shift[i]); } } return s; } }; /// Like Transform, but apply() arg is Fractional (not Vec3 - for type safety). struct FTransform : Transform { FTransform() = default; FTransform(const Transform& t) : Transform(t) {} Fractional apply(const Fractional& p) const { return Fractional(Transform::apply(p)); } }; /// Non-crystallographic symmetry operation (such as in the MTRIXn record) struct NcsOp { std::string id; bool given; Transform tr; Position apply(const Position& p) const { return Position(tr.apply(p)); } }; /// A synonym for convenient passing of hkl. using Miller = std::array; struct MillerHash { std::size_t operator()(const Miller& hkl) const noexcept { return std::size_t((hkl[0] * 1024 + hkl[1]) * 1024 + hkl[2]); // NOLINT misplaced cast } }; struct UnitCellParameters { double a = 1.0, b = 1.0, c = 1.0; double alpha = 90.0, beta = 90.0, gamma = 90.0; UnitCellParameters() = default; explicit UnitCellParameters(const double (&par)[6]) { a = par[0]; b = par[1]; c = par[2]; alpha = par[3]; beta = par[4]; gamma = par[5]; } explicit UnitCellParameters(const std::array& par) { a = par[0]; b = par[1]; c = par[2]; alpha = par[3]; beta = par[4]; gamma = par[5]; } bool operator==(const UnitCellParameters& o) const { return a == o.a && b == o.b && c == o.c && alpha == o.alpha && beta == o.beta && gamma == o.gamma; } bool operator!=(const UnitCellParameters& o) const { return !operator==(o); } bool approx(const UnitCellParameters& o, double epsilon) const { auto eq = [&](double x, double y) { return std::fabs(x - y) < epsilon; }; return eq(a, o.a) && eq(b, o.b) && eq(c, o.c) && eq(alpha, o.alpha) && eq(beta, o.beta) && eq(gamma, o.gamma); } }; /// Unit cell. Contains cell parameters as well as pre-calculated /// orthogonalization and fractionalization matrices, volume, and more. /// Contains symmetry operations (incl. NCS) if they were set from outside. struct UnitCell : UnitCellParameters { UnitCell() = default; UnitCell(double a_, double b_, double c_, double alpha_, double beta_, double gamma_) { set(a_, b_, c_, alpha_, beta_, gamma_); } UnitCell(const std::array& v) { set_from_array(v); } Transform orth; Transform frac; double volume = 1.0; /// reciprocal parameters a*, b*, c*, alpha*, beta*, gamma* double ar = 1.0, br = 1.0, cr = 1.0; double cos_alphar = 0.0, cos_betar = 0.0, cos_gammar = 0.0; bool explicit_matrices = false; short cs_count = 0; // crystallographic symmetries except identity std::vector images; // symmetry operations // Non-crystalline (for example NMR) structures are supposed to use fake // unit cell 1x1x1, but sometimes they don't. A number of non-crystalline // entries in the PDB has incorrectly set unit cell or fract. matrix, // that is why we check both. bool is_crystal() const { return a != 1.0 && frac.mat[0][0] != 1.0; } // compare lengths using relative tolerance rel, angles using tolerance deg bool is_similar(const UnitCell& o, double rel, double deg) const { auto siml = [&](double x, double y) { return std::fabs(x - y) < rel * std::max(x, y); }; auto sima = [&](double x, double y) { return std::fabs(x - y) < deg; }; return siml(a, o.a) && siml(b, o.b) && siml(c, o.c) && sima(alpha, o.alpha) && sima(beta, o.beta) && sima(gamma, o.gamma); } void calculate_properties() { // ensure exact values for right angles double cos_alpha = alpha == 90. ? 0. : std::cos(rad(alpha)); double cos_beta = beta == 90. ? 0. : std::cos(rad(beta)); double cos_gamma = gamma == 90. ? 0. : std::cos(rad(gamma)); double sin_alpha = alpha == 90. ? 1. : std::sin(rad(alpha)); double sin_beta = beta == 90. ? 1. : std::sin(rad(beta)); double sin_gamma = gamma == 90. ? 1. : std::sin(rad(gamma)); if (sin_alpha == 0 || sin_beta == 0 || sin_gamma == 0) fail("Impossible angle - N*180deg."); // volume - formula from Giacovazzo p.62 volume = a * b * c * std::sqrt(1 - cos_alpha * cos_alpha - cos_beta * cos_beta - cos_gamma * cos_gamma + 2 * cos_alpha * cos_beta * cos_gamma); // reciprocal parameters a*, b*, ... (Giacovazzo, p. 64) ar = b * c * sin_alpha / volume; br = a * c * sin_beta / volume; cr = a * b * sin_gamma / volume; double cos_alphar_sin_beta = (cos_beta * cos_gamma - cos_alpha) / sin_gamma; cos_alphar = cos_alphar_sin_beta / sin_beta; //cos_alphar = (cos_beta * cos_gamma - cos_alpha) / (sin_beta * sin_gamma); cos_betar = (cos_alpha * cos_gamma - cos_beta) / (sin_alpha * sin_gamma); cos_gammar = (cos_alpha * cos_beta - cos_gamma) / (sin_alpha * sin_beta); if (explicit_matrices) return; // The orthogonalization matrix we use is described in ITfC B p.262: // "An alternative mode of orthogonalization, used by the Protein // Data Bank and most programs, is to align the a1 axis of the unit // cell with the Cartesian X_1 axis, and to align the a*_3 axis with the // Cartesian X_3 axis." double sin_alphar = std::sqrt(1.0 - cos_alphar * cos_alphar); orth.mat = {a, b * cos_gamma, c * cos_beta, 0., b * sin_gamma, -c * cos_alphar_sin_beta, 0., 0. , c * sin_beta * sin_alphar}; orth.vec = {0., 0., 0.}; double o12 = -cos_gamma / (sin_gamma * a); double o13 = -(cos_gamma * cos_alphar_sin_beta + cos_beta * sin_gamma) / (sin_alphar * sin_beta * sin_gamma * a); double o23 = cos_alphar / (sin_alphar * sin_gamma * b); frac.mat = {1 / a, o12, o13, 0., 1 / orth.mat[1][1], o23, 0., 0., 1 / orth.mat[2][2]}; frac.vec = {0., 0., 0.}; } double cos_alpha() const { return alpha == 90. ? 0. : std::cos(rad(alpha)); } /// B matrix following convention from Busing & Levy (1967), not from cctbx. /// Cf. https://dials.github.io/documentation/conventions.html Mat33 calculate_matrix_B() const { double sin_gammar = std::sqrt(1 - cos_gammar * cos_gammar); double sin_betar = std::sqrt(1 - cos_betar * cos_betar); return Mat33(ar, br * cos_gammar, cr * cos_betar, 0., br * sin_gammar, -cr * sin_betar * cos_alpha(), 0., 0., 1.0 / c); } /// The equivalent isotropic displacement factor. /// Based on Fischer & Tillmanns (1988). Acta Cryst. C44, 775-776. /// The argument is a non-orthogonalized tensor U, /// i.e. the one from SmallStructure::Site, but not from Atom. double calculate_u_eq(const SMat33& ani) const { double aar = a * ar; double bbr = b * br; double ccr = c * cr; // it could be optimized using orth.mat[0][1] and orth.mat[0][2] double cos_beta = beta == 90. ? 0. : std::cos(rad(beta)); double cos_gamma = gamma == 90. ? 0. : std::cos(rad(gamma)); return 1/3. * (sq(aar) * ani.u11 + sq(bbr) * ani.u22 + sq(ccr) * ani.u33 + 2 * (aar * bbr * cos_gamma * ani.u12 + aar * ccr * cos_beta * ani.u13 + bbr * ccr * cos_alpha() * ani.u23)); } void set_matrices_from_fract(const Transform& f) { // mmCIF _atom_sites.fract_transf_* and PDB SCALEn records usually contain // fewer significant digits than the unit cell parameters, and sometimes are // just wrong. Use them only if we seem to have non-standard crystal frame. if (f.mat.approx(frac.mat, 1e-4) && f.vec.approx(frac.vec, 1e-6)) return; // The SCALE record is sometimes incorrect. Here we only catch cases // when CRYST1 is set as for non-crystal and SCALE is very suspicious. if (frac.mat[0][0] == 1.0 && (f.mat[0][0] == 0.0 || f.mat[0][0] > 1.0)) return; frac = f; orth = f.inverse(); explicit_matrices = true; } void set(double a_, double b_, double c_, double alpha_, double beta_, double gamma_) { if (gamma_ == 0.0) // ignore empty/partial CRYST1 (example: 3iyp) return; a = a_; b = b_; c = c_; alpha = alpha_; beta = beta_; gamma = gamma_; calculate_properties(); } void set_from_parameters(const UnitCellParameters& p) { set(p.a, p.b, p.c, p.alpha, p.beta, p.gamma); } void set_from_array(const std::array& v) { set(v[0], v[1], v[2], v[3], v[4], v[5]); } void set_from_vectors(const Vec3& va, const Vec3& vb, const Vec3& vc) { set(va.length(), vb.length(), vc.length(), deg(vb.angle(vc)), deg(vc.angle(va)), deg(va.angle(vb))); } UnitCell changed_basis_backward(const Op& op, bool set_images) { Mat33 mat = orth.mat.multiply(rot_as_mat33(op)); UnitCell new_cell; new_cell.set_from_vectors(mat.column_copy(0), mat.column_copy(1), mat.column_copy(2)); if (set_images && !images.empty()) { new_cell.images.reserve(images.size()); Transform tr{rot_as_mat33(op), tran_as_vec3(op)}; Transform tr_inv = tr.inverse(); for (const FTransform& im : images) new_cell.images.push_back(tr.combine(im).combine(tr_inv)); } return new_cell; } UnitCell changed_basis_forward(const Op& op, bool set_images) { return changed_basis_backward(op.inverse(), set_images); } bool is_compatible_with_groupops(const GroupOps& gops, double eps=1e-3) const { std::array metric = metric_tensor().elements_voigt(); for (const Op& op : gops.sym_ops) { Mat33 m = orth.mat.multiply(rot_as_mat33(op)); std::array other = {{ m.column_dot(0,0), m.column_dot(1,1), m.column_dot(2,2), m.column_dot(1,2), m.column_dot(0,2), m.column_dot(0,1) }}; for (int i = 0; i < 6; ++i) if (std::fabs(metric[i] - other[i]) > eps) return false; } return true; } bool is_compatible_with_spacegroup(const SpaceGroup* sg, double eps=1e-3) const { return sg ? is_compatible_with_groupops(sg->operations(), eps) : false; } void set_cell_images_from_groupops(const GroupOps& group_ops) { images.clear(); cs_count = (short) group_ops.order() - 1; images.reserve(cs_count); for (Op op : group_ops) if (op != Op::identity()) images.push_back(Transform{rot_as_mat33(op), tran_as_vec3(op)}); } void set_cell_images_from_spacegroup(const SpaceGroup* sg) { if (sg) { set_cell_images_from_groupops(sg->operations()); } else { images.clear(); cs_count = 0; } } void add_ncs_images_to_cs_images(const std::vector& ncs) { assert(cs_count == (short) images.size()); for (const NcsOp& ncs_op : ncs) if (!ncs_op.given) { // We need it to operates on fractional, not orthogonal coordinates. FTransform f = frac.combine(ncs_op.tr.combine(orth)); images.push_back(f); for (int i = 0; i < cs_count; ++i) images.push_back(images[i].combine(f)); } } std::vector get_ncs_transforms() const { std::vector ncs; for (size_t n = cs_count; n < images.size(); n += cs_count + 1) ncs.push_back(images[n]); return ncs; } Position orthogonalize(const Fractional& f) const { return Position(orth.apply(f)); } Fractional fractionalize(const Position& o) const { return Fractional(frac.apply(o)); } /// orthogonalize_difference(a-b) == orthogonalize(a) - orthogonalize(b) // The shift (fract.vec) can be non-zero in non-standard settings, // just do not apply it here. Position orthogonalize_difference(const Fractional& delta) const { return Position(orth.mat.multiply(delta)); } /// the inverse of orthogonalize_difference Fractional fractionalize_difference(const Position& delta) const { return Fractional(frac.mat.multiply(delta)); } /// Returns box containing fractional box (a cuboid in fractional /// coordinates can be a parallelepiped in Cartesian coordinates). Box orthogonalize_box(const Box& f) const { Box r; r.minimum = orthogonalize(f.minimum); r.maximum = orthogonalize(f.maximum); if (alpha != 90. || beta == 90. || gamma == 90.) { r.extend(orthogonalize({f.minimum.x, f.minimum.y, f.maximum.z})); r.extend(orthogonalize({f.minimum.x, f.maximum.y, f.maximum.z})); r.extend(orthogonalize({f.minimum.x, f.maximum.y, f.minimum.z})); r.extend(orthogonalize({f.maximum.x, f.maximum.y, f.minimum.z})); r.extend(orthogonalize({f.maximum.x, f.minimum.y, f.minimum.z})); r.extend(orthogonalize({f.maximum.x, f.minimum.y, f.maximum.z})); } return r; } Transform orthogonalize_transform(const FTransform& ftr) const { return orth.combine(ftr.combine(frac)); } Transform op_as_transform(const Op& op) const { return orthogonalize_transform(Transform{rot_as_mat33(op), tran_as_vec3(op)}); } double distance_sq(const Fractional& pos1, const Fractional& pos2) const { Fractional diff = (pos1 - pos2).wrap_to_zero(); return orthogonalize_difference(diff).length_sq(); } double distance_sq(const Position& pos1, const Position& pos2) const { return distance_sq(fractionalize(pos1), fractionalize(pos2)); } double volume_per_image() const { return is_crystal() ? volume / (1 + images.size()) : NAN; } // Helper function. PBC = periodic boundary conditions. bool search_pbc_images(Fractional&& diff, NearestImage& image) const { int neg_shift[3] = {0, 0, 0}; if (is_crystal()) { for (int j = 0; j < 3; ++j) neg_shift[j] = iround(diff.at(j)); diff.x -= neg_shift[0]; diff.y -= neg_shift[1]; diff.z -= neg_shift[2]; } Position orth_diff = orthogonalize_difference(diff); double dsq = orth_diff.length_sq(); if (dsq < image.dist_sq) { image.dist_sq = dsq; for (int j = 0; j < 3; ++j) image.pbc_shift[j] = -neg_shift[j]; return true; } return false; } NearestImage find_nearest_image(const Position& ref, const Position& pos, Asu asu) const { NearestImage image; if (asu == Asu::Different) image.dist_sq = INFINITY; else image.dist_sq = ref.dist_sq(pos); if (asu == Asu::Same) return image; Fractional fpos = fractionalize(pos); Fractional fref = fractionalize(ref); search_pbc_images(fpos - fref, image); if (asu == Asu::Different && image.pbc_shift[0] == 0 && image.pbc_shift[1] == 0 && image.pbc_shift[2] == 0) image.dist_sq = INFINITY; for (int n = 0; n != static_cast(images.size()); ++n) if (search_pbc_images(images[n].apply(fpos) - fref, image)) image.sym_idx = n + 1; return image; } void apply_transform(Fractional& fpos, int image_idx, bool inverse) const { if (image_idx > 0) { const FTransform& t = images.at(image_idx - 1); if (!inverse) fpos = t.apply(fpos); else fpos = FTransform(t.inverse()).apply(fpos); } } NearestImage find_nearest_pbc_image(const Fractional& fref, Fractional fpos, int image_idx=0) const { NearestImage sym_image; sym_image.dist_sq = INFINITY; sym_image.sym_idx = image_idx; apply_transform(fpos, image_idx, false); search_pbc_images(fpos - fref, sym_image); return sym_image; } NearestImage find_nearest_pbc_image(const Position& ref, const Position& pos, int image_idx=0) const { return find_nearest_pbc_image(fractionalize(ref), fractionalize(pos), image_idx); } std::vector find_nearest_pbc_images(const Fractional& fref, double dist, const Fractional& fpos, int image_idx) const { std::vector results; NearestImage im = find_nearest_pbc_image(fref, fpos, image_idx); int sh[3] = {im.pbc_shift[0], im.pbc_shift[1], im.pbc_shift[2]}; for (im.pbc_shift[0] = sh[0]-1; im.pbc_shift[0] <= sh[0]+1; ++im.pbc_shift[0]) for (im.pbc_shift[1] = sh[1]-1; im.pbc_shift[1] <= sh[1]+1; ++im.pbc_shift[1]) for (im.pbc_shift[2] = sh[2]-1; im.pbc_shift[2] <= sh[2]+1; ++im.pbc_shift[2]) { Fractional shift(im.pbc_shift[0], im.pbc_shift[1], im.pbc_shift[2]); im.dist_sq = orthogonalize_difference(fpos - fref + shift).length_sq(); if (im.dist_sq <= sq(dist)) results.push_back(im); } return results; } Position orthogonalize_in_pbc(const Position& ref, const Fractional& fpos) const { Fractional fref = fractionalize(ref); return orthogonalize_difference((fpos - fref).wrap_to_zero()) + ref; } Position find_nearest_pbc_position(const Position& ref, const Position& pos, int image_idx, bool inverse=false) const { Fractional fpos = fractionalize(pos); apply_transform(fpos, image_idx, inverse); return orthogonalize_in_pbc(ref, fpos); } // apply NearestImage symmetry to fpos Fractional fract_image(const NearestImage& im, Fractional fpos) { apply_transform(fpos, im.sym_idx, false); return fpos + Fractional(im.pbc_shift[0], im.pbc_shift[1], im.pbc_shift[2]); } /// Counts nearby symmetry mates (0 = none, 3 = 4-fold axis, etc). /// \pre is_crystal() int is_special_position(const Fractional& fpos, double max_dist) const { const double max_dist_sq = max_dist * max_dist; int n = 0; for (const FTransform& image : images) { Fractional fdiff = (image.apply(fpos) - fpos).wrap_to_zero(); if (orthogonalize_difference(fdiff).length_sq() < max_dist_sq) ++n; } return n; } int is_special_position(const Position& pos, double max_dist = 0.8) const { return is_special_position(fractionalize(pos), max_dist); } /// Calculate 1/d^2 for specified hkl reflection. /// 1/d^2 = (2*sin(theta)/lambda)^2 // The indices are integers, but they may be stored as floating-point // numbers (MTZ format) so we use double to avoid conversions. double calculate_1_d2_double(double h, double k, double l) const { double arh = ar * h; double brk = br * k; double crl = cr * l; return arh * arh + brk * brk + crl * crl + 2 * (arh * brk * cos_gammar + arh * crl * cos_betar + brk * crl * cos_alphar); } double calculate_1_d2(const Miller& hkl) const { return calculate_1_d2_double(hkl[0], hkl[1], hkl[2]); } /// Calculate d-spacing. /// d = lambda/(2*sin(theta)) double calculate_d(const Miller& hkl) const { return 1.0 / std::sqrt(calculate_1_d2(hkl)); } /// Calculate (sin(theta)/lambda)^2 = d*^2/4 double calculate_stol_sq(const Miller& hkl) const { return 0.25 * calculate_1_d2(hkl); } /// https://dictionary.iucr.org/Metric_tensor SMat33 metric_tensor() const { // the order in SMat33 is ... m12 m13 m23 -> a.a b.b c.c a.b a.c b.c return {a*a, b*b, c*c, a*orth.mat[0][1], a*orth.mat[0][2], b*c*cos_alpha()}; } SMat33 reciprocal_metric_tensor() const { return {ar*ar, br*br, cr*cr, ar*br*cos_gammar, ar*cr*cos_betar, br*cr*cos_alphar}; } /// Returns reciprocal unit cell. UnitCell reciprocal() const { auto acosd = [](double x) { return deg(std::acos(x)); }; return UnitCell(ar, br, cr, acosd(cos_alphar), acosd(cos_betar), acosd(cos_gammar)); } Miller get_hkl_limits(double dmin) const { return {{int(a / dmin), int(b / dmin), int(c / dmin)}}; } Mat33 primitive_orth_matrix(char centring_type) const { if (centring_type == 'P') return orth.mat; Mat33 c2p = rot_as_mat33(centred_to_primitive(centring_type)); return orth.mat.multiply(c2p); } }; } // namespace gemmi #endif