339 lines
14 KiB
C++
339 lines
14 KiB
C++
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "JFJochViewerImageStatistics.h"
|
|
#include "../../common/time_utc.h"
|
|
#include <QFormLayout>
|
|
#include <QRegularExpression>
|
|
|
|
static QString mkUnitCell(const UnitCell &uc) {
|
|
return QString("<b>%1</b> Å <b>%2</b> Å <b>%3</b> Å <b>%4</b>° <b>%5</b>° <b>%6</b>°")
|
|
.arg(QString::number(uc.a, 'f', 1))
|
|
.arg(QString::number(uc.b, 'f', 1))
|
|
.arg(QString::number(uc.c, 'f', 1))
|
|
.arg(QString::number(uc.alpha, 'f', 1))
|
|
.arg(QString::number(uc.beta, 'f', 1))
|
|
.arg(QString::number(uc.gamma, 'f', 1));
|
|
}
|
|
|
|
static QString mkSourceInstrumentText(const DiffractionExperiment& exp) {
|
|
QString src, inst;
|
|
const auto &m = exp.GetInstrumentMetadata();
|
|
if (!m.GetSourceName().empty()) src = QString::fromStdString(m.GetSourceName());
|
|
if (!m.GetInstrumentName().empty()) inst = QString::fromStdString(m.GetInstrumentName());
|
|
|
|
if (!src.isEmpty() && !inst.isEmpty()) return src + " / " + inst;
|
|
if (!src.isEmpty()) return src;
|
|
if (!inst.isEmpty()) return inst;
|
|
return QString("-");
|
|
}
|
|
|
|
static QString mkSourceInstrumentTooltip(const DiffractionExperiment &exp) {
|
|
QStringList tooltips;
|
|
|
|
if (exp.GetRingCurrent_mA().has_value()) {
|
|
tooltips << QString("Ring current: <b>%1</b> mA").arg(exp.GetRingCurrent_mA().value(), 0, 'f', 2);
|
|
}
|
|
|
|
if (exp.GetTotalFlux().has_value()) {
|
|
tooltips << QString("Total flux: <b>%1</b> ph/s").arg(exp.GetTotalFlux().value(), 0, 'e', 2);
|
|
}
|
|
|
|
if (exp.GetAttenuatorTransmission().has_value()) {
|
|
tooltips << QString("Attenuation: <b>%1</b>").arg(exp.GetAttenuatorTransmission().value(), 0, 'f', 3);
|
|
}
|
|
|
|
return tooltips.join("<br/>");
|
|
}
|
|
|
|
static QString mkSampleText(const DiffractionExperiment& exp) {
|
|
const auto& name = exp.GetSampleName();
|
|
return name.empty() ? QString("-") : QString::fromStdString(name);
|
|
}
|
|
|
|
static QString mkSampleTooltip(const DiffractionExperiment& exp) {
|
|
if (exp.GetSampleTemperature_K().has_value()) {
|
|
return QString("Sample temperature: <b>%1</b> K").arg(exp.GetSampleTemperature_K().value(), 0, 'f', 2);
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
static QString mkSymmetry(const LatticeMessage& sym) {
|
|
QString text("Bravais lattice: <b>");
|
|
switch (sym.crystal_system) {
|
|
case gemmi::CrystalSystem::Triclinic:
|
|
text += "a";
|
|
break;
|
|
case gemmi::CrystalSystem::Monoclinic:
|
|
text += "m";
|
|
break;
|
|
case gemmi::CrystalSystem::Orthorhombic:
|
|
text += "o";
|
|
break;
|
|
case gemmi::CrystalSystem::Tetragonal:
|
|
text += "t";
|
|
break;
|
|
case gemmi::CrystalSystem::Hexagonal:
|
|
text += "h";
|
|
break;
|
|
case gemmi::CrystalSystem::Cubic:
|
|
text += "c";
|
|
break;
|
|
default: ;
|
|
}
|
|
text += QString(sym.centering) + "</b><br/>";
|
|
text += QString("Niggli class <b>%1</b><br/>").arg(sym.niggli_class);
|
|
return text;
|
|
}
|
|
|
|
JFJochViewerImageStatistics::JFJochViewerImageStatistics(QWidget *parent) : QWidget(parent) {
|
|
QFormLayout* layout = new QFormLayout(this);
|
|
|
|
dataset_name = new QLabel(this);
|
|
layout->addRow(new QLabel("Dataset:"), dataset_name);
|
|
|
|
detector_name = new QLabel(this);
|
|
layout->addRow(new QLabel("Detector:"), detector_name);
|
|
|
|
source_name = new QLabel(this);
|
|
layout->addRow("Source / Instrument:", source_name);
|
|
|
|
sample_name = new QLabel(this);
|
|
layout->addRow("Sample:", sample_name);
|
|
|
|
exposure_time = new QLabel(this);
|
|
layout->addRow(new QLabel("Exposure Time:"), exposure_time);
|
|
|
|
rotation_angle_label = new QLabel(this);
|
|
rotation_angle = new QLabel(this);
|
|
layout->addRow(rotation_angle_label, rotation_angle);
|
|
|
|
valid_values = new QLabel(this);
|
|
layout->addRow(new QLabel("Valid values:"), valid_values);
|
|
|
|
masked_pixels = new QLabel(this);
|
|
layout->addRow(new QLabel("Masked pixels:"), masked_pixels);
|
|
|
|
spots = new QLabel(this);
|
|
layout->addRow(new QLabel("Spots:"), spots);
|
|
|
|
bkg_estimate = new QLabel(this);
|
|
layout->addRow(new QLabel("Background:"), bkg_estimate);
|
|
|
|
indexed = new QLabel(this);
|
|
layout->addRow(new QLabel("Indexing:"), indexed);
|
|
|
|
res_estimate = new QLabel(this);
|
|
layout->addRow(new QLabel("Resolution estimate:"), res_estimate);
|
|
|
|
profile_radius = new QLabel(this);
|
|
layout->addRow(new QLabel("Profile radius:"), profile_radius);
|
|
|
|
b_factor = new QLabel(this);
|
|
layout->addRow(new QLabel("B-factor:"), b_factor);
|
|
}
|
|
|
|
QString TrimZeros(double number, int precision) {
|
|
auto s = QString::number(number, 'f', precision);
|
|
s.remove(QRegularExpression("\\.?0+$"));
|
|
if (s.isEmpty()) s = "0";
|
|
return s;
|
|
}
|
|
|
|
QString FormatTime(std::chrono::microseconds time) {
|
|
return TrimZeros(time.count()/1e6, 6);
|
|
}
|
|
|
|
void JFJochViewerImageStatistics::loadImage(std::shared_ptr<const JFJochReaderImage> image) {
|
|
if (!image) {
|
|
source_name->setText("");
|
|
sample_name->setText("");
|
|
dataset_name->setText("");
|
|
dataset_name->setToolTip("");
|
|
detector_name->setText("");
|
|
detector_name->setToolTip("");
|
|
exposure_time->setText("");
|
|
exposure_time->setToolTip("");
|
|
rotation_angle->setText("");
|
|
rotation_angle->setToolTip("");
|
|
rotation_angle_label->setText("");
|
|
valid_values->setText("");
|
|
valid_values->setToolTip("");
|
|
spots->setText("");
|
|
bkg_estimate->setText("");
|
|
indexed->setText("");
|
|
indexed->setToolTip("");
|
|
b_factor->setText("");
|
|
profile_radius->setText("");
|
|
res_estimate->setText("");
|
|
masked_pixels->setText("");
|
|
masked_pixels->setToolTip("");
|
|
return;
|
|
}
|
|
|
|
QString text;
|
|
|
|
auto &exp = image->Dataset().experiment;
|
|
|
|
text = QString("<b>%1</b>").arg(QString::fromStdString(exp.GetFilePrefix()));
|
|
dataset_name->setText(text);
|
|
dataset_name->setToolTip(QString("Collection data: <b>%1</b><br/>Beam center: <b>%2</b> <b>%3</b> pxl<br/>Detector distance: <b>%4</b> mm<br/>Energy: <b>%5</b> eV<br/>Wavelength: <b>%6</b> Å")
|
|
.arg(QString::fromStdString(utc_to_local_human_readable(image->Dataset().arm_date)))
|
|
.arg(QString::number(exp.GetBeamX_pxl(), 'f', 2))
|
|
.arg(QString::number(exp.GetBeamY_pxl(), 'f', 2))
|
|
.arg(QString::number(exp.GetDetectorDistance_mm(), 'f', 3))
|
|
.arg(QString::number(exp.GetIncidentEnergy_keV() * 1000.0, 'f', 0))
|
|
.arg(QString::number(exp.GetWavelength_A(), 'f', 3)));
|
|
|
|
text = QString("<b>%1</b>").arg(QString::fromStdString(exp.GetDetectorDescription()));
|
|
detector_name->setToolTip(QString("Width: <b>%1</b> pxl (<b>%2</b> mm)<br/>Height: <b>%3</b> pxl (<b>%4</b> mm)<br/>Pixel size: <b>%5</b> μm")
|
|
.arg(QString::number(exp.GetXPixelsNum()))
|
|
.arg(QString::number(exp.GetXPixelsNum() * exp.GetPixelSize_mm(), 'f', 3))
|
|
.arg(QString::number(exp.GetYPixelsNum()))
|
|
.arg(QString::number(exp.GetYPixelsNum() * exp.GetPixelSize_mm(), 'f', 3))
|
|
.arg(QString::number(exp.GetPixelSize_mm() * 1000.0, 'f', 0)));
|
|
detector_name->setText(text);
|
|
|
|
source_name->setText(mkSourceInstrumentText(exp));
|
|
source_name->setToolTip(mkSourceInstrumentTooltip(exp));
|
|
|
|
sample_name->setText(mkSampleText(exp));
|
|
sample_name->setToolTip(mkSampleTooltip(exp));
|
|
|
|
if (exp.GetGoniometer()) {
|
|
rotation_angle_label->setText("Image angle:");
|
|
rotation_angle->setText(QString("<b>%1°</b>")
|
|
.arg(TrimZeros(exp.GetGoniometer()->GetIncrement_deg(), 3)));
|
|
rotation_angle->setToolTip(QString("Start angle: <b>%1°</b><br/>This image: <b>%2°</b>")
|
|
.arg(TrimZeros(exp.GetGoniometer()->GetStart_deg(), 3))
|
|
.arg(TrimZeros(exp.GetGoniometer()->GetAngle_deg(image->ImageData().number), 3))
|
|
);
|
|
} else if (exp.GetGridScan()) {
|
|
rotation_angle_label->setText("Grid scan:");
|
|
rotation_angle->setText(QString("<b>%1</b> x <b>%2</b> μm")
|
|
.arg(QString::number(exp.GetGridScan()->GetGridStepX_um(), 'f', 1))
|
|
.arg(QString::number(exp.GetGridScan()->GetGridStepY_um(), 'f', 1)));
|
|
rotation_angle->setToolTip(QString("Grid size: <b>%1</b> x <b>%2</b> μm<br/>"
|
|
"Grid elements: <b>%3</b> x <b>%4</b>")
|
|
.arg(QString::number(exp.GetGridScan()->GetGridSizeX_um()))
|
|
.arg(QString::number(exp.GetGridScan()->GetGridSizeY_um()))
|
|
.arg(QString::number(exp.GetGridScan()->GetGridSizeX_step()))
|
|
.arg(QString::number(exp.GetGridScan()->GetGridSizeY_step())));
|
|
} else {
|
|
rotation_angle_label->setText("");
|
|
rotation_angle->setText(QString(""));
|
|
rotation_angle->setToolTip("");
|
|
}
|
|
|
|
exposure_time->setText(QString("<b>%1</b> s").arg(FormatTime(exp.GetImageTime())));
|
|
exposure_time->setToolTip(QString("Count time: <b>%1</b> s<br/>").arg(FormatTime(exp.GetImageCountTime())));
|
|
|
|
text = QString("<b>%1</b>").arg(image->ImageData().spots.size());
|
|
spots->setText(text);
|
|
if (image->ImageData().spot_count.has_value())
|
|
spots->setToolTip(QString("Unfiltered (total): <b>%1</b> <br/>Low resolution: <b>%2</b><br/>Ice ring: <b>%3</b><br/>Indexed <b>%4</b><br/>")
|
|
.arg(image->ImageData().spot_count.value())
|
|
.arg(image->ImageData().spot_count_low_res.value_or(0))
|
|
.arg(image->ImageData().spot_count_ice_rings.value_or(0))
|
|
.arg(image->ImageData().spot_count_indexed.value_or(0)));
|
|
|
|
text = QString("<b>%1</b> - <b>%2</b>")
|
|
.arg(image->ValidPixels().begin()->first)
|
|
.arg(image->ValidPixels().rbegin()->first);
|
|
valid_values->setText(text);
|
|
valid_values->setToolTip(QString("Error pixels: <b>%1</b><br/>Saturated pixels: <b>%2</b>")
|
|
.arg(image->ErrorPixels().size()).arg(image->SaturatedPixels().size()));
|
|
|
|
if (!image->Dataset().bkg_estimate.empty()) {
|
|
text = QString("<b>%1</b>").arg(image->ImageData().bkg_estimate.value_or(0));
|
|
bkg_estimate->setText(text);
|
|
} else {
|
|
bkg_estimate->setText("N/A");
|
|
}
|
|
|
|
auto pr = image->ImageData().profile_radius;
|
|
if (pr && std::isfinite(pr.value())) {
|
|
text = QString("<b>%1</b> Å<sup>-1</sup>").arg(QString::number(pr.value(), 'f', 6));
|
|
profile_radius->setText(text);
|
|
} else {
|
|
profile_radius->setText("N/A");
|
|
}
|
|
|
|
auto b = image->ImageData().b_factor;
|
|
if (b && std::isfinite(b.value())) {
|
|
text = QString("<b>%1</b> Å<sup>2</sup>").arg(QString::number(b.value(), 'f', 2));
|
|
b_factor->setText(text);
|
|
} else {
|
|
b_factor->setText("N/A");
|
|
}
|
|
|
|
auto res = image->ImageData().resolution_estimate;
|
|
if (res && std::isfinite(res.value())) {
|
|
text = QString("<b>%1</b> Å").arg(QString::number(res.value(), 'f', 2));
|
|
res_estimate->setText(text);
|
|
} else {
|
|
res_estimate->setText("N/A");
|
|
}
|
|
|
|
float total_pixels = image->Dataset().experiment.GetPixelsNum();
|
|
if (total_pixels == 0)
|
|
total_pixels = 1;
|
|
|
|
auto mask_stats = image->Dataset().pixel_mask.GetStatistics();
|
|
text = QString("<b>%1</b>").arg(mask_stats.total_masked);
|
|
masked_pixels->setText(text);
|
|
masked_pixels->setToolTip(
|
|
QString(
|
|
"Error pixels: <b>%1</b> (%2 %)<br/> Noisy pixels: <b>%3</b> (%4 %) <br/> User mask: <b>%5</b> (%6 %)<br/>"
|
|
"Chip gaps: <b>%7</b> (%8 %)<br/>"
|
|
"Non active area (module gap and fill): <b>%9</b> (%10 %)")
|
|
.arg(mask_stats.error_pixel).arg(QString::number(mask_stats.error_pixel / total_pixels * 100.0, 'f', 1))
|
|
.arg(mask_stats.noisy_pixel).arg(QString::number(mask_stats.noisy_pixel / total_pixels * 100.0, 'f', 1))
|
|
.arg(mask_stats.user_mask).arg(QString::number(mask_stats.user_mask / total_pixels * 100.0, 'f', 1))
|
|
.arg(mask_stats.chip_gap_pixel).arg(QString::number(mask_stats.chip_gap_pixel / total_pixels * 100.0, 'f', 1))
|
|
.arg(mask_stats.module_gap_pixel).arg(QString::number(mask_stats.module_gap_pixel / total_pixels * 100.0, 'f', 1))
|
|
);
|
|
|
|
if (!image->Dataset().indexing_result.empty()) {
|
|
QString tooltip;
|
|
|
|
auto latt = image->ImageData().indexing_lattice;
|
|
if (latt) {
|
|
text = QString("<span style=\"color: purple;\">%1</span>")
|
|
.arg(mkUnitCell(latt->GetUnitCell()));
|
|
|
|
auto vec0 = latt->Vec0();
|
|
auto vec1 = latt->Vec1();
|
|
auto vec2 = latt->Vec2();
|
|
|
|
tooltip = QString("<b>Lattice vectors (Å):</b><br/>"
|
|
"a = (%1, %2, %3)<br/>"
|
|
"b = (%4, %5, %6)<br/>"
|
|
"c = (%7, %8, %9)")
|
|
.arg(vec0.x, 7, 'f', 1)
|
|
.arg(vec0.y, 7, 'f', 1)
|
|
.arg(vec0.z, 7, 'f', 1)
|
|
.arg(vec1.x, 7, 'f', 1)
|
|
.arg(vec1.y, 7, 'f', 1)
|
|
.arg(vec1.z, 7, 'f', 1)
|
|
.arg(vec2.x, 7, 'f', 1)
|
|
.arg(vec2.y, 7, 'f', 1)
|
|
.arg(vec2.z, 7, 'f', 1);
|
|
|
|
if (image->ImageData().lattice_type) {
|
|
tooltip += QString("<br/><br/>");
|
|
tooltip += mkSymmetry(*image->ImageData().lattice_type);
|
|
} else if (image->Dataset().experiment.GetSpaceGroupNumber()) {
|
|
tooltip += QString("<br/><br/>Space group (user-provided): <b>%1</b>")
|
|
.arg(QString::fromStdString(image->Dataset().experiment.GetSpaceGroupName()));
|
|
}
|
|
} else
|
|
text = QString("<span style=\"color: red;\"><b>No lattice</b></span>");
|
|
|
|
indexed->setToolTip(tooltip);
|
|
indexed->setText(text);
|
|
} else {
|
|
indexed->setText("N/A");
|
|
}
|
|
}
|