The files were written on the axes the space-group search named, which for
P2221/P21212 puts the unique axis wherever the a<b<c indexing put it (a 52.51
87.87 137.72 crystal came out P 2 21 21 where XDS writes 87.87 137.72 52.51
P 21 21 2), and a reference MTZ was matched in the data's frame: on permuted
axes its free-R flags landed on unrelated reflections while the log reported a
high matched count.
- New CrystalSetting (scale_merge): changes of basis between settings of one
lattice (cell, group, index operator, basis matrix), the {-1,0,1} det +1
candidates (CellMappingOperators, moved from ModelValidation), MetricViolation
(moved from Rugnux), ChooseOutputSetting and SeatGroupByAbsences.
- Output setting: after every decision the merge, the integrated reflections
(unmerged MTZ), the P1 cross-check, the lattice and the _process.h5 reindex
matrix are relabelled into the ITA standard setting - or, in priority order,
a reference MTZ's, a fitting model's, the -C axis order, a non-standard -S
symbol's. Free-R flags are drawn again on the written axes. Reported as
SETTING_OPERATOR / SETTING_SOURCE.
- Reference MTZ: the group is kept in its setting; after the merge every cell
mapping onto the reference cell (times the twin laws) is scored by the
reference CC, the best is re-seated and re-merged, and the free flags are
inherited only where CC >= 0.5 over >= 50% of the reference range
(REFERENCE_MISMATCH otherwise; --mode scale gates the same way).
REFERENCE_OPERATOR / _CC / _MATCHED_FRACTION / _FREE_FLAGS_INHERITED.
- -S: a fixed group is put on the axes its absences name before merging
(SeatGroupByAbsences), fixing -S 18 on a cell whose pure axis is not c.
- --model: a model in another setting is now a claim the null tests; where it
fits, the data are written in its setting and the validation is remade on
those axes (KeepModelVerdict carries the decisions over).
Tests: [setting] (synthetic #18/#17/I222/C222/c-unique P21/C2 beta/I2->C2/P1,
-C and -S order, absence seating, permuted reference with flags).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1G8gJVAy6gp1K5Dz3NE5C
101 lines
6.2 KiB
C++
101 lines
6.2 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#pragma once
|
|
|
|
#include <array>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#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<gemmi::Op> &UnimodularOperators();
|
|
|
|
// The subset of UnimodularOperators that carries `from` onto `to` (CellsCorrespond).
|
|
std::vector<gemmi::Op> 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<b with c fixed for P2221, P21212, C2221 and
|
|
// C222); monoclinic is b-unique, C-centred, least oblique with beta >= 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<UnitCell> &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<int, 3> 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<ReflectionZ> &reflections);
|