Files
Jungfraujoch/reader/MiniCBF.h
T
leonarski_f 8261b7354e
Build Packages / Create release (push) Successful in 9s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m8s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 11m12s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 16m43s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 20m56s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 22m4s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 26m57s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 28m24s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 27m58s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 29m29s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 18m23s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 19m55s
Build Packages / Generate python client (push) Successful in 32s
Build Packages / Build documentation (push) Successful in 1m32s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 27m20s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 29m27s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 32m41s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 34m46s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 37m25s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 35m56s
Build Packages / Unit tests (push) Successful in 2h8m44s
Build Packages / Validation Battery (push) Successful in 33m35s
v1.0.0-rc.176 (#86)
* jfjoch_broker: Optional live Rugnux analysis of DECTRIS rotation collections inside the broker, with merged data, maps and the placed model written beside the master file, the rotation lattice in the scan result, and a "Rugnux live" tab and dashboard row in the web frontend.
* Rugnux: Faster processing, with molecular replacement and model validation spread over several GPUs and run on the Apple GPU on macOS.
* Rugnux: The polarization stated in miniCBF and d*TREK headers is used, twinned crystals' space groups are decided more reliably, and a run that finds no lattice still reports every image.

Reviewed-on: #86
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-10-19 07:03:28 +02:00

106 lines
5.7 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>
#include "../common/CompressedImage.h"
// 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
// "# Polarization 0.990": the fraction of the beam polarized in the horizontal plane (0.5 is
// unpolarized). Absent where the line is missing or says "None".
std::optional<double> polarization_fraction;
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
// The goniometer rotation axis carried by the base one, stated in the base axis' own frame. A
// head whose inner circle is INCLINED - a fixed-chi stage, a kappa arm - says so only here: its
// "# Chi" and "# Kappa" lines read zero because no such circle is driven. Empty where the table
// states one rotation axis only, which is every Eulerian head.
std::optional<std::array<double, 3>> inner_spindle_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);
// 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).
// The compressed file is read through scratch, which the caller keeps between frames: a fresh
// vector per image is megabytes of allocation and of zeroing nothing ever reads.
// Throws if the image does not fit in capacity pixels.
Header ReadInto(const std::string &path, int32_t *out, size_t capacity, std::vector<uint8_t> &scratch);
// The whole file read into buf, header parsed and checked, without decoding the pixels: the byte-offset
// stream starts at binary_start. For a consumer that decodes it itself - on the device, where there is one.
Header ReadCompressed(const std::string &path, RawByteBuffer &buf, size_t &binary_start);
// 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