Tell a Smargon head position from the spindle by equipment_component

The reader recognised chi and phi by name. phi is an ordinary spindle name in MX, so a file whose
rotation axis is called phi had it read back as a head position as well as the spindle - and writing
that experiment out again threw, because the sample chain then tried to create phi twice. In the
other direction a still with a head position had chi, its alphabetically first stationary axis,
adopted as the goniometer.

Both are now settled by the file: the axes jfjoch writes for a Smargon carry
equipment_component="smargon", the reader takes a head position only from a tagged axis, and skips
tagged axes when looking for the spindle. NXmx defines equipment_component as an identifier of the
component of the equipment a transformation belongs to, which is what this is; there is no
"equipment" attribute in NeXus at all.

Adds HDF5Object::AttrExists, since the tag is absent on every file from anywhere else.

The two tests assert on the written file - the axis length and the attribute - because the reader
cannot see either: it does not look at a shape, and it did not look at the tag. That is the same gap
that let the one-image shape through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VfYvJT5Nb71suJCowRBn5z
This commit is contained in:
2026-08-23 10:49:47 +02:00
co-authored by Claude Opus 5
parent 8dd3ee6576
commit ce11cade84
6 changed files with 156 additions and 4 deletions
+20 -2
View File
@@ -609,8 +609,23 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen
// recorded as "phi" back as stills, silently. Prefer an axis that actually turns; fall back
// to a stationary one, which still says where the head was.
if (master_file->Exists("/entry/sample/transformations")) {
// A Smargon chi/phi is tagged with equipment_component - it is a head position, not the
// spindle. Recognised by that tag and not by name: phi is an ordinary spindle name in MX,
// so a file from anywhere else must not have its rotation axis read back as a head
// position, nor its spindle mistaken for one here.
auto is_smargon_axis = [this](const std::string &name) {
const std::string dname = "/entry/sample/transformations/" + name;
if (!master_file->Exists(dname))
return false;
HDF5DataSet axis(*master_file, dname);
return axis.AttrExists("equipment_component")
&& (axis.ReadAttrStr("equipment_component") == "smargon");
};
std::optional<GoniometerAxis> stationary;
for (const auto &name: master_file->FindLeafs("/entry/sample/transformations")) {
if (is_smargon_axis(name))
continue;
auto axis = ReadAxis(master_file.get(), name);
if (!axis.has_value())
continue;
@@ -628,8 +643,11 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen
// chi and phi are ordinary stationary axes in the file; the settings still keep them in
// their own Smargon field, so put them back there. Without this a re-opened file lost
// the head position entirely - nothing in reader/ read it.
const auto chi = ReadAxis(master_file.get(), "chi");
const auto phi = ReadAxis(master_file.get(), "phi");
std::optional<GoniometerAxis> chi, phi;
if (is_smargon_axis("chi"))
chi = ReadAxis(master_file.get(), "chi");
if (is_smargon_axis("phi"))
phi = ReadAxis(master_file.get(), "phi");
if (chi.has_value() || phi.has_value()) {
SmargonPosition smargon;
if (chi.has_value()) {