Files
Jungfraujoch/receiver/JFJochReceiverService.cpp
T
leonarski_f 749db470ca
Build Packages / build:rpm (rocky9) (push) Successful in 19m56s
Build Packages / Unit tests (push) Skipped
Build Packages / build:windows:nocuda (push) Successful in 16m57s
Build Packages / build:windows:cuda (push) Successful in 19m18s
Build Packages / build:viewer-tgz:cpu (push) Successful in 14m48s
Build Packages / build:viewer-tgz:cuda (push) Successful in 16m18s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 14m19s
Build Packages / build:rugnux:windows (push) Successful in 10m34s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 8m49s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 20m55s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 17m4s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 20m48s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 19m15s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 24m26s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 20m32s
Build Packages / build:rpm (rocky8) (push) Successful in 23m39s
Build Packages / Generate python client (push) Successful in 46s
Build Packages / Build documentation (push) Successful in 1m45s
Build Packages / Create release (push) Skipped
Build Packages / XDS test (durin plugin) (push) Successful in 11m3s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 11m30s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 20m10s
Build Packages / XDS test (neggia plugin) (push) Successful in 10m17s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 23m12s
Build Packages / DIALS test (push) Successful in 20m12s
v1.0.0-rc.164 (#74)
* rugnux now tells you whether a crystal diffracts anisotropically and how far it reaches in each direction, without a second program: a new `9. DIFFRACTION ANISOTROPY` section in `<prefix>_report.txt` and matching `_reflns.pdbx_aniso_B_tensor_*` / `_reflns.jfjoch_aniso_*` items in the merged mmCIF report the anisotropic deltaB, the diffraction limit along each principal direction, and a `NOT DETECTED` / `DETECTED` / `CANNOT DETERMINE` verdict measured against the data set's own systematic error. It is a description only - no intensity is corrected, no reflection is removed, and the merged data do not depend on direction.
* rugnux can hand its integrated observations to another scaling program: `--export-unmerged` writes `<prefix>_unmerged.mtz`, an unmerged MTZ readable by aimless, pointless, careless and `iotbx.merging_statistics`, in `--mode mx` and `--mode scale` alike. Each rotation reflection's partials are summed into one full; `--export-unmerged-partials` writes one row per image instead. Intensities carry the Lorentz-polarization factor and nothing else, since those programs scale the data themselves. Lattice-centring absences are not written; screw and glide absences are.
* rugnux integrates crystals with broad spots better - where it changes anything, per-shell mean I/sigma improves by up to 31% and R_meas by up to 24% - because on rotation data the integration signal radius is now taken from the crystal's own measured spot width instead of a fixed 4 px. `--adaptive-integration-radius=off` restores the fixed radius and an explicit `--integration-radius` still overrides both. The widened radius applies to the final integration pass only, and a pattern too dense for it is re-integrated at 4 px with a note in the log.
* rugnux discards fewer stills reflections for want of a background ring, improving per-shell R_meas over most of the signal-bearing range: the stills background ring now runs to 14 px instead of 12. The gain reverses in shells below a mean I/sigma of about 4.
* rugnux determines the space group with thresholds that mean the same thing on a weak crystal as on a strong one: symmetry operators are scored on resolution-normalised intensities (E squared) instead of raw merged intensities, and a reflection counts as genuinely present on its counting significance instead of on the merged I/sigma, which saturates at the merge's own ISa. The search resolution cut is no longer able to move the answer, and the twin-law H bound moves from 1.70 to 1.85, which stops one class of correct high-symmetry assignment being refused as twinning.
* rugnux says what the space-group search tested and what it could not: the twin-law disagreement H is printed for every operator together with the adopted point group's H ratio and its bound; alternatives that are not on the reported lattice are named with how their cell differs; and a lattice centring the data could not test - the crystal having been integrated on the primitive sub-cell, so the reflections it extinguishes were never measured - is marked `UNTESTED` and warned about where it is adopted, as coming from the lattice metric rather than from the intensities.
* rugnux `--mode scale` re-merges a `_process.h5` in the right symmetry without being told it: the file now records the space group on every run - a two-pass rotation run wrote none before, so re-merging defaulted to P1 - together with the change of basis under `/entry/MX/reindexMatrix` where the lattice was re-seated, and `--mode scale` also reports the Wilson B-factor estimate instead of `WILSON_B= nan`. A file written before this stops with a message naming the two cells and the override to use, instead of failing inside the merge. A third-party reader of a `_process.h5` must apply `reindexMatrix` where it is present.
* rugnux installs on its own, as a package called `rugnux` - `dnf install rugnux` or `apt install rugnux` - instead of arriving inside `jfjoch-viewer`. It pulls in none of the acquisition stack, so a machine that only processes data no longer has to carry the broker, the detector libraries or Qt to get it. Installing it over a `jfjoch-viewer` from rc.163 or earlier, which still owns `/usr/bin/rugnux`, upgrades cleanly rather than failing on the duplicate file.
* rugnux is also a standalone download, built for arm64 as well as x86_64: `rugnux-<version>-linux-{x86_64|aarch64}-cuda<major>.tgz` and `rugnux-<version>-win64-cuda<major>.zip` on the release page, for machines that are not managed by a package manager. The aarch64 build targets GH200 and DGX Spark, and is untested on hardware.
* Every portable Linux binary is now a single self-contained file: cuFFT is linked statically instead of being shipped beside the executable and found through an rpath, so `rugnux` and `jfjoch_viewer` need nothing but an NVIDIA driver, and only to use the GPU. The `.rpm`/`.deb` continue to take cuFFT from the distribution. The developer utilities `jfjoch_extract_hkl` and `jfjoch_recompress` are no longer packaged anywhere.
* Jungfraujoch needs six fewer shared libraries on the machine - libopenblas and libmetis, and libgfortran, libquadmath, libgomp and libz behind them - because the Ceres LAPACK, METIS and SuiteSparse back-ends are no longer built. Nothing in the code ever selected them, and results are unchanged.
* The PCIe driver DKMS package builds for the kernel it is being installed for instead of the running one, so a module built while a kernel update is being applied loads after the reboot.
* The PCIe driver builds on RHEL 9.5 and later, and on their CentOS Stream, Rocky and AlmaLinux equivalents, where the `vm_flags` kernel interface was backported into the 5.14 kernel.
* A data collection started with `async_start` that fails to start - a writer refusing to overwrite an existing file, for instance - is reported as an error by `/wait_until_running` and `/wait_till_done` instead of as a timeout and a successful collection respectively. The error message is the one the writer gave.
* A calibration that is cancelled or that fails to collect its pedestals is no longer reported as a successful one. The broker goes to `Inactive` with an error message and has to be initialized again, instead of sitting in `Idle` looking ready to measure while holding partial pedestals - data collected in that state was silently mis-converted.
* A failed `/initialize` is reported to `/wait_until_running` and `/wait_till_done` as soon as it happens, instead of when their timeout expires.
* `space_group_number` accepts space groups up to 230 in the API schema, so cubic space groups can be recorded. The broker always accepted them; the generated clients rejected them before the request was sent.
* The results report's `REPORT_VERSION` is 3, two sections having been added. Existing key names and table columns are unchanged.
* The merged statistics table has **9** resolution shells instead of 10, which is what XDS reports. The bins were already XDS's - equal steps in 1/d^2 between the lowest- and the highest-resolution reflection the merge kept - so at the same resolution limits the two tables now have the same shell boundaries and can be read row for row. `--resolution-shells` sets a different count.
* `rugnux --model` now settles the frame the merged reflections are written in, not only the frame the R-factors and the maps are computed in: the `.mtz`/`.cif`/`.hkl` come out in the model's indexing, and where the data were merged in the model's enantiomorph they take the model's hand and space group - which on anomalous data puts I(+) and I(-) the right way round. The indexing choice is logged with the winning R-free and the runner-up, so a decision made within noise is visible.
* `rugnux --model` can resolve the indexing ambiguity of a **serial stills** run, which a model could not do before: structure factors computed from the model become the per-image reference, the same role a reference MTZ plays. It needs the cell and space group up front (`-C` / `-S`). Without one or the other, a merohedral serial run still merges both hands together and says so.
* The rugnux documentation opens with a quick start - the default run, and runs with a reference MTZ, with a model, or with the space group and cell pinned - and explains the indexing ambiguity: what it costs on rotation and on serial data, and which of `-z` / `--model` resolves it in each case. The long reference pages now carry a table of contents.

Reviewed-on: #74
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-08-26 22:47:00 +02:00

346 lines
14 KiB
C++

// SPDX-FileCopyrightText: 2024 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "JFJochReceiverService.h"
#include "JFJochReceiverFPGA.h"
#include "JFJochReceiverLite.h"
#include "../preview/JFJochJPEG.h"
#include "../preview/JFJochTIFF.h"
JFJochReceiverService::JFJochReceiverService(AcquisitionDeviceGroup &in_aq_devices,
Logger &in_logger, ImagePusher &pusher,
size_t send_buffer_size_MiB)
: aq_devices(in_aq_devices),
logger(in_logger),
image_buffer(send_buffer_size_MiB * 1024 * 1024),
image_pusher(pusher),
spot_finding_settings(DiffractionExperiment::DefaultDataProcessingSettings()) {
}
JFJochReceiverService &JFJochReceiverService::NumThreads(int64_t input) {
if (input <= 0)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Thread number must be above zero");
nthreads = input;
return *this;
}
void JFJochReceiverService::FinalizeMeasurementChangeState() {
std::unique_lock ul(state_mutex);
state = ReceiverState::Idle;
measurement_done.notify_all();
}
void JFJochReceiverService::FinalizeMeasurement() {
try {
receiver->StopReceiver();
} catch (...) {
FinalizeMeasurementChangeState();
throw;
}
FinalizeMeasurementChangeState();
}
std::optional<JFJochReceiverStatus> JFJochReceiverService::GetStatus() {
return receiver_status.GetStatus();
}
void JFJochReceiverService::Start(const DiffractionExperiment &experiment,
const PixelMask &pixel_mask,
const JFCalibration *calibration,
std::shared_ptr<ImagePuller> puller) {
std::unique_lock ul_state(state_mutex); // unique lock, as it will destroy and create receiver object
if (state != ReceiverState::Idle)
throw JFJochException(JFJochExceptionCategory::WrongDAQState, "Receiver not idle, cannot start");
try {
auto nthreads_local = nthreads;
if (experiment.IsCPUSummation())
nthreads_local = 4;
// First clean-up old measurement
receiver.reset();
preview_image.Configure(experiment, pixel_mask);
switch (experiment.GetDetectorType()) {
case DetectorType::EIGER:
case DetectorType::JUNGFRAU:
receiver = std::make_unique<JFJochReceiverFPGA>(experiment, pixel_mask,
calibration,
aq_devices, image_pusher,
logger,
nthreads_local,
spot_finding_settings,
receiver_status,
plots,
image_buffer,
zmq_preview_socket.get(),
zmq_metadata_socket.get(),
indexer_thread_pool.get());
break;
case DetectorType::DECTRIS:
if (puller)
image_puller = puller;
else {
image_puller = std::make_shared<ZMQImagePuller>(
experiment.GetDetectorSetup().GetDECTRISStream2Addr());
}
receiver = std::make_unique<JFJochReceiverLite>(experiment,
pixel_mask,
*image_puller,
image_pusher,
logger,
nthreads_local,
spot_finding_settings,
receiver_status,
plots,
image_buffer,
zmq_preview_socket.get(),
zmq_metadata_socket.get(),
indexer_thread_pool.get());
break;
}
measurement = std::async(std::launch::async, &JFJochReceiverService::FinalizeMeasurement, this);
state = ReceiverState::Running;
} catch (const std::exception &e) {
// The receiver never started, so drop the status its base constructor had already reset -
// otherwise /status and /statistics keep reporting a zero-progress run that never happened,
// until the next start overwrites it.
receiver_status.Clear();
receiver_status.SetProgress({});
logger.ErrorException(e);
throw;
}
}
void JFJochReceiverService::Cancel(bool silent) {
std::unique_lock ul(state_mutex);
if (state == ReceiverState::Running)
receiver->Cancel(silent);
}
JFJochReceiverOutput JFJochReceiverService::Stop() {
std::unique_lock ul(state_mutex);
measurement_done.wait(ul, [this] { return (state != ReceiverState::Running); });
if (state != ReceiverState::Idle)
throw JFJochException(JFJochExceptionCategory::WrongReceiverState, "Receiver in weird state");
try {
if (measurement.valid())
measurement.get();
} catch (JFJochException &e) {
logger.ErrorException(e);
throw;
}
if (!receiver) {
logger.Warning("Request to stop while receiver not running");
throw JFJochException(JFJochExceptionCategory::WrongReceiverState, "Receiver idle, cannot stop");
}
return receiver->GetFinalStatistics();
}
void JFJochReceiverService::SetSpotFindingSettings(const SpotFindingSettings &settings) {
try {
std::unique_lock ul(state_mutex);
DiffractionExperiment::CheckDataProcessingSettings(settings);
spot_finding_settings = settings;
if (state != ReceiverState::Idle)
receiver->SetSpotFindingSettings(settings);
} catch (std::exception &e) {
logger.ErrorException(e);
throw;
}
}
MultiLinePlot JFJochReceiverService::GetDataProcessingPlot(const PlotRequest &request) {
return plots.GetPlots(request);
}
void JFJochReceiverService::GetPlotRaw(std::vector<float> &v, PlotType type, const std::string &roi) {
plots.GetPlotRaw(v, type, roi);
}
std::vector<AcquisitionDeviceNetConfig> JFJochReceiverService::GetNetworkConfig() {
return aq_devices.GetNetworkConfig();
}
void JFJochReceiverService::LoadInternalGeneratorImage(const DiffractionExperiment &experiment,
const std::vector<uint16_t> &image,
uint64_t image_number) {
std::vector<uint16_t> raw_geom, eiger_geom;
const uint16_t *frame;
if (image.size() == RAW_MODULE_SIZE * experiment.GetModulesNum()) {
frame = image.data();
} else if (image.size() == experiment.GetPixelsNum()) {
raw_geom.resize(RAW_MODULE_SIZE * experiment.GetModulesNum());
ConvertedToRawGeometry(experiment, raw_geom.data(), image.data());
frame = raw_geom.data();
} else
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Size of input array with raw expected image is wrong");
for (int i = 0; i < experiment.GetDataStreamsNum(); i++) {
uint32_t module0 = experiment.GetFirstModuleOfDataStream(i);
switch (experiment.GetDetectorSetup().GetDetectorType()) {
case DetectorType::EIGER:
eiger_geom.resize(RAW_MODULE_SIZE);
for (int m = 0; m < experiment.GetModulesNum(i); m++) {
RawToEigerInput(eiger_geom.data(), frame + (module0 + m) * RAW_MODULE_SIZE);
aq_devices[i].SetInternalGeneratorFrame(eiger_geom.data(),
m + experiment.GetModulesNum(i) * image_number);
}
break;
case DetectorType::JUNGFRAU:
for (int m = 0; m < experiment.GetModulesNum(i); m++)
aq_devices[i].SetInternalGeneratorFrame(frame + (module0 + m) * RAW_MODULE_SIZE,
m + experiment.GetModulesNum(i) * image_number);
break;
default:
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Detector not supported");
}
}
}
void JFJochReceiverService::GetXFELEventCode(std::vector<uint64_t> &v) const {
plots.GetXFELEventCode(v);
}
void JFJochReceiverService::GetXFELPulseID(std::vector<uint64_t> &v) const {
plots.GetXFELPulseID(v);
}
std::vector<DeviceStatus> JFJochReceiverService::GetDeviceStatus() const {
return aq_devices.GetDeviceStatus();
}
std::optional<float> JFJochReceiverService::GetProgress() const {
return receiver_status.GetProgress();
}
JFJochReceiverService &JFJochReceiverService::PreviewSocket(const std::string &addr, const std::optional<int32_t> &watermark) {
if (!addr.empty()) {
logger.Info("ZeroMQ preview socket available at {}", addr);
zmq_preview_socket = std::make_unique<ZMQPreviewSocket>(addr, watermark);
}
return *this;
}
JFJochReceiverService &JFJochReceiverService::MetadataSocket(const std::string &addr) {
if (!addr.empty()) {
logger.Info("ZeroMQ metadata socket available at {}", addr);
zmq_metadata_socket = std::make_unique<ZMQMetadataSocket>(addr);
}
return *this;
}
std::string JFJochReceiverService::GetPreviewSocketAddress() const {
if (zmq_preview_socket)
return zmq_preview_socket->GetAddress();
return "";
}
std::string JFJochReceiverService::GetMetadataSocketAddress() const {
if (zmq_metadata_socket)
return zmq_metadata_socket->GetAddress();
return "";
}
JFJochReceiverService &JFJochReceiverService::PreviewSocketSettings(const ZMQPreviewSettings &input) {
if (zmq_preview_socket)
zmq_preview_socket->ImportSettings(input);
return *this;
}
JFJochReceiverService &JFJochReceiverService::MetadataSocketSettings(const ZMQMetadataSettings &input) {
if (zmq_metadata_socket)
zmq_metadata_socket->ImportSettings(input);
return *this;
}
ZMQPreviewSettings JFJochReceiverService::GetPreviewSocketSettings() const {
if (zmq_preview_socket)
return zmq_preview_socket->GetSettings();
return {};
}
ZMQMetadataSettings JFJochReceiverService::GetMetadataSocketSettings() const {
if (zmq_metadata_socket)
return zmq_metadata_socket->GetSettings();
return {};
}
void JFJochReceiverService::GetStartMessageFromBuffer(std::vector<uint8_t> &v) {
image_buffer.GetStartMessage(v);
}
bool JFJochReceiverService::GetImageFromBuffer(std::vector<uint8_t> &v, int64_t image_number) {
return image_buffer.GetImage(v, image_number);
}
std::string JFJochReceiverService::GetJPEGFromBuffer(const PreviewImageSettings &settings, int64_t image_number) {
std::vector<uint8_t> cbor_image;
if (!image_buffer.GetImage(cbor_image, image_number))
return {};
return preview_image.GenerateImage(settings, cbor_image);
}
std::string JFJochReceiverService::GetTIFFFromBuffer(int64_t image_number) {
std::vector<uint8_t> cbor_image;
if (!image_buffer.GetImage(cbor_image, image_number))
return {};
return PreviewImage::GenerateTIFF(cbor_image);
}
ImageBufferStatus JFJochReceiverService::GetImageBufferStatus() const {
return image_buffer.GetStatus();
}
void JFJochReceiverService::ClearImageBuffer() {
std::unique_lock ul(state_mutex);
// Clearing image buffer during data collection could be catastrophic, so better protect here, even if redundant
// with JFJochStateMachine
if (state == ReceiverState::Idle)
image_buffer.Finalize(std::chrono::milliseconds(2500));
else
throw JFJochException(JFJochExceptionCategory::WrongDAQState,
"Cannot clear image buffer during data collection");
}
JFJochReceiverService &JFJochReceiverService::Indexing(const IndexingSettings &input) {
std::unique_lock ul(state_mutex);
// Clearing image buffer during data collection could be catastrophic, so better protect here, even if redundant
// with JFJochStateMachine
if (state == ReceiverState::Idle) {
// Release the previous run's receiver first. It holds a raw pointer to the indexer pool and
// its own GPU resources; keeping it alive while we rebuild the pool means a fresh GPU indexer
// has to coexist with a stale receiver (e.g. after a failed acquisition start, where the
// receiver is stopped but not destroyed until the next Start), which can make the GPU indexer
// initialisation fail. Destroying the receiver before the pool also avoids the dangling
// pointer. Safe here: state is Idle, so no measurement is using it.
receiver.reset();
logger.Info("Resetting indexing thread pool");
indexer_thread_pool.reset();
if (input.GetAlgorithm() != IndexingAlgorithmEnum::None) {
logger.Info("Creating indexing thread pool...");
indexer_thread_pool = std::make_unique<IndexerThreadPool>(input);
logger.Info(" ... done");
}
return *this;
} else
throw JFJochException(JFJochExceptionCategory::WrongDAQState,
"Cannot change indexing settings during data collection");
}
ImagePusherStatus JFJochReceiverService::GetImagePusherStatus() const {
return image_pusher.GetStatus();
}