Rugnux live status carries the report's pathology checklist (triage)

GET /rugnux/status gains an optional `triage` array (rugnux_triage_item:
label, codes, severity ok|attention|serious|not_measured, text, detail): the
SUMMARY checklist of _report.txt (ReportDocument::checks) line for line,
coloured from the same flags - absent = ok, fired = attention, or serious
for NO_LATTICE/UNUSABLE_MERGE, undetermined = not_measured. Rotation sets it
with the verdict when the run is over (the CANCELLED line in the words the
live warnings use); serial at every provisional merge and at the end, from
BuildReportDocument on that merge (StillsLiveResult).

Tests: the live list equals the report's checks; an unusable merge is
serious; a live serial merge leaves powder/ice/twinning/tNCS/anisotropy
not measured; OpenAPI conversion; receiver rotation and stills runs carry it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-11-11 16:13:30 +01:00
1 parent b7d35eb3bd
commit dec5416a99
16 files changed
+673 -2

No files matched your search

+19
View File
@@ -1294,6 +1294,25 @@ org::openapitools::server::model::Rugnux_live_status Convert(const RugnuxLiveSna
}
ret.setSummary(summary);
ret.setWarnings(input.warnings);
if (!input.triage.empty()) {
std::vector<org::openapitools::server::model::Rugnux_triage_item> triage;
for (const auto &in: input.triage) {
org::openapitools::server::model::Rugnux_triage_item item;
item.setLabel(in.label);
item.setCodes(in.codes);
switch (in.severity) {
case RugnuxLiveTriageSeverity::Ok: item.setSeverity("ok"); break;
case RugnuxLiveTriageSeverity::Attention: item.setSeverity("attention"); break;
case RugnuxLiveTriageSeverity::Serious: item.setSeverity("serious"); break;
case RugnuxLiveTriageSeverity::NotMeasured: item.setSeverity("not_measured"); break;
}
item.setText(in.text);
if (!in.detail.empty())
item.setDetail(in.detail);
triage.push_back(item);
}
ret.setTriage(triage);
}
ret.setPlacedModel(input.structure != nullptr);
ret.setRefinedModel(input.refined_structure != nullptr);
ret.setAnomalousMap(input.structure != nullptr && input.structure->HasAnomalousMap());
+49
View File
@@ -57,6 +57,7 @@ Rugnux_live_status::Rugnux_live_status()
m_Last_image_to_written_sIsSet = false;
m_Verdict = "";
m_Verdict_text = "";
m_TriageIsSet = false;
}
@@ -156,6 +157,27 @@ bool Rugnux_live_status::validate(std::stringstream& msg, const std::string& pat
i++;
}
}
}
if (triageIsSet())
{
const std::vector<org::openapitools::server::model::Rugnux_triage_item>& value = m_Triage;
const std::string currentValuePath = _pathPrefix + ".triage";
{ // Recursive validation of array elements
const std::string oldValuePath = currentValuePath;
int i = 0;
for (const org::openapitools::server::model::Rugnux_triage_item& value : value)
{
const std::string currentValuePath = oldValuePath + "[" + std::to_string(i) + "]";
success = value.validate(msg, currentValuePath + ".triage") && success;
i++;
}
}
@@ -252,8 +274,11 @@ bool Rugnux_live_status::operator==(const Rugnux_live_status& rhs) const
&&
(getWarnings() == rhs.getWarnings())
&&
((!triageIsSet() && !rhs.triageIsSet()) || (triageIsSet() && rhs.triageIsSet() && getTriage() == rhs.getTriage()))
;
}
@@ -306,6 +331,8 @@ void to_json(nlohmann::json& j, const Rugnux_live_status& o)
j["verdict_text"] = o.m_Verdict_text;
j["summary"] = o.m_Summary;
j["warnings"] = o.m_Warnings;
if(o.triageIsSet() || !o.m_Triage.empty())
j["triage"] = o.m_Triage;
}
@@ -391,6 +418,11 @@ void from_json(const nlohmann::json& j, Rugnux_live_status& o)
j.at("verdict_text").get_to(o.m_Verdict_text);
j.at("summary").get_to(o.m_Summary);
j.at("warnings").get_to(o.m_Warnings);
if(j.find("triage") != j.end())
{
j.at("triage").get_to(o.m_Triage);
o.m_TriageIsSet = true;
}
}
@@ -735,6 +767,23 @@ void Rugnux_live_status::setWarnings(std::vector<std::string> const& value)
{
m_Warnings = value;
}
std::vector<org::openapitools::server::model::Rugnux_triage_item> Rugnux_live_status::getTriage() const
{
return m_Triage;
}
void Rugnux_live_status::setTriage(std::vector<org::openapitools::server::model::Rugnux_triage_item> const& value)
{
m_Triage = value;
m_TriageIsSet = true;
}
bool Rugnux_live_status::triageIsSet() const
{
return m_TriageIsSet;
}
void Rugnux_live_status::unsetTriage()
{
m_TriageIsSet = false;
}
} // namespace org::openapitools::server::model
+10
View File
@@ -20,6 +20,7 @@
#include "Rugnux_summary_row.h"
#include "Rugnux_triage_item.h"
#include <string>
#include "Rugnux_live_model.h"
#include "Rugnux_live_step.h"
@@ -229,6 +230,13 @@ public:
/// </summary>
std::vector<std::string> getWarnings() const;
void setWarnings(std::vector<std::string> const& value);
/// <summary>
/// The pathology checklist of the report, in its order. Rotation: once the run is over; serial: at each provisional merge and at the end. Absent until there is one.
/// </summary>
std::vector<org::openapitools::server::model::Rugnux_triage_item> getTriage() const;
void setTriage(std::vector<org::openapitools::server::model::Rugnux_triage_item> const& value);
bool triageIsSet() const;
void unsetTriage();
friend void to_json(nlohmann::json& j, const Rugnux_live_status& o);
friend void from_json(const nlohmann::json& j, Rugnux_live_status& o);
@@ -289,6 +297,8 @@ protected:
std::vector<std::string> m_Warnings;
std::vector<org::openapitools::server::model::Rugnux_triage_item> m_Triage;
bool m_TriageIsSet;
};
+182
View File
@@ -0,0 +1,182 @@
/**
* 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_triage_item.h"
#include "Helpers.h"
#include <sstream>
namespace org::openapitools::server::model
{
Rugnux_triage_item::Rugnux_triage_item()
{
m_Label = "";
m_Severity = "";
m_Text = "";
m_Detail = "";
m_DetailIsSet = false;
}
void Rugnux_triage_item::validate() const
{
std::stringstream msg;
if (!validate(msg))
{
throw org::openapitools::server::helpers::ValidationException(msg.str());
}
}
bool Rugnux_triage_item::validate(std::stringstream& msg) const
{
return validate(msg, "");
}
bool Rugnux_triage_item::validate(std::stringstream& msg, const std::string& pathPrefix) const
{
bool success = true;
const std::string _pathPrefix = pathPrefix.empty() ? "Rugnux_triage_item" : pathPrefix;
/* Codes */ {
const std::vector<std::string>& value = m_Codes;
const std::string currentValuePath = _pathPrefix + ".codes";
{ // Recursive validation of array elements
const std::string oldValuePath = currentValuePath;
int i = 0;
for (const std::string& value : value)
{
const std::string currentValuePath = oldValuePath + "[" + std::to_string(i) + "]";
i++;
}
}
}
return success;
}
bool Rugnux_triage_item::operator==(const Rugnux_triage_item& rhs) const
{
return
(getLabel() == rhs.getLabel())
&&
(getCodes() == rhs.getCodes())
&&
(getSeverity() == rhs.getSeverity())
&&
(getText() == rhs.getText())
&&
((!detailIsSet() && !rhs.detailIsSet()) || (detailIsSet() && rhs.detailIsSet() && getDetail() == rhs.getDetail()))
;
}
bool Rugnux_triage_item::operator!=(const Rugnux_triage_item& rhs) const
{
return !(*this == rhs);
}
void to_json(nlohmann::json& j, const Rugnux_triage_item& o)
{
j = nlohmann::json::object();
j["label"] = o.m_Label;
j["codes"] = o.m_Codes;
j["severity"] = o.m_Severity;
j["text"] = o.m_Text;
if(o.detailIsSet())
j["detail"] = o.m_Detail;
}
void from_json(const nlohmann::json& j, Rugnux_triage_item& o)
{
j.at("label").get_to(o.m_Label);
j.at("codes").get_to(o.m_Codes);
j.at("severity").get_to(o.m_Severity);
j.at("text").get_to(o.m_Text);
if(j.find("detail") != j.end())
{
j.at("detail").get_to(o.m_Detail);
o.m_DetailIsSet = true;
}
}
std::string Rugnux_triage_item::getLabel() const
{
return m_Label;
}
void Rugnux_triage_item::setLabel(std::string const& value)
{
m_Label = value;
}
std::vector<std::string> Rugnux_triage_item::getCodes() const
{
return m_Codes;
}
void Rugnux_triage_item::setCodes(std::vector<std::string> const& value)
{
m_Codes = value;
}
std::string Rugnux_triage_item::getSeverity() const
{
return m_Severity;
}
void Rugnux_triage_item::setSeverity(std::string const& value)
{
m_Severity = value;
}
std::string Rugnux_triage_item::getText() const
{
return m_Text;
}
void Rugnux_triage_item::setText(std::string const& value)
{
m_Text = value;
}
std::string Rugnux_triage_item::getDetail() const
{
return m_Detail;
}
void Rugnux_triage_item::setDetail(std::string const& value)
{
m_Detail = value;
m_DetailIsSet = true;
}
bool Rugnux_triage_item::detailIsSet() const
{
return m_DetailIsSet;
}
void Rugnux_triage_item::unsetDetail()
{
m_DetailIsSet = false;
}
} // namespace org::openapitools::server::model
+108
View File
@@ -0,0 +1,108 @@
/**
* 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_triage_item.h
*
* A line of the pathology checklist of &#x60;_report.txt&#x60; (its SUMMARY), in the report&#39;s words. &#x60;ok&#x60;: looked for and not found; &#x60;attention&#x60;: its warning fired (its codes are in the report&#39;s PATHOLOGY_FLAGS); &#x60;serious&#x60;: a pathology that leaves no result to use - no lattice, or a merge without signal (the report&#39;s verdict FAILED or UNUSABLE); &#x60;not_measured&#x60;: it could not be looked for on this run.
*/
#ifndef Rugnux_triage_item_H_
#define Rugnux_triage_item_H_
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
namespace org::openapitools::server::model
{
/// <summary>
/// A line of the pathology checklist of &#x60;_report.txt&#x60; (its SUMMARY), in the report&#39;s words. &#x60;ok&#x60;: looked for and not found; &#x60;attention&#x60;: its warning fired (its codes are in the report&#39;s PATHOLOGY_FLAGS); &#x60;serious&#x60;: a pathology that leaves no result to use - no lattice, or a merge without signal (the report&#39;s verdict FAILED or UNUSABLE); &#x60;not_measured&#x60;: it could not be looked for on this run.
/// </summary>
class Rugnux_triage_item
{
public:
Rugnux_triage_item();
virtual ~Rugnux_triage_item() = default;
/// <summary>
/// Validate the current data in the model. Throws a ValidationException on failure.
/// </summary>
void validate() const;
/// <summary>
/// Validate the current data in the model. Returns false on error and writes an error
/// message into the given stringstream.
/// </summary>
bool validate(std::stringstream& msg) const;
/// <summary>
/// Helper overload for validate. Used when one model stores another model and calls it's validate.
/// Not meant to be called outside that case.
/// </summary>
bool validate(std::stringstream& msg, const std::string& pathPrefix) const;
bool operator==(const Rugnux_triage_item& rhs) const;
bool operator!=(const Rugnux_triage_item& rhs) const;
/////////////////////////////////////////////
/// Rugnux_triage_item members
/// <summary>
/// Name of the condition, e.g. \&quot;Twinning\&quot; - the summary row&#39;s label
/// </summary>
std::string getLabel() const;
void setLabel(std::string const& value);
/// <summary>
/// The PATHOLOGY_FLAGS codes the line stands for, e.g. TWINNING
/// </summary>
std::vector<std::string> getCodes() const;
void setCodes(std::vector<std::string> const& value);
/// <summary>
///
/// </summary>
std::string getSeverity() const;
void setSeverity(std::string const& value);
/// <summary>
/// The summary row&#39;s own words; empty where it has none
/// </summary>
std::string getText() const;
void setText(std::string const& value);
/// <summary>
/// The report&#39;s warnings that fired for it, one per line; absent where none fired
/// </summary>
std::string getDetail() const;
void setDetail(std::string const& value);
bool detailIsSet() const;
void unsetDetail();
friend void to_json(nlohmann::json& j, const Rugnux_triage_item& o);
friend void from_json(const nlohmann::json& j, Rugnux_triage_item& o);
protected:
std::string m_Label;
std::vector<std::string> m_Codes;
std::string m_Severity;
std::string m_Text;
std::string m_Detail;
bool m_DetailIsSet;
};
} // namespace org::openapitools::server::model
#endif /* Rugnux_triage_item_H_ */
+37
View File
@@ -2882,6 +2882,36 @@ components:
type: string
value:
type: string
rugnux_triage_item:
type: object
description: |
A line of the pathology checklist of `_report.txt` (its SUMMARY), in the report's words.
`ok`: looked for and not found; `attention`: its warning fired (its codes are in the report's
PATHOLOGY_FLAGS); `serious`: a pathology that leaves no result to use - no lattice, or a merge without
signal (the report's verdict FAILED or UNUSABLE); `not_measured`: it could not be looked for on this run.
required:
- label
- codes
- severity
- text
properties:
label:
type: string
description: Name of the condition, e.g. "Twinning" - the summary row's label
codes:
type: array
description: The PATHOLOGY_FLAGS codes the line stands for, e.g. TWINNING
items:
type: string
severity:
type: string
enum: [ok, attention, serious, not_measured]
text:
type: string
description: The summary row's own words; empty where it has none
detail:
type: string
description: The report's warnings that fired for it, one per line; absent where none fired
rugnux_live_model:
type: object
description: |
@@ -3263,6 +3293,13 @@ components:
type: array
items:
type: string
triage:
type: array
description: |
The pathology checklist of the report, in its order. Rotation: once the run is over; serial: at each
provisional merge and at the end. Absent until there is one.
items:
$ref: '#/components/schemas/rugnux_triage_item'
zeromq_metadata_settings:
type: object
required:
+1
View File
@@ -242,6 +242,7 @@ Class | Method | HTTP request | Description
- [RugnuxSettings](docs/RugnuxSettings.md)
- [RugnuxStatus](docs/RugnuxStatus.md)
- [RugnuxSummaryRow](docs/RugnuxSummaryRow.md)
- [RugnuxTriageItem](docs/RugnuxTriageItem.md)
- [ScanResult](docs/ScanResult.md)
- [ScanResultImagesInner](docs/ScanResultImagesInner.md)
- [SpotFindingSettings](docs/SpotFindingSettings.md)
@@ -34,6 +34,7 @@ Name | Type | Description | Notes
**verdict_text** | **str** | |
**summary** | [**List[RugnuxSummaryRow]**](RugnuxSummaryRow.md) | |
**warnings** | **List[str]** | |
**triage** | [**List[RugnuxTriageItem]**](RugnuxTriageItem.md) | The pathology checklist of the report, in its order. Rotation: once the run is over; serial: at each provisional merge and at the end. Absent until there is one. | [optional]
## Example
@@ -0,0 +1,34 @@
# RugnuxTriageItem
A line of the pathology checklist of `_report.txt` (its SUMMARY), in the report's words. `ok`: looked for and not found; `attention`: its warning fired (its codes are in the report's PATHOLOGY_FLAGS); `serious`: a pathology that leaves no result to use - no lattice, or a merge without signal (the report's verdict FAILED or UNUSABLE); `not_measured`: it could not be looked for on this run.
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**label** | **str** | Name of the condition, e.g. \&quot;Twinning\&quot; - the summary row&#39;s label |
**codes** | **List[str]** | The PATHOLOGY_FLAGS codes the line stands for, e.g. TWINNING |
**severity** | **str** | |
**text** | **str** | The summary row&#39;s own words; empty where it has none |
**detail** | **str** | The report&#39;s warnings that fired for it, one per line; absent where none fired | [optional]
## Example
```python
from jfjoch_client.models.rugnux_triage_item import RugnuxTriageItem
# TODO update the JSON string below
json = "{}"
# create an instance of RugnuxTriageItem from a JSON string
rugnux_triage_item_instance = RugnuxTriageItem.from_json(json)
# print the JSON string representation of the object
print(RugnuxTriageItem.to_json())
# convert the object into a dict
rugnux_triage_item_dict = rugnux_triage_item_instance.to_dict()
# create an instance of RugnuxTriageItem from a dict
rugnux_triage_item_from_dict = RugnuxTriageItem.from_dict(rugnux_triage_item_dict)
```
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+8 -2
View File
@@ -274,16 +274,22 @@ void JFJochReceiverRugnuxRotation::RugnuxThread() {
verdict_text = fmt::format("The collection was cancelled after {} images - these results describe "
"an incomplete sweep.", images_received);
}
const std::string cancelled_text = fmt::format("Processing was cancelled after {} images - this report "
"describes an incomplete run", images_received);
std::vector<std::string> warnings;
for (const auto &warning: doc.warnings)
if (result.cancelled && warning.code == PathologyCode::CANCELLED)
warnings.push_back(fmt::format("Processing was cancelled after {} images - this report describes "
"an incomplete run", images_received));
warnings.push_back(cancelled_text);
else if (!warning.developer_only)
warnings.push_back(warning.text);
const double cc_anom = result.merge_statistics.overall.cc_anom;
live_status.Result(verdict, verdict_text, doc.summary_rows, warnings,
std::isfinite(cc_anom) && cc_anom > CC_ANOM_SIGNAL);
std::vector<RugnuxLiveTriage> triage = LiveTriage(doc);
for (auto &t: triage)
if (t.codes == std::vector<std::string>{PathologyCode::CANCELLED})
t.text = t.detail = cancelled_text;
live_status.Triage(triage);
if (const auto live_model = LiveModel(result))
live_status.Model(*live_model);
// The numbers the summary's Space group, Resolution and Signal rows give
+27
View File
@@ -8,6 +8,7 @@
#include <cmath>
#include "../rugnux/Rugnux.h"
#include "../rugnux/ReportDocument.h"
double RugnuxLiveStatus::Since(std::chrono::steady_clock::time_point t) const {
return std::chrono::duration<double>(t - start).count();
@@ -98,6 +99,32 @@ void RugnuxLiveStatus::Result(const std::string &verdict, const std::string &ver
snapshot.anomalous_signal = anomalous_signal;
}
void RugnuxLiveStatus::Triage(const std::vector<RugnuxLiveTriage> &triage) {
std::unique_lock ul(status_mutex);
snapshot.triage = triage;
}
std::vector<RugnuxLiveTriage> LiveTriage(const ReportDocument &doc) {
std::vector<RugnuxLiveTriage> ret;
for (const auto &c: doc.checks) {
RugnuxLiveTriage t{.label = c.label, .codes = c.codes, .text = c.text, .detail = c.detail};
switch (c.status) {
case ReportCheck::Status::Absent:
t.severity = RugnuxLiveTriageSeverity::Ok;
break;
case ReportCheck::Status::Present:
t.severity = std::ranges::any_of(c.codes, PathologyIsSerious) ? RugnuxLiveTriageSeverity::Serious
: RugnuxLiveTriageSeverity::Attention;
break;
case ReportCheck::Status::Undetermined:
t.severity = RugnuxLiveTriageSeverity::NotMeasured;
break;
}
ret.push_back(t);
}
return ret;
}
void RugnuxLiveStatus::Model(const RugnuxLiveModel &model) {
std::unique_lock ul(status_mutex);
snapshot.model = model;
+20
View File
@@ -13,6 +13,7 @@
class RugnuxStructure; // RugnuxStructure.h
struct ProcessResult; // rugnux/Rugnux.h
struct ReportDocument; // rugnux/ReportDocument.h
// Where the live rugnux run of the last collection is.
// Times come from the image puller (when a message came off the socket) and are in seconds since the first image arrived.
@@ -76,6 +77,19 @@ struct RugnuxLiveModel {
std::optional<RugnuxLiveRefinement> refinement;
};
// A line of the report's pathology checklist (ReportDocument::checks), as the SUMMARY of _report.txt has it:
// Ok - looked for and not found; Attention - its warning fired; Serious - a pathology that leaves no result
// to use (PathologyIsSerious); NotMeasured - it could not be looked for on this run.
enum class RugnuxLiveTriageSeverity { Ok, Attention, Serious, NotMeasured };
struct RugnuxLiveTriage {
std::string label; // the summary row's label, e.g. "Twinning"
std::vector<std::string> codes; // the PATHOLOGY_FLAGS codes it stands for
RugnuxLiveTriageSeverity severity = RugnuxLiveTriageSeverity::NotMeasured;
std::string text; // the summary row's own words
std::string detail; // the warnings that fired, one per line
};
// Serial frames analysed on the GPU after a pre-pass (StillsGPUAnalysis)
enum class RugnuxLivePrePass { Holding, Running, Done, Failed };
@@ -115,6 +129,8 @@ struct RugnuxLiveSnapshot {
std::string verdict_text;
std::vector<std::pair<std::string, std::string>> summary;
std::vector<std::string> warnings;
// The report's pathology checklist, in its order: rotation once the run is over, serial at each merge
std::vector<RugnuxLiveTriage> triage;
bool anomalous_signal = false; // the summary's "Anomalous" row says the data carry usable signal
std::optional<RugnuxLiveModel> model; // where a model was given
// The headline numbers of the result, once the run is over
@@ -149,6 +165,7 @@ public:
void Result(const std::string &verdict, const std::string &verdict_text,
const std::vector<std::pair<std::string, std::string>> &summary,
const std::vector<std::string> &warnings, bool anomalous_signal = false);
void Triage(const std::vector<RugnuxLiveTriage> &triage);
void Model(const RugnuxLiveModel &model);
void Headline(const std::string &space_group, std::optional<double> d_min, std::optional<double> isa);
void Latency(double last_image_to_written_s);
@@ -165,5 +182,8 @@ public:
RugnuxLiveSnapshot Get() const;
};
// The pathology checklist of a report, for the live status
std::vector<RugnuxLiveTriage> LiveTriage(const ReportDocument &doc);
// What the run made of the model it was given, for the live status; none where no model was given.
std::optional<RugnuxLiveModel> LiveModel(const ProcessResult &result);
+10
View File
@@ -9,6 +9,7 @@
#include "../common/ThreadAffinity.h"
#include "../image_analysis/MXAnalysisWithoutFPGA.h"
#include "../image_analysis/WriteReflections.h"
#include "../rugnux/ResultReport.h"
#include "../rugnux/RugnuxDefaults.h"
#include "../writer/RugnuxResultBlocks.h"
#include "RugnuxStructure.h"
@@ -428,6 +429,15 @@ void StillsGPUAnalysis::MergeThread() {
}
void StillsGPUAnalysis::PublishMerge(const StillsMergeOutput &out, const std::string &title) {
// The pathology checklist of the report `rugnux` writes, for this merge
int64_t analysed, indexed;
{
std::unique_lock ul(live_mutex);
analysed = live.images_analysed;
indexed = live.images_indexed;
}
status.Triage(LiveTriage(BuildReportDocument(experiment.GetFilePrefix(), experiment.GetFilePrefix(), merge_experiment,
StillsLiveResult(merge_experiment, out, analysed, indexed))));
const auto &overall = out.merge.statistics.overall;
const std::string text = fmt::format("Merge of {}: {} crystals, {:.2f} A, CC1/2 {:.3f}, R_split {:.3f}, "
"completeness {:.1f}%, <I/sigma> {:.1f} ({:.1f} s)", title, out.images,
+1
View File
@@ -127,6 +127,7 @@ ADD_EXECUTABLE(jfjoch_test
ModuleRefinementTest.cpp
LiveCollectionTest.cpp
RugnuxLiveStatusTest.cpp
RugnuxLiveTriageTest.cpp
StillsGPUAnalysisTest.cpp
StillsGPUAnalysisFPGATest.cpp
RugnuxStructureTest.cpp
+10
View File
@@ -193,6 +193,14 @@ TEST_CASE("JFJochReceiverRugnux_Stills", "[JFJochReceiverRugnux]") {
CHECK(HasNote(live, "Merge of 50% of the images (provisional)"));
CHECK(HasNote(live, "Merge of 75% of the images (provisional)"));
CHECK(HasNote(live, "Merge of the collection"));
// The report's checklist of the merge: completeness looked at, powder rings not (no pre-scan here)
const auto triage_line = [&](const std::string &label) {
return std::ranges::find(live.triage, label, &RugnuxLiveTriage::label);
};
REQUIRE(triage_line("Completeness") != live.triage.end());
CHECK(triage_line("Completeness")->severity != RugnuxLiveTriageSeverity::NotMeasured);
REQUIRE(triage_line("Powder") != live.triage.end());
CHECK(triage_line("Powder")->severity == RugnuxLiveTriageSeverity::NotMeasured);
REQUIRE(live.stills);
CHECK(live.stills->prepass == RugnuxLivePrePass::Done);
CHECK(live.stills->images_analysed == images);
@@ -693,6 +701,8 @@ TEST_CASE("JFJochReceiverRugnux_Rotation", "[JFJochReceiverRugnux]") {
CHECK(live.images_received == images);
CHECK(live.state != RugnuxLiveState::Idle);
CHECK(live.state != RugnuxLiveState::Running);
// The report's checklist comes with its verdict
CHECK(live.verdict.empty() == live.triage.empty());
}
TEST_CASE("JFJochReceiverRugnux_Rotation_DoesNotFit", "[JFJochReceiverRugnux]") {
+156
View File
@@ -0,0 +1,156 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <catch2/catch_all.hpp>
#include "../broker/OpenAPIConvert.h"
#include "../receiver/RugnuxLiveStatus.h"
#include "../rugnux/ResultReport.h"
#include "../rugnux/StillsLiveMerge.h"
namespace {
const RugnuxLiveTriage *Find(const std::vector<RugnuxLiveTriage> &triage, const std::string &label) {
for (const auto &t: triage)
if (t.label == label)
return &t;
return nullptr;
}
// A merge to 2 A whose shells inside the fitted resolution are far from complete
ProcessResult ThinMerge() {
ProcessResult result;
result.images_processed = 100;
result.indexing_rate = 0.9f;
result.consensus_cell = UnitCell{.a = 50.0f, .b = 60.0f, .c = 70.0f,
.alpha = 90.0f, .beta = 90.0f, .gamma = 90.0f};
result.has_merge_statistics = true;
auto &o = result.merge_statistics.overall;
o.d_max = 50.0f;
o.d_min = 2.00f;
o.unique_reflections = 1000;
o.possible_unique_reflections = 1100;
o.total_observations = 4000;
o.mean_i_over_sigma = 12.0;
o.cc_half = 0.98;
result.resolution_fit_A = 2.0;
MergeStatisticsShell shell;
shell.d_max = 50.0f;
shell.d_min = 2.0f;
shell.cc_half = shell.cc_half_internal = 0.98;
shell.unique_reflections = 300;
shell.possible_unique_reflections = 2000;
result.merge_statistics.shells.push_back(shell);
return result;
}
}
// The live list is the report's checklist line for line: same labels, codes and words, and its colour read
// off the same flags - so the live view and _report.txt cannot disagree.
TEST_CASE("RugnuxLiveTriage_SameAsReport", "[RugnuxLive]") {
DiffractionExperiment x(DetJF(1));
const ProcessResult result = ThinMerge();
const ReportDocument doc = BuildReportDocument("p", "in.h5", x, result);
const auto triage = LiveTriage(doc);
REQUIRE(triage.size() == doc.checks.size());
REQUIRE(!triage.empty());
for (size_t i = 0; i < triage.size(); i++) {
CHECK(triage[i].label == doc.checks[i].label);
CHECK(triage[i].codes == doc.checks[i].codes);
CHECK(triage[i].text == doc.checks[i].text);
CHECK(triage[i].detail == doc.checks[i].detail);
switch (doc.checks[i].status) {
case ReportCheck::Status::Absent:
CHECK(triage[i].severity == RugnuxLiveTriageSeverity::Ok);
break;
case ReportCheck::Status::Present:
CHECK((triage[i].severity == RugnuxLiveTriageSeverity::Attention
|| triage[i].severity == RugnuxLiveTriageSeverity::Serious));
break;
case ReportCheck::Status::Undetermined:
CHECK(triage[i].severity == RugnuxLiveTriageSeverity::NotMeasured);
break;
}
}
// Every flag of the verdict line is a coloured entry
REQUIRE(!doc.pathology_flags.empty());
for (const auto &code: doc.pathology_flags) {
bool found = false;
for (const auto &t: triage)
if (std::ranges::find(t.codes, code) != t.codes.end())
found = found || t.severity == RugnuxLiveTriageSeverity::Attention
|| t.severity == RugnuxLiveTriageSeverity::Serious;
CHECK(found);
}
const auto *completeness = Find(triage, "Completeness");
REQUIRE(completeness);
CHECK(completeness->severity == RugnuxLiveTriageSeverity::Attention);
CHECK(!completeness->detail.empty());
const auto *twinning = Find(triage, "Twinning");
REQUIRE(twinning);
CHECK(twinning->severity == RugnuxLiveTriageSeverity::NotMeasured);
}
// A merge without signal leaves no result to use: the one red entry
TEST_CASE("RugnuxLiveTriage_UnusableMergeIsSerious", "[RugnuxLive]") {
DiffractionExperiment x(DetJF(1));
ProcessResult result = ThinMerge();
result.merge_statistics.overall.cc_half = -0.005;
const ReportDocument doc = BuildReportDocument("p", "in.h5", x, result);
REQUIRE(doc.verdict == "UNUSABLE");
const auto triage = LiveTriage(doc);
const auto *unusable = Find(triage, "Unusable merge");
REQUIRE(unusable);
CHECK(unusable->severity == RugnuxLiveTriageSeverity::Serious);
CHECK(Find(triage, "Completeness")->severity == RugnuxLiveTriageSeverity::Attention);
}
// The live serial merge has no pre-scan and runs no twinning or pseudo-translation analysis: those are not
// measured, and the report says so rather than "no rings detected".
TEST_CASE("RugnuxLiveTriage_StillsMergeClaimsOnlyWhatItMeasured", "[RugnuxLive]") {
DiffractionExperiment x(DetJF(1));
x.SetUnitCell(UnitCell{.a = 50.0f, .b = 60.0f, .c = 70.0f, .alpha = 90.0f, .beta = 90.0f, .gamma = 90.0f});
StillsMergeOutput out;
out.merge.statistics = ThinMerge().merge_statistics;
out.merge.statistics.stills = true;
out.merge.resolution_fit_A = 2.0;
out.images = 90;
const ProcessResult result = StillsLiveResult(x, out, 100, 90);
const auto triage = LiveTriage(BuildReportDocument("p", "in.h5", x, result));
for (const char *label: {"Powder", "Ice", "Twinning", "Pseudo-symmetry", "Multiple lattices", "Anisotropy"}) {
const auto *t = Find(triage, label);
REQUIRE(t);
CHECK(t->severity == RugnuxLiveTriageSeverity::NotMeasured);
}
CHECK(Find(triage, "Completeness")->severity == RugnuxLiveTriageSeverity::Attention);
}
TEST_CASE("RugnuxLiveTriage_OpenAPI", "[RugnuxLive]") {
RugnuxLiveStatus status;
status.Start(10, std::nullopt, std::chrono::steady_clock::now());
CHECK(!Convert(status.Get()).triageIsSet());
status.Triage({
{.label = "Twinning", .codes = {"TWINNING"}, .severity = RugnuxLiveTriageSeverity::Ok, .text = "no indication"},
{.label = "Completeness", .codes = {"LOW_COMPLETENESS"}, .severity = RugnuxLiveTriageSeverity::Attention,
.text = "40.0 %", .detail = "only 40 % of reciprocal space"},
{.label = "Unusable merge", .codes = {"UNUSABLE_MERGE"}, .severity = RugnuxLiveTriageSeverity::Serious},
{.label = "Ice", .codes = {"ICE_RINGS"}, .severity = RugnuxLiveTriageSeverity::NotMeasured,
.text = "not measured"}
});
const auto api = Convert(status.Get());
REQUIRE(api.triageIsSet());
const auto items = api.getTriage();
REQUIRE(items.size() == 4);
CHECK(items[0].getLabel() == "Twinning");
CHECK(items[0].getCodes() == std::vector<std::string>{"TWINNING"});
CHECK(items[0].getSeverity() == "ok");
CHECK(!items[0].detailIsSet());
CHECK(items[1].getSeverity() == "attention");
CHECK(items[1].getDetail() == "only 40 % of reciprocal space");
CHECK(items[2].getSeverity() == "serious");
CHECK(items[3].getSeverity() == "not_measured");
}