diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index b7179a02..1d88aa30 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -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 diff --git a/rugnux/Rugnux.h b/rugnux/Rugnux.h index efa6f41e..58a7f167 100644 --- a/rugnux/Rugnux.h +++ b/rugnux/Rugnux.h @@ -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 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 space_group_search; }; // Callbacks for progress and live results. Methods may be called from worker threads, so an diff --git a/tools/rugnux_cli.cpp b/tools/rugnux_cli.cpp index 01ea9918..6272f6ae 100644 --- a/tools/rugnux_cli.cpp +++ b/tools/rugnux_cli.cpp @@ -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; diff --git a/viewer/windows/JFJochMergeStatsWindow.cpp b/viewer/windows/JFJochMergeStatsWindow.cpp index 78091240..02dd4018 100644 --- a/viewer/windows/JFJochMergeStatsWindow.cpp +++ b/viewer/windows/JFJochMergeStatsWindow.cpp @@ -12,12 +12,15 @@ #include #include #include +#include #include #include #include #include #include +#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 space_group_number, + const std::optional &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(*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(sgs.candidates.size())); + for (int i = 0; i < static_cast(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) { diff --git a/viewer/windows/JFJochMergeStatsWindow.h b/viewer/windows/JFJochMergeStatsWindow.h index a95eb4fe..b3c574e2 100644 --- a/viewer/windows/JFJochMergeStatsWindow.h +++ b/viewer/windows/JFJochMergeStatsWindow.h @@ -5,8 +5,12 @@ #include +#include +#include + #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 space_group_number, + const std::optional &space_group_search, + QWidget *parent = nullptr); private: MergeStatistics stats_; diff --git a/viewer/windows/JFJochProcessingJobsWindow.cpp b/viewer/windows/JFJochProcessingJobsWindow.cpp index 0f2f2c66..73bb0a80 100644 --- a/viewer/windows/JFJochProcessingJobsWindow.cpp +++ b/viewer/windows/JFJochProcessingJobsWindow.cpp @@ -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); diff --git a/viewer/windows/JFJochProcessingJobsWindow.h b/viewer/windows/JFJochProcessingJobsWindow.h index f24e21db..8554a981 100644 --- a/viewer/windows/JFJochProcessingJobsWindow.h +++ b/viewer/windows/JFJochProcessingJobsWindow.h @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include #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 space_group_number; // final space group (searched or fixed) + std::optional space_group_search; // ranked candidates, when a search ran QToolButton *graph_btn = nullptr; // per-row "show statistics" button (enabled once stats exist) }; struct JobSpec {