Search the primitive cell when re-seating a lattice into a fixed group's setting
Build Packages / build:windows:nocuda (push) Successful in 17m13s
Build Packages / build:windows:cuda (push) Successful in 18m54s
Build Packages / build:viewer-tgz:cpu (push) Successful in 21m26s
Build Packages / build:viewer-tgz:cuda (push) Successful in 22m38s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 22m42s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 23m54s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 28m16s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 29m21s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 29m29s
Build Packages / build:windows:nocuda (pull_request) Successful in 12m58s
Build Packages / build:windows:cuda (pull_request) Successful in 21m15s
Build Packages / XDS test (durin plugin) (push) Successful in 11m53s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 21m26s
Build Packages / build:rpm (rocky9) (push) Successful in 23m9s
Build Packages / Generate python client (push) Successful in 52s
Build Packages / Build documentation (push) Successful in 1m35s
Build Packages / Create release (push) Skipped
Build Packages / build:rpm (ubuntu2404) (push) Successful in 22m17s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 27m18s
Build Packages / build:rpm (rocky8) (push) Successful in 28m44s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 11m13s
Build Packages / XDS test (neggia plugin) (push) Successful in 9m58s
Build Packages / DIALS test (push) Successful in 24m5s
Build Packages / build:viewer-tgz:cpu (pull_request) Successful in 15m14s
Build Packages / build:viewer-tgz:cuda (pull_request) Successful in 17m16s
Build Packages / build:rpm (rocky9_nocuda) (pull_request) Successful in 21m8s
Build Packages / build:rpm (ubuntu2404_nocuda) (pull_request) Successful in 21m13s
Build Packages / build:rpm (rocky8_nocuda) (pull_request) Successful in 23m51s
Build Packages / build:rpm (ubuntu2204_nocuda) (pull_request) Successful in 23m49s
Build Packages / build:rpm (rocky8_sls9) (pull_request) Successful in 20m44s
Build Packages / build:rpm (rocky9_sls9) (pull_request) Successful in 19m15s
Build Packages / build:rpm (rocky9) (pull_request) Successful in 19m58s
Build Packages / build:rpm (ubuntu2404) (pull_request) Successful in 22m1s
Build Packages / XDS test (durin plugin) (pull_request) Successful in 11m7s
Build Packages / Generate python client (pull_request) Successful in 35s
Build Packages / build:rpm (ubuntu2204) (pull_request) Successful in 25m21s
Build Packages / Build documentation (pull_request) Successful in 1m2s
Build Packages / Create release (pull_request) Skipped
Build Packages / build:rpm (rocky8) (pull_request) Successful in 26m44s
Build Packages / XDS test (JFJoch plugin) (pull_request) Successful in 9m16s
Build Packages / XDS test (neggia plugin) (pull_request) Successful in 7m46s
Build Packages / DIALS test (pull_request) Successful in 20m43s
Build Packages / Unit tests (push) Successful in 2h28m34s
Build Packages / Unit tests (pull_request) Successful in 2h39m23s

rotation_lattice is the CONVENTIONAL cell and carries its centring beside it, but
LatticeSearchForClass reaches the character table through a Niggli reduction that
takes whatever it is handed to be primitive. This arm is entered exactly when the
indexed centring differs from the fixed group's, which includes every centred
indexed lattice - so the cell was being reduced as a lattice with the wrong point
set. The other lattice arithmetic in this file already goes through ToPrimitive
for the same reason.

What that could cost: a C-centred orthorhombic conventional cell read as
primitive is itself a perfectly good oP metric, so -S P212121 on one would match
character 32, reindex by the identity with a residual of zero, and report that it
had re-seated the reflections. The centring then agrees, so the guard added
alongside this arm - which exists to stop exactly that merge - never fires, and
the run merges with an absence rule that deletes half the reflections the crystal
has.

