Files
Jungfraujoch/viewer/windows/JFJochProcessingJobsWindow.cpp
T
leonarski_fandClaude Opus 4.8 5bfa70c4a6
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 13m40s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 13m50s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 14m48s
Build Packages / build:rpm (rocky8) (push) Successful in 14m55s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 15m0s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 15m11s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 15m27s
Build Packages / XDS test (durin plugin) (push) Successful in 9m9s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 9m18s
Build Packages / XDS test (neggia plugin) (push) Successful in 8m56s
Build Packages / Create release (push) Skipped
Build Packages / Generate python client (push) Successful in 29s
Build Packages / Build documentation (push) Successful in 1m8s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 11m6s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 12m50s
Build Packages / build:rpm (rocky9) (push) Successful in 13m23s
Build Packages / DIALS test (push) Successful in 13m13s
Build Packages / Unit tests (push) Successful in 1h28m58s
viewer: Processing jobs window - run jfjoch_process in-GUI + result snapshots
Makes the viewer a processing frontend, not just an image viewer:

- JFJochProcessingJobsWindow (menu "Processing"): a table of processing jobs on the open
  dataset. "New job" configures mode (full/azint), image count, threads, output, and (full)
  rotation indexing + scale/merge, using the viewer's current processing settings. A job can
  be Run locally (off the GUI thread via JFJochProcessController, with live status/progress and
  Cancel) or its jfjoch_process command line copied for a cluster.
- A finished local run is registered as a reader metadata snapshot and becomes the active
  dataset (bottom plots + per-image overlays follow it); "View results" / "Show original" switch
  between runs - so several settings attempts on one dataset can be compared.
- JFJochImageReadingWorker gains GetReprocessingInputs() (one locked read of file + experiment +
  mask + spot-finding settings, so jobs share the interactive settings) and
  RegisterProcessingSnapshot/SetActiveSnapshot slots + snapshotsChanged signal over the reader's
  snapshot API. Files only (not live HTTP).

First-cut / open ends: one job at a time; output lands in the working dir (FileWriter rejects
absolute prefixes); Bragg/scaling settings still use defaults until the converged settings
window lands. Viewer builds and runs (offscreen) cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 14:38:17 +02:00

