From 8a04773d1d6221a3304fd39e68de0bd6eab3d4c9 Mon Sep 17 00:00:00 2001
From: Filip Leonarski
Date: Wed, 2 Sep 2026 09:57:58 +0200
Subject: [PATCH] api: record the beam size at the sample, and write it where
NXmx puts it
dataset_settings gains beam_size_x_um and beam_size_y_um, the horizontal and
vertical size of the X-ray beam where it meets the sample. They follow the same
route total_flux takes - OpenAPI, DatasetSettings, the CBOR start message, the
HDF5 master, and back out of a stored file - and nothing consumes them; this is
metadata a beamline can state and a downstream program can read.
NXmx puts this in the application definition rather than the base class: not
NXbeam's extent (rank 2, nP x 2, per scan point, always FWHM of a rectangular
aperture) but NXmx's own incident_beam_size, a recommended rank-1 two-element
array in the order x, y. Both are live and neither is deprecated, so the choice
matters; the MX definition wins in an MX file. Written as one array with a
units attribute of "m", like every other length in the master, so the settings
hold micrometres and FillMessage converts once.
The unit table of ReadLength_m becomes LengthUnitFactor so the array read can
share it: a master written elsewhere may state this in millimetres.
Co-Authored-By: Claude Opus 5 (1M context)
Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N
---
broker/OpenAPIConvert.cpp | 4 +
broker/gen/model/Dataset_settings.cpp | 86 +++++++++++++++++++++
broker/gen/model/Dataset_settings.h | 18 +++++
broker/jfjoch_api.yaml | 15 ++++
broker/redoc-static.html | 9 ++-
common/DatasetSettings.cpp | 26 +++++++
common/DatasetSettings.h | 7 ++
common/DiffractionExperiment.cpp | 24 ++++++
common/DiffractionExperiment.h | 4 +
common/JFJochMessages.h | 3 +
docs/CBOR.md | 2 +
docs/CHANGELOG.md | 1 +
docs/HDF5.md | 1 +
frame_serialize/CBORStream2Deserializer.cpp | 4 +
frame_serialize/CBORStream2Serializer.cpp | 2 +
frontend/src/client/types.gen.ts | 13 ++++
frontend/src/client/zod.gen.ts | 2 +
reader/HDF5MetadataSource.cpp | 31 ++++++--
tests/CBORTest.cpp | 6 ++
writer/HDF5NXmx.cpp | 5 ++
20 files changed, 253 insertions(+), 10 deletions(-)
diff --git a/broker/OpenAPIConvert.cpp b/broker/OpenAPIConvert.cpp
index 7cbcb38eb..e44b9fca2 100644
--- a/broker/OpenAPIConvert.cpp
+++ b/broker/OpenAPIConvert.cpp
@@ -663,6 +663,10 @@ DatasetSettings Convert(const org::openapitools::server::model::Dataset_settings
if (input.totalFluxIsSet())
ret.TotalFlux(input.getTotalFlux());
+ if (input.beamSizeXUmIsSet())
+ ret.BeamSizeX_um(input.getBeamSizeXUm());
+ if (input.beamSizeYUmIsSet())
+ ret.BeamSizeY_um(input.getBeamSizeYUm());
if (input.transmissionIsSet())
ret.AttenuatorTransmission(input.getTransmission());
// Not alternatives: a grid scan is often collected at a given head position, so an axis and a
diff --git a/broker/gen/model/Dataset_settings.cpp b/broker/gen/model/Dataset_settings.cpp
index 86698ba44..a464d929e 100644
--- a/broker/gen/model/Dataset_settings.cpp
+++ b/broker/gen/model/Dataset_settings.cpp
@@ -45,6 +45,10 @@ Dataset_settings::Dataset_settings()
m_Total_fluxIsSet = false;
m_Transmission = 0.0f;
m_TransmissionIsSet = false;
+ m_Beam_size_x_um = 0.0f;
+ m_Beam_size_x_umIsSet = false;
+ m_Beam_size_y_um = 0.0f;
+ m_Beam_size_y_umIsSet = false;
m_GoniometerIsSet = false;
m_Grid_scanIsSet = false;
m_Header_appendixIsSet = false;
@@ -237,6 +241,34 @@ bool Dataset_settings::validate(std::stringstream& msg, const std::string& pathP
}
}
+
+ if (beamSizeXUmIsSet())
+ {
+ const float& value = m_Beam_size_x_um;
+ const std::string currentValuePath = _pathPrefix + ".beamSizeXUm";
+
+
+ if (value < static_cast(0.0))
+ {
+ success = false;
+ msg << currentValuePath << ": must be greater than or equal to 0.0;";
+ }
+
+ }
+
+ if (beamSizeYUmIsSet())
+ {
+ const float& value = m_Beam_size_y_um;
+ const std::string currentValuePath = _pathPrefix + ".beamSizeYUm";
+
+
+ if (value < static_cast(0.0))
+ {
+ success = false;
+ msg << currentValuePath << ": must be greater than or equal to 0.0;";
+ }
+
+ }
if (dataReductionFactorSerialmxIsSet())
{
@@ -478,6 +510,12 @@ bool Dataset_settings::operator==(const Dataset_settings& rhs) const
((!transmissionIsSet() && !rhs.transmissionIsSet()) || (transmissionIsSet() && rhs.transmissionIsSet() && getTransmission() == rhs.getTransmission())) &&
+ ((!beamSizeXUmIsSet() && !rhs.beamSizeXUmIsSet()) || (beamSizeXUmIsSet() && rhs.beamSizeXUmIsSet() && getBeamSizeXUm() == rhs.getBeamSizeXUm())) &&
+
+
+ ((!beamSizeYUmIsSet() && !rhs.beamSizeYUmIsSet()) || (beamSizeYUmIsSet() && rhs.beamSizeYUmIsSet() && getBeamSizeYUm() == rhs.getBeamSizeYUm())) &&
+
+
((!goniometerIsSet() && !rhs.goniometerIsSet()) || (goniometerIsSet() && rhs.goniometerIsSet() && getGoniometer() == rhs.getGoniometer())) &&
@@ -587,6 +625,10 @@ void to_json(nlohmann::json& j, const Dataset_settings& o)
j["total_flux"] = o.m_Total_flux;
if(o.transmissionIsSet())
j["transmission"] = o.m_Transmission;
+ if(o.beamSizeXUmIsSet())
+ j["beam_size_x_um"] = o.m_Beam_size_x_um;
+ if(o.beamSizeYUmIsSet())
+ j["beam_size_y_um"] = o.m_Beam_size_y_um;
if(o.goniometerIsSet())
j["goniometer"] = o.m_Goniometer;
if(o.gridScanIsSet())
@@ -696,6 +738,16 @@ void from_json(const nlohmann::json& j, Dataset_settings& o)
j.at("transmission").get_to(o.m_Transmission);
o.m_TransmissionIsSet = true;
}
+ if(j.find("beam_size_x_um") != j.end())
+ {
+ j.at("beam_size_x_um").get_to(o.m_Beam_size_x_um);
+ o.m_Beam_size_x_umIsSet = true;
+ }
+ if(j.find("beam_size_y_um") != j.end())
+ {
+ j.at("beam_size_y_um").get_to(o.m_Beam_size_y_um);
+ o.m_Beam_size_y_umIsSet = true;
+ }
if(j.find("goniometer") != j.end())
{
j.at("goniometer").get_to(o.m_Goniometer);
@@ -1026,6 +1078,40 @@ void Dataset_settings::unsetTransmission()
{
m_TransmissionIsSet = false;
}
+float Dataset_settings::getBeamSizeXUm() const
+{
+ return m_Beam_size_x_um;
+}
+void Dataset_settings::setBeamSizeXUm(float const value)
+{
+ m_Beam_size_x_um = value;
+ m_Beam_size_x_umIsSet = true;
+}
+bool Dataset_settings::beamSizeXUmIsSet() const
+{
+ return m_Beam_size_x_umIsSet;
+}
+void Dataset_settings::unsetBeam_size_x_um()
+{
+ m_Beam_size_x_umIsSet = false;
+}
+float Dataset_settings::getBeamSizeYUm() const
+{
+ return m_Beam_size_y_um;
+}
+void Dataset_settings::setBeamSizeYUm(float const value)
+{
+ m_Beam_size_y_um = value;
+ m_Beam_size_y_umIsSet = true;
+}
+bool Dataset_settings::beamSizeYUmIsSet() const
+{
+ return m_Beam_size_y_umIsSet;
+}
+void Dataset_settings::unsetBeam_size_y_um()
+{
+ m_Beam_size_y_umIsSet = false;
+}
org::openapitools::server::model::Rotation_axis Dataset_settings::getGoniometer() const
{
return m_Goniometer;
diff --git a/broker/gen/model/Dataset_settings.h b/broker/gen/model/Dataset_settings.h
index 9dc2fcdfa..863e23961 100644
--- a/broker/gen/model/Dataset_settings.h
+++ b/broker/gen/model/Dataset_settings.h
@@ -155,6 +155,20 @@ public:
bool transmissionIsSet() const;
void unsetTransmission();
///
+ /// First element of /entry/instrument/beam/incident_beam_size in NXmx Horizontal size of the X-ray beam where it meets the sample - the FWHM of a focused beam, the full width of a slit-defined one. [um]
+ ///
+ float getBeamSizeXUm() const;
+ void setBeamSizeXUm(float const value);
+ bool beamSizeXUmIsSet() const;
+ void unsetBeam_size_x_um();
+ ///
+ /// Second element of /entry/instrument/beam/incident_beam_size in NXmx Vertical size of the X-ray beam where it meets the sample. [um]
+ ///
+ float getBeamSizeYUm() const;
+ void setBeamSizeYUm(float const value);
+ bool beamSizeYUmIsSet() const;
+ void unsetBeam_size_y_um();
+ ///
///
///
org::openapitools::server::model::Rotation_axis getGoniometer() const;
@@ -361,6 +375,10 @@ protected:
bool m_Total_fluxIsSet;
float m_Transmission;
bool m_TransmissionIsSet;
+ float m_Beam_size_x_um;
+ bool m_Beam_size_x_umIsSet;
+ float m_Beam_size_y_um;
+ bool m_Beam_size_y_umIsSet;
org::openapitools::server::model::Rotation_axis m_Goniometer;
bool m_GoniometerIsSet;
org::openapitools::server::model::Grid_scan m_Grid_scan;
diff --git a/broker/jfjoch_api.yaml b/broker/jfjoch_api.yaml
index 1c844585f..968340e2c 100644
--- a/broker/jfjoch_api.yaml
+++ b/broker/jfjoch_api.yaml
@@ -476,6 +476,21 @@ components:
description: |
/entry/instrument/attenuator/attenuator_transmission
Transmission of attenuator (filter) [no units]
+ beam_size_x_um:
+ type: number
+ format: float
+ minimum: 0.0
+ description: |
+ First element of /entry/instrument/beam/incident_beam_size in NXmx
+ Horizontal size of the X-ray beam where it meets the sample - the FWHM of a focused beam,
+ the full width of a slit-defined one. [um]
+ beam_size_y_um:
+ type: number
+ format: float
+ minimum: 0.0
+ description: |
+ Second element of /entry/instrument/beam/incident_beam_size in NXmx
+ Vertical size of the X-ray beam where it meets the sample. [um]
goniometer:
$ref: "#/components/schemas/rotation_axis"
grid_scan:
diff --git a/broker/redoc-static.html b/broker/redoc-static.html
index 263ee5e05..b13bdeb4b 100644
--- a/broker/redoc-static.html
+++ b/broker/redoc-static.html
@@ -466,6 +466,11 @@ Sample name
Flux incident on beam plane in photons per second. In other words this is the flux integrated over area. [photons/s]
transmission
number <float> [ 0 .. 1 ]
/entry/instrument/attenuator/attenuator_transmission
Transmission of attenuator (filter) [no units]
+
beam_size_x_um
number <float> >= 0
First element of /entry/instrument/beam/incident_beam_size in NXmx
+Horizontal size of the X-ray beam where it meets the sample - the FWHM of a focused beam,
+the full width of a slit-defined one. [um]
+
beam_size_y_um
number <float> >= 0
Second element of /entry/instrument/beam/incident_beam_size in NXmx
+Vertical size of the X-ray beam where it meets the sample. [um]
object (rotation_axis)
Definition of a crystal rotation axis
object (grid_scan)
Definition of a grid scan. May be combined with a goniometer axis: a grid is often collected
at a particular head position, and a stationary axis records where that was.
@@ -517,7 +522,7 @@ Assuming that Smargon is used as static positioner and not moving during the sca
Test Jungfraujoch system
-
http://localhost:5232/start
Request samples
Payload
Content type
application/json
{
"images_per_trigger": 1,
"ntrigger": 1,
"image_time_us": 0,
"beam_x_pxl": 0.1,
"beam_y_pxl": 0.1,
"detector_distance_mm": 0.1,
"incident_energy_keV": 0.001,
"file_prefix": "",
"images_per_file": 1,
"space_group_number": 1,
"sample_name": "",
"compression": "bslz4",
"total_flux": 0.1,
"transmission": 1,
"goniometer": {
"name": "omega",
"step": 0.1,
"start": 50,
"vector": [
1,
0,
0
],
"helical_step_um": [
-5,
-2,
0
],
"screening_wedge_deg": 0.1
},
"grid_scan": {
"n_fast": 20,
"step_x_um": 5,
"step_y_um": -3,
"vertical": false,
"snake": false
},
"header_appendix": null,
"image_appendix": null,
"data_reduction_factor_serialmx": 1,
"pixel_value_low_threshold": 0,
"run_number": 0,
"run_name": "string",
"experiment_group": "string",
"poisson_compression": 16,
"write_nxmx_hdf5_master": true,
"save_calibration": true,
"polarization_factor": -1,
"ring_current_mA": 0.1,
"sample_temperature_K": 0.1,
"poni_rot1_rad": 0,
"poni_rot2_rad": 0,
"poni_rot3_rad": 0,
"unit_cell": {
"a": 37,
"b": 37,
"c": 78,
"alpha": 90,
"beta": 90,
"gamma": 90
},
"spot_finding": true,
"smargon": {
"phi_deg": 0.1,
"chi_deg": 90,
"phi_axis": [
1,
0,
0
],
"chi_axis": [
0,
0,
1
]
},
"max_spot_count": 250,
"detect_ice_rings": true,
"async_start": false,
"xray_fluorescence_spectrum": {
"energy_eV": [
0.1
],
"data": [
0.1
]
}
}
Response samples
500
Content type
application/json
{
"msg": "Detector in wrong state",
"reason": "WrongDAQState"
}
Wait for acquisition running
Block execution of external script till detector and Jungfraujoch are ready to collect data.
+
http://localhost:5232/start
Request samples
Payload
Content type
application/json
{
"images_per_trigger": 1,
"ntrigger": 1,
"image_time_us": 0,
"beam_x_pxl": 0.1,
"beam_y_pxl": 0.1,
"detector_distance_mm": 0.1,
"incident_energy_keV": 0.001,
"file_prefix": "",
"images_per_file": 1,
"space_group_number": 1,
"sample_name": "",
"compression": "bslz4",
"total_flux": 0.1,
"transmission": 1,
"beam_size_x_um": 0.1,
"beam_size_y_um": 0.1,
"goniometer": {
"name": "omega",
"step": 0.1,
"start": 50,
"vector": [
1,
0,
0
],
"helical_step_um": [
-5,
-2,
0
],
"screening_wedge_deg": 0.1
},
"grid_scan": {
"n_fast": 20,
"step_x_um": 5,
"step_y_um": -3,
"vertical": false,
"snake": false
},
"header_appendix": null,
"image_appendix": null,
"data_reduction_factor_serialmx": 1,
"pixel_value_low_threshold": 0,
"run_number": 0,
"run_name": "string",
"experiment_group": "string",
"poisson_compression": 16,
"write_nxmx_hdf5_master": true,
"save_calibration": true,
"polarization_factor": -1,
"ring_current_mA": 0.1,
"sample_temperature_K": 0.1,
"poni_rot1_rad": 0,
"poni_rot2_rad": 0,
"poni_rot3_rad": 0,
"unit_cell": {
"a": 37,
"b": 37,
"c": 78,
"alpha": 90,
"beta": 90,
"gamma": 90
},
"spot_finding": true,
"smargon": {
"phi_deg": 0.1,
"chi_deg": 90,
"phi_axis": [
1,
0,
0
],
"chi_axis": [
0,
0,
1
]
},
"max_spot_count": 250,
"detect_ice_rings": true,
"async_start": false,
"xray_fluorescence_spectrum": {
"energy_eV": [
0.1
],
"data": [
0.1
]
}
}
Response samples
500
Content type
application/json
{
"msg": "Detector in wrong state",
"reason": "WrongDAQState"
}
Wait for acquisition running
Block execution of external script till detector and Jungfraujoch are ready to collect data.
To not block web server for a indefinite period of time, the procedure is provided with a timeout.
Extending timeout is possible, but requires to ensure safety that client will not close the connection and retry the connection.
query Parameters
timeout
integer [ 0 .. 3600 ]
Default: 60
Timeout in seconds (0 == immediate response)
@@ -988,7 +993,7 @@ then image might be replaced in the buffer between calling /images and /image.cb