Files
leonarski_f a9d5367013
Build Packages / Create release (push) Successful in 26s
Build Packages / Build documentation (push) Successful in 1m43s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m7s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 11m48s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 7m22s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 12m24s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m30s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 25m29s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 29m41s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 21m44s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 24m41s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 26m9s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 26m3s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 28m28s
Build Packages / Generate python client (push) Successful in 27s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 31m16s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 20m44s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 26m27s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 22m52s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 27m44s
Build Packages / Unit tests (push) Successful in 1h46m55s
Build Packages / Validation Battery (push) Successful in 52m50s
v1.0.0-rc.181 (#92)
* Rugnux: Several rotation sweeps of one crystal are merged as one dataset (`rugnux a_master.h5 b_master.h5`), on a common indexing and cell, with a non-isomorphous sweep left out and reported; `--sweeps separate` processes each sweep on its own. Bruker `.sfrm` frames and imgCIF files with a full goniometer axis chain are read.
* Rugnux: More robust rotation processing - a retry with a more sensitive spot finder when no lattice is found, a self-consistent detector distance, long axes and doubled cells tested as hypotheses, a `-C` supercell multiple stepped down, and each ice ring merged, down-weighted or rejected on its own.
* Rugnux: Serial stills are merged with a partiality-aware error model, reported as `ERROR_MODEL_A/B/C` with no ISa or R_merge/R_meas/R_pim; runs without `-S` have an overall intensity scale about 0.86-0.88 times that of earlier versions, with unchanged statistics. A second crystal form is left out of the merge and reported, the indexing ambiguity is resolved more reliably, and `--model` no longer influences processing.
* Rugnux: The reported CC1/2 is the standard unweighted one, which can be lower than before on sweeps with weak frames; ISa comes with a 95% interval (`ISA_LOW`/`ISA_HIGH`).
* Rugnux: Model refinement adds anisotropic/TLS, local NCS and twin refinement where R-free supports them; building waters and relabelling ions is now off by default (`--refine-solvent` turns it on).
* NXmx: A detector of several modules is written as `module_0` ... `module_N` only, so DIALS reads each module as its own panel.
* Other: JPEG previews use a bundled encoder (libjpeg-turbo no longer needed), Rugnux prints how to cite it, and the release archives contain the HTML documentation.

Reviewed-on: #92
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-11-02 12:05:20 +01:00

133 lines
7.0 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <array>
#include <cstdint>
#include <map>
#include <optional>
#include <string>
#include <vector>
#include "../common/Coord.h"
#include "../common/DetectorOrientation.h"
#include "../common/GoniometerAxis.h"
// The parts of a full imgCIF header (the form CBFlib writes, and Bruker's APEX writes when it converts
// .sfrm frames) that say where the detector and the goniometer stand. Unlike a PILATUS miniCBF, which
// states its geometry in "# " comment lines, such a file states it only in CIF tables: an axis table
// (_axis), the setting of every axis at this frame (_diffrn_scan_frame_axis) and how it moves per
// frame (_diffrn_scan_axis), and which axes the image's two indices run along
// (_array_structure_list, _array_structure_list_axis).
//
// Every vector here is in the imgCIF laboratory frame (Z from the sample towards the source, Y
// opposite gravity, X completing a right-handed set) and every length in millimetres.
// Following Hammersley, Bernstein & Westbrook (2006) Int. Tables Cryst. G, 444-458
namespace imgcif {
// One "loop_" of a CIF block, as rows keyed by tag. Tags and values are both read as
// whitespace-separated tokens rather than by line, because a template packs several tags onto one
// line ("_axis.vector[1] _axis.vector[2] _axis.vector[3]") and the rows that follow are laid out to
// match the tags, not the lines.
std::vector<std::map<std::string, std::string>> ParseLoop(const std::string &text, const std::string &tag);
// A tag stated outside a loop ("_diffrn_radiation_wavelength.wavelength 0.71073"), or nothing.
std::optional<std::string> Item(const std::string &text, const std::string &tag);
struct Axis {
std::string type; // "rotation" or "translation"
std::string equipment; // "goniometer", "detector", ...
std::string depends_on; // "." for an axis that hangs off nothing
std::array<double, 3> vector{};
std::array<double, 3> offset{};
double setting = 0; // this frame's angle (deg) or displacement (mm)
double increment = 0; // its change over one frame
};
// One index of the stored image: the axis it runs along, and where pixel 1 and each next one sit on it.
struct ArrayIndex {
std::string axis_id;
int64_t dimension = 0;
double displacement = 0; // of index 1, mm
double increment = 0; // per index, mm
};
struct Description {
std::map<std::string, Axis> axes;
ArrayIndex fast; // _array_structure_list.index 1
ArrayIndex slow; // _array_structure_list.index 2
double wavelength_A = 0;
double overload = 0; // _array_intensities.overload; 0 where not stated
double exposure_s = 0;
std::string detector;
// _diffrn_detector.detector names a CCD: its pixel values are ADU, not counted photons.
bool ccd = false;
// _diffrn_radiation_wavelength.polarizn_source_ratio, (Ip - In) / (Ip + In), and
// polarizn_source_norm, the angle (deg) from the laboratory Y axis to the normal of the plane Ip
// is polarized in. Nothing where the header states no ratio.
std::optional<double> polarization_ratio;
double polarization_norm_deg = 0;
};
// The polarization as DiffractionExperiment::PolarizationFactor takes it - the degree, positive for a
// beam polarized in the horizontal (laboratory X-Z) plane - or nothing where the header states none.
// A plane at an angle psi from the horizontal enters as its projection, the degree times cos(2 psi),
// as the correction knows only those two planes.
std::optional<float> PolarizationFactor(const Description &d);
// The description, or nothing when the header is not a full imgCIF one - no axis table, or no
// statement of where the axes stand. Throws on a table that is there but cannot be read.
std::optional<Description> Parse(const std::string &text);
struct Geometry {
std::array<double, 3> origin{}; // centre of stored pixel (0, 0)
std::array<double, 3> fast{}; // one stored column further, minus origin
std::array<double, 3> slow{}; // one stored row further, minus origin
std::string scan_axis; // the goniometer axis that turns during the frame
std::array<double, 3> scan_direction{}; // its laboratory direction at this frame's settings
double scan_start_deg = 0;
double scan_increment_deg = 0;
};
// Where the image and the rotation axis are at this frame, through the whole axis chain. Throws, naming
// what it found, on anything this cannot describe as one rotation about one axis: two axes moving at
// once, a detector or translation axis moving, or a chain that does not close.
Geometry Evaluate(const Description &d);
// An imgCIF laboratory direction in the internal frame. imgCIF puts Z from the sample towards the
// source and Y opposite gravity, while the internal frame has z along the beam and y along increasing
// row, so the two differ by a half turn about x - a rotation and not a mirror, so an axis carried
// through it turns the same way by the same angle.
Coord ToInternal(const std::array<double, 3> &v);
// The goniometer axis as GoniometerAxis takes it, for an imgCIF rotation axis. GoniometerAxis turns
// the crystal by minus the angle about its axis (IndexAndRefine, rot_to_image), the sense an NXmx
// master's vector carries, so a right-handed imgCIF rotation about v is the internal -v.
Coord GoniometerAxisOf(const std::array<double, 3> &v);
// The sweep as GoniometerAxis takes it, turning the scanned axis v from start_deg by increment_deg per
// frame. A sweep that turns backwards (Bruker frames state a negative increment) is the same rotation
// about -v by the opposite angles, and is stated that way: the analysis takes the oscillation width of a
// frame to be positive, as every other format states it.
GoniometerAxis SweepAxis(const std::string &name, double start_deg, double increment_deg,
const std::array<double, 3> &v);
// The detector plane in the terms the geometry is kept in: the point of normal incidence (PONI) in
// pixels, the distance along the normal, and the rotation that takes the detector frame to the
// laboratory, split into the discrete way the image is stored and the continuous rest. The table
// states the plane through three pixel centres, so this is only a change of description - a 2theta
// arm, a detector's small mounting tilts and a stored image turned or mirrored all come out of the same
// three points. Throws, naming path, on pixels that are not square or a grid that is not rectangular.
struct DetectorPlane {
float pixel_mm = 0;
float distance_mm = 0;
float beam_x_pxl = 0;
float beam_y_pxl = 0;
DetectorOrientation orientation;
float rot1 = 0, rot2 = 0, rot3 = 0; // PONI angles, rad
};
DetectorPlane PlaneOf(const Geometry &g, const std::string &path);
} // namespace imgcif