From b0e315e73cef301d9a55c2ee07743cedf8edc53b Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Fri, 31 Jul 2026 13:10:58 +0200 Subject: [PATCH] Bragg prediction: derive the lattice walk from the cell, and expose it in the API Follow-up to making max_hkl a setting: it is now an optional, and unset means "take it from this crystal". The predictor keeps only |q| <= 1/d_min and h = a.q for the real-space axis a, so |h| <= a/d_min exactly - and likewise |k| <= b/d_min and |l| <= c/d_min. max(a,b,c)/d_min therefore bounds all three at once: nothing that could be predicted lies outside it, and nothing inside it is reached by a shorter axis. It applies to rotation and stills alike, both going through the one place the prediction settings are built. Offline (rugnux, viewer) the default is unset, so every crystal gets its own range; --max-hkl overrides it. Online the broker holds a concrete number, because the cost is the cube of it per image and a live acquisition should not have its frame rate decided by whichever sample is mounted: max_hkl joins bragg_integration_settings in the OpenAPI with a default of 100, so an omitted field arrives as that default (the generated model carries it) rather than as "derive it", and the frontend exposes it next to the integration model. Measured against a fixed 100 on six rotation crystals: three are bit-identical, two were being truncated and recover 419k and 5.8k observations with the high-shell CC1/2 going 15.1 -> 25.8% and 52.1 -> 55.3%, and the space group is unchanged 6/6. It reproduces a fixed 200 exactly, which is the bound being tight rather than merely safe. The sixth is worth recording: a 149/83/226 A cell derives 227, and because a single scalar has to cover the longest axis the cube is ~16x what a per-axis box would be - 22% wall clock, for a net 22 observations out of 364k (the per-frame 65536-reflection cap re-selects at the margin when more candidates are offered) and identical CC1/2, ISa and space group. Per-axis limits would remove that; the predictors already map a thread index to h, k and l separately. Co-Authored-By: Claude Opus 5 (1M context) --- broker/OpenAPIConvert.cpp | 7 +++ .../gen/model/Bragg_integration_settings.cpp | 48 +++++++++++++++++++ broker/gen/model/Bragg_integration_settings.h | 9 ++++ broker/jfjoch_api.yaml | 15 ++++++ broker/redoc-static.html | 18 +++++-- common/BraggIntegrationSettings.cpp | 14 +++--- common/BraggIntegrationSettings.h | 14 +++--- docs/CHANGELOG.md | 2 +- docs/RUGNUX.md | 2 +- .../docs/BraggIntegrationSettings.md | 1 + frontend/src/client/types.gen.ts | 13 +++++ frontend/src/client/zod.gen.ts | 3 +- .../components/BraggIntegrationSettings.tsx | 30 +++++++++++- image_analysis/IndexAndRefine.cpp | 29 ++++++++++- rugnux/RugnuxCommandLine.cpp | 4 +- rugnux/rugnux_cli.cpp | 4 +- 16 files changed, 186 insertions(+), 27 deletions(-) diff --git a/broker/OpenAPIConvert.cpp b/broker/OpenAPIConvert.cpp index 3d6378e0..f41a145d 100644 --- a/broker/OpenAPIConvert.cpp +++ b/broker/OpenAPIConvert.cpp @@ -1073,6 +1073,11 @@ BraggIntegrationSettings Convert(const org::openapitools::server::model::Bragg_i default: throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Unknown integration model"); } + // Always a concrete number online, never "derive it from the crystal": the generated model holds + // the schema's default when the request omits the field, so an absent max_hkl arrives here as that + // default rather than as an absent value. Deriving per crystal would make a live acquisition's + // per-image cost depend on whichever sample is mounted. + ret.MaxHKL(input.getMaxHkl()); return ret; } @@ -1091,6 +1096,8 @@ org::openapitools::server::model::Bragg_integration_settings Convert(const Bragg break; } ret.setIntegrationModel(tmp); + if (const auto max_hkl = input.GetMaxHKL()) + ret.setMaxHkl(*max_hkl); return ret; } diff --git a/broker/gen/model/Bragg_integration_settings.cpp b/broker/gen/model/Bragg_integration_settings.cpp index 8b1328bf..30ab9d26 100644 --- a/broker/gen/model/Bragg_integration_settings.cpp +++ b/broker/gen/model/Bragg_integration_settings.cpp @@ -21,6 +21,8 @@ namespace org::openapitools::server::model Bragg_integration_settings::Bragg_integration_settings() { + m_Max_hkl = 100; + m_Max_hklIsSet = false; } @@ -43,7 +45,26 @@ bool Bragg_integration_settings::validate(std::stringstream& msg, const std::str bool success = true; const std::string _pathPrefix = pathPrefix.empty() ? "Bragg_integration_settings" : pathPrefix; + + if (maxHklIsSet()) + { + const int32_t& value = m_Max_hkl; + const std::string currentValuePath = _pathPrefix + ".maxHkl"; + + if (value < 1) + { + success = false; + msg << currentValuePath << ": must be greater than or equal to 1;"; + } + if (value > 511) + { + success = false; + msg << currentValuePath << ": must be less than or equal to 511;"; + } + + } + return success; } @@ -53,8 +74,11 @@ bool Bragg_integration_settings::operator==(const Bragg_integration_settings& rh (getIntegrationModel() == rhs.getIntegrationModel()) + && + ((!maxHklIsSet() && !rhs.maxHklIsSet()) || (maxHklIsSet() && rhs.maxHklIsSet() && getMaxHkl() == rhs.getMaxHkl())) + ; } @@ -67,12 +91,19 @@ void to_json(nlohmann::json& j, const Bragg_integration_settings& o) { j = nlohmann::json::object(); j["integration_model"] = o.m_Integration_model; + if(o.maxHklIsSet()) + j["max_hkl"] = o.m_Max_hkl; } void from_json(const nlohmann::json& j, Bragg_integration_settings& o) { j.at("integration_model").get_to(o.m_Integration_model); + if(j.find("max_hkl") != j.end()) + { + j.at("max_hkl").get_to(o.m_Max_hkl); + o.m_Max_hklIsSet = true; + } } @@ -84,6 +115,23 @@ void Bragg_integration_settings::setIntegrationModel(org::openapitools::server:: { m_Integration_model = value; } +int32_t Bragg_integration_settings::getMaxHkl() const +{ + return m_Max_hkl; +} +void Bragg_integration_settings::setMaxHkl(int32_t const value) +{ + m_Max_hkl = value; + m_Max_hklIsSet = true; +} +bool Bragg_integration_settings::maxHklIsSet() const +{ + return m_Max_hklIsSet; +} +void Bragg_integration_settings::unsetMax_hkl() +{ + m_Max_hklIsSet = false; +} } // namespace org::openapitools::server::model diff --git a/broker/gen/model/Bragg_integration_settings.h b/broker/gen/model/Bragg_integration_settings.h index b82dc5c0..53f9f678 100644 --- a/broker/gen/model/Bragg_integration_settings.h +++ b/broker/gen/model/Bragg_integration_settings.h @@ -63,12 +63,21 @@ public: /// org::openapitools::server::model::Integration_model getIntegrationModel() const; void setIntegrationModel(org::openapitools::server::model::Integration_model const& value); + /// + /// How far the Bragg predictor walks the lattice: reflections with |h|,|k|,|l| above this are never predicted. An axis is truncated once a/d_min exceeds it, so a long axis - or a short one taken to high resolution - loses its outermost reflections. The cost grows as the cube (2n+1)^3 of candidates per image, which is why online keeps a fixed, predictable value instead of taking it from whichever crystal is mounted. Omitting the field selects the default above; it is never interpreted as \"choose per crystal\". The offline tools (rugnux, viewer) do derive it from the refined cell when it is left unset there, but that is their own default and is not reachable through this API. + /// + int32_t getMaxHkl() const; + void setMaxHkl(int32_t const value); + bool maxHklIsSet() const; + void unsetMax_hkl(); friend void to_json(nlohmann::json& j, const Bragg_integration_settings& o); friend void from_json(const nlohmann::json& j, Bragg_integration_settings& o); protected: org::openapitools::server::model::Integration_model m_Integration_model; + int32_t m_Max_hkl; + bool m_Max_hklIsSet; }; diff --git a/broker/jfjoch_api.yaml b/broker/jfjoch_api.yaml index e4c174f3..f880c728 100644 --- a/broker/jfjoch_api.yaml +++ b/broker/jfjoch_api.yaml @@ -2348,6 +2348,21 @@ components: properties: integration_model: $ref: '#/components/schemas/integration_model' + max_hkl: + type: integer + minimum: 1 + maximum: 511 + default: 100 + description: | + How far the Bragg predictor walks the lattice: reflections with |h|,|k|,|l| above this are + never predicted. An axis is truncated once a/d_min exceeds it, so a long axis - or a short + one taken to high resolution - loses its outermost reflections. The cost grows as the cube + (2n+1)^3 of candidates per image, which is why online keeps a fixed, predictable value + instead of taking it from whichever crystal is mounted. + + Omitting the field selects the default above; it is never interpreted as "choose per + crystal". The offline tools (rugnux, viewer) do derive it from the refined cell when it is + left unset there, but that is their own default and is not reachable through this API. jfjoch_settings: type: object required: diff --git a/broker/redoc-static.html b/broker/redoc-static.html index 434419cc..973da47c 100644 --- a/broker/redoc-static.html +++ b/broker/redoc-static.html @@ -609,19 +609,27 @@ If set to true, the thread pool will block until a thread is available.

Responses

Response samples

Content type
application/json
{
  • "algorithm": "FFBIDX",
  • "fft_max_unit_cell_A": 250,
  • "fft_min_unit_cell_A": 10,
  • "fft_high_resolution_A": 2,
  • "fft_num_vectors": 16384,
  • "tolerance": 0.5,
  • "thread_count": 1,
  • "geom_refinement_algorithm": "BeamCenter",
  • "unit_cell_dist_tolerance": 0.05,
  • "viable_cell_min_spots": 10,
  • "index_ice_rings": false,
  • "rotation_indexing": false,
  • "rotation_indexing_min_angular_range_deg": 20,
  • "rotation_indexing_angular_stride_deg": 0.5,
  • "blocking": true
}

Change Bragg integration settings

This can only be done when detector is Idle, Error or Inactive states.

-
Request Body schema: application/json
integration_model
required
string (integration_model)
Default: "ProfileGaussian"
Enum: "ProfileGaussian" "ProfileEmpirical" "BoxSum"

Bragg spot integration model. +

Request Body schema: application/json
integration_model
required
string (integration_model)
Default: "ProfileGaussian"
Enum: "ProfileGaussian" "ProfileEmpirical" "BoxSum"

Bragg spot integration model. ProfileGaussian - profile fit with a measured-width Gaussian (Kabsch-style), the default; more accurate intensities than box summation. ProfileEmpirical - profile fit with a per-resolution-shell empirical profile learned from strong spots. BoxSum - classical uniform box summation minus a ring-mean background; the simpler, faster fallback.

+
max_hkl
integer [ 1 .. 511 ]
Default: 100

How far the Bragg predictor walks the lattice: reflections with |h|,|k|,|l| above this are +never predicted. An axis is truncated once a/d_min exceeds it, so a long axis - or a short +one taken to high resolution - loses its outermost reflections. The cost grows as the cube +(2n+1)^3 of candidates per image, which is why online keeps a fixed, predictable value +instead of taking it from whichever crystal is mounted.

+

Omitting the field selects the default above; it is never interpreted as "choose per +crystal". The offline tools (rugnux, viewer) do derive it from the refined cell when it is +left unset there, but that is their own default and is not reachable through this API.

Responses

Request samples

Content type
application/json
{
  • "integration_model": "ProfileGaussian"
}

Response samples

Content type
application/json
{
  • "msg": "Detector in wrong state",
  • "reason": "WrongDAQState"
}

Get Bragg integration configuration

Can be done anytime

+
http://localhost:5232/config/bragg_integration

Request samples

Content type
application/json
{
  • "integration_model": "ProfileGaussian",
  • "max_hkl": 100
}

Response samples

Content type
application/json
{
  • "msg": "Detector in wrong state",
  • "reason": "WrongDAQState"
}

Get Bragg integration configuration

Can be done anytime

Responses

Response samples

Content type
application/json
{
  • "integration_model": "ProfileGaussian"
}

Change file writer settings

This can only be done when detector is Idle, Error or Inactive states.

+
http://localhost:5232/config/bragg_integration

Response samples

Content type
application/json
{
  • "integration_model": "ProfileGaussian",
  • "max_hkl": 100
}

Change file writer settings

This can only be done when detector is Idle, Error or Inactive states.

Request Body schema: application/json
overwrite
boolean
Default: false

Inform jfjoch_write to overwrite existing files. Otherwise files would be saved with .h5.{timestamp}.tmp suffix.

format
string (file_writer_format)
Default: "NXmxLegacy"
Enum: "NXmxOnlyData" "NXmxLegacy" "NXmxVDS" "NXmxIntegrated" "CBF" "TIFF" "NoFileWritten"

NoFileWritten - no files are written at all NXmxOnlyData - only data files are written, no master file @@ -836,7 +844,7 @@ This can only be done when detector is Idle, Error or

Request samples

Content type
application/json
{
  • "box": {
    },
  • "circle": {
    },
  • "azim": {
    }
}

Response samples

Content type
application/json
{
  • "msg": "Detector in wrong state",
  • "reason": "WrongDAQState"
}

Get general statistics

Responses

Response samples

Content type
application/json
{
  • "detector": {
    },
  • "detector_list": {
    },
  • "detector_settings": {
    },
  • "image_format_settings": {
    },
  • "instrument_metadata": {
    },
  • "file_writer_settings": {
    },
  • "data_processing_settings": {
    },
  • "measurement": {
    },
  • "broker": {
    },
  • "fpga": [
    ],
  • "calibration": [
    ],
  • "zeromq_preview": {
    },
  • "zeromq_metadata": {
    },
  • "dark_mask": {
    },
  • "pixel_mask": {
    },
  • "roi": {
    },
  • "az_int": {
    },
  • "buffer": {
    },
  • "indexing": {
    },
  • "bragg_integration": {
    },
  • "image_pusher": {
    }
}

Get data collection statistics

Results of the last data collection

+
http://localhost:5232/statistics

Response samples

Content type
application/json
{
  • "detector": {
    },
  • "detector_list": {
    },
  • "detector_settings": {
    },
  • "image_format_settings": {
    },
  • "instrument_metadata": {
    },
  • "file_writer_settings": {
    },
  • "data_processing_settings": {
    },
  • "measurement": {
    },
  • "broker": {
    },
  • "fpga": [
    ],
  • "calibration": [
    ],
  • "zeromq_preview": {
    },
  • "zeromq_metadata": {
    },
  • "dark_mask": {
    },
  • "pixel_mask": {
    },
  • "roi": {
    },
  • "az_int": {
    },
  • "buffer": {
    },
  • "indexing": {
    },
  • "bragg_integration": {
    },
  • "image_pusher": {
    }
}

Get data collection statistics

Results of the last data collection

Responses