75e401f0e5
Build Packages / Unit tests (push) Successful in 1h31m59s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 8m43s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 10m5s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 9m27s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 8m56s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 9m24s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 10m27s
Build Packages / build:rpm (rocky8) (push) Successful in 9m20s
Build Packages / build:rpm (rocky9) (push) Successful in 10m50s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 9m54s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 8m38s
Build Packages / DIALS test (push) Successful in 12m13s
Build Packages / XDS test (durin plugin) (push) Successful in 7m8s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 7m8s
Build Packages / XDS test (neggia plugin) (push) Successful in 7m50s
Build Packages / Generate python client (push) Successful in 16s
Build Packages / Build documentation (push) Successful in 50s
Build Packages / Create release (push) Skipped
This is an UNSTABLE release. It includes many experimental features, as well as many AI generated fixes. We recommend using rc.152 for production use. * jfjoch_broker: Add EXPERIMENTAL pixelrefine mode for image processing * jfjoch_broker: Allow to load user mask from 8-bit and 16-bit TIFF files * jfjoch_broker: Add ROI calculation in non-FPGA workflow * jfjoch_broker: Fixes to TCP image pusher * jfjoch_broker: Remove NUMA bindings * jfjoch_broker: Improvements to indexing * jfjoch_broker: For PSI EIGER, trimming energies are taken from the detector configuration (now compulsory) instead of hardcoded values * jfjoch_writer: Save ROI definitions and the per-pixel ROI bitmap in the master file; azimuthal ROIs support phi (angular) sectors * jfjoch_viewer: Major redesign with dockable panels and saved layouts, plus on-canvas creation/move/resize of box, circle and azimuthal ROIs * jfjoch_viewer: Run jfjoch_process reprocessing jobs from inside the GUI and overlay per-run results Reviewed-on: #63
342 lines
16 KiB
C++
342 lines
16 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 <cmath>
|
||
|
||
#include "SliderPlusBox.h"
|
||
#include "NumberLineEdit.h"
|
||
#include "PowderCalibrationWidget.h"
|
||
#include "CollapsibleSection.h"
|
||
#include "../../common/JFJochMath.h"
|
||
#include "../../gemmi_gph/gemmi/symmetry.hpp"
|
||
|
||
JFJochViewerSettingsDock::JFJochViewerSettingsDock(const SpotFindingSettings &spot,
|
||
const IndexingSettings &indexing,
|
||
const AzimuthalIntegrationSettings &azint,
|
||
QWidget *parent)
|
||
: QWidget(parent), spot_(spot), indexing_(indexing), azint_(azint) {
|
||
|
||
// Segmented MX / AzInt toggle: the two communities pick their page; pages never share a screen.
|
||
auto *mxButton = new QPushButton("MX", this);
|
||
auto *azButton = new QPushButton("AzInt", this);
|
||
for (auto *b : {mxButton, azButton}) {
|
||
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);
|
||
|
||
auto *toggleRow = new QHBoxLayout();
|
||
toggleRow->setSpacing(0);
|
||
toggleRow->addWidget(mxButton);
|
||
toggleRow->addWidget(azButton);
|
||
|
||
auto *stack = new QStackedWidget(this);
|
||
stack->addWidget(BuildMXPage());
|
||
stack->addWidget(BuildAzIntPage());
|
||
connect(group, &QButtonGroup::idClicked, stack, &QStackedWidget::setCurrentIndex);
|
||
|
||
// Geometry is common to both communities, so it lives above the toggle rather than per page.
|
||
auto *layout = new QVBoxLayout(this);
|
||
layout->addLayout(toggleRow);
|
||
layout->addWidget(BuildGeometrySection());
|
||
layout->addWidget(stack);
|
||
layout->addStretch();
|
||
}
|
||
|
||
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();
|
||
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 *minPix = new SliderPlusBox(1.0, 20.0, 1.0, 0, page);
|
||
minPix->setValue(std::lround(spot_.min_pix_per_spot));
|
||
spot->addRow("Signal/noise", snr);
|
||
spot->addRow("Photon count", count);
|
||
spot->addRow("Min pixels/spot", minPix);
|
||
spotSection->setContentLayout(spot);
|
||
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 = static_cast<float>(v); EmitSpotFinding(); });
|
||
connect(minPix, &SliderPlusBox::valueChanged, this, [this](double v) {
|
||
spot_.min_pix_per_spot = std::lround(v); EmitSpotFinding(); });
|
||
|
||
// --- Indexing ---
|
||
auto *idxSection = new CollapsibleSection("Indexing", page);
|
||
auto *idx = new QFormLayout();
|
||
idx->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
|
||
auto *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())));
|
||
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));
|
||
refine->addItem("Pixel refinement", static_cast<int>(GeomRefinementAlgorithmEnum::PixelRefine));
|
||
refine->setCurrentIndex(refine->findData(static_cast<int>(indexing_.GetGeomRefinementAlgorithm())));
|
||
idx->addRow("Algorithm", algo);
|
||
idx->addRow("Refinement", refine);
|
||
idxSection->setContentLayout(idx);
|
||
layout->addWidget(idxSection);
|
||
connect(algo, &QComboBox::currentIndexChanged, this, [this, algo] {
|
||
indexing_.Algorithm(static_cast<IndexingAlgorithmEnum>(algo->currentData().toInt()));
|
||
EmitSpotFinding();
|
||
});
|
||
connect(refine, &QComboBox::currentIndexChanged, this, [this, refine] {
|
||
indexing_.GeomRefinementAlgorithm(static_cast<GeomRefinementAlgorithmEnum>(refine->currentData().toInt()));
|
||
EmitSpotFinding();
|
||
});
|
||
|
||
layout->addStretch(); // anchor sections to the top so expanding an accordion grows downward
|
||
return page;
|
||
}
|
||
|
||
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 *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("High Q [Å⁻¹]", highQ);
|
||
az->addRow("Q spacing [Å⁻¹]", spacing);
|
||
az->addRow("Azimuthal bins", azimBins);
|
||
azSection->setContentLayout(az);
|
||
layout->addWidget(azSection);
|
||
|
||
auto emitAz = [=, this] {
|
||
azint_.QRange_recipA(static_cast<float>(lowQ->value()), static_cast<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(); });
|
||
|
||
// Powder calibration (calibrant rings + geometry refinement) - reuse the existing widget.
|
||
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);
|
||
|
||
layout->addStretch(); // anchor sections to the top so expanding an accordion grows downward
|
||
return page;
|
||
}
|
||
|
||
void JFJochViewerSettingsDock::EmitSpotFinding() {
|
||
emit spotFindingChanged(spot_, indexing_, max_spots_);
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
void JFJochViewerSettingsDock::loadImage(std::shared_ptr<const JFJochReaderImage> image) {
|
||
if (powder_)
|
||
powder_->loadImage(image);
|
||
}
|