process/viewer: command-line generator + JFJochProcessController (GUI engine)
Foundation for making processing a first-class GUI activity: - process/JFJochProcessCommandLine: reconstruct the equivalent jfjoch_process / jfjoch_azint command line from a ProcessConfig + DiffractionExperiment + input path, for handing a job off to a cluster. Unit-tested (full + azint). - viewer/JFJochProcessController: runs one JFJochProcess job off the GUI thread (its own private reader; HDF5 access is globally serialized so it is safe next to the interactive reader) and reports back via queued Qt signals (started/phaseChanged/progress/finished/failed). Cancel() forwards to JFJochProcess::Cancel(). Progress is throttled to ~200 updates per run. The visible processing UI (jobs window + snapshot switcher + converged settings) builds on this. Verified: tests/jfjoch_test [process]; jfjoch_viewer links and builds against JFJochProcess. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
ADD_LIBRARY(JFJochProcess STATIC
|
||||
JFJochProcess.cpp
|
||||
JFJochProcess.h
|
||||
JFJochProcessCommandLine.cpp
|
||||
JFJochProcessCommandLine.h
|
||||
)
|
||||
|
||||
TARGET_LINK_LIBRARIES(JFJochProcess JFJochReader JFJochImageAnalysis JFJochWriter)
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "JFJochProcessCommandLine.h"
|
||||
#include "../common/DiffractionExperiment.h"
|
||||
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
std::string quote_if_needed(const std::string &s) {
|
||||
if (s.find_first_of(" \t\"'") == std::string::npos)
|
||||
return s;
|
||||
std::string out = "\"";
|
||||
for (char c: s) {
|
||||
if (c == '"' || c == '\\')
|
||||
out += '\\';
|
||||
out += c;
|
||||
}
|
||||
out += '"';
|
||||
return out;
|
||||
}
|
||||
|
||||
const char *indexing_alg_flag(IndexingAlgorithmEnum a) {
|
||||
switch (a) {
|
||||
case IndexingAlgorithmEnum::FFBIDX: return "ffbidx";
|
||||
case IndexingAlgorithmEnum::FFT: return "fft";
|
||||
case IndexingAlgorithmEnum::FFTW: return "fftw";
|
||||
case IndexingAlgorithmEnum::None: return "none";
|
||||
case IndexingAlgorithmEnum::Auto:
|
||||
default: return "auto";
|
||||
}
|
||||
}
|
||||
|
||||
const char *refine_flag(GeomRefinementAlgorithmEnum r) {
|
||||
switch (r) {
|
||||
case GeomRefinementAlgorithmEnum::None: return "none";
|
||||
case GeomRefinementAlgorithmEnum::OrientationOnly: return "orientation";
|
||||
case GeomRefinementAlgorithmEnum::PixelRefine: return "pixelrefine";
|
||||
case GeomRefinementAlgorithmEnum::BeamCenter:
|
||||
default: return "beam_and_lattice";
|
||||
}
|
||||
}
|
||||
|
||||
std::string num(double v) {
|
||||
std::ostringstream o;
|
||||
o << v;
|
||||
return o.str();
|
||||
}
|
||||
}
|
||||
|
||||
std::string JFJochProcessCommandLine(const ProcessConfig &config,
|
||||
const DiffractionExperiment &experiment,
|
||||
const std::string &input_file) {
|
||||
std::vector<std::string> args;
|
||||
const bool azint = (config.mode == ProcessMode::AzimuthalIntegration);
|
||||
args.emplace_back(azint ? "jfjoch_azint" : "jfjoch_process");
|
||||
|
||||
auto add = [&](const std::string &flag, const std::string &val) {
|
||||
args.push_back(flag);
|
||||
args.push_back(val);
|
||||
};
|
||||
|
||||
if (!config.output_prefix.empty())
|
||||
add("-o", config.output_prefix);
|
||||
add("-N", std::to_string(config.nthreads));
|
||||
if (config.start_image != 0)
|
||||
add("-s", std::to_string(config.start_image));
|
||||
if (config.end_image >= 0)
|
||||
add("-e", std::to_string(config.end_image));
|
||||
if (config.stride != 1)
|
||||
add("-t", std::to_string(config.stride));
|
||||
|
||||
if (azint) {
|
||||
const auto a = experiment.GetAzimuthalIntegrationSettings();
|
||||
add("--min-q", num(a.GetLowQ_recipA()));
|
||||
add("--max-q", num(a.GetHighQ_recipA()));
|
||||
add("--q-spacing", num(a.GetQSpacing_recipA()));
|
||||
add("--azimuthal-bins", std::to_string(a.GetAzimuthalBinCount()));
|
||||
add("--polarization-correction", a.IsPolarizationCorrection() ? "on" : "off");
|
||||
add("--solid-angle-correction", a.IsSolidAngleCorrection() ? "on" : "off");
|
||||
} else {
|
||||
const auto &sf = config.spot_finding;
|
||||
add("--spot-sigma", num(sf.signal_to_noise_threshold));
|
||||
add("--spot-threshold", std::to_string(sf.photon_count_threshold));
|
||||
add("--spot-high-resolution", num(sf.high_resolution_limit));
|
||||
add("--max-spots", std::to_string(experiment.GetMaxSpotCount()));
|
||||
|
||||
const auto idx = experiment.GetIndexingSettings();
|
||||
add("-X", indexing_alg_flag(idx.GetAlgorithm()));
|
||||
add("-r", refine_flag(idx.GetGeomRefinementAlgorithm()));
|
||||
|
||||
if (const auto sg = experiment.GetSpaceGroupNumber())
|
||||
add("-S", std::to_string(*sg));
|
||||
if (const auto uc = experiment.GetUnitCell()) {
|
||||
std::ostringstream o;
|
||||
o << uc->a << "," << uc->b << "," << uc->c << "," << uc->alpha << "," << uc->beta << "," << uc->gamma;
|
||||
add("-C", o.str());
|
||||
}
|
||||
if (const auto bw = experiment.GetBandwidthFWHM())
|
||||
add("--bandwidth", num(*bw));
|
||||
|
||||
const auto bragg = experiment.GetBraggIntegrationSettings();
|
||||
std::ostringstream radii;
|
||||
radii << bragg.GetR1() << "," << bragg.GetR2() << "," << bragg.GetR3();
|
||||
add("--integration-radius", radii.str());
|
||||
|
||||
if (config.rotation_indexing) {
|
||||
if (config.two_pass_rotation)
|
||||
add("-R", std::to_string(config.rotation_indexing_image_count));
|
||||
else
|
||||
args.emplace_back("--single-pass-rotation");
|
||||
if (!config.reuse_rotation_spots)
|
||||
args.emplace_back("--redo-rotation-spots");
|
||||
}
|
||||
|
||||
if (config.run_scaling) {
|
||||
args.emplace_back("-M");
|
||||
const auto sc = experiment.GetScalingSettings();
|
||||
if (const auto pm = sc.GetPartialityModel())
|
||||
add("-P", *pm == PartialityModel::Unity ? "unity" : *pm == PartialityModel::Rotation ? "rot" : "fixed");
|
||||
if (!sc.GetMergeFriedel())
|
||||
args.emplace_back("-A");
|
||||
if (sc.GetRefineB())
|
||||
args.emplace_back("-B");
|
||||
}
|
||||
}
|
||||
|
||||
args.push_back(input_file);
|
||||
|
||||
std::ostringstream cmd;
|
||||
for (size_t i = 0; i < args.size(); i++) {
|
||||
if (i)
|
||||
cmd << ' ';
|
||||
cmd << quote_if_needed(args[i]);
|
||||
}
|
||||
return cmd.str();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "JFJochProcess.h" // ProcessConfig, ProcessMode
|
||||
|
||||
class DiffractionExperiment;
|
||||
|
||||
// Reconstruct an equivalent jfjoch_process / jfjoch_azint command line for a configured run, so a
|
||||
// job set up in the GUI can be handed off to a cluster. Covers the settings that matter for the
|
||||
// run, not every obscure flag; geometry is taken from the input file, so geometry overrides are
|
||||
// not emitted.
|
||||
std::string JFJochProcessCommandLine(const ProcessConfig &config,
|
||||
const DiffractionExperiment &experiment,
|
||||
const std::string &input_file);
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "../writer/FileWriter.h"
|
||||
#include "../reader/JFJochHDF5Reader.h"
|
||||
#include "../process/JFJochProcess.h"
|
||||
#include "../process/JFJochProcessCommandLine.h"
|
||||
|
||||
namespace {
|
||||
// Write a small VDS dataset of `n` flat images and return nothing (prefix_master.h5 +
|
||||
@@ -131,3 +132,50 @@ TEST_CASE("JFJochProcess_Cancel", "[HDF5][Full]") {
|
||||
remove("process_cancel_in_data_000001.h5");
|
||||
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("JFJochProcessCommandLine_Full", "[process]") {
|
||||
DiffractionExperiment x(DetJF(1));
|
||||
IndexingSettings idx;
|
||||
idx.Algorithm(IndexingAlgorithmEnum::FFT);
|
||||
idx.GeomRefinementAlgorithm(GeomRefinementAlgorithmEnum::BeamCenter);
|
||||
x.ImportIndexingSettings(idx);
|
||||
x.SpaceGroupNumber(96);
|
||||
|
||||
ProcessConfig config;
|
||||
config.mode = ProcessMode::FullAnalysis;
|
||||
config.nthreads = 8;
|
||||
config.output_prefix = "run1";
|
||||
config.end_image = 500;
|
||||
config.rotation_indexing = true;
|
||||
config.two_pass_rotation = true;
|
||||
config.rotation_indexing_image_count = 30;
|
||||
config.spot_finding = DiffractionExperiment::DefaultDataProcessingSettings();
|
||||
|
||||
const std::string cmd = JFJochProcessCommandLine(config, x, "/data/lyso_master.h5");
|
||||
CHECK(cmd.rfind("jfjoch_process", 0) == 0);
|
||||
CHECK(cmd.find("-N 8") != std::string::npos);
|
||||
CHECK(cmd.find("-e 500") != std::string::npos);
|
||||
CHECK(cmd.find("-o run1") != std::string::npos);
|
||||
CHECK(cmd.find("-X fft") != std::string::npos);
|
||||
CHECK(cmd.find("-S 96") != std::string::npos);
|
||||
CHECK(cmd.find("-R 30") != std::string::npos);
|
||||
CHECK(cmd.find("/data/lyso_master.h5") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("JFJochProcessCommandLine_AzInt", "[process]") {
|
||||
DiffractionExperiment x(DetJF(1));
|
||||
AzimuthalIntegrationSettings a;
|
||||
a.AzimuthalBinCount(4);
|
||||
x.ImportAzimuthalIntegrationSettings(a);
|
||||
|
||||
ProcessConfig config;
|
||||
config.mode = ProcessMode::AzimuthalIntegration;
|
||||
config.nthreads = 2;
|
||||
config.output_prefix = "az";
|
||||
|
||||
const std::string cmd = JFJochProcessCommandLine(config, x, "in.h5");
|
||||
CHECK(cmd.rfind("jfjoch_azint", 0) == 0);
|
||||
CHECK(cmd.find("--azimuthal-bins 4") != std::string::npos);
|
||||
CHECK(cmd.find("--min-q") != std::string::npos);
|
||||
CHECK(cmd.find("in.h5") != std::string::npos);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ ADD_EXECUTABLE(jfjoch_viewer jfjoch_viewer.cpp JFJochViewerWindow.cpp JFJochView
|
||||
image_viewer/JFJochSimpleImage.h
|
||||
JFJochImageReadingWorker.cpp
|
||||
JFJochImageReadingWorker.h
|
||||
JFJochProcessController.cpp
|
||||
JFJochProcessController.h
|
||||
JFJochViewerDatasetInfo.cpp
|
||||
JFJochViewerDatasetInfo.h
|
||||
JFJochViewerSidePanel.cpp
|
||||
@@ -99,7 +101,7 @@ ADD_EXECUTABLE(jfjoch_viewer jfjoch_viewer.cpp JFJochViewerWindow.cpp JFJochView
|
||||
|
||||
TARGET_LINK_LIBRARIES(jfjoch_viewer Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Charts Qt6::Concurrent
|
||||
Qt6::OpenGL Qt6::OpenGLWidgets
|
||||
JFJochReader JFJochLogger JFJochCommon JFJochWriter JFJochImageAnalysis
|
||||
JFJochReader JFJochLogger JFJochCommon JFJochWriter JFJochImageAnalysis JFJochProcess
|
||||
fftw3f)
|
||||
|
||||
# Native GUI app per platform: a .app bundle on macOS, GUI subsystem (no console window) on
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#include "JFJochProcessController.h"
|
||||
#include "../reader/JFJochHDF5Reader.h"
|
||||
|
||||
#include <QMetaType>
|
||||
|
||||
JFJochProcessController::JFJochProcessController(QObject *parent) : QObject(parent) {
|
||||
qRegisterMetaType<ProcessResult>("ProcessResult");
|
||||
}
|
||||
|
||||
JFJochProcessController::~JFJochProcessController() {
|
||||
cancel();
|
||||
joinWorker_();
|
||||
}
|
||||
|
||||
void JFJochProcessController::joinWorker_() {
|
||||
if (worker_.joinable())
|
||||
worker_.join();
|
||||
}
|
||||
|
||||
void JFJochProcessController::start(const QString &file_path, DiffractionExperiment experiment,
|
||||
PixelMask pixel_mask, ProcessConfig config) {
|
||||
if (running_.exchange(true))
|
||||
return; // a job is already running
|
||||
|
||||
cancel_pending_ = false;
|
||||
joinWorker_(); // reap the previous (finished) worker, if any
|
||||
worker_ = std::thread(&JFJochProcessController::run_, this,
|
||||
file_path, std::move(experiment), std::move(pixel_mask), std::move(config));
|
||||
emit started();
|
||||
}
|
||||
|
||||
void JFJochProcessController::cancel() {
|
||||
cancel_pending_ = true;
|
||||
if (auto *p = active_.load())
|
||||
p->Cancel();
|
||||
}
|
||||
|
||||
void JFJochProcessController::run_(QString file_path, DiffractionExperiment experiment,
|
||||
PixelMask pixel_mask, ProcessConfig config) {
|
||||
try {
|
||||
JFJochHDF5Reader reader;
|
||||
reader.ReadFile(file_path.toStdString());
|
||||
|
||||
JFJochProcess process(reader, std::move(experiment), std::move(pixel_mask), std::move(config));
|
||||
active_ = &process;
|
||||
if (cancel_pending_)
|
||||
process.Cancel();
|
||||
|
||||
ProcessResult result = process.Run(this);
|
||||
|
||||
active_ = nullptr;
|
||||
running_ = false;
|
||||
emit finished(result);
|
||||
} catch (const std::exception &e) {
|
||||
active_ = nullptr;
|
||||
running_ = false;
|
||||
emit failed(QString::fromStdString(e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void JFJochProcessController::OnPhase(const std::string &phase) {
|
||||
emit phaseChanged(QString::fromStdString(phase));
|
||||
}
|
||||
|
||||
void JFJochProcessController::OnProgress(uint64_t done, uint64_t total) {
|
||||
// Throttle to ~200 updates so a long run does not flood the GUI event queue.
|
||||
const uint64_t step = total > 200 ? total / 200 : 1;
|
||||
if (done == total || done % step == 0)
|
||||
emit progress(done, total);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
|
||||
#include "../process/JFJochProcess.h"
|
||||
#include "../common/DiffractionExperiment.h"
|
||||
#include "../common/PixelMask.h"
|
||||
|
||||
Q_DECLARE_METATYPE(ProcessResult)
|
||||
|
||||
// Runs one JFJochProcess 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
|
||||
// is safe alongside the interactive reader), so the viewer becomes a processing frontend without
|
||||
// blocking the UI. Cancel() is forwarded to JFJochProcess::Cancel() (atomic) and works from any
|
||||
// thread / at any point of the run.
|
||||
class JFJochProcessController : public QObject, private JFJochProcessObserver {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit JFJochProcessController(QObject *parent = nullptr);
|
||||
~JFJochProcessController() override;
|
||||
|
||||
bool running() const { return running_; }
|
||||
|
||||
public slots:
|
||||
// Start a job over `file_path` with the given (fully configured) experiment + mask + config.
|
||||
// Ignored if a job is already running.
|
||||
void start(const QString &file_path, DiffractionExperiment experiment,
|
||||
PixelMask pixel_mask, ProcessConfig config);
|
||||
void cancel();
|
||||
|
||||
signals:
|
||||
void started();
|
||||
void phaseChanged(QString phase);
|
||||
void progress(quint64 done, quint64 total);
|
||||
void finished(ProcessResult result);
|
||||
void failed(QString error);
|
||||
|
||||
private:
|
||||
// JFJochProcessObserver - called from worker threads, forwarded as queued signals.
|
||||
void OnPhase(const std::string &phase) override;
|
||||
void OnProgress(uint64_t done, uint64_t total) override;
|
||||
|
||||
void run_(QString file_path, DiffractionExperiment experiment, PixelMask pixel_mask, ProcessConfig config);
|
||||
void joinWorker_();
|
||||
|
||||
std::thread worker_;
|
||||
std::atomic<JFJochProcess *> active_{nullptr};
|
||||
std::atomic<bool> running_{false};
|
||||
std::atomic<bool> cancel_pending_{false};
|
||||
};
|
||||
Reference in New Issue
Block a user