With rugnux enabled, a serial (non-rotation) collection in the FPGA workflow is analysed as the DECTRIS frames are: the frame transformation threads compress each converted frame and hand it to StillsGPUAnalysis, whose threads run MXAnalysisWithoutFPGA (GPU spot finding, indexing, integration) and pass the result back to the receiver to be written. Before any frame is analysed, the first 100 are run through rugnux as `rugnux --no-merge` on them (RunStillsPrePass, in memory): its beam centre (the direct beam, from the scattered background and, where frames index, the stills geometry trial or the bundle refinement with a given cell) and its mask (plus the beam-stop shadow and the defective pixels) are what every frame of the collection is then analysed with. Frames arriving meanwhile are held. The start message waits for the pre-pass, so it states that centre and mask; the END message carries the centre as refined_beam_center_x/y. The start message dataset now carries the detector modules, unit cell and space group, which the pre-pass needs. tools/jfjoch_stills_gpu_replay replays a stored serial collection through the same analysis (pre-pass, indexed fraction, sustained rate). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UqXYdJKPUFLoeAchjzv3x4
202 lines
8.2 KiB
C++
202 lines
8.2 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "JFJochStartMessageReader.h"
|
|
|
|
#include <filesystem>
|
|
|
|
#include "spdlog/fmt/fmt.h"
|
|
#include "../common/JFJochException.h"
|
|
|
|
namespace {
|
|
// The data file name the writer gives file `file_number` (HDF5Metadata::DataFileName), without
|
|
// the directory: the data files sit next to the master file.
|
|
std::string DataFileName(const StartMessage &start, uint64_t file_number) {
|
|
const std::string prefix = std::filesystem::path(start.file_prefix).filename().string();
|
|
return fmt::format("{:s}_data_{:06d}.h5", prefix, file_number + 1);
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<JFJochReaderDataset> DatasetFromStartMessage(const StartMessage &start,
|
|
const DiffractionExperiment &defaults) {
|
|
auto dataset = std::make_shared<JFJochReaderDataset>();
|
|
dataset->arm_date = start.arm_date;
|
|
dataset->jfjoch_release = start.jfjoch_release;
|
|
dataset->error_value = start.error_value;
|
|
dataset->file_detect_ice_rings = start.detect_ice_rings;
|
|
|
|
auto &x = dataset->experiment;
|
|
x = defaults;
|
|
// The reader hands every image out as signed 32-bit, as JFJochHDF5Reader does.
|
|
x.BitDepthImage(32);
|
|
x.PixelSigned(true);
|
|
x.FilePrefix(std::filesystem::path(start.file_prefix).filename().string());
|
|
x.BeamX_pxl(start.beam_center_x);
|
|
x.BeamY_pxl(start.beam_center_y);
|
|
x.DetectorDistance_mm(start.detector_distance * 1000.0f);
|
|
x.PoniRot1_rad(start.poni_rot1.value_or(0.0f));
|
|
x.PoniRot2_rad(start.poni_rot2.value_or(0.0f));
|
|
x.PoniRot3_rad(start.poni_rot3.value_or(0.0f));
|
|
x.IncidentEnergy_keV(WVL_1A_IN_KEV / start.incident_wavelength);
|
|
if (start.incident_wavelength_spread)
|
|
x.BandwidthFWHM(start.incident_wavelength_spread.value() / start.incident_wavelength);
|
|
x.DetectIceRings(start.detect_ice_rings.value_or(false));
|
|
|
|
DetectorSetup detector = DetDECTRIS(start.image_size_x, start.image_size_y, start.detector_description, {});
|
|
detector.PixelSize_um(start.pixel_size_x * 1e6f);
|
|
detector.MirrorY(start.mirror_y);
|
|
detector.ImageOrientation(DetectorOrientation(start.detector_orientation_mirror_y,
|
|
start.detector_orientation_quarter_turns));
|
|
detector.SensorThickness_um(start.sensor_thickness * 1e6f);
|
|
detector.SensorMaterial(start.sensor_material);
|
|
detector.SaturationLimit(SaturationLimitFromValue(start.saturation_value));
|
|
detector.BitDepthImage(32);
|
|
detector.MinFrameTime(std::chrono::microseconds(0));
|
|
detector.MinCountTime(std::chrono::microseconds(0));
|
|
detector.ReadOutTime(std::chrono::nanoseconds(0));
|
|
x.Detector(detector);
|
|
// The modules one by one, where the start message states them (a JUNGFRAU's, as imaged).
|
|
if (!start.detector_modules.empty())
|
|
x.Modules(DetectorModules(start.detector_modules));
|
|
x.SetUnitCell(start.unit_cell);
|
|
if (start.space_group_number)
|
|
x.SpaceGroupNumber(static_cast<int64_t>(*start.space_group_number));
|
|
|
|
x.FrameTime(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::duration<float>(start.frame_time)),
|
|
std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::duration<float>(start.count_time)));
|
|
|
|
x.Goniometer(start.goniometer);
|
|
x.GridScan(start.grid_scan);
|
|
x.Smargon(start.smargon_position);
|
|
x.NumTriggers(1);
|
|
x.ImagesPerTrigger(static_cast<int64_t>(start.number_of_images));
|
|
|
|
InstrumentMetadata instrument;
|
|
instrument.SourceName(start.source_name);
|
|
instrument.SourceType(start.source_type);
|
|
instrument.InstrumentName(start.instrument_name);
|
|
x.ImportInstrumentMetadata(instrument);
|
|
x.SampleName(start.sample_name);
|
|
x.SampleTemperature_K(start.sample_temperature_K);
|
|
x.RingCurrent_mA(start.ring_current_mA);
|
|
x.TotalFlux(start.total_flux);
|
|
x.AttenuatorTransmission(start.attenuator_transmission);
|
|
if (start.beam_size_x && start.beam_size_y) {
|
|
x.BeamSizeX_um(start.beam_size_x.value() * 1e6f);
|
|
x.BeamSizeY_um(start.beam_size_y.value() * 1e6f);
|
|
}
|
|
|
|
if (!start.pixel_mask.empty())
|
|
dataset->pixel_mask = std::make_shared<const PixelMask>(start.pixel_mask.begin()->second);
|
|
else
|
|
dataset->pixel_mask = std::make_shared<const PixelMask>(
|
|
std::vector<uint32_t>(start.image_size_x * start.image_size_y));
|
|
return dataset;
|
|
}
|
|
|
|
void JFJochStartMessageReader::Open(const StartMessage &start, const std::string &master_path) {
|
|
if (start.images_per_file <= 0)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"The start message gives no images per file");
|
|
|
|
number_of_images_ = start.number_of_images;
|
|
images_per_file_ = static_cast<uint64_t>(start.images_per_file);
|
|
const uint64_t files = (number_of_images_ + images_per_file_ - 1) / images_per_file_;
|
|
|
|
const std::filesystem::path directory = std::filesystem::path(master_path).parent_path();
|
|
HDF5ImageLocator::Layout layout;
|
|
layout.format = FileWriterFormat::NXmxLegacy;
|
|
layout.images_per_file = images_per_file_;
|
|
for (uint64_t f = 0; f < files; f++)
|
|
layout.legacy_files.push_back({(directory / DataFileName(start, f)).string(), "/entry/data/data"});
|
|
{
|
|
std::unique_lock ul(hdf5_mutex);
|
|
image_source_.Configure(std::move(layout));
|
|
}
|
|
{
|
|
std::unique_lock ul(m_);
|
|
file_images_.assign(files, -1);
|
|
ended_ = false;
|
|
lost_.clear();
|
|
}
|
|
SetStartMessage(DatasetFromStartMessage(start, default_experiment));
|
|
}
|
|
|
|
void JFJochStartMessageReader::FileClosed(uint64_t file_number, uint64_t image_count) {
|
|
{
|
|
std::unique_lock ul(m_);
|
|
if (file_number < file_images_.size())
|
|
file_images_[file_number] = static_cast<int64_t>(image_count);
|
|
}
|
|
cv_.notify_all();
|
|
}
|
|
|
|
void JFJochStartMessageReader::Ended() {
|
|
{
|
|
std::unique_lock ul(m_);
|
|
ended_ = true;
|
|
}
|
|
cv_.notify_all();
|
|
}
|
|
|
|
void JFJochStartMessageReader::Lost(const std::string &why) {
|
|
{
|
|
std::unique_lock ul(m_);
|
|
if (!ended_)
|
|
lost_ = why;
|
|
}
|
|
cv_.notify_all();
|
|
}
|
|
|
|
uint64_t JFJochStartMessageReader::GetNumberOfImages() const {
|
|
return number_of_images_;
|
|
}
|
|
|
|
void JFJochStartMessageReader::Close() {
|
|
std::unique_lock ul(hdf5_mutex);
|
|
image_source_.Clear();
|
|
SetStartMessage({});
|
|
}
|
|
|
|
bool JFJochStartMessageReader::WaitForImage(int64_t image_number) {
|
|
if (image_number < 0 || static_cast<uint64_t>(image_number) >= number_of_images_)
|
|
throw JFJochException(JFJochExceptionCategory::HDF5, "Image out of bounds");
|
|
const uint64_t f = static_cast<uint64_t>(image_number) / images_per_file_;
|
|
std::unique_lock ul(m_);
|
|
cv_.wait(ul, [&] { return file_images_[f] >= 0 || ended_ || !lost_.empty(); });
|
|
if (file_images_[f] < 0) {
|
|
if (!lost_.empty())
|
|
throw LiveCollectionLost(lost_);
|
|
return false; // the collection ended without this file
|
|
}
|
|
return static_cast<int64_t>(static_cast<uint64_t>(image_number) % images_per_file_) < file_images_[f];
|
|
}
|
|
|
|
bool JFJochStartMessageReader::ReadRawImage(int64_t image_number, JFJochReaderRawImage &ret) {
|
|
if (!WaitForImage(image_number))
|
|
return false;
|
|
// As JFJochHDF5Reader: only the chunk lookup under the HDF5 lock, the read after it.
|
|
std::optional<HDF5ImageSource::DirectChunk> chunk;
|
|
{
|
|
std::unique_lock ul(hdf5_mutex);
|
|
const auto loc = image_source_.Resolve(image_number);
|
|
chunk = image_source_.PrepareDirectRead(loc);
|
|
if (!chunk) {
|
|
ret.image = image_source_.ReadImageAt(ret.image_buffer, loc);
|
|
return true;
|
|
}
|
|
}
|
|
ret.image = HDF5ImageSource::ReadDirect(ret.image_buffer, *chunk);
|
|
return true;
|
|
}
|
|
|
|
bool JFJochStartMessageReader::LoadImage_i(std::shared_ptr<JFJochReaderDataset> &dataset, DataMessage &message,
|
|
std::vector<uint8_t> &buffer, int64_t image_number, bool) {
|
|
if (!dataset || !WaitForImage(image_number))
|
|
return false;
|
|
std::unique_lock ul(hdf5_mutex);
|
|
message.image = image_source_.ReadImageAt(buffer, image_source_.Resolve(image_number));
|
|
message.number = image_number;
|
|
return true;
|
|
}
|