Files
Jungfraujoch/tests/RugnuxLiveStatusTest.cpp
T
leonarski_f 128830d15e rugnux live status: the model's scores, and whether the data carry anomalous signal
/rugnux/status gains `model` (rugnux_live_model) - placement (RIGID_BODY/MR/NOT_PLACED), R-work and
R-free of the placed, unrefined model, CC(model, data), R-work over its random-orientation null, mean
2mFo-DFc at the atoms, the rigid body's move, molecular replacement's decision, group, copies,
TFZ/RFZ/LLG-Z and search resolution, and the strongest anomalous site - taken from
ProcessResult::model_validation and ::model_prior (LiveModel). `anomalous_signal` says the summary's
"Anomalous" row reports usable signal (CC_anom > CC_ANOM_SIGNAL, now a named constant shared with the
report), so the viewer can show the anomalous map by default only then.

The step that placed the model (Molecular replacement, or Model validation) now carries notes: what
MR placed and its TFZ/RFZ, R-work/R-free (unrefined) and CC(model, data), the rigid-body move.

Generated code regenerated from jfjoch_api.yaml: broker/gen (cpp-pistache-server), 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 f602e931dc6afcccb3767b8c6df87650d9496033)
2026-10-24 19:06:04 +02:00

151 lines
5.0 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);
}