Build Packages / Unit tests (push) Successful in 1h22m15s
Build Packages / build:windows:nocuda (push) Successful in 18m0s
Build Packages / build:windows:cuda (push) Successful in 20m30s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m32s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m39s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 8m55s
Build Packages / build:rugnux:windows (push) Successful in 11m25s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 20m6s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m27s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 20m19s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 15m34s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 20m25s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m36s
Build Packages / build:rpm (rocky8) (push) Successful in 17m43s
Build Packages / build:rpm (rocky9) (push) Successful in 13m34s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 21m28s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 18m19s
Build Packages / DIALS test (push) Successful in 12m36s
Build Packages / XDS test (durin plugin) (push) Successful in 6m56s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 6m48s
Build Packages / XDS test (neggia plugin) (push) Successful in 6m7s
Build Packages / Generate python client (push) Successful in 11s
Build Packages / Build documentation (push) Successful in 36s
Build Packages / Create release (push) Skipped
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 5m11s
* `rugnux --mode calibration` writes `<prefix>.json` beside the `.poni`, whose `dataset_settings` member is a `jfjoch_broker` `dataset_settings` body as it stands. * `rugnux` and `jfjoch_viewer` read PILATUS miniCBF sweeps natively, without conversion. * Masters written by other facilities open, including Eiger 1.x and third-party NXmx variants. * `rugnux` measures the beam centre on every run, and indexes with it when the file's value indexes nothing. * A detector swung out on a 2theta arm is placed where the file says it stands, and the calibration can hold the tilt fixed. * `rugnux` writes the unmerged MTZ by default, and a P1 merge beside it, so a wrong space group can be re-merged without reprocessing. * Significant improvements to symmetry handling in `rugnux`: the lattice, the point group, the setting and the systematic absences. * The `rugnux` report gives the resolution the CC1/2 fit reached, beside the range the reflections were written to. * The `rugnux` report gives the twinning statistics measured before the space group was decided, beside the ones measured after. * The `rugnux` report gives the strong-direction diffraction limit, and warns when CC1/2 is not monotone with resolution. * `rugnux` ranks screw axes on the evidence their absences carry, rather than on how many control reflections a candidate happens to have. * Twinning is no longer reported when the L-test contradicts it. * The `rugnux` report gives the detector tilt, the measured tilt and the direct beam beside the beam centre, and a post-refined beam centre is judged against the run's own measurement rather than the file's. * `--no-refine-tilt` holds the detector tilt at the value in the file, instead of zeroing it, when the calibration starts from the spots. * The `jfjoch_viewer` grid scan view draws the cells in the proportion of the scan steps, so the map has the shape of the scanned area. Reviewed-on: #76 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
95 lines
4.9 KiB
C++
95 lines
4.9 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 <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
// PILATUS miniCBF: an ASCII header, four separator bytes, then one byte-offset compressed image.
|
|
// No CIF parser and no libcbf are needed - every value is on a "# " comment line or a MIME line.
|
|
namespace minicbf {
|
|
|
|
// The four bytes that end the MIME header and begin the binary section.
|
|
inline constexpr unsigned char BINARY_SEPARATOR[4] = {0x0c, 0x1a, 0x04, 0xd5};
|
|
|
|
struct Header {
|
|
std::string detector; // "PILATUS3 6M, S/N 60-0136"
|
|
int64_t nx = 0; // fast dimension (columns)
|
|
int64_t ny = 0; // slow dimension (rows)
|
|
int64_t nelem = 0; // pixel count declared by the MIME header
|
|
double pixel_x_m = 0;
|
|
double pixel_y_m = 0;
|
|
double thickness_m = 0;
|
|
std::string material = "Si"; // NORMALISED: the file says "Silicon", the rest of the code wants "Si"
|
|
double distance_m = 0;
|
|
double beam_x_px = 0;
|
|
double beam_y_px = 0;
|
|
double wavelength_A = 0;
|
|
double start_angle_deg = 0;
|
|
double angle_increment_deg = 0;
|
|
double two_theta_deg = 0;
|
|
// Goniometer head angles and the per-image increment of each, in degrees. A writer spells "this
|
|
// head has no such axis" as -9999, which is a sentinel and not an angle, so an axis the header
|
|
// does not really carry is absent here rather than 9999 degrees away from zero.
|
|
std::optional<double> chi_deg;
|
|
std::optional<double> omega_deg;
|
|
double chi_increment_deg = 0;
|
|
double phi_increment_deg = 0;
|
|
double omega_increment_deg = 0;
|
|
double exposure_s = 0;
|
|
double period_s = 0;
|
|
int64_t count_cutoff = 0; // saturation
|
|
std::string axis_name = "omega";
|
|
bool byte_offset = false; // the only conversion supported
|
|
|
|
// The imgCIF axis table the CBF template block carries after the "# " lines, as the file states
|
|
// it: vectors in the imgCIF laboratory frame (Z from the sample towards the source, Y opposite
|
|
// gravity, X completing a right-handed set). Turning them into any other frame is the caller's
|
|
// business, not this format reader's. Most headers carry no such table and leave all of these
|
|
// empty, which is not the same as their stating the usual mounting.
|
|
std::optional<std::array<double, 3>> fast_direction; // laboratory direction of a +1 column step
|
|
std::optional<std::array<double, 3>> slow_direction; // ... of a +1 row step
|
|
std::optional<std::array<double, 3>> spindle_axis; // the base goniometer rotation axis
|
|
std::optional<std::array<double, 3>> detector_axis; // the detector's own rotation axis: a 2theta arm
|
|
// "# Oscillation_axis X.CW +SLOW". Some writers say which of the two image directions the spindle
|
|
// runs along instead of stating a vector. The axis NAME on that line is a poor discriminator (see
|
|
// ScansPhi, and note that the one header carrying both a name and a table says "X" where its table
|
|
// says Y) but this token is not: where both are present they agree.
|
|
bool spindle_along_slow = false;
|
|
};
|
|
|
|
// Whether the sweep turns PHI rather than the base (omega) axis. What moved is the axis with a
|
|
// non-zero increment - a header routinely carries a "# Phi" angle for a phi that stands still - so
|
|
// the name is consulted only when no increment is stated at all. It is a poor discriminator: some
|
|
// writers put a direction convention there ("X.CW") instead of an axis name.
|
|
bool ScansPhi(const Header &h);
|
|
|
|
// Byte offset of the binary section (just past the separator), or nothing if there is none.
|
|
std::optional<size_t> FindBinarySection(const uint8_t *data, size_t size);
|
|
|
|
// Parse the ASCII header. Pass the bytes BEFORE the separator; headers are not a fixed size (one
|
|
// Diamond I24 set carries 6335 bytes, well past a 4 kB guess), so never parse a fixed prefix.
|
|
Header ParseHeader(const char *data, size_t size);
|
|
|
|
// x-CBF_BYTE_OFFSET -> int32. Deltas against a running value, smallest container first: int8,
|
|
// escaping to int16 via -128, to int32 via -32768, to int64 via INT32_MIN. Little-endian, packed.
|
|
// Throws if the stream ends before n_pixels are produced. out must hold n_pixels.
|
|
void DecodeByteOffset(const uint8_t *data, size_t size, int32_t *out, size_t n_pixels);
|
|
|
|
// Header + pixels of one file, read from disk.
|
|
Header Read(const std::string &path, std::vector<int32_t> &out);
|
|
|
|
// The same, decoding into memory the caller already has (one read of the file, no extra copy).
|
|
// Throws if the image does not fit in capacity pixels.
|
|
Header ReadInto(const std::string &path, int32_t *out, size_t capacity);
|
|
|
|
// Header only - reads just enough of the file to reach the separator. Cheap enough to call per
|
|
// frame for the rotation angles.
|
|
Header ReadHeader(const std::string &path);
|
|
|
|
} // namespace minicbf
|