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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
This commit is contained in:
leonarski_fandClaude Opus 5 committed 2026-09-13 15:02:04 +02:00
1 parent b243fa21d3
commit e3da313c84
3 files changed
+15 -3

No files matched your search

+8 -3
View File
@@ -152,16 +152,21 @@ std::optional<DetectorOrientation> 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<float>(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;
}
+2
View File
@@ -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
}
}
+5
View File
@@ -54,6 +54,11 @@ struct Header {
std::optional<std::array<double, 3>> fast_direction; // laboratory direction of a +1 column step
std::optional<std::array<double, 3>> slow_direction; // ... of a +1 row step
std::optional<std::array<double, 3>> 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<std::array<double, 3>> inner_spindle_axis;
std::optional<std::array<double, 3>> 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