Files
Jungfraujoch/reader/SMV.h
T
leonarski_fandClaude Opus 5.5 ac7c6af5a8 Read Rigaku d*TREK SMV images; add two home-source sets to the open battery
The SMV reader only knew the ADSC vocabulary, so a Rigaku Saturn frame was
recognised as SMV and then refused for want of PIXEL_SIZE. A header naming
DETECTOR_NAMES is now read as d*TREK:

- pixel size and the point of normal incidence from <det>SPATIAL_DISTORTION_INFO;
- image directions from <det>DETECTOR_VECTORS combined by
  <det>SPATIAL_DISTORTION_VECTORS, matched to a DetectorOrientation (a Saturn
  944+ image is mirrored and turned a quarter, so the hand is preserved);
- distance and the detector circles (2theta etc.) from <det>GONIO_*, composed
  into PONI angles; spindle from ROTATION_VECTOR, angles from ROTATION;
- wavelength from SCAN_WAVELENGTH, or the second number of SOURCE_WAVELENGTH
  (the first is a count - the old NumAny fallback would have read 1.0 A);
- pixels above 32767 decoded as (v - 32768) * RAXIS_COMPRESSION_RATIO.

dxtbx's FormatSMVRigakuSaturnNoTS, which claims headers without
DTREK_DATE_TIME, ignores the distortion vectors; on a Saturn 944+ sweep that
puts the spindle 90 degrees off (DIALS: 12% indexed vs 98% with the vectors
applied, at the deposited cell). The arm geometry is confirmed by the data:
the refined direct beam of a 2theta = 10 deg sweep lands at x = 621.1 against
621.6 predicted. The anomalous map peaks at 11.5 sigma on the two K+ and
6-7 sigma on S and P, so the hand is right.

Battery, open arm: 5cc8 (rotating anode, Saturn 944+ SMV, 2theta = 10 deg
sweep) and 9jq9 (Ga K-alpha liquid-metal jet, PILATUS3 1M miniCBF, single
450 deg sweep), both IRRMC. Run 20261001-2132_84228b_dtrek-home-source:
9jq9 passes (P 21 21 21, 1.65 A); 5cc8 merges to 1.53 A at the deposited
cell but is called P 21 21 21 against the deposited P 21 21 2 (screw
evidence on 00l 68-78 nats, tNCS at (1/2,1/2,0.11)) - left failing, not
yet adjudicated. The Saturn gain (~5 ADU/photon) is not in the header and
is not modelled.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 21:44:44 +02:00

92 lines
4.3 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>
// SMV: the format ADSC Quantum detectors wrote, and which Rayonix and several others still write.
// A plain ASCII header of "KEY=value;" lines between braces, then the pixels - no container, no
// compression, no binary header to decode by offset. The header states its own length, so even
// that is not assumed.
//
// {
// HEADER_BYTES= 512; DIM=2; BYTE_ORDER=little_endian; TYPE=unsigned_short;
// SIZE1=3072; SIZE2=3072; PIXEL_SIZE=0.102588; DISTANCE=250.000000;
// OSC_START=325.000000; OSC_RANGE=0.200000; WAVELENGTH=0.999839;
// BEAM_CENTER_X=157.500000; BEAM_CENTER_Y=157.500000;
// }
//
// Note the beam centre is in MILLIMETRES, not pixels, and its convention is the one thing writers
// disagree about - see the comment on beam_x_mm in the struct.
//
// Rigaku's d*TREK writes the same container with a different vocabulary: the detector and the
// goniometer as named circles with their vectors (CCD_GONIO_VALUES, ROTATION_VECTOR, ...), the beam
// position in pixels, and its own overflow encoding for 16-bit pixels. A header naming
// DETECTOR_NAMES is read that way.
namespace smv {
struct Header {
int64_t nx = 0; // SIZE1, the fast dimension
int64_t ny = 0; // SIZE2, the slow dimension
size_t data_offset = 0; // HEADER_BYTES
bool little_endian = true;
int64_t bytes_per_pixel = 2; // TYPE: unsigned_short
double pixel_x_m = 0;
double pixel_y_m = 0;
double distance_m = 0;
// BEAM_CENTER_X/Y, converted to pixels here. Writers disagree about this field more than about
// any other: it is in millimetres, and which of the two is the fast direction is a convention
// rather than a rule. Where a file also carries the ADSC-era synonyms (BEAM_CENTRE_X, or the
// MOSFLM-ordered pair) they are read too. A run whose centre is wrong is recoverable - the
// beam-centre estimator measures it from the data - but a wrong one is not detectable here.
double beam_x_px = 0;
double beam_y_px = 0;
double wavelength_A = 0;
double start_angle_deg = 0; // OSC_START, falling back to PHI
double angle_increment_deg = 0; // OSC_RANGE
double two_theta_deg = 0;
double exposure_s = 0;
int64_t saturation = 0; // CCD_IMAGE_SATURATION: the value a saturated pixel carries
std::string detector; // DETECTOR_SN, where stated
std::string axis_name = "phi";
// d*TREK only (Rigaku CrystalClear): the geometry is stated as vectors rather than the ADSC
// scalars, in the imgCIF laboratory frame - z from the sample to the source, y up. Absent on an
// ADSC header, where two_theta_deg above carries the one detector circle there is.
std::optional<std::array<double, 3>> fast_direction;
std::optional<std::array<double, 3>> slow_direction;
std::optional<std::array<double, 3>> spindle_axis;
struct Circle {
std::array<double, 3> axis;
double angle_deg;
};
std::vector<Circle> detector_circles; // header order: the outermost circle first
// RAXIS_COMPRESSION_RATIO: a stored value above 32767 is (value - 32768) * ratio. 0 = not used.
int64_t overflow_ratio = 0;
std::map<std::string, std::string> raw; // every key, for anything not modelled above
};
// True if the path names something this reader can open: an SMV file, or a directory holding at
// least one. Reads a few hundred bytes at most.
bool CanRead(const std::string &path);
// The files of one sweep, in collection order. path is a directory, or one frame inside it.
std::vector<std::string> CollectSweep(const std::string &path);
// Header only - reads just the leading brace block. Cheap enough to call per frame.
Header ReadHeader(const std::string &path);
// Header + pixels. out is resized to nx * ny.
Header Read(const std::string &path, std::vector<int32_t> &out);
// The same, widening the stored pixels into memory the caller already has. scratch carries the
// raw bytes between frames so a worker walking a sweep allocates once.
Header ReadInto(const std::string &path, int32_t *out, size_t capacity, std::vector<uint8_t> &scratch);
} // namespace smv