// SPDX-FileCopyrightText: 2024 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../reader/JFJochHDF5Reader.h" #include "../reader/JFJochMultiReader.h" #include "../reader/JFJochCBFReader.h" #include "../reader/JFJochMarCCDReader.h" #include "../reader/JFJochSMVReader.h" #include "../reader/JFJochXFELReader.h" #include "../reader/JFJochXTCReader.h" #include "../reader/CrystFELGeometry.h" #include "../reader/NXmxModules.h" #include "../reader/JFJochStartMessageReader.h" #include "BrokerFeed.h" #include "../writer/HDF5NXmx.h" #include "../writer/NXmxModules.h" #include #include #include #include #include #include "../common/CudaJitCache.h" #include "../common/Logger.h" #include "../common/ThreadAffinity.h" #include "../common/GitInfo.h" #include "../common/time_utc.h" #include "../common/Definitions.h" #include "../common/DiffractionExperiment.h" #include "../common/PixelMask.h" #include "../common/CUDAWrapper.h" #include "../common/JFJochMath.h" #include "../image_analysis/bragg_integration/CalcISigma.h" #include "../image_analysis/geom_refinement/Calibrants.h" #include "../image_analysis/indexing/IndexerThreadPool.h" #include "../image_analysis/LoadFCalcFromMtz.h" #include "../image_analysis/SensorAbsorption.h" #include "../image_analysis/UpdateReflectionResolution.h" #include "../image_analysis/WriteReflections.h" #include "../image_analysis/scale_merge/CrystalSetting.h" // CellInBasis, HklOperator #include "../image_analysis/scale_merge/Merge.h" #include "../image_analysis/scale_merge/RfreeFlags.h" #include "../image_analysis/scale_merge/ReindexAmbiguity.h" // MatchReference #include "../image_analysis/scale_merge/ScaleOnTheFly.h" #include "../image_analysis/scale_merge/StillsPartialityRefine.h" #include "../image_analysis/scale_merge/SublatticeRelabel.h" #include "../image_analysis/scale_merge/RotationScaleMerge.h" #include "../image_analysis/scale_merge/ResolutionCutoff.h" #include "../image_analysis/scale_merge/AnisotropyAnalysis.h" #include "../image_analysis/scale_merge/FrenchWilson.h" #include "../image_analysis/scale_merge/TwinningAnalysis.h" #include "../image_analysis/scale_merge/TranslationalNCS.h" #include "../image_analysis/scale_merge/SearchSpaceGroup.h" #include "Rugnux.h" #include "RugnuxDefaults.h" #include "../image_analysis/structure_refinement/ModelValidation.h" #include "../writer/WriteModel.h" #include "../writer/RugnuxModelFile.h" #include "ResultReport.h" #include "RugnuxOutputFiles.h" #include "RugnuxConsole.h" #ifdef __APPLE__ #include "../image_analysis/bragg_integration/BraggIntegrationEngineMetal.h" #endif // Default rot3d per-frame scale-G smoothing range (XDS DELPHI-like), in degrees of rotation. constexpr double SMOOTH_G_DEFAULT_DEG = 5.0; // Default rot3d per-batch relative-B batch width (bare --relative-b), in degrees of rotation. constexpr double RELATIVE_B_DEFAULT_DEG = 10.0; // What the run does (--mode). Each of these was once a flag of its own; one option keeps them // mutually exclusive by construction. enum class RugnuxMode { MX, Azint, Scale, Calibration }; // Azimuthal sectors used by --mode calibration --calibration rings when --azim-phi-bins was not given. // The default of 1 is a plain radial profile, in which a ring has been averaged over every direction so // nothing is left to say where its centre is (RingsFromAzimuthalProfile refuses below 4). Sweeping the // count on a LaB6 exposure and on a crystal's ice rings, 32 and above agree to about 0.1 px and the // residual is flat, while 8 sectors is visibly coarser (0.3 px away, twice the residual). Going higher // buys nothing and runs into the 65534-bin limit on q x azimuth, so 32 is the default. constexpr int CALIBRATION_AZIM_BINS_DEFAULT = 32; void print_usage() { std::cout << "Usage rugnux {} {} []" << std::endl; std::cout << " Inputs are files, never a directory, read in the order given (a shell glob gives them sorted): " "all the files of one stills dataset - rugnux run*.h5 (SACLA), *.cxi, *.xtc (LCLS) - or several " "Jungfraujoch/NXmx masters of stills, read as one run: their images one after another, merged " "together, the indexing rate reported per master as well; these must share the detector, " "wavelength, distance and beam centre. Several inputs must be one format; rotation sweeps (name " "one frame of a CBF/marCCD/SMV sweep), --mode other than mx and following a broker take one" << std::endl; std::cout << " With a broker URL (http://host:port/) and no on disk yet, follows the broker's current " "collection while it is written: must be the path its master file will have, the data " "files are read next to it as the broker reports them closed, and the run ends with the images the " "collection delivered. Rotation data, --mode mx only. The collection's token is read from the " "environment variable JUNGFRAUJOCH_HTTP_TOKEN and sent only to the broker. With on disk, " "the URL is ignored" << std::endl; std::cout << " --simulate-live Read the stored as the collection it was while being written, " "the way a broker URL follows one: image i becomes readable (i+1)* ms after the run starts (0: all " "at once). The run then takes the live path - the first passes decide their near ties on the images in " "so far, re-checked once the last one is in, and the run is made again where one ends elsewhere - " "where on a stored sweep they decide on all of its images at once. Same result; for testing the " "live workflow without a broker. --mode mx only" << std::endl; std::cout << " --fast-live With --simulate-live or a broker URL: run as the broker's rugnux live " "mode does - every decision stands where it was first taken, nothing is checked or run again once the " "sweep is in (near-tie re-checks, geometry and rotation-scale walks, metric-symmetry arms, supercell and " "quality guards, doubled cell), the integration radius is fixed, and SIGINT cuts the run to what it has " "established. A difficult crystal can come out worse" << std::endl; std::cout << "Options:" << std::endl; std::cout << " -o, --output-prefix Output file prefix (default: output). mx and scale runs always write " "_report.txt, the results report; every run writes _log.txt, the full timestamped log " "(debug level); an empty prefix writes nothing at all" << std::endl; std::cout << " -N, --threads Number of threads (default: every CPU the run may use - its affinity and cgroup quota - of which the " "per-image loop uses at most 16 per GPU; an explicit value is used as given)" << std::endl; std::cout << " --no-gpu Run on the CPU only, as on a machine without a GPU: CUDA is not " "touched and every GPU step takes its CPU path (FFT indexes with FFTW, FFBIDX with its CPU implementation)" << std::endl; std::cout << " -s, --start-image Start image number (default: 0)" << std::endl; std::cout << " -e, --end-image End image number, exclusive: -s 0 -e 100 processes images 0-99 (default: all)" << std::endl; std::cout << " -t, --stride Image stride (default: 1)" << std::endl; std::cout << " -v, --verbose Show the whole log on the terminal in place of the steps " "(the log file is written either way)" << std::endl; std::cout << std::endl; std::cout << " Mode" << std::endl; std::cout << " --mode What this run does (default: mx)" << std::endl; std::cout << " mx Full analysis - spot finding, indexing, integration and merging" << std::endl; std::cout << " azint Only azimuthal integration (no spot finding/indexing); writes _process.h5" << std::endl; std::cout << " scale Only re-scale/merge the already-integrated reflections in (no re-integration)" << std::endl; std::cout << " calibration Determine the detector geometry from powder rings; writes .poni for pyFAI and .json, whose dataset_settings member is a jfjoch_broker dataset_settings body as it stands" << std::endl; std::cout << std::endl; std::cout << " Calibration (--mode calibration)" << std::endl; std::cout << " --calibrant Powder standard: lab6|agbh|ceo2|si|ice (default: lab6, case-insensitive). Overridden by -C: a unit cell given there IS the standard, with its rings enumerated from the cell and its absences from -S where that is given, or taken as primitive where it is not. -S supplies SYMMETRY absences only: a standard whose extinctions come from its atomic basis rather than from a symmetry element - silicon, whose 222 is absent in Fd-3m for that reason - is better named with --calibrant" << std::endl; std::cout << " --calibration How the rings are measured: rings|spots (default: rings). rings sums the (q x azimuth) azimuthal profile over every processed image and fits the ring arcs in it; spots pools the found spots and fits those. -s/-e/--stride select the images; rings defaults --azim-phi-bins to 32" << std::endl; std::cout << " --no-refine-tilt Do not refine the detector tilt: hold rot1/rot2 at the header value and fit only the beam centre and the distance. For a calibration handed to a program that cannot express a tilted detector, e.g. XDS. On stills it holds the tilt in the geometry refinement too" << std::endl; std::cout << std::endl; std::cout << " Detector geometry" << std::endl; std::cout << " (Serial XFEL stills are read without a geometry file: SACLA Cheetah files are placed by their own /metadata; Cheetah CXI files of a single-panel detector (square frame) are one flat panel with the pixel size, photon energy and camera length they state; LCLS-I XTC streams of a Rayonix take the photon energy and pixel size from the XTC and need --detector-distance, or -C to have it searched. The beam centre is measured, the run is read at the mean photon energy, and frames that state no gain are counted in photons where their background resolves single photons, and otherwise analysed in the units they are stored in at the gain their background shows - see --gain)" << std::endl; std::cout << " --crystfel-geometry Read the HDF5/CXI files with their panels placed by this CrystFEL geometry file: the module geometry (panel ranges, fast/slow directions, corners, pixel size, coffset, the frames' data path), if stated the camera length, and the masks (mask arrays, bad regions, mask_edge_pixels, no_index) are taken from it; photon energy, camera length and - where the geometry names no mask array - masks otherwise come from the data files (Cheetah CXI, SACLA) or the command line, gain and saturation are measured. Tilted panels, dimN layouts, per-panel data and detector shifts are refused" << std::endl; std::cout << std::endl; std::cout << " Detector mask" << std::endl; std::cout << " --mask-good [:][@] HDF5/CXI stills: a mask array in the frames' own layout (as stored, before the panels are placed) - in , or without it in each data file (a per-frame mask stored beside the frames); a pixel is usable only where it is non-zero, or with only where it has all of them. May be given more than once" << std::endl; std::cout << " --mask-bad [:][@] As --mask-good, but a pixel is unusable where the array is non-zero, or with where it has any of them (e.g. @0xfeff)" << std::endl; std::cout << " --detect-beam-stop[=N|off] Find the beam stop and its holder in a projection of N images and add them to the pixel mask (bit 9), so nothing shadowed by them is integrated. ON by default (60 images); =off disables. Reflections behind the stop are attenuated but not flagged, so they are integrated low with a plausible sigma and no existing rejection catches them" << std::endl; std::cout << " --beam-center-check[=off] Measure the beam centre from the isotropy of the scattered background on EVERY run, report how far the file's value is from it, and index a second first pass with it to see whether the two centres give the same lattice. The fit reads the projection --detect-beam-stop already builds, so it costs no extra frames. On a run that indexes, the measured centre is adopted where the file's centre indexes nothing and the measured one indexes a majority, and where the two centres return cells related by an integer volume factor, in either direction, and the measured centre's lattice carries materially more of the pooled spots against its own chance level - the axis harmonic a centre error along the spindle produces, which nothing downstream repairs. Where the indexing cannot tell the two centres apart - the same cell with a different metric symmetry, or the same lattice found at both only by reading less of each frame - the first pass is run at both and the run keeps the one that merges better (on the same lattice: the one that measures more reflections at I/sigma >= 2). ON by default; =off disables On stills, where there is no first pass, the file's centre, the measured one and the point-symmetry capture's are each indexed on a sample of frames and the run takes the one that puts materially more spots on the lattice of their frame; with a reference cell (-C) the detector distance is searched too, over 30-3000 mm at every centre, the file's value being one hypothesis, before the stills geometry refinement" << std::endl; std::cout << std::endl; std::cout << " Spot finding" << std::endl; std::cout << " --spot-sigma Noise sigma level for spot finding (default: 4.0)" << std::endl; std::cout << " --spot-threshold Photon count threshold for spot finding (default: 10)" << std::endl; std::cout << " --min-pix-per-spot Minimum connected strong pixels per spot. If omitted, min-pix is chosen PER IMAGE (stills indexing): the frame is scored at min-pix 2/3/1 (one lattice search per frame, refined on each list) and the one maximising indexed count x indexed fraction is kept. Give an explicit value to force a fixed min-pix instead." << std::endl; std::cout << " --adaptive-spots Self-calibrating detection (DEFAULT): the strong-pixel threshold comes from each image's own per-resolution-ring noise instead of the fixed --spot-threshold, so one setting adapts across datasets (no per-dataset --spot-threshold/--spot-sigma tuning)." << std::endl; std::cout << " --no-adaptive-spots Turn adaptive detection off and use the fixed --spot-threshold / --spot-sigma finder instead" << std::endl; std::cout << " --spot-false-pixels Adaptive detection operating point: expected noise pixels tolerated per frame (default: 100; implies --adaptive-spots)" << std::endl; std::cout << " --spot-high-resolution High resolution limit for spot finding. If omitted (or 0), spot finding is not clipped in resolution and extends as far as the detector reaches" << std::endl; std::cout << " --spot-low-resolution Low resolution limit for spot finding, in A (default: 50; lower it, e.g. 24, to exclude the direct-beam halo on weakly-diffracting serial data; 0 removes the limit)" << std::endl; std::cout << " --max-spots Max spot count per image, the strongest ones, handed to indexing. Default: measured from the data on rotation (as deep into each image's spot list as its spots still lie on the lattice), capped at 1000" << std::endl; std::cout << " --detect-ice-rings[=on|off] Flag ice-ring spots (de-prioritised in indexing) and exclude ice-ring reflections from scaling. Default: the master file's setting, or - where the file says nothing - on for rotation and off for stills" << std::endl; std::cout << std::endl; std::cout << " Indexing" << std::endl; std::cout << " (A dataset with a rotation goniometer axis is processed as rotation data by default; use --force-still to override)" << std::endl; std::cout << " --force-still Process a rotation (goniometer) dataset as independent stills (still indexing + per-image ScaleOnTheFly) instead of rotation" << std::endl; std::cout << " -R, --two-pass-rotation[=num] Two-pass offline rotation indexing (default for goniometer data; optional first-pass image count, default: 100)" << std::endl; std::cout << " --single-pass-rotation[=num] Use online-like single-pass rotation indexing (optional: min angular range deg)" << std::endl; std::cout << " --min-indexed-spots Spots a frame must have on the lattice before it counts as indexed (default: 6, minimum 4). It sets the reported indexing rate and the count the rotation first pass ranks candidate lattices by; whether the run has a lattice at all is decided on the pooled spots instead, and integration is not gated by it" << std::endl; std::cout << " --force-rotation-lattice Force rotation indexer with external lattice (in Angstrom) : \"a0x,a0y,a0z,a1x,a1y,a1z,a2x,a2y,a2z\" (9 floats, skips first pass)" << std::endl; std::cout << " --rotation-scale Goniometer rotation scale: the stage turned k times the angle stored in the file (the commanded one). Applied to both passes; overrides the fitted correction" << std::endl; std::cout << " --rotation-no-postrefine Disable the (default-on) two-pass rotation post-refine (post-refine detector distance/beam + cell/axis, then re-integrate; only the refined pass is written, as the canonical _* output)" << std::endl; std::cout << " --prepass-fraction Integrate the first fraction f of the sweep, but at least 120 deg of it, in the two-pass rotation geometry pre-pass (0.05-1, default 0.5; 1 = the whole sweep); pass 2 reads every frame. The pre-scan and the first-pass indexing sample the same part of the sweep" << std::endl; std::cout << " -X, --indexing-algorithm Indexing algorithm (Auto|FFT|cuFFT|FFTW|FFBIDX|None, default Auto). FFT is " "the de-novo FFT search, on the GPU (cuFFT) where there is one and --no-gpu is not given, otherwise on the " "CPU (FFTW); cuFFT and FFTW name one implementation each, and cuFFT needs a GPU. FFBIDX is the known-cell " "fast-feedback indexer for stills, likewise on the GPU where there is one and otherwise on the CPU; " "Auto picks it for stills whenever a cell is known" << std::endl; std::cout << " -S, --space-group Space group number (96) or symbol (P43212) - for indexing and scaling" << std::endl; std::cout << " -C, --unit-cell Fix reference unit cell: \"a,b,c,alpha,beta,gamma\"" << std::endl; std::cout << " --fft-min-unit-cell Shortest cell axis the FFT search accepts, in A (default: 10). A candidate with a shorter axis is discarded, but a de-novo rotation run also tries a second first-pass hypothesis with the floor at 5 A, so a small-molecule cell is indexed without this flag; -C lowers it on its own to cover the cell given" << std::endl; std::cout << " -r, --refine Geometry refinement algorithm (none|orientation|beam_and_lattice|flex); flex tries all three per image and keeps whichever indexes the most spots (alias: multi)" << std::endl; std::cout << " --refine-geometry[=N|off] Stills: extra first pass that bundle-adjusts the shared beam/distance/cell from N strongly-indexed frames (default: 200) then re-indexes (lifts weak-stills indexing). Default ON for stills when a reference cell is given (-C / reference MTZ); =off disables" << std::endl; std::cout << " --refine-modules[=on|off|diagnose] Stills, in that bundle adjustment: a detector of several modules (the input's own - NXdetector_module groups, SACLA sensors, geometry-file panels - or --module-geometry) has each placed as a rigid block - shift and in-plane turn - where that predicts the spots better on frames held out of the fit (default: on). Used from there on for the panels of a foreign detector (SACLA sensors, geometry-file panels); for a JUNGFRAU measured and reported only. The placement is written to _modules.h5" << std::endl; std::cout << " --module-geometry Where the detector's modules are: an NXmx file with one NXdetector_module group per module (module_0, module_1, ...; what a run writes as _modules.h5), or a CrystFEL geometry file whose panels are laid on the image (fs = column, ss = row). Replaces the input's own modules" << std::endl; std::cout << " --index-ice-rings[=on|off] Index on the spots flagged as sitting on an ice ring too, instead of setting them aside (default: off; no effect without --detect-ice-rings)" << std::endl; std::cout << std::endl; std::cout << " Scaling and merging (on by default)" << std::endl; std::cout << " --no-merge Skip scaling and merging; write only the per-image _process.h5" << std::endl; std::cout << " --scale-fulls rot3d: after the 3D combine, refit a per-frame scale on the fulls (XDS order, Unity model). Default ON for rot3d" << std::endl; std::cout << " --no-scale-fulls Disable the rot3d scale-fulls refit (it is on by default for rot3d)" << std::endl; std::cout << " --write-process-h5 Also write the (large) _process.h5 when merging (default: only .mtz/.cif when merging)" << std::endl; std::cout << " --finalist-ledger Report the full-resolution evidence for each space group the search considered, not only the one it adopted, and the twin-immune zone evidence of the adopted group's 2-folds (report-only; the decision is unchanged). Both are read on a merge of the observations in P1 that only this option makes, so it costs one extra merge" << std::endl; std::cout << " --developer Write the full _report.txt: the pipeline-internal keys (the anisotropy gate, the space-group candidate and operator tables, the model-fit null, the sweep internals) and the long explanations, which the default report leaves out. Nothing is computed differently - the same report, rendered in full" << std::endl; std::cout << " --smooth-g[=deg] rot3d: smooth per-frame scale G over a deg-degree rotation range (XDS DELPHI-like) before the combine (default: 5 for rot3d; 0 = off)" << std::endl; std::cout << " --relative-b[=deg] rot3d: fit a per-batch relative-B (beyond the single decay slope) over deg-degree batches; cross-validated (default: 10 deg when bare; off otherwise)" << std::endl; std::cout << " --no-scaling-corrections rot3d: disable the (default-on) decay + absorption + modulation correction surfaces fitted on the fulls after scale-fulls" << std::endl; std::cout << " --no-expected-variance-merge stills: disable the default expected-variance merge weighting (which rebuilds each weak observation's signal variance at the reflection mean to de-bias the inverse-variance merge); restores observed-sigma weighting" << std::endl; std::cout << " -A, --anomalous Anomalous mode (don't merge Friedel pairs)" << std::endl; std::cout << " --scaling-high-resolution High resolution limit for scaling/merging (manual override; default: no limit)" << std::endl; std::cout << " --scaling-low-resolution Low resolution limit for scaling/merging, in A (default: 50, also XDS's own default; 0 = no limit). The beam stop suppresses air scatter well beyond the shadow the beam-stop mask covers, so reflections coarser than this are measured on a depressed background and mostly integrate negative; masking does not reach them" << std::endl; std::cout << " --resolution-cutoff Automatic high-resolution cutoff for the written reflections + reported shells: cc-logistic|off (default: cc-logistic; ignored when --scaling-high-resolution is set)" << std::endl; std::cout << " --resolution-cc-target CC1/2 target defining the cc-logistic fall-off (default: 0.30)" << std::endl; std::cout << " --resolution-shells Number of resolution shells in the reported statistics table (default: 9, as XDS)" << std::endl; std::cout << " --report-resolution [,] Also report the merging statistics over this resolution range (A, either order; dmax defaults to the run's own low-resolution limit) as a second table beside the run's own - the REFRES_* keys of the report - binned from the same merged reflections, so a run can be read against another program's table at that program's range. Report-only: the cut, the scaling, the error model and every decision stay the run's own, and shells finer than the run's own limit are empty rather than filled from data the run did not keep" << std::endl; std::cout << " --ice-min-score Ice-presence gate: measured ice score (1 = no ice) a run must reach before ANY ice handling is applied - the flagging and the exclusion from scaling (default: 1.5). The eleven hexagonal bands cover 16-26% of the unique reflections whether or not the crystal has ice, so handling ice on a clean crystal is a pure loss. 0 = no gate (always handle ice)" << std::endl; std::cout << " --ice-min-spot-ratio Second ice-presence channel: found spots on the hexagonal rings over the same q width of ice-free flanks beside them (1 = spots spread evenly). Ice in large crystallites diffracts as discrete spots and leaves the radial profile flat, so --ice-min-score alone is blind to it. Default 2.0; 0 disables this channel" << std::endl; std::cout << " --min-partiality Minimum partiality to accept reflection (default: 0.02)" << std::endl; std::cout << " --capture-uncertainty Systematic sigma num*(1-captured fraction)*: on rot3d fulls, the share of the rocking curve not captured; on stills, the share of each partial its partiality does not see (default: 1.0 for rot3d, 0 otherwise)" << std::endl; std::cout << " --min-captured-fraction rot3d: drop a combined full whose rocking curve was captured below this fraction (edge-of-sweep truncated fulls) (default: 0.7 for rotation, 0 otherwise; 0 = off)" << std::endl; std::cout << " --mosaicity Diagnostic: fix the scaling mosaicity (deg) instead of the per-image seed" << std::endl; std::cout << " --prediction-mosaicity Diagnostic: fix the rocking width the PREDICTION window opens to (deg), leaving the partiality on the per-image sigma_M. Prediction and partiality are one number by default, so a sigma_M that moves takes the integrated reflection population with it" << std::endl; std::cout << " --reject-outliers Per-observation merge outlier rejection, N sigma from the per-reflection median, the systematic part of the band multiplicative (default: 6 for rot3d, XDS/DIALS-style; 0 = off)" << std::endl; std::cout << " --search-min-zeta De-novo space-group search only: also search a merge of just the observations whose Lorentz geometry |zeta| reaches this, and report both answers (default: 0.85 for rotation, 0 = single search). Reflections crossing the Ewald sphere near-tangentially are measured worst and can make a real symmetry operator look like a twin law; where the two searches disagree, the merge of ALL the observations decides" << std::endl; std::cout << " --min-image-cc Per-image CC limit in percent (default: no limit)" << std::endl; std::cout << " --scaling-iterations Cap on the per-frame scaling iterations; the loop stops earlier once the scales settle (default: 100)" << std::endl; std::cout << " -z, --reference Reference data of the same crystal form: an MTZ, or a PDB structure-factor mmCIF (e.g. 1abc-sf.cif, first merged block with a usable column; R-free set from _refln.status f/o, else pdbx_r_free_flag). Gzipped or not, recognised by content. --reference-mtz is the same option" << std::endl; std::cout << " --reference-column