Build Packages / Create release (push) Successful in 7s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 4m56s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 9m10s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 13m51s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 16m56s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 20m59s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m25s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 21m17s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 14m14s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m59s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 25m48s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 15m52s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 19m33s
Build Packages / Generate python client (push) Successful in 29s
Build Packages / Build documentation (push) Successful in 1m19s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 20m49s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 25m31s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 25m24s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 26m5s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 22m19s
Build Packages / Unit tests (push) Failing after 2h28m24s
Build Packages / Validation Battery (push) Successful in 40m33s
* Rugnux: Processes a collection live while jfjoch_broker writes it (`rugnux <master.h5> <broker URL>`). * Rugnux: Processing starts on the first images and confirms its choices once all are in, so analysis can begin during collection. * Rugnux: Faster processing, especially on servers with several GPUs and CPU sockets and on computers without a GPU (`--no-gpu`, macOS). * Rugnux: Uses much less GPU memory, so large data sets stay on the GPU of smaller cards, and a data set too large for the card is processed on the CPU instead. * Rugnux: On macOS, Bragg integration runs on the Apple GPU. * Rugnux: Better handling of twinning and pseudo-symmetry, with the twin hypothesis written as `<prefix>_twin_<SG>.mtz`, and a wrong `--model` no longer confirms the data or decides the hand. * Rugnux: `--model` accepts predicted (AlphaFold) models and falls back to molecular replacement when the model is not placed as a rigid body (experimental `--mr` forces it), settling space group, hand and twinning and writing a different answer as `<prefix>_mr.mtz`. * Rugnux: Molecular replacement is faster and settles the space group in more cases with several copies in the asymmetric unit. * Rugnux: Deposition-ready mmCIF, a richer report, and numbered steps on the terminal with the full log in `<prefix>_log.txt`; `_P1.mtz`, `--estimate-beam-center` and `--beam-center-search` are removed. * Rugnux: The beam-stop shadow is found correctly on tilted detectors. * Rugnux: Stills files that record a stationary goniometer axis get the default stills geometry refinement. * jfjoch_viewer: "Analyze dataset" uses the same defaults as a bare `rugnux <file>`, and shows the results of processed CBF, marCCD and SMV sweeps. * jfjoch_viewer: The Files panel refreshes on F5 and every few seconds, and the plot type and other selections stay put during live collection. * jfjoch_writer: Memory stays bounded and old data files are closed efficiently on long runs, any failure ends the run in an error state the broker is told about, and the master file appears only after its data files; the ZeroMQ pusher logs when a stalled writer makes it drop images. * Fixes: The TCP image pusher no longer blocks the broker's receiver, GPU out-of-memory failures stop with a clear message, and the XDS plugin masks module and chip edges. --------- Co-authored-by: Filip Leonarski <jungfrau@filips-mac-mini.home> Reviewed-on: #85 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
147 lines
7.6 KiB
C++
147 lines
7.6 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 LiveComboBox(this);
|
|
// Without this a combo asks to be as wide as its longest entry - and it is never allowed to
|
|
// shrink below that, which is what used to pin the whole settings dock wide as soon as the
|
|
// powder section was expanded. The current text elides instead; the tooltip carries the rest.
|
|
calibrantCombo->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon);
|
|
calibrantCombo->setMinimumContentsLength(12);
|
|
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->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon);
|
|
methodCombo->setMinimumContentsLength(12);
|
|
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);
|
|
});
|
|
|
|
// One button per row rather than two side by side: the panel lives in a narrow left-hand dock,
|
|
// and two buttons abreast cannot be narrower than the sum of their two labels.
|
|
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(new QLabel("Dataset method:"),1,0);
|
|
refine_row->addWidget(methodCombo,1,1);
|
|
|
|
refine_row->addWidget(findBeamCenterButton,2, 0, 1, 2);
|
|
refine_row->addWidget(optimizeBeamCenterButton,3, 0, 1, 2);
|
|
refine_row->addWidget(calibrantRingsButton,4, 0, 1, 2);
|
|
refine_row->addWidget(refineTiltCheck,5, 0, 1, 2);
|
|
|
|
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) {
|
|
// With the group where the dataset has one: the absences then come from the symmetry rather
|
|
// than from a bare P-lattice assumption, and a centred cell's list no longer opens with a
|
|
// reflection that is not there - which would scale the whole calibration by the ratio between
|
|
// that ring and the first real one. Same call the CLI makes.
|
|
if (sample_sg)
|
|
return CalculateXtalRings(sample_cell.value(), *sample_sg);
|
|
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() {
|
|
// Called for every image, live ones included: the combo only changes when the list does, and
|
|
// keeps the user's calibrant by name.
|
|
QVector<LiveComboBox::Item> items;
|
|
for (const auto &c : Calibrants())
|
|
items.push_back({QString::fromStdString(c.name), QString::fromStdString(c.name)});
|
|
if (sample_cell) {
|
|
// The cell goes in the tooltip, not in the entry: a 40-character item would set the combo's
|
|
// minimum width and, through it, the minimum width of the dock this widget sits in.
|
|
items.push_back({"Current sample cell", "Current sample cell",
|
|
QString("%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->SetItems(items);
|
|
}
|
|
|
|
void PowderCalibrationWidget::loadImage(std::shared_ptr<const JFJochReaderImage> image) {
|
|
if (image) {
|
|
sample_cell = image->Dataset().experiment.GetUnitCell();
|
|
sample_sg = image->Dataset().experiment.GetGemmiSpaceGroup();
|
|
}
|
|
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());
|
|
}
|