271 lines
10 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "JFJochProcessingJobsWindow.h"
#include "../../process/JFJochProcessCommandLine.h"
#include <QApplication>
#include <QCheckBox>
#include <QClipboard>
#include <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QFileInfo>
#include <QFormLayout>
#include <QHeaderView>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QSpinBox>
#include <QTableWidget>
#include <QToolBar>
#include <QVBoxLayout>
#include <thread>
namespace {
enum Column { COL_NAME = 0, COL_MODE, COL_IMAGES, COL_STATUS, COL_INDEX, COL_CELL, COL_COUNT };
int default_threads() {
const unsigned hc = std::thread::hardware_concurrency();
return hc == 0 ? 4 : static_cast<int>(hc);
}
}
JFJochProcessingJobsWindow::JFJochProcessingJobsWindow(JFJochImageReadingWorker *worker, QWidget *parent)
: JFJochHelperWindow(parent), worker_(worker) {
setWindowTitle("Processing");
resize(720, 280);
controller_ = new JFJochProcessController(this);
connect(controller_, &JFJochProcessController::phaseChanged, this, &JFJochProcessingJobsWindow::onPhase);
connect(controller_, &JFJochProcessController::progress, this, &JFJochProcessingJobsWindow::onProgress);
connect(controller_, &JFJochProcessController::finished, this, &JFJochProcessingJobsWindow::onFinished);
connect(controller_, &JFJochProcessController::failed, this, &JFJochProcessingJobsWindow::onFailed);
auto *toolbar = addToolBar("Jobs");
toolbar->setMovable(false);
toolbar->addAction("New job…", this, &JFJochProcessingJobsWindow::newJob);
toolbar->addAction("Cancel", this, &JFJochProcessingJobsWindow::cancelJob);
toolbar->addSeparator();
toolbar->addAction("View results", this, &JFJochProcessingJobsWindow::viewResults);
toolbar->addAction("Show original", this, &JFJochProcessingJobsWindow::showOriginal);
table_ = new QTableWidget(0, COL_COUNT, this);
table_->setHorizontalHeaderLabels({"Name", "Mode", "Images", "Status", "Index %", "Unit cell"});
table_->horizontalHeader()->setStretchLastSection(true);
table_->setSelectionBehavior(QAbstractItemView::SelectRows);
table_->setSelectionMode(QAbstractItemView::SingleSelection);
table_->setEditTriggers(QAbstractItemView::NoEditTriggers);
setCentralWidget(table_);
}
int JFJochProcessingJobsWindow::askJob(const ReprocessingInputs &inputs, JobSpec &spec) {
QDialog dlg(this);
dlg.setWindowTitle("New processing job");
auto *mode = new QComboBox(&dlg);
mode->addItem("Full analysis", static_cast<int>(ProcessMode::FullAnalysis));
mode->addItem("Azimuthal integration", static_cast<int>(ProcessMode::AzimuthalIntegration));
auto *images = new QSpinBox(&dlg);
images->setRange(0, 1'000'000'000);
images->setValue(0);
images->setSpecialValueText("all");
auto *threads = new QSpinBox(&dlg);
threads->setRange(1, 256);
threads->setValue(default_threads());
auto *save = new QCheckBox("Save _process.h5", &dlg);
save->setChecked(true);
const QString stem = QFileInfo(inputs.file).completeBaseName().remove(QStringLiteral("_master"));
auto *prefix = new QLineEdit(QStringLiteral("%1_run%2").arg(stem).arg(job_counter_ + 1), &dlg);
auto *rotation = new QCheckBox("Rotation indexing (two-pass)", &dlg);
rotation->setChecked(inputs.experiment.GetGoniometer().has_value());
auto *scaling = new QCheckBox("Scale && merge", &dlg);
auto sync_full = [&] {
const bool full = mode->currentData().toInt() == static_cast<int>(ProcessMode::FullAnalysis);
rotation->setEnabled(full);
scaling->setEnabled(full);
};
connect(mode, &QComboBox::currentIndexChanged, &dlg, sync_full);
sync_full();
auto *form = new QFormLayout;
form->addRow("Mode", mode);
form->addRow("Images", images);
form->addRow("Threads", threads);
form->addRow(save);
form->addRow("Output prefix", prefix);
form->addRow(rotation);
form->addRow(scaling);
int result = 0;
auto *run = new QPushButton("Run locally", &dlg);
auto *copy = new QPushButton("Copy command", &dlg);
auto *cancel = new QPushButton("Cancel", &dlg);
run->setDefault(true);
connect(run, &QPushButton::clicked, &dlg, [&] { result = 1; dlg.accept(); });
connect(copy, &QPushButton::clicked, &dlg, [&] { result = 2; dlg.accept(); });
connect(cancel, &QPushButton::clicked, &dlg, [&] { result = 0; dlg.reject(); });
auto *buttons = new QHBoxLayout;
buttons->addStretch();
buttons->addWidget(run);
buttons->addWidget(copy);
buttons->addWidget(cancel);
auto *layout = new QVBoxLayout(&dlg);
layout->addLayout(form);
layout->addLayout(buttons);
dlg.exec();
if (result != 0) {
spec.mode = static_cast<ProcessMode>(mode->currentData().toInt());
spec.images = images->value();
spec.threads = threads->value();
spec.save = save->isChecked();
spec.prefix = prefix->text();
spec.rotation = rotation->isEnabled() && rotation->isChecked();
spec.scaling = scaling->isEnabled() && scaling->isChecked();
}
return result;
}
ProcessConfig JFJochProcessingJobsWindow::buildConfig(const JobSpec &spec, const ReprocessingInputs &inputs) const {
ProcessConfig config;
config.mode = spec.mode;
config.nthreads = spec.threads;
config.end_image = spec.images > 0 ? spec.images : -1;
config.output_prefix = spec.save ? spec.prefix.toStdString() : std::string();
config.spot_finding = inputs.spot_finding;
if (spec.mode == ProcessMode::FullAnalysis) {
config.rotation_indexing = spec.rotation;
config.two_pass_rotation = true;
config.run_scaling = spec.scaling;
}
return config;
}
void JFJochProcessingJobsWindow::newJob() {
const ReprocessingInputs inputs = worker_->GetReprocessingInputs();
if (!inputs.valid) {
QMessageBox::information(this, "Processing", "Open a file first (processing is not available for live HTTP data).");
return;
}
JobSpec spec;
const int action = askJob(inputs, spec);
if (action == 0)
return;
const ProcessConfig config = buildConfig(spec, inputs);
if (action == 2) { // copy command line
const QString cmd = QString::fromStdString(
JFJochProcessCommandLine(config, inputs.experiment, inputs.file.toStdString()));
QApplication::clipboard()->setText(cmd);
QMessageBox::information(this, "Command line", cmd + "\n\n(copied to clipboard)");
return;
}
if (controller_->running()) {
QMessageBox::information(this, "Processing", "A job is already running; wait for it to finish or cancel it.");
return;
}
const bool full = spec.mode == ProcessMode::FullAnalysis;
const QString name = QStringLiteral("%1 %2").arg(full ? "Full" : "AzInt").arg(++job_counter_);
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("-"));
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, inputs.experiment, inputs.pixel_mask, config);
emit writeStatusBar("Started processing job " + name);
}
void JFJochProcessingJobsWindow::cancelJob() {
if (controller_->running()) {
controller_->cancel();
emit writeStatusBar("Cancelling processing job…");
}
}
void JFJochProcessingJobsWindow::viewResults() {
const int row = table_->currentRow();
if (row < 0 || row >= static_cast<int>(jobs_.size()))
return;
if (!jobs_[row].has_result) {
QMessageBox::information(this, "Processing", "This job has no saved results to view.");
return;
}
emit activateSnapshot(jobs_[row].name);
}
void JFJochProcessingJobsWindow::showOriginal() {
emit activateSnapshot("Original");
}
void JFJochProcessingJobsWindow::setStatus(int row, const QString &text) {
if (row >= 0 && row < table_->rowCount())
table_->item(row, COL_STATUS)->setText(text);
}
void JFJochProcessingJobsWindow::onPhase(QString phase) {
if (running_row_ >= 0 && phase != "Processing images")
setStatus(running_row_, phase);
}
void JFJochProcessingJobsWindow::onProgress(quint64 done, quint64 total) {
if (running_row_ >= 0)
setStatus(running_row_, QStringLiteral("running %1%").arg(total ? done * 100 / total : 0));
}
void JFJochProcessingJobsWindow::onFinished(ProcessResult result) {
const int row = running_row_;
running_row_ = -1;
if (row < 0 || row >= static_cast<int>(jobs_.size()))
return;
setStatus(row, result.cancelled ? "cancelled" : "done");
if (result.indexing_rate.has_value())
table_->item(row, COL_INDEX)->setText(QStringLiteral("%1%").arg(result.indexing_rate.value() * 100.0, 0, 'f', 0));
if (result.consensus_cell.has_value()) {
const auto &c = result.consensus_cell.value();
table_->item(row, COL_CELL)->setText(
QStringLiteral("%1 %2 %3 %4 %5 %6")
.arg(c.a, 0, 'f', 1).arg(c.b, 0, 'f', 1).arg(c.c, 0, 'f', 1)
.arg(c.alpha, 0, 'f', 1).arg(c.beta, 0, 'f', 1).arg(c.gamma, 0, 'f', 1));
}
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 writeStatusBar(result.cancelled ? "Processing cancelled" : "Processing finished");
}
void JFJochProcessingJobsWindow::onFailed(QString error) {
const int row = running_row_;
running_row_ = -1;
setStatus(row, "failed");
QMessageBox::warning(this, "Processing failed", error);
}