Files
Jungfraujoch/viewer/widgets/JFJochViewerSettingsDock.cpp
T
leonarski_fandClaude Opus 4.8 58c60e03fe viewer: Phase 1b — inline MX/AzInt settings dock
Surface a surgical subset of processing settings in an always-visible dock
instead of hidden windows:

- New JFJochViewerSettingsDock with an MX / AzInt segmented toggle.
  MX page: geometry (energy, distance, beam X/Y), a new unit-cell +
  space-group editor (no such input existed before; enables known-cell
  ffbidx), spot finding (S/N, photon count, min pixels/spot), indexing
  algorithm and geometry refinement. AzInt page: q range / spacing /
  azimuthal bins plus the existing powder-calibration widget.
- Edits feed straight into the worker (UpdateSpotFindingSettings,
  UpdateAzintSettings, UpdateDataset, FindCenter); fields populate from the
  loaded dataset.
- Add CeO2 and Silicon calibrant presets.
- Dock it left, objectName "settingsDock", show it in the Processing
  perspective (hidden in Image).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 16:26:15 +02:00

269 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 <QGroupBox>
#include <QPushButton>
#include <QButtonGroup>
#include <QStackedWidget>
#include <QCheckBox>
#include <QComboBox>
#include <QLabel>
#include <cmath>
#include "SliderPlusBox.h"
#include "NumberLineEdit.h"
#include "PowderCalibrationWidget.h"
#include "TitleLabel.h"
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);
auto *layout = new QVBoxLayout(this);
layout->addLayout(toggleRow);
layout->addWidget(stack);
layout->addStretch();
}
QWidget *JFJochViewerSettingsDock::BuildMXPage() {
auto *page = new QWidget(this);
auto *layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
// --- Geometry ---
layout->addWidget(new TitleLabel("Geometry", page));
auto *geomGroup = new QGroupBox(page);
auto *geom = new QFormLayout(geomGroup);
energy_ = new NumberLineEdit(1.0, 200.0, 12.4, 4, "keV", page);
distance_ = new NumberLineEdit(10.0, 5000.0, 100.0, 2, "mm", page);
beamX_ = new NumberLineEdit(0.0, 20000.0, 0.0, 1, "px", page);
beamY_ = new NumberLineEdit(0.0, 20000.0, 0.0, 1, "px", page);
geom->addRow("Photon energy", energy_);
geom->addRow("Detector distance", distance_);
geom->addRow("Beam center X", beamX_);
geom->addRow("Beam center Y", beamY_);
layout->addWidget(geomGroup);
for (auto *f : {energy_, distance_, beamX_, beamY_})
connect(f, &NumberLineEdit::newValue, this, [this] { EmitExperiment(); });
// --- Unit cell + space group (new: no input existed before) ---
layout->addWidget(new TitleLabel("Unit cell", page));
auto *cellGroup = new QGroupBox(page);
auto *cell = new QFormLayout(cellGroup);
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);
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_);
cell->addRow("a, b, c", abc);
cell->addRow("α, β, γ", angles);
cell->addRow("Space group", spaceGroup_);
layout->addWidget(cellGroup);
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);
EmitExperiment();
});
for (auto *f : {cellA_, cellB_, cellC_, cellAlpha_, cellBeta_, cellGamma_, spaceGroup_})
connect(f, &NumberLineEdit::newValue, this, [this] { EmitExperiment(); });
// --- Spot finding ---
layout->addWidget(new TitleLabel("Spot finding", page));
auto *spotGroup = new QGroupBox(page);
auto *spot = new QFormLayout(spotGroup);
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);
layout->addWidget(spotGroup);
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 ---
layout->addWidget(new TitleLabel("Indexing", page));
auto *idxGroup = new QGroupBox(page);
auto *idx = new QFormLayout(idxGroup);
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);
layout->addWidget(idxGroup);
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();
});
return page;
}
QWidget *JFJochViewerSettingsDock::BuildAzIntPage() {
auto *page = new QWidget(this);
auto *layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(new TitleLabel("Azimuthal integration", page));
auto *azGroup = new QGroupBox(page);
auto *az = new QFormLayout(azGroup);
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);
layout->addWidget(azGroup);
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.
layout->addWidget(new TitleLabel("Powder calibration", page));
powder_ = new PowderCalibrationWidget(page);
connect(powder_, &PowderCalibrationWidget::findBeamCenter, this, &JFJochViewerSettingsDock::findBeamCenter);
connect(powder_, &PowderCalibrationWidget::ringsFromCalibration, this, &JFJochViewerSettingsDock::ringsFromCalibration);
layout->addWidget(powder_);
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()));
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());
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));
}
}
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);
}