Measured on the 38-crystal rotation battery, every crystal pinned to its XDS
reference space group with the new --force-xds-sg: 35/38 groups correct, and the
same 3 runs abort before and after. The fixed-group arm is reached by 4 crystals;
3 of them index P-centred, where ToPrimitive is the identity. The one crystal
that feeds a genuinely centred cell in (F-centred cubic against a reference
P3121) refuses either way, with a byte-identical message. Both crystals the arm
re-seats successfully (P -> C 1 2 1) produce byte-identical merged CIFs. So this
removes the hazard without moving a result.

--force-xds-sg is new: the harness only ever ran de novo, so the fixed-group path
had no coverage in it at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y5XisyYxmF8mUEQjzpMRe2
This commit is contained in:
2026-08-25 08:02:50 +02:00
co-authored by Claude Opus 5
parent 4870c0214a
commit 3ed7628dd5
2 changed files with 19 additions and 4 deletions
+12 -2
View File
@@ -2130,8 +2130,18 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
const auto want_system = fixed_sg->crystal_system() == gemmi::CrystalSystem::Trigonal
&& fixed_sg->centring_type() == 'P'
? gemmi::CrystalSystem::Hexagonal : fixed_sg->crystal_system();
const auto cand = LatticeSearchForClass(*end_msg.rotation_lattice, want_system,
fixed_sg->centring_type());
// Search the PRIMITIVE cell. rotation_lattice is the conventional one and its centring
// is carried beside it, but the character table is reached through a Niggli reduction
// that takes whatever it is handed to be primitive - so a centred conventional cell
// reduces as a lattice with the wrong point set. This arm is entered exactly when the
// centrings differ, which includes every centred indexed lattice, so the distinction is
// not academic: a C-centred orthorhombic conventional cell read as primitive is itself a
// perfectly good oP metric, and -S P212121 on one would match, reindex by the identity
// and merge in a group whose absence rule deletes half the reflections that exist.
// The other lattice arithmetic in this file goes through ToPrimitive for the same reason.
const auto cand = LatticeSearchForClass(
end_msg.rotation_lattice->ToPrimitive(indexed_centering), want_system,
fixed_sg->centring_type());
if (cand && reindex_into(*cand)) {
const auto &uc = *result.consensus_cell;
logger.Info("Reindexed the {}-centred indexed lattice into the {}-centred setting the "
+7 -2
View File
@@ -250,7 +250,7 @@ def find_rugnux(explicit):
return None
def run_rugnux(master, workdir, name, xds, rugnux_bin, threads, timeout, reuse, extra_args=""):
def run_rugnux(master, workdir, name, xds, rugnux_bin, threads, timeout, reuse, extra_args="", force_sg=False):
"""Run rugnux; return (cif_path, error). Resolution + anomalous matched to XDS."""
workdir.mkdir(parents=True, exist_ok=True)
cif = workdir / f"{name}.cif"
@@ -267,6 +267,8 @@ def run_rugnux(master, workdir, name, xds, rugnux_bin, threads, timeout, reuse,
cmd += ["--scaling-low-resolution", f"{xds['dmax']:.3f}"]
if threads:
cmd += ["-N", str(threads)]
if force_sg and xds.get("sg"):
cmd += ["-S", str(xds["sg"])]
if extra_args:
import shlex
cmd += shlex.split(extra_args)
@@ -391,6 +393,9 @@ def main():
ap.add_argument("--reuse", action="store_true", help="skip rugnux where its .cif already exists")
ap.add_argument("--xds-only", action="store_true", help="only parse+print the XDS side (no rugnux run)")
ap.add_argument("--progress", action="store_true", help="print per-crystal progress to stderr")
ap.add_argument("--force-xds-sg", action="store_true",
help="pin each crystal to the XDS reference space group with -S, instead of "
"letting rugnux determine it de novo (exercises the fixed-group path)")
ap.add_argument("--extra-args", default="", help="extra args appended to every rugnux command (e.g. '--rotation-refine-geometry')")
args = ap.parse_args()
@@ -432,7 +437,7 @@ def main():
file=sys.stderr, flush=True)
cif, err, elapsed = run_rugnux(master, workdir / name, name, xds,
rugnux_bin, args.threads, args.timeout, args.reuse,
args.extra_args)
args.extra_args, args.force_xds_sg)
if cif:
try:
rug = parse_rugnux_cif(cif)