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:
@@ -717,7 +717,6 @@ void DiffractionExperiment::FillMessage(StartMessage &message) const {
|
||||
|
||||
message.goniometer = dataset.GetGoniometer();
|
||||
message.grid_scan = dataset.GetGridScan();
|
||||
message.transformations = BuildTransformationChain();
|
||||
|
||||
message.run_number = GetRunNumber();
|
||||
message.run_name = GetRunName();
|
||||
@@ -1457,30 +1456,54 @@ bool DiffractionExperiment::IsDetectorMirroredY() const {
|
||||
}
|
||||
|
||||
// Base first, sample last - the order things are physically mounted in, which is also the order an
|
||||
// NXmx depends_on chain has to be built in. The grid stage is a base stage (an Aerotech xyz at SLS)
|
||||
// that the spindle sits on; the spindle carries the head; the head carries the sample.
|
||||
std::vector<TransformationAxis> DiffractionExperiment::BuildTransformationChain() const {
|
||||
std::vector<TransformationAxis> chain;
|
||||
// NXmx depends_on chain composes in. The grid stage is a base stage (an Aerotech xyz at SLS) that
|
||||
// the spindle sits on; the spindle carries the head; the head carries the sample.
|
||||
//
|
||||
// An axis that moves carries one value per image; one that does not carries a single value. Nothing
|
||||
// is derived on the way out to NXmx - that is the point of holding the positions rather than a start
|
||||
// and an increment, so that measured positions can be carried here later without changing anything
|
||||
// downstream.
|
||||
std::vector<DetectorTransformation> DiffractionExperiment::BuildTransformationChain(int64_t image_num) const {
|
||||
std::vector<DetectorTransformation> chain;
|
||||
std::string parent; // empty = mounted on the base
|
||||
|
||||
if (const auto grid_scan = GetGridScan()) {
|
||||
chain.push_back({.name = "grid_scan_x", .rotation = false, .vector = {1, 0, 0}});
|
||||
chain.push_back({.name = "grid_scan_y", .rotation = false, .vector = {0, 1, 0}});
|
||||
const auto add = [&chain, &parent](DetectorTransformation axis) {
|
||||
axis.DependsOn(parent);
|
||||
parent = axis.GetName();
|
||||
chain.push_back(std::move(axis));
|
||||
};
|
||||
|
||||
const auto to_float = [](const std::vector<double> &input) {
|
||||
return std::vector<float>(input.begin(), input.end());
|
||||
};
|
||||
|
||||
if (const auto grid_scan = GetGridScan(); grid_scan.has_value() && (image_num > 0)) {
|
||||
add(DetectorTransformation("grid_scan_x", TransformationType::Translation, {1, 0, 0})
|
||||
.Values(to_float(grid_scan->GetXContainer_m(image_num))));
|
||||
add(DetectorTransformation("grid_scan_y", TransformationType::Translation, {0, 1, 0})
|
||||
.Values(to_float(grid_scan->GetYContainer_m(image_num))));
|
||||
}
|
||||
|
||||
if (const auto goniometer = GetGoniometer())
|
||||
chain.push_back({.name = goniometer->GetName(),
|
||||
.rotation = true,
|
||||
.vector = goniometer->GetAxis(),
|
||||
.start = goniometer->GetStart_deg(),
|
||||
.increment = goniometer->GetIncrement_deg()});
|
||||
if (const auto goniometer = GetGoniometer()) {
|
||||
DetectorTransformation axis(goniometer->GetName(), TransformationType::Rotation,
|
||||
goniometer->GetAxis());
|
||||
if (goniometer->IsScanning() && (image_num > 0))
|
||||
axis.Values(to_float(goniometer->GetAngleContainer(image_num)));
|
||||
else
|
||||
axis.Value(goniometer->GetStart_deg());
|
||||
add(std::move(axis));
|
||||
} else if (GetGridScan().has_value()) {
|
||||
// A grid scan still sits on a spindle that simply does not turn. NXmx cannot say "there is no
|
||||
// rotation", and a sample chain of translations alone is not something readers accept.
|
||||
add(DetectorTransformation("omega", TransformationType::Rotation, {-1, 0, 0}).Value(0.0f));
|
||||
}
|
||||
|
||||
// Smargon chi and phi are ordinary axes that happen not to move; they are only separate in the
|
||||
// settings for historical reasons.
|
||||
// Smargon chi and phi are ordinary axes that happen not to move.
|
||||
if (const auto smargon = dataset.GetSmargonPosition()) {
|
||||
chain.push_back({.name = "chi", .rotation = true,
|
||||
.vector = smargon->chi_axis, .start = smargon->chi_deg});
|
||||
chain.push_back({.name = "phi", .rotation = true,
|
||||
.vector = smargon->phi_axis, .start = smargon->phi_deg});
|
||||
add(DetectorTransformation("chi", TransformationType::Rotation, smargon->chi_axis)
|
||||
.Value(smargon->chi_deg));
|
||||
add(DetectorTransformation("phi", TransformationType::Rotation, smargon->phi_axis)
|
||||
.Value(smargon->phi_deg));
|
||||
}
|
||||
|
||||
return chain;
|
||||
|
||||
Reference in New Issue
Block a user