Files
leonarski_fandClaude Opus 5 2ab8c55dfa rugnux and the viewer read SMV and gzipped miniCBF
Two more of the formats deposited data actually arrives in, found by processing a
corpus of it: every PETRA III EMBL set is .cbf.gz, and NSRRC and the whole ADSC
Quantum era are SMV. Both were previously "no native input".

SMV is an ASCII "KEY=value;" block between braces, then the pixels - no container,
no compression, nothing to decode by offset - so reader/SMV.{h,cpp} and
JFJochSMVReader are a smaller job than the marCCD pair they sit beside, and need no
new dependency at all. Two things the format does not give us, both said out loud
rather than papered over:

* It states no saturation value, so overloads are judged on the 16-bit container
  alone. That can only fail to call a pixel saturated, never condemn a good one,
  but a CCD at the top of its range does saturate, so the reader warns once.
* Its beam centre is in MILLIMETRES and which of X/Y is the fast direction is a
  convention rather than a rule. Measured on one ALS ADSC sweep the file's value is
  TRANSPOSED: as stated it indexes 2/60 frames, and the run's own beam-centre
  measurement (which adopts the right one automatically) indexes 60/60. Swapping it
  here would fit that writer and might break another, so the header is read as the
  format defines it and the measurement stays the arbiter. Revisit with a second
  vendor's SMV in hand.

.cbf.gz needed only Slurp() in MiniCBF.cpp, through which every read already passes:
it sniffs the two-byte gzip magic - not the file name - and takes a zlib path when it
is there, leaving the plain path free of zlib's buffer copy. zlib-ng is already in the
build, so this is a link line, not a dependency. The sweep template grew a suffix,
because ".cbf" and ".cbf.gz" are separate sweeps and std::filesystem cannot split the
double extension on its own.

The viewer's single cbf_reader becomes three, dispatched by CanRead() in the same
order as rugnux. Dispatch is by CONTENT in both: ".img" is used by miniCBF, marCCD
AND SMV depending on the writer, and a PDB detector label has now been wrong about
the format four times, so an extension decides nothing.

Measured, de novo, no flags: 9fcg (1800 gzipped frames) gives P4 and a cell 0.06%
from the deposited one at 1.37 A against a deposited 1.54; 6oel (ADSC SMV) gives
F4132 - 96 operations, the most a protein space group can have - and a cell 0.05%
out, 100% indexed. Tests cover both formats and the transposed-beam-centre case with
fixtures written byte for byte, so they need no external data.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 22:35:05 +02:00

71 lines
3.2 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <cstdint>
#include <map>
#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.
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;
std::string detector; // DETECTOR_SN, where stated
std::string axis_name = "phi";
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