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
61 lines
3.0 KiB
C++
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
|