reader: take a miniCBF's mounting from the imgCIF axis table its header states
A miniCBF header states three things about how the instrument is put together that the reader was assuming instead: which laboratory direction the image's columns run along, which its rows run along, and which the spindle turns about. Some beamlines append a CBF template block holding the full imgCIF axis table, which says all three outright. Two instruments in the corpus are not what was assumed, in two different ways. One mounts its detector a quarter turn round, so the image's columns run vertically. Another turns its spindle about the VERTICAL, with the image mounted the usual way; its table says so, and its "# Oscillation_axis" line says so a second way, by naming the image direction the spindle runs along rather than a vector. Either error leaves the spindle 90 degrees from the image. That is not a sign, so the run's axis-sign rescue cannot reach it, and no refinement recovers it: all three affected sweeps indexed nothing usable. So the table is read. The element axes give the image orientation, matched against the eight discrete mountings exactly as the NXmx module directions already are - the match itself moves to DetectorOrientation, so both readers share one definition rather than two copies. The goniometer axis with no parent gives the spindle DIRECTION; its sign stays the rescue's business, which is the part a convention can legitimately differ on. The detector axis with no parent gives the 2theta arm, replacing the assumption that the arm shares the spindle's axis - the one header stating both states them with the same vector, so this changes no answer, only what it rests on. imgCIF's frame differs from the internal one by a half turn about x, a rotation and not a mirror, as writer/HDF5NXmx.cpp already records from the other side. Where a header carries no table, a "+SLOW" on the Oscillation_axis line still says the spindle runs along the image's slow direction. That is the only thing one of the three affected sets says about it. The axis NAME on that line stays unusable - the header that carries both says "X.CW" where its own table says Y - but the direction token is not: where both are present they agree, which is what makes reading it evidence rather than a guess. Also: naming a frame with no directory at all now finds its sweep. parent_path() of a bare filename is empty and iterating an empty path finds nothing, so running from inside the data directory reported that no images were found. Measured, with nothing on the command line. The vertical-spindle protein set goes from no usable lattice to 100% indexed, P 6(3) 2 2 with a cell 0.43% from deposited, 87846 reflections at 86.3% completeness and CC(1/2) 0.995. Its companion from the same detector, which has no table and only the +SLOW token, goes from a spurious monoclinic cell at 2.3% completeness and I/sigma 0.21 to the right orthorhombic lattice, 97.7% indexed, 59.7% complete, CC(1/2) 0.996. The quarter-turned set's three sweeps, at three arm positions, now all index without the hand-passed quarter turn they needed and agree on one cell to 0.03 A. Six miniCBF sets that state no table and no +SLOW - including one whose Oscillation_axis line names an axis in a third dialect - are byte-identical in .hkl, .mtz, .cif and the image statistics, as are two NXmx sets, which is the shared orientation matcher moving nothing on that path either. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3yNBXk4wKdMZy1ak2NY7f
This commit is contained in:
@@ -8,7 +8,9 @@
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <regex>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "../common/JFJochException.h"
|
||||
|
||||
@@ -48,6 +50,133 @@ std::optional<double> Angle(const std::string &text, const char *pattern) {
|
||||
return v;
|
||||
}
|
||||
|
||||
// One "loop_" of the imgCIF template block these headers carry, as rows keyed by tag. Tags and values
|
||||
// are both read as whitespace-separated tokens rather than by line, because a template packs several
|
||||
// tags onto one line ("_axis.vector[1] _axis.vector[2] _axis.vector[3]") and the rows that follow are
|
||||
// laid out to match the tags, not the lines.
|
||||
std::vector<std::map<std::string, std::string>> ParseLoop(const std::string &text, const std::string &tag) {
|
||||
// The loop_ that introduces the tag, not the tag's own position: everything before it is another
|
||||
// loop's data.
|
||||
const size_t tag_at = text.find("\n" + tag);
|
||||
if (tag_at == std::string::npos)
|
||||
return {};
|
||||
const size_t loop_at = text.rfind("loop_", tag_at);
|
||||
if (loop_at == std::string::npos)
|
||||
return {};
|
||||
|
||||
// Bare tokens, and the quoted ones a CIF value may be - a quoted value holding spaces would
|
||||
// otherwise be counted as several columns and shift every row after it.
|
||||
std::vector<std::string> tokens;
|
||||
for (size_t i = loop_at + 5; i < text.size();) {
|
||||
while ((i < text.size()) && std::isspace(static_cast<unsigned char>(text[i])))
|
||||
i++;
|
||||
if (i >= text.size())
|
||||
break;
|
||||
size_t end;
|
||||
if ((text[i] == '\'') || (text[i] == '"')) {
|
||||
end = text.find(text[i], i + 1);
|
||||
if (end == std::string::npos)
|
||||
break;
|
||||
tokens.push_back(text.substr(i + 1, end - i - 1));
|
||||
end++;
|
||||
} else {
|
||||
end = i;
|
||||
while ((end < text.size()) && !std::isspace(static_cast<unsigned char>(text[end])))
|
||||
end++;
|
||||
tokens.push_back(text.substr(i, end - i));
|
||||
}
|
||||
// A second loop_, or a tag belonging to another category, ends this one.
|
||||
if ((tokens.back() == "loop_")
|
||||
|| (tokens.back().starts_with("_") && !tokens.back().starts_with(tag.substr(0, tag.find('.') + 1)))) {
|
||||
tokens.pop_back();
|
||||
break;
|
||||
}
|
||||
i = end;
|
||||
}
|
||||
|
||||
std::vector<std::string> names;
|
||||
size_t first_value = 0;
|
||||
while ((first_value < tokens.size()) && tokens[first_value].starts_with("_"))
|
||||
names.push_back(tokens[first_value++]);
|
||||
if (names.empty())
|
||||
return {};
|
||||
|
||||
std::vector<std::map<std::string, std::string>> rows;
|
||||
for (size_t i = first_value; i + names.size() <= tokens.size(); i += names.size()) {
|
||||
std::map<std::string, std::string> row;
|
||||
for (size_t j = 0; j < names.size(); j++)
|
||||
row[names[j]] = tokens[i + j];
|
||||
rows.push_back(std::move(row));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
std::string Field(const std::map<std::string, std::string> &row, const std::string &name) {
|
||||
const auto it = row.find(name);
|
||||
return (it == row.end()) ? std::string() : it->second;
|
||||
}
|
||||
|
||||
// The imgCIF axis table: which axis turns or translates in which laboratory direction, and which two
|
||||
// axes the image's columns and rows run along. Absent from most headers, which say nothing about any
|
||||
// of this and are left exactly as they were read before.
|
||||
//
|
||||
// The element vectors are stated in the frame of the axis they depend on. Between them and the
|
||||
// detector's own rotation every header seen has translations only, so they describe the image in the
|
||||
// unswung detector frame - which is where the image orientation belongs, with the arm applied on top.
|
||||
void ParseAxisTable(const std::string &text, Header &h) {
|
||||
const auto axes = ParseLoop(text, "_axis.id");
|
||||
if (axes.empty())
|
||||
return;
|
||||
|
||||
std::map<std::string, std::array<double, 3>> vector_of;
|
||||
for (const auto &row: axes) {
|
||||
const std::string id = Field(row, "_axis.id");
|
||||
const std::string type = Field(row, "_axis.type");
|
||||
const std::string equipment = Field(row, "_axis.equipment");
|
||||
const std::string depends_on = Field(row, "_axis.depends_on");
|
||||
std::array<double, 3> v{};
|
||||
try {
|
||||
for (int i = 0; i < 3; i++)
|
||||
v[i] = std::stod(Field(row, "_axis.vector[" + std::to_string(i + 1) + "]"));
|
||||
} catch (const std::exception &) {
|
||||
continue; // "." for a vector: the table states no direction for this axis
|
||||
}
|
||||
vector_of[id] = v;
|
||||
|
||||
// The base spindle and the detector arm are the rotations that hang off nothing: everything
|
||||
// further in is carried by them. Naming neither, so a beamline is free to call them anything.
|
||||
if ((type == "rotation") && (depends_on == ".")) {
|
||||
if (equipment == "goniometer")
|
||||
h.spindle_axis = v;
|
||||
else if (equipment == "detector")
|
||||
h.detector_axis = v;
|
||||
}
|
||||
}
|
||||
|
||||
// Which axis the fast index runs along, and which the slow, through the two tables that say so.
|
||||
std::map<std::string, std::string> axis_of_set;
|
||||
for (const auto &row: ParseLoop(text, "_array_structure_list_axis.axis_set_id"))
|
||||
axis_of_set[Field(row, "_array_structure_list_axis.axis_set_id")]
|
||||
= Field(row, "_array_structure_list_axis.axis_id");
|
||||
|
||||
for (const auto &row: ParseLoop(text, "_array_structure_list.array_id")) {
|
||||
const std::string set = Field(row, "_array_structure_list.axis_set_id");
|
||||
const auto id = axis_of_set.contains(set) ? axis_of_set[set] : set;
|
||||
const auto it = vector_of.find(id);
|
||||
if (it == vector_of.end())
|
||||
continue;
|
||||
std::array<double, 3> v = it->second;
|
||||
if (Field(row, "_array_structure_list.direction") == "decreasing")
|
||||
for (double &c: v)
|
||||
c = -c;
|
||||
const std::string index = Field(row, "_array_structure_list.index");
|
||||
if (index == "1")
|
||||
h.fast_direction = v;
|
||||
else if (index == "2")
|
||||
h.slow_direction = v;
|
||||
}
|
||||
}
|
||||
|
||||
int16_t ReadI16(const uint8_t *p) { int16_t v; std::memcpy(&v, p, 2); return v; }
|
||||
int32_t ReadI32(const uint8_t *p) { int32_t v; std::memcpy(&v, p, 4); return v; }
|
||||
int64_t ReadI64(const uint8_t *p) { int64_t v; std::memcpy(&v, p, 8); return v; }
|
||||
@@ -99,6 +228,11 @@ Header ParseHeader(const char *data, size_t size) {
|
||||
h.period_s = Num(t, R"(#\s*Exposure_period\s+([\d.eE+-]+))");
|
||||
h.count_cutoff = Int(t, R"(#\s*Count_cutoff\s+(\d+))");
|
||||
h.axis_name = Match(t, R"(#\s*Oscillation_axis\s+(\S+))").value_or("omega");
|
||||
// "+SLOW" / "+FAST" on that same line: which of the image's two directions the spindle runs
|
||||
// along. Some writers state that instead of an axis name, and it is the only thing a header with
|
||||
// no axis table says about the spindle's direction at all.
|
||||
h.spindle_along_slow = Match(t, R"(#\s*Oscillation_axis[^\r\n]*\+(SLOW|slow))").has_value();
|
||||
ParseAxisTable(t, h);
|
||||
|
||||
// "# Silicon sensor, ..." / "# CdTe sensor, ...". The rest of the code compares the material
|
||||
// against "CdTe" (BraggIntegrationEngine), so an unnormalised "Silicon" would silently give a
|
||||
|
||||
Reference in New Issue
Block a user