diff --git a/common/JFJochMessages.h b/common/JFJochMessages.h index ac9255090..70f762546 100644 --- a/common/JFJochMessages.h +++ b/common/JFJochMessages.h @@ -442,6 +442,10 @@ struct EndMessage { std::optional rotation_lattice; std::vector rotation_extra_lattices; std::optional unit_cell; + // The e.s.d.s of unit_cell's six parameters, in its order (A and degrees; an angle the group fixes has + // zero), as the offline analysis measured them (CellEsd.h). Rotation only; absent where not measured. + // Written to /entry/sample/unit_cell_esd. + std::optional unit_cell_esd; // Space group determined by the offline analysis (overrides the start message when writing the // master, since it is only known after merging). Both spellings are carried and a reader prefers // the name: only the name keeps the setting - "P 1 1 2" and "R 3:R" both come back from a number diff --git a/docs/CBOR.md b/docs/CBOR.md index d98a3c267..20f6ff90a 100644 --- a/docs/CBOR.md +++ b/docs/CBOR.md @@ -312,6 +312,7 @@ See [DECTRIS documentation](https://github.com/dectris/documentation/tree/main/s | spindle_lost_unique_fraction | float | Fraction (0-1) of unique reflections the mounting made unmeasurable, exact under the measured point group; offline (Rugnux) only | | | indexing_rate | float | Mean indexing rate for the whole run | | | unit_cell | object (optional) | Unit cell of the system, based on the actual experiment: a, b, c \[angstrom\] and alpha, beta, gamma \[degree\] | | +| unit_cell_esd | object (optional) | e.s.d.s of `unit_cell`, same layout: a, b, c \[angstrom\] and alpha, beta, gamma \[degree\]; rotation, offline (Rugnux) and in-broker analysis only | | | rotation_lattice_type | object | Bravais lattice classification of the total rotation solution over the run, if available; same schema as `lattice_type` | | | - centering | string | One-letter centering code: P, A, B, C, I, F, or R | | | - niggli_class | int64 | Integer identifier for the Niggli-reduced Bravais class | | diff --git a/docs/HDF5.md b/docs/HDF5.md index a84f5a709..e6908eab6 100644 --- a/docs/HDF5.md +++ b/docs/HDF5.md @@ -285,6 +285,7 @@ DIALS/dxtbx reads it (one panel per module). | `temperature` | NXmx | K | | `transformations/` (NXtransformations) | NXmx | the sample chain, written in mounting order; hard-linked as `/entry/sample/goniometer` | | `unit_cell` | base | `[a, b, c, α, β, γ]` | +| `unit_cell_esd` | | e.s.d.s of `unit_cell`, in its order `[σa, σb, σc, σα, σβ, σγ]` (Angstrom, degrees; zero for an angle the space group fixes); rotation runs analysed by rugnux, offline or in the broker | | `space_group_number` | base | International Tables number | | `space_group` | base | extended Hermann-Mauguin name, e.g. `P 43 21 2`, `R 3:H` — this is the field that carries the **setting**, and the one the reader takes the group from; the number alone always reads back as the reference setting | | `ub_matrix` | base | `[1, 3, 3]`, Angstrom⁻¹ | diff --git a/frame_serialize/CBORStream2Deserializer.cpp b/frame_serialize/CBORStream2Deserializer.cpp index d6f4e647e..066bad575 100644 --- a/frame_serialize/CBORStream2Deserializer.cpp +++ b/frame_serialize/CBORStream2Deserializer.cpp @@ -1769,6 +1769,8 @@ namespace { GetCBORUInt8Array(value, message.niggli_class); else if (key == "unit_cell") message.unit_cell = ProcessUnitCellElement(value); + else if (key == "unit_cell_esd") + message.unit_cell_esd = ProcessUnitCellElement(value); else if (key == "space_group_name") message.space_group_name = GetCBORString(value); else if (key == "space_group_number") diff --git a/frame_serialize/CBORStream2Serializer.cpp b/frame_serialize/CBORStream2Serializer.cpp index 4e9615615..46e7510dc 100644 --- a/frame_serialize/CBORStream2Serializer.cpp +++ b/frame_serialize/CBORStream2Serializer.cpp @@ -969,6 +969,7 @@ void CBORStream2Serializer::SerializeSequenceEnd(const EndMessage& message) { CBOR_ENC(mapEncoder, "niggli_class", message.niggli_class); CBOR_ENC(mapEncoder, "pixel_sum", message.pixel_sum); CBOR_ENC(mapEncoder, "unit_cell", message.unit_cell); + CBOR_ENC(mapEncoder, "unit_cell_esd", message.unit_cell_esd); // Both spellings of the determined group. The name carries the setting and is what a reader // should take; the number is beside it for a reader written before the name existed. CBOR_ENC(mapEncoder, "space_group_name", message.space_group_name); diff --git a/reader/HDF5MetadataSource.cpp b/reader/HDF5MetadataSource.cpp index f6fea51fa..d53bee837 100644 --- a/reader/HDF5MetadataSource.cpp +++ b/reader/HDF5MetadataSource.cpp @@ -944,6 +944,8 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen .beta = tmp[4], .gamma = tmp[5] }); + if (const auto e = master_file->ReadOptVector("/entry/sample/unit_cell_esd"); e.size() == 6) + dataset->unit_cell_esd = UnitCell{.a = e[0], .b = e[1], .c = e[2], .alpha = e[3], .beta = e[4], .gamma = e[5]}; // The name carries the setting, the number cannot ("R 3:R" reads back as "R 3:H"), so the // name is preferred; the number is the fallback for a file written before it was recorded. if (const auto *sg = gemmi::find_spacegroup_by_name(master_file->GetString("/entry/sample/space_group"))) diff --git a/reader/JFJochReaderDataset.h b/reader/JFJochReaderDataset.h index 5c01a5248..53c898d33 100644 --- a/reader/JFJochReaderDataset.h +++ b/reader/JFJochReaderDataset.h @@ -42,6 +42,9 @@ struct JFJochReaderDataset { // /entry/MX/reindexMatrix; the reader applies it, so everything this class hands out is already in // the cell's setting. std::optional> reindex_matrix; + // The e.s.d.s of the experiment's unit cell, a b c alpha beta gamma (/entry/sample/unit_cell_esd); empty + // where the file has none. + std::optional unit_cell_esd; // Stills: the rocking curve the run's partialities were computed from (/entry/MX/stillsRockingWidth*). std::optional stills_run_width; diff --git a/receiver/JFJochReceiverRugnux.cpp b/receiver/JFJochReceiverRugnux.cpp index 36036a6ea..12948715f 100644 --- a/receiver/JFJochReceiverRugnux.cpp +++ b/receiver/JFJochReceiverRugnux.cpp @@ -518,6 +518,7 @@ void JFJochReceiverRugnux::AddToEndMessage(EndMessage &message) { message.rotation_lattice_type = r.rotation_lattice_type; message.rotation_extra_lattices = r.rotation_extra_lattices; message.unit_cell = r.unit_cell; + message.unit_cell_esd = r.unit_cell_esd; message.space_group_name = r.space_group_name; message.space_group_number = r.space_group_number; message.reindex_matrix = r.reindex_matrix; diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 7584cdb35..806de40e0 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -1314,6 +1314,12 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b end_msg.image_scale_mosaicity = scaling.mosaicity_deg; } + if (result.cell_esd) { + const std::array &e = result.cell_esd->esd; + end_msg.unit_cell_esd = UnitCell{static_cast(e[0]), static_cast(e[1]), static_cast(e[2]), + static_cast(e[3]), static_cast(e[4]), static_cast(e[5])}; + } + if (sink) sink->End(end_msg); result.end_message = end_msg; diff --git a/tests/CBORTest.cpp b/tests/CBORTest.cpp index 05b2e1790..76cbe36ce 100644 --- a/tests/CBORTest.cpp +++ b/tests/CBORTest.cpp @@ -578,6 +578,33 @@ TEST_CASE("CBORSerialize_End_SpaceGroup", "[CBOR]") { CHECK(*out.space_group_number == static_cast(sg->number)); } +TEST_CASE("CBORSerialize_End_UnitCellEsd", "[CBOR]") { + std::vector buffer(1024 * 1024); + CBORStream2Serializer serializer(buffer.data(), buffer.size()); + + EndMessage message{}; + message.max_image_number = 0; + message.unit_cell = UnitCell{.a = 40, .b = 50, .c = 60, .alpha = 90, .beta = 101.5f, .gamma = 90}; + message.unit_cell_esd = UnitCell{.a = 0.002f, .b = 0.003f, .c = 0.004f, .alpha = 0, .beta = 0.011f, .gamma = 0}; + REQUIRE_NOTHROW(serializer.SerializeSequenceEnd(message)); + + auto deserialized = CBORStream2Deserialize(buffer.data(), serializer.GetBufferSize()); + REQUIRE(deserialized); + REQUIRE(deserialized->end_message); + const EndMessage &out = *deserialized->end_message; + REQUIRE(out.unit_cell_esd.has_value()); + CHECK(*out.unit_cell_esd == *message.unit_cell_esd); + CHECK(*out.unit_cell == *message.unit_cell); + + // Absent stays absent: a stills run has no e.s.d.s. + message.unit_cell_esd.reset(); + CBORStream2Serializer serializer2(buffer.data(), buffer.size()); + REQUIRE_NOTHROW(serializer2.SerializeSequenceEnd(message)); + auto deserialized2 = CBORStream2Deserialize(buffer.data(), serializer2.GetBufferSize()); + REQUIRE(deserialized2->end_message); + CHECK_FALSE(deserialized2->end_message->unit_cell_esd.has_value()); +} + TEST_CASE("CBORSerialize_End_SpaceGroupNumberOnly", "[CBOR]") { // A sender written before the name existed fills in only the number; the message still arrives // and the missing name is absent rather than empty, so a reader can tell it apart. diff --git a/tests/JFJochReaderTest.cpp b/tests/JFJochReaderTest.cpp index 320d434fe..24953b013 100644 --- a/tests/JFJochReaderTest.cpp +++ b/tests/JFJochReaderTest.cpp @@ -2831,6 +2831,54 @@ TEST_CASE("JFJochReader_StillsRunWidth", "[HDF5][Full]") { REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0); } +TEST_CASE("JFJochReader_UnitCellEsd", "[HDF5][Full]") { + for (const bool with_esd: {true, false}) { + DiffractionExperiment x(DetJF(1)); + x.FilePrefix("read_unit_cell_esd") + .ImagesPerTrigger(1) + .ImagesPerFile(1) + .OverwriteExistingFiles(true) + .BitDepthImage(16) + .PixelSigned(true) + .SetFileWriterFormat(FileWriterFormat::NXmxVDS) + .Compression(CompressionAlgorithm::NO_COMPRESSION); + + const UnitCell cell{.a = 61.2f, .b = 61.2f, .c = 97.5f, .alpha = 90, .beta = 90, .gamma = 120}; + const UnitCell esd{.a = 0.0031f, .b = 0.0031f, .c = 0.0074f, .alpha = 0, .beta = 0, .gamma = 0}; + RegisterHDF5Filter(); + { + StartMessage start_message; + x.FillMessage(start_message); + FileWriter writer(start_message); + std::vector image(x.GetPixelsNum(), 0); + DataMessage message{}; + message.image = CompressedImage(image, x.GetXPixelsNum(), x.GetYPixelsNum()); + message.number = 0; + REQUIRE_NOTHROW(writer.WriteHDF5(message)); + EndMessage end_message; + end_message.max_image_number = 1; + end_message.unit_cell = cell; + if (with_esd) + end_message.unit_cell_esd = esd; + writer.WriteHDF5(end_message); + writer.Finalize(); + } + { + JFJochHDF5Reader reader; + REQUIRE_NOTHROW(reader.ReadFile("read_unit_cell_esd_master.h5")); + const auto dataset = reader.GetDataset(); + REQUIRE(dataset->experiment.GetUnitCell().has_value()); + CHECK(dataset->experiment.GetUnitCell()->c == cell.c); + REQUIRE(dataset->unit_cell_esd.has_value() == with_esd); + if (with_esd) + CHECK(*dataset->unit_cell_esd == esd); + } + remove("read_unit_cell_esd_master.h5"); + remove("read_unit_cell_esd_data_000001.h5"); + } + REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0); +} + static std::vector MakeTestSpots(int i) { return { SpotToSave{ diff --git a/writer/HDF5NXmx.cpp b/writer/HDF5NXmx.cpp index 550fae738..8836e4f96 100644 --- a/writer/HDF5NXmx.cpp +++ b/writer/HDF5NXmx.cpp @@ -825,6 +825,8 @@ void NXmx::Sample(const StartMessage &start, const EndMessage &end) { if (unit_cell) SaveUnitCell(group, "unit_cell", unit_cell.value()); + if (end.unit_cell_esd) + SaveUnitCell(group, "unit_cell_esd", end.unit_cell_esd.value()); if (input_unit_cell) SaveUnitCell(group, "input_unit_cell", input_unit_cell.value());