// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #include "JFJochViewerProcessingWindow.h" #include #include #include #include #include #include #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 &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(IndexingAlgorithmEnum::Auto)}, {"FFBIDX — GPU, needs known cell", static_cast(IndexingAlgorithmEnum::FFBIDX)}, {"FFT — GPU, de-novo", static_cast(IndexingAlgorithmEnum::FFT)}, {"FFTW — CPU, de-novo", static_cast(IndexingAlgorithmEnum::FFTW)}, {"None — skip indexing", static_cast(IndexingAlgorithmEnum::None)}}, static_cast(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(IndexingAlgorithmEnum::FFBIDX), static_cast(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(GeomRefinementAlgorithmEnum::None)}, {"Orientation only", static_cast(GeomRefinementAlgorithmEnum::OrientationOnly)}, {"Beam center + lattice", static_cast(GeomRefinementAlgorithmEnum::BeamCenter)}, {"Pixel refinement (experimental)", static_cast(GeomRefinementAlgorithmEnum::PixelRefine)}}, static_cast(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(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( 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(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(id)); UpdateResolvedAlgorithmLabel(); Update(); }); connect(m_geomRefGroup, &QButtonGroup::idClicked, [this](int id) { m_indexing.GeomRefinementAlgorithm(static_cast(id)); Update(); }); // Indexing settings signals connect(m_idxTolerance, &SliderPlusBox::valueChanged, [this](double val) { m_indexing.Tolerance(static_cast(val)); Update(); }); connect(m_idxUnitCellDistTolerance, &SliderPlusBox::valueChanged, [this](double val) { m_indexing.UnitCellDistTolerance(static_cast(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(std::lround(val))); Update(); }); connect(m_iceRingWidthQRecipA, &SliderPlusBox::valueChanged, [this](double val) { m_settings.ice_ring_width_Q_recipA = static_cast(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)")); }