rugnux report: XDS geometry keywords as one report section, not p_XDS.INP
The refined geometry is no longer written as a whole p_XDS.INP (and the report's XDS_INPUT= key is gone). Instead the report carries one section, XDS GEOMETRY KEYWORDS, with the XDS.INP keywords that carry the geometry the result was integrated at: ORGX/ORGY (PONI, 1-based), DETECTOR_DISTANCE, the detector axes where the detector is tilted, INCIDENT_BEAM_DIRECTION with X-RAY_WAVELENGTH, and ROTATION_AXIS in XDS's sense (rugnux's axis negated, as verified with XDS IDXREF). No cell or space group: XDS determines those. The lines carry a "# XDS.INP> " prefix, so sed lifts them ready to paste. The same keywords were logged by LogXDSGeometry at the start of every pass, with the axis in rugnux's sense; that printing is removed, so the report section is the one place they are stated. Docs updated (RUGNUX_REPORT, RUGNUX_INTEGRATION, RUGNUX, RUGNUX_TUTORIAL). Output only; p.mtz md5-identical on 3 battery sets. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
This commit is contained in:
+1
-2
@@ -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)
|
||||
```
|
||||
|
||||
|
||||
+14
-10
@@ -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.
|
||||
|
||||
|
||||
+26
-2
@@ -152,8 +152,32 @@ BEAM_CENTRE_SHIFT= 7.15
|
||||
BEAM_CENTRE_TOLERANCE= 3.49
|
||||
```
|
||||
|
||||
**`XDS_INPUT=`** in section 1 names `<prefix>_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.
|
||||
|
||||
@@ -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.
|
||||
- `<prefix>_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
|
||||
|
||||
@@ -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
|
||||
|
||||
+60
-4
@@ -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<float>(t[0] * RAD)).PoniRot2_rad(static_cast<float>(t[1] * RAD))
|
||||
.PoniRot3_rad(static_cast<float>(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' <this file>"));
|
||||
Add(s, Blank());
|
||||
Add(s, Prose(k));
|
||||
doc.sections.push_back(std::move(s));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- 2. CRYSTAL
|
||||
{
|
||||
ReportSection s;
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
+1
-3
@@ -302,7 +302,7 @@ struct ProcessResult {
|
||||
std::array<double, 3> 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<Coord> used_rotation_axis;
|
||||
std::optional<UnitCell> 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<double> lattice_angle_departure_deg;
|
||||
MeanProcessingTime mean_processing_time{};
|
||||
std::optional<std::string> written_master_path;
|
||||
// <prefix>_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
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <spdlog/fmt/fmt.h>
|
||||
|
||||
#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<float>(t[0] * RAD)).PoniRot2_rad(static_cast<float>(t[1] * RAD))
|
||||
.PoniRot3_rad(static_cast<float>(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<unsigned char>(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 (<prefix>_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;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "../common/DiffractionExperiment.h"
|
||||
#include "../common/Logger.h"
|
||||
#include "Rugnux.h"
|
||||
|
||||
// <prefix>_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 <output_prefix>_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);
|
||||
@@ -29,7 +29,6 @@
|
||||
#include "../reader/JFJochSMVReader.h"
|
||||
#include "../reader/JFJochStartMessageReader.h"
|
||||
#include "BrokerFeed.h"
|
||||
#include "XDSInput.h"
|
||||
#include "../writer/HDF5NXmx.h"
|
||||
#include <hdf5.h>
|
||||
#include <spdlog/sinks/basic_file_sink.h>
|
||||
@@ -3064,10 +3063,6 @@ static int RunRugnux(int argc, char **argv) {
|
||||
provenance.wall_time_s = std::chrono::duration<double>(
|
||||
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();
|
||||
|
||||
|
||||
+11
-21
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user