symmetry: the cell a group is adopted on is refined under that group
Build Packages / Create release (push) Successful in 15s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 7m21s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 8m28s
Build Packages / build:viewer-tgz:cpu (push) Successful in 9m53s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m57s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 15m12s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 15m50s
Build Packages / build:windows:nocuda (push) Successful in 17m15s
Build Packages / build:windows:cuda (push) Successful in 19m50s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 23m14s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 19m9s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 19m12s
Build Packages / build:rugnux:windows (push) Successful in 10m50s
Build Packages / Generate python client (push) Successful in 44s
Build Packages / Build documentation (push) Successful in 1m23s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 20m10s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m23s
Build Packages / build:rpm (rocky8) (push) Successful in 18m2s
Build Packages / build:rpm (rocky9) (push) Successful in 18m22s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 13m46s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 12m25s
Build Packages / Unit tests (push) Successful in 1h12m4s

A space group confirmed from the intensities AFTER integration is one no
constrained fit has produced. The Bravais class is decided on the unrefined
indexing candidate, so a two-fold the spot positions never offered leaves the
freely refined metric standing in the group's own setting - a C 1 2 1 whose
alpha is 88.5 - and that cell goes to the report, the master file and the MTZ.

At adoption, re-refine the lattice under the group's constraint against the
accumulated rotation spots, at the geometry the images were integrated at, and
keep it when the spots do - the bar the indexer's own pseudo-symmetry guard
uses. Where they refuse it, report the nearest metric the group fixes and say
that a deviation that size is not refinement noise. A cell whose violation is
above MAX_METRIC_VIOLATION is the WRONG cell for its group rather than an
unconstrained one, and nothing here touches it: a visible mismatch must not
become a plausible-looking one.

The projection is applied as a change of basis, not as three rebuilt vectors:
the three-Coord constructor enforces a right-handed basis, and a left-handed
lattice came back with an axis flipped and its free angle replaced by the
supplement, which failed the merge outright.

Battery over 151 datasets, base against this: 144 byte-identical, 7 cells moved
onto their group's metric, no space group changed, no run gained or lost, open
arm 94/99 both ways. Every merge statistic of the seven is unchanged except
completeness, which rises on four and falls 0.1 % on one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KWkZ1o2aoQ9EimF2wtzBky
This commit is contained in:
leonarski_fandClaude Opus 5 committed 2026-09-15 15:57:12 +02:00
1 parent 22b4700c1e
commit 0fccbe21b5
11 files changed
+380 -9

No files matched your search

+43
View File
@@ -596,3 +596,46 @@ TEST_CASE("CrystalLattice::ToPrimitive gives an R-centred lattice its rhombohedr
CHECK(back.c == Catch::Approx(c).epsilon(1e-4));
CHECK(back.gamma == Catch::Approx(120.0).epsilon(1e-4));
}
// SymmetrizeMetric is what a run falls back on when it has adopted a group on a cell no fit under
// that group produced. It has to do two things: leave the group's metric exactly satisfied, and move
// the cell as little as that requires - which means the orientation it arrives in is kept.
TEST_CASE("SymmetrizeMetric puts a cell onto the metric its group fixes") {
const auto &c2 = *gemmi::find_spacegroup_by_name("C 1 2 1");
// A C-centred monoclinic setting whose alpha is 1.5 deg off, as a freely refined metric promoted
// after integration arrives: the group cannot describe it.
const CrystalLattice off(160.0, 140.0, 90.0, 88.5, 119.9, 90.1);
const auto fixed = SymmetrizeMetric(off, c2).GetUnitCell();
CHECK(fixed.alpha == Catch::Approx(90.0).margin(1e-3));
CHECK(fixed.gamma == Catch::Approx(90.0).margin(1e-3));
// The free angle and the lengths stay where they were, to well inside the move it had to make.
CHECK(fixed.beta == Catch::Approx(119.9).margin(0.2));
CHECK(fixed.a == Catch::Approx(160.0).epsilon(2e-3));
CHECK(fixed.b == Catch::Approx(140.0).epsilon(2e-3));
CHECK(fixed.c == Catch::Approx(90.0).epsilon(2e-3));
// A cell the group already describes is not moved at all, whatever orientation it is in.
const CrystalLattice ok(80.0, 50.0, 60.0, 90.0, 105.0, 90.0);
const auto same = SymmetrizeMetric(ok, c2).GetUnitCell();
check_uc(same, 80.0, 50.0, 60.0, 90.0, 105.0, 90.0, 1e-3, 1e-3);
// ...including one that is not axis-aligned: the answer is a property of the lattice, not of the
// frame it is written in, and the orientation it came in is the orientation it goes out in.
const RotMatrix rot(0.7, Coord(1.0f, 2.0f, 3.0f).Normalize());
const CrystalLattice turned = off.Multiply(rot);
// A LEFT-handed basis keeps its hand and its angles: the three-Coord constructor would flip an
// axis and hand back the supplement of beta, which is a different cell from the one the
// reflections are indexed on - and a metric projection may not change the cell that much.
const CrystalLattice flipped = off.Multiply(gemmi::Mat33(-1, 0, 0, 0, 1, 0, 0, 0, 1));
REQUIRE(flipped.CalcVolume() < 0);
const auto left = SymmetrizeMetric(flipped, c2);
CHECK(left.CalcVolume() < 0);
CHECK(left.GetUnitCell().beta == Catch::Approx(flipped.GetUnitCell().beta).margin(1e-3));
CHECK(left.GetUnitCell().alpha == Catch::Approx(90.0).margin(1e-3));
const auto turned_fixed = SymmetrizeMetric(turned, c2);
check_uc(turned_fixed.GetUnitCell(), fixed.a, fixed.b, fixed.c, 90.0, fixed.beta, 90.0, 1e-3, 1e-3);
CHECK(turned_fixed.Vec0() * turned.Vec0()
== Catch::Approx(turned_fixed.Vec0().Length() * turned.Vec0().Length()).epsilon(1e-4));
}