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:
@@ -2925,4 +2925,73 @@ TEST_CASE("JFJochReader_Snapshots", "[HDF5][Full]") {
|
||||
remove("test_snap_proc_master.h5");
|
||||
|
||||
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
|
||||
}
|
||||
}
|
||||
// The chain may be sent in the END message or left to the writer to build. Both must produce the
|
||||
// same file - the sent one is written verbatim, which is what will later allow measured positions to
|
||||
// be reported, and the built one is what a producer that does not send it gets.
|
||||
TEST_CASE("JFJochReader_TransformationChain_SentAndBuilt", "[HDF5][Full]") {
|
||||
DiffractionExperiment x(DetJF(1));
|
||||
x.ImagesPerTrigger(5).OverwriteExistingFiles(true);
|
||||
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.Goniometer(GoniometerAxis("omega", 95, 0.1f, Coord(0,-1,0), {}));
|
||||
x.Smargon(SmargonPosition{.phi_deg = -7.25f, .chi_deg = 12.5f});
|
||||
|
||||
RegisterHDF5Filter();
|
||||
std::vector<uint16_t> image(x.GetPixelsNum(), 0);
|
||||
|
||||
const auto write = [&](const std::string &prefix, bool send_chain) {
|
||||
DiffractionExperiment local = x;
|
||||
local.FilePrefix(prefix);
|
||||
StartMessage start_message;
|
||||
local.FillMessage(start_message);
|
||||
|
||||
FileWriter file_set(start_message);
|
||||
DataMessage message{};
|
||||
for (int i = 0; i < local.GetImageNum(); i++) {
|
||||
message.image = CompressedImage(image, local.GetXPixelsNum(), local.GetYPixelsNum());
|
||||
message.number = i;
|
||||
REQUIRE_NOTHROW(file_set.WriteHDF5(message));
|
||||
}
|
||||
|
||||
EndMessage end_message;
|
||||
end_message.max_image_number = local.GetImageNum();
|
||||
if (send_chain)
|
||||
end_message.transformations = local.BuildTransformationChain(local.GetImageNum());
|
||||
file_set.WriteHDF5(end_message);
|
||||
file_set.Finalize();
|
||||
};
|
||||
|
||||
write("test_chain_built", false);
|
||||
write("test_chain_sent", true);
|
||||
|
||||
const auto read = [](const std::string &prefix) {
|
||||
JFJochHDF5Reader reader;
|
||||
reader.ReadFile(prefix + "_master.h5");
|
||||
return reader.GetDataset()->experiment;
|
||||
};
|
||||
|
||||
const auto built = read("test_chain_built");
|
||||
const auto sent = read("test_chain_sent");
|
||||
|
||||
REQUIRE(built.GetGoniometer().has_value());
|
||||
REQUIRE(sent.GetGoniometer().has_value());
|
||||
CHECK(sent.GetGoniometer()->GetName() == built.GetGoniometer()->GetName());
|
||||
CHECK(sent.GetGoniometer()->GetStart_deg()
|
||||
== Catch::Approx(built.GetGoniometer()->GetStart_deg()).margin(1e-3));
|
||||
CHECK(sent.GetGoniometer()->GetIncrement_deg()
|
||||
== Catch::Approx(built.GetGoniometer()->GetIncrement_deg()).margin(1e-4));
|
||||
|
||||
// chi/phi survive both routes, which they did not before they became ordinary axes.
|
||||
REQUIRE(built.GetDatasetSettings().GetSmargonPosition().has_value());
|
||||
REQUIRE(sent.GetDatasetSettings().GetSmargonPosition().has_value());
|
||||
CHECK(sent.GetDatasetSettings().GetSmargonPosition()->chi_deg
|
||||
== Catch::Approx(12.5f).margin(1e-3));
|
||||
CHECK(sent.GetDatasetSettings().GetSmargonPosition()->phi_deg
|
||||
== Catch::Approx(-7.25f).margin(1e-3));
|
||||
|
||||
remove("test_chain_built_master.h5");
|
||||
remove("test_chain_sent_master.h5");
|
||||
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user