viewer: overlay one plot line per processing run (with editable names)
The dataset-info plot now overlays every run as a separate named line instead of replacing the plot when a snapshot is activated: - Reader gains AllSnapshotDatasets() (every snapshot's dataset, Original first). - The worker owns the run collection: a stable snapshot id plus an editable display label (RunData), emitted as runsChanged(runs, active_id) on file open, snapshot register, activate and rename. RenameRun(id, label) updates the legend label without touching the reader key (decoupled id vs label). - The chart view draws the active run as the primary series (markers, hover, binning, axes) and the other runs as overlay lines sharing its axes, with a legend shown when overlaying (appendSeries factored out for both). - The dataset-info widget holds the run list + the live run, extracts the selected metric from each (ExtractMetric), and unions the available metrics across runs. - The live run is its own "Live" overlay (lightweight per-tick refresh, no combo rebuild); it is cleared on finish so the persisted snapshot takes over. - The processing jobs table gets a "Started" column and an editable Name column; editing a name renames that run's legend label. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -178,3 +178,17 @@ std::string JFJochHDF5Reader::ActiveSnapshot() const {
|
||||
std::unique_lock ul(hdf5_mutex);
|
||||
return active_snapshot_;
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, std::shared_ptr<const JFJochReaderDataset>>>
|
||||
JFJochHDF5Reader::AllSnapshotDatasets() const {
|
||||
std::unique_lock ul(hdf5_mutex);
|
||||
std::vector<std::pair<std::string, std::shared_ptr<const JFJochReaderDataset>>> out;
|
||||
out.reserve(snapshots_.size());
|
||||
// "Original" first, then the rest, so overlay colours stay stable across updates.
|
||||
if (auto it = snapshots_.find("Original"); it != snapshots_.end())
|
||||
out.emplace_back(it->first, it->second->Dataset());
|
||||
for (const auto &[name, source]: snapshots_)
|
||||
if (name != "Original")
|
||||
out.emplace_back(name, source->Dataset());
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -63,4 +63,7 @@ public:
|
||||
void SetActiveSnapshot(const std::string &name);
|
||||
std::vector<std::string> SnapshotNames() const;
|
||||
std::string ActiveSnapshot() const;
|
||||
|
||||
// All snapshot datasets (name -> dataset), for overlaying every run's plots at once.
|
||||
std::vector<std::pair<std::string, std::shared_ptr<const JFJochReaderDataset>>> AllSnapshotDatasets() const;
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ ADD_EXECUTABLE(jfjoch_viewer jfjoch_viewer.cpp JFJochViewerWindow.cpp JFJochView
|
||||
image_viewer/JFJochSimpleImage.h
|
||||
JFJochImageReadingWorker.cpp
|
||||
JFJochImageReadingWorker.h
|
||||
RunData.h
|
||||
JFJochProcessController.cpp
|
||||
JFJochProcessController.h
|
||||
JFJochViewerDatasetInfo.cpp
|
||||
|
||||
@@ -97,6 +97,8 @@ JFJochImageReadingWorker::JFJochImageReadingWorker(const SpotFindingSettings &se
|
||||
qRegisterMetaType<ROIDefinition>("ROIDefinition");
|
||||
qRegisterMetaType<BraggIntegrationSettings>("BraggIntegrationSettings");
|
||||
qRegisterMetaType<ScalingSettings>("ScalingSettings");
|
||||
qRegisterMetaType<RunData>("RunData");
|
||||
qRegisterMetaType<QVector<RunData>>("QVector<RunData>");
|
||||
spot_finding_settings = settings;
|
||||
|
||||
indexing = std::make_unique<IndexerThreadPool>(indexing_settings);
|
||||
@@ -287,6 +289,12 @@ void JFJochImageReadingWorker::LoadFile_i(const QString &filename, qint64 image_
|
||||
|
||||
emit datasetLoaded(dataset);
|
||||
|
||||
// Reset the run collection for the freshly opened file (file mode only has snapshots).
|
||||
run_labels_.clear();
|
||||
if (!http_mode)
|
||||
run_labels_["Original"] = "Original";
|
||||
EmitRuns_i();
|
||||
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
|
||||
logger.Info(fmt::format("Loaded file {} in {} ms", filename.toStdString(), duration));
|
||||
@@ -948,6 +956,7 @@ void JFJochImageReadingWorker::ActivateSnapshot_i(const QString &name) {
|
||||
for (const auto &n: file_reader.SnapshotNames())
|
||||
names << QString::fromStdString(n);
|
||||
emit snapshotsChanged(names, QString::fromStdString(file_reader.ActiveSnapshot()));
|
||||
EmitRuns_i();
|
||||
|
||||
if (current_image.has_value()) {
|
||||
// A snapshot carries its own per-image results; just re-read the image against the new
|
||||
@@ -959,17 +968,37 @@ void JFJochImageReadingWorker::ActivateSnapshot_i(const QString &name) {
|
||||
}
|
||||
}
|
||||
|
||||
void JFJochImageReadingWorker::RegisterProcessingSnapshot(QString name, QString master_path) {
|
||||
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()) {
|
||||
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()));
|
||||
}
|
||||
|
||||
void JFJochImageReadingWorker::RenameRun(QString id, QString label) {
|
||||
QMutexLocker ul(&m);
|
||||
run_labels_[id.toStdString()] = label;
|
||||
EmitRuns_i();
|
||||
}
|
||||
|
||||
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");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
file_reader.RegisterSnapshot(name.toStdString(), master_path.toStdString());
|
||||
ActivateSnapshot_i(name);
|
||||
file_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) {
|
||||
logger.Error("Failed to register snapshot {}: {}", name.toStdString(), e.what());
|
||||
logger.Error("Failed to register snapshot {}: {}", id.toStdString(), e.what());
|
||||
emit fileLoadError("Snapshot error", QString::fromStdString(e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ struct ReprocessingInputs {
|
||||
SpotFindingSettings spot_finding;
|
||||
};
|
||||
|
||||
#include "RunData.h"
|
||||
|
||||
class JFJochImageReadingWorker : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
@@ -69,6 +71,7 @@ private:
|
||||
// Once the user edits ROIs they override whatever the file carried, for this and
|
||||
// every subsequently loaded image, until a new file is opened.
|
||||
std::optional<ROIDefinition> roi_override_;
|
||||
std::map<std::string, QString> run_labels_; // snapshot id -> editable display label
|
||||
IndexingSettings indexing_settings;
|
||||
AzimuthalIntegrationSettings azint_settings;
|
||||
BraggIntegrationSettings bragg_settings;
|
||||
@@ -138,6 +141,7 @@ private:
|
||||
void ApplyROIOverrideToImage_i(); // rewrite current_image_ptr's dataset with roi_override_
|
||||
void setAutoLoadMode_i(AutoloadMode mode);
|
||||
void ActivateSnapshot_i(const QString &name);
|
||||
void EmitRuns_i(); // emit runsChanged with every snapshot dataset + its label
|
||||
|
||||
signals:
|
||||
void datasetLoaded(std::shared_ptr<const JFJochReaderDataset>);
|
||||
@@ -154,6 +158,7 @@ signals:
|
||||
void httpConnectionChanged(bool connected, QString addr);
|
||||
void liveRateChanged(double hz);
|
||||
void snapshotsChanged(QStringList names, QString active);
|
||||
void runsChanged(QVector<RunData> runs, QString active_id);
|
||||
|
||||
public:
|
||||
JFJochImageReadingWorker(const SpotFindingSettings &settings, const DiffractionExperiment& experiment, QObject *parent = nullptr);
|
||||
@@ -202,6 +207,7 @@ public slots:
|
||||
|
||||
// Register a reprocessing result (_process.h5) as a metadata snapshot over the same images and
|
||||
// make it the active dataset; SetActiveSnapshot switches between registered snapshots.
|
||||
void RegisterProcessingSnapshot(QString name, QString master_path);
|
||||
void RegisterProcessingSnapshot(QString id, QString label, QString master_path);
|
||||
void SetActiveSnapshot(QString name);
|
||||
void RenameRun(QString id, QString label); // change a run's display label (legend)
|
||||
};
|
||||
|
||||
@@ -83,7 +83,20 @@ void JFJochViewerDatasetInfo::UpdateLabels() {
|
||||
combo_box->addItem("Spot count (ice rings)", 3);
|
||||
combo_box->addItem("Spot count (low res.)", 4);
|
||||
|
||||
if (!dataset->indexing_result.empty()) {
|
||||
// Offer indexing/scaling metrics if the active run, any overlay run, or the live run has them.
|
||||
bool has_indexing = !dataset->indexing_result.empty();
|
||||
bool has_scale = !dataset->image_scale_factor.empty();
|
||||
for (const auto &r: runs_)
|
||||
if (r.dataset) {
|
||||
has_indexing = has_indexing || !r.dataset->indexing_result.empty();
|
||||
has_scale = has_scale || !r.dataset->image_scale_factor.empty();
|
||||
}
|
||||
if (live_run_) {
|
||||
has_indexing = has_indexing || !live_run_->indexing_result.empty();
|
||||
has_scale = has_scale || !live_run_->image_scale_factor.empty();
|
||||
}
|
||||
|
||||
if (has_indexing) {
|
||||
combo_box->insertSeparator(1000);
|
||||
combo_box->addItem("Indexing result", 5);
|
||||
combo_box->addItem("Profile radius", 6);
|
||||
@@ -93,7 +106,7 @@ void JFJochViewerDatasetInfo::UpdateLabels() {
|
||||
combo_box->addItem("Indexing lattice count", 13);
|
||||
}
|
||||
|
||||
if (!dataset->image_scale_factor.empty()) {
|
||||
if (has_scale) {
|
||||
combo_box->insertSeparator(1000);
|
||||
combo_box->addItem("Scale factor", 10);
|
||||
combo_box->addItem("CC", 11);
|
||||
@@ -122,7 +135,7 @@ void JFJochViewerDatasetInfo::datasetLoaded(std::shared_ptr<const JFJochReaderDa
|
||||
combo_box->setCurrentIndex(0);
|
||||
UpdatePlot();
|
||||
} else {
|
||||
chart_view->loadValues<float>({}, 0, false);
|
||||
chart_view->loadValues({}, 0, false);
|
||||
grid_scan_image->clear();
|
||||
UpdateLabels();
|
||||
}
|
||||
@@ -140,6 +153,46 @@ void JFJochViewerDatasetInfo::imageSelectedInChart(int64_t number) {
|
||||
emit imageSelected(number, 1);
|
||||
}
|
||||
|
||||
std::vector<float> JFJochViewerDatasetInfo::ExtractMetric(const JFJochReaderDataset &ds, int val,
|
||||
bool &one_over_d2) const {
|
||||
one_over_d2 = false;
|
||||
std::vector<float> data;
|
||||
if (val == 0) data = ds.bkg_estimate;
|
||||
else if (val == 1) data = ds.spot_count;
|
||||
else if (val == 2) data = ds.spot_count_indexed;
|
||||
else if (val == 3) data = ds.spot_count_ice_rings;
|
||||
else if (val == 4) data = ds.spot_count_low_res;
|
||||
else if (val == 5) data = ds.indexing_result;
|
||||
else if (val == 6) data = ds.profile_radius;
|
||||
else if (val == 7) { data = ds.resolution_estimate; one_over_d2 = true; }
|
||||
else if (val == 8) data = ds.b_factor;
|
||||
else if (val == 9) data = ds.mosaicity_deg;
|
||||
else if (val == 10) data = ds.image_scale_factor;
|
||||
else if (val == 11) data = ds.image_scale_cc;
|
||||
else if (val == 12) data = ds.integrated_reflections;
|
||||
else if (val == 13) data = ds.indexing_lattice_count;
|
||||
else if (val >= 100) {
|
||||
const int roi_index = (val - 100) / 4;
|
||||
if (val % 4 == 0) {
|
||||
if (roi_index < ds.roi_sum.size() && ds.roi_sum.size() == ds.roi_npixel.size())
|
||||
for (int i = 0; i < ds.roi_sum[roi_index].size(); i++)
|
||||
data.push_back(static_cast<float>(ds.roi_sum[roi_index][i])
|
||||
/ static_cast<float>(ds.roi_npixel[roi_index][i]));
|
||||
} else if (val % 4 == 1) {
|
||||
if (roi_index < ds.roi_sum.size()) {
|
||||
data.reserve(ds.roi_sum[roi_index].size());
|
||||
for (auto &v: ds.roi_sum[roi_index])
|
||||
data.push_back(v);
|
||||
}
|
||||
} else if (val % 4 == 2) {
|
||||
if (roi_index < ds.roi_x.size()) data = ds.roi_x[roi_index];
|
||||
} else if (val % 4 == 3) {
|
||||
if (roi_index < ds.roi_y.size()) data = ds.roi_y[roi_index];
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
void JFJochViewerDatasetInfo::UpdatePlot() {
|
||||
int index = combo_box->currentIndex();
|
||||
if (combo_box->count() == 0 || index < 0)
|
||||
@@ -147,83 +200,58 @@ void JFJochViewerDatasetInfo::UpdatePlot() {
|
||||
|
||||
int val = combo_box->itemData(index).toInt();
|
||||
|
||||
if (dataset) {
|
||||
int64_t image_number = 0;
|
||||
if (image)
|
||||
image_number = image->ImageData().number;
|
||||
if (!dataset) {
|
||||
grid_button->setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
bool one_over_d2 = false;
|
||||
std::vector<float> data;
|
||||
if (val == 0)
|
||||
data = dataset->bkg_estimate;
|
||||
else if (val == 1)
|
||||
data = dataset->spot_count;
|
||||
else if (val == 2)
|
||||
data = dataset->spot_count_indexed;
|
||||
else if (val == 3)
|
||||
data = dataset->spot_count_ice_rings;
|
||||
else if (val == 4)
|
||||
data = dataset->spot_count_low_res;
|
||||
else if (val == 5)
|
||||
data = dataset->indexing_result;
|
||||
else if (val == 6)
|
||||
data = dataset->profile_radius;
|
||||
else if (val == 7) {
|
||||
data = dataset->resolution_estimate;
|
||||
one_over_d2 = true;
|
||||
} else if (val == 8) {
|
||||
data = dataset->b_factor;
|
||||
} else if (val == 9) {
|
||||
data = dataset->mosaicity_deg;
|
||||
} else if (val == 10) {
|
||||
data = dataset->image_scale_factor;
|
||||
} else if (val == 11) {
|
||||
data = dataset->image_scale_cc;
|
||||
} else if (val == 12) {
|
||||
data = dataset->integrated_reflections;
|
||||
} else if (val == 13) {
|
||||
data = dataset->indexing_lattice_count;
|
||||
} else if (val >= 100) {
|
||||
int roi_index = (val - 100) / 4;
|
||||
const int64_t image_number = image ? image->ImageData().number : 0;
|
||||
|
||||
if (val % 4 == 0) {
|
||||
if (roi_index < dataset->roi_sum.size() && dataset->roi_sum.size() == dataset->roi_npixel.size()) {
|
||||
for(int i = 0; i < dataset->roi_sum[roi_index].size(); i++) {
|
||||
data.push_back(static_cast<float>(dataset->roi_sum[roi_index][i])
|
||||
/ static_cast<float>(dataset->roi_npixel[roi_index][i]));
|
||||
}
|
||||
}
|
||||
} else if (val % 4 == 1) {
|
||||
if (roi_index < dataset->roi_sum.size()) {
|
||||
data.reserve(dataset->roi_sum[roi_index].size());
|
||||
for (auto &v: dataset->roi_sum[roi_index])
|
||||
data.push_back(v);
|
||||
}
|
||||
} else if (val % 4 == 2) {
|
||||
if (roi_index < dataset->roi_x.size())
|
||||
data = dataset->roi_x[roi_index];
|
||||
} else if (val % 4 == 3) {
|
||||
if (roi_index < dataset->roi_y.size())
|
||||
data = dataset->roi_y[roi_index];
|
||||
}
|
||||
}
|
||||
bool one_over_d2 = false;
|
||||
std::vector<float> data = ExtractMetric(*dataset, val, one_over_d2);
|
||||
|
||||
chart_view->loadValues(data, image_number, one_over_d2, dataset.get());
|
||||
if (dataset->experiment.GetGridScan()) {
|
||||
stack->setCurrentWidget(grid_scan_image);
|
||||
grid_scan_image->loadData(data, dataset->experiment.GetGridScan().value(), one_over_d2);
|
||||
grid_button->setEnabled(true);
|
||||
grid_button->setChecked(true);
|
||||
} else {
|
||||
grid_scan_image->clear();
|
||||
stack->setCurrentWidget(chart_view);
|
||||
grid_button->setEnabled(false);
|
||||
}
|
||||
// One overlay line per other run, plus the in-progress live run.
|
||||
std::vector<JFJochDatasetInfoChartView::NamedSeries> overlays;
|
||||
QString primary_name;
|
||||
for (const auto &run: runs_) {
|
||||
if (run.id == active_id_) { primary_name = run.label; continue; }
|
||||
if (!run.dataset || run.dataset == dataset) continue; // never draw the active run twice
|
||||
bool ignore = false;
|
||||
overlays.push_back({run.label, ExtractMetric(*run.dataset, val, ignore)});
|
||||
}
|
||||
if (live_run_ && live_run_ != dataset) {
|
||||
bool ignore = false;
|
||||
overlays.push_back({QStringLiteral("Live"), ExtractMetric(*live_run_, val, ignore)});
|
||||
}
|
||||
|
||||
chart_view->loadValues(data, image_number, one_over_d2, dataset.get(), primary_name, std::move(overlays));
|
||||
|
||||
if (dataset->experiment.GetGridScan()) {
|
||||
stack->setCurrentWidget(grid_scan_image);
|
||||
grid_scan_image->loadData(data, dataset->experiment.GetGridScan().value(), one_over_d2);
|
||||
grid_button->setEnabled(true);
|
||||
grid_button->setChecked(true);
|
||||
} else {
|
||||
grid_scan_image->clear();
|
||||
stack->setCurrentWidget(chart_view);
|
||||
grid_button->setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
void JFJochViewerDatasetInfo::runsChanged(QVector<RunData> runs, QString active_id) {
|
||||
runs_ = std::move(runs);
|
||||
active_id_ = std::move(active_id);
|
||||
UpdateLabels();
|
||||
if (last_selection < combo_box->count())
|
||||
combo_box->setCurrentIndex(last_selection);
|
||||
UpdatePlot();
|
||||
}
|
||||
|
||||
void JFJochViewerDatasetInfo::liveRunUpdated(std::shared_ptr<const JFJochReaderDataset> in_dataset) {
|
||||
live_run_ = std::move(in_dataset);
|
||||
UpdatePlot(); // lightweight refresh; no combo rebuild on every live tick
|
||||
}
|
||||
|
||||
void JFJochViewerDatasetInfo::comboBoxSelected(int index) {
|
||||
UpdatePlot();
|
||||
}
|
||||
|
||||
@@ -10,21 +10,28 @@
|
||||
#include "charts/JFJochDatasetInfoChartView.h"
|
||||
#include "../reader/JFJochReader.h"
|
||||
#include "image_viewer/JFJochGridScanImage.h"
|
||||
#include "RunData.h"
|
||||
|
||||
class JFJochViewerDatasetInfo : public QWidget {
|
||||
Q_OBJECT
|
||||
QComboBox *combo_box;
|
||||
JFJochDatasetInfoChartView *chart_view;
|
||||
const std::vector<float> *GetDataset();
|
||||
std::shared_ptr<const JFJochReaderDataset> dataset;
|
||||
std::shared_ptr<const JFJochReaderDataset> dataset; // the active (primary) run
|
||||
std::shared_ptr<const JFJochReaderImage> image;
|
||||
JFJochGridScanImage *grid_scan_image = nullptr;
|
||||
|
||||
QVector<RunData> runs_; // all runs, for overlay lines
|
||||
QString active_id_; // which run is primary
|
||||
std::shared_ptr<const JFJochReaderDataset> live_run_; // in-progress reprocessing, if any
|
||||
|
||||
QStackedWidget *stack = nullptr;
|
||||
QPushButton *grid_button = nullptr;
|
||||
|
||||
int last_selection;
|
||||
void UpdatePlot();
|
||||
// Pull the metric selected by combo value `val` out of one run's dataset.
|
||||
std::vector<float> ExtractMetric(const JFJochReaderDataset &ds, int val, bool &one_over_d2) const;
|
||||
|
||||
void UpdateLabels();
|
||||
signals:
|
||||
@@ -41,6 +48,10 @@ public slots:
|
||||
void datasetLoaded(std::shared_ptr<const JFJochReaderDataset> dataset);
|
||||
void imageLoaded(std::shared_ptr<const JFJochReaderImage> image);
|
||||
void setColorMap(int color_map);
|
||||
// All runs (original + reprocessing snapshots) and which one is active/primary.
|
||||
void runsChanged(QVector<RunData> runs, QString active_id);
|
||||
// Live results of the in-progress reprocessing run (updated as it goes), or nullptr to clear.
|
||||
void liveRunUpdated(std::shared_ptr<const JFJochReaderDataset> dataset);
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -401,8 +401,15 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString
|
||||
reading_worker, &JFJochImageReadingWorker::SetActiveSnapshot);
|
||||
connect(processingJobsWindow, &JFJochProcessingJobsWindow::writeStatusBar,
|
||||
statusbar, &JFJochViewerStatusBar::display);
|
||||
connect(processingJobsWindow, &JFJochProcessingJobsWindow::renameRun,
|
||||
reading_worker, &JFJochImageReadingWorker::RenameRun);
|
||||
connect(reading_worker, &JFJochImageReadingWorker::httpConnectionChanged,
|
||||
processingJobsWindow, &JFJochProcessingJobsWindow::onHttpConnectionChanged);
|
||||
connect(reading_worker, &JFJochImageReadingWorker::runsChanged, this,
|
||||
[this](QVector<RunData> runs, QString active) {
|
||||
lastRuns = std::move(runs);
|
||||
lastActiveRunId = std::move(active);
|
||||
});
|
||||
|
||||
// Dock the processing panel in the bottom-right corner, next to the dataset-info plots.
|
||||
auto processingDock = new QDockWidget("Processing", this);
|
||||
@@ -442,6 +449,7 @@ void JFJochViewerWindow::NewDatasetInfo() {
|
||||
auto info = new JFJochViewerDatasetInfo(this);
|
||||
info->datasetLoaded(lastDataset);
|
||||
info->imageLoaded(lastImage);
|
||||
info->runsChanged(lastRuns, lastActiveRunId);
|
||||
|
||||
auto dock = new QDockWidget(QString("Dataset info"), this);
|
||||
dock->setAllowedAreas(Qt::BottomDockWidgetArea);
|
||||
@@ -458,9 +466,12 @@ void JFJochViewerWindow::NewDatasetInfo() {
|
||||
info, &JFJochViewerDatasetInfo::datasetLoaded);
|
||||
connect(reading_worker, &JFJochImageReadingWorker::imageLoaded,
|
||||
info, &JFJochViewerDatasetInfo::imageLoaded);
|
||||
// Live processing results: refresh the plots while a job runs.
|
||||
// All runs (original + reprocessing snapshots) overlay as separate lines.
|
||||
connect(reading_worker, &JFJochImageReadingWorker::runsChanged,
|
||||
info, &JFJochViewerDatasetInfo::runsChanged);
|
||||
// Live processing results: the in-progress run as its own overlay line.
|
||||
connect(processingJobsWindow, &JFJochProcessingJobsWindow::liveDataset,
|
||||
info, &JFJochViewerDatasetInfo::datasetLoaded);
|
||||
info, &JFJochViewerDatasetInfo::liveRunUpdated);
|
||||
connect(info, &JFJochViewerDatasetInfo::imageSelected,
|
||||
reading_worker, &JFJochImageReadingWorker::LoadImage);
|
||||
connect(toolBarDisplay, &JFJochViewerToolbarDisplay::colorMapChanged,
|
||||
|
||||
@@ -32,6 +32,8 @@ private:
|
||||
|
||||
std::shared_ptr<const JFJochReaderDataset> lastDataset; // added
|
||||
std::shared_ptr<const JFJochReaderImage> lastImage; // added
|
||||
QVector<RunData> lastRuns; // for dataset-info docks opened later
|
||||
QString lastActiveRunId;
|
||||
|
||||
QProgressDialog *retryDialog = nullptr;
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include <QMetaType>
|
||||
#include <memory>
|
||||
|
||||
#include "../reader/JFJochReaderDataset.h"
|
||||
|
||||
// One processing run (the original file or a reprocessing snapshot), for overlaying its plots.
|
||||
// id is the stable reader-snapshot key; label is the editable display name shown in the legend.
|
||||
struct RunData {
|
||||
QString id;
|
||||
QString label;
|
||||
std::shared_ptr<const JFJochReaderDataset> dataset;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(RunData)
|
||||
Q_DECLARE_METATYPE(QVector<RunData>)
|
||||
@@ -128,6 +128,71 @@ void JFJochDatasetInfoChartView::resetZoom() {
|
||||
chart()->zoomReset();
|
||||
}
|
||||
|
||||
void JFJochDatasetInfoChartView::loadValues(const std::vector<float> &input, int64_t image, bool one_over_d2,
|
||||
const JFJochReaderDataset *dataset, const QString &primaryName,
|
||||
std::vector<NamedSeries> overlays) {
|
||||
m_yOneOverD = one_over_d2;
|
||||
primaryName_ = primaryName;
|
||||
|
||||
// d -> 1/d^2 for resolution plots; identity otherwise. Applied to every series alike.
|
||||
auto transform = [one_over_d2](const std::vector<float> &in, std::vector<float> &out) {
|
||||
out.resize(in.size());
|
||||
for (size_t i = 0; i < in.size(); i++) {
|
||||
if (one_over_d2) {
|
||||
const float d = in[i];
|
||||
out[i] = std::isfinite(d) ? 1.0f / (d * d) : 0.0f;
|
||||
} else
|
||||
out[i] = in[i];
|
||||
}
|
||||
};
|
||||
|
||||
transform(input, values);
|
||||
overlays_ = std::move(overlays);
|
||||
for (auto &ov: overlays_)
|
||||
transform(ov.values, ov.values); // in-place
|
||||
|
||||
if (dataset != nullptr) {
|
||||
goniometer_axis = dataset->experiment.GetGoniometer();
|
||||
image_time_us = dataset->experiment.GetImageTime().count();
|
||||
} else {
|
||||
goniometer_axis = {};
|
||||
image_time_us = {};
|
||||
}
|
||||
|
||||
curr_image = image;
|
||||
updateChart();
|
||||
}
|
||||
|
||||
void JFJochDatasetInfoChartView::appendSeries(QLineSeries *s, const std::vector<float> &vals,
|
||||
double &mn, double &mx) const {
|
||||
if (binning == 1) {
|
||||
for (int i = 0; i < static_cast<int>(vals.size()); i++) {
|
||||
const double v = vals[static_cast<size_t>(i)];
|
||||
if (!std::isfinite(v)) continue;
|
||||
s->append(i, v);
|
||||
mn = std::min(mn, v);
|
||||
mx = std::max(mx, v);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < static_cast<int>(vals.size() / static_cast<size_t>(binning)); i++) {
|
||||
double tmp = 0.0;
|
||||
int64_t count = 0;
|
||||
for (int b = 0; b < binning; b++) {
|
||||
const int64_t idx = static_cast<int64_t>(i) * binning + b;
|
||||
if (idx >= static_cast<int64_t>(vals.size())) break;
|
||||
const double v = vals[static_cast<size_t>(idx)];
|
||||
if (std::isfinite(v)) { tmp += v; count++; }
|
||||
}
|
||||
if (count > 0) {
|
||||
const double mean = tmp / static_cast<double>(count);
|
||||
s->append((i + 0.5) * binning, mean);
|
||||
mn = std::min(mn, mean);
|
||||
mx = std::max(mx, mean);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void JFJochDatasetInfoChartView::updateChart() {
|
||||
// Important: drop any stale QObject pointers BEFORE rebuilding the chart.
|
||||
series = nullptr;
|
||||
@@ -156,44 +221,23 @@ void JFJochDatasetInfoChartView::buildTimeDomainChart() {
|
||||
// At least one full point
|
||||
|
||||
series = new QLineSeries(this);
|
||||
if (!primaryName_.isEmpty())
|
||||
series->setName(primaryName_);
|
||||
currentSeries = new QScatterSeries(this);
|
||||
|
||||
double dispMin = std::numeric_limits<double>::infinity();
|
||||
double dispMax = -std::numeric_limits<double>::infinity();
|
||||
|
||||
if (binning == 1) {
|
||||
for (int i = 0; i < static_cast<int>(values.size()); i++) {
|
||||
if (!std::isfinite(values[static_cast<size_t>(i)])) continue;
|
||||
const double disp = values[static_cast<size_t>(i)];
|
||||
if (!std::isfinite(disp)) continue;
|
||||
series->append(i, disp);
|
||||
if (disp < dispMin) dispMin = disp;
|
||||
if (disp > dispMax) dispMax = disp;
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < static_cast<int>(values.size() / static_cast<size_t>(binning)); i++) {
|
||||
double tmp = 0.0;
|
||||
int64_t count = 0;
|
||||
for (int b = 0; b < binning; b++) {
|
||||
const int64_t idx = static_cast<int64_t>(i) * binning + b;
|
||||
if (idx >= static_cast<int64_t>(values.size()))
|
||||
break;
|
||||
const double v = values[static_cast<size_t>(idx)];
|
||||
if (std::isfinite(v)) {
|
||||
tmp += v;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count > 0) {
|
||||
const double mean = tmp / static_cast<double>(count);
|
||||
const double disp = mean;
|
||||
if (std::isfinite(disp)) {
|
||||
series->append((i + 0.5) * binning, disp);
|
||||
if (disp < dispMin) dispMin = disp;
|
||||
if (disp > dispMax) dispMax = disp;
|
||||
}
|
||||
}
|
||||
}
|
||||
appendSeries(series, values, dispMin, dispMax);
|
||||
|
||||
// Overlay runs share the primary's axes and range; build them now so the Y range fits all.
|
||||
std::vector<QLineSeries *> overlayLines;
|
||||
overlayLines.reserve(overlays_.size());
|
||||
for (const auto &ov: overlays_) {
|
||||
auto *line = new QLineSeries(this);
|
||||
line->setName(ov.name);
|
||||
appendSeries(line, ov.values, dispMin, dispMax);
|
||||
overlayLines.push_back(line);
|
||||
}
|
||||
|
||||
// ---- current point marker as binned value when binning > 1 ----
|
||||
@@ -366,6 +410,16 @@ void JFJochDatasetInfoChartView::buildTimeDomainChart() {
|
||||
axisY->setLabelsVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Attach overlay lines to the primary series' final axes; show the legend when overlaying.
|
||||
const auto finalAxes = series->attachedAxes();
|
||||
for (auto *line: overlayLines) {
|
||||
chart()->addSeries(line);
|
||||
for (auto *ax: finalAxes)
|
||||
line->attachAxis(ax);
|
||||
}
|
||||
chart()->legend()->setVisible(!overlays_.empty());
|
||||
chart()->legend()->setAlignment(Qt::AlignBottom);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ class JFJochDatasetInfoChartView : public QChartView {
|
||||
int64_t m_hoverPendingIdx = -1;
|
||||
|
||||
std::vector<float> values;
|
||||
QString primaryName_; // legend name of the active (primary) run
|
||||
int64_t binning = 1;
|
||||
int64_t curr_image = 0;
|
||||
|
||||
@@ -44,6 +45,14 @@ class JFJochDatasetInfoChartView : public QChartView {
|
||||
// Build regular (index or goniometer) chart
|
||||
void buildTimeDomainChart();
|
||||
|
||||
public:
|
||||
// One overlay line (a non-active run), drawn alongside the primary series with a legend entry.
|
||||
struct NamedSeries { QString name; std::vector<float> values; };
|
||||
private:
|
||||
std::vector<NamedSeries> overlays_;
|
||||
// Append (binned) finite points of vals to s, extending the [mn, mx] data range.
|
||||
void appendSeries(QLineSeries *s, const std::vector<float> &vals, double &mn, double &mx) const;
|
||||
|
||||
// FFT view toggle
|
||||
bool m_showFFT = false;
|
||||
|
||||
@@ -76,36 +85,14 @@ public slots:
|
||||
void resetZoom();
|
||||
|
||||
public:
|
||||
template<class T>
|
||||
void loadValues(const std::vector<T> &input,
|
||||
// Load the primary (active) run's values plus any overlay runs. one_over_d2 applies the same
|
||||
// d -> 1/d^2 transform to every series; primaryName/overlay names drive the legend.
|
||||
void loadValues(const std::vector<float> &input,
|
||||
int64_t image,
|
||||
bool one_over_d2,
|
||||
const JFJochReaderDataset *dataset = nullptr) {
|
||||
m_yOneOverD = one_over_d2;
|
||||
|
||||
values.resize(input.size());
|
||||
|
||||
if (dataset != nullptr) {
|
||||
goniometer_axis = dataset->experiment.GetGoniometer();
|
||||
image_time_us = dataset->experiment.GetImageTime().count();
|
||||
} else {
|
||||
goniometer_axis = {};
|
||||
image_time_us = {};
|
||||
}
|
||||
|
||||
for (int i = 0; i < input.size(); i++) {
|
||||
if (one_over_d2) {
|
||||
float d = static_cast<float>(input[i]);
|
||||
if (!std::isfinite(d))
|
||||
values[i] = 0.0f;
|
||||
else
|
||||
values[i] = 1.0f / (d * d);
|
||||
} else
|
||||
values[i] = static_cast<float>(input[i]);
|
||||
}
|
||||
curr_image = image;
|
||||
updateChart();
|
||||
}
|
||||
const JFJochReaderDataset *dataset = nullptr,
|
||||
const QString &primaryName = QString(),
|
||||
std::vector<NamedSeries> overlays = {});
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <QCheckBox>
|
||||
#include <QClipboard>
|
||||
#include <QComboBox>
|
||||
#include <QDateTime>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFileInfo>
|
||||
@@ -27,7 +28,7 @@
|
||||
#include <thread>
|
||||
|
||||
namespace {
|
||||
enum Column { COL_NAME = 0, COL_MODE, COL_IMAGES, COL_STATUS, COL_INDEX, COL_CELL, COL_COUNT };
|
||||
enum Column { COL_NAME = 0, COL_STARTED, COL_MODE, COL_IMAGES, COL_STATUS, COL_INDEX, COL_CELL, COL_COUNT };
|
||||
|
||||
int default_threads() {
|
||||
const unsigned hc = std::thread::hardware_concurrency();
|
||||
@@ -55,11 +56,21 @@ JFJochProcessingJobsWindow::JFJochProcessingJobsWindow(JFJochImageReadingWorker
|
||||
toolbar_->addAction("Show original", this, &JFJochProcessingJobsWindow::showOriginal);
|
||||
|
||||
table_ = new QTableWidget(0, COL_COUNT, this);
|
||||
table_->setHorizontalHeaderLabels({"Name", "Mode", "Images", "Status", "Index %", "Unit cell"});
|
||||
table_->setHorizontalHeaderLabels({"Name", "Started", "Mode", "Images", "Status", "Index %", "Unit cell"});
|
||||
table_->horizontalHeader()->setStretchLastSection(true);
|
||||
table_->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
table_->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
table_->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
// Only the Name column is editable (per-item flags); editing it renames the run's legend label.
|
||||
table_->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed);
|
||||
connect(table_, &QTableWidget::itemChanged, this, [this](QTableWidgetItem *item) {
|
||||
if (item->column() != COL_NAME)
|
||||
return;
|
||||
const int row = item->row();
|
||||
if (row < 0 || row >= static_cast<int>(jobs_.size()) || item->text() == jobs_[row].label)
|
||||
return;
|
||||
jobs_[row].label = item->text();
|
||||
emit renameRun(jobs_[row].id, jobs_[row].label);
|
||||
});
|
||||
|
||||
auto *message = new QLabel("Dataset re-processing is currently available only in File mode.\n\n"
|
||||
"Open a stored HDF5 file to run processing jobs.", this);
|
||||
@@ -210,16 +221,33 @@ void JFJochProcessingJobsWindow::newJob() {
|
||||
}
|
||||
|
||||
const bool full = spec.mode == ProcessMode::FullAnalysis;
|
||||
const QString name = QStringLiteral("%1 %2").arg(full ? "Full" : "AzInt").arg(++job_counter_);
|
||||
const int run_number = ++job_counter_;
|
||||
const QString label = QStringLiteral("%1 %2").arg(full ? "Full" : "AzInt").arg(run_number);
|
||||
const QString id = QStringLiteral("run-%1").arg(run_number);
|
||||
|
||||
JobInfo info;
|
||||
info.id = id;
|
||||
info.label = label;
|
||||
if (spec.save)
|
||||
info.snapshot_path = QString::fromStdString(config.output_prefix) + "_process.h5";
|
||||
|
||||
const int row = table_->rowCount();
|
||||
table_->insertRow(row);
|
||||
table_->setItem(row, COL_NAME, new QTableWidgetItem(name));
|
||||
table_->setItem(row, COL_MODE, new QTableWidgetItem(full ? "Full" : "AzInt"));
|
||||
table_->setItem(row, COL_IMAGES, new QTableWidgetItem(spec.images > 0 ? QString::number(spec.images) : "all"));
|
||||
table_->setItem(row, COL_STATUS, new QTableWidgetItem("queued"));
|
||||
table_->setItem(row, COL_INDEX, new QTableWidgetItem("-"));
|
||||
table_->setItem(row, COL_CELL, new QTableWidgetItem("-"));
|
||||
jobs_.push_back(info); // jobs_[row] must exist before setItem(COL_NAME) fires itemChanged
|
||||
|
||||
auto fixed = [](const QString &text) { // a non-editable cell
|
||||
auto *cell = new QTableWidgetItem(text);
|
||||
cell->setFlags(cell->flags() & ~Qt::ItemIsEditable);
|
||||
return cell;
|
||||
};
|
||||
|
||||
table_->setItem(row, COL_NAME, new QTableWidgetItem(label)); // editable (default flags)
|
||||
table_->setItem(row, COL_STARTED, fixed(QDateTime::currentDateTime().toString("HH:mm:ss")));
|
||||
table_->setItem(row, COL_MODE, fixed(full ? "Full" : "AzInt"));
|
||||
table_->setItem(row, COL_IMAGES, fixed(spec.images > 0 ? QString::number(spec.images) : "all"));
|
||||
table_->setItem(row, COL_STATUS, fixed("queued"));
|
||||
table_->setItem(row, COL_INDEX, fixed("-"));
|
||||
table_->setItem(row, COL_CELL, fixed("-"));
|
||||
|
||||
// A progress bar lives in the Status cell while the job runs; it shows the phase as text until
|
||||
// image processing starts, then "<done> / <expected>" with the bar filling in the background.
|
||||
@@ -230,15 +258,10 @@ void JFJochProcessingJobsWindow::newJob() {
|
||||
running_bar_->setFormat("queued");
|
||||
table_->setCellWidget(row, COL_STATUS, running_bar_);
|
||||
|
||||
JobInfo info;
|
||||
info.name = name;
|
||||
if (spec.save)
|
||||
info.snapshot_path = QString::fromStdString(config.output_prefix) + "_process.h5";
|
||||
jobs_.push_back(info);
|
||||
running_row_ = row;
|
||||
|
||||
controller_->start(inputs.file, experiment, inputs.pixel_mask, config);
|
||||
emit writeStatusBar("Started processing job " + name);
|
||||
emit writeStatusBar("Started processing job " + label);
|
||||
}
|
||||
|
||||
void JFJochProcessingJobsWindow::cancelJob() {
|
||||
@@ -256,7 +279,7 @@ void JFJochProcessingJobsWindow::viewResults() {
|
||||
QMessageBox::information(this, "Processing", "This job has no saved results to view.");
|
||||
return;
|
||||
}
|
||||
emit activateSnapshot(jobs_[row].name);
|
||||
emit activateSnapshot(jobs_[row].id);
|
||||
}
|
||||
|
||||
void JFJochProcessingJobsWindow::showOriginal() {
|
||||
@@ -287,6 +310,7 @@ void JFJochProcessingJobsWindow::onProgress(quint64 done, quint64 total) {
|
||||
}
|
||||
|
||||
void JFJochProcessingJobsWindow::onFinished(ProcessResult result) {
|
||||
emit liveDataset(nullptr); // the finished run takes over from the live overlay
|
||||
const int row = running_row_;
|
||||
running_row_ = -1;
|
||||
if (row >= 0)
|
||||
@@ -308,12 +332,13 @@ void JFJochProcessingJobsWindow::onFinished(ProcessResult result) {
|
||||
|
||||
if (!result.cancelled && result.written_master_path.has_value() && !jobs_[row].snapshot_path.isEmpty()) {
|
||||
jobs_[row].has_result = true;
|
||||
emit registerSnapshot(jobs_[row].name, jobs_[row].snapshot_path); // also activates it
|
||||
emit registerSnapshot(jobs_[row].id, jobs_[row].label, jobs_[row].snapshot_path); // also activates it
|
||||
}
|
||||
emit writeStatusBar(result.cancelled ? "Processing cancelled" : "Processing finished");
|
||||
}
|
||||
|
||||
void JFJochProcessingJobsWindow::onFailed(QString error) {
|
||||
emit liveDataset(nullptr); // clear the live overlay
|
||||
const int row = running_row_;
|
||||
running_row_ = -1;
|
||||
if (row >= 0)
|
||||
|
||||
@@ -26,10 +26,11 @@ public:
|
||||
explicit JFJochProcessingJobsWindow(JFJochImageReadingWorker *worker, QWidget *parent = nullptr);
|
||||
|
||||
signals:
|
||||
void registerSnapshot(QString name, QString master_path);
|
||||
void activateSnapshot(QString name);
|
||||
void registerSnapshot(QString id, QString label, QString master_path);
|
||||
void activateSnapshot(QString id);
|
||||
void renameRun(QString id, QString label);
|
||||
void writeStatusBar(QString message, int timeout_ms = 0);
|
||||
// Live per-image results while a job runs, for the dataset-info plots.
|
||||
// Live per-image results while a job runs, for the dataset-info plots (nullptr clears it).
|
||||
void liveDataset(std::shared_ptr<const JFJochReaderDataset> dataset);
|
||||
|
||||
public slots:
|
||||
@@ -47,7 +48,8 @@ private slots:
|
||||
|
||||
private:
|
||||
struct JobInfo {
|
||||
QString name;
|
||||
QString id; // stable reader-snapshot key
|
||||
QString label; // editable display name (legend / table)
|
||||
QString snapshot_path; // empty unless saved
|
||||
bool has_result = false;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user