diff --git a/docs/RUGNUX.md b/docs/RUGNUX.md index 2b8c7f7e0..8b3198a35 100644 --- a/docs/RUGNUX.md +++ b/docs/RUGNUX.md @@ -52,7 +52,7 @@ that, or to lift the per-image loop's default ceiling of 16 workers per GPU. Nothing more is needed to pick the workflow: a dataset carrying a **goniometer axis** is processed as a rotation sweep, one without as **independent stills**, and scaling and merging run by default -in both. A rotation run that merges — the default — leaves nine files next to each other: +in both. A rotation run that merges — the default — leaves eight files next to each other: ``` myrun.mtz merged intensities + French-Wilson amplitudes, for CCP4 / phenix @@ -63,7 +63,6 @@ myrun_unmerged.mtz every observation before scaling, for pointless / aimless / myrun_report.txt what the run determined: cell, space group, statistics, warnings myrun_plot.txt one row per image, for plotting how the crystal behaved over the sweep myrun_detector.jpg the detector with the pixel mask and the beam-stop shadow drawn on it -myrun_XDS.INP the geometry the run refined, as an XDS input - a starting point for XDS myrun_log.txt the full timestamped log of the run (debug level) ``` diff --git a/docs/RUGNUX_INTEGRATION.md b/docs/RUGNUX_INTEGRATION.md index 542f5d8c5..f97e653f9 100644 --- a/docs/RUGNUX_INTEGRATION.md +++ b/docs/RUGNUX_INTEGRATION.md @@ -504,23 +504,27 @@ sweep that does not have them will get this far and no further. ## Comparing the geometry with XDS -Every run logs the detector geometry a second time in **XDS's convention**, so it can be read -straight across against the `IDXREF.LP` / `CORRECT.LP` of an XDS run on the same data: +The report states the geometry the result was integrated at in **XDS's convention**, in its +`XDS GEOMETRY KEYWORDS` section (see [the results report](RUGNUX_REPORT.md)), so it can be read +straight across against the `IDXREF.LP` / `CORRECT.LP` of an XDS run on the same data, or pasted into +`XDS.INP`: ``` -XDS convention: ORGX= 1091.00 ORGY= 1137.00 DETECTOR_DISTANCE= 75.0000 -XDS convention: DIRECTION_OF_DETECTOR_X-AXIS= 1.000000 0.000000 0.000000 -XDS convention: DIRECTION_OF_DETECTOR_Y-AXIS= 0.000000 1.000000 0.000000 -XDS convention: INCIDENT_BEAM_DIRECTION= 0 0 1 X-RAY_WAVELENGTH= 1.000000 QX= QY= 0.075000 -XDS convention: ROTATION_AXIS= -1.000000 0.000000 0.000000 +# XDS.INP> ORGX= 1091.00 ORGY= 1137.00 +# XDS.INP> DETECTOR_DISTANCE= 75.000 +# XDS.INP> INCIDENT_BEAM_DIRECTION= 0 0 1 X-RAY_WAVELENGTH= 1.000000 +# XDS.INP> ROTATION_AXIS= 1.000000 0.000000 0.000000 ``` -XDS is never given this geometry — the [XDS plugin](SOFTWARE_INTEGRATION.md) supplies image data -only, and XDS refines its own from `XDS.INP` — which is what makes the comparison worth having. The +The [XDS plugin](SOFTWARE_INTEGRATION.md) supplies image data only, so XDS refines its own geometry +from `XDS.INP` - which is what makes the comparison worth having. The two laboratory frames coincide (x along increasing detector column, y along increasing row, z along the beam), so the numbers are directly comparable, and a tilt appears as the two detector axis vectors rather than as angles, which is how XDS reports it after refinement. Two things to keep in mind: **`ORGX`/`ORGY` are 1-based**, because XDS counts pixels from 1 and Jungfraujoch from 0; and they are the **PONI**, the same quantity Jungfraujoch's beam centre is — so no correction is needed — -but not the direct beam once the detector is tilted (see above). +but not the direct beam once the detector is tilted (see above). **`ROTATION_AXIS` is rugnux's axis +negated**: rugnux turns the crystal by -phi about its axis, XDS by +phi, and with rugnux's own sign +`IDXREF` indexes only the chance floor of a mis-signed spindle. The detector axes appear only where the +detector is tilted. diff --git a/docs/RUGNUX_REPORT.md b/docs/RUGNUX_REPORT.md index 162a97c74..cf20d76cf 100644 --- a/docs/RUGNUX_REPORT.md +++ b/docs/RUGNUX_REPORT.md @@ -152,8 +152,32 @@ BEAM_CENTRE_SHIFT= 7.15 BEAM_CENTRE_TOLERANCE= 3.49 ``` -**`XDS_INPUT=`** in section 1 names `_XDS.INP`, the same geometry written as an XDS input -(rotation runs; see the tutorial's list of output files). +**`XDS GEOMETRY KEYWORDS`**, the section after section 1, is the same geometry as the `XDS.INP` +keywords that carry it, ready to paste into an XDS run: `ORGX`/`ORGY` (the PONI, numbered from 1 as XDS +numbers pixels), `DETECTOR_DISTANCE`, the detector axes `DIRECTION_OF_DETECTOR_X-AXIS`/`Y-AXIS` where the +detector is tilted (they carry the tilt), `INCIDENT_BEAM_DIRECTION` with `X-RAY_WAVELENGTH`, and +`ROTATION_AXIS` - the axis the run integrated at, **negated**, because XDS turns the crystal the other way +about the axis it is given (checked with XDS `IDXREF`). Geometry only: XDS determines the cell and the +space group itself. The lines carry a fixed prefix, so + +```bash +sed -n 's/^# XDS.INP> //p' myrun_report.txt +``` + +prints them as XDS reads them: + +``` +ORGX= 749.15 ORGY= 817.32 +DETECTOR_DISTANCE= 175.668 +DIRECTION_OF_DETECTOR_X-AXIS= 1.000000 0.000000 0.000444 +DIRECTION_OF_DETECTOR_Y-AXIS= 0.000000 1.000000 -0.000976 +INCIDENT_BEAM_DIRECTION= 0 0 1 X-RAY_WAVELENGTH= 0.918400 +ROTATION_AXIS= 1.000000 0.000000 0.000000 +``` + +The conventions are XDS's own (W. Kabsch, "XDS" (2010), Acta Cryst. D66, 125-132 +[doi:10.1107/S0907444909047337](https://doi.org/10.1107/S0907444909047337)); this section is the one +place the run states them. **Which pass.** A rotation run integrates twice — once at the geometry in the input file (the first half of the sweep but at least 120°, `--prepass-fraction`), then the whole sweep again at the post-refined geometry — and can integrate a third time if a guard rejects the second pass. diff --git a/docs/RUGNUX_TUTORIAL.md b/docs/RUGNUX_TUTORIAL.md index 8f68b2acd..213fe8db5 100644 --- a/docs/RUGNUX_TUTORIAL.md +++ b/docs/RUGNUX_TUTORIAL.md @@ -331,20 +331,6 @@ Absorption is corrected only by the empirical surface a rotation run fits and ke is missing. A shadow drawn where the background is unbroken, or a dark patch the run left uncoloured, is visible at a glance; read it against the `Beam stop shadow: N pixels ...` line in the log. -- `_XDS.INP` — **the geometry the run ended on, as an XDS input**: on a rotation run the - post-refined beam centre (`ORGX`/`ORGY`, the foot of the perpendicular, numbered from 1 as XDS - numbers pixels), distance, detector axes (`DIRECTION_OF_DETECTOR_X-AXIS`/`Y-AXIS`, which carry the - tilt), wavelength, rotation axis (the one the run integrated at, after any sign rescue, negated: XDS turns - the crystal the other way about the axis it is given), oscillation - (scaled where the run adopted a goniometer rotation scale), starting angle and data range, plus the - space group and cell (left commented out where the group is not in the reference setting of its - number, which is the only setting a number names), `FRIEDEL'S_LAW` per `-A` and the merged - resolution range. For HDF5 the template is the master file and the `LIB=` line for the reader - plugin is left for the user to point at durin or neggia; the file's own pixel mask reaches XDS - through the plugin, the beam-stop shadow rugnux detected does not (XDS finds its own in `DEFPIX`). - The conventions are XDS's own (W. Kabsch, "XDS" (2010), Acta Cryst. D66, 125-132 - [doi:10.1107/S0907444909047337](https://doi.org/10.1107/S0907444909047337)). It is a starting - point - XDS refines the geometry again - and the report names it as **`XDS_INPUT=`** in section 1. Merged statistics (⟨I/σ⟩, CC1/2, completeness, …), the error model and timing are printed to the console. By default the written resolution is trimmed automatically where CC1/2 falls off diff --git a/rugnux/CMakeLists.txt b/rugnux/CMakeLists.txt index 5db9f5764..6b437d83b 100644 --- a/rugnux/CMakeLists.txt +++ b/rugnux/CMakeLists.txt @@ -23,8 +23,6 @@ ADD_LIBRARY(JFJochRugnux STATIC DiagnosticOutput.h BrokerFeed.cpp BrokerFeed.h - XDSInput.cpp - XDSInput.h ) TARGET_LINK_LIBRARIES(JFJochRugnux JFJochReader JFJochImageAnalysis JFJochWriter JFJochModelWriter JFJochPreview gemmi fftw3f diff --git a/rugnux/ResultReport.cpp b/rugnux/ResultReport.cpp index 4be52ae86..3b563f951 100644 --- a/rugnux/ResultReport.cpp +++ b/rugnux/ResultReport.cpp @@ -43,7 +43,8 @@ namespace { // --developer key on rotation runs; POWDER_ICE_* / POWDER_NON_ICE_SPOT_FRACTION; // the ICE_RINGS and POWDER_RINGS flags. // 16: FILE_DIRECT_BEAM / BACKGROUND_DIRECT_BEAM / BEAM_CENTRE_SHIFT / BEAM_CENTRE_TOLERANCE in - // section 1 and the BEAM_CENTRE_OFF flag - the file's beam centre against the refined one. + // section 1 and the BEAM_CENTRE_OFF flag - the file's beam centre against the refined one; + // the XDS GEOMETRY KEYWORDS section; OVERLOADED_* / OVERLAPPED_* and the OVERLOADS / OVERLAPS flags. // 8: every line that is not `KEY= value` and not blank starts with `#`, warnings included // (`# WARNING:`); the header gains RUGNUX_DOWNLOAD and BUILD_CXX_FLAGS, and RUGNUX_GIT is // the full build-time hash. @@ -553,9 +554,6 @@ ReportDocument BuildReportDocument(const std::string &output_prefix, } if (result.spot_resolution_estimate_A.has_value()) Add(s, KeyReal("SPOT_RESOLUTION_ESTIMATE", *result.spot_resolution_estimate_A, "{:.2f}")); - // The same geometry once more, as an XDS input (rugnux/XDSInput.h). - if (!result.xds_input_path.empty()) - Add(s, KeyText("XDS_INPUT", result.xds_input_path)); if (result.pass_count > 1) { Add(s, KeyText("PASS", fmt::format("{} of {}", result.pass_number, result.pass_count))); Add(s, KeyText("PASS_DECISION", result.pass_decision, true)); @@ -682,6 +680,64 @@ ReportDocument BuildReportDocument(const std::string &output_prefix, doc.sections.push_back(std::move(s)); } + // ------------------------------------------------- XDS GEOMETRY KEYWORDS + // The geometry the result was integrated at - on a rotation run the post-refined one - as the XDS.INP + // keywords that carry it, so it can be pasted into an XDS run rather than retyped. Geometry only: + // XDS determines the cell and the space group itself. Comment lines, like all prose here, with a + // fixed prefix so `sed -n 's/^# XDS.INP> //p'` lifts them. + // + // Following the XDS conventions, Kabsch (2010) Acta Cryst. D66, 125-132: XDS places a pixel at + // x_lab(i,j) = (i-ORGX)*QX*X_axis + (j-ORGY)*QY*Y_axis + DETECTOR_DISTANCE*(X_axis x Y_axis) + // in a laboratory frame that is ours (x along a column step, y along a row step, z along the beam), + // with pixels numbered from 1 and ORGX/ORGY the foot of the perpendicular from the crystal - our PONI + // (docs/DETECTOR_GEOMETRY.md). A tilt therefore goes out as the two detector axes, taken as + // differences of LabCoord. + // + // ROTATION_AXIS is rugnux's NEGATED: rugnux turns the crystal by -phi about its axis (IndexAndRefine + // takes the lattice to an image with GetTransformationAngle(-angle)), XDS by a right-handed +phi about + // ROTATION_AXIS. Checked with IDXREF on two sets given this geometry: the negated axis indexes 99.5% + // and 14% of the spots, rugnux's own sign 37% and 0.04% (the chance floor of a mis-signed spindle). + // The axis is the one the run integrated at, after any sign rescue. + if (std::isfinite(result.used_beam_x_pxl) && std::isfinite(result.used_distance_mm)) { + constexpr double RAD = PI / 180.0; + const auto &t = result.used_detector_tilt_deg; + DiffractionExperiment x = experiment; + x.BeamX_pxl(result.used_beam_x_pxl).BeamY_pxl(result.used_beam_y_pxl) + .DetectorDistance_mm(result.used_distance_mm) + .PoniRot1_rad(static_cast(t[0] * RAD)).PoniRot2_rad(static_cast(t[1] * RAD)) + .PoniRot3_rad(static_cast(t[2] * RAD)); + const auto geom = x.GetDiffractionGeometry(); + const float bx = geom.GetBeamX_pxl(), by = geom.GetBeamY_pxl(), pixel_mm = geom.GetPixelSize_mm(); + const Coord origin = geom.LabCoord(bx, by); + const Coord x_axis = ((geom.LabCoord(bx + 1.0f, by) - origin) / pixel_mm).Normalize(); + const Coord y_axis = ((geom.LabCoord(bx, by + 1.0f) - origin) / pixel_mm).Normalize(); + + std::string k; + const auto line = [&k](const std::string &l) { k += " XDS.INP> " + l + "\n"; }; + line(fmt::format("ORGX= {:.2f} ORGY= {:.2f}", bx + 1.0f, by + 1.0f)); + line(fmt::format("DETECTOR_DISTANCE= {:.3f}", geom.GetDetectorDistance_mm())); + if (t[0] != 0.0 || t[1] != 0.0 || t[2] != 0.0) { + line(fmt::format("DIRECTION_OF_DETECTOR_X-AXIS= {:.6f} {:.6f} {:.6f}", x_axis.x, x_axis.y, x_axis.z)); + line(fmt::format("DIRECTION_OF_DETECTOR_Y-AXIS= {:.6f} {:.6f} {:.6f}", y_axis.x, y_axis.y, y_axis.z)); + } + line(fmt::format("INCIDENT_BEAM_DIRECTION= 0 0 1 X-RAY_WAVELENGTH= {:.6f}", geom.GetWavelength_A())); + if (const auto gonio = experiment.GetGoniometer()) { + const Coord axis = -result.used_rotation_axis.value_or(gonio->GetAxis()); + // + 0.0f: no "-0.000000" from a negated zero. + line(fmt::format("ROTATION_AXIS= {:.6f} {:.6f} {:.6f}", axis.x + 0.0f, axis.y + 0.0f, axis.z + 0.0f)); + } + k.pop_back(); + + ReportSection s; + s.title = "XDS GEOMETRY KEYWORDS"; + Add(s, Prose(" The geometry this result was integrated at, as XDS.INP keywords - ORGX/ORGY numbered from\n" + " 1, ROTATION_AXIS in XDS's sense of rotation. Geometry only; XDS determines the cell and\n" + " space group itself. Lift them with: sed -n 's/^# XDS.INP> //p' ")); + Add(s, Blank()); + Add(s, Prose(k)); + doc.sections.push_back(std::move(s)); + } + // ------------------------------------------------------------- 2. CRYSTAL { ReportSection s; diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 33dd36c9f..41f94a17f 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -309,73 +309,6 @@ Rugnux::Rugnux(JFJochReader &reader, DiffractionExperiment experiment, } } -// Report the detector geometry the way XDS states it, so a run can be compared line by line against -// the IDXREF.LP / CORRECT.LP of XDS on the same data. XDS is never given this geometry - the durin -// plugin hands it image data only (plugin_get_header returns dimensions, bytes per pixel, pixel size -// and frame count, nothing else) and XDS refines its own from XDS.INP - which is precisely why -// having ours in the same convention is worth printing. -// -// The two laboratory frames coincide: x along increasing detector column, y along increasing row, -// z along the beam. XDS places a pixel at -// x_lab(i,j) = (i-ORGX)*QX*X_axis + (j-ORGY)*QY*Y_axis + DETECTOR_DISTANCE*(X_axis x Y_axis) -// and DiffractionGeometry::LabCoord is that same expression with X_axis = poni_rot*(1,0,0) and -// Y_axis = poni_rot*(0,1,0). So the tilt goes out as the two axis vectors rather than as angles, -// which is also how XDS reports it after refinement. Taking the axes as differences of LabCoord -// keeps them in step with whatever the geometry currently is, tilt included. -// -// ORGX/ORGY are 1-based - XDS counts pixels from 1 - and are the foot of the perpendicular from the -// crystal rather than the direct beam, which is what our beam centre is too -// (docs/DETECTOR_GEOMETRY.md), so they map across without correction. -// -// ROTATION_AXIS is printed once per pass, and it is not always the vector stored in the file: the -// sign rescue further down adopts the opposite sign when the file's own indexes almost nothing, and -// the following pass then prints what it adopted. On the corpus the first and last line of a run -// disagree in sign on 12 of 115 rotation runs, so the LAST one - the axis the run actually -// integrated at - is the one to read. -// -// That sign is not reliably the one XDS wants - but it is not a constant either, so do NOT negate -// this line on principle. It is paired with the detector axis directions printed just above it and -// has to be read together with them: two rotation datasets whose detector axes are both +x/+y wanted -// the negation, while a third whose axes are negative wanted the sign as printed. The measure is the -// fraction of COLSPOT spots IDXREF can index - 8.9% as printed against 92.6% negated, and 1060/10080 -// against 9624/10080; the other way round, 43.9% as printed against 13.5%. -// -// Read the HIGH member of each pair. That statistic has a chance floor which reaches about 15% on a -// dense reciprocal lattice - a null with the frame numbers permuted scored 14.9% against a real -// 15.4% - so a low value means "IDXREF gave up", not a measured near-zero: both wrong-sign runs -// above stopped on INSUFFICIENT PERCENTAGE (< 50%) OF INDEXED REFLECTIONS, one of them after -// refining the right cell and rejecting 9020 spots as too far from their ideal positions, which is -// the signature of a mis-signed spindle. So try both signs and keep the one that indexes; a sign -// carried over from another file is worth nothing. -// -// One of those two runs read a file our own converter had written, whose axis default was the -// negative sign. It therefore says what XDS does with a given sign, which is what this comment is -// about, and nothing about what a foreign file stores. -static void LogXDSGeometry(const DiffractionExperiment &experiment) { - Logger logger("Rugnux"); - - const auto geom = experiment.GetDiffractionGeometry(); - const float beam_x = geom.GetBeamX_pxl(); - const float beam_y = geom.GetBeamY_pxl(); - const float pixel_mm = geom.GetPixelSize_mm(); - - const Coord origin = geom.LabCoord(beam_x, beam_y); - const Coord x_axis = ((geom.LabCoord(beam_x + 1.0f, beam_y) - origin) / pixel_mm).Normalize(); - const Coord y_axis = ((geom.LabCoord(beam_x, beam_y + 1.0f) - origin) / pixel_mm).Normalize(); - - logger.Info("XDS convention: ORGX= {:.2f} ORGY= {:.2f} DETECTOR_DISTANCE= {:.4f}", - beam_x + 1.0f, beam_y + 1.0f, geom.GetDetectorDistance_mm()); - logger.Info("XDS convention: DIRECTION_OF_DETECTOR_X-AXIS= {:.6f} {:.6f} {:.6f}", - x_axis.x, x_axis.y, x_axis.z); - logger.Info("XDS convention: DIRECTION_OF_DETECTOR_Y-AXIS= {:.6f} {:.6f} {:.6f}", - y_axis.x, y_axis.y, y_axis.z); - logger.Info("XDS convention: INCIDENT_BEAM_DIRECTION= 0 0 1 X-RAY_WAVELENGTH= {:.6f} QX= QY= {:.6f}", - geom.GetWavelength_A(), pixel_mm); - if (const auto goniometer = experiment.GetGoniometer()) - logger.Info("XDS convention: ROTATION_AXIS= {:.6f} {:.6f} {:.6f}", - goniometer->GetAxis().x, goniometer->GetAxis().y, goniometer->GetAxis().z); -} - namespace rugnux_internal { // The FFT recovers a basis vector only up to fft_max_unit_cell_A: FFTIndexer sizes its projected // histogram from that value and the transform's last usable bin IS that length. So a reference cell @@ -787,7 +720,6 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b : calibration ? "powder calibration" : "azimuthal integration"; logger.Info("Processing {} images (range {}-{}, stride {}) using {} threads [{}]", images_to_process, start_image, end_image, config_.stride, config_.nthreads, mode_name); - LogXDSGeometry(experiment_); if (observer) observer->OnPhase(full ? "Full analysis" : calibration ? "Powder calibration" : "Azimuthal integration"); diff --git a/rugnux/Rugnux.h b/rugnux/Rugnux.h index cbe0bafbd..55a34ad6e 100644 --- a/rugnux/Rugnux.h +++ b/rugnux/Rugnux.h @@ -302,7 +302,7 @@ struct ProcessResult { std::array used_detector_tilt_deg{}; double used_direct_beam_x_pxl = 0.0, used_direct_beam_y_pxl = 0.0; // The rotation axis the same pass integrated at - the sign rescue can adopt the opposite of the - // file's (see LogXDSGeometry). Empty on stills. + // file's. The report's XDS keyword block reads it. Empty on stills. std::optional used_rotation_axis; std::optional consensus_cell; // The centring `consensus_cell` is written in; empty when nothing named one (the stills path @@ -329,8 +329,6 @@ struct ProcessResult { std::optional lattice_angle_departure_deg; MeanProcessingTime mean_processing_time{}; std::optional written_master_path; - // _XDS.INP, where the CLI wrote the run's geometry as an XDS input (rugnux/XDSInput.h). - std::string xds_input_path; std::string merge_statistics_text; // populated when scaling/merging ran // Structured merge statistics (per-shell + overall), populated when scaling/merging ran. ISa is diff --git a/rugnux/XDSInput.cpp b/rugnux/XDSInput.cpp deleted file mode 100644 index e8361c8f8..000000000 --- a/rugnux/XDSInput.cpp +++ /dev/null @@ -1,167 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute -// SPDX-License-Identifier: GPL-3.0-only - -#include -#include -#include -#include -#include - -#include - -#include "../common/GitInfo.h" -#include "../common/JFJochMath.h" -#include "XDSInput.h" - -std::string RenderXDSInput(const std::string &input_file, - const DiffractionExperiment &experiment, - const ProcessResult &result, - int64_t first_image, int64_t end_image) { - // The geometry the result was integrated at, on a copy of the file's experiment. - DiffractionExperiment x = experiment; - if (std::isfinite(result.used_beam_x_pxl) && std::isfinite(result.used_distance_mm)) { - constexpr double RAD = PI / 180.0; - const auto &t = result.used_detector_tilt_deg; - x.BeamX_pxl(result.used_beam_x_pxl).BeamY_pxl(result.used_beam_y_pxl) - .DetectorDistance_mm(result.used_distance_mm) - .PoniRot1_rad(static_cast(t[0] * RAD)).PoniRot2_rad(static_cast(t[1] * RAD)) - .PoniRot3_rad(static_cast(t[2] * RAD)); - } - const auto geom = x.GetDiffractionGeometry(); - const float beam_x = geom.GetBeamX_pxl(); - const float beam_y = geom.GetBeamY_pxl(); - const float pixel_mm = geom.GetPixelSize_mm(); - - // Following the XDS conventions, Kabsch (2010) Acta Cryst. D66, 125-132: XDS places a pixel at - // x_lab(i,j) = (i-ORGX)*QX*X_axis + (j-ORGY)*QY*Y_axis + DETECTOR_DISTANCE*(X_axis x Y_axis) - // in a laboratory frame that is ours (x along a column step, y along a row step, z along the - // beam), with pixels numbered from 1 and ORGX/ORGY the foot of the perpendicular from the crystal - - // our PONI (see LogXDSGeometry in Rugnux.cpp and docs/DETECTOR_GEOMETRY.md). So the tilt goes out - // as the two detector axes, taken as differences of LabCoord. - const Coord origin = geom.LabCoord(beam_x, beam_y); - const Coord x_axis = ((geom.LabCoord(beam_x + 1.0f, beam_y) - origin) / pixel_mm).Normalize(); - const Coord y_axis = ((geom.LabCoord(beam_x, beam_y + 1.0f) - origin) / pixel_mm).Normalize(); - - const bool hdf5 = input_file.ends_with(".h5") || input_file.ends_with(".nxs"); - const bool cbf = input_file.ends_with(".cbf") || input_file.ends_with(".cbf.gz"); - std::string input_path = input_file; - try { - input_path = std::filesystem::absolute(input_file).string(); - } catch (const std::exception &) { - } - - // A CBF sweep is one file per frame: XDS takes a template with ? for the frame number - the last run - // of digits in the name - and numbers the frames by it, so the data range starts at the given file's. - std::string template_path = input_path; - int64_t first_frame = 1; - // The HDF5 plugins take the master file's name with "master" as ??????, as XDS has it for - // an EIGER data set. - if (hdf5 && input_path.ends_with("_master.h5")) - template_path.replace(template_path.size() - 9, 6, "??????"); - if (cbf) { - const size_t name = template_path.find_last_of('/') + 1; - const size_t end = template_path.find_last_of("0123456789"); - if (end != std::string::npos && end >= name) { - size_t begin = end; - while (begin > name && std::isdigit(static_cast(template_path[begin - 1]))) - --begin; - first_frame = std::stoll(template_path.substr(begin, end - begin + 1)); - template_path.replace(begin, end - begin + 1, std::string(end - begin + 1, '?')); - } - } - - std::string s; - s += fmt::format("! XDS.INP written by rugnux {} from {}\n", jfjoch_version(), input_path); - s += "! The detector geometry is the one rugnux integrated at - on a rotation run the post-refined\n" - "! one - not the file's; XDS refines it again from here. Check before use: this file is a\n" - "! starting point, not a validated XDS setup.\n"; - s += "JOB= XYCORR INIT COLSPOT IDXREF DEFPIX INTEGRATE CORRECT\n\n"; - - if (hdf5) - s += "! HDF5 is read through a plugin: uncomment and point LIB= at durin-plugin.so (or the\n" - "! Dectris neggia plugin), which opens the _master.h5 the template names with ??????.\n" - "! The plugin applies the file's own pixel mask.\n" - "!LIB= /path/to/durin-plugin.so\n"; - else if (!cbf) - s += "! rugnux read this input itself; XDS may need the frames converted, and the template\n" - "! below written with ?????? in place of the frame number.\n"; - s += fmt::format("NAME_TEMPLATE_OF_DATA_FRAMES= {}\n", template_path); - s += fmt::format("DATA_RANGE= {} {}\n", first_frame + first_image, first_frame + end_image - 1); - s += fmt::format("SPOT_RANGE= {} {}\n\n", first_frame + first_image, first_frame + end_image - 1); - - s += fmt::format("DETECTOR= {} MINIMUM_VALID_PIXEL_VALUE= 0 OVERLOAD= {}\n", - cbf ? "PILATUS" : "EIGER", SaturationValueFromLimit(experiment.GetSaturationLimit())); - s += fmt::format("NX= {} NY= {} QX= {:.6f} QY= {:.6f}\n", - experiment.GetXPixelsNumConv(), experiment.GetYPixelsNumConv(), pixel_mm, pixel_mm); - const float thickness_um = experiment.GetDetectorSetup().GetSensorThickness_um(); - if (thickness_um > 0.0f) - s += fmt::format("SENSOR_THICKNESS= {:.3f}\n", thickness_um / 1000.0f); - s += fmt::format("ORGX= {:.2f} ORGY= {:.2f}\n", beam_x + 1.0f, beam_y + 1.0f); - s += fmt::format("DETECTOR_DISTANCE= {:.3f}\n", geom.GetDetectorDistance_mm()); - s += fmt::format("DIRECTION_OF_DETECTOR_X-AXIS= {:.6f} {:.6f} {:.6f}\n", x_axis.x, x_axis.y, x_axis.z); - s += fmt::format("DIRECTION_OF_DETECTOR_Y-AXIS= {:.6f} {:.6f} {:.6f}\n", y_axis.x, y_axis.y, y_axis.z); - s += "INCIDENT_BEAM_DIRECTION= 0 0 1\n"; - s += fmt::format("X-RAY_WAVELENGTH= {:.6f}\n", geom.GetWavelength_A()); - s += "! Polarization is not in the file; a typical synchrotron:\n" - "!FRACTION_OF_POLARIZATION= 0.99 POLARIZATION_PLANE_NORMAL= 0 1 0\n\n"; - - if (const auto gonio = experiment.GetGoniometer()) { - // Negated: rugnux turns the crystal by -phi about its axis (IndexAndRefine takes the lattice - // to an image with GetTransformationAngle(-angle)), XDS by a right-handed +phi about - // ROTATION_AXIS. Checked with IDXREF on the export: 99.5% of the spots index with the - // negated axis, 37% (the chance floor of a mis-signed spindle) with rugnux's own. - const Coord axis = -result.used_rotation_axis.value_or(gonio->GetAxis()); - // The oscillation the run integrated at: the file's, scaled where the run adopted a goniometer - // rotation scale (as OSCILLATION_RANGE in the report). - const double scale = result.post_refine && result.post_refine->rotation_scale_suspect - ? result.post_refine->rotation_scale : 1.0; - // + 0.0f: no "-0.000000" from a negated zero. - s += fmt::format("ROTATION_AXIS= {:.6f} {:.6f} {:.6f}\n", axis.x + 0.0f, axis.y + 0.0f, axis.z + 0.0f); - s += fmt::format("OSCILLATION_RANGE= {:.6f}\n", gonio->GetIncrement_deg() * scale); - s += fmt::format("STARTING_FRAME= {}\n", first_frame); - s += fmt::format("STARTING_ANGLE= {:.4f}\n\n", gonio->GetStart_deg()); - } - - if (result.space_group && result.consensus_cell) { - const auto &c = *result.consensus_cell; - // A number names only the reference setting; a cell in another one would be read wrongly. - const char *prefix = result.space_group->is_reference_setting() ? "" : "!"; - if (*prefix) - s += fmt::format("! The run's space group {} is not in the reference setting of its number;\n" - "! XDS would read the cell below in the wrong setting, so both are left out.\n", - result.space_group->xhm()); - s += fmt::format("{}SPACE_GROUP_NUMBER= {}\n", prefix, result.space_group->number); - s += fmt::format("{}UNIT_CELL_CONSTANTS= {:.3f} {:.3f} {:.3f} {:.3f} {:.3f} {:.3f}\n", prefix, - c.a, c.b, c.c, c.alpha, c.beta, c.gamma); - } - s += fmt::format("FRIEDEL'S_LAW= {}\n", experiment.GetScalingSettings().GetMergeFriedel() ? "TRUE" : "FALSE"); - if (result.has_merge_statistics) - s += fmt::format("INCLUDE_RESOLUTION_RANGE= {:.3f} {:.3f}\n", - result.merge_statistics.overall.d_max, result.merge_statistics.overall.d_min); - s += "\n! rugnux also masked the beam-stop shadow it detected (_detector.jpg); XDS finds its\n" - "! own in DEFPIX, or add UNTRUSTED_RECTANGLE= / UNTRUSTED_QUADRILATERAL= lines here.\n"; - return s; -} - -std::string WriteXDSInput(const std::string &output_prefix, - const std::string &input_file, - const DiffractionExperiment &experiment, - const ProcessResult &result, - int64_t first_image, int64_t end_image, - Logger &logger) { - if (output_prefix.empty() || !experiment.GetGoniometer()) - return {}; - const std::string filename = output_prefix + "_XDS.INP"; - try { - std::ofstream f(filename); - if (!f) - throw std::runtime_error("cannot open for writing"); - f << RenderXDSInput(input_file, experiment, result, first_image, end_image); - if (!f) - throw std::runtime_error("write failed"); - } catch (const std::exception &e) { - logger.Warning("Could not write the XDS input {}: {}", filename, e.what()); - return {}; - } - return filename; -} diff --git a/rugnux/XDSInput.h b/rugnux/XDSInput.h deleted file mode 100644 index 393cfd7ef..000000000 --- a/rugnux/XDSInput.h +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute -// SPDX-License-Identifier: GPL-3.0-only - -#pragma once - -#include -#include - -#include "../common/DiffractionExperiment.h" -#include "../common/Logger.h" -#include "Rugnux.h" - -// _XDS.INP: the geometry the run integrated at - on a rotation run the post-refined one - as an -// XDS input, so the same data can be processed by XDS from where rugnux ended rather than from the -// header. `experiment` is the one the run was given (the file's), `result` carries what the run -// refined; the images are the file's 0-based [first_image, end_image). -std::string RenderXDSInput(const std::string &input_file, - const DiffractionExperiment &experiment, - const ProcessResult &result, - int64_t first_image, int64_t end_image); - -// Writes _XDS.INP and returns its name; returns an empty string, writing nothing, on a -// run with no output prefix or no rotation, and logs and swallows a failure to write. -std::string WriteXDSInput(const std::string &output_prefix, - const std::string &input_file, - const DiffractionExperiment &experiment, - const ProcessResult &result, - int64_t first_image, int64_t end_image, - Logger &logger); diff --git a/rugnux/rugnux_cli.cpp b/rugnux/rugnux_cli.cpp index 4e9d30bc5..9e74ef787 100644 --- a/rugnux/rugnux_cli.cpp +++ b/rugnux/rugnux_cli.cpp @@ -29,7 +29,6 @@ #include "../reader/JFJochSMVReader.h" #include "../reader/JFJochStartMessageReader.h" #include "BrokerFeed.h" -#include "XDSInput.h" #include "../writer/HDF5NXmx.h" #include #include @@ -3064,10 +3063,6 @@ static int RunRugnux(int argc, char **argv) { provenance.wall_time_s = std::chrono::duration( std::chrono::steady_clock::now() - invocation_start).count(); TimingMark report_mark(logger, "results report"); - // The geometry the run ended on, as an XDS input beside the report, which names it. - if (rotation_indexing) - result.xds_input_path = WriteXDSInput(output_prefix, input_file, experiment, result, start_image, - end_image, logger); const ReportDocument doc = WriteResultReport(output_prefix, input_file, experiment, result, logger, provenance); report_mark.Stop(); diff --git a/tests/ResultReportTest.cpp b/tests/ResultReportTest.cpp index ff0e11731..5d60a7a0d 100644 --- a/tests/ResultReportTest.cpp +++ b/tests/ResultReportTest.cpp @@ -7,7 +7,6 @@ #include "../image_analysis/scale_merge/Merge.h" #include "../reader/JFJochHDF5Reader.h" #include "../rugnux/ResultReport.h" -#include "../rugnux/XDSInput.h" #include "../common/CUDAWrapper.h" #include "../writer/FileWriter.h" @@ -1271,35 +1270,26 @@ TEST_CASE("ResultReport_OverloadsAndOverlaps", "[Diagnostics]") { CHECK(text.find("Move the detector back") != std::string::npos); } -TEST_CASE("XDSInput_Render", "[Diagnostics]") { +TEST_CASE("ResultReport_XDSGeometryKeywords", "[Diagnostics]") { DiffractionExperiment x(DetJF(1)); x.BeamX_pxl(500.0f).BeamY_pxl(400.0f).DetectorDistance_mm(100.0f); x.Goniometer(GoniometerAxis("omega", 10.0f, 0.1f, Coord(1, 0, 0), {})); ProcessResult result; + result.consensus_cell = UnitCell{.a = 79.0f, .b = 79.0f, .c = 38.0f, + .alpha = 90.0f, .beta = 90.0f, .gamma = 90.0f}; // The refined geometry, not the file's, goes out - 1-based, untilted here. result.used_beam_x_pxl = 510.0f; result.used_beam_y_pxl = 395.5f; result.used_distance_mm = 120.0f; result.used_rotation_axis = Coord(-1, 0, 0); - const auto text = RenderXDSInput("/data/x_master.h5", x, result, 0, 900); - CHECK(text.find("\nORGX= 511.00 ORGY= 396.50\n") != std::string::npos); - CHECK(text.find("\nDETECTOR_DISTANCE= 120.000\n") != std::string::npos); - CHECK(text.find("\nDIRECTION_OF_DETECTOR_X-AXIS= 1.000000 0.000000 0.000000\n") != std::string::npos); + const auto text = RenderResultReport("p", "in.h5", x, result); + CHECK(text.find("XDS GEOMETRY KEYWORDS") != std::string::npos); + CHECK(text.find("\n# XDS.INP> ORGX= 511.00 ORGY= 396.50\n") != std::string::npos); + CHECK(text.find("\n# XDS.INP> DETECTOR_DISTANCE= 120.000\n") != std::string::npos); // XDS turns the crystal the other way about the axis it is given. - CHECK(text.find("\nROTATION_AXIS= 1.000000 0.000000 0.000000\n") != std::string::npos); - CHECK(text.find("\nNAME_TEMPLATE_OF_DATA_FRAMES= /data/x_??????.h5\n") != std::string::npos); - CHECK(text.find("\nDATA_RANGE= 1 900\n") != std::string::npos); - CHECK(text.find("\nSTARTING_ANGLE= 10.0000\n") != std::string::npos); - CHECK(text.find("\nOSCILLATION_RANGE= 0.100000\n") != std::string::npos); -} - -TEST_CASE("XDSInput_CBFTemplate", "[Diagnostics]") { - DiffractionExperiment x(DetJF(1)); - x.Goniometer(GoniometerAxis("omega", 0.0f, 0.1f, Coord(1, 0, 0), {})); - const auto text = RenderXDSInput("/data/run_2_0101.cbf", x, ProcessResult{}, 0, 50); - CHECK(text.find("\nNAME_TEMPLATE_OF_DATA_FRAMES= /data/run_2_????.cbf\n") != std::string::npos); - CHECK(text.find("\nDATA_RANGE= 101 150\n") != std::string::npos); - CHECK(text.find("\nSTARTING_FRAME= 101\n") != std::string::npos); - CHECK(text.find("\nDETECTOR= PILATUS") != std::string::npos); + CHECK(text.find("\n# XDS.INP> ROTATION_AXIS= 1.000000 0.000000 0.000000\n") != std::string::npos); + // Untilted: no detector axes; and never the cell or the space group. + CHECK(text.find("XDS.INP> DIRECTION_OF_DETECTOR") == std::string::npos); + CHECK(text.find("XDS.INP> UNIT_CELL") == std::string::npos); }