diff --git a/viewer/CMakeLists.txt b/viewer/CMakeLists.txt index 2d41295f..105671d3 100644 --- a/viewer/CMakeLists.txt +++ b/viewer/CMakeLists.txt @@ -60,6 +60,8 @@ ADD_EXECUTABLE(jfjoch_viewer jfjoch_viewer.cpp JFJochViewerWindow.cpp JFJochView widgets/SliderPlusBox.h widgets/JFJochViewerSidePanelChart.cpp widgets/JFJochViewerSidePanelChart.h + widgets/JFJochViewerSettingsDock.cpp + widgets/JFJochViewerSettingsDock.h windows/JFJochViewerSpotListWindow.cpp windows/JFJochViewerSpotListWindow.h image_viewer/JFJochAzIntImage.cpp diff --git a/viewer/JFJochViewerWindow.cpp b/viewer/JFJochViewerWindow.cpp index 3befd9a9..ac1378fa 100644 --- a/viewer/JFJochViewerWindow.cpp +++ b/viewer/JFJochViewerWindow.cpp @@ -15,6 +15,7 @@ #include "JFJochImageReadingWorker.h" #include "image_viewer/JFJochDiffractionImage.h" #include "JFJochViewerSidePanel.h" +#include "widgets/JFJochViewerSettingsDock.h" #include "JFJochViewerStatusBar.h" #include "../common/CUDAWrapper.h" #include "windows/JFJochViewerImageListWindow.h" @@ -421,6 +422,32 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString lastActiveRunId = std::move(active); }); + // Inline settings dock: the surgical MX/AzInt subset, always visible (left), wired straight + // into the running analysis so edits re-run on the current image / dataset. + auto *settingsPanel = new JFJochViewerSettingsDock(spot_finding_settings, indexing_settings, + experiment.GetAzimuthalIntegrationSettings(), this); + settingsDock = new QDockWidget("Settings", this); + settingsDock->setObjectName("settingsDock"); + settingsDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); + settingsDock->setWidget(settingsPanel); + addDockWidget(Qt::LeftDockWidgetArea, settingsDock); + menuBar->AddDockEntry(settingsDock, "Settings"); + + connect(settingsPanel, &JFJochViewerSettingsDock::spotFindingChanged, + reading_worker, &JFJochImageReadingWorker::UpdateSpotFindingSettings); + connect(settingsPanel, &JFJochViewerSettingsDock::azintChanged, + reading_worker, &JFJochImageReadingWorker::UpdateAzintSettings); + connect(settingsPanel, &JFJochViewerSettingsDock::experimentChanged, + reading_worker, &JFJochImageReadingWorker::UpdateDataset); + connect(settingsPanel, &JFJochViewerSettingsDock::findBeamCenter, + reading_worker, &JFJochImageReadingWorker::FindCenter); + connect(reading_worker, &JFJochImageReadingWorker::datasetLoaded, + settingsPanel, &JFJochViewerSettingsDock::datasetLoaded); + connect(reading_worker, &JFJochImageReadingWorker::imageLoaded, + settingsPanel, &JFJochViewerSettingsDock::loadImage); + connect(settingsPanel, &JFJochViewerSettingsDock::ringsFromCalibration, + side_panel, &JFJochViewerSidePanel::SetRings); + // Dock the processing panel in the bottom-right corner, next to the dataset-info plots. processingDock = new QDockWidget("Processing", this); processingDock->setObjectName("processingDock"); @@ -464,9 +491,10 @@ JFJochViewerWindow::~JFJochViewerWindow() { } void JFJochViewerWindow::ApplyPerspective(Perspective p) { - // Image: just the image + inspector. Processing: also the dataset-info plots and jobs panel. + // Image: just the image + inspector. Processing: also settings, dataset-info plots, jobs panel. const bool processing = (p == Perspective::Processing); if (inspectorDock) inspectorDock->setVisible(true); + if (settingsDock) settingsDock->setVisible(processing); if (processingDock) processingDock->setVisible(processing); for (auto *d : findChildren()) if (d->objectName().startsWith("datasetInfoDock")) diff --git a/viewer/JFJochViewerWindow.h b/viewer/JFJochViewerWindow.h index ea19a1d3..3c5960fb 100644 --- a/viewer/JFJochViewerWindow.h +++ b/viewer/JFJochViewerWindow.h @@ -40,6 +40,7 @@ private: QDockWidget *lastDatasetInfoDock = nullptr; // most recent dataset-info dock, for docking layout QDockWidget *inspectorDock = nullptr; // image inspector (former right-hand side panel) + QDockWidget *settingsDock = nullptr; // inline MX/AzInt settings panel QDockWidget *processingDock = nullptr; // processing jobs panel QByteArray defaultLayoutState; // captured after construction, for "Reset layout" int datasetInfoCounter = 0; // gives each dataset-info dock a unique objectName diff --git a/viewer/widgets/JFJochViewerSettingsDock.cpp b/viewer/widgets/JFJochViewerSettingsDock.cpp new file mode 100644 index 00000000..2584a676 --- /dev/null +++ b/viewer/widgets/JFJochViewerSettingsDock.cpp @@ -0,0 +1,268 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#include "JFJochViewerSettingsDock.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#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(v); EmitSpotFinding(); }); + connect(count, &SliderPlusBox::valueChanged, this, [this](double v) { + spot_.photon_count_threshold = static_cast(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(IndexingAlgorithmEnum::Auto)); + algo->addItem("FFBIDX (GPU, known cell)", static_cast(IndexingAlgorithmEnum::FFBIDX)); + algo->addItem("FFT (GPU, de-novo)", static_cast(IndexingAlgorithmEnum::FFT)); + algo->addItem("FFTW (CPU, de-novo)", static_cast(IndexingAlgorithmEnum::FFTW)); + algo->addItem("None", static_cast(IndexingAlgorithmEnum::None)); + algo->setCurrentIndex(algo->findData(static_cast(indexing_.GetAlgorithm()))); + auto *refine = new QComboBox(page); + refine->addItem("None", static_cast(GeomRefinementAlgorithmEnum::None)); + refine->addItem("Orientation only", static_cast(GeomRefinementAlgorithmEnum::OrientationOnly)); + refine->addItem("Beam center + lattice", static_cast(GeomRefinementAlgorithmEnum::BeamCenter)); + refine->addItem("Pixel refinement", static_cast(GeomRefinementAlgorithmEnum::PixelRefine)); + refine->setCurrentIndex(refine->findData(static_cast(indexing_.GetGeomRefinementAlgorithm()))); + idx->addRow("Algorithm", algo); + idx->addRow("Refinement", refine); + layout->addWidget(idxGroup); + connect(algo, &QComboBox::currentIndexChanged, this, [this, algo] { + indexing_.Algorithm(static_cast(algo->currentData().toInt())); + EmitSpotFinding(); + }); + connect(refine, &QComboBox::currentIndexChanged, this, [this, refine] { + indexing_.GeomRefinementAlgorithm(static_cast(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(lowQ->value()), static_cast(highQ->value())); + azint_.QSpacing_recipA(static_cast(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(energy_->value())); + experiment_.DetectorDistance_mm(static_cast(distance_->value())); + experiment_.BeamX_pxl(static_cast(beamX_->value())); + experiment_.BeamY_pxl(static_cast(beamY_->value())); + if (cellKnown_->isChecked()) { + experiment_.SetUnitCell(UnitCell{ + static_cast(cellA_->value()), static_cast(cellB_->value()), + static_cast(cellC_->value()), static_cast(cellAlpha_->value()), + static_cast(cellBeta_->value()), static_cast(cellGamma_->value())}); + const int sg = static_cast(std::lround(spaceGroup_->value())); + experiment_.SpaceGroupNumber(sg > 0 ? std::optional(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 dataset) { + if (!dataset) + return; + experiment_ = dataset->experiment; + have_experiment_ = true; + RefreshGeometryFields(); +} + +void JFJochViewerSettingsDock::loadImage(std::shared_ptr image) { + if (powder_) + powder_->loadImage(image); +} diff --git a/viewer/widgets/JFJochViewerSettingsDock.h b/viewer/widgets/JFJochViewerSettingsDock.h new file mode 100644 index 00000000..4794df21 --- /dev/null +++ b/viewer/widgets/JFJochViewerSettingsDock.h @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include +#include + +#include "../../common/DiffractionExperiment.h" +#include "../../common/IndexingSettings.h" +#include "../../common/AzimuthalIntegrationSettings.h" +#include "../../image_analysis/spot_finding/SpotFindingSettings.h" +#include "../../reader/JFJochReaderDataset.h" +#include "../../reader/JFJochReaderImage.h" + +class QStackedWidget; +class QCheckBox; +class QComboBox; +class SliderPlusBox; +class NumberLineEdit; +class PowderCalibrationWidget; + +// Compact, always-visible settings panel with an MX / AzInt toggle: the surgical subset of +// processing settings that the two communities reach for most, kept in a dock instead of a +// hidden window. Edits feed straight back into the running analysis via the worker. +class JFJochViewerSettingsDock : public QWidget { + Q_OBJECT +public: + JFJochViewerSettingsDock(const SpotFindingSettings &spot, const IndexingSettings &indexing, + const AzimuthalIntegrationSettings &azint, QWidget *parent = nullptr); + +public slots: + void datasetLoaded(std::shared_ptr dataset); + void loadImage(std::shared_ptr image); + +signals: + void spotFindingChanged(const SpotFindingSettings &spot, const IndexingSettings &indexing, int64_t max_spots); + void azintChanged(const AzimuthalIntegrationSettings &settings); + void experimentChanged(const DiffractionExperiment &experiment); + void findBeamCenter(const UnitCell &calibrant, bool guess); + void ringsFromCalibration(QVector rings); + +private: + SpotFindingSettings spot_; + IndexingSettings indexing_; + AzimuthalIntegrationSettings azint_; + DiffractionExperiment experiment_; + bool have_experiment_ = false; // geometry edits only take effect once a dataset is loaded + int64_t max_spots_ = 1000; + + // MX geometry + crystal fields + NumberLineEdit *energy_ = nullptr; + NumberLineEdit *distance_ = nullptr; + NumberLineEdit *beamX_ = nullptr; + NumberLineEdit *beamY_ = nullptr; + QCheckBox *cellKnown_ = nullptr; + NumberLineEdit *cellA_ = nullptr, *cellB_ = nullptr, *cellC_ = nullptr; + NumberLineEdit *cellAlpha_ = nullptr, *cellBeta_ = nullptr, *cellGamma_ = nullptr; + NumberLineEdit *spaceGroup_ = nullptr; + + PowderCalibrationWidget *powder_ = nullptr; + + QWidget *BuildMXPage(); + QWidget *BuildAzIntPage(); + void EmitSpotFinding(); + void EmitExperiment(); + void RefreshGeometryFields(); +}; diff --git a/viewer/widgets/PowderCalibrationWidget.cpp b/viewer/widgets/PowderCalibrationWidget.cpp index 286f3c8a..8e24bf94 100644 --- a/viewer/widgets/PowderCalibrationWidget.cpp +++ b/viewer/widgets/PowderCalibrationWidget.cpp @@ -52,6 +52,14 @@ UnitCell PowderCalibrationWidget::GetCalibrant() const { uc = UnitCell(5.1769, 4.7218, 58.380, 89.440, 89.634, 75.854); break; case 2: + // CeO2, cubic fluorite (a = 5.4115 Å), NIST SRM 674b. + uc = UnitCell(5.4115, 5.4115, 5.4115, 90, 90, 90); + break; + case 3: + // Silicon, cubic (a = 5.43102 Å), NIST SRM 640. + uc = UnitCell(5.43102, 5.43102, 5.43102, 90, 90, 90); + break; + case 4: if (sample_cell) uc = sample_cell.value(); break; @@ -66,6 +74,8 @@ void PowderCalibrationWidget::updateCalibrantList() { calibrantCombo->addItem("LaB6"); calibrantCombo->addItem("Silver Behenate"); + calibrantCombo->addItem("CeO2"); + calibrantCombo->addItem("Silicon"); if (sample_cell) calibrantCombo->addItem(QString("Current sample (%1 %2 %3 %4 %5 %6)") .arg(QString::number(sample_cell->a, 'f', 1))