diff --git a/process/CMakeLists.txt b/process/CMakeLists.txt index 4ebcbac7..17c11c0c 100644 --- a/process/CMakeLists.txt +++ b/process/CMakeLists.txt @@ -4,6 +4,8 @@ ADD_LIBRARY(JFJochProcess STATIC JFJochProcess.cpp JFJochProcess.h + JFJochProcessCommandLine.cpp + JFJochProcessCommandLine.h ) TARGET_LINK_LIBRARIES(JFJochProcess JFJochReader JFJochImageAnalysis JFJochWriter) diff --git a/process/JFJochProcessCommandLine.cpp b/process/JFJochProcessCommandLine.cpp new file mode 100644 index 00000000..50586ae3 --- /dev/null +++ b/process/JFJochProcessCommandLine.cpp @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#include "JFJochProcessCommandLine.h" +#include "../common/DiffractionExperiment.h" + +#include +#include + +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 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(); +} diff --git a/process/JFJochProcessCommandLine.h b/process/JFJochProcessCommandLine.h new file mode 100644 index 00000000..da0ad3e8 --- /dev/null +++ b/process/JFJochProcessCommandLine.h @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include + +#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); diff --git a/tests/JFJochProcessTest.cpp b/tests/JFJochProcessTest.cpp index 5e744016..bf25799d 100644 --- a/tests/JFJochProcessTest.cpp +++ b/tests/JFJochProcessTest.cpp @@ -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); +} diff --git a/viewer/CMakeLists.txt b/viewer/CMakeLists.txt index cab9d63c..0ecc75d1 100644 --- a/viewer/CMakeLists.txt +++ b/viewer/CMakeLists.txt @@ -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 diff --git a/viewer/JFJochProcessController.cpp b/viewer/JFJochProcessController.cpp new file mode 100644 index 00000000..ee3e1bc1 --- /dev/null +++ b/viewer/JFJochProcessController.cpp @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#include "JFJochProcessController.h" +#include "../reader/JFJochHDF5Reader.h" + +#include + +JFJochProcessController::JFJochProcessController(QObject *parent) : QObject(parent) { + qRegisterMetaType("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); +} diff --git a/viewer/JFJochProcessController.h b/viewer/JFJochProcessController.h new file mode 100644 index 00000000..50e0f16e --- /dev/null +++ b/viewer/JFJochProcessController.h @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include +#include + +#include +#include + +#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 active_{nullptr}; + std::atomic running_{false}; + std::atomic cancel_pending_{false}; +};