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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lc5JG6kJqZoCWaoZ43JGTW
This commit is contained in:
2026-08-29 21:49:09 +02:00
co-authored by Claude Opus 5
parent 172a845cbb
commit 84efb0666c
7 changed files with 68 additions and 31 deletions
+1
View File
@@ -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.
+5 -2
View File
@@ -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 <file.h5>` opens a file (or an `http://host:port` URL) on
- **Command line** — `jfjoch_viewer <file>` opens a file (or an `http://host:port` URL) on
start-up. `--dbus <true|false>` (`-d`) enables or disables the D-Bus interface (default: enabled);
`--help` and `--version` behave as usual.
+38 -24
View File
@@ -108,7 +108,8 @@ JFJochImageReadingWorker::JFJochImageReadingWorker(const SpotFindingSettings &se
indexing = std::make_unique<IndexerThreadPool>(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::optional<DiffractionEx
dataset = http_reader.GetDataset();
} else {
if (experiment)
file_reader.UpdateGeomMetadata(experiment.value());
dataset = file_reader.GetDataset();
file_reader->UpdateGeomMetadata(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<uint32_t> &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<SimpleImage>();
try {
tmp->image = file_reader.ReadCalibration(tmp->buffer, dataset.toStdString());
tmp->image = hdf5_reader.ReadCalibration(tmp->buffer, dataset.toStdString());
std::shared_ptr<const SimpleImage> 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<RunData> 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 {};
+9 -1
View File
@@ -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;
+12 -2
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only
#include "JFJochProcessController.h"
#include "../reader/JFJochCBFReader.h"
#include "../reader/JFJochHDF5Reader.h"
#include <QMetaType>
@@ -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.
+1 -1
View File
@@ -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.
+2 -1
View File
@@ -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())