rugnux finds the beam stop and its holder in a projection of 60 images and marks them in the pixel mask as bit 9 (--detect-beam-stop[=N|off], on by default). Reflections behind the stop are attenuated but not flagged, so they integrate low with a plausible sigma and nothing downstream catches them: the signal-box gate requires 100% valid pixels and shadow pixels are valid, the background clip is high-side only, and the |zeta| cut applies only to the space-group search merge. The detection compares each pixel's background against the typical background at the same radius on two channels. An azimuthal one (the ring median) finds the holder arm, which is a minority of its ring; a radial one (the background just outside) finds the disk, which the ring median cannot see because inside a fully blocked ring the median is the shadow itself. Pixels are pooled over a 5x5 box and tested only where the background has actually been counted, so low-background data no longer masks the whole detector. Recorded reflections are carved back out - a beam stop cannot block a reflection that was measured. Bit 9 belongs to the run that found it, not to the dataset: it is cleared when a run starts, so a mask read back from a file that carries one starts clear. The user mask (bit 8) is left alone. Scaling and merging gain a low-resolution limit, default 50 A (--scaling-low-resolution <num>, 0 removes it), applied per observation before scaling so it also protects the per-frame scale fit and the space-group search. 50 A is the value XDS configurations use; rugnux_vs_xds.py now matches both of XDS's resolution limits instead of only the high one, so the lowest shell is the same shell in the two programs. The viewer draws the detected shadow in coral with a "Show beam stop" switch in the side panel, exposes the low-resolution limit in the settings dock, and offers detection in its processing jobs. Adding an image marker meant giving the reader a MIN_REAL_PXL_VALUE, because several places classify a pixel by range rather than by equality and would otherwise read the new marker as a very negative intensity. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
124 lines
6.2 KiB
C++
124 lines
6.2 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#pragma once
|
|
|
|
#include <QElapsedTimer>
|
|
#include <QString>
|
|
#include <QWidget>
|
|
#include <cstdint>
|
|
#include <optional>
|
|
#include <vector>
|
|
|
|
#include "../JFJochProcessController.h"
|
|
#include "../JFJochImageReadingWorker.h" // ReprocessingInputs
|
|
#include "../widgets/PowderCalibrationWidget.h" // CalibrationSelection
|
|
|
|
class QTableWidget;
|
|
class QProgressBar;
|
|
class QStackedWidget;
|
|
class QToolBar;
|
|
class QToolButton;
|
|
|
|
// Makes processing a first-class GUI activity: a dockable panel with a table of processing jobs run
|
|
// on the current dataset. A job can be run locally (off the GUI thread, via JFJochProcessController)
|
|
// or its rugnux command line copied for a cluster. A finished local run is registered as a
|
|
// reader snapshot so its results become a selectable view of the dataset. Re-processing reads a
|
|
// stored HDF5 file, so the panel shows an explanatory message while connected to a live HTTP stream.
|
|
class JFJochProcessingJobsWindow : public QWidget {
|
|
Q_OBJECT
|
|
public:
|
|
explicit JFJochProcessingJobsWindow(JFJochImageReadingWorker *worker, QWidget *parent = nullptr);
|
|
|
|
signals:
|
|
void registerSnapshot(QString id, QString label, QString master_path);
|
|
void activateSnapshot(QString id);
|
|
void renameRun(QString id, QString label);
|
|
void removeRun(QString id);
|
|
void writeStatusBar(QString message, int timeout_ms = 0);
|
|
// Live per-image results while a job runs, for the dataset-info plots (nullptr clears it).
|
|
void liveDataset(std::shared_ptr<const JFJochReaderDataset> dataset);
|
|
void jobStarted(); // a reprocessing job was launched — reveal the (hidden-by-default) panel
|
|
|
|
public slots:
|
|
void onHttpConnectionChanged(bool connected, QString addr);
|
|
void clearJobs(); // reset the table on a new file (re-adds the Original row)
|
|
void setActiveRun(QString active_id); // bold the row of the run currently shown in the plots
|
|
// The panel's "Analyze dataset" action; the mode is the panel's selected page. The calibrant
|
|
// selection is only used by ProcessMode::Calibration.
|
|
void newJob(ProcessMode mode = ProcessMode::FullAnalysis, CalibrationSelection calibration = {});
|
|
|
|
private slots:
|
|
void cancelJob();
|
|
void removeResult();
|
|
void viewResults();
|
|
void onPhase(QString phase);
|
|
void onProgress(quint64 done, quint64 total);
|
|
void onFinished(ProcessResult result);
|
|
void onFailed(QString error);
|
|
|
|
private:
|
|
struct JobInfo {
|
|
QString id; // stable reader-snapshot key
|
|
QString label; // editable display name (legend / table)
|
|
QString snapshot_path; // empty unless saved
|
|
bool has_result = false;
|
|
// Merge statistics captured when a scaling/merging job finished (drive the analysis window).
|
|
bool has_merge_stats = false;
|
|
MergeStatistics merge_stats;
|
|
double isa = 0.0;
|
|
bool merge_has_reference = false;
|
|
TwinningAnalysisResult twinning; // twinning test of the merged intensities
|
|
std::optional<int64_t> space_group_number; // final space group (searched or fixed)
|
|
std::optional<SearchSpaceGroupResult> space_group_search; // ranked candidates, when a search ran
|
|
std::vector<std::pair<float, float>> merged_i_sigma; // ISa diagnostic scatter
|
|
// Detector calibration result (Calibration mode): the fit, the header geometry it is compared
|
|
// against, and where the .poni was written.
|
|
std::optional<CalibrationResult> calibration;
|
|
std::optional<DiffractionGeometry> calibration_header;
|
|
QString poni_path;
|
|
QToolButton *graph_btn = nullptr; // per-row "show statistics" button (enabled once stats exist)
|
|
};
|
|
struct JobSpec {
|
|
ProcessMode mode = ProcessMode::FullAnalysis;
|
|
int start_image = 0;
|
|
int end_image = 0; // 0 == to the end
|
|
int threads = 4;
|
|
bool save_h5 = true; // write the per-image _process.h5 (also drives the viewer snapshot)
|
|
bool save_merged = true; // write the merged .mtz/.cif
|
|
QString prefix;
|
|
bool rotation = false;
|
|
int rotation_images = 30; // images used to find the rotation lattice (first pass)
|
|
bool scaling = false;
|
|
bool refine_geometry = false; // stills-only global geometry bundle-adjust (needs a known cell)
|
|
int refine_geometry_frames = 200; // strong indexed frames fed to that bundle adjustment
|
|
bool rotation_postrefine = true; // rotation-only two-pass geometry post-refine (default on)
|
|
bool detect_beam_stop = true; // project frames and mask the beam-stop shadow (default on)
|
|
int detect_beam_stop_frames = 60; // frames projected to find it
|
|
CalibrationSelection calibration; // Calibration mode: calibrant rings + rings/spots method
|
|
};
|
|
|
|
// Returns 0 = cancel, 1 = run locally, 2 = copy command line; fills spec from the dialog.
|
|
int askJob(const ReprocessingInputs &inputs, JobSpec &spec);
|
|
ProcessConfig buildConfig(const JobSpec &spec, const ReprocessingInputs &inputs) const;
|
|
void setStatus(int row, const QString &text);
|
|
void addOriginalRow(); // the file's own data as the first, always-present run
|
|
void addRowActions(int row, const QString &id); // per-row graph (stats) + trash (remove) buttons
|
|
void showStats(const QString &id); // open the merge-statistics window for a run
|
|
void removeRunById(const QString &id); // remove a run from the table + reader
|
|
|
|
JFJochImageReadingWorker *worker_;
|
|
JFJochProcessController *controller_;
|
|
// The experiment a running calibration job was launched with: WritePoniFile needs the detector
|
|
// size and wavelength from it once the fit comes back.
|
|
DiffractionExperiment calibration_experiment_;
|
|
QToolBar *toolbar_;
|
|
QStackedWidget *stack_; // page 0: jobs table, page 1: HTTP-mode message
|
|
QTableWidget *table_;
|
|
QProgressBar *running_bar_ = nullptr; // lives in the running row's Status cell
|
|
QElapsedTimer job_timer_; // running job wall-clock, for rate + ETA
|
|
int running_row_ = -1;
|
|
int job_counter_ = 0;
|
|
std::vector<JobInfo> jobs_;
|
|
};
|