Files
Jungfraujoch/viewer/windows/JFJochViewerProcessingWindow.cpp
T
leonarski_f b735aec1c4 viewer: show effective indexing algorithm; disable GPU options without CUDA
- The Indexing tab now shows what the selected algorithm resolves to on this
  machine/dataset ("Effective on this system: ..."), mirroring
  DiffractionExperiment::GetIndexingAlgorithm() so Auto is no longer ambiguous
  (GPU present? unit cell known?). Cell-known state is forwarded from the loaded
  dataset via JFJochSettingsWindow::datasetLoaded.
- FFBIDX and FFT (GPU) radio options are disabled on builds without CUDA, where
  only the FFTW CPU indexer exists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 11:09:21 +02:00

334 lines
14 KiB
C++

// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "JFJochViewerProcessingWindow.h"
#include <QHBoxLayout>
#include <QGroupBox>
#include <QFormLayout>
#include <QButtonGroup>
#include <QRadioButton>
#include <vector>
#include "../../common/CUDAWrapper.h" // get_gpu_count()
namespace {
struct RadioChoice { const char *label; int value; };
QString algorithm_name(IndexingAlgorithmEnum a) {
switch (a) {
case IndexingAlgorithmEnum::FFBIDX: return "FFBIDX (GPU)";
case IndexingAlgorithmEnum::FFT: return "FFT (GPU)";
case IndexingAlgorithmEnum::FFTW: return "FFTW (CPU)";
case IndexingAlgorithmEnum::Auto: return "Auto";
default: return "None";
}
}
// A titled group of mutually-exclusive radio buttons, with an optional explanatory line.
QGroupBox *MakeRadioGroup(const QString &title, const QString &help,
const std::vector<RadioChoice> &choices, int current,
QButtonGroup *group, QWidget *parent) {
auto *box = new QGroupBox(title, parent);
auto *layout = new QVBoxLayout(box);
if (!help.isEmpty()) {
auto *label = new QLabel(help, box);
label->setWordWrap(true);
layout->addWidget(label);
}
for (const auto &c: choices) {
auto *button = new QRadioButton(c.label, box);
button->setChecked(c.value == current);
group->addButton(button, c.value);
layout->addWidget(button);
}
return box;
}
}
JFJochViewerProcessingWindow::JFJochViewerProcessingWindow(const SpotFindingSettings &settings, const IndexingSettings& indexing,QWidget *parent)
: JFJochHelperWindow(parent), m_settings(settings), m_indexing(indexing) {
setWindowTitle("Image processing settings");
// --- Spot finding page ---
m_spotFindingPage = new QWidget(this);
auto spotLayout = new QVBoxLayout(m_spotFindingPage);
auto generalGroup = new QGroupBox("Spot finding", m_spotFindingPage);
auto generalLayout = new QFormLayout(generalGroup);
m_enableCheckBox = new QCheckBox("Enable", this);
m_enableCheckBox->setChecked(m_settings.enable);
generalLayout->addRow("", m_enableCheckBox);
m_signalToNoise = new SliderPlusBox(1.0, 10.0, 0.1, 1, this);
m_signalToNoise->setValue(m_settings.signal_to_noise_threshold);
generalLayout->addRow("Signal to Noise threshold:", m_signalToNoise);
m_photonCount = new SliderPlusBox(0.0, 100.0, 1.0, 0, this);
m_photonCount->setValue(std::lround(m_settings.photon_count_threshold));
generalLayout->addRow("Photon count threshold:", m_photonCount);
m_minPixPerSpot = new SliderPlusBox(1.0, 20.0, 1.0, 0, this);
m_minPixPerSpot->setValue(std::lround(m_settings.min_pix_per_spot));
generalLayout->addRow("Minimum pixels per spot:", m_minPixPerSpot);
m_maxPixPerSpot = new SliderPlusBox(10.0, 200.0, 1.0, 0, this);
m_maxPixPerSpot->setValue(std::lround(m_settings.max_pix_per_spot));
generalLayout->addRow("Maximum pixels per spot:", m_maxPixPerSpot);
// New: Max spot count slider [100 .. 2000]
m_maxSpotCount = new SliderPlusBox(100.0, 2000.0, 10.0, 0, this);
// If there is no existing value source, initialize to a sensible default
m_maxSpotCount->setValue(1000.0);
generalLayout->addRow("Maximum spots per image:", m_maxSpotCount);
m_highResolution = new SliderPlusBox(0.8, 10.0, 0.1, 1, this);
m_highResolution->setValue(m_settings.high_resolution_limit);
generalLayout->addRow("High resolution", m_highResolution);
m_lowResolution = new SliderPlusBox(10.0, 100.0, 0.1, 1, this);
m_lowResolution->setValue(m_settings.low_resolution_limit);
generalLayout->addRow("Low resolution", m_lowResolution);
// High-res spurious spot filter (gap in 1/d)
m_highResSpuriousFilterCheckBox = new QCheckBox("Enable high-res spurious spot filter", this);
const bool gapFilterEnabled = m_settings.high_res_gap_Q_recipA > 0.0f;
m_highResSpuriousFilterCheckBox->setChecked(m_settings.high_res_gap_Q_recipA.has_value());
generalLayout->addRow("", m_highResSpuriousFilterCheckBox);
m_highResSpuriousGapOneOverD = new SliderPlusBox(0.1, 5.0, 0.01, 2, this);
// If disabled by setting = 0, show a reasonable default on the slider but keep disabled
const double initialGap = m_settings.high_res_gap_Q_recipA.value_or(0.25);
m_highResSpuriousGapOneOverD->setValue(initialGap);
m_highResSpuriousGapOneOverD->setEnabled(gapFilterEnabled);
generalLayout->addRow("Gap threshold in Q-space [A^-1]:", m_highResSpuriousGapOneOverD);
m_iceRingWidthQRecipA = new SliderPlusBox(0.0, 0.3, 0.001, 3, this);
m_iceRingWidthQRecipA->setValue(m_settings.ice_ring_width_Q_recipA);
generalLayout->addRow("Ice ring width in Q-space [A^-1]:", m_iceRingWidthQRecipA);
auto processingGroup = new QGroupBox("Other steps", m_spotFindingPage);
auto processingLayout = new QFormLayout(processingGroup);
m_quickIntegrationCheckBox = new QCheckBox("Enable Bragg Integration", this);
m_quickIntegrationCheckBox->setChecked(m_settings.quick_integration);
processingLayout->addRow("", m_quickIntegrationCheckBox);
spotLayout->addWidget(generalGroup);
spotLayout->addWidget(processingGroup);
spotLayout->addStretch();
// --- Indexing page ---
m_indexingPage = new QWidget(this);
auto indexLayout = new QVBoxLayout(m_indexingPage);
m_indexingCheckBox = new QCheckBox("Enable Indexing", this);
m_indexingCheckBox->setChecked(m_settings.indexing);
indexLayout->addWidget(m_indexingCheckBox);
m_indexAlgGroup = new QButtonGroup(this);
indexLayout->addWidget(MakeRadioGroup(
"Indexing algorithm",
"FFBIDX is fast but needs a known cell; FFT/FFTW index de-novo (FFT on GPU, FFTW on CPU). "
"Auto picks the best available.",
{{"Auto", static_cast<int>(IndexingAlgorithmEnum::Auto)},
{"FFBIDX — GPU, needs known cell", static_cast<int>(IndexingAlgorithmEnum::FFBIDX)},
{"FFT — GPU, de-novo", static_cast<int>(IndexingAlgorithmEnum::FFT)},
{"FFTW — CPU, de-novo", static_cast<int>(IndexingAlgorithmEnum::FFTW)},
{"None — skip indexing", static_cast<int>(IndexingAlgorithmEnum::None)}},
static_cast<int>(m_indexing.GetAlgorithm()), m_indexAlgGroup, m_indexingPage));
#ifndef JFJOCH_USE_CUDA
// The GPU indexers are not built without CUDA - only FFTW (CPU) is available.
for (int id: {static_cast<int>(IndexingAlgorithmEnum::FFBIDX), static_cast<int>(IndexingAlgorithmEnum::FFT)}) {
if (auto *button = m_indexAlgGroup->button(id)) {
button->setEnabled(false);
button->setToolTip("Requires a CUDA build");
}
}
#endif
m_resolvedAlgLabel = new QLabel(m_indexingPage);
indexLayout->addWidget(m_resolvedAlgLabel);
m_geomRefGroup = new QButtonGroup(this);
indexLayout->addWidget(MakeRadioGroup(
"Geometry refinement",
"How the geometry is refined once a lattice is found.",
{{"None", static_cast<int>(GeomRefinementAlgorithmEnum::None)},
{"Orientation only", static_cast<int>(GeomRefinementAlgorithmEnum::OrientationOnly)},
{"Beam center + lattice", static_cast<int>(GeomRefinementAlgorithmEnum::BeamCenter)},
{"Pixel refinement (experimental)", static_cast<int>(GeomRefinementAlgorithmEnum::PixelRefine)}},
static_cast<int>(m_indexing.GetGeomRefinementAlgorithm()), m_geomRefGroup, m_indexingPage));
auto indexingGroup = new QGroupBox("Indexing parameters", m_indexingPage);
auto indexingLayout = new QFormLayout(indexingGroup);
m_idxIndexIceRings = new QCheckBox("Index ice rings", this);
m_idxIndexIceRings->setChecked(m_indexing.GetIndexIceRings());
indexingLayout->addRow("", m_idxIndexIceRings);
m_idxTolerance = new SliderPlusBox(0.0, 0.5, 0.001, 3, this);
m_idxTolerance->setValue(m_indexing.GetTolerance());
indexingLayout->addRow("Indexing tolerance", m_idxTolerance);
m_idxUnitCellDistTolerance = new SliderPlusBox(0.001, 0.200, 0.001, 3, this);
m_idxUnitCellDistTolerance->setValue(m_indexing.GetUnitCellDistTolerance());
indexingLayout->addRow("Unit cell dist tol vs ref", m_idxUnitCellDistTolerance);
m_idxViableCellMinSpots = new SliderPlusBox(6.0, 200.0, 1.0, 0, this);
m_idxViableCellMinSpots->setValue(static_cast<double>(m_indexing.GetViableCellMinSpots()));
indexingLayout->addRow("Viable cell min spots", m_idxViableCellMinSpots);
indexLayout->addWidget(indexingGroup);
indexLayout->addStretch();
// --- Connections ---
connect(m_enableCheckBox, &QCheckBox::toggled, [this](bool checked) {
m_settings.enable = checked;
Update();
});
connect(m_signalToNoise, &SliderPlusBox::valueChanged, [this](double val) {
m_settings.signal_to_noise_threshold = val;
Update();
});
connect(m_photonCount, &SliderPlusBox::valueChanged, [this](double val) {
m_settings.photon_count_threshold = val;
Update();
});
connect(m_minPixPerSpot, &SliderPlusBox::valueChanged, [this](double val) {
m_settings.min_pix_per_spot = std::lround(val);
Update();
});
connect(m_maxPixPerSpot, &SliderPlusBox::valueChanged, [this](double val) {
m_settings.max_pix_per_spot = std::lround(val);
Update();
});
// New: update on max spot count change
connect(m_maxSpotCount, &SliderPlusBox::valueChanged, [this](double /*val*/) {
Update();
});
connect(m_highResolution, &SliderPlusBox::valueChanged, [this](double val) {
m_settings.high_resolution_limit = val;
Update();
});
connect(m_lowResolution, &SliderPlusBox::valueChanged, [this](double val) {
m_settings.low_resolution_limit = val;
Update();
});
// Toggle for high-res spurious spot filter
connect(m_highResSpuriousFilterCheckBox, &QCheckBox::toggled, [this](bool checked) {
m_highResSpuriousGapOneOverD->setEnabled(checked);
if (checked) {
// If currently disabled (zero), restore a sensible default
if (m_settings.high_res_gap_Q_recipA) {
m_settings.high_res_gap_Q_recipA = static_cast<float>(
m_highResSpuriousGapOneOverD->value());
}
} else
m_settings.high_res_gap_Q_recipA = std::nullopt;
Update();
});
// Gap slider handler
connect(m_highResSpuriousGapOneOverD, &SliderPlusBox::valueChanged, [this](double val) {
m_settings.high_res_gap_Q_recipA = static_cast<float>(val);
if (val > 0.0 && !m_highResSpuriousFilterCheckBox->isChecked()) {
m_highResSpuriousFilterCheckBox->setChecked(true);
}
Update();
});
// Indexing enable in its own group now
connect(m_indexingCheckBox, &QCheckBox::toggled, [this](bool checked) {
m_settings.indexing = checked;
Update();
});
connect(m_quickIntegrationCheckBox, &QCheckBox::toggled, [this](bool checked) {
m_settings.quick_integration = checked;
Update();
});
connect(m_indexAlgGroup, &QButtonGroup::idClicked, [this](int id) {
m_indexing.Algorithm(static_cast<IndexingAlgorithmEnum>(id));
UpdateResolvedAlgorithmLabel();
Update();
});
connect(m_geomRefGroup, &QButtonGroup::idClicked, [this](int id) {
m_indexing.GeomRefinementAlgorithm(static_cast<GeomRefinementAlgorithmEnum>(id));
Update();
});
// Indexing settings signals
connect(m_idxTolerance, &SliderPlusBox::valueChanged, [this](double val) {
m_indexing.Tolerance(static_cast<float>(val));
Update();
});
connect(m_idxUnitCellDistTolerance, &SliderPlusBox::valueChanged, [this](double val) {
m_indexing.UnitCellDistTolerance(static_cast<float>(val));
Update();
});
connect(m_idxIndexIceRings, &QCheckBox::toggled, [this](bool checked) {
m_indexing.IndexIceRings(checked);
Update();
});
connect(m_idxViableCellMinSpots, &SliderPlusBox::valueChanged, [this](double val) {
m_indexing.ViableCellMinSpots(static_cast<int64_t>(std::lround(val)));
Update();
});
connect(m_iceRingWidthQRecipA, &SliderPlusBox::valueChanged, [this](double val) {
m_settings.ice_ring_width_Q_recipA = static_cast<float>(val);
Update();
});
UpdateResolvedAlgorithmLabel();
}
void JFJochViewerProcessingWindow::Update() {
const auto max_spots = std::lround(m_maxSpotCount->value());
emit settingsChanged(m_settings, m_indexing, max_spots);
}
void JFJochViewerProcessingWindow::setUnitCellKnown(bool known) {
m_unitCellKnown = known;
UpdateResolvedAlgorithmLabel();
}
void JFJochViewerProcessingWindow::UpdateResolvedAlgorithmLabel() {
// Mirror DiffractionExperiment::GetIndexingAlgorithm() so the user sees what Auto/FFBIDX
// actually resolve to on this machine (GPU present?) and dataset (unit cell known?).
const bool gpu = get_gpu_count() > 0;
IndexingAlgorithmEnum resolved;
switch (m_indexing.GetAlgorithm()) {
case IndexingAlgorithmEnum::FFBIDX:
resolved = m_unitCellKnown ? IndexingAlgorithmEnum::FFBIDX : IndexingAlgorithmEnum::None;
break;
case IndexingAlgorithmEnum::Auto:
resolved = !gpu ? IndexingAlgorithmEnum::FFTW
: (m_unitCellKnown ? IndexingAlgorithmEnum::FFBIDX : IndexingAlgorithmEnum::FFT);
break;
case IndexingAlgorithmEnum::FFT: resolved = IndexingAlgorithmEnum::FFT; break;
case IndexingAlgorithmEnum::FFTW: resolved = IndexingAlgorithmEnum::FFTW; break;
default: resolved = IndexingAlgorithmEnum::None; break;
}
m_resolvedAlgLabel->setText("Effective on this system: " + algorithm_name(resolved)
+ (gpu ? "" : " (no GPU detected)"));
}