Merge branch 'ci-fix' into rc175: jfjoch_mr_replay leaves the repository; unit tests and the battery take turns on the CI GPU

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-12 14:28:45 +02:00
co-authored by Claude Opus 5.5
3 changed files with 6 additions and 117 deletions
+6
View File
@@ -411,6 +411,12 @@ jobs:
# was not already run, and a release build is the one case where the wait is pure delay.
if: "!(github.event_name == 'workflow_dispatch' && (github.event.inputs.create_release == 'true' || github.event.inputs.create_release == true))"
runs-on: jfjoch_rocky8
# The battery (battery.yml) runs on the same machine and GPU, and its rugnux takes the card's
# memory as its sets need it: a GPU test beside it finds the card full, and a GPU out-of-memory
# ends the run by design. One at a time, in the battery's own concurrency group.
concurrency:
group: battery
cancel-in-progress: false
container:
image: gitea.psi.ch/leonarski_f/jfjoch_rocky8:2609
options: --gpus all
-5
View File
@@ -12,11 +12,6 @@ TARGET_LINK_LIBRARIES(jfjoch_extract_hkl JFJochReader)
# rugnux, the single offline analysis CLI, lives in rugnux/ next to its library.
# rugnux --af-model's molecular replacement re-run on a run's merged MTZ files (developer utility, not
# installed): a survey of the search over many datasets in minutes instead of a reprocessing each.
ADD_EXECUTABLE(jfjoch_mr_replay jfjoch_mr_replay.cpp)
TARGET_LINK_LIBRARIES(jfjoch_mr_replay JFJochStructureRefinement)
# In-place re-compress /entry/data/data of a _data file from bitshuffle/LZ4 to bitshuffle/zstd.
ADD_EXECUTABLE(jfjoch_recompress jfjoch_recompress.cpp)
TARGET_LINK_LIBRARIES(jfjoch_recompress JFJochHDF5Wrappers)
-112
View File
@@ -1,112 +0,0 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
// Developer utility: rugnux --af-model's molecular replacement re-run on the merged reflections a rugnux
// run wrote, without reprocessing the images - the answer's p.mtz and, where the run made one, the twin
// hypothesis' p_twin_<group>.mtz. The search, the decision and the hand check are rugnux's own code; the
// output is the MODEL PRIOR keys and table of the rugnux report.
//
// jfjoch_mr_replay <model> <answer group> <p.mtz> [<twin group> <p_twin.mtz>]
//
// The answer group is the one the run searched in: where it took the model's enantiomorph into its
// label, the file carries the relabelled group and the original has to be given here.
#include <algorithm>
#include <cmath>
#include <iostream>
#include <thread>
#include <gemmi/mtz.hpp>
#include "../common/Logger.h"
#include "../image_analysis/structure_refinement/MolecularReplacement.h"
namespace {
std::vector<MergedReflection> read_merged(const std::string &path, UnitCell &cell) {
gemmi::Mtz mtz;
mtz.read_file(path);
const gemmi::UnitCell &c = mtz.cell;
cell = UnitCell(c.a, c.b, c.c, c.alpha, c.beta, c.gamma);
const auto col = [&](const char *label) -> int {
const gemmi::Mtz::Column *x = mtz.column_with_label(label);
return x ? static_cast<int>(x->idx) : -1;
};
const int i_mean = col("IMEAN"), s_mean = col("SIGIMEAN"), ip = col("I(+)"), sip = col("SIGI(+)"),
im = col("I(-)"), sim = col("SIGI(-)"), f = col("F"), sf = col("SIGF"), fp = col("F(+)"),
sfp = col("SIGF(+)"), fm = col("F(-)"), sfm = col("SIGF(-)"), free = col("FreeR_flag");
const size_t nc = mtz.columns.size();
const auto get = [&](size_t row, int i) { return i < 0 ? NAN : mtz.data[row * nc + i]; };
std::vector<MergedReflection> out;
for (size_t row = 0; row < static_cast<size_t>(mtz.nreflections); row++) {
MergedReflection r;
r.h = static_cast<int>(get(row, 0));
r.k = static_cast<int>(get(row, 1));
r.l = static_cast<int>(get(row, 2));
r.I = get(row, i_mean);
r.sigma = get(row, s_mean);
r.I_plus = get(row, ip);
r.sigma_plus = get(row, sip);
r.I_minus = get(row, im);
r.sigma_minus = get(row, sim);
r.F = get(row, f);
r.sigmaF = get(row, sf);
r.F_plus = get(row, fp);
r.sigmaF_plus = get(row, sfp);
r.F_minus = get(row, fm);
r.sigmaF_minus = get(row, sfm);
r.rfree_flag = get(row, free) == 0.0f;
r.d = static_cast<float>(1.0 / std::sqrt(c.calculate_1_d2(gemmi::Miller{{r.h, r.k, r.l}})));
out.push_back(r);
}
return out;
}
} // namespace
int main(int argc, char **argv) {
if (argc != 4 && argc != 6) {
std::cerr << "Usage: jfjoch_mr_replay <model> <answer group> <p.mtz> [<twin group> <p_twin.mtz>]\n";
return 1;
}
Logger logger("jfjoch_mr_replay");
const gemmi::SpaceGroup *answer_sg = gemmi::find_spacegroup_by_name(argv[2]);
UnitCell cell{}, twin_cell{};
const std::vector<MergedReflection> merged = read_merged(argv[3], cell);
std::vector<MergedReflection> twin;
std::vector<MRMerge> merges{{"MERGE", &merged, cell, *answer_sg}};
if (argc == 6) {
twin = read_merged(argv[5], twin_cell);
merges.push_back({"TWIN_SUBGROUP_MERGE", &twin, twin_cell, *gemmi::find_spacegroup_by_name(argv[4])});
}
const size_t nthreads = std::max(1u, std::thread::hardware_concurrency());
MolecularReplacementResult mr = RunMolecularReplacement(argv[1], merges, logger, nthreads);
if (mr.chosen >= 0 && mr.decision == "ENANTIOMORPH")
CheckEnantiomorphHand(mr, merged, *answer_sg, logger, nthreads);
std::cout << "MODEL_PRIOR= " << (mr.ok ? "PERFORMED" : "NOT_PERFORMED") << "\n";
if (!mr.ok) {
std::cout << "MODEL_PRIOR_REASON= " << mr.failure_reason << "\n";
return 0;
}
std::cout << "MODEL_PRIOR_DECISION= " << mr.decision << "\n";
if (mr.chosen >= 0) {
std::cout << "MODEL_PRIOR_SPACE_GROUP= " << mr.candidates[mr.chosen].space_group.xhm() << "\n";
std::cout << "MODEL_PRIOR_COPIES= " << mr.candidates[mr.chosen].copies << "\n";
}
if (mr.hand_check.performed)
std::cout << "MODEL_PRIOR_HAND_CHECK= " << (mr.hand_check.adopted ? "ADOPTED" : "VETOED") << "\n";
std::cout << fmt::format("MODEL_PRIOR_SECONDS= {:.2f}\n", mr.t_model + mr.t_transform + mr.t_rotation
+ mr.t_translation + mr.t_rescore + mr.t_null
+ mr.hand_check.seconds);
for (const MRCandidate &c : mr.candidates) {
const auto r = [](double v) { return std::isfinite(v) ? fmt::format("{:.3f}", v) : std::string("-"); };
std::cout << fmt::format(
"# {:<19s} {:<11s} {:5.2f} {:6d} {:6d} {:5.1f} {:5.1f} {:4.1f}% {:>6s} {:>6s} {:>10s} {}\n",
c.merge == 0 ? "MERGE" : "TWIN_SUBGROUP_MERGE", c.space_group.xhm(), c.vm, c.copies, c.copies_likely,
c.rf_z, c.tf_z,
100 * c.clash_fraction, r(c.r_work), r(c.r_free),
std::isfinite(c.null_sigma) ? fmt::format("{:+.1f}", c.null_sigma) : std::string("-"), c.verdict);
}
return 0;
}