viewer: show the space group in the merge-statistics window

The de-novo space-group search (SearchSpaceGroup) already ran inside Rugnux but
only its chosen number survived on ProcessResult, and the viewer dropped even
that. Carry the structured SearchSpaceGroupResult (point group + ranked
candidate scores) on ProcessResult and thread it into JFJochMergeStatsWindow,
which now shows a 'Space group' hero card (the final group, searched or fixed)
plus a compact table of candidate groups and their absence scores.

The library no longer renders the search to text (it used to embed it in
merge_statistics_text); rugnux_cli formats it for stdout instead, so the CLI
keeps its text table while the viewer draws a proper table and does not spew it
to stdout. Also surface a user-fixed space group on the result so the card
shows it too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 09:52:25 +02:00
co-authored by Claude Opus 4.8
parent 228841870d
commit 0ed943dd4d
7 changed files with 90 additions and 8 deletions
+4 -3
View File
@@ -669,7 +669,6 @@ ProcessResult Rugnux::Run(RugnuxObserver *observer) {
if (end_msg.rotation_lattice_type.has_value())
sg_opts.lattice_system = end_msg.rotation_lattice_type->crystal_system;
auto sg_search = SearchSpaceGroup(sm.merged, sg_opts);
stats_text << SearchSpaceGroupResultToText(sg_search) << "\n\n";
// Miller-index reindex under a change of basis: (h,k,l)_conv = reindex * (h,k,l)_prim.
const auto reindex_hkl = [](auto &r, const gemmi::Mat33 &m) {
@@ -723,8 +722,6 @@ ProcessResult Rugnux::Run(RugnuxObserver *observer) {
SearchSpaceGroupOptions o2 = sg_opts;
o2.lattice_system = cand.system;
const auto s2 = SearchSpaceGroup(merged_c, o2);
stats_text << "Pseudo-symmetry re-test in the metric candidate's conventional setting:\n"
<< SearchSpaceGroupResultToText(s2) << "\n\n";
if (s2.best_space_group.has_value() && s2.best_space_group->number > 1) {
// The centring cannot be confirmed from absences here (integrated in the primitive
// cell, so the centring-absent reflections do not exist) - it is metric-determined.
@@ -785,6 +782,10 @@ ProcessResult Rugnux::Run(RugnuxObserver *observer) {
phase("Re-scaling in space group " + sg.short_name());
sm = scale_and_merge(sg.short_name(), false);
}
result.space_group_search = sg_search;
} else {
// A space group was fixed by the user; surface it so the viewer/CLI can still show it.
result.space_group_number = experiment_.GetSpaceGroupNumber();
}
// Ice-ring CC1/2 mask: a hexagonal-ice ring whose merged half-set CC1/2 collapses well below its
+8 -2
View File
@@ -17,6 +17,7 @@
#include "../image_analysis/spot_finding/SpotFindingSettings.h"
#include "../image_analysis/scale_merge/Merge.h" // MergeStatistics
#include "../image_analysis/scale_merge/TwinningAnalysis.h" // TwinningAnalysisResult
#include "../image_analysis/scale_merge/SearchSpaceGroup.h" // SearchSpaceGroupResult
class JFJochHDF5Reader;
@@ -93,9 +94,14 @@ struct ProcessResult {
// Twinning analysis of the final merged intensities (l_test_pairs == 0 when not computed).
TwinningAnalysisResult twinning;
// Space group determined by the search (when the user did not fix one), used for the final
// re-scale/merge and written to the master file.
// Space group used for the final re-scale/merge and written to the master file: determined by the
// search when the user did not fix one, otherwise the fixed group.
std::optional<int64_t> space_group_number;
// Structured result of the de-novo space-group search (point group + ranked candidate scores),
// populated only when a search ran (no space group was fixed). The library no longer renders it to
// text - the CLI formats it for stdout, the viewer draws it as a table.
std::optional<SearchSpaceGroupResult> space_group_search;
};
// Callbacks for progress and live results. Methods may be called from worker threads, so an
+6
View File
@@ -27,6 +27,7 @@
#include "../image_analysis/scale_merge/RotationScaleMerge.h"
#include "../image_analysis/scale_merge/ResolutionCutoff.h"
#include "../image_analysis/scale_merge/TwinningAnalysis.h"
#include "../image_analysis/scale_merge/SearchSpaceGroup.h"
#include "../rugnux/Rugnux.h"
// Default rot3d per-frame scale-G smoothing range (XDS DELPHI-like), in degrees of rotation.
@@ -1291,6 +1292,11 @@ int main(int argc, char **argv) {
}
g_active_process = nullptr;
// The space-group search is rendered here (not in the library) so the viewer does not emit it on
// stdout and the CLI owns the format.
if (result.space_group_search.has_value())
std::cout << std::endl << SearchSpaceGroupResultToText(*result.space_group_search) << std::endl;
if (!result.merge_statistics_text.empty())
std::cout << std::endl << result.merge_statistics_text << std::endl;
+56 -1
View File
@@ -12,12 +12,15 @@
#include <QHeaderView>
#include <QLabel>
#include <QStackedWidget>
#include <QStringList>
#include <QTableWidget>
#include <QToolButton>
#include <QVBoxLayout>
#include <QtCharts/QChart>
#include <QtCharts/QValueAxis>
#include "gemmi/symmetry.hpp" // find_spacegroup_by_number
#include "../charts/JFJochSimpleChartView.h"
#include "../widgets/ToolbarIcons.h"
@@ -62,7 +65,10 @@ namespace {
JFJochMergeStatsWindow::JFJochMergeStatsWindow(const QString &title, const MergeStatistics &stats,
double isa, bool has_reference,
const TwinningAnalysisResult &twinning, QWidget *parent)
const TwinningAnalysisResult &twinning,
std::optional<int64_t> space_group_number,
const std::optional<SearchSpaceGroupResult> &space_group_search,
QWidget *parent)
: QWidget(parent, Qt::Window), stats_(stats), has_reference_(has_reference) {
setWindowTitle("Merge statistics — " + title);
setAttribute(Qt::WA_DeleteOnClose);
@@ -87,8 +93,57 @@ JFJochMergeStatsWindow::JFJochMergeStatsWindow(const QString &title, const Merge
hero->addWidget(MakeCard(num(multiplicity, 1), "Multiplicity", this));
if (has_reference_)
hero->addWidget(MakeCard(pct(o.cc_ref * 100.0), "CCref", this));
if (space_group_number.has_value()) {
const gemmi::SpaceGroup *sg = gemmi::find_spacegroup_by_number(static_cast<int>(*space_group_number));
const QString sg_name = sg ? QString::fromStdString(sg->short_name())
: QString::number(*space_group_number);
hero->addWidget(MakeCard(sg_name, "Space group", this));
}
layout->addLayout(hero);
// De-novo space-group search: the point group + the ranked candidates it scored (only present when
// the space group was not fixed by the user). A summary line above a compact table.
if (space_group_search.has_value() && !space_group_search->candidates.empty()) {
const auto &sgs = *space_group_search;
QString summary = "Point group " + QString::fromStdString(sgs.point_group_hm.empty() ? "?" : sgs.point_group_hm);
if (!sgs.alternatives.empty()) {
QStringList alts;
for (const auto &alt : sgs.alternatives)
alts << QString::fromStdString(alt.short_name());
summary += " · indistinguishable from these data: " + alts.join(", ");
}
auto *sgLabel = new QLabel(summary, this);
sgLabel->setStyleSheet("color: gray;");
layout->addWidget(sgLabel);
auto *sgTable = new QTableWidget(this);
sgTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
sgTable->verticalHeader()->setVisible(false);
const QStringList sgHeaders{"Space group", "Absent", "Viol.", "⟨I/σ⟩ abs", "⟨I/σ⟩ pres", "Consistent"};
sgTable->setColumnCount(sgHeaders.size());
sgTable->setHorizontalHeaderLabels(sgHeaders);
sgTable->setRowCount(static_cast<int>(sgs.candidates.size()));
for (int i = 0; i < static_cast<int>(sgs.candidates.size()); ++i) {
const auto &c = sgs.candidates[i];
int col = 0;
auto set = [&](const QString &t, bool leftAlign = false) {
auto *it = new QTableWidgetItem(t);
it->setTextAlignment((leftAlign ? Qt::AlignLeft : Qt::AlignRight) | Qt::AlignVCenter);
if (c.selected) { QFont f = it->font(); f.setBold(true); it->setFont(f); }
sgTable->setItem(i, col++, it);
};
set(QString::fromStdString(c.space_group.short_name()) + (c.selected ? " *" : ""), true);
set(QString::number(c.absent_observed));
set(QString::number(c.absent_violations));
set(QString::number(c.absent_mean_i_over_sigma, 'f', 2));
set(QString::number(c.present_mean_i_over_sigma, 'f', 2));
set(c.consistent ? QStringLiteral("yes") : QStringLiteral("no"));
}
sgTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
sgTable->setMaximumHeight(160); // a secondary detail — keep the shells plot the focus
layout->addWidget(sgTable);
}
// --- Twinning test (Padilla-Yeates L-test + second moment) ---
// A red-outlined banner flags suspected twinning; otherwise a quiet grey "all clear".
if (twinning.l_test_pairs > 0) {
+8 -1
View File
@@ -5,8 +5,12 @@
#include <QWidget>
#include <cstdint>
#include <optional>
#include "../../image_analysis/scale_merge/Merge.h" // MergeStatistics
#include "../../image_analysis/scale_merge/TwinningAnalysis.h" // TwinningAnalysisResult
#include "../../image_analysis/scale_merge/SearchSpaceGroup.h" // SearchSpaceGroupResult
class QComboBox;
class QLabel;
@@ -23,7 +27,10 @@ class JFJochMergeStatsWindow : public QWidget {
public:
JFJochMergeStatsWindow(const QString &title, const MergeStatistics &stats,
double isa, bool has_reference,
const TwinningAnalysisResult &twinning, QWidget *parent = nullptr);
const TwinningAnalysisResult &twinning,
std::optional<int64_t> space_group_number,
const std::optional<SearchSpaceGroupResult> &space_group_search,
QWidget *parent = nullptr);
private:
MergeStatistics stats_;
@@ -364,7 +364,8 @@ void JFJochProcessingJobsWindow::showStats(const QString &id) {
for (const auto &j: jobs_) {
if (j.id == id && j.has_merge_stats) {
auto *win = new JFJochMergeStatsWindow(j.label, j.merge_stats, j.isa, j.merge_has_reference,
j.twinning, window());
j.twinning, j.space_group_number, j.space_group_search,
window());
win->show();
return;
}
@@ -518,6 +519,8 @@ void JFJochProcessingJobsWindow::onFinished(ProcessResult result) {
jobs_[row].isa = result.error_model_isa;
jobs_[row].merge_has_reference = result.has_reference;
jobs_[row].twinning = result.twinning;
jobs_[row].space_group_number = result.space_group_number;
jobs_[row].space_group_search = result.space_group_search;
if (jobs_[row].graph_btn)
jobs_[row].graph_btn->setEnabled(true);
showStats(jobs_[row].id);
@@ -6,6 +6,8 @@
#include <QElapsedTimer>
#include <QString>
#include <QWidget>
#include <cstdint>
#include <optional>
#include <vector>
#include "../JFJochProcessController.h"
@@ -64,6 +66,8 @@ private:
double isa = 0.0;
bool merge_has_reference = false;
TwinningAnalysisResult twinning; // twinning test of the merged intensities
std::optional<int64_t> space_group_number; // final space group (searched or fixed)
std::optional<SearchSpaceGroupResult> space_group_search; // ranked candidates, when a search ran
QToolButton *graph_btn = nullptr; // per-row "show statistics" button (enabled once stats exist)
};
struct JobSpec {