RESULT-CHANGING: rotation indexer counts a centred candidate's indexed fraction with its centring
Since the overlap test keeps integer sub-cells of an accepted cell (9b4acd6ab), a half-volume primitive
cell whose metric is C-centred monoclinic reaches the candidate selection. Its conventional C cell is
the true primitive cell, and IndexedFraction counted spots on that conventional cell ignoring the
centring, so it indexed exactly what the P cell indexes; the sub-cell rule, comparing primitive
volumes, then took the C label. On a monoclinic P crystal with no centring pseudo-symmetry (h+k odd as
strong as h+k even at every resolution) the first pass came out "monoclinic C", and with a
user-fixed P 1 21 1 the run was refused by the fixed-group lattice check.
A spot now counts only where its rounded conventional index is a node of the centred lattice (an
integer index on the primitive cell). A genuinely centred crystal is unaffected: its spots obey the
centring. Measured: 14 witness sets (I, F, R, C lattices, small molecules) give the same first-pass
lattices and merging results; the affected set merges in P 1 21 1 with and without -S.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
This commit is contained in:
1 parent
556e10c18f
commit
35f49a295d
1 file changed
+27
-9
@@ -99,17 +99,33 @@ namespace {
|
||||
// Miller indices within tol. Comparing a symmetry-constrained refinement against an
|
||||
// unconstrained (triclinic) one is a data-driven test for a false promotion: a wrong
|
||||
// higher-symmetry constraint snaps a pseudo cell onto ideal angles and misplaces most spots.
|
||||
float IndexedFraction(const CrystalLattice &latt, const std::vector<Coord> &coords, float tol) {
|
||||
//
|
||||
// A centred lattice is counted with its centring: a spot whose index on the conventional cell is
|
||||
// one the centring extinguishes is not on the lattice. Without that a primitive lattice labelled
|
||||
// centred indexes exactly what the same cell labelled P does, at half the primitive volume - and
|
||||
// the sub-cell rule of the candidate selection takes the smaller of two cells that index alike.
|
||||
float IndexedFraction(const CrystalLattice &latt, const std::vector<Coord> &coords, float tol,
|
||||
char centering = 'P') {
|
||||
if (coords.empty())
|
||||
return 0.0f;
|
||||
const Coord a = latt.Vec0(), b = latt.Vec1(), c = latt.Vec2();
|
||||
// Rows: the primitive cell's Miller indices as combinations of the conventional ones.
|
||||
const gemmi::Mat33 to_prim = gemmi::rot_as_mat33(gemmi::centred_to_primitive(centering)).transpose();
|
||||
const float tol_sq = tol * tol;
|
||||
size_t indexed = 0;
|
||||
for (const Coord &s : coords) {
|
||||
const float dh = a * s - std::round(a * s); // Coord operator* = dot product = Miller index
|
||||
const float dk = b * s - std::round(b * s);
|
||||
const float dl = c * s - std::round(c * s);
|
||||
if (dh * dh + dk * dk + dl * dl < tol_sq)
|
||||
const float h = std::round(a * s), k = std::round(b * s), l = std::round(c * s);
|
||||
const float dh = a * s - h; // Coord operator* = dot product = Miller index
|
||||
const float dk = b * s - k;
|
||||
const float dl = c * s - l;
|
||||
if (dh * dh + dk * dk + dl * dl >= tol_sq)
|
||||
continue;
|
||||
bool allowed = true;
|
||||
for (int i = 0; i < 3 && centering != 'P'; i++) {
|
||||
const double p = to_prim[i][0] * h + to_prim[i][1] * k + to_prim[i][2] * l;
|
||||
allowed = allowed && std::abs(p - std::round(p)) < 0.01;
|
||||
}
|
||||
if (allowed)
|
||||
++indexed;
|
||||
}
|
||||
return static_cast<float>(indexed) / static_cast<float>(coords.size());
|
||||
@@ -421,18 +437,20 @@ void RotationIndexer::RunIndexingLocked() {
|
||||
// Refine (constrained metric solve + score by the refined-geometry indexed fraction, the
|
||||
// reliable discriminator). Runs on its own thread per solve.
|
||||
struct Solved { bool ok = false; float frac = 0.0f; XtalOptimizerData data; };
|
||||
auto solve = [&](XtalOptimizerData d) -> Solved {
|
||||
auto solve = [&](XtalOptimizerData d, char centering) -> Solved {
|
||||
const bool ok = XtalOptimizer(d, v_, kCeresThreads);
|
||||
const float frac = ok ? IndexedFraction(d.latt, accumulate(d.geom, d.axis), index_tol) : 0.0f;
|
||||
const float frac = ok ? IndexedFraction(d.latt, accumulate(d.geom, d.axis), index_tol, centering)
|
||||
: 0.0f;
|
||||
return {ok, frac, std::move(d)};
|
||||
};
|
||||
std::vector<std::future<Solved>> constrained_f(n_try), tri_f(n_try);
|
||||
for (size_t ci = 0; ci < n_try; ci++) {
|
||||
if (!work[ci].viable)
|
||||
continue;
|
||||
constrained_f[ci] = std::async(std::launch::async, [&, ci] { return solve(work[ci].constrained); });
|
||||
constrained_f[ci] = std::async(std::launch::async,
|
||||
[&, ci] { return solve(work[ci].constrained, work[ci].sr.centering); });
|
||||
if (work[ci].has_tri)
|
||||
tri_f[ci] = std::async(std::launch::async, [&, ci] { return solve(work[ci].tri); });
|
||||
tri_f[ci] = std::async(std::launch::async, [&, ci] { return solve(work[ci].tri, 'P'); });
|
||||
}
|
||||
|
||||
// Assemble and select serially, in candidate order - identical to refining them one by one.
|
||||
|
||||
Reference in new issue
Block a user