Carry the sample transformation chain as DetectorTransformation

Replaces the start-message TransformationAxis of the previous commit, which was
the wrong shape in two ways.

DetectorTransformation (common/) mirrors a NeXus NXtransformations axis and holds
nothing else: name, type, units, vector, offset, depends_on and the positions
themselves. Deliberately without cleverness - the values are either a single
number for an axis that does not move or one per image, and nothing derives a
position from a start and an increment. That is the point: a producer will later
want to report where a stage actually WENT rather than where it was told to go,
and a structure that stores start+increment cannot express that. A million images
cost 4 MB per axis, which is not a reason to be clever.

Hence also the move to the END message: measured positions are only known once
the run is over.

And hence no metadata version bump, which the previous commit did make. The chain
is optional; when it is absent the writer builds the identical chain from the
start message, exactly as before. Nothing on the wire changes for a producer that
does not send it, so a broker and a writer of different releases still interwork -
the constraint the previous version stated is withdrawn.

The writer transcribes a chain it is given, without recomputing an angle, which
is what makes measured positions possible end to end.

JFJochReader_TransformationChain_SentAndBuilt writes the same run both ways and
checks the two files read back the same, chi/phi included.
CBORSerialize_End_Transformations covers the wire 1:1, asserting the order
survives and that a moving axis keeps one value per image while a stationary one
keeps a single value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-22 23:36:50 +02:00
co-authored by Claude Opus 5
parent 7efcf631de
commit e4edcd6fa9
13 changed files with 404 additions and 102 deletions
+27 -17
View File
@@ -1015,10 +1015,9 @@ namespace {
}
}
// The ordered sample chain (see TransformationAxis). An array, so the mounting order survives -
// a CBOR map's order carries no guarantee. Sent alongside the goniometer map, which says the same
// thing without the order for consumers that only know DECTRIS stream2.
void ProcessTransformations(StartMessage &message, CborValue &value) {
// The ordered sample chain, 1:1 with DetectorTransformation. An array, so the mounting order
// survives; a CBOR map carries no order a consumer may rely on.
void ProcessTransformations(std::vector<DetectorTransformation> &chain, CborValue &value) {
CborValue array_value;
cborErr(cbor_value_enter_container(&value, &array_value));
@@ -1026,28 +1025,39 @@ namespace {
CborValue map_value;
cborErr(cbor_value_enter_container(&array_value, &map_value));
TransformationAxis axis;
DetectorTransformation axis;
std::vector<float> values;
while (!cbor_value_at_end(&map_value)) {
const auto key = GetCBORString(map_value);
if (key == "name")
axis.name = GetCBORString(map_value);
else if (key == "type")
axis.rotation = (GetCBORString(map_value) == "rotation");
axis.Name(GetCBORString(map_value));
else if (key == "transformation_type")
axis.Type(GetCBORString(map_value) == "rotation" ? TransformationType::Rotation
: TransformationType::Translation);
else if (key == "units")
axis.Units(GetCBORString(map_value));
else if (key == "vector")
axis.vector = GetCoord(map_value);
else if (key == "start")
axis.start = GetCBORFloat(map_value);
else if (key == "increment")
axis.increment = GetCBORFloat(map_value);
axis.Vector(GetCoord(map_value));
else if (key == "offset")
axis.Offset(GetCoord(map_value));
else if (key == "depends_on")
axis.DependsOn(GetCBORString(map_value));
else if (key == "equipment")
axis.Equipment(GetCBORString(map_value));
else if (key == "equipment_component")
axis.EquipmentComponent(GetCBORString(map_value));
else if (key == "values")
GetCBORFloatArray(map_value, values);
else
cbor_value_advance(&map_value);
}
cborErr(cbor_value_leave_container(&array_value, &map_value));
if (axis.name.empty())
if (axis.GetName().empty())
throw JFJochException(JFJochExceptionCategory::CBORError,
"Transformation axis without a name");
message.transformations.push_back(axis);
axis.Values(values);
chain.push_back(axis);
}
cborErr(cbor_value_leave_container(&value, &array_value));
}
@@ -1298,8 +1308,6 @@ namespace {
ProcessAxis(value, message.detector_translation);
else if (key == "goniometer")
ProcessGoniometerMap(message, value);
else if (key == "transformations")
ProcessTransformations(message, value);
else if (key == "grid_scan")
message.grid_scan = ProcessGridScan(value);
else if (key == "pixel_mask_enabled")
@@ -1435,6 +1443,8 @@ namespace {
message.run_name = GetCBORString(value);
else if (key == "series_id")
message.run_number = GetCBORUInt(value);
else if (key == "transformations")
ProcessTransformations(message.transformations, value);
else if (key == "max_image_number")
message.max_image_number = GetCBORUInt(value);
else if (key == "images_collected")