Files
Jungfraujoch/tools/jfjoch_stills_gpu_replay.cpp
leonarski_f 7f2ebd8209
Build Packages / Build documentation (push) Successful in 46s
Build Packages / Create release (push) Successful in 8s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m25s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 9m46s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 13m25s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 16m41s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 23m46s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m37s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 24m31s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 28m9s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 20m47s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 26m21s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 27m36s
Build Packages / Generate python client (push) Successful in 31s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 29m27s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 31m46s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 25m22s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 30m36s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 31m49s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 23m57s
Build Packages / Unit tests (push) Successful in 1h47m52s
Build Packages / Validation Battery (push) Successful in 44m55s
v1.0.0-rc.184 (#95)
* Rugnux: Rotation processing recovers most of the speed lost since 1.0.0-rc.180; live processing in jfjoch_broker finishes sooner.
* Rugnux: More reliable rotation processing and indexing, including small molecules and supercells.
* Rugnux: More robust serial data and model refinement; molecular replacement handles multi-chain models.
* jfjoch_broker (experimental): Live serial processing keeps the detector geometry fixed for the whole run.
* jfjoch_broker: Pixel sum without an FPGA, a mask bit for inactive pixels, and several stability fixes.
* jfjoch_broker: Rotation indexing and quick integration moved to rugnux live mode; their API settings are deprecated and ignored.
* jfjoch_broker, Rugnux: Azimuthal integration can use logarithmic (log10) Q bins or a given number of bins, set from the API, the web interface and the Rugnux command line.
* jfjoch_viewer: Build and display fixes; the README is rewritten.

Reviewed-on: #95
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-11-10 22:06:51 +01:00

231 lines
12 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
// Replays the frames of a stored serial collection through the broker's GPU analysis of converted
// JUNGFRAU frames (StillsGPUAnalysis): the pre-pass on the first images, then every frame analysed with the
// beam centre and mask it decided. Reports the pre-pass, the indexed fraction and the sustained rate, and
// writes the mask the frames were analysed with (<prefix>_mask.u32, uint32, row by row).
#include <atomic>
#include <chrono>
#include <cstring>
#include <fstream>
#include <iostream>
#include <sstream>
#include <thread>
#include <getopt.h>
#include <memory>
#include <mutex>
#include "../common/ImageBuffer.h"
#include "../common/Logger.h"
#include "../image_analysis/indexing/IndexerThreadPool.h"
#include "../reader/JFJochHDF5Reader.h"
#include "../receiver/StillsGPUAnalysis.h"
#include "../writer/HDF5Objects.h"
void print_usage() {
std::cout << "Usage: jfjoch_stills_gpu_replay [options] <input master>" << std::endl;
std::cout << " -o <prefix> output prefix (default stills_gpu_replay)" << std::endl;
std::cout << " -s <image> first image (default 0)" << std::endl;
std::cout << " -n <images> images to replay (default all)" << std::endl;
std::cout << " -r <Hz> images given per second (default 0 = as fast as they are read)" << std::endl;
std::cout << " -N <num> analysis threads (default 16)" << std::endl;
std::cout << " -b <MiB> image buffer the frames wait in (default 8192)" << std::endl;
std::cout << " -x <px>, -y <px> beam centre, overriding the file" << std::endl;
std::cout << " -C <a,b,c,alpha,beta,gamma> unit cell of the collection" << std::endl;
std::cout << " -F the broker's default fixed-threshold spot finder (default: adaptive, as the receiver)" << std::endl;
std::cout << " -c leave the file's unit cell out of the collection" << std::endl;
std::cout << " -S <group> space group of the collection (the merge's)" << std::endl;
std::cout << " -M no merge (StillsGPUAnalysis merge = false)" << std::endl;
std::cout << " -e <images> the first images given as empty frames (no diffraction)" << std::endl;
}
int main(int argc, char **argv) {
Logger logger("jfjoch_stills_gpu_replay");
RegisterHDF5Filter();
std::string prefix = "stills_gpu_replay";
int64_t first = 0;
std::optional<int64_t> images;
double rate = 0.0;
int nthreads = 16;
size_t buffer_MiB = 8192;
std::optional<float> beam_x, beam_y;
std::optional<UnitCell> cell;
bool no_cell = false;
bool fixed_threshold = false;
std::optional<std::string> space_group;
bool merge = true;
int64_t empty_frames = 0;
int opt;
while ((opt = getopt(argc, argv, "o:s:n:r:N:b:x:y:C:cFS:Me:")) != -1) {
switch (opt) {
case 'o': prefix = optarg; break;
case 's': first = atoll(optarg); break;
case 'n': images = atoll(optarg); break;
case 'r': rate = atof(optarg); break;
case 'N': nthreads = atoi(optarg); break;
case 'b': buffer_MiB = atoll(optarg); break;
case 'x': beam_x = atof(optarg); break;
case 'y': beam_y = atof(optarg); break;
case 'c': no_cell = true; break;
case 'F': fixed_threshold = true; break;
case 'S': space_group = optarg; break;
case 'M': merge = false; break;
case 'e': empty_frames = atoll(optarg); break;
case 'C': {
UnitCell c{};
if (sscanf(optarg, "%f,%f,%f,%f,%f,%f", &c.a, &c.b, &c.c, &c.alpha, &c.beta, &c.gamma) != 6) {
print_usage();
return EXIT_FAILURE;
}
cell = c;
break;
}
default: print_usage(); return EXIT_FAILURE;
}
}
if (optind + 1 != argc) {
print_usage();
return EXIT_FAILURE;
}
JFJochHDF5Reader reader;
reader.ReadFile(argv[optind]);
const auto dataset = reader.GetDataset();
const int64_t n = std::min<int64_t>(images.value_or(reader.GetNumberOfImages() - first),
reader.GetNumberOfImages() - first);
const auto stored = reader.GetStoredPixelFormat();
DiffractionExperiment x(dataset->experiment);
x.ImagesPerTrigger(n).NumTriggers(1);
if (beam_x)
x.BeamX_pxl(*beam_x);
if (beam_y)
x.BeamY_pxl(*beam_y);
if (no_cell) {
// Indexed de novo, as the broker does where no cell is stated.
x.SetUnitCell(std::nullopt);
IndexingSettings indexing = x.GetIndexingSettings();
indexing.Algorithm(IndexingAlgorithmEnum::Auto);
x.ImportIndexingSettings(indexing);
}
if (cell)
x.SetUnitCell(cell);
if (space_group)
x.SetSpaceGroup(*gemmi::find_spacegroup_by_name(*space_group));
const PixelMask &mask = *dataset->pixel_mask;
StartMessage start;
x.FillMessage(start);
start.number_of_images = n;
start.bit_depth_image = stored.bit_depth;
start.pixel_signed = stored.is_signed;
start.pixel_mask["default"] = mask.GetMask(x);
SpotFindingSettings spot_finding = DiffractionExperiment::DefaultDataProcessingSettings();
// As JFJochReceiverFPGA analyses converted frames.
if (!fixed_threshold) {
spot_finding.adaptive_threshold = true;
spot_finding.min_pix_per_spot = std::nullopt;
}
IndexerThreadPool pool(x.GetIndexingSettings());
// The frames wait in an image buffer, as in the receiver; here every slot can hold one.
ImageBuffer buffer(buffer_MiB * 1024 * 1024);
buffer.StartMeasurement(x);
std::atomic<int64_t> analysed = 0, indexed = 0, spots = 0, given = 0;
std::atomic<bool> caught_up = false;
std::atomic<double> processing_s = 0.0;
std::chrono::steady_clock::time_point t_ready;
const auto t0 = std::chrono::steady_clock::now();
{
// As the receiver: half the buffer for the frames waiting, the other half for those sent.
std::mutex run_profile_mutex;
std::unique_ptr<AzimuthalIntegrationProfile> run_profile;
StillsGPUAnalysis gpu(x, mask, start, &pool, nthreads, buffer.GetStatus().total_slots / 2, logger,
[&](const StillsGPUAnalysis::Setup &s) {
t_ready = std::chrono::steady_clock::now();
run_profile = std::make_unique<AzimuthalIntegrationProfile>(*s.mapping);
const auto &m = s.mask.GetMask();
std::ofstream f(prefix + "_mask.u32", std::ios::binary);
f.write(reinterpret_cast<const char *>(m.data()), m.size() * sizeof(uint32_t));
logger.Info("Ready after {:.1f} s: beam centre {:.2f} {:.2f}",
std::chrono::duration<double>(t_ready - t0).count(),
s.experiment.GetBeamX_pxl(), s.experiment.GetBeamY_pxl());
},
[&](DataMessage &m, const AzimuthalIntegrationProfile &profile) {
{
// As the receiver's plots do: the frames' profiles add up over the run.
std::unique_lock ul(run_profile_mutex);
*run_profile += profile;
}
// The analysis has caught up with the frames given.
if (++analysed + nthreads >= given && !caught_up.exchange(true))
logger.Info("Analysis caught up {:.1f} s after it started, {} frames analysed",
std::chrono::duration<double>(std::chrono::steady_clock::now()
- t_ready).count(), analysed.load());
spots += static_cast<int64_t>(m.spots.size());
processing_s += m.processing_time_s.value_or(0.0f);
if (m.indexing_result.value_or(false))
++indexed;
},
[&] { return spot_finding; }, {}, nullptr, merge);
JFJochReaderRawImage raw;
std::vector<uint8_t> empty;
for (int64_t i = 0; i < n; i++) {
if (rate > 0)
std::this_thread::sleep_until(t0 + std::chrono::duration<double>(static_cast<double>(i) / rate));
reader.ReadRawImage(first + i, raw);
if (i < empty_frames) {
// An empty frame of the same size and pixel type: no compression, every pixel zero.
empty.assign(raw.image.GetUncompressedSize(), 0);
raw.image = CompressedImage(empty.data(), empty.size(), raw.image.GetWidth(), raw.image.GetHeight(),
raw.image.GetMode());
}
auto held = std::make_shared<HeldImage>();
// Unlike the receiver, the replay waits for a slot rather than lose the frame.
while ((held->slot = buffer.GetImageSlot()) == nullptr)
std::this_thread::sleep_for(std::chrono::milliseconds(1));
memcpy(held->slot->GetImage(), raw.image.GetCompressed(), raw.image.GetCompressedSize());
held->message.number = i;
held->message.image = CompressedImage(held->slot->GetImage(), raw.image.GetCompressedSize(),
raw.image.GetWidth(), raw.image.GetHeight(), raw.image.GetMode(),
raw.image.GetCompressionAlgorithm());
held->message.image_collection_efficiency = 1.0f;
while (!gpu.Hold(held))
std::this_thread::sleep_for(std::chrono::milliseconds(1));
++given;
}
logger.Info("Gave {} images in {:.1f} s", n,
std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count());
gpu.Finish();
if (const auto c = gpu.GetConsensusUnitCell())
logger.Info("Consensus cell {:.3f} {:.3f} {:.3f} {:.2f} {:.2f} {:.2f}", c->a, c->b, c->c, c->alpha, c->beta,
c->gamma);
if (const auto &m = gpu.GetMerged()) {
const auto &o = m->merge.statistics.overall;
logger.Info("Merge: {} crystals, {} unique to {:.2f} A, CC1/2 {:.4f}, R_split {:.4f}, <I/sigma> {:.2f}, "
"{:.2f} s after the last frame was analysed", m->images, o.unique_reflections, o.d_min,
o.cc_half, o.r_split, o.mean_i_over_sigma, m->seconds);
std::ostringstream os;
os << m->merge.statistics;
logger.Info("Merging statistics:\n{}", os.str());
}
}
const auto t1 = std::chrono::steady_clock::now();
const double after_ready = std::chrono::duration<double>(t1 - t_ready).count();
logger.Info("Analysed {} images, {} indexed ({:.1f}%); {:.1f} s after the pre-pass = {:.1f} Hz; total {:.1f} s",
analysed.load(), indexed.load(), 100.0 * indexed / std::max<int64_t>(1, analysed), after_ready,
analysed / after_ready, std::chrono::duration<double>(t1 - t0).count());
logger.Info("Per image: {:.1f} ms analysis (one thread), {:.1f} spots",
1e3 * processing_s / std::max<int64_t>(1, analysed),
static_cast<double>(spots) / std::max<int64_t>(1, analysed));
return EXIT_SUCCESS;
}