rugnux live: the whole-merge statistics as numbers - the Overall row of the shell table

The live status carried the per-shell table as numbers but the whole-merge completeness,
multiplicity, I/sigma, CC1/2, R_meas/R_split and CCref only as prose in the summary rows, so the
frontend had to parse text for its headline numbers. `overall` (a `rugnux_live_shell`, optional,
present with `shells`) is now the "Overall" row of the same table, converted by the same code as the
shell rows (LiveShell), with the overall R-free of the model validated on that merge added where
the per-shell R-free is. Rotation sets it with the merge of the collection, serial at each
provisional merge and at the end. The test compares it with the "Overall" line `_report.txt` prints.

Regenerated: broker/gen (Rugnux_live_status), the frontend client and the python client docs.

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:57:06 +01:00
1 parent 5f5dfc6ae3
commit d5ec4920a1
12 files changed
+155 -53

No files matched your search

+20 -16
View File
@@ -1355,27 +1355,31 @@ org::openapitools::server::model::Rugnux_live_status Convert(const RugnuxLiveSna
stills.setAnalysisRateHz(st.analysis_rate_hz.value());
ret.setStills(stills);
}
const auto convert_shell = [](const RugnuxLiveShell &in) {
org::openapitools::server::model::Rugnux_live_shell sh;
sh.setDMin(in.d_min);
sh.setDMax(in.d_max);
sh.setNObs(in.n_obs);
sh.setNUnique(in.n_unique);
if (in.completeness) sh.setCompleteness(in.completeness.value());
if (in.multiplicity) sh.setMultiplicity(in.multiplicity.value());
if (in.i_over_sigma) sh.setIOverSigma(in.i_over_sigma.value());
if (in.r_meas) sh.setRMeas(in.r_meas.value());
if (in.cc_half) sh.setCcHalf(in.cc_half.value());
if (in.cc_ref) sh.setCcRef(in.cc_ref.value());
if (in.r_split) sh.setRSplit(in.r_split.value());
if (in.r_free) sh.setRFree(in.r_free.value());
return sh;
};
if (!input.shells.empty()) {
ret.setShellsMerge(input.shells_merge);
std::vector<org::openapitools::server::model::Rugnux_live_shell> shells;
for (const auto &in: input.shells) {
org::openapitools::server::model::Rugnux_live_shell sh;
sh.setDMin(in.d_min);
sh.setDMax(in.d_max);
sh.setNObs(in.n_obs);
sh.setNUnique(in.n_unique);
if (in.completeness) sh.setCompleteness(in.completeness.value());
if (in.multiplicity) sh.setMultiplicity(in.multiplicity.value());
if (in.i_over_sigma) sh.setIOverSigma(in.i_over_sigma.value());
if (in.r_meas) sh.setRMeas(in.r_meas.value());
if (in.cc_half) sh.setCcHalf(in.cc_half.value());
if (in.cc_ref) sh.setCcRef(in.cc_ref.value());
if (in.r_split) sh.setRSplit(in.r_split.value());
if (in.r_free) sh.setRFree(in.r_free.value());
shells.push_back(sh);
}
for (const auto &in: input.shells)
shells.push_back(convert_shell(in));
ret.setShells(shells);
}
if (input.overall)
ret.setOverall(convert_shell(input.overall.value()));
if (input.model) {
const RugnuxLiveModel &m = input.model.value();
org::openapitools::server::model::Rugnux_live_model model;
+29 -1
View File
@@ -31,6 +31,7 @@ Rugnux_live_status::Rugnux_live_status()
m_Shells_merge = "";
m_Shells_mergeIsSet = false;
m_ShellsIsSet = false;
m_OverallIsSet = false;
m_Space_group = "";
m_Space_groupIsSet = false;
m_D_min = 0.0;
@@ -104,7 +105,7 @@ bool Rugnux_live_status::validate(std::stringstream& msg, const std::string& pat
}
}
if (stepProgressIsSet())
{
const float& value = m_Step_progress;
@@ -244,6 +245,9 @@ bool Rugnux_live_status::operator==(const Rugnux_live_status& rhs) const
((!shellsIsSet() && !rhs.shellsIsSet()) || (shellsIsSet() && rhs.shellsIsSet() && getShells() == rhs.getShells())) &&
((!overallIsSet() && !rhs.overallIsSet()) || (overallIsSet() && rhs.overallIsSet() && getOverall() == rhs.getOverall())) &&
((!spaceGroupIsSet() && !rhs.spaceGroupIsSet()) || (spaceGroupIsSet() && rhs.spaceGroupIsSet() && getSpaceGroup() == rhs.getSpaceGroup())) &&
@@ -333,6 +337,8 @@ void to_json(nlohmann::json& j, const Rugnux_live_status& o)
j["shells_merge"] = o.m_Shells_merge;
if(o.shellsIsSet() || !o.m_Shells.empty())
j["shells"] = o.m_Shells;
if(o.overallIsSet())
j["overall"] = o.m_Overall;
if(o.spaceGroupIsSet())
j["space_group"] = o.m_Space_group;
if(o.dMinIsSet())
@@ -397,6 +403,11 @@ void from_json(const nlohmann::json& j, Rugnux_live_status& o)
j.at("shells").get_to(o.m_Shells);
o.m_ShellsIsSet = true;
}
if(j.find("overall") != j.end())
{
j.at("overall").get_to(o.m_Overall);
o.m_OverallIsSet = true;
}
if(j.find("space_group") != j.end())
{
j.at("space_group").get_to(o.m_Space_group);
@@ -578,6 +589,23 @@ void Rugnux_live_status::unsetShells()
{
m_ShellsIsSet = false;
}
org::openapitools::server::model::Rugnux_live_shell Rugnux_live_status::getOverall() const
{
return m_Overall;
}
void Rugnux_live_status::setOverall(org::openapitools::server::model::Rugnux_live_shell const& value)
{
m_Overall = value;
m_OverallIsSet = true;
}
bool Rugnux_live_status::overallIsSet() const
{
return m_OverallIsSet;
}
void Rugnux_live_status::unsetOverall()
{
m_OverallIsSet = false;
}
std::string Rugnux_live_status::getSpaceGroup() const
{
return m_Space_group;
+10 -1
View File
@@ -112,13 +112,20 @@ public:
bool shellsMergeIsSet() const;
void unsetShells_merge();
/// <summary>
/// Merging statistics by resolution shell of the latest merge, coarse to fine, as the shell table of &#x60;_report.txt&#x60;. Rotation: once the run is over; serial: at each provisional merge and at the end.
/// Merging statistics by resolution shell of the latest merge, coarse to fine, as the shell table of &#x60;_report.txt&#x60;. Rotation: once the run is over; serial: at each provisional merge and at the end. &#x60;overall&#x60; is the same table&#39;s \&quot;Overall\&quot; row (whole-merge completeness, multiplicity, I/sigma, CC1/2, R_meas or R_split, CCref, and the overall R-free of the model validated on it), present with &#x60;shells&#x60;.
/// </summary>
std::vector<org::openapitools::server::model::Rugnux_live_shell> getShells() const;
void setShells(std::vector<org::openapitools::server::model::Rugnux_live_shell> const& value);
bool shellsIsSet() const;
void unsetShells();
/// <summary>
///
/// </summary>
org::openapitools::server::model::Rugnux_live_shell getOverall() const;
void setOverall(org::openapitools::server::model::Rugnux_live_shell const& value);
bool overallIsSet() const;
void unsetOverall();
/// <summary>
/// Space group of the result (the summary&#39;s \&quot;Space group\&quot; row); absent until the run is over
/// </summary>
std::string getSpaceGroup() const;
@@ -274,6 +281,8 @@ protected:
bool m_Shells_mergeIsSet;
std::vector<org::openapitools::server::model::Rugnux_live_shell> m_Shells;
bool m_ShellsIsSet;
org::openapitools::server::model::Rugnux_live_shell m_Overall;
bool m_OverallIsSet;
std::string m_Space_group;
bool m_Space_groupIsSet;
double m_D_min;
+4
View File
@@ -3283,8 +3283,12 @@ components:
description: |
Merging statistics by resolution shell of the latest merge, coarse to fine, as the shell table of
`_report.txt`. Rotation: once the run is over; serial: at each provisional merge and at the end.
`overall` is the same table's "Overall" row (whole-merge completeness, multiplicity, I/sigma, CC1/2,
R_meas or R_split, CCref, and the overall R-free of the model validated on it), present with `shells`.
items:
$ref: '#/components/schemas/rugnux_live_shell'
overall:
$ref: '#/components/schemas/rugnux_live_shell'
space_group:
type: string
description: Space group of the result (the summary's "Space group" row); absent until the run is over
+2 -1
View File
@@ -14,7 +14,8 @@ Name | Type | Description | Notes
**model** | [**RugnuxLiveModel**](RugnuxLiveModel.md) | | [optional]
**stills** | [**RugnuxLiveStills**](RugnuxLiveStills.md) | | [optional]
**shells_merge** | **str** | Which merge &#x60;shells&#x60; are of - \&quot;the collection\&quot;, or on serial data a provisional merge such as \&quot;50% of the images (provisional)\&quot;; absent until there is a merge | [optional]
**shells** | [**List[RugnuxLiveShell]**](RugnuxLiveShell.md) | Merging statistics by resolution shell of the latest merge, coarse to fine, as the shell table of &#x60;_report.txt&#x60;. Rotation: once the run is over; serial: at each provisional merge and at the end. | [optional]
**shells** | [**List[RugnuxLiveShell]**](RugnuxLiveShell.md) | Merging statistics by resolution shell of the latest merge, coarse to fine, as the shell table of &#x60;_report.txt&#x60;. Rotation: once the run is over; serial: at each provisional merge and at the end. &#x60;overall&#x60; is the same table&#39;s \&quot;Overall\&quot; row (whole-merge completeness, multiplicity, I/sigma, CC1/2, R_meas or R_split, CCref, and the overall R-free of the model validated on it), present with &#x60;shells&#x60;. | [optional]
**overall** | [**RugnuxLiveShell**](RugnuxLiveShell.md) | | [optional]
**space_group** | **str** | Space group of the result (the summary&#39;s \&quot;Space group\&quot; row); absent until the run is over | [optional]
**d_min** | **float** | High-resolution limit of the merge, in Angstrom; absent until the run is over, or without a merge | [optional]
**isa** | **float** | ISa of the error model; absent until the run is over, or where it was not determined | [optional]
+3
View File
@@ -2402,9 +2402,12 @@ export type rugnux_live_status = {
/**
* Merging statistics by resolution shell of the latest merge, coarse to fine, as the shell table of
* `_report.txt`. Rotation: once the run is over; serial: at each provisional merge and at the end.
* `overall` is the same table's "Overall" row (whole-merge completeness, multiplicity, I/sigma, CC1/2,
* R_meas or R_split, CCref, and the overall R-free of the model validated on it), present with `shells`.
*
*/
shells?: Array<rugnux_live_shell>;
overall?: rugnux_live_shell;
/**
* Space group of the result (the summary's "Space group" row); absent until the run is over
*/
+1
View File
@@ -1043,6 +1043,7 @@ export const zRugnuxLiveStatus = z.object({
stills: zRugnuxLiveStills.optional(),
shells_merge: z.string().optional(),
shells: z.array(zRugnuxLiveShell).optional(),
overall: zRugnuxLiveShell.optional(),
space_group: z.string().optional(),
d_min: z.number().optional(),
isa: z.number().optional(),
+5 -3
View File
@@ -300,9 +300,11 @@ void JFJochReceiverRugnuxRotation::RugnuxThread() {
if (!result.merge_statistics.stills && result.error_model_isa_resolved && result.error_model_isa > 0.0)
isa = result.error_model_isa;
live_status.Headline(result.space_group ? result.space_group->xhm() : "", d_min, isa);
if (result.has_merge_statistics)
live_status.Shells("the collection", LiveShells(result.merge_statistics,
result.model_validation ? &*result.model_validation : nullptr));
if (result.has_merge_statistics) {
const ModelValidationResult *validation = result.model_validation ? &*result.model_validation : nullptr;
live_status.Shells("the collection", LiveShells(result.merge_statistics, validation),
LiveOverall(result.merge_statistics, validation));
}
// For the broker's viewer routes; a copy of the map coefficients, as the end message takes the originals
if (result.outputs && result.outputs->placed_model) {
const RugnuxOutputs &outputs = *result.outputs;
+44 -25
View File
@@ -17,12 +17,37 @@ namespace {
out = v;
}
// The validation's shells are the merge's, labelled by the same d_min (report_shell_d_min)
void AddShellRFree(std::vector<RugnuxLiveShell> &shells, const ModelValidationResult &validation) {
// The validation's shells are the merge's, labelled by the same d_min (report_shell_d_min); its overall R-free
// is the overall row's
void AddShellRFree(std::vector<RugnuxLiveShell> &shells, std::optional<RugnuxLiveShell> &overall,
const ModelValidationResult &validation) {
for (const auto &m : validation.cc_model_shells)
for (auto &sh : shells)
if (static_cast<float>(sh.d_min) == m.d_min && m.n_free > 0)
SetIfFinite(sh.r_free, m.r_free);
if (overall && validation.ok)
SetIfFinite(overall->r_free, validation.r_free);
}
// A row of the report's shell table - a shell, or the overall row
RugnuxLiveShell LiveShell(const MergeStatisticsShell &in, bool stills) {
RugnuxLiveShell sh;
sh.d_min = in.d_min;
sh.d_max = in.d_max;
sh.n_obs = in.total_observations;
sh.n_unique = in.unique_reflections;
if (in.possible_unique_reflections > 0)
sh.completeness = static_cast<double>(in.unique_reflections) / in.possible_unique_reflections;
if (in.unique_reflections > 0)
sh.multiplicity = static_cast<double>(in.total_observations) / in.unique_reflections;
SetIfFinite(sh.i_over_sigma, in.mean_i_over_sigma);
if (!stills)
SetIfFinite(sh.r_meas, in.r_meas);
SetIfFinite(sh.cc_half, in.cc_half);
SetIfFinite(sh.cc_ref, in.cc_ref);
if (in.n_split_pairs > 0)
SetIfFinite(sh.r_split, in.r_split);
return sh;
}
}
@@ -159,16 +184,18 @@ void RugnuxLiveStatus::Stills(const RugnuxLiveStills &stills) {
snapshot.stills = stills;
}
void RugnuxLiveStatus::Shells(const std::string &merge, const std::vector<RugnuxLiveShell> &shells) {
void RugnuxLiveStatus::Shells(const std::string &merge, const std::vector<RugnuxLiveShell> &shells,
const std::optional<RugnuxLiveShell> &overall) {
std::unique_lock ul(status_mutex);
snapshot.shells_merge = merge;
snapshot.shells = shells;
snapshot.overall = overall;
}
void RugnuxLiveStatus::ShellRFree(const std::string &merge, const ModelValidationResult &validation) {
std::unique_lock ul(status_mutex);
if (snapshot.shells_merge == merge)
AddShellRFree(snapshot.shells, validation);
AddShellRFree(snapshot.shells, snapshot.overall, validation);
}
void RugnuxLiveStatus::Latency(double last_image_to_written_s) {
@@ -238,31 +265,23 @@ RugnuxLiveSnapshot RugnuxLiveStatus::Get() const {
std::vector<RugnuxLiveShell> LiveShells(const MergeStatistics &statistics, const ModelValidationResult *validation) {
std::vector<RugnuxLiveShell> ret;
// The rows of the report's table: a shell with no reflection is not printed
for (const auto &in : statistics.shells) {
if (in.unique_reflections == 0)
continue;
RugnuxLiveShell sh;
sh.d_min = in.d_min;
sh.d_max = in.d_max;
sh.n_obs = in.total_observations;
sh.n_unique = in.unique_reflections;
if (in.possible_unique_reflections > 0)
sh.completeness = static_cast<double>(in.unique_reflections) / in.possible_unique_reflections;
sh.multiplicity = static_cast<double>(in.total_observations) / in.unique_reflections;
SetIfFinite(sh.i_over_sigma, in.mean_i_over_sigma);
if (!statistics.stills)
SetIfFinite(sh.r_meas, in.r_meas);
SetIfFinite(sh.cc_half, in.cc_half);
SetIfFinite(sh.cc_ref, in.cc_ref);
if (in.n_split_pairs > 0)
SetIfFinite(sh.r_split, in.r_split);
ret.push_back(sh);
}
for (const auto &in : statistics.shells)
if (in.unique_reflections > 0)
ret.push_back(LiveShell(in, statistics.stills));
std::optional<RugnuxLiveShell> no_overall;
if (validation)
AddShellRFree(ret, *validation);
AddShellRFree(ret, no_overall, *validation);
return ret;
}
RugnuxLiveShell LiveOverall(const MergeStatistics &statistics, const ModelValidationResult *validation) {
std::vector<RugnuxLiveShell> no_shells;
std::optional<RugnuxLiveShell> ret = LiveShell(statistics.overall, statistics.stills);
if (validation)
AddShellRFree(no_shells, ret, *validation);
return *ret;
}
std::optional<RugnuxLiveModel> LiveModel(const ProcessResult &result) {
if (!result.model_validation && !result.model_prior)
return std::nullopt;
+6 -1
View File
@@ -160,6 +160,7 @@ struct RugnuxLiveSnapshot {
// The shell table of the latest merge: the merge of the collection, or on stills a provisional one
std::string shells_merge; // which merge, e.g. "the collection" or "50% of the images"
std::vector<RugnuxLiveShell> shells;
std::optional<RugnuxLiveShell> overall; // the same merge's overall row
};
class RugnuxLiveStatus {
@@ -196,7 +197,8 @@ public:
void Structure(std::shared_ptr<const RugnuxStructure> structure,
std::shared_ptr<const RugnuxStructure> refined_structure = nullptr);
void Stills(const RugnuxLiveStills &stills);
void Shells(const std::string &merge, const std::vector<RugnuxLiveShell> &shells);
void Shells(const std::string &merge, const std::vector<RugnuxLiveShell> &shells,
const std::optional<RugnuxLiveShell> &overall);
// The R-free by shell of a model placed on a merge, if that merge's table is the one shown
void ShellRFree(const std::string &merge, const ModelValidationResult &validation);
// Back to Idle with nothing of the last run, as before any collection.
@@ -213,3 +215,6 @@ std::optional<RugnuxLiveModel> LiveModel(const ProcessResult &result);
// The shell table of a merge as the report prints it, with the R-free by shell of the model validated on it
std::vector<RugnuxLiveShell> LiveShells(const MergeStatistics &statistics,
const ModelValidationResult *validation = nullptr);
// The overall row of the same table, with the overall R-free of the model validated on it
RugnuxLiveShell LiveOverall(const MergeStatistics &statistics, const ModelValidationResult *validation = nullptr);
+1 -1
View File
@@ -447,7 +447,7 @@ void StillsGPUAnalysis::PublishMerge(const StillsMergeOutput &out, const std::st
overall.mean_i_over_sigma, out.seconds);
logger.Info("{}", text);
status.Note(text);
status.Shells(title, LiveShells(out.merge.statistics));
status.Shells(title, LiveShells(out.merge.statistics), LiveOverall(out.merge.statistics));
}
void StillsGPUAnalysis::SubmitModel(StillsMergeOutput &out, const std::string &title, bool final) {
+30 -4
View File
@@ -386,7 +386,7 @@ namespace {
std::vector<std::string> row;
for (std::string w; words >> w;)
row.push_back(w);
if (in_serial == serial && !row.empty() && std::isdigit(row[0][0]))
if (in_serial == serial && !row.empty() && (std::isdigit(row[0][0]) || row[0] == "Overall"))
rows[row[0]] = row;
}
return rows;
@@ -408,12 +408,13 @@ TEST_CASE("RugnuxLiveShells_AreTheReportTable", "[RugnuxLive]") {
TestShell(2.5, 2.2, 900, 600, 1000)};
ms.shells[1].cc_ref = 0.93;
ms.shells[3].mean_i_over_sigma = NAN;
ms.overall = TestShell(50.0, 2.2, 7900, 2600, 3960);
SECTION("rotation") {
const auto shells = LiveShells(ms);
const auto rows = ReportRows(ms, false);
REQUIRE(shells.size() == 3);
REQUIRE(rows.size() == 3);
REQUIRE(rows.size() == 4); // three shells and the Overall row
for (const auto &sh : shells) {
const auto &row = rows.at(fmt::format("{:.2f}", sh.d_min));
// d_min N_obs N_uniq N_possib Compl Mult <I/sig> R_merge R_meas R_pim CC1/2 CC* CCref CCanom SigAno
@@ -432,6 +433,19 @@ TEST_CASE("RugnuxLiveShells_AreTheReportTable", "[RugnuxLive]") {
CHECK(shells[0].d_max == Catch::Approx(50.0));
CHECK(shells[1].cc_ref);
CHECK(!shells[2].i_over_sigma);
// The overall row of the same table
const auto overall = LiveOverall(ms);
const auto &row = rows.at("Overall");
REQUIRE(row.size() == 15);
CHECK(row[1] == std::to_string(overall.n_obs));
CHECK(row[2] == std::to_string(overall.n_unique));
CHECK(row[4] == Percent(overall.completeness));
CHECK(row[5] == Plain(overall.multiplicity));
CHECK(row[6] == Plain(overall.i_over_sigma));
CHECK(row[8] == Percent(overall.r_meas));
CHECK(row[10] == Percent(overall.cc_half));
CHECK(overall.d_min == Catch::Approx(2.2));
}
SECTION("stills") {
@@ -444,7 +458,7 @@ TEST_CASE("RugnuxLiveShells_AreTheReportTable", "[RugnuxLive]") {
const auto rows = ReportRows(ms, false);
const auto serial = ReportRows(ms, true);
REQUIRE(shells.size() == 3);
REQUIRE(rows.size() == 3);
REQUIRE(rows.size() == 4); // three shells and the Overall row
for (const auto &sh : shells) {
const auto &row = rows.at(fmt::format("{:.2f}", sh.d_min));
// No R_merge / R_meas / R_pim on stills
@@ -474,14 +488,20 @@ TEST_CASE("RugnuxLiveShells_AreTheReportTable", "[RugnuxLive]") {
CHECK(shells[1].r_free.value() == Catch::Approx(0.27));
CHECK(!shells[2].r_free);
v.ok = true;
v.r_free = 0.25;
CHECK(LiveOverall(ms, &v).r_free.value() == Catch::Approx(0.25));
RugnuxLiveStatus status;
status.Start(10, 1.0, std::chrono::steady_clock::now());
status.Shells("50% of the images (provisional)", LiveShells(ms));
status.Shells("50% of the images (provisional)", LiveShells(ms), LiveOverall(ms));
// A model of another merge does not label this one
status.ShellRFree("the collection", v);
CHECK(!status.Get().shells[0].r_free);
CHECK(!status.Get().overall->r_free);
status.ShellRFree("50% of the images (provisional)", v);
CHECK(status.Get().shells[0].r_free.value() == Catch::Approx(0.21));
CHECK(status.Get().overall->r_free.value() == Catch::Approx(0.25));
nlohmann::json j = Convert(status.Get());
CHECK(j["shells_merge"] == "50% of the images (provisional)");
@@ -493,10 +513,16 @@ TEST_CASE("RugnuxLiveShells_AreTheReportTable", "[RugnuxLive]") {
CHECK(!j["shells"][0].contains("cc_ref"));
CHECK(!j["shells"][0].contains("r_split"));
CHECK(!j["shells"][2].contains("i_over_sigma"));
REQUIRE(j.contains("overall"));
CHECK(j["overall"]["n_unique"].get<int64_t>() == 2600);
CHECK(j["overall"]["cc_half"].get<double>() == Catch::Approx(0.5 + 0.1 * 2.2f));
CHECK(j["overall"]["r_free"].get<double>() == Catch::Approx(0.25));
status.Clear();
CHECK(status.Get().shells.empty());
CHECK(!status.Get().overall);
j = Convert(status.Get());
CHECK(!j.contains("shells"));
CHECK(!j.contains("overall"));
}
}