// 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 #include #include #include #include "SliderPlusBox.h" #include "NumberLineEdit.h" #include "PowderCalibrationWidget.h" #include "CollapsibleSection.h" #include "../../common/JFJochMath.h" #include "../../common/CUDAWrapper.h" #include "../../gemmi_gph/gemmi/symmetry.hpp" namespace { // A single diffraction frame (image) vs a stack of frames (dataset), drawn white so they read on // the navy "Analyze" hero buttons. QIcon FramesIcon(int frames) { const int S = 28; QPixmap pm(S, S); pm.fill(Qt::transparent); QPainter p(&pm); p.setRenderHint(QPainter::Antialiasing); p.setPen(QPen(Qt::white, 2.0)); p.setBrush(Qt::NoBrush); const double side = 13.0, step = 4.0; for (int i = frames - 1; i >= 0; --i) p.drawRoundedRect(QRectF(4 + i * step, 4 + i * step, side, side), 2.5, 2.5); p.setBrush(Qt::white); // a diffraction "spot" in the front frame p.drawEllipse(QPointF(4 + side / 2.0, 4 + side / 2.0), 2.0, 2.0); p.end(); return QIcon(pm); } } JFJochViewerSettingsDock::JFJochViewerSettingsDock(const SpotFindingSettings &spot, const IndexingSettings &indexing, const AzimuthalIntegrationSettings &azint, const BraggIntegrationSettings &bragg, const ScalingSettings &scaling, QWidget *parent) : QWidget(parent), spot_(spot), indexing_(indexing), azint_(azint), bragg_(bragg), scaling_(scaling) { auto *layout = new QVBoxLayout(this); // The two analysis actions sit on top of the panel. "Analyze image" is a toggle (re-analyse the // current frame now and on every change while armed); "Analyze dataset" launches a processing job // whose kind (MX vs azimuthal) is decided by the MX/AzInt toggle below — no separate switch. const QString heroStyle = "QPushButton { background-color:#1F3A5F; color:white; border:none; border-radius:3px;" " padding:5px 10px; } QPushButton:hover { background-color:#16314f; }" " QPushButton:checked { background-color:#FA7268; } QPushButton:disabled { background-color:#9aa6b3; }"; auto *analyzeImageBtn = new QPushButton(FramesIcon(1), " Analyze image", this); analyzeImageBtn->setCheckable(true); analyzeImageBtn->setStyleSheet(heroStyle); analyzeImageBtn->setToolTip("Re-analyse the current image now, and keep re-analysing on every" " image / settings change while active"); auto *analyzeDatasetBtn = new QPushButton(FramesIcon(3), " Analyze dataset", this); analyzeDatasetBtn->setStyleSheet(heroStyle); analyzeDatasetBtn->setToolTip("Process the whole dataset (MX or azimuthal, per the toggle below)"); auto *analyzeRow = new QHBoxLayout(); analyzeRow->addWidget(analyzeImageBtn); analyzeRow->addWidget(analyzeDatasetBtn); layout->addLayout(analyzeRow); layout->addSpacing(10); connect(analyzeImageBtn, &QPushButton::toggled, this, &JFJochViewerSettingsDock::reanalyzeImage); connect(analyzeDatasetBtn, &QPushButton::clicked, this, [this] { emit analyzeDataset(azint_mode_); }); // 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, this, [this, stack](int id) { azint_mode_ = (id == 1); stack->setCurrentIndex(id); }); // Geometry is common to both communities, so it lives above the toggle rather than per page. 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(); UpdateAlgorithmDescription(); // Auto resolves to FFBIDX vs FFT depending on a known cell 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 *highResSpot = new SliderPlusBox(0.5, 5.0, 0.1, 1, page); highResSpot->setValue(spot_.high_resolution_limit); auto *minPix = new NumberLineEdit(1.0f, 50.0f, static_cast(spot_.min_pix_per_spot), 0, "px", page); auto *maxSpots = new NumberLineEdit(10.0f, 100000.0f, static_cast(max_spots_), 0, "", page); spot->addRow("Signal/noise", snr); spot->addRow("Photon count", count); spot->addRow("High resolution [Å]", highResSpot); spot->addRow("Min pixels/spot", minPix); spot->addRow("Max spots/image", maxSpots); spotSection->setContentLayout(spot); spotSection->setExpanded(false); layout->addWidget(spotSection); 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 = std::llround(v); EmitSpotFinding(); }); connect(highResSpot, &SliderPlusBox::valueChanged, this, [this](double v) { spot_.high_resolution_limit = static_cast(v); EmitSpotFinding(); }); connect(minPix, &NumberLineEdit::newValue, this, [this, minPix] { spot_.min_pix_per_spot = std::llround(minPix->value()); EmitSpotFinding(); }); connect(maxSpots, &NumberLineEdit::newValue, this, [this, maxSpots] { max_spots_ = std::llround(maxSpots->value()); EmitSpotFinding(); }); // --- Indexing --- auto *idxSection = new CollapsibleSection("Indexing", page); auto *idx = new QFormLayout(); idx->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); 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()))); algoDesc_ = new QLabel(page); algoDesc_->setWordWrap(true); algoDesc_->setStyleSheet("color: gray;"); 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->setCurrentIndex(refine->findData(static_cast(indexing_.GetGeomRefinementAlgorithm()))); // One high-level mode switch instead of separate partiality / rot3d / rotation-indexing options: // for a rotation dataset, unchecked = the rotation good-path (rotation indexing + Rotation // partiality + rot3d combine + scale-fulls), checked = treat it as stills (fixed partiality, // per-frame indexing). Disabled (and a no-op) for datasets that are already stills. stills_ = new QCheckBox("Process as stills", page); stills_->setEnabled(false); // datasetLoaded enables it only for rotation (goniometer) datasets stills_->setToolTip("Treat a rotation dataset as independent stills (fixed partiality, per-frame " "indexing). Unchecked on a rotation dataset = rotation indexing + 3D rotation scaling."); idx->addRow("Algorithm", algo_); idx->addRow("", algoDesc_); idx->addRow("Refinement", refine); idx->addRow("", stills_); idxSection->setContentLayout(idx); idxSection->setExpanded(false); layout->addWidget(idxSection); connect(algo_, &QComboBox::currentIndexChanged, this, [this] { indexing_.Algorithm(static_cast(algo_->currentData().toInt())); UpdateAlgorithmDescription(); EmitSpotFinding(); }); connect(refine, &QComboBox::currentIndexChanged, this, [this, refine] { indexing_.GeomRefinementAlgorithm(static_cast(refine->currentData().toInt())); EmitSpotFinding(); }); connect(stills_, &QCheckBox::toggled, this, [this] { ApplyProcessingMode(); }); UpdateAlgorithmDescription(); layout->addWidget(BuildBraggSection()); layout->addWidget(BuildScalingSection()); layout->addWidget(BuildReferenceSection()); layout->addStretch(); // anchor sections to the top so expanding an accordion grows downward return page; } QWidget *JFJochViewerSettingsDock::BuildReferenceSection() { // A reference dataset for scaling: drives CCref and reference-based scaling. It is // independent of the loaded data (the worker keeps it across file switches); this just lets the // user pick the MTZ + column and shows what it contains and whether it matches the data. auto *section = new CollapsibleSection("Reference dataset", this); auto *form = new QFormLayout(); form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); refButton_ = new QPushButton("Choose MTZ…", this); refColumn_ = new QComboBox(this); refColumn_->setEnabled(false); refColumn_->setToolTip("Reference intensity / structure-factor column (F is squared to an intensity)."); refSummary_ = new QLabel("No reference loaded", this); refSummary_->setWordWrap(true); refWarning_ = new QLabel(this); refWarning_->setWordWrap(true); refWarning_->setVisible(false); form->addRow(refButton_); form->addRow("Column", refColumn_); form->addRow(refSummary_); form->addRow(refWarning_); section->setContentLayout(form); section->setExpanded(false); // folded on start (only geometry + unit cell start open) connect(refButton_, &QPushButton::clicked, this, [this] { const QString path = QFileDialog::getOpenFileName(this, "Reference MTZ", refPath_, "MTZ files (*.mtz);;All files (*)"); if (path.isEmpty()) return; refPath_ = path; emit referenceSelected(path, QString()); // empty column -> let the worker auto-select }); // activated (not currentIndexChanged) so re-populating the combo on load doesn't re-trigger. connect(refColumn_, &QComboBox::activated, this, [this] { if (!refPath_.isEmpty()) emit referenceSelected(refPath_, refColumn_->currentText()); }); return section; } 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); azSection->setExpanded(false); layout->addWidget(azSection); 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. 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); powderSection->setExpanded(false); 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::UpdateAlgorithmDescription() { if (!algo_ || !algoDesc_) return; const auto a = static_cast(algo_->currentData().toInt()); const bool gpu = get_gpu_count() > 0; const bool cell_known = cellKnown_ && cellKnown_->isChecked(); QString text; switch (a) { case IndexingAlgorithmEnum::FFBIDX: text = "GPU, needs a known cell — best for sparse serial stills"; break; case IndexingAlgorithmEnum::FFT: text = "GPU, de-novo — best for strong rotation data"; break; case IndexingAlgorithmEnum::FFTW: text = "CPU, de-novo — no GPU needed"; break; case IndexingAlgorithmEnum::Auto: { const IndexingAlgorithmEnum r = !gpu ? IndexingAlgorithmEnum::FFTW : (cell_known ? IndexingAlgorithmEnum::FFBIDX : IndexingAlgorithmEnum::FFT); const QString rn = r == IndexingAlgorithmEnum::FFBIDX ? "FFBIDX" : r == IndexingAlgorithmEnum::FFT ? "FFT" : "FFTW"; text = QString("resolves to %1 here (%2, cell %3)") .arg(rn, gpu ? "GPU" : "no GPU", cell_known ? "known" : "unknown"); break; } default: text = "no indexing"; break; } algoDesc_->setText(text); } QWidget *JFJochViewerSettingsDock::BuildBraggSection() { auto *section = new CollapsibleSection("Bragg integration", this); auto *form = new QFormLayout(); form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); auto *gaussian = new QCheckBox("Gaussian profile fit", this); gaussian->setChecked(bragg_.GetIntegrator() != IntegratorMode::BoxSum); gaussian->setToolTip("Profile-fit the spots (more accurate intensities); off = classical box-sum."); auto *r1 = new NumberLineEdit(1.0f, 30.0f, bragg_.GetR1(), 1, "px", this); auto *r2 = new NumberLineEdit(1.0f, 30.0f, bragg_.GetR2(), 1, "px", this); auto *r3 = new NumberLineEdit(1.0f, 30.0f, bragg_.GetR3(), 1, "px", this); auto *radii = new QHBoxLayout(); radii->addWidget(r1); radii->addWidget(r2); radii->addWidget(r3); form->addRow("", gaussian); form->addRow("Radii r1/r2/r3", radii); section->setContentLayout(form); section->setExpanded(false); // folded on start (only geometry + unit cell start open) auto emitBragg = [=, this] { bragg_.Integrator(gaussian->isChecked() ? IntegratorMode::ProfileGaussian : IntegratorMode::BoxSum); bragg_.R1(static_cast(r1->value())).R2(static_cast(r2->value())) .R3(static_cast(r3->value())); emit braggChanged(bragg_); }; connect(gaussian, &QCheckBox::toggled, this, [emitBragg] { emitBragg(); }); for (auto *f : {r1, r2, r3}) connect(f, &NumberLineEdit::newValue, this, [emitBragg] { emitBragg(); }); return section; } QWidget *JFJochViewerSettingsDock::BuildScalingSection() { // The partiality model + rot3d combine + scale-fulls are driven by "Process as stills" (indexing // section); the panel keeps the full scaling_ so those fields are preserved here, not reset. auto *section = new CollapsibleSection("Scaling", this); auto *form = new QFormLayout(); form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); auto *friedel = new QCheckBox("Merge Friedel pairs", this); friedel->setChecked(scaling_.GetMergeFriedel()); auto *refineB = new QCheckBox("Refine B-factor", this); refineB->setChecked(scaling_.GetRefineB()); auto *limitRes = new QCheckBox("High-resolution limit", this); limitRes->setChecked(scaling_.GetHighResolutionLimit_A().has_value()); auto *highRes = new NumberLineEdit(0.3f, 5.0f, scaling_.GetHighResolutionLimit_A().value_or(2.0), 1, "Å", this); highRes->setEnabled(limitRes->isChecked()); form->addRow("", friedel); form->addRow("", refineB); // Compact, and aligned with the checkboxes above: the limit checkbox + value sit together in the // field column (not as a row label, which would indent it differently). auto *resRow = new QHBoxLayout(); resRow->addWidget(limitRes); resRow->addWidget(highRes, 1); form->addRow("", resRow); section->setContentLayout(form); section->setExpanded(false); // folded on start (only geometry + unit cell start open) auto emitScaling = [=, this] { scaling_.MergeFriedel(friedel->isChecked()); scaling_.RefineB(refineB->isChecked()); scaling_.HighResolutionLimit_A(limitRes->isChecked() ? std::optional(highRes->value()) : std::nullopt); emit scalingChanged(scaling_); }; connect(friedel, &QCheckBox::toggled, this, [emitScaling] { emitScaling(); }); connect(refineB, &QCheckBox::toggled, this, [emitScaling] { emitScaling(); }); connect(limitRes, &QCheckBox::toggled, this, [emitScaling, highRes](bool on) { highRes->setEnabled(on); emitScaling(); }); connect(highRes, &NumberLineEdit::newValue, this, [emitScaling] { emitScaling(); }); return section; } 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())); experiment_.PoniRot1_rad(static_cast(rot1_->value() * PI / 180.0)); experiment_.PoniRot2_rad(static_cast(rot2_->value() * PI / 180.0)); 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()); 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(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 dataset) { if (!dataset) return; experiment_ = dataset->experiment; have_experiment_ = true; RefreshGeometryFields(); // "Process as stills" only applies to a rotation dataset; enable it accordingly and apply the // resulting indexing/scaling mode so it reaches the worker. if (stills_) { stills_->setEnabled(experiment_.GetGoniometer().has_value()); ApplyProcessingMode(); } } void JFJochViewerSettingsDock::ApplyProcessingMode() { // Rotation good-path unless the dataset is stills or the user forces "Process as stills". const bool rotation_data = experiment_.GetGoniometer().has_value(); const bool rotation_mode = rotation_data && stills_ && !stills_->isChecked(); indexing_.RotationIndexing(rotation_mode); scaling_.SetPartialityModel(rotation_mode ? PartialityModel::Rotation : PartialityModel::Fixed); scaling_.Combine3D(rotation_mode); scaling_.ScaleFulls(rotation_mode); EmitSpotFinding(); // carries indexing_ (incl. RotationIndexing) to the worker emit scalingChanged(scaling_); } void JFJochViewerSettingsDock::loadImage(std::shared_ptr image) { if (powder_) powder_->loadImage(image); } void JFJochViewerSettingsDock::referenceLoaded(ReferenceMtzInfo info) { { QSignalBlocker block(refColumn_); // re-populating must not emit referenceSelected refColumn_->clear(); refColumn_->addItems(info.columns); const int idx = refColumn_->findText(info.used_column); if (idx >= 0) refColumn_->setCurrentIndex(idx); } refColumn_->setEnabled(info.loaded && !info.columns.isEmpty()); refSummary_->setText(info.loaded ? info.summary : "No reference loaded"); if (info.warning.isEmpty()) { refWarning_->setVisible(false); } else { // Amber = a loaded-but-mismatched reference (a caution); red = a load failure. refWarning_->setStyleSheet(info.loaded ? "color: #B8860B;" : "color: #C0392B;"); refWarning_->setText(info.warning); refWarning_->setVisible(true); } }