Files
Jungfraujoch/reader/SweepLayout.h
T
leonarski_fandClaude Opus 5 5f78fc156f Place CBF/marCCD/SMV frames on the sweep their own headers state
A series of one file per image was laid out end to end: the rotation start came
from the first file and the step from the difference between the first two, so a
series with frames missing came out compressed - one 179.8 degree deposited sweep
of 1108 files out of 1800 was read as 111 degrees, and every frame past the first
gap was analysed at the wrong spindle angle. Indexing then found a lattice that
took 4% of the validation spots, and two other gapped series aborted outright
with "it is not this crystal's lattice".

Every one of these formats writes each image's own start angle in its own header,
so the sweep is fully recoverable. The new reader/SweepLayout places each frame at
the slot its own angle puts it in and leaves a missing frame as a gap - a slot with
no file, which ReadRawImage reports as nothing to read, which every image loop in
the pipeline already passes over. The goniometer's start + increment * image_number
is then the true angle of every image, and the sweep range, the per-10-degree
delta-CC1/2 batches and the sweep-quality ledger all read the rotation the headers
describe. The rotation step is the smallest move between two frames that really are
adjacent, not the first pair.

The three readers shared this code by duplication; it is now written once. The same
place refuses what is not a sweep rather than averaging it into one: headers that
disagree about the detector distance, the beam centre, the wavelength or the
oscillation width, angles that do not sit on a single step (a folder of screening
shots), or two frames claiming the same angle - each naming the frames. A series
that does not turn at all is left exactly as it was.

A directory holding fewer files than its own numbering spans is also reported, with
both counts: that is the signal that a sweep was not unpacked or copied whole, which
otherwise shows up only as a resolution nobody can explain.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nW6FNRP1bBJJ8pfHiByAT
2026-09-20 18:45:18 +02:00

61 lines
3.0 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <string>
#include <vector>
// Where the frames of a one-file-per-image sweep sit on the spindle.
//
// A deposited series of CBF / marCCD / SMV files is not always complete: frames go missing between
// the beamline and the archive, and the numbers that are left are scattered through the range. Every
// one of these formats writes each image's own start angle in its own header, so the sweep is fully
// recoverable - but only if the frames are placed at the angles they state rather than laid out
// end to end. Reading a gapped series as contiguous compresses the sweep: one deposited 179.8 degree
// series of 1108 files out of 1800 came out as 111 degrees, and every frame past the first gap was
// analysed at the wrong spindle angle.
//
// So the sweep is a grid of SLOTS, one per rotation step over the range the headers span, and a frame
// occupies the slot its own angle puts it in. A slot with no file is a missing image, not a compressed
// out one: the reader hands it back as "nothing to read", which every image loop already handles, and
// the goniometer's start + increment * image_number is then the true angle of every image.
namespace sweep {
// What one frame's own header says about where it sits and how the instrument stood while it was
// taken. The last four are not used to place the frame - they are what the whole series has to agree
// on for it to be one sweep at all.
struct Frame {
std::string path;
double angle_deg = 0;
double increment_deg = 0;
double distance_m = 0;
double beam_x_px = 0;
double beam_y_px = 0;
double wavelength_A = 0;
};
struct Layout {
std::vector<std::string> files; // one entry per slot; empty where the series has no frame
double start_deg = 0; // angle of slot 0
double increment_deg = 0; // one slot, signed with the direction the sweep turns
size_t present = 0; // files that are really there (files.size() - the gaps)
};
// Place the frames on the sweep they describe. frames must be in collection order (the order the
// file names sort in) and must not be empty.
//
// Throws JFJochException, naming the frames, when the series is not one sweep: when the headers
// disagree about the instrument, when two frames claim the same angle, or when the angles do not sit
// on a single rotation step - a folder of screening images taken at scattered angles is not a sweep,
// and silently averaging it into one is how such a set comes out as an indexing failure instead of a
// clear refusal.
//
// A series that does not turn at all - one angle repeated, a grid scan or a set of stills - is left
// exactly as it came, one slot per file, with the header's nominal increment.
//
// logger_name is the reader's own logger, so the gap report names the format the user passed.
Layout Place(const std::vector<Frame> &frames, const std::string &logger_name);
} // namespace sweep