Read any rotation axis by name, and tell a stationary axis from a sweep

Two things the goniometer handling conflated.

The axis name is free-form everywhere that writes it - the API imposes only
minLength, the CBOR map uses the name as its key, and tests/CBORTest.cpp round
trips one literally called "z" - but the reader looked for exactly
"/entry/sample/transformations/omega". A sweep recorded as "phi" therefore came
back as stills, in the viewer and in rugnux, with nothing to indicate it. The
reader now walks the transformations group and takes whichever axis is a
rotation, preferring one that turns; the grid scan is read independently rather
than as the else-branch of the same test, since a grid scan can be taken at a
given head position.

Second: "an axis is defined" and "the axis is turning" were the same question,
answered inconsistently - GetImagesPerFile checked the increment, IsRotationIndexing
did not, and the CBOR decoder deleted zero-increment axes outright so the
ambiguity could never surface. GoniometerAxis::IsScanning now asks it explicitly
and the call sites go through it, so a stationary axis can be carried without
being mistaken for rotation data. That mistake is not hypothetical:
RotationIndexerCounter leaves its stride at zero for a zero increment, and
Process() then never fires, so indexing would silently never run.

Keeping stationary axes is also what lets the writer state where the head was for
a still or a grid scan, which is the next step.

JFJochReader_Goniometer_NonOmegaName covers the naming case through the writer
and back; nothing did before, because both existing round trips use "omega".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-22 22:20:41 +02:00
co-authored by Claude Opus 5
parent 1f4f77fe42
commit b35672a0c3
8 changed files with 115 additions and 28 deletions
+40 -18
View File
@@ -604,21 +604,40 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen
metadata.SourceName(master_file->GetString("/entry/source/name"));
dataset->experiment.ImportInstrumentMetadata(metadata);
// The rotation axis is whatever the file calls it. The name is free-form throughout the API,
// the CBOR stream and the writer, so looking only for "omega" - as this did - read a sweep
// 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")) {
if (master_file->Exists("/entry/sample/transformations/omega")) {
auto omega = ReadAxis(master_file.get(), "omega");
dataset->experiment.Goniometer(omega);
} else if (master_file->Exists("/entry/sample/grid_scan")) {
GridScanSettings grid(
master_file->GetInt("/entry/sample/grid_scan/n_fast"),
master_file->GetFloat("/entry/sample/grid_scan/step_x") * 1e6f,
master_file->GetFloat("/entry/sample/grid_scan/step_y") * 1e6f,
master_file->GetOptBool("/entry/sample/grid_scan/snake_scan").value_or(false),
master_file->GetOptBool("/entry/sample/grid_scan/vertical_scan").value_or(false)
);
grid.ImageNum(number_of_images);
dataset->experiment.GridScan(grid);
std::optional<GoniometerAxis> stationary;
for (const auto &name: master_file->FindLeafs("/entry/sample/transformations")) {
auto axis = ReadAxis(master_file.get(), name);
if (!axis.has_value())
continue;
if (axis->IsScanning()) {
dataset->experiment.Goniometer(axis);
stationary.reset();
break;
}
if (!stationary.has_value())
stationary = axis;
}
if (stationary.has_value())
dataset->experiment.Goniometer(stationary);
}
// Independent of the axis: a grid scan can be taken at a given head position, so the two are
// not alternatives.
if (master_file->Exists("/entry/sample/grid_scan")) {
GridScanSettings grid(
master_file->GetInt("/entry/sample/grid_scan/n_fast"),
master_file->GetFloat("/entry/sample/grid_scan/step_x") * 1e6f,
master_file->GetFloat("/entry/sample/grid_scan/step_y") * 1e6f,
master_file->GetOptBool("/entry/sample/grid_scan/snake_scan").value_or(false),
master_file->GetOptBool("/entry/sample/grid_scan/vertical_scan").value_or(false)
);
grid.ImageNum(number_of_images);
dataset->experiment.GridScan(grid);
}
auto tmp = master_file->ReadOptVector<float>("/entry/sample/unit_cell");
@@ -1060,16 +1079,19 @@ std::optional<GoniometerAxis> HDF5MetadataSource::ReadAxis(HDF5Object *file, con
std::vector<double> angle;
dataset.ReadVector(angle);
if (angle.size() < 2)
if (angle.empty())
return {};
if (dataset.ReadAttrStr("transformation_type") != "rotation")
return {};
std::vector<double> end = file->ReadOptVector<double>(dname + "_end");
// A single value, or every value the same, is a stationary axis: it says where the head was
// rather than that anything turned. Increment 0 is the honest description of that, and
// GoniometerAxis::IsScanning is what separates it from a sweep.
double start = angle[0];
double incr = angle[1] - angle[0];
if (dataset.ReadAttrStr("transformation_type") != "rotation")
return {};
double incr = (angle.size() < 2) ? 0.0 : angle[1] - angle[0];
std::vector<double> axis_vec = dataset.ReadAttrVec("vector");
if (axis_vec.size() != 3)