reader: place a detector swung out on a 2theta arm where the file says it stands
Chemical crystallography reaches high angle by swinging the detector out on a 2theta arm. Both readers had the number and neither used it: the miniCBF header's Detector_2theta was parsed into a struct member nothing ever read, and on the NXmx side the rotation was in the depends_on chain, which was not followed at all. A sweep taken at 30 degrees was therefore processed with its detector plane 30 degrees from where it stood, and nothing indexed. The geometry could already express it, and needed no change: the arm turns the detector about the sample, so the distance is still measured along the detector normal and the beam centre is still the point of normal incidence - which is exactly the PONI convention, and a swung detector is one PONI rotation. What moves is the direct beam, by distance*tan(2theta), off the beam centre and often off the detector. NXmx is the harder half, because the swing has no field of its own: it is one rotation in the chain the detector's position depends on, and "two_theta" is only one beamline's name for that dataset. So the chain is followed and its rotations composed, rather than a field of one name being looked for - each transformation states its vector in the frame of the one it depends on, which is why the product is the whole placement. Translations are skipped; they are the distance and the beam centre, which the file states separately in the square-on frame. Vectors come from McStas through the same 180-degree turn about z the module directions already use, a proper rotation, so an axis carried through it turns the same way. The three rotations a file this system writes ARE that chain, and are also read as the PONI angles - so those three paths are skipped, or every tilted file we have ever written would come back tilted twice. That is the one way this change could have broken existing data, and the test for it writes a tilted file and reads it back. For miniCBF the arm turns about the base spindle axis: on the four-circle geometry those headers describe the two are one axis, and the imgCIF axis table such a header carries states them with the same vector. Both now come from one constant, so a later correction to the frame moves them together. Measured. On a swung NXmx sweep the chain gives rot2 = -0.34907 rad for the 20 degrees it states, and the sweep goes from "nothing was integrated" to 25000 reflections at 82.2% completeness and CC(1/2) 0.9993, in the same space group and the same cell to 0.03 A as the square-on sweep of that crystal; the opposite sign indexes nothing. A miniCBF sweep at 30 degrees goes the same way, to 0.585 A, and a second sweep of that crystal at 55 degrees reaches 0.476 A and reproduces the cell again - with a low-resolution limit of 2.36 A rather than 13 A, which is what a detector swung that far records. On all of them post-refinement recovers the header's own beam centre and distance, and the beam stop shadow sits within four pixels of where the swung geometry puts the direct beam, 417 and 537 pixels from where the unswung one does. Seven sets whose detector is square to the beam, three of them carrying a chain whose 2theta is zero, are byte-identical in .hkl, .mtz, .cif and the image statistics. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3yNBXk4wKdMZy1ak2NY7f
This commit is contained in:
@@ -14,8 +14,14 @@
|
||||
#include "../common/Logger.h"
|
||||
#include "../common/ROIDefinition.h"
|
||||
|
||||
// A McStas direction in the internal frame. The two differ by a 180 degree turn about z, which is a
|
||||
// rotation and not a mirror - so an axis carried through it turns the same way by the same angle.
|
||||
static Coord McStasToInternal(const std::vector<double> &v) {
|
||||
return {static_cast<float>(-v[0]), static_cast<float>(-v[1]), static_cast<float>(v[2])};
|
||||
}
|
||||
|
||||
// The image orientation the file itself states, in its NXdetector_module pixel directions. NXmx gives
|
||||
// those in the McStas frame, which is the internal frame turned 180 degrees about z.
|
||||
// those in the McStas frame.
|
||||
//
|
||||
// Only an exact match against one of the eight discrete orientations is taken. Anything else is a
|
||||
// continuous rotation of the detector in its own plane, which belongs in rot1/rot2/rot3 and cannot be
|
||||
@@ -36,8 +42,8 @@ static std::optional<DetectorOrientation> ReadModuleOrientation(HDF5Object *file
|
||||
if ((f.size() != 3) || (s.size() != 3))
|
||||
return {};
|
||||
|
||||
const Coord fast(-f[0], -f[1], f[2]);
|
||||
const Coord slow(-s[0], -s[1], s[2]);
|
||||
const Coord fast = McStasToInternal(f);
|
||||
const Coord slow = McStasToInternal(s);
|
||||
|
||||
for (int64_t quarter_turns = 0; quarter_turns < 4; quarter_turns++) {
|
||||
for (bool mirror_y: {false, true}) {
|
||||
@@ -50,6 +56,63 @@ static std::optional<DetectorOrientation> ReadModuleOrientation(HDF5Object *file
|
||||
return {};
|
||||
}
|
||||
|
||||
// Where the detector stands, from the chain of transformations the file says it depends on.
|
||||
//
|
||||
// NXmx has no field for a detector swung out on a 2theta arm. It states the detector's position as a
|
||||
// depends_on chain and the arm is one rotation in that chain, so following the chain is the only way
|
||||
// to find it: "two_theta" is one beamline's name for that dataset and the next spells it otherwise.
|
||||
//
|
||||
// Only the rotations are taken, composed from the detector outwards. Each transformation states its
|
||||
// vector in the frame of the one it depends on, so the product is the rotation that carries a
|
||||
// detector square to the beam to where this one stands. The translations in the chain are the
|
||||
// detector distance and the beam centre, which the file states separately in that square-on frame -
|
||||
// the arm turns the detector about the sample and moves neither, and a Diamond master writes the same
|
||||
// beam_center_x/y for a swung sweep as for the square-on one beside it. Nothing comes back when no
|
||||
// rotation in the chain turns, which is every detector square to the beam.
|
||||
static std::optional<RotMatrix> ReadDetectorRotationChain(HDF5Object *file) {
|
||||
std::string node = file->GetString("/entry/instrument/detector/depends_on");
|
||||
if (node.empty() && file->IsDataSet("/entry/instrument/detector/module/module_offset")) {
|
||||
HDF5DataSet module_offset(*file, "/entry/instrument/detector/module/module_offset");
|
||||
if (module_offset.AttrExists("depends_on"))
|
||||
node = module_offset.ReadAttrStr("depends_on");
|
||||
}
|
||||
|
||||
// A file this system wrote states its PONI angles in the chain as well, and they are read from
|
||||
// these three paths just before this is called. Taking them here too would apply the tilt twice.
|
||||
static const std::set<std::string> poni_angles = {"/entry/instrument/detector/transformations/rot1",
|
||||
"/entry/instrument/detector/transformations/rot2",
|
||||
"/entry/instrument/detector/transformations/rot3"};
|
||||
|
||||
RotMatrix chain;
|
||||
bool turns = false;
|
||||
std::set<std::string> seen;
|
||||
while ((node != ".") && !node.empty() && file->IsDataSet(node) && seen.insert(node).second) {
|
||||
HDF5DataSet axis(*file, node);
|
||||
const std::string current = node;
|
||||
node = axis.AttrExists("depends_on") ? axis.ReadAttrStr("depends_on") : ".";
|
||||
|
||||
if (poni_angles.contains(current) || !axis.AttrExists("transformation_type") || !axis.AttrExists("vector")
|
||||
|| (axis.ReadAttrStr("transformation_type") != "rotation"))
|
||||
continue;
|
||||
|
||||
std::vector<double> value;
|
||||
axis.ReadVector(value);
|
||||
const auto vec = axis.ReadAttrVec("vector");
|
||||
if (value.empty() || (value[0] == 0.0) || (vec.size() != 3))
|
||||
continue;
|
||||
|
||||
// NXmx states a rotation in degrees unless it says otherwise.
|
||||
const bool radians = axis.AttrExists("units") && (axis.ReadAttrStr("units") == "rad");
|
||||
const auto angle_rad = static_cast<float>(radians ? value[0] : value[0] * PI / 180.0);
|
||||
chain = RotMatrix(angle_rad, McStasToInternal(vec)) * chain;
|
||||
turns = true;
|
||||
}
|
||||
|
||||
if (!turns)
|
||||
return {};
|
||||
return chain;
|
||||
}
|
||||
|
||||
|
||||
inline std::pair<gemmi::CrystalSystem, char> parse_bravais_lattice(const std::string &val) {
|
||||
if (val.empty())
|
||||
@@ -721,6 +784,21 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen
|
||||
master_file->GetOptFloat("/entry/instrument/detector/transformations/rot2").value_or(0.0));
|
||||
dataset->experiment.PoniRot3_rad(
|
||||
master_file->GetOptFloat("/entry/instrument/detector/transformations/rot3").value_or(0.0));
|
||||
// A detector swung out on a 2theta arm - routine in chemical crystallography - and any other
|
||||
// rotation the file puts in the detector's chain. It turns the detector about the sample, so
|
||||
// it carries the whole square-on geometry with it and composes on the left of the PONI
|
||||
// rotation the file states directly.
|
||||
if (const auto chain = ReadDetectorRotationChain(master_file.get())) {
|
||||
float rot1 = 0, rot2 = 0, rot3 = 0;
|
||||
PoniAnglesFromMatrix(chain.value()
|
||||
* PoniRotMatrix(dataset->experiment.GetPoniRot1_rad(),
|
||||
dataset->experiment.GetPoniRot2_rad(),
|
||||
dataset->experiment.GetPoniRot3_rad()),
|
||||
rot1, rot2, rot3);
|
||||
dataset->experiment.PoniRot1_rad(rot1).PoniRot2_rad(rot2).PoniRot3_rad(rot3);
|
||||
Logger("HDF5Reader").Info("Detector placed by its NXmx transformation chain: "
|
||||
"rot1 {:.5f} rot2 {:.5f} rot3 {:.5f} rad", rot1, rot2, rot3);
|
||||
}
|
||||
dataset->experiment.SampleTemperature_K(master_file->GetOptFloat("/entry/sample/temperature"));
|
||||
|
||||
dataset->experiment.BeamX_pxl(master_file->GetFloat("/entry/instrument/detector/beam_center_x"));
|
||||
|
||||
Reference in New Issue
Block a user