From 84efb0666cb7ccfdf0ee3e5605de30f3ea07681a Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sat, 29 Aug 2026 21:49:09 +0200 Subject: [PATCH] viewer: open a miniCBF sweep, the same way rugnux already does The native miniCBF reader added for rugnux is a plain JFJochReader, so the viewer only needed to be told which one to open a file with. JFJochImageReadingWorker held a concrete JFJochHDF5Reader; it now holds both readers and a JFJochReader* pointing at whichever the open file needs, chosen by JFJochCBFReader::CanRead. Naming any frame opens the whole sweep - the reader's own template matcher decides which frames belong to it, so a directory holding two sweeps or XDS auxiliary files is not spliced together. The previous file is closed before the new one is opened, so switching format between HDF5 and CBF in one session leaves nothing behind. Everything the viewer draws already goes through JFJochReader and JFJochReaderDataset, so nothing else had to move. The two HDF5-only features stay on the HDF5 reader: calibration images, and the reprocessing snapshots, which are metadata read back over the same images the reader is serving and have nothing to attach to for a directory of CBFs. A raw sweep therefore shows geometry and pictures with an empty run list, which is the state a live HTTP stream is already in. Reprocessing jobs run on a CBF sweep too - JFJochProcessController opens its own reader the same way, and Rugnux has handled a CBF source since the reader landed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Lc5JG6kJqZoCWaoZ43JGTW --- docs/CHANGELOG.md | 1 + docs/JFJOCH_VIEWER.md | 7 +++- viewer/JFJochImageReadingWorker.cpp | 62 ++++++++++++++++++----------- viewer/JFJochImageReadingWorker.h | 10 ++++- viewer/JFJochProcessController.cpp | 14 ++++++- viewer/JFJochProcessController.h | 2 +- viewer/JFJochViewerMenu.cpp | 3 +- 7 files changed, 68 insertions(+), 31 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3a64f02f2..50ecdf0f8 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## 1.0.0 ### 1.0.0-rc.166 +* `jfjoch_viewer` opens PILATUS miniCBF sweeps - naming any frame opens the whole sweep - and can run a processing job on one. * The rotation first pass refines twelve candidate lattices rather than four, so a correct cell that the pre-refinement ranking put fifth is still reached. * A candidate cell whose three rows are coplanar is rejected before refinement, instead of producing a not-a-number Jacobian and several hundred lines of solver output. * When the directions an FFT search shortlists all lie in one plane, one further transform is spent along the plane normal, which is where the missing row must be - so a crystal with a very long axis can index. diff --git a/docs/JFJOCH_VIEWER.md b/docs/JFJOCH_VIEWER.md index 273d2fd42..88afc5fd0 100644 --- a/docs/JFJOCH_VIEWER.md +++ b/docs/JFJOCH_VIEWER.md @@ -23,6 +23,9 @@ install it. - Opens HDF5 files written by [`jfjoch_writer`](JFJOCH_WRITER.md) (`*_master.h5`) and the `*_process.h5` files produced by [`rugnux`](RUGNUX.md). It also opens NXmx files written by DECTRIS detectors, though that path has had only limited testing. +- Opens PILATUS miniCBF rotation sweeps. These store one frame per file, so naming any frame opens + the whole sweep it belongs to. A raw CBF carries the images and the geometry but no analysis + results, so the spot, reflection and per-image plot panels stay empty until something is computed. - Runs an **embedded data-processing pipeline** — the same analysis code as the rest of Jungfraujoch — performing spot finding, indexing and integration on the displayed image, with the result drawn over it. This interactive analysis is not written anywhere. @@ -74,11 +77,11 @@ they run on — a V100 needs the CUDA 12 `.tgz`. See ## Opening data -- **File ▸ Open** (`Ctrl+O`) — open a local HDF5 file. +- **File ▸ Open** (`Ctrl+O`) — open a local HDF5 file, or any frame of a miniCBF sweep. - **File ▸ Open HTTP** (`Ctrl+H`) — connect to a `jfjoch_broker` HTTP endpoint to follow a live collection. The dialog defaults to host `localhost` and port `8080`; these defaults can be overridden with the environment variables `JUNGFRAUJOCH_HTTP_HOST` and `JUNGFRAUJOCH_HTTP_PORT`. -- **Command line** — `jfjoch_viewer ` opens a file (or an `http://host:port` URL) on +- **Command line** — `jfjoch_viewer ` opens a file (or an `http://host:port` URL) on start-up. `--dbus ` (`-d`) enables or disables the D-Bus interface (default: enabled); `--help` and `--version` behave as usual. diff --git a/viewer/JFJochImageReadingWorker.cpp b/viewer/JFJochImageReadingWorker.cpp index 11282d2a3..c3f6e7f24 100644 --- a/viewer/JFJochImageReadingWorker.cpp +++ b/viewer/JFJochImageReadingWorker.cpp @@ -108,7 +108,8 @@ JFJochImageReadingWorker::JFJochImageReadingWorker(const SpotFindingSettings &se indexing = std::make_unique(indexing_settings, IndexerConstruction::OnFirstUse); http_reader.Experiment(experiment); - file_reader.Experiment(experiment); + hdf5_reader.Experiment(experiment); + cbf_reader.Experiment(experiment); thumb_color_scale_.Select(ColorScaleEnum::Indigo); autoload_timer = new QTimer(this); @@ -209,6 +210,7 @@ void JFJochImageReadingWorker::LoadFile_i(const QString &filename, qint64 image_ if (filename.startsWith("http://") || filename.startsWith("https://")) { http_mode = true; + cbf_mode = false; http_reader.ReadURL(filename.toStdString()); total_images = http_reader.GetNumberOfImages(); dataset = http_reader.GetDataset(); @@ -265,9 +267,19 @@ void JFJochImageReadingWorker::LoadFile_i(const QString &filename, qint64 image_ file_open_retry_attempts++; } - file_reader.ReadFile(filename.toStdString()); - total_images = file_reader.GetNumberOfImages(); - dataset = file_reader.GetDataset(); + // Naming one miniCBF frame opens the sweep it belongs to; anything else is HDF5. + // Release the previously open file first, so switching format leaves nothing behind. + file_reader->Close(); + cbf_mode = JFJochCBFReader::CanRead(filename.toStdString()); + if (cbf_mode) { + file_reader = &cbf_reader; + cbf_reader.ReadFiles(filename.toStdString()); + } else { + file_reader = &hdf5_reader; + hdf5_reader.ReadFile(filename.toStdString()); + } + total_images = file_reader->GetNumberOfImages(); + dataset = file_reader->GetDataset(); setAutoLoadMode_i(AutoloadMode::None); if (retry && file_open_retry_active) { @@ -334,7 +346,7 @@ void JFJochImageReadingWorker::CloseFile() { if (http_mode) http_reader.Close(); else - file_reader.Close(); + file_reader->Close(); status_timer->stop(); setAutoLoadMode_i(AutoloadMode::None); @@ -398,7 +410,7 @@ void JFJochImageReadingWorker::LoadImage_i(int64_t image_number, int64_t summati } else { if (image_number < 0 || image_number + summation > total_images) return; - current_image_ptr = file_reader.LoadImage(image_number, summation); + current_image_ptr = file_reader->LoadImage(image_number, summation); } if (!current_image_ptr) { @@ -469,8 +481,8 @@ void JFJochImageReadingWorker::UpdateDataset_i(const std::optionalUpdateGeomMetadata(experiment.value()); + dataset = file_reader->GetDataset(); } if (!dataset) { logger.Error("UpdateDataset_i: dataset is null (http_mode={}) - skipping update to avoid crash", http_mode); @@ -799,8 +811,8 @@ void JFJochImageReadingWorker::UpdateUserMask_i(const std::vector &mas http_reader.UpdateUserMask(mask); dataset = http_reader.GetDataset(); } else { - file_reader.UpdateUserMask(mask); - dataset = file_reader.GetDataset(); + file_reader->UpdateUserMask(mask); + dataset = file_reader->GetDataset(); } if (!dataset) { @@ -886,7 +898,7 @@ void JFJochImageReadingWorker::LoadCalibration(QString dataset) { auto tmp = std::make_shared(); try { - tmp->image = file_reader.ReadCalibration(tmp->buffer, dataset.toStdString()); + tmp->image = hdf5_reader.ReadCalibration(tmp->buffer, dataset.toStdString()); std::shared_ptr ctmp = tmp; emit simpleImageLoaded(ctmp); @@ -1043,7 +1055,7 @@ void JFJochImageReadingWorker::LoadSpots(int64_t start_image, int64_t end_image, if (http_mode) result = http_reader.ReadAllSpots(start_image, end_image, stride); else - result = file_reader.ReadAllSpots(start_image, end_image, stride); + result = file_reader->ReadAllSpots(start_image, end_image, stride); emit spotsLoaded(result); } @@ -1065,8 +1077,8 @@ ReprocessingInputs JFJochImageReadingWorker::GetReprocessingInputs() const { void JFJochImageReadingWorker::ActivateSnapshot_i(const QString &name) { // Assumes m locked! Switch the reader's active metadata snapshot and refresh the view. - file_reader.SetActiveSnapshot(name.toStdString()); - auto dataset = file_reader.GetDataset(); + hdf5_reader.SetActiveSnapshot(name.toStdString()); + auto dataset = hdf5_reader.GetDataset(); if (!dataset) return; @@ -1077,9 +1089,9 @@ void JFJochImageReadingWorker::ActivateSnapshot_i(const QString &name) { EmitDatasetLoaded_i(dataset); QStringList names; - for (const auto &n: file_reader.SnapshotNames()) + for (const auto &n: hdf5_reader.SnapshotNames()) names << QString::fromStdString(n); - emit snapshotsChanged(names, QString::fromStdString(file_reader.ActiveSnapshot())); + emit snapshotsChanged(names, QString::fromStdString(hdf5_reader.ActiveSnapshot())); EmitRuns_i(); if (current_image.has_value()) { @@ -1096,13 +1108,13 @@ void JFJochImageReadingWorker::EmitRuns_i() { // Assumes m locked! Build the run list (every snapshot dataset + its display label). QVector runs; if (!http_mode) { - for (const auto &[id, dataset]: file_reader.AllSnapshotDatasets()) { + for (const auto &[id, dataset]: hdf5_reader.AllSnapshotDatasets()) { const auto it = run_labels_.find(id); const QString label = (it != run_labels_.end()) ? it->second : QString::fromStdString(id); runs.push_back(RunData{QString::fromStdString(id), label, dataset}); } } - emit runsChanged(runs, http_mode ? QString() : QString::fromStdString(file_reader.ActiveSnapshot())); + emit runsChanged(runs, http_mode ? QString() : QString::fromStdString(hdf5_reader.ActiveSnapshot())); } void JFJochImageReadingWorker::RenameRun(QString id, QString label) { @@ -1115,20 +1127,22 @@ void JFJochImageReadingWorker::RemoveRun(QString id) { QMutexLocker ul(&m); if (http_mode || id == "Original") return; - file_reader.RemoveSnapshot(id.toStdString()); + hdf5_reader.RemoveSnapshot(id.toStdString()); run_labels_.erase(id.toStdString()); // Refresh the (possibly now-Original) active view; ActivateSnapshot_i also re-emits runsChanged. - ActivateSnapshot_i(QString::fromStdString(file_reader.ActiveSnapshot())); + ActivateSnapshot_i(QString::fromStdString(hdf5_reader.ActiveSnapshot())); } void JFJochImageReadingWorker::RegisterProcessingSnapshot(QString id, QString label, QString master_path) { QMutexLocker ul(&m); - if (http_mode) { - logger.Error("Processing snapshots are only available for files"); + if (http_mode || cbf_mode) { + // A snapshot is metadata read back over the SAME images the reader is serving, which only the + // HDF5 reader can do; a CBF sweep's job output is a stand-alone file, opened like any other. + logger.Error("Processing snapshots are only available for HDF5 files"); return; } try { - file_reader.RegisterSnapshot(id.toStdString(), master_path.toStdString()); + hdf5_reader.RegisterSnapshot(id.toStdString(), master_path.toStdString()); run_labels_[id.toStdString()] = label; ActivateSnapshot_i(id); // activates + emits datasetLoaded / snapshotsChanged / runsChanged } catch (const std::exception &e) { @@ -1184,7 +1198,7 @@ QImage JFJochImageReadingWorker::RenderThumbnail_i(int64_t image_number, bool sh QMutexLocker locker(&m); if (http_mode || image_number < 0 || image_number >= total_images) return {}; - img = file_reader.LoadImage(image_number, 1); + img = file_reader->LoadImage(image_number, 1); } if (!img) return {}; diff --git a/viewer/JFJochImageReadingWorker.h b/viewer/JFJochImageReadingWorker.h index 8ca1795b8..210c8e899 100644 --- a/viewer/JFJochImageReadingWorker.h +++ b/viewer/JFJochImageReadingWorker.h @@ -16,6 +16,7 @@ #include "../common/ColorScale.h" #include "../reader/JFJochHDF5Reader.h" +#include "../reader/JFJochCBFReader.h" #include "../common/Logger.h" #include "JFJochHttpReader.h" #include "../image_analysis/MXAnalysisWithoutFPGA.h" @@ -72,7 +73,14 @@ private: AutoloadMode autoload_mode = AutoloadMode::None; - JFJochHDF5Reader file_reader; + // A stored file is read by one of these two, whichever its format needs; file_reader points at + // the one in use. The HDF5-only extras - calibration images and reprocessing snapshots - always + // go through hdf5_reader, and a raw CBF sweep simply has none of them. + JFJochHDF5Reader hdf5_reader; + JFJochCBFReader cbf_reader; + JFJochReader *file_reader = &hdf5_reader; + bool cbf_mode = false; + JFJochHttpReader http_reader; QString current_file; diff --git a/viewer/JFJochProcessController.cpp b/viewer/JFJochProcessController.cpp index 8301737a9..dd859518e 100644 --- a/viewer/JFJochProcessController.cpp +++ b/viewer/JFJochProcessController.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only #include "JFJochProcessController.h" +#include "../reader/JFJochCBFReader.h" #include "../reader/JFJochHDF5Reader.h" #include @@ -44,8 +45,17 @@ void JFJochProcessController::cancel() { void JFJochProcessController::run_(QString file_path, DiffractionExperiment experiment, PixelMask pixel_mask, ProcessConfig config) { try { - JFJochHDF5Reader reader; - reader.ReadFile(file_path.toStdString()); + JFJochHDF5Reader hdf5_reader; + JFJochCBFReader cbf_reader; + JFJochReader *reader_ptr; + if (JFJochCBFReader::CanRead(file_path.toStdString())) { + cbf_reader.ReadFiles(file_path.toStdString()); + reader_ptr = &cbf_reader; + } else { + hdf5_reader.ReadFile(file_path.toStdString()); + reader_ptr = &hdf5_reader; + } + JFJochReader &reader = *reader_ptr; // Seed the live dataset with the experiment so the chart has geometry context; per-image // results are filled in by OnImageProcessed as the run progresses. diff --git a/viewer/JFJochProcessController.h b/viewer/JFJochProcessController.h index 03d16eefe..5b11a67f6 100644 --- a/viewer/JFJochProcessController.h +++ b/viewer/JFJochProcessController.h @@ -20,7 +20,7 @@ Q_DECLARE_METATYPE(ProcessResult) // Runs one Rugnux job off the GUI thread and reports back via queued Qt signals. The job -// opens its own private JFJochHDF5Reader on the file (HDF5 access is globally serialized, so this +// opens its own private reader on the file (HDF5 access is globally serialized, so this // is safe alongside the interactive reader), so the viewer becomes a processing frontend without // blocking the UI. Cancel() is forwarded to Rugnux::Cancel() (atomic) and works from any // thread / at any point of the run. diff --git a/viewer/JFJochViewerMenu.cpp b/viewer/JFJochViewerMenu.cpp index 8d0393758..e66c3bd0a 100644 --- a/viewer/JFJochViewerMenu.cpp +++ b/viewer/JFJochViewerMenu.cpp @@ -153,7 +153,8 @@ void JFJochViewerMenu::openSelected() { this, "Open File", // Dialog title "", // Default folder - "HDF5 Master Files (*_master.h5 *_process.h5);; HDF5 Files (*.h5);;All Files (*)" // Filter for .h5 files + // A CBF is one frame per file: picking any frame opens the sweep it belongs to. + "HDF5 Master Files (*_master.h5 *_process.h5);; HDF5 Files (*.h5);; CBF Images (*.cbf);;All Files (*)" ); if (!fileName.isEmpty())