// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include #include #include #include "gemmi/symmetry.hpp" #include "gemmi/unitcell.hpp" #include "../../common/UnitCell.h" // Settings of one lattice: which axes a cell and a space group are written on. Every change of basis // here is stated the way GEMMI states it - `cob` acts on fractional coordinates (x' = cob x) - so the // cell follows with CellInBasis, the group with SpaceGroupInBasis and a Miller index with // HklOperator(cob). Only integer, determinant +1 changes of basis are used: they keep the volume and // the hand, so a reflection file relabelled by one describes exactly the same measurements. // The operator a Miller index moves by (for apply_to_hkl / ReindexMergedIntoAsu). gemmi::Op HklOperator(const gemmi::Op &cob); // An index operator written the way CCP4 REINDEX and POINTLESS write one - "k,l,h" is h'=k, k'=l, // l'=h - rather than as GEMMI prints the transposed matrix apply_to_hkl uses. std::string IndexTriplet(const gemmi::Op &hkl_op); // The same change as a basis matrix P - new axis i = sum_j P[i][j] old axis j, and h' = P h - which is // what CrystalLattice::Multiply and the _process.h5 reindex matrix take. gemmi::Mat33 BasisMatrix(const gemmi::Op &cob); UnitCell CellInBasis(const UnitCell &cell, const gemmi::Op &cob); // The group `sg` becomes in the new basis, as a setting in GEMMI's table (an origin shift is searched // for, since a permutation can move the origin of a screw-axis group); nullptr when none names it. const gemmi::SpaceGroup *SpaceGroupInBasis(const gemmi::SpaceGroup &sg, const gemmi::Op &cob); // How far a cell stands from the metric its space group requires. A group's own rotations must leave // the metric tensor G invariant - R^T G R = G - so a cell that fails that cannot be described by that // group whatever its centring is: -S P41212 on a=37.909 b=78.031 c=77.594 has the right centring and // the wrong axis, the 4-fold running along a where the group puts it along c, and the group's // operators do not act on the reflections' own indices. Returned relative to the largest diagonal // element of G, so it is dimensionless and comparable across cells. double MetricViolation(const UnitCell &uc, const gemmi::SpaceGroup &sg); // The metric never fits exactly - the cell is refined against the data, not constrained to the group - // so the test needs a tolerance, and it is used to REFUSE, so the tolerance has to sit above what a // correct answer reaches rather than below what a wrong one does. Measured over 113 corpus runs, every // group determined from its own cell scores under 0.032 (the worst is a C222 call on a cell whose // alpha is 87.75); the known axis-permuted case scores 0.764. 0.1 leaves 3x headroom over the first // and 7.6x under the second. // SearchSpaceGroup's CellHostsRotations asks the same question at 2e-3, normalised per element pair: // that is the right bound for ADMITTING an extra candidate setting, and it would refuse 6 of those // 113 runs if it were used here. constexpr double MAX_METRIC_VIOLATION = 0.1; // Two cells that describe the same lattice on the same axes, within refinement noise (5% in each // length, 3 deg in each angle) - two crystals of one form measured apart. bool CellsCorrespond(const gemmi::UnitCell &a, const gemmi::UnitCell &b); // Every integer change of basis with entries in {-1,0,1} and determinant +1, identity first. That set // holds every axis permutation and sign flip and the off-diagonal that turns an I-centred monoclinic // cell into a C-centred one; a halved or doubled axis is not in it, and must not be - that is an // indexing error, not a choice of description. const std::vector &UnimodularOperators(); // The subset of UnimodularOperators that carries `from` onto `to` (CellsCorrespond). std::vector CellMappingOperators(const gemmi::UnitCell &from, const gemmi::UnitCell &to); // The change of basis that writes `sg` on `cell` in the setting the files should carry. Every // candidate is a relabelling of the same lattice, so nothing measured depends on the choice. // - `target_cell` (the cell given with -C): the candidate whose cell is closest to it, in any setting // of the group - or of `target`, where that is given too. // - `target` (a group given with -S, in whatever setting it was given): that setting. // - neither: the ITA reference setting, as XDS and POINTLESS write it. Orthorhombic axes the group // does not tie are ordered shortest first (a<=b<=c; a= 90; triclinic keeps the // reduced cell the run indexed. // Among equal candidates the one closest to the identity wins, so a run already in the chosen setting // is left exactly as it is. Identity where no candidate qualifies. gemmi::Op ChooseOutputSetting(const UnitCell &cell, const gemmi::SpaceGroup &sg, const gemmi::SpaceGroup *target = nullptr, const std::optional &target_cell = std::nullopt); // One reflection's evidence for being present: its I/sigma, with the partials of a rotation sweep summed. struct ReflectionZ { std::array hkl{}; double z = 0.0; }; // A space group given by the user names its symmetry elements by axis - P 21 21 2 puts the pure // two-fold on c - while the indexed cell orders its axes by length, and for a group whose axes are not // all alike the metric cannot say which axis is which. The absences can. Every change of basis that // keeps a cell the group can describe and the centring the lattice has is a candidate, and the one // whose predicted absences agree best with the measured ones wins (see the .cpp), the identity where // they tie. Returns the change of basis to put the reflections through so that `sg`, exactly as given, // describes them. `reflections` need only hold those on axial rows and zones - no other can be absent. gemmi::Op SeatGroupByAbsences(const UnitCell &cell, const gemmi::SpaceGroup &sg, const std::vector &reflections);