Files
Jungfraujoch/tests/RugnuxLiveStatusTest.cpp
T
leonarski_f 2fb4b1d015 Rugnux live page: a hero panel with resolution, ISa, space group and whether the model was placed
Under the header of the Rugnux live page, four big-number cards in the manner of jfjoch_viewer's merge
statistics window: Resolution (d_min of the merge), ISa, Space group (short symbol, screw axes
subscripted) and the model. The model card shows the fit over random orientations - R-work of the placed
model below its random-orientation null, in sd - which is the test rugnux places a model by, both for
molecular replacement (after the TFZ/LLG-Z gate) and for a model that fits where it is. The card is green
where rugnux placed the model (placement MR or RIGID_BODY), red where it did not (with MR's reason), grey
where no model was uploaded; no threshold of its own. "—" while the run is going, "searching…" during
the MR step.

/rugnux/status gains `space_group`, `d_min` and `isa` (set with the result; absent until then, isa also
where the error model did not determine it), filled from ProcessResult in RugnuxThread with the values
the summary's Space group, Resolution and Signal rows print.

Generated code regenerated from jfjoch_api.yaml: broker/gen (cpp-pistache-server 7.20.0), the TypeScript
client (npm run openapi) and broker/redoc-static.html. Python client not regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
(cherry picked from commit fcc019707751aab9d8c9f349309eeadcf364b9c2)
2026-10-25 07:20:28 +01:00

175 lines
5.7 KiB
C++

// 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 "../receiver/RugnuxLiveStatus.h"
#include "../receiver/JFJochReceiverRugnux.h"
#include "../broker/OpenAPIConvert.h"
#include "../image_puller/TestImagePuller.h"
using namespace std::chrono_literals;
TEST_CASE("RugnuxLiveStatus_TimesFromFirstImage", "[RugnuxLive]") {
// 11 images at 100 Hz, the first 3 s after START, reported out of order as several forward threads do
const auto start = std::chrono::steady_clock::now() - 10s;
const auto first = start + 3s;
RugnuxLiveStatus status;
status.Start(11, 0.11, start);
for (int i: {1, 0, 2, 3, 4, 5, 6, 7, 8, 10, 9})
status.ImageReceived(first + i * 10ms);
auto s = status.Get();
CHECK(s.expected_collection_s.value() == Catch::Approx(0.11));
REQUIRE(s.images_received == 11);
REQUIRE(s.start_to_first_image_s.value() == Catch::Approx(3.0));
CHECK(!s.last_image_s);
// "now" is 10 s after START, 7 s after the first image
CHECK(s.elapsed_s == Catch::Approx(7.0).margin(0.5));
REQUIRE(status.LastImage() == first + 100ms);
s = status.Get();
REQUIRE(s.last_image_s.value() == Catch::Approx(0.1));
CHECK((s.images_received - 1) / s.last_image_s.value() == Catch::Approx(100.0));
}
TEST_CASE("RugnuxLiveStatus_BeforeFirstImage", "[RugnuxLive]") {
RugnuxLiveStatus status;
status.Start(10, std::nullopt, std::chrono::steady_clock::now() - 2s);
status.Step("waiting");
const auto s = status.Get();
CHECK(!s.start_to_first_image_s);
CHECK(!s.expected_collection_s);
CHECK(s.elapsed_s == 0.0);
REQUIRE(s.steps.size() == 1);
CHECK(s.steps[0].started_s <= 0.0);
CHECK(!status.LastImage());
}
TEST_CASE("RugnuxLiveStatus_Clear", "[RugnuxLive]") {
RugnuxLiveStatus status;
status.Start(10, 1.0, std::chrono::steady_clock::now() - 2s);
status.ImageReceived(std::chrono::steady_clock::now() - 1s);
status.Step("indexing");
status.Probe("lattice found");
status.Result("OK", "fine", {{"Space group", "P 1"}}, {"a warning"});
status.Finish(RugnuxLiveState::Done);
REQUIRE(status.LastImage());
status.Clear();
const auto s = status.Get();
CHECK(s.state == RugnuxLiveState::Idle);
CHECK(s.images_expected == 0);
CHECK(s.images_received == 0);
CHECK(s.steps.empty());
CHECK(s.probe.empty());
CHECK(s.verdict.empty());
CHECK(s.summary.empty());
CHECK(s.warnings.empty());
CHECK(!s.start_to_first_image_s);
CHECK(!s.last_image_s);
CHECK(s.elapsed_s == 0.0);
CHECK(!status.LastImage());
}
TEST_CASE("TestImagePuller_SetsReceiveTime", "[RugnuxLive]") {
TestImagePuller puller;
const auto before = std::chrono::steady_clock::now();
puller.Put(ImagePullerOutput{});
const auto given = before - 5s;
puller.Put(ImagePullerOutput{.received = given});
auto out = puller.PollImage();
REQUIRE(out);
CHECK(out->received >= before);
out = puller.PollImage();
REQUIRE(out);
CHECK(out->received == given);
}
TEST_CASE("RugnuxLiveModel_MolecularReplacement", "[RugnuxLive]") {
ProcessResult r;
CHECK(!LiveModel(r));
MolecularReplacementResult mr;
mr.ok = true;
mr.model_name = "model.cif";
mr.predicted_model = true;
mr.decision = "AGREES";
mr.d_min_search = 2.5;
MRCandidate c;
c.space_group = *gemmi::find_spacegroup_by_name("P 1 21 1");
c.copies = 2;
c.tf_z = 12.5;
c.rf_z = 6.0;
c.null_sigma = 20.0;
mr.candidates = {MRCandidate{}, c};
mr.chosen = 1;
r.model_prior = mr;
ModelValidationResult v;
v.ok = true;
v.r_work = 0.38;
v.r_free = 0.40;
v.cc_model_overall = 0.71;
v.mean_atom_density_sigma = 1.6;
v.anomalous_sites = {{"SD MET A1", 5.5}};
r.model_validation = v;
r.model_placement = "MR";
const auto m = LiveModel(r);
REQUIRE(m);
CHECK(m->placement == "MR");
CHECK(m->predicted);
CHECK(m->mr_space_group == "P 1 21 1");
CHECK(m->copies.value() == 2);
CHECK(m->tf_z.value() == Catch::Approx(12.5));
CHECK(!m->llg_z);
CHECK(m->fit_sigma.value() == Catch::Approx(20.0)); // MR's null: the rigid body's was not tested
CHECK(m->r_free.value() == Catch::Approx(0.40));
CHECK(m->cc_model.value() == Catch::Approx(0.71));
CHECK(!m->rigid_body_shift_A);
CHECK(m->anomalous_site == "SD MET A1");
RugnuxLiveStatus status;
status.Start(10, 1.0, std::chrono::steady_clock::now());
status.Result("OK", "fine", {}, {}, true);
status.Model(*m);
nlohmann::json j = Convert(status.Get());
CHECK(j["anomalous_signal"] == true);
CHECK(j["model"]["placement"] == "MR");
CHECK(j["model"]["copies"] == 2);
CHECK(j["model"]["r_free"].get<double>() == Catch::Approx(0.40));
CHECK(!j["model"].contains("llg_z"));
CHECK(!j["model"].contains("rigid_body_angle_deg"));
status.Clear();
CHECK(!status.Get().model);
CHECK(!status.Get().anomalous_signal);
}
TEST_CASE("RugnuxLiveStatus_Headline", "[RugnuxLive]") {
RugnuxLiveStatus status;
status.Start(10, 1.0, std::chrono::steady_clock::now());
nlohmann::json j = Convert(status.Get());
CHECK(!j.contains("space_group"));
CHECK(!j.contains("d_min"));
CHECK(!j.contains("isa"));
status.Headline("P 1 21 1", 1.25, 24.2);
j = Convert(status.Get());
CHECK(j["space_group"] == "P 1 21 1");
CHECK(j["d_min"].get<double>() == Catch::Approx(1.25));
CHECK(j["isa"].get<double>() == Catch::Approx(24.2));
// ISa undetermined
status.Headline("P 1 21 1", 1.25, std::nullopt);
j = Convert(status.Get());
CHECK(!j.contains("isa"));
status.Clear();
CHECK(status.Get().space_group.empty());
CHECK(!status.Get().d_min);
}