diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index b0141f9d..68dc7d9c 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -50,6 +50,10 @@ These are copied into the source tree (see the path) rather than fetched. | [GEMMI](https://github.com/project-gemmi/gemmi) | `gemmi_gph/` | Global Phasing Ltd. | MPL-2.0 | [gemmi.txt](licenses/gemmi.txt) | | [PEGTL](https://github.com/taocpp/PEGTL) | `gemmi_gph/gemmi/third_party/tao/` | Dr. Colin Hirsch and Daniel Frey | MIT | [pegtl.txt](licenses/pegtl.txt) | | [sajson](https://github.com/chadaustin/sajson) | `gemmi_gph/gemmi/third_party/sajson.h` | Chad Austin | MIT | [sajson.txt](licenses/sajson.txt) | +| [fast_float](https://github.com/fastfloat/fast_float) | `gemmi_gph/gemmi/third_party/fast_float.h` | The fast_float authors (Daniel Lemire et al.) | Apache-2.0 OR MIT OR BSL-1.0 | [fast-float.txt](licenses/fast-float.txt) | +| [half](https://half.sourceforge.net/) | `gemmi_gph/gemmi/third_party/half.hpp` | Christian Rau | MIT | [half.txt](licenses/half.txt) | +| [pocketfft](https://github.com/mreineck/pocketfft) | `gemmi_gph/gemmi/third_party/pocketfft_hdronly.h` | Max-Planck-Society; Peter Bell; MIT (FFTW-derived parts) | BSD-3-Clause | [pocketfft.txt](licenses/pocketfft.txt) | +| [tinydir](https://github.com/cxong/tinydir) | `gemmi_gph/gemmi/third_party/tinydir.h` | Cong Xu, Lautis Sun, Baudouin Feildel, Andargor | BSD-2-Clause | [tinydir.txt](licenses/tinydir.txt) | | [traccc (ACTS)](https://github.com/acts-project/traccc) | `image_analysis/spot_finding/StrongPixelSet.cpp`, `SpotExtractorGPU.cu` | CERN, for the benefit of the ACTS project | MPL-2.0 | [traccc.txt](licenses/traccc.txt) | | [xbflash.qspi](https://github.com/Xilinx/XRT) | `tools/xbflash.qspi/` | Xilinx / AMD | Apache-2.0 | [xbflash-qspi.txt](licenses/xbflash-qspi.txt) | | [wingetopt](https://github.com/alex85k/wingetopt) | `tools/wingetopt/` | Todd C. Miller; The NetBSD Foundation | ISC AND BSD-2-Clause | [wingetopt.txt](licenses/wingetopt.txt) | @@ -80,6 +84,12 @@ served frontend, so the shipped web UI carries its own attribution. * **MPL-2.0** (Eigen, GEMMI, libzmq, traccc): file-level copyleft. GEMMI is vendored in `gemmi_gph/` in trimmed form; libzmq is fetched at build time; Eigen is provided externally (header-only). The corresponding source is available from each project upstream. +* **GEMMI's own bundled third-party headers** — PEGTL, sajson, fast_float, half, pocketfft and + tinydir, all under `gemmi_gph/gemmi/third_party/` — are listed separately above rather than being + absorbed into GEMMI's row: they are other authors' code under other licences (MIT, BSD and + Apache/MIT/BSL), and GEMMI's MPL-2.0 does not speak for them. Their terms are carried in the + headers themselves rather than in `LICENSE` files, so the `licenses/*.txt` copies are kept by hand; + only PEGTL ships a `LICENSE`, which `COLLECT.sh` copies. * **traccc** is the one entry that is not a vendored directory. Its sparse connected-component labelling enters two otherwise first-party files: `StrongPixelSet.cpp` adapts the SparseCCL source, and `SpotExtractorGPU.cu` follows the design of its GPU counterpart. MPL-2.0 is file-level, so both diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b1bbe899..70db92e4 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,7 @@ 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. * The rugnux results report records how the run was invoked, what it cost and what it ran on: `COMMAND_LINE=` is the command line as one shell-ready line, `WALL_TIME=` the whole invocation in seconds, and `GPU_COUNT=` / `GPU=` how many GPUs were visible and which ones. The total wall time is also printed on stdout, next to the processing time it is slightly larger than. +* The rugnux results report has a `10. MODEL VALIDATION` section when `--model` was given: R-work / R-free with their reflection counts, the bulk-solvent and scale parameters, the mean 2Fo-Fc density at the atom centres, the reindexing operators and where the maps were written - or `MODEL_VALIDATION= NOT_PERFORMED` and the reason, when the model could not be used. `REPORT_VERSION` is 4; `WARNINGS` moves from section 10 to 11 and no existing key changed. * `rugnux --model` reads an mmCIF coordinate file as well as a PDB one, gzipped or not, taking the format from the file's content rather than its name. A `.cif` model was rejected before, and the run then finished successfully with no R-free and no maps. * A model `rugnux --model` cannot use - unreadable, or without atoms, a unit cell or a usable space group - is reported as a `WARNING:` line in the results report instead of only in the log, so a run that was asked to validate and could not is no longer indistinguishable from one that was never asked. * rugnux says which GPUs it can see before it starts processing, so a machine that turns out to have none - a driver mismatch, a leftover `CUDA_VISIBLE_DEVICES` - is noticed while there is still time to stop rather than after a run that took far longer than it should have. diff --git a/docs/RUGNUX.md b/docs/RUGNUX.md index 38ed78d0..39ec0222 100644 --- a/docs/RUGNUX.md +++ b/docs/RUGNUX.md @@ -553,7 +553,9 @@ Rates, per-image costs and progress remain on stdout only. Sections, in order: `1. DATA SET`, `2. INDEXING`, `3. GEOMETRY POST-REFINEMENT` (rotation only), `4. SPACE GROUP DETERMINATION`, `5. SCALING AND MERGING`, `6. TWINNING`, `7. RADIATION DAMAGE`, -`8. SWEEP QUALITY`, `9. DIFFRACTION ANISOTROPY`, `10. WARNINGS`. +`8. SWEEP QUALITY`, `9. DIFFRACTION ANISOTROPY`, `10. MODEL VALIDATION` (only with `--model`), +`11. WARNINGS`. Numbers are fixed: a section that does not apply is left out and the ones after it +keep their numbers. **`SPOT_RESOLUTION_ESTIMATE=`** in section 1 is how far the merged data are expected to reach, read off the found spots alone — no lattice, no integration, no merge — so it is there on a run that never @@ -775,6 +777,13 @@ cell or no usable space group, does not fail the run: it is logged, and the resu `WARNING: Model validation did not run: …` line, so a run that silently produced no R-free and no maps cannot be mistaken for one that was never given `--model`. +Either way the results report carries a **`10. MODEL VALIDATION`** section: `R_WORK=` / `R_FREE=` +with their reflection counts, the bulk-solvent and overall scale parameters, the mean 2Fo-Fc density +at the atom centres, the reindexing operators the written reflections were brought into the model's +frame with, and `MAPS_PREFIX=`; or `MODEL_VALIDATION= NOT_PERFORMED` with +`MODEL_VALIDATION_REASON=` when the model could not be used. A run given no `--model` has no such +section at all. + It is a *data-quality lens*, independent of the internal statistics: R-free measures the merged intensities against external truth, where CC1/2 and Rmeas only measure them against themselves. It also settles the two things merged intensities alone cannot: the enantiomorph (data diff --git a/licenses/COLLECT.sh b/licenses/COLLECT.sh index 2b508a51..c6397dd6 100644 --- a/licenses/COLLECT.sh +++ b/licenses/COLLECT.sh @@ -69,9 +69,11 @@ copy_license bitshuffle-hperf.txt compression/bitshuffle_hperf/LICENSE-APACHE copy_license lz4.txt compression/lz4/LICENSE copy_license xilinx-hls-headers.txt fpga/include/LICENSE.HLS_Headers copy_license gemmi.txt gemmi_gph/LICENSE.txt -# GEMMI bundles PEGTL under its own include tree and under its own (MIT) licence; the CIF parser -# rugnux reads mmCIF models with is built on it. sajson has no LICENSE file of its own - its terms -# are the comment block at the top of the header - so licenses/sajson.txt is kept by hand. +# GEMMI bundles six third-party headers under its own include tree, each under its own licence +# (see THIRD_PARTY_NOTICES.md). Only PEGTL ships a LICENSE file; sajson, fast_float, half, pocketfft +# and tinydir carry their terms in a comment at the top of the header, so licenses/sajson.txt, +# fast-float.txt, half.txt, pocketfft.txt and tinydir.txt are kept by hand - re-extract them from the +# headers when GEMMI is updated. copy_license pegtl.txt gemmi_gph/gemmi/third_party/tao/LICENSE copy_license xbflash-qspi.txt tools/xbflash.qspi/LICENSE copy_license wingetopt.txt tools/wingetopt/LICENSE diff --git a/licenses/fast-float.txt b/licenses/fast-float.txt new file mode 100644 index 00000000..0e330d29 --- /dev/null +++ b/licenses/fast-float.txt @@ -0,0 +1,90 @@ +fast_float by Daniel Lemire + fast_float by João Paulo Magalhaes + + + with contributions from Eugene Golushkov + with contributions from Maksim Kita + with contributions from Marcin Wojdyr + with contributions from Neal Richardson + with contributions from Tim Paine + with contributions from Fabio Pellacini + with contributions from Lénárd Szolnoki + with contributions from Jan Pharago + with contributions from Maya Warrier + with contributions from Taha Khokhar + with contributions from Anders Dalvander + + + Licensed under the Apache License, Version 2.0, or the + MIT License or the Boost License. This file may not be copied, + modified, or distributed except according to those terms. + + MIT License Notice + + MIT License + + Copyright (c) 2021 The fast_float authors + + Permission is hereby granted, free of charge, to any + person obtaining a copy of this software and associated + documentation files (the "Software"), to deal in the + Software without restriction, including without + limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software + is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice + shall be included in all copies or substantial portions + of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF + ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT + SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR + IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + Apache License (Version 2.0) Notice + + Copyright 2021 The fast_float authors + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + + BOOST License Notice + + Boost Software License - Version 1.0 - August 17th, 2003 + + Permission is hereby granted, free of charge, to any person or organization + obtaining a copy of the software and accompanying documentation covered by + this license (the "Software") to use, reproduce, display, distribute, + execute, and transmit the Software, and to prepare derivative works of the + Software, and to permit third-parties to whom the Software is furnished to + do so, all subject to the following: + + The copyright notices in the Software and this entire statement, including + the above license grant, this restriction and the following disclaimer, + must be included in all copies of the Software, in whole or in part, and + all derivative works of the Software, unless such copies or derivative + works are solely in the form of machine-executable object code generated by + a source language processor. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT + SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE + FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. diff --git a/licenses/half.txt b/licenses/half.txt new file mode 100644 index 00000000..76bce9cc --- /dev/null +++ b/licenses/half.txt @@ -0,0 +1,15 @@ +half - IEEE 754-based half-precision floating-point library. + + Copyright (c) 2012-2025 Christian Rau + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, + modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/licenses/pocketfft.txt b/licenses/pocketfft.txt new file mode 100644 index 00000000..05c5f89b --- /dev/null +++ b/licenses/pocketfft.txt @@ -0,0 +1,41 @@ +This file is part of pocketfft. + +Copyright (C) 2010-2024 Max-Planck-Society +Copyright (C) 2019-2020 Peter Bell + +For the odd-sized DCT-IV transforms: + Copyright (C) 2003, 2007-14 Matteo Frigo + Copyright (C) 2003, 2007-14 Massachusetts Institute of Technology + +For the prev_good_size search: + Copyright (C) 2024 Tan Ping Liang, Peter Bell + +For the safeguards against integer overflow in good_size search: + Copyright (C) 2024 Cris Luengo + +Authors: Martin Reinecke, Peter Bell + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. +* Neither the name of the copyright holder nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/licenses/tinydir.txt b/licenses/tinydir.txt new file mode 100644 index 00000000..ae4d884d --- /dev/null +++ b/licenses/tinydir.txt @@ -0,0 +1,26 @@ +Copyright (c) 2013-2021, tinydir authors: +- Cong Xu +- Lautis Sun +- Baudouin Feildel +- Andargor +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/rugnux/ModelValidation.cpp b/rugnux/ModelValidation.cpp index b2a91f6e..5190f3a0 100644 --- a/rugnux/ModelValidation.cpp +++ b/rugnux/ModelValidation.cpp @@ -56,6 +56,7 @@ ModelValidationResult ValidateAgainstModel(const std::vector & std::optional data_space_group_number, bool probe_indexing_ambiguity) { ModelValidationResult result; + result.model_path = model_path; // --- read the atomic model --- gemmi::Structure st; diff --git a/rugnux/ModelValidation.h b/rugnux/ModelValidation.h index 76fce6d4..8c173ba7 100644 --- a/rugnux/ModelValidation.h +++ b/rugnux/ModelValidation.h @@ -17,6 +17,7 @@ class Logger; // Result of validating merged intensities against an atomic model. struct ModelValidationResult { bool ok = false; + std::string model_path; // the file that was asked for, so the report can name it // Why it did not run, when ok is false: the file could not be read, had no atoms, no cell or no // usable space group. A caller that asked for a model must be able to say so rather than finish // quietly with no R-free and no maps, which reads exactly like a run that was never asked. diff --git a/rugnux/ResultReport.cpp b/rugnux/ResultReport.cpp index 5aa4e2f3..788c94ab 100644 --- a/rugnux/ResultReport.cpp +++ b/rugnux/ResultReport.cpp @@ -19,7 +19,7 @@ namespace { // The version of this file format. Bumped when a key is renamed or removed, a table column moves, // or a reason code changes meaning - a consumer can gate on it. - constexpr int REPORT_VERSION = 3; + constexpr int REPORT_VERSION = 4; const char *BANNER = " ******************************************************************************"; @@ -344,11 +344,49 @@ std::string RenderResultReport(const std::string &output_prefix, *std::min_element(an.d_min_axis, an.d_min_axis + 3))); } - // --------------------------------------------------------------- 10. WARNINGS + // ------------------------------------------------------- 10. MODEL VALIDATION + if (result.model_validation.has_value()) { + const auto &mv = *result.model_validation; + Section(os, "10. MODEL VALIDATION"); + Key(os, "MODEL_FILE", mv.model_path); + if (!mv.ok) { + Key(os, "MODEL_VALIDATION", "NOT_PERFORMED"); + Key(os, "MODEL_VALIDATION_REASON", mv.failure_reason); + os << "\n A model was given but could not be used, so there are no R-factors and no maps.\n" + << " Everything else in this report is unaffected: the merge does not depend on the model.\n"; + } else { + Key(os, "MODEL_SPACE_GROUP_NUMBER", mv.model_space_group_number); + Key(os, "R_WORK", fmt::format("{:.4f}", mv.r_work)); + Key(os, "R_FREE", fmt::format("{:.4f}", mv.r_free)); + Key(os, "R_WORK_REFLECTIONS", mv.n_work); + Key(os, "R_FREE_REFLECTIONS", mv.n_free); + Key(os, "BULK_SOLVENT_K_SOL", fmt::format("{:.3f}", mv.k_sol)); + Key(os, "BULK_SOLVENT_B_SOL", fmt::format("{:.1f}", mv.b_sol)); + Key(os, "SCALE_OVERALL", fmt::format("{:.4f}", mv.k_overall)); + Key(os, "MEAN_ATOM_DENSITY_SIGMA", fmt::format("{:.2f}", mv.mean_atom_density_sigma)); + // The relabelings that were applied to the written reflections, so a reader can tell + // whether the file is in the indexing it was merged in or in the model's. + Key(os, "MODEL_HAND_OPERATOR", mv.hand_op.triplet()); + Key(os, "MODEL_INDEXING_OPERATOR", mv.indexing_op.triplet()); + if (!mv.maps_prefix.empty()) + Key(os, "MAPS_PREFIX", mv.maps_prefix); + os << "\n R-free here measures the merged intensities against an external structure, which is\n" + << " what CC1/2 and R_meas cannot do - they only measure the data against themselves. The\n" + << " model is not refined: it is scaled to the data with a flat bulk solvent and an overall\n" + << " anisotropic B, so these R-factors are higher than a refined structure's and are a\n" + << " data-quality reading, not a refinement result.\n"; + if (!(mv.hand_op == gemmi::Op::identity()) || !(mv.indexing_op == gemmi::Op::identity())) + os << "\n The written reflections were reindexed into the model's frame - the operators above\n" + << " say how - so the reflection files, the R-factors and the maps all describe one\n" + << " indexing.\n"; + } + } + + // --------------------------------------------------------------- 11. WARNINGS if (result.cancelled) warnings.emplace_back(fmt::format("Processing was cancelled after {} images - this report " "describes an incomplete run", result.images_processed)); - Section(os, "10. WARNINGS"); + Section(os, "11. WARNINGS"); os << " Everything that needs a person's attention, one line each, marked so a script can find\n" << " them with a single grep for \"WARNING:\".\n\n"; Key(os, "WARNING_COUNT", warnings.size()); diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 67118786..c947c246 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -3295,6 +3295,7 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b // A model that was asked for and could not be used has to say so where anyone will see // it. Without this the run ends successfully with no R-free, no maps and nothing in the // report - indistinguishable from a run that was never given --model at all. + result.model_validation = validation; if (!validation.failure_reason.empty()) result.warnings.push_back("Model validation did not run: " + validation.failure_reason); if (data_sg.has_value()) { diff --git a/rugnux/Rugnux.h b/rugnux/Rugnux.h index 5c9f92af..04c87a48 100644 --- a/rugnux/Rugnux.h +++ b/rugnux/Rugnux.h @@ -18,6 +18,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 "ModelValidation.h" // ModelValidationResult #include "../image_analysis/scale_merge/SearchSpaceGroup.h" // SearchSpaceGroupResult #include "../image_analysis/geom_refinement/PostRefine.h" // PostRefineResult #include "../image_analysis/rotation_indexer/RotationIndexer.h" // RotationIndexerResult @@ -199,6 +200,11 @@ struct ProcessResult { // Twinning analysis of the final merged intensities (l_test_pairs == 0 when not computed). TwinningAnalysisResult twinning; + // What --model found, when one was given. Set even when the validation could not run - its + // failure_reason then says why - so the report can report the failure rather than leave a run + // that was asked to validate looking like one that was not. Unset when no model was asked for. + std::optional model_validation; + // 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; diff --git a/rugnux/rugnux_cli.cpp b/rugnux/rugnux_cli.cpp index 511dd83e..141a2f18 100644 --- a/rugnux/rugnux_cli.cpp +++ b/rugnux/rugnux_cli.cpp @@ -1669,6 +1669,7 @@ static int RunRugnux(int argc, char **argv) { // A model that was asked for and could not be used has to reach the report, or the run ends // looking exactly like one that was never given --model. std::string model_validation_failure; + std::optional model_validation; if (!output_prefix.empty() && !model_pdb.empty()) { const auto data_sg = experiment.GetSpaceGroupNumber(); // With a reference MTZ the merohedral indexing was already resolved (stills per-image @@ -1678,6 +1679,7 @@ static int RunRugnux(int argc, char **argv) { output_prefix, logger, data_sg ? std::optional(static_cast(*data_sg)) : std::nullopt, /*probe_indexing_ambiguity=*/reference_data.empty()); + model_validation = validation; if (!validation.failure_reason.empty()) model_validation_failure = validation.failure_reason; if (data_sg.has_value()) { @@ -1742,6 +1744,7 @@ static int RunRugnux(int argc, char **argv) { scale_result.error_model_b = error_model_b; scale_result.has_reference = !reference_data.empty(); scale_result.twinning = twinning; + scale_result.model_validation = model_validation; if (!model_validation_failure.empty()) scale_result.warnings.push_back("Model validation did not run: " + model_validation_failure); provenance.wall_time_s = std::chrono::duration( diff --git a/tests/ResultReportTest.cpp b/tests/ResultReportTest.cpp index 57e3c698..9271413a 100644 --- a/tests/ResultReportTest.cpp +++ b/tests/ResultReportTest.cpp @@ -69,7 +69,7 @@ TEST_CASE("ResultReport_Render", "[Diagnostics]") { const auto text = RenderResultReport("prefix", "in.h5", x, result); // The stable keys a consumer greps for. - CHECK(text.find("\nREPORT_VERSION= 3\n") != std::string::npos); + CHECK(text.find("\nREPORT_VERSION= 4\n") != std::string::npos); CHECK(text.find("\nOUTPUT_PREFIX= prefix\n") != std::string::npos); CHECK(text.find("\nINDEXING_RATE= 0.8700\n") != std::string::npos); CHECK(text.find("\nSPACE_GROUP_NUMBER= 96\n") != std::string::npos); @@ -224,6 +224,48 @@ TEST_CASE("ResultReport_ProvenanceKeys", "[Diagnostics]") { CHECK(without.find("GPU_COUNT=") == std::string::npos); } +// A model that was asked for gets a section either way: the R-factors when it worked, and why it did +// not when it did not. A run that never asked for one has no section at all. +TEST_CASE("ResultReport_ModelValidationSection", "[Diagnostics]") { + DiffractionExperiment x(DetJF(1)); + x.ImagesPerTrigger(600); + + ProcessResult result; + result.images_processed = 600; + result.consensus_cell = UnitCell{.a = 79.0f, .b = 79.0f, .c = 38.0f, + .alpha = 90.0f, .beta = 90.0f, .gamma = 90.0f}; + + CHECK(RenderResultReport("p", "in.h5", x, result).find("MODEL VALIDATION") == std::string::npos); + + ModelValidationResult good; + good.ok = true; + good.model_path = "model.cif"; + good.model_space_group_number = 96; + good.r_work = 0.1823; + good.r_free = 0.2145; + good.n_work = 16682; + good.n_free = 928; + good.mean_atom_density_sigma = 1.42; + result.model_validation = good; + + const auto text = RenderResultReport("p", "in.h5", x, result); + CHECK(text.find("10. MODEL VALIDATION") != std::string::npos); + CHECK(text.find("\nMODEL_FILE= model.cif\n") != std::string::npos); + CHECK(text.find("\nR_FREE= 0.2145\n") != std::string::npos); + CHECK(text.find("\nR_FREE_REFLECTIONS= 928\n") != std::string::npos); + CHECK(text.find("MODEL_VALIDATION= NOT_PERFORMED") == std::string::npos); + + ModelValidationResult failed; + failed.model_path = "broken.pdb"; + failed.failure_reason = "model broken.pdb has no atoms or no unit cell"; + result.model_validation = failed; + + const auto failed_text = RenderResultReport("p", "in.h5", x, result); + CHECK(failed_text.find("\nMODEL_VALIDATION= NOT_PERFORMED\n") != std::string::npos); + CHECK(failed_text.find("has no atoms") != std::string::npos); + CHECK(failed_text.find("R_FREE=") == std::string::npos); +} + // get_gpu_description collapses repeats, so a four-card machine reads as one line rather than the // same name four times. Build-independent: without CUDA there are no names and it is empty. TEST_CASE("ResultReport_GpuDescription", "[Diagnostics]") {