Files
Jungfraujoch/viewer/widgets/PowderCalibrationWidget.cpp
T
leonarski_fandClaude Opus 5 03456dcddd calibration: an option to hold the detector tilt fixed
--mode calibration fits five parameters - beam centre, distance and the two
PONI tilts - and a program that cannot express a tilted detector has nowhere to
put the last two. Dropping them after the fact is worse than never fitting
them: the centre and the distance of a tilted fit have already absorbed the
tilt, so the flattened geometry is right nowhere.

rugnux --no-refine-tilt, the "Refine detector tilt" tick box on the viewer's
Calib page and RingOptimizer's refine_tilt argument hold rot1/rot2 at the value
the geometry came in with and fit the remaining three. That is the best
flat-detector answer, and the one such a program would refine to itself.

Measured on a five-distance calibrant series. At short distance the tilt is
real and reproducible - three independent fits agreeing to 0.01 deg, radial rms
1.4 -> 0.4 px - and its direct beam agrees with the background beam-centre
estimator to 0.05 px, so the tilted model is the physically right one. The
pinned fit then displaces the centre 2.6 px to absorb the tilt and lands within
0.03 px of the same place at every distance.

Past ~300 mm, where only two rings reach the detector, the tilt is instead
under-determined: it comes out with the opposite sign to every short-distance
fit and drags the PONI 28 px while the rms does not move (0.960 against 0.962).
The existing degeneracy guard only fires on a strictly single ring, so it does
not catch that; declining a tilt that does not pay for itself in rms is left
for a separate change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MxrrPcxodNiXzhNiECCVp5
2026-08-30 20:44:12 +02:00

125 lines
5.7 KiB
C++

// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "PowderCalibrationWidget.h"
#include <QPushButton>
#include <QGridLayout>
#include <QLabel>
#include "../../common/JFJochMath.h"
#include "../image_analysis/geom_refinement/AssignSpotsToRings.h"
#include "../image_analysis/geom_refinement/Calibrants.h"
PowderCalibrationWidget::PowderCalibrationWidget(QWidget *parent) : QWidget(parent) {
auto layout = new QVBoxLayout(this);
calibrantCombo = new QComboBox(this);
updateCalibrantList();
// How a whole-dataset run measures the rings. The two buttons below always work from the spots of
// the image on screen, so this only takes effect for "Analyze dataset".
methodCombo = new QComboBox(this);
methodCombo->addItem("Rings (summed profile)", static_cast<int>(CalibrationMethod::Rings));
methodCombo->addItem("Spots (pooled spot lists)", static_cast<int>(CalibrationMethod::Spots));
methodCombo->setToolTip("How \"Analyze dataset\" measures the powder rings: rings sums the "
"(q x azimuth) azimuthal profile over every image and fits the ring arcs in it; "
"spots pools the found spots and fits those.");
// The detector tilt is a free parameter of every one of these fits. Unticking it holds rot1/rot2
// where the geometry already has them - what a calibration handed to a program with no way to
// express a tilted detector (XDS) needs. It applies to the two buttons and to "Analyze dataset".
refineTiltCheck = new QCheckBox("Refine detector tilt", this);
refineTiltCheck->setChecked(true);
refineTiltCheck->setToolTip("Fit the detector tilt (PONI rot1/rot2) together with the beam centre "
"and the distance. Untick to hold the tilt where it is and fit only the "
"beam centre and the distance - for a calibration that will be given to "
"a program which cannot express a tilted detector, such as XDS.");
auto findBeamCenterButton = new QPushButton("Guess detector calibration", this);
connect(findBeamCenterButton, &QPushButton::clicked,this, &PowderCalibrationWidget::findBeamCenterClicked);
auto optimizeBeamCenterButton = new QPushButton("Refine detector calibration", this);
connect(optimizeBeamCenterButton, &QPushButton::clicked,this, &PowderCalibrationWidget::optimizeBeamCenterClicked);
auto calibrantRingsButton = new QPushButton("Display calibrant rings", this);
connect(calibrantRingsButton, &QPushButton::clicked, this, [this]() {
QVector<float> d_rings;
for (float q : GetCalibrantRings())
d_rings.append(2 * PI / q);
emit ringsFromCalibration(d_rings);
});
auto refine_row = new QGridLayout();
refine_row->setSpacing(12);
refine_row->addWidget(new QLabel("Calibrant:"),0,0);
refine_row->addWidget(calibrantCombo,0,1);
refine_row->addWidget(findBeamCenterButton,1, 0);
refine_row->addWidget(optimizeBeamCenterButton,1, 1);
refine_row->addWidget(calibrantRingsButton,2, 0);
refine_row->addWidget(refineTiltCheck,2, 1);
refine_row->addWidget(new QLabel("Dataset method:"),3,0);
refine_row->addWidget(methodCombo,3,1);
auto hint = new QLabel("The two buttons fit the image on screen; \"Analyze dataset\" fits the whole "
"run and writes a .poni.", this);
hint->setWordWrap(true);
hint->setStyleSheet("color: gray;");
layout->addLayout(refine_row);
layout->addWidget(hint);
setLayout(layout);
}
std::vector<float> PowderCalibrationWidget::GetCalibrantRings() const {
// The combo lists the shared calibrant table (in its order), then the current sample's own cell,
// which is not a powder standard but is what a single-crystal image's spots actually sit on.
const int idx = calibrantCombo->currentIndex();
const auto &table = Calibrants();
if (idx >= 0 && idx < static_cast<int>(table.size()))
return CalibrantRings(table[idx].name);
if (sample_cell)
return CalculateXtalRings(sample_cell.value());
return CalibrantRings(table.front().name);
}
CalibrationSelection PowderCalibrationWidget::Selection() const {
return {calibrantCombo->currentText(), GetCalibrantRings(),
static_cast<CalibrationMethod>(methodCombo->currentData().toInt()),
refineTiltCheck->isChecked()};
}
void PowderCalibrationWidget::updateCalibrantList() {
calibrantCombo->clear();
for (const auto &c : Calibrants())
calibrantCombo->addItem(QString::fromStdString(c.name));
if (sample_cell)
calibrantCombo->addItem(QString("Current sample (%1 %2 %3 %4 %5 %6)")
.arg(QString::number(sample_cell->a, 'f', 1))
.arg(QString::number(sample_cell->b, 'f', 1))
.arg(QString::number(sample_cell->c, 'f', 1))
.arg(QString::number(sample_cell->alpha, 'f', 1))
.arg(QString::number(sample_cell->beta, 'f', 1))
.arg(QString::number(sample_cell->gamma, 'f', 1)));
calibrantCombo->setCurrentIndex(0);
}
void PowderCalibrationWidget::loadImage(std::shared_ptr<const JFJochReaderImage> image) {
if (image)
sample_cell = image->Dataset().experiment.GetUnitCell();
updateCalibrantList();
}
void PowderCalibrationWidget::findBeamCenterClicked() {
const std::vector<float> rings = GetCalibrantRings();
emit findBeamCenter(QVector<float>(rings.begin(), rings.end()), true, refineTiltCheck->isChecked());
}
void PowderCalibrationWidget::optimizeBeamCenterClicked() {
const std::vector<float> rings = GetCalibrantRings();
emit findBeamCenter(QVector<float>(rings.begin(), rings.end()), false, refineTiltCheck->isChecked());
}