rugnux live: a cancelled rotation run reports CANCELLED, counted in images received

A cancelled collection showed verdict FAILED, "No crystal lattice was found
in these frames" and "cancelled after 0 images", with step titles carrying
the planned image counts. The live status now has verdict CANCELLED, says
how many images came before the cancel, and a step title's ", N images" is
capped at the images received once the run is cut by a cancel. The report
of a rugnux CLI run is unchanged. The frontend shows CANCELLED as info,
not as an error.

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-08 11:19:08 +01:00
1 parent 2978a9f928
commit 8f3ba8d389
6 files changed
+72 -3

No files matched your search

+2
View File
@@ -11,6 +11,8 @@ export const verdictSeverity = (verdict: string) => {
return "success";
case "WARNINGS":
return "warning";
case "CANCELLED":
return "info";
default:
return "error";
}
+14 -2
View File
@@ -254,12 +254,24 @@ void JFJochReceiverRugnuxRotation::RugnuxThread() {
// The verdict and summary of the report a `rugnux` run writes, without writing it.
const ReportDocument doc = BuildReportDocument(experiment.GetFilePrefix(), experiment.GetFilePrefix(),
setup.experiment, result);
// A cancelled collection is no failed analysis, and the run stops at the images that came
const int64_t images_received = live_status.Get().images_received;
std::string verdict = doc.verdict;
std::string verdict_text = doc.verdict_text;
if (result.cancelled) {
verdict = "CANCELLED";
verdict_text = fmt::format("The collection was cancelled after {} images - these results describe "
"an incomplete sweep.", images_received);
}
std::vector<std::string> warnings;
for (const auto &warning: doc.warnings)
if (!warning.developer_only)
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));
else if (!warning.developer_only)
warnings.push_back(warning.text);
const double cc_anom = result.merge_statistics.overall.cc_anom;
live_status.Result(doc.verdict, doc.verdict_text, doc.summary_rows, warnings,
live_status.Result(verdict, verdict_text, doc.summary_rows, warnings,
std::isfinite(cc_anom) && cc_anom > CC_ANOM_SIGNAL);
if (const auto live_model = LiveModel(result))
live_status.Model(*live_model);
+17
View File
@@ -4,6 +4,7 @@
#include "RugnuxLiveStatus.h"
#include <algorithm>
#include <cctype>
#include <cmath>
#include "../rugnux/Rugnux.h"
@@ -16,6 +17,19 @@ double RugnuxLiveStatus::Now() const {
return Since(std::chrono::steady_clock::now());
}
// A step of the run, "..., N images", covers no more images than came: after a cancel the run goes on over those.
void RugnuxLiveStatus::CapImages(std::string &title) const {
const std::string suffix = " images";
const size_t pos = title.rfind(", ");
if (snapshot.cut != "cancel" || pos == std::string::npos || !title.ends_with(suffix))
return;
const std::string count = title.substr(pos + 2, title.size() - pos - 2 - suffix.size());
if (count.empty() || !std::all_of(count.begin(), count.end(), ::isdigit))
return;
if (std::stoll(count) > snapshot.images_received)
title = title.substr(0, pos + 2) + std::to_string(snapshot.images_received) + suffix;
}
void RugnuxLiveStatus::CloseStep() {
if (!snapshot.steps.empty() && !snapshot.steps.back().duration_s)
snapshot.steps.back().duration_s = Now() - snapshot.steps.back().started_s;
@@ -47,6 +61,7 @@ void RugnuxLiveStatus::Step(const std::string &title) {
std::unique_lock ul(status_mutex);
CloseStep();
snapshot.steps.push_back(RugnuxLiveStep{.title = title, .started_s = Now()});
CapImages(snapshot.steps.back().title);
snapshot.step_progress.reset();
}
@@ -117,6 +132,8 @@ void RugnuxLiveStatus::Cut(const std::string &reason) {
std::unique_lock ul(status_mutex);
snapshot.cut = reason;
snapshot.cut_s = Now();
for (auto &step: snapshot.steps)
CapImages(step.title);
}
void RugnuxLiveStatus::Probe(const std::string &text) {
+2 -1
View File
@@ -111,7 +111,7 @@ struct RugnuxLiveSnapshot {
std::string cut;
std::optional<double> cut_s;
std::optional<double> last_image_to_written_s;
std::string verdict; // OK | WARNINGS | UNUSABLE | FAILED
std::string verdict; // OK | WARNINGS | UNUSABLE | FAILED | CANCELLED
std::string verdict_text;
std::vector<std::pair<std::string, std::string>> summary;
std::vector<std::string> warnings;
@@ -136,6 +136,7 @@ class RugnuxLiveStatus {
double Since(std::chrono::steady_clock::time_point t) const;
double Now() const;
void CloseStep();
void CapImages(std::string &title) const;
public:
// start_received, received: when the message came off the socket; expected_collection_s: none if not known
void Start(int64_t images_expected, std::optional<double> expected_collection_s,
+8
View File
@@ -698,6 +698,14 @@ TEST_CASE("JFJochReceiverRugnux_Rotation_Cancel", "[JFJochReceiverRugnux]") {
const auto live = service.GetRugnuxStatus();
CHECK(live.cut == "cancel");
CHECK(live.state != RugnuxLiveState::Running);
// A cancelled collection, not a failed analysis, and counted in the images that came
CHECK(live.verdict == "CANCELLED");
CHECK(live.verdict_text.find("after 2 images") != std::string::npos);
for (const auto &step: live.steps) {
const size_t pos = step.title.rfind(", ");
if (pos != std::string::npos && step.title.ends_with(" images"))
CHECK(std::stoll(step.title.substr(pos + 2)) <= 2);
}
}
SECTION("A silent cancel lets the collection finish") {
Stream stream(x, images);
+29
View File
@@ -322,3 +322,32 @@ TEST_CASE("RugnuxLiveModel_NonFiniteScores", "[RugnuxLive]") {
CHECK(!j["model"]["refinement"]["blobs"][0].contains("nearest_atom_distance_A"));
CHECK(j.dump().find("null") == std::string::npos);
}
TEST_CASE("RugnuxLiveStatus_StepsAfterCancel", "[RugnuxLive]") {
// A step covers the images that came: after a cancel, not the ones it was planned for
const auto start = std::chrono::steady_clock::now();
RugnuxLiveStatus status;
status.Start(1000, 10.0, start);
status.Step("Integration (geometry pre-pass), 600 images");
for (int i = 0; i < 384; i++)
status.ImageReceived(start + i * 10ms);
status.Step("Pre-scan");
status.Cut("cancel");
status.Step("Integration, 1000 images");
status.Step("Integration, 200 images");
const auto s = status.Get();
REQUIRE(s.steps.size() == 4);
CHECK(s.steps[0].title == "Integration (geometry pre-pass), 384 images");
CHECK(s.steps[1].title == "Pre-scan");
CHECK(s.steps[2].title == "Integration, 384 images");
CHECK(s.steps[3].title == "Integration, 200 images");
}
TEST_CASE("RugnuxLiveStatus_StepsAfterDeadline", "[RugnuxLive]") {
RugnuxLiveStatus status;
status.Start(1000, 10.0, std::chrono::steady_clock::now());
status.Step("Integration, 1000 images");
status.Cut("deadline");
CHECK(status.Get().steps[0].title == "Integration, 1000 images");
}