From 336fafffd0df7f44d9b8f3491252908d17e16bf4 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Tue, 1 Sep 2026 11:58:35 +0200 Subject: [PATCH] cbor: carry the direct beam on the start message beam_center_x/y is the PONI - the foot of the perpendicular from the sample - so on a tilted detector it is not where the beam lands, and the two are distance*tan(tilt)/pixel apart (~8 px on a real in-house setup). A consumer that wants the beam position, which is what most of them mean, had to redo the tilt arithmetic or get it wrong. Add optional direct_beam_x/direct_beam_y to StartMessage, filled from the geometry's own GetDirectBeam_pxl() so there is no second formula, and put them at the top level of the CBOR start map beside beam_center_x/y. Optional keys, so a consumer that does not know them skips them. No _pxl suffix: the stream2 neighbours (beam_center_x, pixel_size_x, detector_distance) carry none and the units are in docs/CBOR.md. The API's calibration schema spells it direct_beam_x_pxl because its neighbours there are beam_x_pxl - same quantity, each matching its own neighbourhood. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N --- common/DiffractionExperiment.cpp | 3 +++ common/JFJochMessages.h | 8 ++++++++ frame_serialize/CBORStream2Deserializer.cpp | 4 ++++ frame_serialize/CBORStream2Serializer.cpp | 4 ++++ tests/CBORTest.cpp | 8 ++++++++ 5 files changed, 27 insertions(+) diff --git a/common/DiffractionExperiment.cpp b/common/DiffractionExperiment.cpp index fa535919b..db0bdfa26 100644 --- a/common/DiffractionExperiment.cpp +++ b/common/DiffractionExperiment.cpp @@ -654,6 +654,9 @@ void DiffractionExperiment::FillMessage(StartMessage &message) const { message.images_per_file = GetImagesPerFile(); message.beam_center_x = GetBeamX_pxl(); message.beam_center_y = GetBeamY_pxl(); + const auto direct_beam = GetDiffractionGeometry().GetDirectBeam_pxl(); + message.direct_beam_x = direct_beam.first; + message.direct_beam_y = direct_beam.second; message.detector_distance = GetDetectorDistance_mm() * 1e-3f; message.incident_wavelength = GetWavelength_A(); // NXmx incident_wavelength_spread is the FWHM of the wavelength distribution, diff --git a/common/JFJochMessages.h b/common/JFJochMessages.h index 5e3360708..dae9b38a2 100644 --- a/common/JFJochMessages.h +++ b/common/JFJochMessages.h @@ -210,6 +210,14 @@ struct StartMessage { float detector_distance; float beam_center_x; float beam_center_y; + // Where the undeflected beam lands on the detector, in pixels. This is NOT beam_center_x/y above: + // that is the PONI, the foot of the perpendicular from the sample, and the two part company as + // soon as the detector is tilted. Derived from the rest of the geometry + // (DiffractionGeometry::GetDirectBeam_pxl), and carried so a consumer that wants the beam + // position - which is what most of them mean, XDS among them - does not have to redo the tilt + // arithmetic. Absent in a stream written before these fields existed. + std::optional direct_beam_x; + std::optional direct_beam_y; uint64_t number_of_images; diff --git a/frame_serialize/CBORStream2Deserializer.cpp b/frame_serialize/CBORStream2Deserializer.cpp index d6e8a753e..22ee26e4f 100644 --- a/frame_serialize/CBORStream2Deserializer.cpp +++ b/frame_serialize/CBORStream2Deserializer.cpp @@ -1254,6 +1254,10 @@ namespace { message.beam_center_x = GetCBORFloat(value); else if (key == "beam_center_y") message.beam_center_y = GetCBORFloat(value); + else if (key == "direct_beam_x") + message.direct_beam_x = GetCBORFloat(value); + else if (key == "direct_beam_y") + message.direct_beam_y = GetCBORFloat(value); else if (key == "detector_distance") message.detector_distance = GetCBORFloat(value); else if (key == "number_of_images") diff --git a/frame_serialize/CBORStream2Serializer.cpp b/frame_serialize/CBORStream2Serializer.cpp index a37c135df..e8c0cc006 100644 --- a/frame_serialize/CBORStream2Serializer.cpp +++ b/frame_serialize/CBORStream2Serializer.cpp @@ -689,6 +689,10 @@ void CBORStream2Serializer::SerializeSequenceStart(const StartMessage& message) CBOR_ENC_AXIS(mapEncoder, "detector_translation", message.detector_translation); CBOR_ENC(mapEncoder, "beam_center_x", message.beam_center_x); CBOR_ENC(mapEncoder, "beam_center_y", message.beam_center_y); + // Not a DECTRIS field, and skipped by a consumer that does not know it. beam_center_x/y above is + // the PONI, which is not where the beam lands once the detector is tilted; these two are. + CBOR_ENC(mapEncoder, "direct_beam_x", message.direct_beam_x); + CBOR_ENC(mapEncoder, "direct_beam_y", message.direct_beam_y); CBOR_ENC(mapEncoder, "countrate_correction_enabled", message.countrate_correction_enabled); CBOR_ENC(mapEncoder, "flatfield_enabled", message.flatfield_enabled); CBOR_ENC(mapEncoder, "number_of_images", message.number_of_images); diff --git a/tests/CBORTest.cpp b/tests/CBORTest.cpp index 33a677382..5e5abca64 100644 --- a/tests/CBORTest.cpp +++ b/tests/CBORTest.cpp @@ -17,6 +17,8 @@ TEST_CASE("CBORSerialize_Start", "[CBOR]") { .detector_distance = 0.0005, .beam_center_x = 456.6, .beam_center_y = 124.3, + .direct_beam_x = 461.2, + .direct_beam_y = 129.9, .number_of_images = 34567, .image_size_x = 456, .image_size_y = 457, @@ -89,6 +91,12 @@ TEST_CASE("CBORSerialize_Start", "[CBOR]") { CHECK(output_message.detector_distance == Catch::Approx(message.detector_distance)); CHECK(output_message.beam_center_x == Catch::Approx(message.beam_center_x)); CHECK(output_message.beam_center_y == Catch::Approx(message.beam_center_y)); + // The PONI (beam_center_*) and the direct beam are different points on a tilted detector, so the + // round trip has to keep them apart, not merely keep two floats. + REQUIRE(output_message.direct_beam_x); + REQUIRE(output_message.direct_beam_y); + CHECK(output_message.direct_beam_x.value() == Catch::Approx(message.direct_beam_x.value())); + CHECK(output_message.direct_beam_y.value() == Catch::Approx(message.direct_beam_y.value())); CHECK(output_message.number_of_images == message.number_of_images); CHECK(output_message.image_size_x == message.image_size_x); CHECK(output_message.image_size_y == message.image_size_y);