From e3da313c844d94be55e354ac6b2bb4d0da3da148 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sun, 13 Sep 2026 15:02:04 +0200 Subject: [PATCH] reader: a goniometer whose inner circle is inclined says so in its axis table A fixed-chi stage carries phi at a standing angle to the base circle, and states that angle in the CBF _axis table and nowhere else - its "# Chi" line reads 0.0000, because no chi circle is driven. ParseAxisTable kept only rotation axes hanging off nothing, so the inclination was discarded and RotationAxis() fell back to that zero: a phi scan was run about the base axis, tens of degrees from the one the crystal actually turned about. Measured on a magic-angle head 54.74 degrees off omega: the axis goes from 1 0 0 to 0.577382 -0.707087 -0.408237, against dxtbx's 0.5774 -0.7071 -0.4083, and the first pass from a P-centred triclinic 27.2 58.1 126.2 that indexes nothing to P-orthorhombic 5.16 14.05 14.81, which is the cell DIALS and XDS both reach on these frames. The mirrored inclination returns to nothing, so it is that angle and not any perturbation of the axis. Every other header in the corpus states one rotation axis, which is every Eulerian head, and is untouched. The two that share this table were written three days later by a template that puts the same angle on the "# Chi" line, agreeing to 0.03 degrees - they read identically either way. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56 --- reader/JFJochCBFReader.cpp | 11 ++++++++--- reader/MiniCBF.cpp | 2 ++ reader/MiniCBF.h | 5 +++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/reader/JFJochCBFReader.cpp b/reader/JFJochCBFReader.cpp index 7d554119f..2556df484 100644 --- a/reader/JFJochCBFReader.cpp +++ b/reader/JFJochCBFReader.cpp @@ -152,16 +152,21 @@ std::optional ImageOrientation(const minicbf::Header &h) { // scan turns about the base axis however the cradle is set - which is why a header carrying a large // fixed chi still comes out as the base axis here - and only a phi scan is carried by chi and by // omega. Chi turns about the beam, as the imgCIF axis convention has it, pointing back at the -// source; internal z points the other way, hence the minus. A kappa arm cannot be expressed at all: -// its inclination is a property of the hardware that no miniCBF header states. +// source; internal z points the other way, hence the minus. A head whose inner circle is inclined - +// a fixed-chi stage, a kappa arm - states that inclination in its axis table and nowhere else. Coord RotationAxis(const minicbf::Header &h) { const Coord base = BaseAxis(h); if (!minicbf::ScansPhi(h)) return base; const auto rad = [](double deg) { return static_cast(deg * PI / 180.0); }; + // Where the table states the inner axis outright, that IS the inclination, and the driven-circle + // angles do not describe it: a fixed-chi stage carries phi at a standing angle to the base with + // no chi circle to report, so "# Chi" reads zero while the axis is tens of degrees away. const Coord chi_axis(0.0f, 0.0f, -1.0f); - const Coord tilted = RotMatrix(rad(h.chi_deg.value_or(0.0)), chi_axis) * base; + const Coord tilted = h.inner_spindle_axis.has_value() + ? ImgCIFToInternal(*h.inner_spindle_axis) + : RotMatrix(rad(h.chi_deg.value_or(0.0)), chi_axis) * base; return RotMatrix(rad(h.omega_deg.value_or(0.0)), base) * tilted; } diff --git a/reader/MiniCBF.cpp b/reader/MiniCBF.cpp index 14d6b8208..3b4626b03 100644 --- a/reader/MiniCBF.cpp +++ b/reader/MiniCBF.cpp @@ -194,6 +194,8 @@ void ParseAxisTable(const std::string &text, Header &h) { h.spindle_axis = v; else if (equipment == "detector") h.detector_axis = v; + } else if ((type == "rotation") && (equipment == "goniometer")) { + h.inner_spindle_axis = v; // carried by the base axis: a fixed-chi or kappa inclination } } diff --git a/reader/MiniCBF.h b/reader/MiniCBF.h index dd8cb92a1..9cb3fd96d 100644 --- a/reader/MiniCBF.h +++ b/reader/MiniCBF.h @@ -54,6 +54,11 @@ struct Header { std::optional> fast_direction; // laboratory direction of a +1 column step std::optional> slow_direction; // ... of a +1 row step std::optional> spindle_axis; // the base goniometer rotation axis + // The goniometer rotation axis carried by the base one, stated in the base axis' own frame. A + // head whose inner circle is INCLINED - a fixed-chi stage, a kappa arm - says so only here: its + // "# Chi" and "# Kappa" lines read zero because no such circle is driven. Empty where the table + // states one rotation axis only, which is every Eulerian head. + std::optional> inner_spindle_axis; std::optional> detector_axis; // the detector's own rotation axis: a 2theta arm // "# Oscillation_axis X.CW +SLOW". Some writers say which of the two image directions the spindle // runs along instead of stating a vector. The axis NAME on that line is a poor discriminator (see