reader: read the detector arm and the mounting each file states
A detector swung out on a 2theta arm was read as if it stood square on, and a miniCBF written with a vertical spindle or a quarter-turned image was read with the standard mounting assumed. Both are stated in the file and both were ignored: the NXmx depends_on chain was never followed, and Detector_2theta was parsed into a field nothing read. Recovers three datasets that produced no usable lattice, and the small-molecule sweeps at 30 and 55 degrees. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3yNBXk4wKdMZy1ak2NY7f
This commit is contained in:
@@ -46,6 +46,18 @@ RotMatrix DetectorOrientation::Matrix() const {
|
||||
return {rz_x, mirror_y ? -rz_y : rz_y, {0, 0, 1}};
|
||||
}
|
||||
|
||||
std::optional<DetectorOrientation> DetectorOrientation::Match(const Coord &fast, const Coord &slow) {
|
||||
for (int64_t quarter_turns = 0; quarter_turns < 4; quarter_turns++) {
|
||||
for (const bool mirror_y: {false, true}) {
|
||||
const DetectorOrientation orientation(mirror_y, quarter_turns);
|
||||
const RotMatrix m = orientation.Matrix();
|
||||
if (((m.Column(0) - fast).Length() < 1e-3f) && ((m.Column(1) - slow).Length() < 1e-3f))
|
||||
return orientation;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool DetectorOrientation::operator==(const DetectorOrientation &other) const {
|
||||
return (mirror_y == other.mirror_y) && (quarter_turns == other.quarter_turns);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "Coord.h"
|
||||
|
||||
// How the stored image is laid out in the detector plane: mirrored in Y, and/or turned by a multiple
|
||||
@@ -38,5 +40,11 @@ public:
|
||||
// Rz(quarter_turns * 90 deg) * diag(1,-1,1)^mirror_y. Entries are exactly 0 and +-1.
|
||||
[[nodiscard]] RotMatrix Matrix() const;
|
||||
|
||||
// The orientation whose fast and slow axes are the two directions given, or nothing when they are
|
||||
// not one of the eight. Nothing means the image is turned in its own plane by something that is
|
||||
// not a multiple of 90 degrees, which is a continuous rotation of the detector and belongs in the
|
||||
// PONI angles - it cannot be told apart from the tilt by looking at the two directions alone.
|
||||
[[nodiscard]] static std::optional<DetectorOrientation> Match(const Coord &fast, const Coord &slow);
|
||||
|
||||
bool operator==(const DetectorOrientation &other) const;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# Changelog
|
||||
## 1.0.0
|
||||
### 1.0.0-rc.166
|
||||
* A miniCBF sweep takes the mounting from the imgCIF axis table its header carries: which laboratory direction the image's columns and rows run along, and which the spindle turns about. Where there is no table, a `+SLOW` on the `Oscillation_axis` line still says the spindle runs along the image's slow direction. All three were previously assumed, and two instruments are not what was assumed.
|
||||
* Naming a miniCBF frame with no directory - a frame in the working directory - finds its sweep instead of reporting that no images were found.
|
||||
* A detector swung out on a 2theta arm is placed where it stands, from the depends_on transformation chain of an NXmx master or the `Detector_2theta` line of a miniCBF header; both were previously read and then ignored.
|
||||
* `jfjoch_viewer` opens PILATUS miniCBF sweeps - naming any frame opens the whole sweep - and can run a processing job on one.
|
||||
* A detector whose stored image is mirrored in Y or mounted at a multiple of 90 degrees can be described as such, in the detector configuration or with `--detector-mirror-y` / `--detector-quarter-turns`, rather than having to be expressed as a detector rotation.
|
||||
* The rotation first pass refines twelve candidate lattices rather than four, so a correct cell that the pre-refinement ranking put fifth is still reached.
|
||||
|
||||
@@ -59,6 +59,45 @@ rot2 = asin(-slow.z) rot1 = atan2(-fast.z, normal.z) rot3 = atan
|
||||
with `rot2` in [-90°, 90°]. The angles are what is stored and what is written out, so a geometry
|
||||
given as angles comes back exactly as it was given.
|
||||
|
||||
## What a miniCBF header states about the mounting
|
||||
|
||||
A PILATUS miniCBF gives the geometry twice. The `# ` lines every writer produces carry the distance, the
|
||||
beam centre and the angles; some beamlines then append a CBF template block holding a full **imgCIF axis
|
||||
table**, which states the laboratory direction of the image's fast and slow pixel directions, of the base
|
||||
goniometer axis, and of a 2theta arm where there is one. Where that table is present it is read, in
|
||||
preference to any assumption - it is the same information NXmx puts in `fast_pixel_direction` /
|
||||
`slow_pixel_direction` and the goniometer `vector`, in the form this format states it.
|
||||
|
||||
imgCIF's laboratory frame has Z from the sample towards the source and Y opposite gravity, so it differs
|
||||
from the internal frame by a half turn about x - a rotation, not a mirror, so an axis carried through it
|
||||
turns the same way by the same angle.
|
||||
|
||||
Two things a header can state that an assumption gets wrong by 90 degrees, which no refinement recovers
|
||||
and which the run's axis-sign rescue cannot reach either, a quarter turn not being a sign:
|
||||
|
||||
* the image mounted a quarter turn round, so its columns run vertically;
|
||||
* a spindle that turns about the **vertical** rather than the horizontal.
|
||||
|
||||
Where a header carries no axis table, a `+SLOW` on its `# Oscillation_axis` line still says the spindle
|
||||
runs along the image's slow direction rather than its fast one. The axis *name* on that line is not
|
||||
usable - one header says `X.CW +SLOW` where its own table says the axis is Y - but the direction token is,
|
||||
and on the header that states both they agree.
|
||||
|
||||
## A detector swung out on a 2theta arm
|
||||
|
||||
Chemical crystallography reaches high angle by swinging the detector out on a 2theta arm rather than by
|
||||
moving it closer. The arm turns the detector about the sample, so it changes nothing else: the distance
|
||||
is still measured along the detector normal, and the beam centre is still the point of normal incidence,
|
||||
which is where the arm's own axis meets the detector and does not move. The swing is therefore exactly a
|
||||
PONI rotation, and the direct beam is what moves - by `distance * tan(2theta)`, off the beam centre and
|
||||
often off the detector altogether.
|
||||
|
||||
Nothing has to be given for this: rugnux takes it from the file. An NXmx master states the detector's
|
||||
position as a `depends_on` chain of transformations, and the arm is one rotation in that chain - so the
|
||||
chain is followed, rather than a field of one particular name being looked for. A PILATUS miniCBF states
|
||||
it as `# Detector_2theta`, which turns about the same axis as the base spindle, the two being one axis on
|
||||
the four-circle geometry those headers describe.
|
||||
|
||||
## Mirrored and quarter-turned detectors
|
||||
|
||||
On top of the continuous tilt the detector setup carries a **discrete image orientation**: whether the
|
||||
|
||||
@@ -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,18 +42,64 @@ 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]);
|
||||
return DetectorOrientation::Match(McStasToInternal(f), McStasToInternal(s));
|
||||
}
|
||||
|
||||
for (int64_t quarter_turns = 0; quarter_turns < 4; quarter_turns++) {
|
||||
for (bool mirror_y: {false, true}) {
|
||||
const DetectorOrientation orientation(mirror_y, quarter_turns);
|
||||
const RotMatrix m = orientation.Matrix();
|
||||
if (((m.Column(0) - fast).Length() < 1e-3f) && ((m.Column(1) - slow).Length() < 1e-3f))
|
||||
return orientation;
|
||||
}
|
||||
// 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");
|
||||
}
|
||||
return {};
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -721,6 +773,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"));
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
|
||||
@@ -57,7 +58,12 @@ std::optional<Template> TemplateOf(const std::string &filename) {
|
||||
std::vector<std::string> CollectSweep(const std::string &path) {
|
||||
std::filesystem::path p(path);
|
||||
const bool is_dir = std::filesystem::is_directory(p);
|
||||
const std::filesystem::path dir = is_dir ? p : p.parent_path();
|
||||
// A frame named with no directory at all is in this one - parent_path() of a bare filename is
|
||||
// empty, and iterating an empty path finds nothing, so naming a frame from inside its own
|
||||
// directory found no sweep.
|
||||
std::filesystem::path dir = is_dir ? p : p.parent_path();
|
||||
if (dir.empty())
|
||||
dir = ".";
|
||||
|
||||
// Naming a frame selects ITS sweep. Naming a directory selects the sweep with the most frames in
|
||||
// it, which is the one a user pointing at a data directory means.
|
||||
@@ -90,14 +96,54 @@ std::vector<std::string> CollectSweep(const std::string &path) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// An imgCIF laboratory direction in the internal frame. imgCIF puts Z from the sample towards the
|
||||
// source and Y opposite gravity, while the internal frame has z along the beam and y along increasing
|
||||
// row, so the two differ by a half turn about x - a rotation and not a mirror, so an axis carried
|
||||
// through it turns the same way by the same angle. (writer/HDF5NXmx.cpp states the same relation from
|
||||
// the other side, where it separates this from the McStas one, which is a half turn about z.)
|
||||
Coord ImgCIFToInternal(const std::array<double, 3> &v) {
|
||||
return {static_cast<float>(v[0]), static_cast<float>(-v[1]), static_cast<float>(-v[2])};
|
||||
}
|
||||
|
||||
// The base rotation axis where a header states nothing about it, in the internal frame (x along
|
||||
// increasing detector column, y along increasing row, z along the beam). It is a convention: such a
|
||||
// header names its rotation axis but gives no direction, so this is the sign an NXmx master writes
|
||||
// for the same instruments, and a file that needs the other one is settled from the data by the run's
|
||||
// axis-sign rescue.
|
||||
const Coord ASSUMED_BASE_AXIS(-1.0f, 0.0f, 0.0f);
|
||||
|
||||
// The base rotation axis of the instrument, in the internal frame.
|
||||
//
|
||||
// Two headers in every corpus examined here state it and were being overruled by the assumption. One
|
||||
// beamline's PILATUS turns about the VERTICAL: its axis table says so outright, and its "# Oscillation
|
||||
// _axis" line says so a second way, by naming the image direction the spindle runs along rather than a
|
||||
// vector. Assuming the horizontal axis put the spindle 90 degrees out - which no amount of refinement
|
||||
// recovers, and which a sign rescue cannot reach either, since it is not a sign - and the run indexed
|
||||
// nothing. The sign is still the rescue's business; the DIRECTION is the file's.
|
||||
Coord BaseAxis(const minicbf::Header &h) {
|
||||
if (h.spindle_axis.has_value())
|
||||
return ImgCIFToInternal(*h.spindle_axis);
|
||||
if (h.spindle_along_slow)
|
||||
return {0.0f, -1.0f, 0.0f}; // minus the slow direction, as the default is minus the fast
|
||||
return ASSUMED_BASE_AXIS;
|
||||
}
|
||||
|
||||
// How the stored image sits in the detector plane, where the header's axis table states it - the same
|
||||
// thing the NXmx module directions say, in the form this format says it. Nothing comes back when the
|
||||
// header carries no table, or when what it states is not one of the eight discrete orientations.
|
||||
std::optional<DetectorOrientation> ImageOrientation(const minicbf::Header &h) {
|
||||
if (!h.fast_direction.has_value() || !h.slow_direction.has_value())
|
||||
return {};
|
||||
return DetectorOrientation::Match(ImgCIFToInternal(*h.fast_direction),
|
||||
ImgCIFToInternal(*h.slow_direction));
|
||||
}
|
||||
|
||||
// The axis a miniCBF sweep turns about, in the internal frame (x along increasing detector column,
|
||||
// y along increasing row, z along the beam).
|
||||
//
|
||||
// The base axis is a convention: a miniCBF names its rotation axis but never states a direction, so
|
||||
// this is the sign an NXmx master writes for the same instruments, and a file that needs the other
|
||||
// one is settled from the data by the run's axis-sign rescue. The sense of the omega rotation below
|
||||
// follows that same convention, so a sweep parked at a non-zero omega inherits whichever sign the
|
||||
// base axis turns out to have.
|
||||
// The base axis comes from the header where it states one (BaseAxis above) and is otherwise assumed.
|
||||
// The sense of the omega rotation below follows the base axis, so a sweep parked at a non-zero omega
|
||||
// inherits whichever direction it turns out to have.
|
||||
//
|
||||
// The head is base -> chi -> phi, so only the axes OUTSIDE the scanned one can tilt it. An omega
|
||||
// scan turns about the base axis however the cradle is set - which is why a header carrying a large
|
||||
@@ -106,7 +152,7 @@ std::vector<std::string> CollectSweep(const std::string &path) {
|
||||
// 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.
|
||||
Coord RotationAxis(const minicbf::Header &h) {
|
||||
const Coord base(-1.0f, 0.0f, 0.0f);
|
||||
const Coord base = BaseAxis(h);
|
||||
if (!minicbf::ScansPhi(h))
|
||||
return base;
|
||||
|
||||
@@ -153,6 +199,8 @@ void JFJochCBFReader::ReadFiles(const std::string &path) {
|
||||
// Images are handed out as signed 32-bit whatever the file stored, so that is the depth the rest
|
||||
// of the code must see; the real overflow is the header's Count_cutoff, set above.
|
||||
detector.BitDepthImage(32);
|
||||
if (const auto orientation = ImageOrientation(header0_))
|
||||
detector.ImageOrientation(orientation.value());
|
||||
detector.MinFrameTime(std::chrono::microseconds(0));
|
||||
detector.MinCountTime(std::chrono::microseconds(0));
|
||||
detector.ReadOutTime(std::chrono::nanoseconds(0));
|
||||
@@ -161,6 +209,24 @@ void JFJochCBFReader::ReadFiles(const std::string &path) {
|
||||
dataset_->experiment.BeamX_pxl(static_cast<float>(header0_.beam_x_px));
|
||||
dataset_->experiment.BeamY_pxl(static_cast<float>(header0_.beam_y_px));
|
||||
dataset_->experiment.DetectorDistance_mm(static_cast<float>(header0_.distance_m * 1000.0));
|
||||
|
||||
// A detector swung out on a 2theta arm, which small-molecule collection uses routinely. The arm
|
||||
// turns the detector about the sample, so it carries the square-on geometry with it: the header's
|
||||
// Detector_distance stays the distance along the detector normal and Beam_xy stays the point of
|
||||
// normal incidence, neither of which the swing moves - which is exactly what the PONI convention
|
||||
// wants, so the swing is a PONI rotation and nothing else in the header changes. It turns about
|
||||
// the axis the header's table gives the arm, and otherwise about the base spindle axis: on the
|
||||
// four-circle geometry these headers describe the arm and the spindle are one axis, and the one
|
||||
// header here that states both states them with the same vector.
|
||||
if (header0_.two_theta_deg != 0.0) {
|
||||
const Coord axis = header0_.detector_axis.has_value()
|
||||
? ImgCIFToInternal(*header0_.detector_axis) : BaseAxis(header0_);
|
||||
float rot1 = 0, rot2 = 0, rot3 = 0;
|
||||
PoniAnglesFromMatrix(RotMatrix(static_cast<float>(header0_.two_theta_deg * PI / 180.0), axis),
|
||||
rot1, rot2, rot3);
|
||||
dataset_->experiment.PoniRot1_rad(rot1).PoniRot2_rad(rot2).PoniRot3_rad(rot3);
|
||||
}
|
||||
|
||||
dataset_->experiment.IncidentEnergy_keV(WVL_1A_IN_KEV / static_cast<float>(header0_.wavelength_A));
|
||||
dataset_->experiment.FrameTime(
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -44,6 +45,21 @@ struct Header {
|
||||
int64_t count_cutoff = 0; // saturation
|
||||
std::string axis_name = "omega";
|
||||
bool byte_offset = false; // the only conversion supported
|
||||
|
||||
// The imgCIF axis table the CBF template block carries after the "# " lines, as the file states
|
||||
// it: vectors in the imgCIF laboratory frame (Z from the sample towards the source, Y opposite
|
||||
// gravity, X completing a right-handed set). Turning them into any other frame is the caller's
|
||||
// business, not this format reader's. Most headers carry no such table and leave all of these
|
||||
// empty, which is not the same as their stating the usual mounting.
|
||||
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
|
||||
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
|
||||
// ScansPhi, and note that the one header carrying both a name and a table says "X" where its table
|
||||
// says Y) but this token is not: where both are present they agree.
|
||||
bool spindle_along_slow = false;
|
||||
};
|
||||
|
||||
// Whether the sweep turns PHI rather than the base (omega) axis. What moved is the axis with a
|
||||
|
||||
@@ -831,3 +831,48 @@ TEST_CASE("DetectorOrientation_recip_roundtrip") {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A detector swung out on a 2theta arm, which is how chemical crystallography reaches high angle.
|
||||
// The arm turns the detector about the sample, so the geometry that describes it is the PONI rotation
|
||||
// and nothing else moves: the distance stays the distance along the detector normal and the beam
|
||||
// centre stays the point of normal incidence. What DOES move is the direct beam, which is no longer
|
||||
// at the beam centre - the two coincide only on a detector square to the beam.
|
||||
TEST_CASE("DiffractionGeometry_TwoThetaArm", "[LinearAlgebra][Coord]") {
|
||||
const float two_theta = 30.0f * PI / 180.0f;
|
||||
const float distance_mm = 160.0f, pixel_mm = 0.172f, wavelength = 0.6889f;
|
||||
const float bx = 740.0f, by = 866.0f;
|
||||
|
||||
DiffractionGeometry geom;
|
||||
geom.BeamX_pxl(bx).BeamY_pxl(by).DetectorDistance_mm(distance_mm)
|
||||
.PixelSize_mm(pixel_mm).Wavelength_A(wavelength);
|
||||
// The arm turns about the internal x axis; a rotation of +2theta about it is rot2 = -2theta.
|
||||
geom.PoniRot2_rad(-two_theta);
|
||||
|
||||
// The beam centre pixel is the PONI: still on the detector normal through the sample, and now
|
||||
// 2theta away from the beam.
|
||||
CHECK(geom.TwoTheta_rad(bx, by) == Catch::Approx(two_theta));
|
||||
CHECK(geom.LabCoord(bx, by).Length() == Catch::Approx(distance_mm));
|
||||
CHECK(geom.GetNormalAxis() * Coord(0, 0, 1) == Catch::Approx(cosf(two_theta)));
|
||||
// The plane turned about x, so the fast axis - along +x - did not move, and the slow one tipped
|
||||
// out of the detector plane by the full 2theta.
|
||||
CHECK((geom.GetFastAxis() - Coord(1, 0, 0)).Length() < 1e-6f);
|
||||
CHECK(geom.GetSlowAxis() * Coord(0, 0, 1) == Catch::Approx(sinf(two_theta)));
|
||||
|
||||
// The direct beam is off the PONI by D*tan(2theta), along the direction the arm swung.
|
||||
auto [direct_x, direct_y] = geom.GetDirectBeam_pxl();
|
||||
CHECK(direct_x == Catch::Approx(bx));
|
||||
CHECK(direct_y == Catch::Approx(by + distance_mm * tanf(two_theta) / pixel_mm));
|
||||
|
||||
// Resolution at the PONI is the Bragg spacing of 2theta, not of a pixel at zero distance from
|
||||
// the beam centre - the reason a swung detector reaches so much further than a square-on one.
|
||||
CHECK(geom.PxlToRes(bx, by) == Catch::Approx(wavelength / (2.0f * sinf(two_theta / 2.0f))));
|
||||
|
||||
// Round trip through reciprocal space, at the PONI and away from it in both directions.
|
||||
const std::vector<std::pair<float, float>> probes =
|
||||
{{bx, by}, {bx + 300.0f, by - 500.0f}, {bx - 700.0f, by + 200.0f}};
|
||||
for (const auto &[x, y]: probes) {
|
||||
auto [back_x, back_y] = geom.RecipToDetector(geom.DetectorToRecip(x, y));
|
||||
CHECK(back_x == Catch::Approx(x));
|
||||
CHECK(back_y == Catch::Approx(y));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,14 @@
|
||||
#include "../common/ScanResultGenerator.h"
|
||||
#include "../writer/FileWriter.h"
|
||||
#include "../reader/JFJochHDF5Reader.h"
|
||||
#include "../reader/JFJochCBFReader.h"
|
||||
#include "../reader/MiniCBF.h"
|
||||
#include "../compression/JFJochCompressor.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <future>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
TEST_CASE("HDF5DataType_Sign","[HDF5]") {
|
||||
HDF5DataType type_u8((uint8_t)0), type_fl(0.0f), type_i32((int32_t) 0), type_u32((uint32_t) 0);
|
||||
@@ -3594,3 +3599,339 @@ TEST_CASE("JFJochReader_ThirdPartyNXmxMaster", "[HDF5][Full]") {
|
||||
// No leftover HDF5 objects
|
||||
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
|
||||
}
|
||||
|
||||
// A detector swung out on a 2theta arm. NXmx has no field for it: the swing is one rotation in the
|
||||
// depends_on chain the detector's position is stated as, and "two_theta" is only one beamline's name
|
||||
// for that dataset. So the chain is what the reader follows, and the chain here carries two rotations
|
||||
// about different axes, outboard of the translation that sets the distance - a file that stated only
|
||||
// the innermost one, or composed them the other way round, gives a different plane.
|
||||
//
|
||||
// Both axes are stated in McStas, which is the internal frame turned half a turn about z: a reader
|
||||
// that takes the vector as it stands swings the detector the wrong way, which is twice the error of
|
||||
// not reading it at all.
|
||||
TEST_CASE("JFJochReader_DetectorTwoThetaArm", "[HDF5][Full]") {
|
||||
const hsize_t nx = 8, ny = 6;
|
||||
const double two_theta_deg = 20.0, tilt_deg = 7.0;
|
||||
std::vector<uint16_t> image(nx * ny, 5);
|
||||
WriteThirdPartyDataFile("two_theta_000001.h5", image, 2, ny, nx);
|
||||
|
||||
{
|
||||
HDF5File master("two_theta_master.h5");
|
||||
HDF5Group entry(master, "entry");
|
||||
entry.SaveScalar("definition", "NXmx");
|
||||
HDF5Group instrument(entry, "instrument");
|
||||
HDF5Group beam(instrument, "beam");
|
||||
beam.SaveScalar("incident_wavelength", 0.6889)->Units("angstrom");
|
||||
HDF5Group transformations(instrument, "transformations");
|
||||
// Outermost first in the file, innermost first along the chain: det_z -> two_theta -> tilt
|
||||
transformations.SaveVector("tilt", std::vector<double>{tilt_deg})
|
||||
->Transformation("deg", ".", "detector", "", "rotation", {0, 1, 0});
|
||||
transformations.SaveVector("two_theta", std::vector<double>{two_theta_deg})
|
||||
->Transformation("deg", "/entry/instrument/transformations/tilt",
|
||||
"detector", "", "rotation", {-1, 0, 0});
|
||||
transformations.SaveVector("det_z", std::vector<double>{160.0})
|
||||
->Transformation("mm", "/entry/instrument/transformations/two_theta",
|
||||
"detector", "", "translation", {0, 0, 1});
|
||||
HDF5Group detector(instrument, "detector");
|
||||
detector.SaveScalar("depends_on", "/entry/instrument/transformations/det_z");
|
||||
detector.SaveScalar("description", "PILATUS 2M");
|
||||
detector.SaveScalar("beam_center_x", 4.0)->Units("pixels");
|
||||
detector.SaveScalar("beam_center_y", 3.0)->Units("pixels");
|
||||
detector.SaveScalar("distance", 0.160)->Units("m");
|
||||
detector.SaveScalar("x_pixel_size", 0.172)->Units("mm");
|
||||
detector.SaveScalar("y_pixel_size", 0.172)->Units("mm");
|
||||
detector.SaveScalar("sensor_thickness", 0.32)->Units("mm");
|
||||
detector.SaveScalar("count_time", 0.2);
|
||||
detector.SaveScalar("saturation_value", static_cast<int64_t>(65535));
|
||||
HDF5Group data(entry, "data");
|
||||
data.ExternalLink("two_theta_000001.h5", "/data", "data_000001");
|
||||
}
|
||||
|
||||
DiffractionGeometry geom;
|
||||
{
|
||||
JFJochHDF5Reader reader;
|
||||
REQUIRE_NOTHROW(reader.ReadFile("two_theta_master.h5"));
|
||||
geom = reader.GetDataset()->experiment.GetDiffractionGeometry();
|
||||
}
|
||||
|
||||
// The chain as it stands in the internal frame: McStas (-1,0,0) is internal (1,0,0) and McStas
|
||||
// (0,1,0) is internal (0,-1,0), and the outer rotation multiplies on the left.
|
||||
const auto to_rad = [](double deg) { return static_cast<float>(deg * PI / 180.0); };
|
||||
const RotMatrix expected = RotMatrix(to_rad(tilt_deg), {0, -1, 0})
|
||||
* RotMatrix(to_rad(two_theta_deg), {1, 0, 0});
|
||||
for (int64_t column = 0; column < 3; column++)
|
||||
CHECK((geom.GetDetectorMatrix().Column(column) - expected.Column(column)).Length() < 1e-5f);
|
||||
|
||||
// Distance and beam centre are the ones the file states: the arm turns the detector about the
|
||||
// sample and moves neither.
|
||||
CHECK(geom.GetDetectorDistance_mm() == Catch::Approx(160.0));
|
||||
CHECK(geom.GetBeamX_pxl() == Catch::Approx(4.0));
|
||||
CHECK(geom.GetBeamY_pxl() == Catch::Approx(3.0));
|
||||
// And the beam centre pixel is now that far from the beam - the whole point of a 2theta arm.
|
||||
CHECK(geom.TwoTheta_rad(4.0f, 3.0f) * 180.0f / PI
|
||||
== Catch::Approx(angle_deg(expected * Coord(0, 0, 1), Coord(0, 0, 1))));
|
||||
CHECK(geom.TwoTheta_rad(4.0f, 3.0f) * 180.0f / PI > two_theta_deg);
|
||||
|
||||
remove("two_theta_000001.h5");
|
||||
remove("two_theta_master.h5");
|
||||
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
|
||||
}
|
||||
|
||||
// The same file with the arm parked at zero: the chain is there, nothing in it turns, and the
|
||||
// geometry must be exactly the square-on one. This is nearly every file, so it has to cost nothing.
|
||||
TEST_CASE("JFJochReader_DetectorTwoThetaZeroIsSquareOn", "[HDF5][Full]") {
|
||||
const hsize_t nx = 8, ny = 6;
|
||||
std::vector<uint16_t> image(nx * ny, 5);
|
||||
WriteThirdPartyDataFile("two_theta_zero_000001.h5", image, 2, ny, nx);
|
||||
|
||||
{
|
||||
HDF5File master("two_theta_zero_master.h5");
|
||||
HDF5Group entry(master, "entry");
|
||||
entry.SaveScalar("definition", "NXmx");
|
||||
HDF5Group instrument(entry, "instrument");
|
||||
HDF5Group beam(instrument, "beam");
|
||||
beam.SaveScalar("incident_wavelength", 0.6889)->Units("angstrom");
|
||||
HDF5Group transformations(instrument, "transformations");
|
||||
transformations.SaveVector("two_theta", std::vector<double>{0.0})
|
||||
->Transformation("deg", ".", "detector", "", "rotation", {-1, 0, 0});
|
||||
transformations.SaveVector("det_z", std::vector<double>{160.0})
|
||||
->Transformation("mm", "/entry/instrument/transformations/two_theta",
|
||||
"detector", "", "translation", {0, 0, 1});
|
||||
HDF5Group detector(instrument, "detector");
|
||||
detector.SaveScalar("depends_on", "/entry/instrument/transformations/det_z");
|
||||
detector.SaveScalar("description", "PILATUS 2M");
|
||||
detector.SaveScalar("beam_center_x", 4.0)->Units("pixels");
|
||||
detector.SaveScalar("beam_center_y", 3.0)->Units("pixels");
|
||||
detector.SaveScalar("distance", 0.160)->Units("m");
|
||||
detector.SaveScalar("x_pixel_size", 0.172)->Units("mm");
|
||||
detector.SaveScalar("y_pixel_size", 0.172)->Units("mm");
|
||||
detector.SaveScalar("sensor_thickness", 0.32)->Units("mm");
|
||||
detector.SaveScalar("count_time", 0.2);
|
||||
detector.SaveScalar("saturation_value", static_cast<int64_t>(65535));
|
||||
HDF5Group data(entry, "data");
|
||||
data.ExternalLink("two_theta_zero_000001.h5", "/data", "data_000001");
|
||||
}
|
||||
|
||||
DiffractionGeometry geom;
|
||||
{
|
||||
JFJochHDF5Reader reader;
|
||||
REQUIRE_NOTHROW(reader.ReadFile("two_theta_zero_master.h5"));
|
||||
geom = reader.GetDataset()->experiment.GetDiffractionGeometry();
|
||||
}
|
||||
|
||||
CHECK(geom.GetPoniRot1_rad() == 0.0f);
|
||||
CHECK(geom.GetPoniRot2_rad() == 0.0f);
|
||||
CHECK(geom.GetPoniRot3_rad() == 0.0f);
|
||||
CHECK(geom.TwoTheta_rad(4.0f, 3.0f) == 0.0f);
|
||||
|
||||
remove("two_theta_zero_000001.h5");
|
||||
remove("two_theta_zero_master.h5");
|
||||
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
|
||||
}
|
||||
|
||||
// A file this system wrote states its own PONI angles twice: as the three scalars the reader takes
|
||||
// them from, and as three rotations in the detector's depends_on chain. Following the chain must
|
||||
// therefore skip them - applied on top of the scalars they would tilt the detector twice, which is
|
||||
// how a correct 2theta reader breaks every tilted file this system has ever written. A test that only
|
||||
// wrote an untilted detector could not see it.
|
||||
TEST_CASE("JFJochReader_DetectorChainDoesNotDoubleTheTilt", "[HDF5][Full]") {
|
||||
const float rot1 = 0.031f, rot2 = -0.047f, rot3 = 0.019f;
|
||||
|
||||
DiffractionExperiment x(DetJF(1));
|
||||
x.ImagesPerTrigger(2).OverwriteExistingFiles(true).FilePrefix("test_ponichain");
|
||||
x.BeamX_pxl(100).BeamY_pxl(200).DetectorDistance_mm(150)
|
||||
.IncidentEnergy_keV(WVL_1A_IN_KEV).PixelSigned(false).BitDepthImage(16)
|
||||
.FrameTime(std::chrono::microseconds(500), std::chrono::microseconds(10));
|
||||
x.PoniRot1_rad(rot1).PoniRot2_rad(rot2).PoniRot3_rad(rot3);
|
||||
|
||||
RegisterHDF5Filter();
|
||||
std::vector<uint16_t> image(x.GetPixelsNum(), 0);
|
||||
|
||||
StartMessage start_message;
|
||||
x.FillMessage(start_message);
|
||||
FileWriter file_set(start_message);
|
||||
DataMessage message{};
|
||||
for (int i = 0; i < x.GetImageNum(); i++) {
|
||||
message.image = CompressedImage(image, x.GetXPixelsNum(), x.GetYPixelsNum());
|
||||
message.number = i;
|
||||
REQUIRE_NOTHROW(file_set.WriteHDF5(message));
|
||||
}
|
||||
EndMessage end_message;
|
||||
end_message.max_image_number = x.GetImageNum();
|
||||
file_set.WriteHDF5(end_message);
|
||||
file_set.Finalize();
|
||||
|
||||
DiffractionGeometry geom;
|
||||
{
|
||||
JFJochHDF5Reader reader;
|
||||
REQUIRE_NOTHROW(reader.ReadFile("test_ponichain_master.h5"));
|
||||
geom = reader.GetDataset()->experiment.GetDiffractionGeometry();
|
||||
}
|
||||
CHECK(geom.GetPoniRot1_rad() == Catch::Approx(rot1).margin(1e-6));
|
||||
CHECK(geom.GetPoniRot2_rad() == Catch::Approx(rot2).margin(1e-6));
|
||||
CHECK(geom.GetPoniRot3_rad() == Catch::Approx(rot3).margin(1e-6));
|
||||
for (int64_t column = 0; column < 3; column++)
|
||||
CHECK((geom.GetDetectorMatrix().Column(column)
|
||||
- PoniRotMatrix(rot1, rot2, rot3).Column(column)).Length() < 1e-5f);
|
||||
|
||||
remove("test_ponichain_master.h5");
|
||||
remove("test_ponichain_data_000001.h5");
|
||||
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
|
||||
}
|
||||
|
||||
// A miniCBF header states more about the instrument than the "# " lines do: the CBF template block
|
||||
// some beamlines write carries a full imgCIF axis table, saying which laboratory direction the image's
|
||||
// columns and rows run along and which the spindle turns about. The reader assumed all three, and two
|
||||
// instruments in the corpus are not what it assumed - one stores its image a quarter turn round, the
|
||||
// other turns about the VERTICAL. Either way the spindle ends up 90 degrees from the image, which is
|
||||
// not a sign and so is beyond the axis-sign rescue; both indexed nothing at all.
|
||||
namespace {
|
||||
// Two frames of a sweep whose pixels are all zero. Every delta of a zero image is zero, so the
|
||||
// byte-offset stream is one 0x00 per pixel - which is a valid stream and enough to open a sweep.
|
||||
void WriteMiniCBFSweep(const std::string &prefix, const std::string &header_body,
|
||||
int64_t nx, int64_t ny) {
|
||||
for (int frame = 1; frame <= 2; frame++) {
|
||||
std::ostringstream head;
|
||||
head << "###CBF: VERSION 1.5\n_array_data.header_convention \"PILATUS_1.2\"\n"
|
||||
<< "_array_data.header_contents\n"
|
||||
<< "# Detector: PILATUS3 6M, S/N 60-0119\n"
|
||||
<< "# Pixel_size 172e-6 m x 172e-6 m\n"
|
||||
<< "# Silicon sensor, thickness 0.000450 m\n"
|
||||
<< "# Exposure_time 0.1 s\n# Exposure_period 0.1 s\n# Count_cutoff 768595 counts\n"
|
||||
<< "# Wavelength 0.96864 A\n# Detector_distance 0.33161 m\n"
|
||||
<< "# Beam_xy (12.00, 8.00) pixels\n"
|
||||
<< "# Start_angle " << (frame - 1) * 0.1 << " deg.\n# Angle_increment 0.1000 deg.\n"
|
||||
<< "# Omega " << (frame - 1) * 0.1 << " deg.\n# Omega_increment 0.1000 deg.\n"
|
||||
<< "# Phi 0.0000 deg.\n# Phi_increment 0.0000 deg.\n"
|
||||
<< "# Chi 0.0000 deg.\n# Chi_increment 0.0000 deg.\n"
|
||||
<< header_body
|
||||
<< "_array_data.data\n--CIF-BINARY-FORMAT-SECTION--\n"
|
||||
<< "Content-Type: application/octet-stream;\n"
|
||||
<< " conversions=\"x-CBF_BYTE_OFFSET\"\n"
|
||||
<< "Content-Transfer-Encoding: BINARY\n"
|
||||
<< "X-Binary-Size: " << nx * ny << "\n"
|
||||
<< "X-Binary-Element-Type: \"signed 32-bit integer\"\n"
|
||||
<< "X-Binary-Number-of-Elements: " << nx * ny << "\n"
|
||||
<< "X-Binary-Size-Fastest-Dimension: " << nx << "\n"
|
||||
<< "X-Binary-Size-Second-Dimension: " << ny << "\n\n";
|
||||
|
||||
std::ostringstream name;
|
||||
name << prefix << "_" << std::setfill('0') << std::setw(4) << frame << ".cbf";
|
||||
std::ofstream f(name.str(), std::ios::binary);
|
||||
const std::string text = head.str();
|
||||
f.write(text.data(), static_cast<std::streamsize>(text.size()));
|
||||
f.write(reinterpret_cast<const char *>(minicbf::BINARY_SEPARATOR),
|
||||
sizeof(minicbf::BINARY_SEPARATOR));
|
||||
const std::vector<char> zeros(static_cast<size_t>(nx * ny), 0);
|
||||
f.write(zeros.data(), static_cast<std::streamsize>(zeros.size()));
|
||||
}
|
||||
}
|
||||
|
||||
void RemoveMiniCBFSweep(const std::string &prefix) {
|
||||
for (int frame = 1; frame <= 2; frame++) {
|
||||
std::ostringstream name;
|
||||
name << prefix << "_" << std::setfill('0') << std::setw(4) << frame << ".cbf";
|
||||
remove(name.str().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// The axis table in the form these headers write it, several tags to a line.
|
||||
std::string AxisTable(const std::string &rows, int64_t nx, int64_t ny) {
|
||||
return "loop_\n_axis.id\n_axis.type\n_axis.equipment\n_axis.depends_on\n"
|
||||
"_axis.vector[1] _axis.vector[2] _axis.vector[3]\n"
|
||||
"_axis.offset[1] _axis.offset[2] _axis.offset[3]\n"
|
||||
+ rows +
|
||||
"loop_\n_array_structure_list.array_id\n_array_structure_list.index\n"
|
||||
"_array_structure_list.dimension\n_array_structure_list.precedence\n"
|
||||
"_array_structure_list.direction\n_array_structure_list.axis_set_id\n"
|
||||
"ARRAY1 1 " + std::to_string(nx) + " 1 increasing ELEMENT_X\n"
|
||||
"ARRAY1 2 " + std::to_string(ny) + " 2 increasing ELEMENT_Y\n"
|
||||
"loop_\n_array_structure_list_axis.axis_set_id\n_array_structure_list_axis.axis_id\n"
|
||||
"_array_structure_list_axis.displacement\n_array_structure_list_axis.displacement_increment\n"
|
||||
"ELEMENT_X ELEMENT_X 0.0 0.1720\nELEMENT_Y ELEMENT_Y 0.0 0.1720\n";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("JFJochCBFReader_AxisTableStatesTheMounting", "[HDF5][Full]") {
|
||||
const int64_t nx = 24, ny = 16;
|
||||
|
||||
// A header that states nothing: the assumption, and the behaviour of nearly every file there is.
|
||||
SECTION("no table, no hint - the assumption stands") {
|
||||
WriteMiniCBFSweep("cbfaxis_plain", "# Detector_2theta 0.0000 deg.\n# Oscillation_axis OMEGA\n",
|
||||
nx, ny);
|
||||
JFJochCBFReader reader;
|
||||
REQUIRE_NOTHROW(reader.ReadFiles("cbfaxis_plain_0001.cbf"));
|
||||
const auto x = reader.GetDataset()->experiment;
|
||||
REQUIRE(x.GetGoniometer().has_value());
|
||||
CHECK((x.GetGoniometer()->GetAxis() - Coord(-1, 0, 0)).Length() < 1e-6f);
|
||||
CHECK(x.GetDetectorSetup().GetImageOrientation().IsIdentity());
|
||||
CHECK(x.GetDiffractionGeometry().GetPoniRot2_rad() == 0.0f);
|
||||
reader.Close();
|
||||
RemoveMiniCBFSweep("cbfaxis_plain");
|
||||
}
|
||||
|
||||
// A spindle that turns about the VERTICAL, with the image mounted the usual way round. imgCIF Y is
|
||||
// up and the internal frame's y is down, so the stated (0,1,0) is internal (0,-1,0) - and NOT the
|
||||
// (-1,0,0) that was assumed, which is 90 degrees away and indexes nothing.
|
||||
SECTION("vertical spindle, standard image") {
|
||||
WriteMiniCBFSweep("cbfaxis_vert",
|
||||
"# Detector_2theta 0.0000 deg.\n# Oscillation_axis X.CW +SLOW\n"
|
||||
+ AxisTable("GON_OMEGA rotation goniometer . 0 1 0 . . .\n"
|
||||
"DET_Z translation detector . 0 0 -1 0 0 0\n"
|
||||
"ELEMENT_X translation detector DET_Z 1 0 0 -1 1 0\n"
|
||||
"ELEMENT_Y translation detector ELEMENT_X 0 -1 0 0 0 0\n", nx, ny),
|
||||
nx, ny);
|
||||
JFJochCBFReader reader;
|
||||
REQUIRE_NOTHROW(reader.ReadFiles("cbfaxis_vert_0001.cbf"));
|
||||
const auto x = reader.GetDataset()->experiment;
|
||||
REQUIRE(x.GetGoniometer().has_value());
|
||||
CHECK((x.GetGoniometer()->GetAxis() - Coord(0, -1, 0)).Length() < 1e-6f);
|
||||
// The image itself is standard, so nothing about it is turned - the axis was the whole error.
|
||||
CHECK(x.GetDetectorSetup().GetImageOrientation().IsIdentity());
|
||||
reader.Close();
|
||||
RemoveMiniCBFSweep("cbfaxis_vert");
|
||||
}
|
||||
|
||||
// The same vertical spindle, stated only by the "+SLOW" token, which is all a header with no axis
|
||||
// table says. Two datasets from that instrument are in this state.
|
||||
SECTION("vertical spindle from the +SLOW token alone") {
|
||||
WriteMiniCBFSweep("cbfaxis_slow", "# Detector_2theta 0.0000 deg.\n# Oscillation_axis X.CW +SLOW\n",
|
||||
nx, ny);
|
||||
JFJochCBFReader reader;
|
||||
REQUIRE_NOTHROW(reader.ReadFiles("cbfaxis_slow_0001.cbf"));
|
||||
const auto x = reader.GetDataset()->experiment;
|
||||
REQUIRE(x.GetGoniometer().has_value());
|
||||
CHECK((x.GetGoniometer()->GetAxis() - Coord(0, -1, 0)).Length() < 1e-6f);
|
||||
reader.Close();
|
||||
RemoveMiniCBFSweep("cbfaxis_slow");
|
||||
}
|
||||
|
||||
// An image stored a quarter turn round, on a detector swung out to 30 degrees. The two are read
|
||||
// together or not at all: the arm turns about a laboratory axis, and which way that runs across
|
||||
// the stored image is exactly what the mounting says.
|
||||
SECTION("quarter-turned image on a swung arm") {
|
||||
WriteMiniCBFSweep("cbfaxis_turn",
|
||||
"# Detector_2theta 30.0000 deg.\n# Oscillation_axis OMEGA\n"
|
||||
+ AxisTable("GON_OMEGA rotation goniometer . 1 0 0 . . .\n"
|
||||
"DET_2THETA rotation detector . 1 0 0 . . .\n"
|
||||
"DET_Z translation detector DET_2THETA 0 0 -1 0 0 0\n"
|
||||
"ELEMENT_X translation detector DET_Z 0 1 0 -1 1 0\n"
|
||||
"ELEMENT_Y translation detector ELEMENT_X 1 0 0 0 0 0\n", nx, ny),
|
||||
nx, ny);
|
||||
JFJochCBFReader reader;
|
||||
REQUIRE_NOTHROW(reader.ReadFiles("cbfaxis_turn_0001.cbf"));
|
||||
const auto x = reader.GetDataset()->experiment;
|
||||
REQUIRE(x.GetGoniometer().has_value());
|
||||
CHECK((x.GetGoniometer()->GetAxis() - Coord(1, 0, 0)).Length() < 1e-6f);
|
||||
// fast = imgCIF (0,1,0) = internal (0,-1,0), slow = imgCIF (1,0,0) = internal (1,0,0)
|
||||
CHECK(x.GetDetectorSetup().GetImageOrientation() == DetectorOrientation(false, 3));
|
||||
// and the arm turns about its own stated axis, internal +x, by the stated 30 degrees
|
||||
const auto geom = x.GetDiffractionGeometry();
|
||||
const RotMatrix expected = RotMatrix(static_cast<float>(30.0 * PI / 180.0), {1, 0, 0})
|
||||
* DetectorOrientation(false, 3).Matrix();
|
||||
for (int64_t column = 0; column < 3; column++)
|
||||
CHECK((geom.GetDetectorMatrix().Column(column) - expected.Column(column)).Length() < 1e-5f);
|
||||
reader.Close();
|
||||
RemoveMiniCBFSweep("cbfaxis_turn");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user