/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)
151 lines
5.0 KiB
C++
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);
|
|
}
|