From cb2dd697ddf2680daf1667f1b6f40aeb79ebea97 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Wed, 11 Nov 2026 22:24:06 +0100 Subject: [PATCH] API: the per-image scale and the frames the merge left out in the rugnux live status A rotation run now leaves two more things in `GET /rugnux/status` once it is over: `frame_scales`, the per-image scale factor of the merge (one entry per image, 0 where the image got none), and `frame_ranges`, the stretches of the sweep the merge downgraded or rejected, each with its disposition and the reason the sweep-quality diagnostic saw there - which frames rugnux left out, and why. Both come from what the run already has (the end message's scale vector and SweepQuality's per-image dispositions, run-length encoded with the reason of the range they lie in); nothing is computed anew. The C++ model, the Redoc page, the TypeScript client and the python client docs are regenerated from the YAML. RugnuxLiveFrames_OpenAPI covers the encoding and the conversion. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK --- broker/OpenAPIConvert.cpp | 16 +++ broker/gen/model/Rugnux_live_frame_range.cpp | 133 ++++++++++++++++++ broker/gen/model/Rugnux_live_frame_range.h | 98 +++++++++++++ broker/gen/model/Rugnux_live_status.cpp | 100 ++++++++++++- broker/gen/model/Rugnux_live_status.h | 19 +++ broker/jfjoch_api.yaml | 43 ++++++ broker/redoc-static.html | 4 +- docs/JFJOCH_BROKER.md | 7 +- .../docs/RugnuxLiveFrameRange.md | 33 +++++ docs/python_client/docs/RugnuxLiveStatus.md | 2 + frontend/src/client/index.ts | 2 +- frontend/src/client/types.gen.ts | 36 +++++ frontend/src/client/zod.gen.ts | 15 ++ receiver/JFJochReceiverRugnuxRotation.cpp | 1 + receiver/RugnuxLiveStatus.cpp | 28 ++++ receiver/RugnuxLiveStatus.h | 17 +++ tests/RugnuxLiveTriageTest.cpp | 34 +++++ 17 files changed, 582 insertions(+), 6 deletions(-) create mode 100644 broker/gen/model/Rugnux_live_frame_range.cpp create mode 100644 broker/gen/model/Rugnux_live_frame_range.h create mode 100644 docs/python_client/docs/RugnuxLiveFrameRange.md diff --git a/broker/OpenAPIConvert.cpp b/broker/OpenAPIConvert.cpp index 1b7e567c3..a9f7800c8 100644 --- a/broker/OpenAPIConvert.cpp +++ b/broker/OpenAPIConvert.cpp @@ -1380,6 +1380,22 @@ org::openapitools::server::model::Rugnux_live_status Convert(const RugnuxLiveSna } if (input.overall) ret.setOverall(convert_shell(input.overall.value())); + if (!input.frame_scales.empty()) { + std::vector scales; // JSON has no NaN: an image without a scale carries 0 + for (float g: input.frame_scales) + scales.push_back(std::isfinite(g) ? g : 0.0f); + ret.setFrameScales(scales); + std::vector ranges; + for (const auto &in: input.frame_ranges) { + org::openapitools::server::model::Rugnux_live_frame_range r; + r.setFirstImage(in.first_image); + r.setLastImage(in.last_image); + r.setDisposition(in.disposition); + r.setReason(in.reason); + ranges.push_back(r); + } + ret.setFrameRanges(ranges); + } if (input.model) { const RugnuxLiveModel &m = input.model.value(); org::openapitools::server::model::Rugnux_live_model model; diff --git a/broker/gen/model/Rugnux_live_frame_range.cpp b/broker/gen/model/Rugnux_live_frame_range.cpp new file mode 100644 index 000000000..2f767fb33 --- /dev/null +++ b/broker/gen/model/Rugnux_live_frame_range.cpp @@ -0,0 +1,133 @@ +/** +* Jungfraujoch +* API to control Jungfraujoch developed by the Paul Scherrer Institute (Switzerland). Jungfraujoch is a data acquisition and analysis system for pixel array detectors, primarly PSI JUNGFRAU. Jungfraujoch uses FPGA boards to acquire data at high data rates. # License Clarification While this API definition is licensed under GPL-3.0, **the GPL copyleft provisions do not apply** when this file is used solely to generate OpenAPI clients or when implementing applications that interact with the API. Generated client code and applications using this API definition are not subject to the GPL license requirements and may be distributed under terms of your choosing. This exception is similar in spirit to the Linux Kernel's approach to userspace API headers and the GCC Runtime Library Exception. The Linux Kernel developers have explicitly stated that user programs that merely use the kernel interfaces (syscalls, ioctl definitions, etc.) are not derivative works of the kernel and are not subject to the terms of the GPL. This exception is intended to allow wider use of this API specification without imposing GPL requirements on applications that merely interact with the API, regardless of whether they communicate through network calls or other mechanisms. +* +* The version of the OpenAPI document: 1.0.0-rc.186 +* Contact: filip.leonarski@psi.ch +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ + + +#include "Rugnux_live_frame_range.h" +#include "Helpers.h" + +#include + +namespace org::openapitools::server::model +{ + +Rugnux_live_frame_range::Rugnux_live_frame_range() +{ + m_First_image = 0L; + m_Last_image = 0L; + m_Disposition = ""; + m_Reason = ""; + +} + +void Rugnux_live_frame_range::validate() const +{ + std::stringstream msg; + if (!validate(msg)) + { + throw org::openapitools::server::helpers::ValidationException(msg.str()); + } +} + +bool Rugnux_live_frame_range::validate(std::stringstream& msg) const +{ + return validate(msg, ""); +} + +bool Rugnux_live_frame_range::validate(std::stringstream& msg, const std::string& pathPrefix) const +{ + bool success = true; + const std::string _pathPrefix = pathPrefix.empty() ? "Rugnux_live_frame_range" : pathPrefix; + + + return success; +} + +bool Rugnux_live_frame_range::operator==(const Rugnux_live_frame_range& rhs) const +{ + return + + + (getFirstImage() == rhs.getFirstImage()) + && + + (getLastImage() == rhs.getLastImage()) + && + + (getDisposition() == rhs.getDisposition()) + && + + (getReason() == rhs.getReason()) + + + ; +} + +bool Rugnux_live_frame_range::operator!=(const Rugnux_live_frame_range& rhs) const +{ + return !(*this == rhs); +} + +void to_json(nlohmann::json& j, const Rugnux_live_frame_range& o) +{ + j = nlohmann::json::object(); + j["first_image"] = o.m_First_image; + j["last_image"] = o.m_Last_image; + j["disposition"] = o.m_Disposition; + j["reason"] = o.m_Reason; + +} + +void from_json(const nlohmann::json& j, Rugnux_live_frame_range& o) +{ + j.at("first_image").get_to(o.m_First_image); + j.at("last_image").get_to(o.m_Last_image); + j.at("disposition").get_to(o.m_Disposition); + j.at("reason").get_to(o.m_Reason); + +} + +int64_t Rugnux_live_frame_range::getFirstImage() const +{ + return m_First_image; +} +void Rugnux_live_frame_range::setFirstImage(int64_t const value) +{ + m_First_image = value; +} +int64_t Rugnux_live_frame_range::getLastImage() const +{ + return m_Last_image; +} +void Rugnux_live_frame_range::setLastImage(int64_t const value) +{ + m_Last_image = value; +} +std::string Rugnux_live_frame_range::getDisposition() const +{ + return m_Disposition; +} +void Rugnux_live_frame_range::setDisposition(std::string const& value) +{ + m_Disposition = value; +} +std::string Rugnux_live_frame_range::getReason() const +{ + return m_Reason; +} +void Rugnux_live_frame_range::setReason(std::string const& value) +{ + m_Reason = value; +} + + +} // namespace org::openapitools::server::model + diff --git a/broker/gen/model/Rugnux_live_frame_range.h b/broker/gen/model/Rugnux_live_frame_range.h new file mode 100644 index 000000000..4420507de --- /dev/null +++ b/broker/gen/model/Rugnux_live_frame_range.h @@ -0,0 +1,98 @@ +/** +* Jungfraujoch +* API to control Jungfraujoch developed by the Paul Scherrer Institute (Switzerland). Jungfraujoch is a data acquisition and analysis system for pixel array detectors, primarly PSI JUNGFRAU. Jungfraujoch uses FPGA boards to acquire data at high data rates. # License Clarification While this API definition is licensed under GPL-3.0, **the GPL copyleft provisions do not apply** when this file is used solely to generate OpenAPI clients or when implementing applications that interact with the API. Generated client code and applications using this API definition are not subject to the GPL license requirements and may be distributed under terms of your choosing. This exception is similar in spirit to the Linux Kernel's approach to userspace API headers and the GCC Runtime Library Exception. The Linux Kernel developers have explicitly stated that user programs that merely use the kernel interfaces (syscalls, ioctl definitions, etc.) are not derivative works of the kernel and are not subject to the terms of the GPL. This exception is intended to allow wider use of this API specification without imposing GPL requirements on applications that merely interact with the API, regardless of whether they communicate through network calls or other mechanisms. +* +* The version of the OpenAPI document: 1.0.0-rc.186 +* Contact: filip.leonarski@psi.ch +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +/* + * Rugnux_live_frame_range.h + * + * A stretch of a rotation sweep the merge did not take at face value: downgraded (merged at the reduced weight its own scale and sigmas give it) or rejected (nothing of it reached the merge), with the reason the sweep-quality diagnostic saw - the lines of the report's SWEEP section. + */ + +#ifndef Rugnux_live_frame_range_H_ +#define Rugnux_live_frame_range_H_ + + +#include +#include + +namespace org::openapitools::server::model +{ + +/// +/// A stretch of a rotation sweep the merge did not take at face value: downgraded (merged at the reduced weight its own scale and sigmas give it) or rejected (nothing of it reached the merge), with the reason the sweep-quality diagnostic saw - the lines of the report's SWEEP section. +/// +class Rugnux_live_frame_range +{ +public: + Rugnux_live_frame_range(); + virtual ~Rugnux_live_frame_range() = default; + + + /// + /// Validate the current data in the model. Throws a ValidationException on failure. + /// + void validate() const; + + /// + /// Validate the current data in the model. Returns false on error and writes an error + /// message into the given stringstream. + /// + bool validate(std::stringstream& msg) const; + + /// + /// Helper overload for validate. Used when one model stores another model and calls it's validate. + /// Not meant to be called outside that case. + /// + bool validate(std::stringstream& msg, const std::string& pathPrefix) const; + + bool operator==(const Rugnux_live_frame_range& rhs) const; + bool operator!=(const Rugnux_live_frame_range& rhs) const; + + ///////////////////////////////////////////// + /// Rugnux_live_frame_range members + + /// + /// First image of the stretch, inclusive, numbered as the collection's images + /// + int64_t getFirstImage() const; + void setFirstImage(int64_t const value); + /// + /// Last image of the stretch, inclusive + /// + int64_t getLastImage() const; + void setLastImage(int64_t const value); + /// + /// + /// + std::string getDisposition() const; + void setDisposition(std::string const& value); + /// + /// What was seen over the stretch, e.g. \"out of beam\", \"no diffraction\", \"radiation damage\" + /// + std::string getReason() const; + void setReason(std::string const& value); + + friend void to_json(nlohmann::json& j, const Rugnux_live_frame_range& o); + friend void from_json(const nlohmann::json& j, Rugnux_live_frame_range& o); +protected: + int64_t m_First_image; + + int64_t m_Last_image; + + std::string m_Disposition; + + std::string m_Reason; + + +}; + +} // namespace org::openapitools::server::model + +#endif /* Rugnux_live_frame_range_H_ */ diff --git a/broker/gen/model/Rugnux_live_status.cpp b/broker/gen/model/Rugnux_live_status.cpp index c5d1e8c7f..0eb575857 100644 --- a/broker/gen/model/Rugnux_live_status.cpp +++ b/broker/gen/model/Rugnux_live_status.cpp @@ -32,6 +32,8 @@ Rugnux_live_status::Rugnux_live_status() m_Shells_mergeIsSet = false; m_ShellsIsSet = false; m_OverallIsSet = false; + m_Frame_scalesIsSet = false; + m_Frame_rangesIsSet = false; m_Space_group = ""; m_Space_groupIsSet = false; m_D_min = 0.0; @@ -105,7 +107,49 @@ bool Rugnux_live_status::validate(std::stringstream& msg, const std::string& pat } } - + + if (frameScalesIsSet()) + { + const std::vector& value = m_Frame_scales; + const std::string currentValuePath = _pathPrefix + ".frameScales"; + + + { // Recursive validation of array elements + const std::string oldValuePath = currentValuePath; + int i = 0; + for (const float& value : value) + { + const std::string currentValuePath = oldValuePath + "[" + std::to_string(i) + "]"; + + + + i++; + } + } + + } + + if (frameRangesIsSet()) + { + const std::vector& value = m_Frame_ranges; + const std::string currentValuePath = _pathPrefix + ".frameRanges"; + + + { // Recursive validation of array elements + const std::string oldValuePath = currentValuePath; + int i = 0; + for (const org::openapitools::server::model::Rugnux_live_frame_range& value : value) + { + const std::string currentValuePath = oldValuePath + "[" + std::to_string(i) + "]"; + + success = value.validate(msg, currentValuePath + ".frameRanges") && success; + + i++; + } + } + + } + if (stepProgressIsSet()) { const float& value = m_Step_progress; @@ -248,6 +292,12 @@ bool Rugnux_live_status::operator==(const Rugnux_live_status& rhs) const ((!overallIsSet() && !rhs.overallIsSet()) || (overallIsSet() && rhs.overallIsSet() && getOverall() == rhs.getOverall())) && + ((!frameScalesIsSet() && !rhs.frameScalesIsSet()) || (frameScalesIsSet() && rhs.frameScalesIsSet() && getFrameScales() == rhs.getFrameScales())) && + + + ((!frameRangesIsSet() && !rhs.frameRangesIsSet()) || (frameRangesIsSet() && rhs.frameRangesIsSet() && getFrameRanges() == rhs.getFrameRanges())) && + + ((!spaceGroupIsSet() && !rhs.spaceGroupIsSet()) || (spaceGroupIsSet() && rhs.spaceGroupIsSet() && getSpaceGroup() == rhs.getSpaceGroup())) && @@ -339,6 +389,10 @@ void to_json(nlohmann::json& j, const Rugnux_live_status& o) j["shells"] = o.m_Shells; if(o.overallIsSet()) j["overall"] = o.m_Overall; + if(o.frameScalesIsSet() || !o.m_Frame_scales.empty()) + j["frame_scales"] = o.m_Frame_scales; + if(o.frameRangesIsSet() || !o.m_Frame_ranges.empty()) + j["frame_ranges"] = o.m_Frame_ranges; if(o.spaceGroupIsSet()) j["space_group"] = o.m_Space_group; if(o.dMinIsSet()) @@ -408,6 +462,16 @@ void from_json(const nlohmann::json& j, Rugnux_live_status& o) j.at("overall").get_to(o.m_Overall); o.m_OverallIsSet = true; } + if(j.find("frame_scales") != j.end()) + { + j.at("frame_scales").get_to(o.m_Frame_scales); + o.m_Frame_scalesIsSet = true; + } + if(j.find("frame_ranges") != j.end()) + { + j.at("frame_ranges").get_to(o.m_Frame_ranges); + o.m_Frame_rangesIsSet = true; + } if(j.find("space_group") != j.end()) { j.at("space_group").get_to(o.m_Space_group); @@ -606,6 +670,40 @@ void Rugnux_live_status::unsetOverall() { m_OverallIsSet = false; } +std::vector Rugnux_live_status::getFrameScales() const +{ + return m_Frame_scales; +} +void Rugnux_live_status::setFrameScales(std::vector const value) +{ + m_Frame_scales = value; + m_Frame_scalesIsSet = true; +} +bool Rugnux_live_status::frameScalesIsSet() const +{ + return m_Frame_scalesIsSet; +} +void Rugnux_live_status::unsetFrame_scales() +{ + m_Frame_scalesIsSet = false; +} +std::vector Rugnux_live_status::getFrameRanges() const +{ + return m_Frame_ranges; +} +void Rugnux_live_status::setFrameRanges(std::vector const& value) +{ + m_Frame_ranges = value; + m_Frame_rangesIsSet = true; +} +bool Rugnux_live_status::frameRangesIsSet() const +{ + return m_Frame_rangesIsSet; +} +void Rugnux_live_status::unsetFrame_ranges() +{ + m_Frame_rangesIsSet = false; +} std::string Rugnux_live_status::getSpaceGroup() const { return m_Space_group; diff --git a/broker/gen/model/Rugnux_live_status.h b/broker/gen/model/Rugnux_live_status.h index 3cf7590d1..1113d4d96 100644 --- a/broker/gen/model/Rugnux_live_status.h +++ b/broker/gen/model/Rugnux_live_status.h @@ -25,6 +25,7 @@ #include #include "Rugnux_live_model.h" #include "Rugnux_live_step.h" +#include "Rugnux_live_frame_range.h" #include #include "Rugnux_live_stills.h" #include @@ -126,6 +127,20 @@ public: bool overallIsSet() const; void unsetOverall(); /// + /// Rotation, once the run is over: the per-image scale factor of the merge, one entry per image of the collection in its order, 0 where the image got none (not indexed, or rejected). Absent on serial data and until there is a merge. + /// + std::vector getFrameScales() const; + void setFrameScales(std::vector const value); + bool frameScalesIsSet() const; + void unsetFrame_scales(); + /// + /// Rotation, once the run is over: the stretches of the sweep the merge downgraded or rejected, in order; every image outside them is merged at its own weight. Absent on serial data and until there is a merge; empty where the whole sweep was merged. + /// + std::vector getFrameRanges() const; + void setFrameRanges(std::vector const& value); + bool frameRangesIsSet() const; + void unsetFrame_ranges(); + /// /// Space group of the result (the summary's \"Space group\" row); absent until the run is over /// std::string getSpaceGroup() const; @@ -283,6 +298,10 @@ protected: bool m_ShellsIsSet; org::openapitools::server::model::Rugnux_live_shell m_Overall; bool m_OverallIsSet; + std::vector m_Frame_scales; + bool m_Frame_scalesIsSet; + std::vector m_Frame_ranges; + bool m_Frame_rangesIsSet; std::string m_Space_group; bool m_Space_groupIsSet; double m_D_min; diff --git a/broker/jfjoch_api.yaml b/broker/jfjoch_api.yaml index 5e3e0fa5e..5201be5e1 100644 --- a/broker/jfjoch_api.yaml +++ b/broker/jfjoch_api.yaml @@ -3225,6 +3225,32 @@ components: type: number format: double description: R-free of the placed model (as given, not refined) over the shell's free reflections + rugnux_live_frame_range: + type: object + description: | + A stretch of a rotation sweep the merge did not take at face value: downgraded (merged at the reduced + weight its own scale and sigmas give it) or rejected (nothing of it reached the merge), with the + reason the sweep-quality diagnostic saw - the lines of the report's SWEEP section. + required: + - first_image + - last_image + - disposition + - reason + properties: + first_image: + type: integer + format: int64 + description: First image of the stretch, inclusive, numbered as the collection's images + last_image: + type: integer + format: int64 + description: Last image of the stretch, inclusive + disposition: + type: string + enum: [downgraded, rejected] + reason: + type: string + description: What was seen over the stretch, e.g. "out of beam", "no diffraction", "radiation damage" rugnux_live_status: type: object description: | @@ -3289,6 +3315,23 @@ components: $ref: '#/components/schemas/rugnux_live_shell' overall: $ref: '#/components/schemas/rugnux_live_shell' + frame_scales: + type: array + description: | + Rotation, once the run is over: the per-image scale factor of the merge, one entry per image of the + collection in its order, 0 where the image got none (not indexed, or rejected). Absent on serial + data and until there is a merge. + items: + type: number + format: float + frame_ranges: + type: array + description: | + Rotation, once the run is over: the stretches of the sweep the merge downgraded or rejected, in + order; every image outside them is merged at its own weight. Absent on serial data and until there + is a merge; empty where the whole sweep was merged. + items: + $ref: '#/components/schemas/rugnux_live_frame_range' space_group: type: string description: Space group of the result (the summary's "Space group" row); absent until the run is over diff --git a/broker/redoc-static.html b/broker/redoc-static.html index 7e1a31eda..d7da8212d 100644 --- a/broker/redoc-static.html +++ b/broker/redoc-static.html @@ -885,7 +885,7 @@ and validation of every following rugnux live collection, until it is removed wi
Authorizations:
bearerAuth

Responses

Response samples

Content type
application/json
{
  • "state": "Idle",
  • "placed_model": true,
  • "refined_model": true,
  • "anomalous_map": true,
  • "anomalous_signal": true,
  • "model": {
    },
  • "stills": {
    },
  • "shells_merge": "string",
  • "shells": [
    ],
  • "space_group": "string",
  • "d_min": 0.1,
  • "isa": 0.1,
  • "probe": "string",
  • "probe_s": 0.1,
  • "start_to_first_image_s": 0.1,
  • "message": "string",
  • "images_expected": 0,
  • "expected_collection_s": 0.1,
  • "images_received": 0,
  • "elapsed_s": 0.1,
  • "step_progress": 1,
  • "steps": [
    ],
  • "last_image_s": 0.1,
  • "cut": "string",
  • "cut_s": 0.1,
  • "last_image_to_written_s": 0.1,
  • "verdict": "string",
  • "verdict_text": "string",
  • "summary": [
    ],
  • "warnings": [
    ]
}

Get the model as placed by the last rugnux live run

The model placed against the data by molecular replacement or validation, as mmCIF - the same text as the +

http://localhost:5232/rugnux/status

Response samples

Content type
application/json
{
  • "state": "Idle",
  • "placed_model": true,
  • "refined_model": true,
  • "anomalous_map": true,
  • "anomalous_signal": true,
  • "model": {
    },
  • "stills": {
    },
  • "shells_merge": "string",
  • "shells": [
    ],
  • "overall": {
    },
  • "frame_scales": [
    ],
  • "frame_ranges": [
    ],
  • "space_group": "string",
  • "d_min": 0.1,
  • "isa": 0.1,
  • "probe": "string",
  • "probe_s": 0.1,
  • "start_to_first_image_s": 0.1,
  • "message": "string",
  • "images_expected": 0,
  • "expected_collection_s": 0.1,
  • "images_received": 0,
  • "elapsed_s": 0.1,
  • "step_progress": 1,
  • "steps": [
    ],
  • "last_image_s": 0.1,
  • "cut": "string",
  • "cut_s": 0.1,
  • "last_image_to_written_s": 0.1,
  • "verdict": "string",
  • "verdict_text": "string",
  • "summary": [
    ],
  • "warnings": [
    ],
  • "triage": [
    ]
}

Get the model as placed by the last rugnux live run

The model placed against the data by molecular replacement or validation, as mmCIF - the same text as the _model.cif file written beside the data: the data's cell and space group, with the input model's own metadata (entities, sequences, references, authors, software) kept. Kept as long as /rugnux/status. Where the run refined the model, the refined model unless asked for stage=placed.

@@ -1144,7 +1144,7 @@ then image might be replaced in the buffer between calling /images and /image.cb