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())