From ba5b7cda81e04d16987aaba2f1d319e3db3decab Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sun, 20 Sep 2026 19:02:12 +0200 Subject: [PATCH] Do not let a marCCD file name 2-theta as the scanned circle The scanned circle was read as the first start/end pair that differs, and two_theta is the first pair in the goniostat block. A file that leaves its end block at zero while the detector sits on a swung-out 2-theta arm therefore named the arm as the spindle and stopped before reaching phi; with no rotation range recorded either, the increment fell back to -start_angle, which differs per frame, and the sweep was refused outright. The header's own rotation_axis index (offset 732) is taken where it names a spindle circle, and the pair scan is the fallback for files that never filled it in - read past two_theta, which no sweep turns. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_013nW6FNRP1bBJJ8pfHiByAT --- reader/MarCCD.cpp | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/reader/MarCCD.cpp b/reader/MarCCD.cpp index 96af47893..68ac7c171 100644 --- a/reader/MarCCD.cpp +++ b/reader/MarCCD.cpp @@ -38,6 +38,7 @@ constexpr size_t OFF_END_XTAL_TO_DETECTOR = 728; // ... and at its end constexpr size_t OFF_EXPOSURE_TIME = 656; // milliseconds constexpr size_t OFF_START_TWOTHETA = 668; // millidegrees, and the seven that follow it constexpr size_t OFF_END_TWOTHETA = 700; // the same seven, at the end of the exposure +constexpr size_t OFF_ROTATION_AXIS = 732; // which circle the sweep turned, as an index below constexpr size_t OFF_ROTATION_RANGE = 736; // millidegrees constexpr size_t OFF_PIXELSIZE_X = 772; // nanometres constexpr size_t OFF_PIXELSIZE_Y = 776; @@ -146,21 +147,33 @@ Header Parse(const std::vector &h, const std::string &path) { out.exposure_s = I32(h, OFF_EXPOSURE_TIME) * 1e-3; out.saturated_value = I32(h, OFF_SATURATED); - // Which circle moved. The header also carries a rotation_axis index, but the start/end pair - // that differs is the same answer read off the angles themselves, and it stays right on a file - // whose index field was never filled in. + // Which circle moved. The header names it as an index into the circle list, and that is taken + // where it names a spindle circle: reading the start/end pairs alone takes the FIRST pair that + // differs, and two_theta is the first of them, so a file that left its end block at zero while + // the detector sat on a swung-out 2-theta arm had the arm named as the spindle - and, with no + // rotation range recorded either, an increment of -start_angle that differs on every frame, + // which SweepLayout then refuses the whole sweep over. + // + // The index is not always filled in, and a file that leaves it at zero cannot be told from one + // that means two_theta by it, so the fallback is the pairs as before - read past two_theta, + // which is a detector circle and never the one a sweep turns. out.angle_increment_deg = I32(h, OFF_ROTATION_RANGE) * 1e-3; - for (size_t i = 0; i < N_CIRCLES; i++) { + const auto take_circle = [&](size_t i) { const double start = I32(h, OFF_START_TWOTHETA + 4 * i) * 1e-3; const double end = I32(h, OFF_END_TWOTHETA + 4 * i) * 1e-3; - if (start != end) { - out.axis_name = CIRCLE_NAMES[i]; - out.start_angle_deg = start; - if (out.angle_increment_deg == 0.0) - out.angle_increment_deg = end - start; - break; - } - } + if (start == end) + return false; + out.axis_name = CIRCLE_NAMES[i]; + out.start_angle_deg = start; + if (out.angle_increment_deg == 0.0) + out.angle_increment_deg = end - start; + return true; + }; + const int32_t named = I32(h, OFF_ROTATION_AXIS); + if (named <= 0 || static_cast(named) >= N_CIRCLES || !take_circle(static_cast(named))) + for (size_t i = 1; i < N_CIRCLES; i++) + if (take_circle(i)) + break; return out; }