From ef07770c5f24898b2c62744fbde6235abc730420 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Tue, 25 Aug 2026 17:21:21 +0200 Subject: [PATCH] rugnux: name the unmerged export from the prefix, and drop centring absences --export-unmerged and --export-unmerged-partials no longer take a filename; they write _unmerged.mtz and _unmerged_partials.mtz, as --write-process-h5 already does for _process.h5. Lattice-centring absences are no longer written. They are integrated on purpose - prediction runs in P so the space-group search can confirm or disprove the centring - but in an exported file they cost the reader the answer: pointless reads the interstitial nodes, takes the lattice for primitive, and demotes I23 to P2_1 3, while aimless roughly halves its merging them. They were also only present when the indexer had settled on the conventional centred cell, so the file's content depended on which of two equivalent cells it picked. Screw and glide absences are still written. They are the evidence a space group is chosen on, so a program reading this file can check that choice independently; deleting them turns pointless's test into an assumption ("we cannot determine if the reflections are truly systematically absent"). Deleting them changes no merging statistic. XDS and DIALS draw the line in the same place - XDS ships screw absences and never measures a centring one, and DIALS's absence machinery has only screw-axis classes. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CHMmeM1d489zvNFT7ZMN2P --- docs/CHANGELOG.md | 2 +- image_analysis/WriteReflections.cpp | 12 ++++++++++ rugnux/Rugnux.cpp | 14 ++++++----- rugnux/Rugnux.h | 14 +++++------ rugnux/rugnux_cli.cpp | 36 ++++++++++++++--------------- 5 files changed, 46 insertions(+), 32 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e154e1dd..13aeb112 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,7 +3,7 @@ ### 1.0.0-rc.164 This is an UNSTABLE release. It includes many experimental features, as well as many AI generated fixes. We recommend using rc.152 for production use. -* rugnux: `--export-unmerged ` writes the integrated observations as an unmerged MTZ - readable by aimless, pointless, careless and `iotbx.merging_statistics` - in `--mode mx` and `--mode scale` alike, with each rotation reflection's partials summed into one full; `--export-unmerged-partials ` writes one row per image instead. +* rugnux: `--export-unmerged` writes the integrated observations as `_unmerged.mtz`, an unmerged MTZ readable by aimless, pointless, careless and `iotbx.merging_statistics`, in `--mode mx` and `--mode scale` alike; each rotation reflection's partials are summed into one full, and `--export-unmerged-partials` writes one row per image instead. Lattice-centring absences are not written; screw and glide absences are. ### 1.0.0-rc.163 This is an UNSTABLE release. It includes many experimental features, as well as many AI generated fixes. We recommend using rc.152 for production use. diff --git a/image_analysis/WriteReflections.cpp b/image_analysis/WriteReflections.cpp index 5a402a21..2ebfc046 100644 --- a/image_analysis/WriteReflections.cpp +++ b/image_analysis/WriteReflections.cpp @@ -5,6 +5,7 @@ #include "scale_merge/Merge.h" #include "scale_merge/HKLKey.h" #include "scale_merge/TwinningAnalysis.h" +#include "bragg_integration/SystematicAbsence.h" #include "../common/ParallelFor.h" #include @@ -692,7 +693,18 @@ void WriteUnmergedMtzReflections(const std::vector &outcomes // it, and every program this file is for wants to handle it its own way. gemmi::UnmergedHklMover hkl_mover(mtz.spacegroup); std::set batch_numbers; + + // Lattice-centring absences are integrated on purpose - prediction runs in P so the space-group + // search can confirm or disprove the centring - but they are not written here. POINTLESS reads + // the interstitial nodes, takes the lattice for primitive and demotes the space group, and they + // roughly halve AIMLESS's . Screw and glide absences ARE written: they are the evidence + // the space group was chosen on, so a program reading this file can check that choice, and + // removing them turns its test into an assumption. XDS and DIALS draw the line in the same place. + const char centering = mtz.spacegroup ? mtz.spacegroup->hm[0] : 'P'; + const auto add_row = [&](const Reflection &r) { + if (systematic_absence(r.h, r.k, r.l, centering)) + return; std::array hkl{r.h, r.k, r.l}; const int isym = hkl_mover.move_to_asu(hkl); // A summed full's image_number is its rocking-curve centroid, so this is the batch the diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 815f0332..211949d5 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -2991,17 +2991,19 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b // rather than the merged ones: the partiality and the per-image scale are left for the reading // program, which fits a scale model of its own. if (full && !cancelled_ && write_output && !geometry_prepass && result.consensus_cell) { - if (!config_.unmerged_export_path.empty()) { + if (config_.export_unmerged) { if (observer) observer->OnPhase("Writing unmerged reflections"); + const std::string path = config_.output_prefix + "_unmerged.mtz"; WriteUnmergedMtzReflections(indexer->GetIntegrationOutcome(), *result.consensus_cell, - experiment_, true, config_.unmerged_export_path); - logger.Info("Unmerged observations written to {}", config_.unmerged_export_path); + experiment_, true, path); + logger.Info("Unmerged observations written to {}", path); } - if (!config_.unmerged_partials_export_path.empty()) { + if (config_.export_unmerged_partials) { if (observer) observer->OnPhase("Writing unmerged reflections"); + const std::string path = config_.output_prefix + "_unmerged_partials.mtz"; WriteUnmergedMtzReflections(indexer->GetIntegrationOutcome(), *result.consensus_cell, - experiment_, false, config_.unmerged_partials_export_path); - logger.Info("Unmerged partials written to {}", config_.unmerged_partials_export_path); + experiment_, false, path); + logger.Info("Unmerged partials written to {}", path); } } diff --git a/rugnux/Rugnux.h b/rugnux/Rugnux.h index 3c54800c..409d590d 100644 --- a/rugnux/Rugnux.h +++ b/rugnux/Rugnux.h @@ -119,14 +119,14 @@ struct ProcessConfig { // Diagnostic: if set, the -P rot3d combine writes the unmerged fulls here (for comparison vs XDS). std::string observation_dump_path; - // If set, the integrated observations are written here as an unmerged MTZ (--export-unmerged), - // for aimless / pointless / careless and anything else that scales unmerged data itself - each - // rocking event summed into one full. - std::string unmerged_export_path; + // Write the integrated observations as _unmerged.mtz (--export-unmerged), for aimless / + // pointless / careless and anything else that scales unmerged data itself - each rocking event + // summed into one full. + bool export_unmerged = false; - // The same file with each partial written as its own row instead, for the reader to sum - // (--export-unmerged-partials). Independent of the above; a run can write both. - std::string unmerged_partials_export_path; + // The same as _unmerged_partials.mtz, with each partial written as its own row for the + // reader to sum (--export-unmerged-partials). Independent of the above; a run can write both. + bool export_unmerged_partials = false; // If set, after merging, validate the merged intensities against this atomic model (PDB): // compute R-free with an optimized bulk solvent and write 2Fo-Fc / Fo-Fc maps (--model). diff --git a/rugnux/rugnux_cli.cpp b/rugnux/rugnux_cli.cpp index 95f87899..7c685358 100644 --- a/rugnux/rugnux_cli.cpp +++ b/rugnux/rugnux_cli.cpp @@ -162,8 +162,8 @@ void print_usage() { std::cout << " -z, --reference-mtz Reference MTZ file" << std::endl; std::cout << " --reference-column