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:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user