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

Request samples

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": {
    },
  • "grid_scan": {
    },
  • "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": {
    },
  • "spot_finding": true,
  • "smargon": {
    },
  • "max_spot_count": 250,
  • "detect_ice_rings": true,
  • "async_start": false,
  • "xray_fluorescence_spectrum": {
    }
}

Response samples

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

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": {
    },
  • "grid_scan": {
    },
  • "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": {
    },
  • "spot_finding": true,
  • "smargon": {
    },
  • "max_spot_count": 250,
  • "detect_ice_rings": true,
  • "async_start": false,
  • "xray_fluorescence_spectrum": {
    }
}

Response samples

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