A miniCBF header states three things about how the instrument is put together that the reader was assuming instead: which laboratory direction the image's columns run along, which its rows run along, and which the spindle turns about. Some beamlines append a CBF template block holding the full imgCIF axis table, which says all three outright. Two instruments in the corpus are not what was assumed, in two different ways. One mounts its detector a quarter turn round, so the image's columns run vertically. Another turns its spindle about the VERTICAL, with the image mounted the usual way; its table says so, and its "# Oscillation_axis" line says so a second way, by naming the image direction the spindle runs along rather than a vector. Either error leaves the spindle 90 degrees from the image. That is not a sign, so the run's axis-sign rescue cannot reach it, and no refinement recovers it: all three affected sweeps indexed nothing usable. So the table is read. The element axes give the image orientation, matched against the eight discrete mountings exactly as the NXmx module directions already are - the match itself moves to DetectorOrientation, so both readers share one definition rather than two copies. The goniometer axis with no parent gives the spindle DIRECTION; its sign stays the rescue's business, which is the part a convention can legitimately differ on. The detector axis with no parent gives the 2theta arm, replacing the assumption that the arm shares the spindle's axis - the one header stating both states them with the same vector, so this changes no answer, only what it rests on. imgCIF's frame differs from the internal one by a half turn about x, a rotation and not a mirror, as writer/HDF5NXmx.cpp already records from the other side. Where a header carries no table, a "+SLOW" on the Oscillation_axis line still says the spindle runs along the image's slow direction. That is the only thing one of the three affected sets says about it. The axis NAME on that line stays unusable - the header that carries both says "X.CW" where its own table says Y - but the direction token is not: where both are present they agree, which is what makes reading it evidence rather than a guess. Also: naming a frame with no directory at all now finds its sweep. parent_path() of a bare filename is empty and iterating an empty path finds nothing, so running from inside the data directory reported that no images were found. Measured, with nothing on the command line. The vertical-spindle protein set goes from no usable lattice to 100% indexed, P 6(3) 2 2 with a cell 0.43% from deposited, 87846 reflections at 86.3% completeness and CC(1/2) 0.995. Its companion from the same detector, which has no table and only the +SLOW token, goes from a spurious monoclinic cell at 2.3% completeness and I/sigma 0.21 to the right orthorhombic lattice, 97.7% indexed, 59.7% complete, CC(1/2) 0.996. The quarter-turned set's three sweeps, at three arm positions, now all index without the hand-passed quarter turn they needed and agree on one cell to 0.03 A. Six miniCBF sets that state no table and no +SLOW - including one whose Oscillation_axis line names an axis in a third dialect - are byte-identical in .hkl, .mtz, .cif and the image statistics, as are two NXmx sets, which is the shared orientation matcher moving nothing on that path either. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3yNBXk4wKdMZy1ak2NY7f
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
|