Build Packages / build:viewer-tgz:cpu (push) Successful in 19m32s
Build Packages / build:windows:nocuda (push) Successful in 19m57s
Build Packages / build:viewer-tgz:cuda (push) Successful in 22m45s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 22m38s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 23m24s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 28m8s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 28m9s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 28m18s
Build Packages / XDS test (durin plugin) (push) Successful in 11m10s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 20m21s
Build Packages / build:windows:cuda (push) Successful in 22m5s
Build Packages / build:rpm (rocky9) (push) Successful in 20m57s
Build Packages / Generate python client (push) Successful in 34s
Build Packages / Build documentation (push) Successful in 1m29s
Build Packages / Create release (push) Skipped
Build Packages / build:rpm (ubuntu2204) (push) Successful in 25m41s
Build Packages / DIALS test (push) Successful in 21m19s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 21m34s
Build Packages / build:rpm (rocky8) (push) Successful in 27m4s
Build Packages / XDS test (neggia plugin) (push) Successful in 10m19s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 10m58s
Build Packages / Unit tests (push) Successful in 1h17m36s
Image buffer: the per-image CBOR metadata headroom had been re-derived from the online reflection cap alone, which cut it from 4 MiB to 2.55 MB while the measured worst case - reflections plus the capped spot list plus the three azimuthal arrays - is 2.9 MB, so the receiver dropped the frames with the most to say. Restore it and give it a name that both the code and its guard test read: written down twice, the two had drifted and the test kept passing against the value the code had left. Spot finding: an unset low_resolution_limit means no limit at that end, as an unset high_resolution_limit already did. An optional rather than a zero sentinel, because zero is not a natural "no limit" here - every pixel lies above it, so the plain comparison masked the whole image instead of none of it, and nothing validated the zero. The API field is no longer required; a zero is folded into the unset case at the boundary, where older clients still send it, so one spelling reaches the analysis code. The FPGA takes its fixed-point ceiling instead, since ap_ufixed<16,9> wraps above 512 A and would have masked everything. image_preprocessing: check the CUDA calls on the fused decode path - the one new GPU file with none, and the path fed by bytes we did not produce. An unchecked synchronise returned the host-written sentinel as if it were a measurement, so the decode looked successful and the fallback to the host decoder never fired. rugnux: --stride no longer writes one past the end of the per-image arrays, whose count floored where the worker loop ceils, and the written process file links the images actually processed rather than the first N - each frame's picture now sits next to its own analysis. Powder calibration: the face-centred calibrants no longer list their systematically absent rings, so the distance fit starts from a reflection that exists rather than an extinct one; the triclinic calibrant covers both signs of h and k instead of a single octant, which is only valid for a diagonal metric. The test asserted the old behaviour - one ring formula for every cubic standard - and is rewritten. CBOR: skip an unknown tagged value in the end block, as the other four blocks already do. One advance lands on the tagged item rather than past it, so an older reader fed a newer end message threw and never finalized its file. Viewer: a settings value the setter rejects no longer escapes as an uncaught throw from a worker slot, and the field offers only what the setter accepts. Space-group search: judge stage B on the same "present" cut stage A already computes. Merged sigma is floored so no reflection reads above ISa, so on a low-ISa merge the fixed cut left both stage B tests unsatisfiable - every screw axis passed unchallenged and the centering rescue switched itself off on exactly the weak data it exists for. Where the fixed cut is the smaller of the two they are equal and this is inert: over the 37-crystal rotation battery every crystal reports the identical space group and identical merge statistics, so it is a no-op there and the low-ISa case it targets remains unmeasured. rugnux: --polarization reaches --mode azint, which parsed the flag and then dropped it; that mode also applies the same polarization default as every other mode. Acknowledge the ACTS/traccc project, whose sparse connected-component labelling both spot extractors take their algorithm from, with its citation and its license. The rc.161 change list is brought back to one line per entry, and the user-visible changes that were missing from it added. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
771 lines
38 KiB
C++
771 lines
38 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||
// SPDX-License-Identifier: GPL-3.0-only
|
||
|
||
#include "JFJochViewerSettingsDock.h"
|
||
|
||
#include <QVBoxLayout>
|
||
#include <QHBoxLayout>
|
||
#include <QFormLayout>
|
||
#include <QPushButton>
|
||
#include <QButtonGroup>
|
||
#include <QStackedWidget>
|
||
#include <QCheckBox>
|
||
#include <QComboBox>
|
||
#include <QLabel>
|
||
#include <QSignalBlocker>
|
||
#include <QFileDialog>
|
||
#include <QIcon>
|
||
#include <QPixmap>
|
||
#include <QPainter>
|
||
|
||
#include <cmath>
|
||
|
||
#include "../../common/Definitions.h" // MAX_SPOT_COUNT
|
||
#include "SliderPlusBox.h"
|
||
#include "NumberLineEdit.h"
|
||
#include "PowderCalibrationWidget.h"
|
||
#include "CollapsibleSection.h"
|
||
#include "../../common/JFJochMath.h"
|
||
#include "../../common/CUDAWrapper.h"
|
||
#include "../../gemmi_gph/gemmi/symmetry.hpp"
|
||
|
||
namespace {
|
||
// A single diffraction frame (image) vs a stack of frames (dataset), drawn white so they read on
|
||
// the navy "Analyze" hero buttons.
|
||
QIcon FramesIcon(int frames) {
|
||
const int S = 28;
|
||
QPixmap pm(S, S);
|
||
pm.fill(Qt::transparent);
|
||
QPainter p(&pm);
|
||
p.setRenderHint(QPainter::Antialiasing);
|
||
p.setPen(QPen(Qt::white, 2.0));
|
||
p.setBrush(Qt::NoBrush);
|
||
const double side = 13.0, step = 4.0;
|
||
for (int i = frames - 1; i >= 0; --i)
|
||
p.drawRoundedRect(QRectF(4 + i * step, 4 + i * step, side, side), 2.5, 2.5);
|
||
p.setBrush(Qt::white); // a diffraction "spot" in the front frame
|
||
p.drawEllipse(QPointF(4 + side / 2.0, 4 + side / 2.0), 2.0, 2.0);
|
||
p.end();
|
||
return QIcon(pm);
|
||
}
|
||
}
|
||
|
||
JFJochViewerSettingsDock::JFJochViewerSettingsDock(const SpotFindingSettings &spot,
|
||
const IndexingSettings &indexing,
|
||
const AzimuthalIntegrationSettings &azint,
|
||
const BraggIntegrationSettings &bragg,
|
||
const ScalingSettings &scaling,
|
||
QWidget *parent)
|
||
: QWidget(parent), spot_(spot), indexing_(indexing), azint_(azint), bragg_(bragg), scaling_(scaling),
|
||
adaptive_min_pix_(!spot.min_pix_per_spot.has_value()),
|
||
min_pix_value_(spot.min_pix_per_spot.value_or(2)) {
|
||
|
||
auto *layout = new QVBoxLayout(this);
|
||
|
||
// The two analysis actions sit on top of the panel. "Analyze image" is a toggle (re-analyse the
|
||
// current frame now and on every change while armed); "Analyze dataset" launches a processing job
|
||
// whose kind (MX vs azimuthal) is decided by the MX/AzInt toggle below — no separate switch.
|
||
const QString heroStyle =
|
||
"QPushButton { background-color:#1F3A5F; color:white; border:none; border-radius:3px;"
|
||
" padding:5px 10px; } QPushButton:hover { background-color:#16314f; }"
|
||
" QPushButton:checked { background-color:#FA7268; } QPushButton:disabled { background-color:#9aa6b3; }";
|
||
auto *analyzeImageBtn = new QPushButton(FramesIcon(1), " Analyze image", this);
|
||
analyzeImageBtn->setCheckable(true);
|
||
analyzeImageBtn->setStyleSheet(heroStyle);
|
||
analyzeImageBtn->setToolTip("Re-analyse the current image now, and keep re-analysing on every"
|
||
" image / settings change while active");
|
||
analyzeDatasetBtn_ = new QPushButton(FramesIcon(3), " Analyze dataset", this);
|
||
analyzeDatasetBtn_->setStyleSheet(heroStyle);
|
||
analyzeDatasetBtn_->setToolTip("Process the whole dataset (MX, azimuthal or calibration, per the toggle below)");
|
||
auto *analyzeRow = new QHBoxLayout();
|
||
analyzeRow->addWidget(analyzeImageBtn);
|
||
analyzeRow->addWidget(analyzeDatasetBtn_);
|
||
layout->addLayout(analyzeRow);
|
||
layout->addSpacing(10);
|
||
connect(analyzeImageBtn, &QPushButton::toggled, this, &JFJochViewerSettingsDock::reanalyzeImage);
|
||
connect(analyzeDatasetBtn_, &QPushButton::clicked, this,
|
||
[this] { emit analyzeDataset(mode_, powder_->Selection()); });
|
||
|
||
// Segmented MX / AzInt / Calib toggle: each community picks its page; pages never share a screen.
|
||
// The page is also the mode "Analyze dataset" runs, so there is one control, not two.
|
||
auto *mxButton = new QPushButton("MX", this);
|
||
auto *azButton = new QPushButton("AzInt", this);
|
||
auto *calibButton = new QPushButton("Calib", this);
|
||
for (auto *b : {mxButton, azButton, calibButton}) {
|
||
b->setCheckable(true);
|
||
b->setStyleSheet("QPushButton:checked { background-color: #1F3A5F; color: white; }");
|
||
}
|
||
mxButton->setChecked(true);
|
||
auto *group = new QButtonGroup(this);
|
||
group->setExclusive(true);
|
||
group->addButton(mxButton, 0);
|
||
group->addButton(azButton, 1);
|
||
group->addButton(calibButton, 2);
|
||
|
||
auto *toggleRow = new QHBoxLayout();
|
||
toggleRow->setSpacing(0);
|
||
toggleRow->addWidget(mxButton);
|
||
toggleRow->addWidget(azButton);
|
||
toggleRow->addWidget(calibButton);
|
||
|
||
auto *stack = new QStackedWidget(this);
|
||
stack->addWidget(BuildMXPage());
|
||
stack->addWidget(BuildAzIntPage());
|
||
stack->addWidget(BuildCalibrationPage());
|
||
connect(group, &QButtonGroup::idClicked, this, [this, stack](int id) {
|
||
mode_ = id == 1 ? ProcessMode::AzimuthalIntegration
|
||
: id == 2 ? ProcessMode::Calibration : ProcessMode::FullAnalysis;
|
||
stack->setCurrentIndex(id);
|
||
});
|
||
|
||
// Geometry is common to both communities, so it lives above the toggle rather than per page.
|
||
layout->addLayout(toggleRow);
|
||
layout->addWidget(BuildGeometrySection());
|
||
layout->addWidget(stack);
|
||
layout->addStretch();
|
||
}
|
||
|
||
void JFJochViewerSettingsDock::setHttpConnection(bool connected, QString) {
|
||
analyzeDatasetBtn_->setEnabled(!connected);
|
||
analyzeDatasetBtn_->setToolTip(connected
|
||
? "Dataset re-processing is only available for an open file, not a live HTTP stream"
|
||
: "Process the whole dataset (MX, azimuthal or calibration, per the toggle below)");
|
||
}
|
||
|
||
QWidget *JFJochViewerSettingsDock::BuildGeometrySection() {
|
||
auto *section = new CollapsibleSection("Geometry", this);
|
||
auto *geom = new QFormLayout();
|
||
geom->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); // fields fill the panel width
|
||
|
||
energy_ = new NumberLineEdit(1.0, 200.0, 12.4, 4, "keV", this);
|
||
distance_ = new NumberLineEdit(10.0, 5000.0, 100.0, 2, "mm", this);
|
||
beamX_ = new NumberLineEdit(-20000.0, 20000.0, 0.0, 1, "px", this);
|
||
beamY_ = new NumberLineEdit(-20000.0, 20000.0, 0.0, 1, "px", this);
|
||
rot1_ = new NumberLineEdit(-180.0, 180.0, 0.0, 3, "°", this);
|
||
rot2_ = new NumberLineEdit(-180.0, 180.0, 0.0, 3, "°", this);
|
||
|
||
// The detector origin is the PONI (PyFAI) point; in MX (XDS-style) terms it is the beam origin,
|
||
// which coincides with the beam center only when the detector is untilted.
|
||
const QString beamTip =
|
||
"Beam origin (XDS convention): the PONI point where the un-tilted beam meets the detector. "
|
||
"Equals the beam center only when the detector tilt is zero.";
|
||
beamX_->setToolTip(beamTip);
|
||
beamY_->setToolTip(beamTip);
|
||
const QString tiltTip = "Detector tilt about the two in-plane axes (PyFAI PONI rot1 / rot2), in degrees.";
|
||
rot1_->setToolTip(tiltTip);
|
||
rot2_->setToolTip(tiltTip);
|
||
|
||
auto *beam = new QHBoxLayout();
|
||
beam->addWidget(beamX_);
|
||
beam->addWidget(beamY_);
|
||
auto *tilt = new QHBoxLayout();
|
||
tilt->addWidget(rot1_);
|
||
tilt->addWidget(rot2_);
|
||
|
||
geom->addRow("Photon energy", energy_);
|
||
geom->addRow("Detector distance", distance_);
|
||
geom->addRow("Beam origin", beam);
|
||
geom->addRow("Detector tilt", tilt);
|
||
|
||
for (auto *f : {energy_, distance_, beamX_, beamY_, rot1_, rot2_})
|
||
connect(f, &NumberLineEdit::newValue, this, [this] { EmitExperiment(); });
|
||
|
||
section->setContentLayout(geom);
|
||
return section;
|
||
}
|
||
|
||
QWidget *JFJochViewerSettingsDock::BuildMXPage() {
|
||
auto *page = new QWidget(this);
|
||
auto *layout = new QVBoxLayout(page);
|
||
layout->setContentsMargins(0, 0, 0, 0);
|
||
|
||
// --- Unit cell + space group (new: no input existed before) ---
|
||
auto *cellSection = new CollapsibleSection("Unit cell", page);
|
||
auto *cell = new QFormLayout();
|
||
cell->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
|
||
cellKnown_ = new QCheckBox("Known unit cell", page);
|
||
cell->addRow("", cellKnown_);
|
||
cellA_ = new NumberLineEdit(1.0, 2000.0, 79.0, 3, "Å", page);
|
||
cellB_ = new NumberLineEdit(1.0, 2000.0, 79.0, 3, "Å", page);
|
||
cellC_ = new NumberLineEdit(1.0, 2000.0, 38.0, 3, "Å", page);
|
||
cellAlpha_ = new NumberLineEdit(1.0, 179.0, 90.0, 2, "°", page);
|
||
cellBeta_ = new NumberLineEdit(1.0, 179.0, 90.0, 2, "°", page);
|
||
cellGamma_ = new NumberLineEdit(1.0, 179.0, 90.0, 2, "°", page);
|
||
spaceGroup_ = new NumberLineEdit(0.0, 230.0, 0.0, 0, "", page);
|
||
spaceGroup_->setToolTip("Space group number (1–230); 0 = unset.");
|
||
spaceGroupName_ = new QLabel("—", page);
|
||
auto *abc = new QHBoxLayout();
|
||
abc->addWidget(cellA_); abc->addWidget(cellB_); abc->addWidget(cellC_);
|
||
auto *angles = new QHBoxLayout();
|
||
angles->addWidget(cellAlpha_); angles->addWidget(cellBeta_); angles->addWidget(cellGamma_);
|
||
// Number and symbol split the line evenly, like the beam-origin row.
|
||
auto *sgRow = new QHBoxLayout();
|
||
sgRow->addWidget(spaceGroup_, 1);
|
||
sgRow->addWidget(spaceGroupName_, 1);
|
||
cell->addRow("a, b, c", abc);
|
||
cell->addRow("α, β, γ", angles);
|
||
cell->addRow("Space group", sgRow);
|
||
cellSection->setContentLayout(cell);
|
||
layout->addWidget(cellSection);
|
||
|
||
auto enableCellFields = [this](bool on) {
|
||
for (auto *f : {cellA_, cellB_, cellC_, cellAlpha_, cellBeta_, cellGamma_, spaceGroup_})
|
||
f->setEnabled(on);
|
||
};
|
||
enableCellFields(false);
|
||
connect(cellKnown_, &QCheckBox::toggled, this, [this, enableCellFields](bool on) {
|
||
enableCellFields(on);
|
||
UpdateSpaceGroupName();
|
||
UpdateAlgorithmDescription(); // Auto resolves to FFBIDX vs FFT depending on a known cell
|
||
EmitExperiment();
|
||
});
|
||
for (auto *f : {cellA_, cellB_, cellC_, cellAlpha_, cellBeta_, cellGamma_})
|
||
connect(f, &NumberLineEdit::newValue, this, [this] { EmitExperiment(); });
|
||
connect(spaceGroup_, &NumberLineEdit::newValue, this, [this] {
|
||
UpdateSpaceGroupName();
|
||
EmitExperiment();
|
||
});
|
||
|
||
// --- Spot finding ---
|
||
auto *spotSection = new CollapsibleSection("Spot finding", page);
|
||
auto *spot = new QFormLayout();
|
||
spot->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
|
||
auto *snr = new SliderPlusBox(1.0, 10.0, 0.1, 1, page);
|
||
snr->setValue(spot_.signal_to_noise_threshold);
|
||
auto *count = new SliderPlusBox(0.0, 100.0, 1.0, 0, page);
|
||
count->setValue(std::lround(spot_.photon_count_threshold));
|
||
auto *adaptive = new QCheckBox("Adaptive threshold (per-ring, auto)", page);
|
||
adaptive->setChecked(spot_.adaptive_threshold);
|
||
adaptive->setToolTip("Self-calibrating detection: the strong-pixel threshold is derived from each "
|
||
"image's own per-resolution-ring noise, so one setting adapts across datasets. "
|
||
"The signal/noise and photon-count settings are not used while this is on. "
|
||
"Default for stills and rotation alike, as in rugnux.");
|
||
auto *highResSpot = new SliderPlusBox(0.5, 5.0, 0.1, 1, page);
|
||
highResSpot->setValue(spot_.high_resolution_limit.value_or(1.5f));
|
||
auto *autoHighResSpot = new QCheckBox("To detector edge", page);
|
||
autoHighResSpot->setChecked(!spot_.high_resolution_limit.has_value());
|
||
autoHighResSpot->setToolTip("Find spots as far as the detector reaches, instead of clipping the "
|
||
"detection at a fixed resolution. The high-resolution value is not used "
|
||
"while this is on.");
|
||
auto *minPix = new NumberLineEdit(1.0f, 50.0f, static_cast<float>(min_pix_value_), 0, "px", page);
|
||
auto *adaptiveMinPix = new QCheckBox("Adaptive min-pix (stills)", page);
|
||
adaptiveMinPix->setChecked(adaptive_min_pix_);
|
||
adaptiveMinPix->setToolTip("Choose the minimum pixels/spot per image: index the frame at min-pix "
|
||
"3/2/1 and keep whichever maximises indexed count x indexed fraction. "
|
||
"Stills only - a rotation dataset builds one lattice from all frames and "
|
||
"always uses the fixed min-pixels/spot value below.");
|
||
// The same bounds DatasetSettings::MaxSpotCount enforces - offering more than it accepts only got
|
||
// the value rejected further down.
|
||
auto *maxSpots = new NumberLineEdit(10.0f, static_cast<float>(MAX_SPOT_COUNT),
|
||
static_cast<float>(max_spots_), 0, "", page);
|
||
spot->addRow("", adaptive);
|
||
spot->addRow("Signal/noise", snr);
|
||
spot->addRow("Photon count", count);
|
||
spot->addRow("", autoHighResSpot);
|
||
spot->addRow("High resolution [Å]", highResSpot);
|
||
spot->addRow("", adaptiveMinPix);
|
||
spot->addRow("Min pixels/spot", minPix);
|
||
spot->addRow("Max spots/image", maxSpots);
|
||
spotSection->setContentLayout(spot);
|
||
spotSection->setExpanded(false);
|
||
layout->addWidget(spotSection);
|
||
connect(snr, &SliderPlusBox::valueChanged, this, [this](double v) {
|
||
spot_.signal_to_noise_threshold = static_cast<float>(v); EmitSpotFinding(); });
|
||
connect(count, &SliderPlusBox::valueChanged, this, [this](double v) {
|
||
spot_.photon_count_threshold = std::llround(v); EmitSpotFinding(); });
|
||
connect(highResSpot, &SliderPlusBox::valueChanged, this, [this](double v) {
|
||
spot_.high_resolution_limit = static_cast<float>(v); EmitSpotFinding(); });
|
||
connect(minPix, &NumberLineEdit::newValue, this, [this, minPix] {
|
||
min_pix_value_ = std::llround(minPix->value()); EmitSpotFinding(); });
|
||
connect(maxSpots, &NumberLineEdit::newValue, this, [this, maxSpots] {
|
||
max_spots_ = std::llround(maxSpots->value()); EmitSpotFinding(); });
|
||
// The adaptive finder sets its own threshold from each image's noise, so the signal/noise and
|
||
// photon-count sliders do nothing while it is on - grey them out to make that clear.
|
||
auto syncAdaptiveEnabled = [snr, count](bool on) {
|
||
snr->setEnabled(!on);
|
||
count->setEnabled(!on);
|
||
};
|
||
syncAdaptiveEnabled(spot_.adaptive_threshold);
|
||
connect(adaptive, &QCheckBox::toggled, this, [this, syncAdaptiveEnabled](bool on) {
|
||
spot_.adaptive_threshold = on;
|
||
syncAdaptiveEnabled(on);
|
||
EmitSpotFinding();
|
||
});
|
||
// An unset high-resolution limit (std::nullopt) means "to the detector edge", so the value is unused
|
||
// while that is on - grey it out, as for the other automatic settings.
|
||
auto syncHighResEnabled = [highResSpot](bool auto_on) { highResSpot->setEnabled(!auto_on); };
|
||
syncHighResEnabled(!spot_.high_resolution_limit.has_value());
|
||
connect(autoHighResSpot, &QCheckBox::toggled, this, [this, highResSpot, syncHighResEnabled](bool on) {
|
||
if (on)
|
||
spot_.high_resolution_limit = std::nullopt;
|
||
else
|
||
spot_.high_resolution_limit = static_cast<float>(highResSpot->value());
|
||
syncHighResEnabled(on);
|
||
EmitSpotFinding();
|
||
});
|
||
|
||
// The fixed min-pixels/spot field stays live even with adaptive min-pix on: it is what a rotation
|
||
// dataset uses, and what stills fall back to when adaptive is switched off.
|
||
connect(adaptiveMinPix, &QCheckBox::toggled, this, [this](bool on) {
|
||
adaptive_min_pix_ = on;
|
||
EmitSpotFinding();
|
||
});
|
||
|
||
// --- Indexing ---
|
||
auto *idxSection = new CollapsibleSection("Indexing", page);
|
||
auto *idx = new QFormLayout();
|
||
idx->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
|
||
algo_ = new QComboBox(page);
|
||
algo_->addItem("Auto", static_cast<int>(IndexingAlgorithmEnum::Auto));
|
||
algo_->addItem("FFBIDX (GPU, known cell)", static_cast<int>(IndexingAlgorithmEnum::FFBIDX));
|
||
algo_->addItem("FFT (GPU, de-novo)", static_cast<int>(IndexingAlgorithmEnum::FFT));
|
||
algo_->addItem("FFTW (CPU, de-novo)", static_cast<int>(IndexingAlgorithmEnum::FFTW));
|
||
algo_->addItem("None", static_cast<int>(IndexingAlgorithmEnum::None));
|
||
algo_->setCurrentIndex(algo_->findData(static_cast<int>(indexing_.GetAlgorithm())));
|
||
algoDesc_ = new QLabel(page);
|
||
algoDesc_->setWordWrap(true);
|
||
algoDesc_->setStyleSheet("color: gray;");
|
||
auto *refine = new QComboBox(page);
|
||
refine->addItem("None", static_cast<int>(GeomRefinementAlgorithmEnum::None));
|
||
refine->addItem("Orientation only", static_cast<int>(GeomRefinementAlgorithmEnum::OrientationOnly));
|
||
refine->addItem("Beam center + lattice", static_cast<int>(GeomRefinementAlgorithmEnum::BeamCenter));
|
||
// "Flex" (the CLI's -r flex): tries all per-image refinements and keeps whichever indexes the most
|
||
// spots — lets the pipeline decide. Named "Flex", not "multi", to avoid the CrystFEL sense of multi
|
||
// (multi-lattice integration).
|
||
refine->addItem("Flex (best per-image refinement)", static_cast<int>(GeomRefinementAlgorithmEnum::Flex));
|
||
refine->setCurrentIndex(refine->findData(static_cast<int>(indexing_.GetGeomRefinementAlgorithm())));
|
||
// One high-level mode switch instead of separate partiality / rot3d / rotation-indexing options:
|
||
// for a rotation dataset, unchecked = the rotation good-path (rotation indexing + Rotation
|
||
// partiality + rot3d combine + scale-fulls), checked = treat it as stills (fixed partiality,
|
||
// per-frame indexing). Disabled (and a no-op) for datasets that are already stills.
|
||
stills_ = new QCheckBox("Process as stills", page);
|
||
stills_->setEnabled(false); // datasetLoaded enables it only for rotation (goniometer) datasets
|
||
stills_->setToolTip("Treat a rotation dataset as independent stills (fixed partiality, per-frame "
|
||
"indexing). Unchecked on a rotation dataset = rotation indexing + 3D rotation scaling.");
|
||
idx->addRow("Algorithm", algo_);
|
||
idx->addRow("", algoDesc_);
|
||
idx->addRow("Refinement", refine);
|
||
idx->addRow("", stills_);
|
||
idxSection->setContentLayout(idx);
|
||
idxSection->setExpanded(false);
|
||
layout->addWidget(idxSection);
|
||
connect(algo_, &QComboBox::currentIndexChanged, this, [this] {
|
||
indexing_.Algorithm(static_cast<IndexingAlgorithmEnum>(algo_->currentData().toInt()));
|
||
UpdateAlgorithmDescription();
|
||
EmitSpotFinding();
|
||
});
|
||
connect(refine, &QComboBox::currentIndexChanged, this, [this, refine] {
|
||
indexing_.GeomRefinementAlgorithm(static_cast<GeomRefinementAlgorithmEnum>(refine->currentData().toInt()));
|
||
EmitSpotFinding();
|
||
});
|
||
connect(stills_, &QCheckBox::toggled, this, [this] { ApplyProcessingMode(); });
|
||
UpdateAlgorithmDescription();
|
||
|
||
layout->addWidget(BuildBraggSection());
|
||
layout->addWidget(BuildScalingSection());
|
||
layout->addWidget(BuildReferenceSection());
|
||
|
||
layout->addStretch(); // anchor sections to the top so expanding an accordion grows downward
|
||
return page;
|
||
}
|
||
|
||
QWidget *JFJochViewerSettingsDock::BuildReferenceSection() {
|
||
// A reference dataset for scaling: drives CCref and reference-based scaling. It is
|
||
// independent of the loaded data (the worker keeps it across file switches); this just lets the
|
||
// user pick the MTZ + column and shows what it contains and whether it matches the data.
|
||
auto *section = new CollapsibleSection("Reference dataset", this);
|
||
auto *form = new QFormLayout();
|
||
form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
|
||
|
||
refButton_ = new QPushButton("Choose MTZ…", this);
|
||
refColumn_ = new QComboBox(this);
|
||
refColumn_->setEnabled(false);
|
||
refColumn_->setToolTip("Reference intensity / structure-factor column (F is squared to an intensity).");
|
||
refSummary_ = new QLabel("No reference loaded", this);
|
||
refSummary_->setWordWrap(true);
|
||
refWarning_ = new QLabel(this);
|
||
refWarning_->setWordWrap(true);
|
||
refWarning_->setVisible(false);
|
||
|
||
form->addRow(refButton_);
|
||
form->addRow("Column", refColumn_);
|
||
form->addRow(refSummary_);
|
||
form->addRow(refWarning_);
|
||
section->setContentLayout(form);
|
||
section->setExpanded(false); // folded on start (only geometry + unit cell start open)
|
||
|
||
connect(refButton_, &QPushButton::clicked, this, [this] {
|
||
const QString path = QFileDialog::getOpenFileName(this, "Reference MTZ", refPath_,
|
||
"MTZ files (*.mtz);;All files (*)");
|
||
if (path.isEmpty())
|
||
return;
|
||
refPath_ = path;
|
||
emit referenceSelected(path, QString()); // empty column -> let the worker auto-select
|
||
});
|
||
// activated (not currentIndexChanged) so re-populating the combo on load doesn't re-trigger.
|
||
connect(refColumn_, &QComboBox::activated, this, [this] {
|
||
if (!refPath_.isEmpty())
|
||
emit referenceSelected(refPath_, refColumn_->currentText());
|
||
});
|
||
|
||
return section;
|
||
}
|
||
|
||
QWidget *JFJochViewerSettingsDock::BuildAzIntPage() {
|
||
auto *page = new QWidget(this);
|
||
auto *layout = new QVBoxLayout(page);
|
||
layout->setContentsMargins(0, 0, 0, 0);
|
||
|
||
auto *azSection = new CollapsibleSection("Azimuthal integration", page);
|
||
auto *az = new QFormLayout();
|
||
az->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
|
||
auto *lowQ = new SliderPlusBox(1e-5, 10.0, 0.001, 4, page);
|
||
lowQ->setValue(azint_.GetLowQ_recipA());
|
||
auto *highQ = new SliderPlusBox(2e-5, 10.0, 0.001, 4, page);
|
||
highQ->setValue(azint_.GetHighQ_recipA());
|
||
auto *autoHighQ = new QCheckBox("To detector edge", page);
|
||
autoHighQ->setChecked(!azint_.GetRequestedHighQ_recipA().has_value());
|
||
autoHighQ->setToolTip("Integrate out to the highest Q the detector reaches. The high-Q value is not "
|
||
"used while this is on.");
|
||
auto *spacing = new SliderPlusBox(1e-5, 1.0, 0.001, 5, page, SliderPlusBox::ScaleType::Logarithmic);
|
||
spacing->setValue(azint_.GetQSpacing_recipA());
|
||
auto *azimBins = new QComboBox(page);
|
||
for (int b : {1, 2, 4, 8, 16, 32, 64, 128})
|
||
azimBins->addItem(QString::number(b), b);
|
||
azimBins->setCurrentIndex(azimBins->findData(azint_.GetAzimuthalBinCount()));
|
||
az->addRow("Low Q [Å⁻¹]", lowQ);
|
||
az->addRow("", autoHighQ);
|
||
az->addRow("High Q [Å⁻¹]", highQ);
|
||
az->addRow("Q spacing [Å⁻¹]", spacing);
|
||
az->addRow("Azimuthal bins", azimBins);
|
||
azSection->setContentLayout(az);
|
||
azSection->setExpanded(false);
|
||
layout->addWidget(azSection);
|
||
|
||
auto emitAz = [=, this] {
|
||
azint_.QRange_recipA(static_cast<float>(lowQ->value()),
|
||
autoHighQ->isChecked() ? std::nullopt
|
||
: std::optional<float>(highQ->value()));
|
||
azint_.QSpacing_recipA(static_cast<float>(spacing->value()));
|
||
azint_.AzimuthalBinCount(azimBins->currentData().toInt());
|
||
emit azintChanged(azint_);
|
||
};
|
||
connect(lowQ, &SliderPlusBox::valueChanged, this, [emitAz] { emitAz(); });
|
||
connect(highQ, &SliderPlusBox::valueChanged, this, [emitAz] { emitAz(); });
|
||
connect(spacing, &SliderPlusBox::valueChanged, this, [emitAz] { emitAz(); });
|
||
connect(azimBins, &QComboBox::currentIndexChanged, this, [emitAz] { emitAz(); });
|
||
connect(autoHighQ, &QCheckBox::toggled, this, [emitAz, highQ](bool on) {
|
||
highQ->setEnabled(!on);
|
||
emitAz();
|
||
});
|
||
highQ->setEnabled(!autoHighQ->isChecked());
|
||
|
||
layout->addStretch(); // anchor sections to the top so expanding an accordion grows downward
|
||
return page;
|
||
}
|
||
|
||
QWidget *JFJochViewerSettingsDock::BuildCalibrationPage() {
|
||
auto *page = new QWidget(this);
|
||
auto *layout = new QVBoxLayout(page);
|
||
layout->setContentsMargins(0, 0, 0, 0);
|
||
|
||
// The powder-calibration widget is the whole page: it already carries the calibrant and the two
|
||
// interactive (current-image) fits, and it now also carries the method a whole-dataset run uses,
|
||
// so the interactive and dataset paths share one calibrant selection.
|
||
auto *powderSection = new CollapsibleSection("Powder calibration", page);
|
||
auto *powderLayout = new QVBoxLayout();
|
||
powderLayout->setContentsMargins(0, 0, 0, 0);
|
||
powder_ = new PowderCalibrationWidget(page);
|
||
connect(powder_, &PowderCalibrationWidget::findBeamCenter, this, &JFJochViewerSettingsDock::findBeamCenter);
|
||
connect(powder_, &PowderCalibrationWidget::ringsFromCalibration, this, &JFJochViewerSettingsDock::ringsFromCalibration);
|
||
powderLayout->addWidget(powder_);
|
||
powderSection->setContentLayout(powderLayout);
|
||
layout->addWidget(powderSection);
|
||
|
||
// The rings method reads the ring's position at every azimuth, so the run has to be integrated in
|
||
// azimuthal sectors; one sector is a plain radial profile and cannot locate the ring. The bin count
|
||
// is the AzInt page's single setting - say here what it is set to rather than duplicating it.
|
||
auto *bins = new QLabel(page);
|
||
bins->setWordWrap(true);
|
||
bins->setStyleSheet("color: gray;");
|
||
layout->addWidget(bins);
|
||
auto refreshBins = [this, bins] {
|
||
bins->setText(QStringLiteral("Rings method: azimuthal bins (AzInt page) = %1%2")
|
||
.arg(azint_.GetAzimuthalBinCount())
|
||
.arg(azint_.GetAzimuthalBinCount() < 4
|
||
? QStringLiteral(" — too few to locate a ring; the run will use 32")
|
||
: QString()));
|
||
};
|
||
refreshBins();
|
||
connect(this, &JFJochViewerSettingsDock::azintChanged, this, [refreshBins] { refreshBins(); });
|
||
|
||
layout->addStretch();
|
||
return page;
|
||
}
|
||
|
||
void JFJochViewerSettingsDock::SyncMinPix() {
|
||
// Per-image min-pix indexes each frame on its own, which only means something for stills; rotation
|
||
// indexing builds one lattice from all frames, so it keeps the fixed value.
|
||
spot_.min_pix_per_spot = (adaptive_min_pix_ && !indexing_.GetRotationIndexing())
|
||
? std::optional<int64_t>()
|
||
: std::optional<int64_t>(min_pix_value_);
|
||
}
|
||
|
||
void JFJochViewerSettingsDock::EmitSpotFinding() {
|
||
SyncMinPix();
|
||
emit spotFindingChanged(spot_, indexing_, max_spots_);
|
||
}
|
||
|
||
void JFJochViewerSettingsDock::UpdateAlgorithmDescription() {
|
||
if (!algo_ || !algoDesc_)
|
||
return;
|
||
const auto a = static_cast<IndexingAlgorithmEnum>(algo_->currentData().toInt());
|
||
const bool gpu = get_gpu_count() > 0;
|
||
const bool cell_known = cellKnown_ && cellKnown_->isChecked();
|
||
QString text;
|
||
switch (a) {
|
||
case IndexingAlgorithmEnum::FFBIDX:
|
||
text = "GPU, needs a known cell — best for sparse serial stills"; break;
|
||
case IndexingAlgorithmEnum::FFT:
|
||
text = "GPU, de-novo — best for strong rotation data"; break;
|
||
case IndexingAlgorithmEnum::FFTW:
|
||
text = "CPU, de-novo — no GPU needed"; break;
|
||
case IndexingAlgorithmEnum::Auto: {
|
||
const IndexingAlgorithmEnum r = !gpu ? IndexingAlgorithmEnum::FFTW
|
||
: (cell_known ? IndexingAlgorithmEnum::FFBIDX : IndexingAlgorithmEnum::FFT);
|
||
const QString rn = r == IndexingAlgorithmEnum::FFBIDX ? "FFBIDX"
|
||
: r == IndexingAlgorithmEnum::FFT ? "FFT" : "FFTW";
|
||
text = QString("resolves to %1 here (%2, cell %3)")
|
||
.arg(rn, gpu ? "GPU" : "no GPU", cell_known ? "known" : "unknown");
|
||
break;
|
||
}
|
||
default:
|
||
text = "no indexing"; break;
|
||
}
|
||
algoDesc_->setText(text);
|
||
}
|
||
|
||
QWidget *JFJochViewerSettingsDock::BuildBraggSection() {
|
||
auto *section = new CollapsibleSection("Bragg integration", this);
|
||
auto *form = new QFormLayout();
|
||
form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
|
||
|
||
auto *gaussian = new QCheckBox("Gaussian profile fit", this);
|
||
gaussian->setChecked(bragg_.GetIntegrator() != IntegratorMode::BoxSum);
|
||
gaussian->setToolTip("Profile-fit the spots (more accurate intensities); off = classical box-sum.");
|
||
auto *r1 = new NumberLineEdit(1.0f, 30.0f, bragg_.GetR1(), 1, "px", this);
|
||
auto *r2 = new NumberLineEdit(1.0f, 30.0f, bragg_.GetR2(), 1, "px", this);
|
||
auto *r3 = new NumberLineEdit(1.0f, 30.0f, bragg_.GetR3(), 1, "px", this);
|
||
auto *radii = new QHBoxLayout();
|
||
radii->addWidget(r1); radii->addWidget(r2); radii->addWidget(r3);
|
||
// Background trim: use the old symmetric trimmed mean for the r2..r3 ring (drop the lowest and
|
||
// highest fraction of ring pixels) in place of the default high-side sigma clip. Setting the trim
|
||
// clears the clip, so unchecked simply leaves the default estimator in place.
|
||
const float trim = bragg_.GetBackgroundTrimFraction();
|
||
auto *bkgTrim = new QCheckBox("Background trim", this);
|
||
bkgTrim->setChecked(trim > 0.0f);
|
||
bkgTrim->setToolTip("Estimate the local Bragg background with a symmetric trimmed mean of the "
|
||
"background ring instead of the default high-side sigma clip. A symmetric trim "
|
||
"is biased low on Poisson data and adds a few counts to every partial, so this "
|
||
"is for back compatibility; unchecked = the 4 sigma clip.");
|
||
auto *bkgTrimFrac = new NumberLineEdit(0.01f, 0.49f, trim > 0.0f ? trim : 0.10f, 2, "", this);
|
||
bkgTrimFrac->setEnabled(bkgTrim->isChecked());
|
||
auto *trimRow = new QHBoxLayout();
|
||
trimRow->addWidget(bkgTrim);
|
||
trimRow->addWidget(bkgTrimFrac, 1);
|
||
form->addRow("", gaussian);
|
||
form->addRow("Radii r1/r2/r3", radii);
|
||
form->addRow("", trimRow);
|
||
section->setContentLayout(form);
|
||
section->setExpanded(false); // folded on start (only geometry + unit cell start open)
|
||
|
||
auto emitBragg = [=, this] {
|
||
bragg_.Integrator(gaussian->isChecked() ? IntegratorMode::ProfileGaussian : IntegratorMode::BoxSum);
|
||
bragg_.R1(static_cast<float>(r1->value())).R2(static_cast<float>(r2->value()))
|
||
.R3(static_cast<float>(r3->value()));
|
||
// Either estimator clears the other, so unchecking has to put the default clip back explicitly.
|
||
if (bkgTrim->isChecked())
|
||
bragg_.BackgroundTrimFraction(static_cast<float>(bkgTrimFrac->value()));
|
||
else
|
||
bragg_.BackgroundClipNSigma(BraggIntegrationSettings().GetBackgroundClipNSigma());
|
||
emit braggChanged(bragg_);
|
||
};
|
||
connect(gaussian, &QCheckBox::toggled, this, [emitBragg] { emitBragg(); });
|
||
connect(bkgTrim, &QCheckBox::toggled, this,
|
||
[bkgTrim, bkgTrimFrac, emitBragg] { bkgTrimFrac->setEnabled(bkgTrim->isChecked()); emitBragg(); });
|
||
connect(bkgTrimFrac, &NumberLineEdit::newValue, this, [emitBragg] { emitBragg(); });
|
||
for (auto *f : {r1, r2, r3})
|
||
connect(f, &NumberLineEdit::newValue, this, [emitBragg] { emitBragg(); });
|
||
return section;
|
||
}
|
||
|
||
QWidget *JFJochViewerSettingsDock::BuildScalingSection() {
|
||
// The partiality model + rot3d combine + scale-fulls are driven by "Process as stills" (indexing
|
||
// section); the panel keeps the full scaling_ so those fields are preserved here, not reset.
|
||
auto *section = new CollapsibleSection("Scaling", this);
|
||
auto *form = new QFormLayout();
|
||
form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
|
||
|
||
auto *friedel = new QCheckBox("Merge Friedel pairs", this);
|
||
friedel->setChecked(scaling_.GetMergeFriedel());
|
||
auto *corrections = new QCheckBox("Correction surfaces (decay + absorption)", this);
|
||
corrections->setChecked(scaling_.GetCorrectionSurfaces());
|
||
corrections->setToolTip("Rotation only: fit a radiation-damage decay and a goniometer-frame absorption "
|
||
"surface on the fulls. Cross-validated, so they no-op when their systematic is "
|
||
"absent. On by default.");
|
||
auto *partRefine = new QCheckBox("Partiality post-refinement (stills)", this);
|
||
partRefine->setChecked(scaling_.GetStillsPartialityRefine());
|
||
partRefine->setToolTip("Stills: refine a per-crystal orientation tilt against the running merge and "
|
||
"recompute each reflection's partiality (physical Ewald-proximity model), then "
|
||
"re-scale/merge. On by default; uncheck for the simple model (each reflection a "
|
||
"full, single-pass). No effect on rotation data.");
|
||
auto *limitRes = new QCheckBox("High-resolution limit", this);
|
||
limitRes->setChecked(scaling_.GetHighResolutionLimit_A().has_value());
|
||
auto *highRes = new NumberLineEdit(0.3f, 5.0f, scaling_.GetHighResolutionLimit_A().value_or(2.0), 1, "Å", this);
|
||
highRes->setEnabled(limitRes->isChecked());
|
||
|
||
form->addRow("", friedel);
|
||
form->addRow("", corrections);
|
||
form->addRow("", partRefine);
|
||
// Compact, and aligned with the checkboxes above: the limit checkbox + value sit together in the
|
||
// field column (not as a row label, which would indent it differently).
|
||
auto *resRow = new QHBoxLayout();
|
||
resRow->addWidget(limitRes);
|
||
resRow->addWidget(highRes, 1);
|
||
form->addRow("", resRow);
|
||
section->setContentLayout(form);
|
||
section->setExpanded(false); // folded on start (only geometry + unit cell start open)
|
||
|
||
auto emitScaling = [=, this] {
|
||
scaling_.MergeFriedel(friedel->isChecked());
|
||
scaling_.CorrectionSurfaces(corrections->isChecked());
|
||
scaling_.StillsPartialityRefine(partRefine->isChecked());
|
||
scaling_.HighResolutionLimit_A(limitRes->isChecked()
|
||
? std::optional<double>(highRes->value()) : std::nullopt);
|
||
emit scalingChanged(scaling_);
|
||
};
|
||
connect(friedel, &QCheckBox::toggled, this, [emitScaling] { emitScaling(); });
|
||
connect(corrections, &QCheckBox::toggled, this, [emitScaling] { emitScaling(); });
|
||
connect(partRefine, &QCheckBox::toggled, this, [emitScaling] { emitScaling(); });
|
||
connect(limitRes, &QCheckBox::toggled, this, [emitScaling, highRes](bool on) {
|
||
highRes->setEnabled(on); emitScaling(); });
|
||
connect(highRes, &NumberLineEdit::newValue, this, [emitScaling] { emitScaling(); });
|
||
return section;
|
||
}
|
||
|
||
void JFJochViewerSettingsDock::EmitExperiment() {
|
||
if (!have_experiment_)
|
||
return;
|
||
experiment_.IncidentEnergy_keV(static_cast<float>(energy_->value()));
|
||
experiment_.DetectorDistance_mm(static_cast<float>(distance_->value()));
|
||
experiment_.BeamX_pxl(static_cast<float>(beamX_->value()));
|
||
experiment_.BeamY_pxl(static_cast<float>(beamY_->value()));
|
||
experiment_.PoniRot1_rad(static_cast<float>(rot1_->value() * PI / 180.0));
|
||
experiment_.PoniRot2_rad(static_cast<float>(rot2_->value() * PI / 180.0));
|
||
if (cellKnown_->isChecked()) {
|
||
experiment_.SetUnitCell(UnitCell{
|
||
static_cast<float>(cellA_->value()), static_cast<float>(cellB_->value()),
|
||
static_cast<float>(cellC_->value()), static_cast<float>(cellAlpha_->value()),
|
||
static_cast<float>(cellBeta_->value()), static_cast<float>(cellGamma_->value())});
|
||
const int sg = static_cast<int>(std::lround(spaceGroup_->value()));
|
||
experiment_.SpaceGroupNumber(sg > 0 ? std::optional<int64_t>(sg) : std::nullopt);
|
||
} else {
|
||
experiment_.SetUnitCell(std::nullopt);
|
||
experiment_.SpaceGroupNumber(std::nullopt);
|
||
}
|
||
emit experimentChanged(experiment_);
|
||
}
|
||
|
||
void JFJochViewerSettingsDock::RefreshGeometryFields() {
|
||
// Populate fields from the loaded experiment. NumberLineEdit::setValue does not emit newValue
|
||
// (that fires only on user editing), so this cannot feed back into EmitExperiment.
|
||
energy_->setValue(experiment_.GetIncidentEnergy_keV());
|
||
distance_->setValue(experiment_.GetDetectorDistance_mm());
|
||
beamX_->setValue(experiment_.GetBeamX_pxl());
|
||
beamY_->setValue(experiment_.GetBeamY_pxl());
|
||
rot1_->setValue(experiment_.GetPoniRot1_rad() * 180.0 / PI);
|
||
rot2_->setValue(experiment_.GetPoniRot2_rad() * 180.0 / PI);
|
||
|
||
const auto cell = experiment_.GetUnitCell();
|
||
QSignalBlocker blockKnown(cellKnown_);
|
||
cellKnown_->setChecked(cell.has_value());
|
||
for (auto *f : {cellA_, cellB_, cellC_, cellAlpha_, cellBeta_, cellGamma_, spaceGroup_})
|
||
f->setEnabled(cell.has_value());
|
||
if (cell) {
|
||
cellA_->setValue(cell->a); cellB_->setValue(cell->b); cellC_->setValue(cell->c);
|
||
cellAlpha_->setValue(cell->alpha); cellBeta_->setValue(cell->beta); cellGamma_->setValue(cell->gamma);
|
||
spaceGroup_->setValue(experiment_.GetSpaceGroupNumber().value_or(0));
|
||
}
|
||
UpdateSpaceGroupName();
|
||
}
|
||
|
||
void JFJochViewerSettingsDock::UpdateSpaceGroupName() {
|
||
if (!cellKnown_->isChecked()) {
|
||
spaceGroupName_->setText("—");
|
||
return;
|
||
}
|
||
const int n = static_cast<int>(std::lround(spaceGroup_->value()));
|
||
if (n >= 1 && n <= 230) {
|
||
try {
|
||
const auto &sg = gemmi::get_spacegroup_by_number(n); // Hermann–Mauguin short symbol
|
||
spaceGroupName_->setText(QString::fromStdString(sg.short_name()));
|
||
} catch (...) {
|
||
spaceGroupName_->setText("invalid");
|
||
}
|
||
} else {
|
||
spaceGroupName_->setText(n == 0 ? "—" : "invalid");
|
||
}
|
||
}
|
||
|
||
void JFJochViewerSettingsDock::datasetLoaded(std::shared_ptr<const JFJochReaderDataset> dataset) {
|
||
if (!dataset)
|
||
return;
|
||
experiment_ = dataset->experiment;
|
||
have_experiment_ = true;
|
||
RefreshGeometryFields();
|
||
|
||
// "Process as stills" only applies to a rotation dataset; enable it accordingly and apply the
|
||
// resulting indexing/scaling mode so it reaches the worker.
|
||
if (stills_) {
|
||
stills_->setEnabled(experiment_.GetGoniometer().has_value());
|
||
ApplyProcessingMode();
|
||
}
|
||
}
|
||
|
||
void JFJochViewerSettingsDock::ApplyProcessingMode() {
|
||
// Rotation good-path unless the dataset is stills or the user forces "Process as stills".
|
||
const bool rotation_data = experiment_.GetGoniometer().has_value();
|
||
const bool rotation_mode = rotation_data && stills_ && !stills_->isChecked();
|
||
indexing_.RotationIndexing(rotation_mode); // rotation indexing -> rotation scaling/merge downstream
|
||
scaling_.ScaleFulls(rotation_mode);
|
||
EmitSpotFinding(); // carries indexing_ (incl. RotationIndexing) to the worker
|
||
emit scalingChanged(scaling_);
|
||
}
|
||
|
||
void JFJochViewerSettingsDock::loadImage(std::shared_ptr<const JFJochReaderImage> image) {
|
||
if (powder_)
|
||
powder_->loadImage(image);
|
||
}
|
||
|
||
void JFJochViewerSettingsDock::referenceLoaded(ReferenceMtzInfo info) {
|
||
{
|
||
QSignalBlocker block(refColumn_); // re-populating must not emit referenceSelected
|
||
refColumn_->clear();
|
||
refColumn_->addItems(info.columns);
|
||
const int idx = refColumn_->findText(info.used_column);
|
||
if (idx >= 0)
|
||
refColumn_->setCurrentIndex(idx);
|
||
}
|
||
refColumn_->setEnabled(info.loaded && !info.columns.isEmpty());
|
||
refSummary_->setText(info.loaded ? info.summary : "No reference loaded");
|
||
|
||
if (info.warning.isEmpty()) {
|
||
refWarning_->setVisible(false);
|
||
} else {
|
||
// Amber = a loaded-but-mismatched reference (a caution); red = a load failure.
|
||
refWarning_->setStyleSheet(info.loaded ? "color: #B8860B;" : "color: #C0392B;");
|
||
refWarning_->setText(info.warning);
|
||
refWarning_->setVisible(true);
|
||
}
|
||
}
|