viewer: no uncaught exceptions from slots (grid-scan "Image out of bounds" crash)

An exception escaping a Qt slot terminates the viewer. At the beamline it died with
"Input parameter invalid (Image out of bounds)": the grid-scan map in the dataset-info
panel asked GridScanSettings for the cell of an image outside the grid. Image and dataset
can disagree - live, the broker's image and dataset are fetched apart and can belong to
different collections, and the worker re-emits its current image against a newer dataset.
The map now marks no cell for such an image.

Opening a file with an image number it does not have (a D-Bus caller keeping its
position from the previous dataset) showed no image and kept the previous file's image,
which was then re-emitted against the new dataset. The file now opens at image 0 and the
previous image is dropped.

The rest of the viewer was audited for the same failure and guarded:
- worker slots that were not covered: dataset update, azimuthal/Bragg settings, analysis
  (Analyze, re-analysis toggle, spot-finding settings), beam-centre fit (incl. the setters
  rejecting a diverged fit), user mask update/save/upload, ROI definition, run removal;
  azimuthal settings with more bins than the mapping supports are refused up front, so they
  cannot be kept and fail every later file open;
- settings dock: azimuthal Q range, Bragg radii and experiment fields are built on a copy
  and an invalid combination keeps the previous settings; R1 and cell-angle field ranges
  now match the setters;
- azimuthal-image double-click on a bin with no pixel, azimuthal ROI handle dragged onto
  the beam centre (infinite d), ROI editor given inf/nan, non-numeric
  JUNGFRAUJOCH_HTTP_PORT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-10-31 08:23:55 +01:00
1 parent 67e9c84bd1
commit 18e50c590b
7 files changed
+200 -102

No files matched your search

+94 -36
View File
@@ -352,8 +352,17 @@ void JFJochImageReadingWorker::LoadFile_i(const QString &filename, qint64 image_
}
current_image.reset();
current_image_ptr.reset(); // the previous file's image must not be re-emitted against this dataset
current_summation = 1;
current_file = filename;
// An image number carried over from the previous dataset (e.g. a D-Bus caller keeping its
// position) may not exist in this one: open the file at its first image instead of showing none.
if (!http_mode && image_number >= 0 && image_number + summation > total_images) {
logger.Warning("Image {} out of range for {} ({} images), showing image 0",
image_number, filename.toStdString(), total_images);
image_number = 0;
summation = 1;
}
if (dataset) {
curr_experiment = dataset->experiment;
roi_override_.reset(); // new file: use its ROIs, forget any earlier edits
@@ -581,7 +590,11 @@ void JFJochImageReadingWorker::UpdateDataset_i(const std::optional<DiffractionEx
void JFJochImageReadingWorker::UpdateDataset(const DiffractionExperiment &experiment) {
QMutexLocker ul(&m);
UpdateDataset_i(experiment);
try {
UpdateDataset_i(experiment);
} catch (const std::exception &e) {
logger.Error("Dataset update failed: {}", e.what());
}
}
void JFJochImageReadingWorker::ReanalyzeImage_i() {
@@ -602,7 +615,12 @@ void JFJochImageReadingWorker::ReanalyzeImage_i() {
// Azimuthal profile for the analysis/display pipeline. AzimuthalIntegrationProfile
// holds a mutex (non-copyable), so keep it via unique_ptr re-created each analysis.
last_profile_ = std::make_unique<AzimuthalIntegrationProfile>(*azint_mapping);
image_analysis->Analyze(new_image->ImageData(), *last_profile_, spot_finding_settings);
try {
image_analysis->Analyze(new_image->ImageData(), *last_profile_, spot_finding_settings);
} catch (const std::exception &e) {
logger.Error("Analysis of image failed: {}", e.what());
return;
}
current_image_ptr = new_image;
auto end_time = std::chrono::high_resolution_clock::now();
@@ -624,27 +642,28 @@ void JFJochImageReadingWorker::FindCenter(QVector<float> ring_q, bool guess, boo
DiffractionGeometry geom = current_image_ptr->Dataset().experiment.GetDiffractionGeometry();
const std::vector<float> ring_Q(ring_q.begin(), ring_q.end());
// A diverged fit is rejected by the setters (NaN beam, sub-mm distance), so they sit in the try too.
try {
if (guess)
GuessGeometry(geom, current_image_ptr->ImageData().spots, ring_Q, refine_tilt);
else
OptimizeGeometry(geom, current_image_ptr->ImageData().spots, ring_Q, refine_tilt);
} catch (const JFJochException &e) {
logger.ErrorException(e);
logger.Info("Geometry found X: {} pxl Y: {} pxl Dist: {} mm", geom.GetBeamX_pxl(), geom.GetBeamY_pxl(),
geom.GetDetectorDistance_mm());
DiffractionExperiment new_experiment = current_image_ptr->Dataset().experiment;
new_experiment.BeamX_pxl(geom.GetBeamX_pxl()).BeamY_pxl(geom.GetBeamY_pxl())
.DetectorDistance_mm(geom.GetDetectorDistance_mm())
.PoniRot1_rad(geom.GetPoniRot1_rad())
.PoniRot2_rad(geom.GetPoniRot2_rad())
.PoniRot3_rad(geom.GetPoniRot3_rad());
UpdateDataset_i(new_experiment);
} catch (const std::exception &e) {
logger.Error("Finding beam center failed: {}", e.what());
return;
}
logger.Info("Geometry found X: {} pxl Y: {} pxl Dist: {} mm", geom.GetBeamX_pxl(), geom.GetBeamY_pxl(),
geom.GetDetectorDistance_mm());
DiffractionExperiment new_experiment = current_image_ptr->Dataset().experiment;
new_experiment.BeamX_pxl(geom.GetBeamX_pxl()).BeamY_pxl(geom.GetBeamY_pxl())
.DetectorDistance_mm(geom.GetDetectorDistance_mm())
.PoniRot1_rad(geom.GetPoniRot1_rad())
.PoniRot2_rad(geom.GetPoniRot2_rad())
.PoniRot3_rad(geom.GetPoniRot3_rad());
UpdateDataset_i(new_experiment);
QVector<float> rings;
for (int i = 0; i < 15 && i < ring_Q.size(); i++) {
rings.push_back(2 * PI / ring_Q[i]);
@@ -695,14 +714,29 @@ void JFJochImageReadingWorker::ReanalyzeImages(bool input) {
void JFJochImageReadingWorker::UpdateAzintSettings(const AzimuthalIntegrationSettings &settings) {
QMutexLocker locker(&m);
// Checked here, not only where the mapping is built: settings that cannot be mapped would
// otherwise be kept and fail every later file open and dataset update.
if (settings.GetBinCount() >= UINT16_MAX) {
logger.Error("Azimuthal integration settings ignored: {} bins, at most 65534 supported",
settings.GetBinCount());
return;
}
azint_settings = settings;
UpdateDataset_i(std::nullopt);
try {
UpdateDataset_i(std::nullopt);
} catch (const std::exception &e) {
logger.Error("Azimuthal integration update failed: {}", e.what());
}
}
void JFJochImageReadingWorker::UpdateBraggIntegrationSettings(BraggIntegrationSettings settings) {
QMutexLocker locker(&m);
bragg_settings = settings;
UpdateDataset_i(std::nullopt);
try {
UpdateDataset_i(std::nullopt);
} catch (const std::exception &e) {
logger.Error("Bragg integration update failed: {}", e.what());
}
}
void JFJochImageReadingWorker::UpdateScalingSettings(ScalingSettings settings) {
@@ -778,7 +812,11 @@ void JFJochImageReadingWorker::EmitReferenceInfo_i() {
void JFJochImageReadingWorker::SetROIDefinition(const ROIDefinition &rois) {
QMutexLocker locker(&m);
SetROIDefinition_i(rois);
try {
SetROIDefinition_i(rois);
} catch (const std::exception &e) {
logger.Error("ROI update failed: {}", e.what());
}
}
void JFJochImageReadingWorker::MaskFromSelectedROI(QString name, bool add) {
@@ -879,25 +917,30 @@ void JFJochImageReadingWorker::ApplyROIOverrideToImage_i() {
void JFJochImageReadingWorker::UpdateUserMask_i(const std::vector<uint32_t> &mask) {
std::shared_ptr<const JFJochReaderDataset> dataset;
if (http_mode) {
http_reader.UpdateUserMask(mask);
dataset = http_reader.GetDataset();
} else {
file_reader->UpdateUserMask(mask);
dataset = file_reader->GetDataset();
}
try {
if (http_mode) {
http_reader.UpdateUserMask(mask);
dataset = http_reader.GetDataset();
} else {
file_reader->UpdateUserMask(mask);
dataset = file_reader->GetDataset();
}
if (!dataset) {
logger.Error("UpdateUserMask_i: dataset is null (http_mode={}) - skipping update to avoid crash", http_mode);
if (!dataset) {
logger.Error("UpdateUserMask_i: dataset is null (http_mode={}) - skipping update to avoid crash", http_mode);
return;
}
UpdateAzint_i(dataset.get());
EmitDatasetLoaded_i(dataset);
current_image_ptr = std::make_shared<JFJochReaderImage>(current_image_ptr->ImageData(), dataset);
} catch (const std::exception &e) {
logger.Error("User mask update failed: {}", e.what());
return;
}
UpdateAzint_i(dataset.get());
EmitDatasetLoaded_i(dataset);
current_image_ptr = std::make_shared<JFJochReaderImage>(current_image_ptr->ImageData(), dataset);
if (current_image.has_value())
LoadImage_i(current_image.value(), current_summation);
}
@@ -918,7 +961,11 @@ void JFJochImageReadingWorker::SaveUserMaskTIFF(QString filename) {
return;
auto user_mask = current_image_ptr->Dataset().pixel_mask->GetUserMask();
CompressedImage mask_image(user_mask, current_image_ptr->Dataset().experiment.GetXPixelsNum(), current_image_ptr->Dataset().experiment.GetYPixelsNum());
WriteTIFFToFile(filename.toStdString(), mask_image);
try {
WriteTIFFToFile(filename.toStdString(), mask_image);
} catch (const std::exception &e) {
logger.Error("Could not write mask TIFF {}: {}", filename.toStdString(), e.what());
}
}
void JFJochImageReadingWorker::LoadUserMaskTIFF(QString filename, bool replace) {
@@ -961,8 +1008,15 @@ void JFJochImageReadingWorker::UploadUserMask() {
return;
auto user_mask = current_image_ptr->Dataset().pixel_mask->GetUserMask();
if (http_mode)
if (!http_mode)
return;
try {
http_reader.UploadUserMask(user_mask);
} catch (const HttpUnauthorized &) {
HandleUnauthorized_i(current_file);
} catch (const std::exception &e) {
logger.Error("Upload of user mask failed: {}", e.what());
}
}
void JFJochImageReadingWorker::LoadCalibration(QString dataset) {
@@ -1265,7 +1319,11 @@ void JFJochImageReadingWorker::RemoveRun(QString id) {
}
run_labels_.erase(id.toStdString());
// Refresh the (possibly now-Original) active view; ActivateSnapshot_i also re-emits runsChanged.
ActivateSnapshot_i(QString::fromStdString(ActiveRun_i()));
try {
ActivateSnapshot_i(QString::fromStdString(ActiveRun_i()));
} catch (const std::exception &e) {
logger.Error("Could not activate run after removal: {}", e.what());
}
}
void JFJochImageReadingWorker::RegisterProcessingSnapshot(QString id, QString label, QString master_path) {
+1 -1
View File
@@ -327,7 +327,7 @@ void JFJochViewerMenu::openHttpSelected() {
char *port_std = std::getenv("JUNGFRAUJOCH_HTTP_PORT");
if (port_std != nullptr) {
const int tmp = std::stoi(std::string(port_std));
const int tmp = QString(port_std).toInt(); // 0 for a non-number, unlike std::stoi which throws
if (tmp < 65536 && tmp > 0)
portSpinBox->setValue(tmp);
} else
+6 -2
View File
@@ -107,7 +107,11 @@ void JFJochAzIntImage::mouseDoubleClickEvent(QMouseEvent *event) {
float phi = image->Dataset().az_int_bin_to_phi[idx];
auto geom = image->Dataset().experiment.GetDiffractionGeometry();
auto coord = geom.ResPhiToPxl(2 * PI / q, phi / 180.0 * PI);
emit zoomOnBin(QPointF(coord.first, coord.second));
try {
auto coord = geom.ResPhiToPxl(2 * PI / q, phi / 180.0 * PI);
emit zoomOnBin(QPointF(coord.first, coord.second));
} catch (const std::exception &) {
// a bin beyond 2 theta = 180 deg (manual High Q above 4 pi / lambda) is on no pixel
}
}
}
@@ -834,11 +834,14 @@ void JFJochDiffractionImage::roiEditMove(const QPointF &scenePos) {
case RoiEdit::ResizeCircle:
edit_radius_ = std::max(1.0, QLineF(edit_center_, scenePos).length());
break;
// PxlToRes is infinite on the beam centre, which ROIAzimuthal rejects: keep the last finite value
case RoiEdit::AzimInner:
edit_d_max_ = geom.PxlToRes(cursor_r);
if (const float d = geom.PxlToRes(cursor_r); std::isfinite(d))
edit_d_max_ = d;
break;
case RoiEdit::AzimOuter:
edit_d_min_ = geom.PxlToRes(cursor_r);
if (const float d = geom.PxlToRes(cursor_r); std::isfinite(d))
edit_d_min_ = d;
break;
case RoiEdit::RotatePhiMin:
edit_phi_min_ = geom.Phi_rad(scenePos.x(), scenePos.y()) * 180.0f / static_cast<float>(PI);
+4 -1
View File
@@ -155,7 +155,10 @@ void JFJochGridScanImage::mouseDoubleClickEvent(QMouseEvent *event) {
}
void JFJochGridScanImage::setImage(int64_t val) {
if (settings) {
// The image can lie outside the grid: live, the broker's image and dataset are fetched apart and
// can belong to different collections, and a re-emitted image can meet a newer dataset's grid.
// Such an image has no cell to mark (GetElementPos*_step would throw on the GUI thread).
if (settings && val >= 0 && val < settings->GetNElem()) {
current_image_W = settings->GetElementPosX_step(val);
current_image_H = settings->GetElementPosY_step(val);
} else {
+29 -23
View File
@@ -114,29 +114,35 @@ void JFJochViewerROIList::ApplyEditor() {
const std::string sel = SelectedName().toStdString();
bool changed = false;
for (auto &b : rois_.boxes)
if (b.GetName() == sel) {
b = ROIBox(sel, fedit_[0]->text().toLongLong(), fedit_[1]->text().toLongLong(),
fedit_[2]->text().toLongLong(), fedit_[3]->text().toLongLong());
changed = true; break;
}
for (auto &c : rois_.circles)
if (c.GetName() == sel) {
c = ROICircle(sel, fedit_[0]->text().toDouble(), fedit_[1]->text().toDouble(),
std::max(0.1, fedit_[2]->text().toDouble()));
changed = true; break;
}
for (auto &a : rois_.azimuthal)
if (a.GetName() == sel) {
const float qmin = fedit_[0]->text().toFloat();
const float qmax = fedit_[1]->text().toFloat();
const float phimin = fedit_[2]->text().toFloat();
const float phimax = fedit_[3]->text().toFloat();
const float d_min = (qmax > 0) ? 2.0f * static_cast<float>(PI) / qmax : a.GetDMin_A();
const float d_max = (qmin > 0) ? 2.0f * static_cast<float>(PI) / qmin : a.GetDMax_A();
a = ROIAzimuthal(sel, d_min, d_max, phimin, phimax);
changed = true; break;
}
// The ROI constructors reject what QString::toDouble lets through ("inf", "nan"); an exception
// escaping a slot terminates the viewer, so such an entry is just not applied.
try {
for (auto &b : rois_.boxes)
if (b.GetName() == sel) {
b = ROIBox(sel, fedit_[0]->text().toLongLong(), fedit_[1]->text().toLongLong(),
fedit_[2]->text().toLongLong(), fedit_[3]->text().toLongLong());
changed = true; break;
}
for (auto &c : rois_.circles)
if (c.GetName() == sel) {
c = ROICircle(sel, fedit_[0]->text().toDouble(), fedit_[1]->text().toDouble(),
std::max(0.1, fedit_[2]->text().toDouble()));
changed = true; break;
}
for (auto &a : rois_.azimuthal)
if (a.GetName() == sel) {
const float qmin = fedit_[0]->text().toFloat();
const float qmax = fedit_[1]->text().toFloat();
const float phimin = fedit_[2]->text().toFloat();
const float phimax = fedit_[3]->text().toFloat();
const float d_min = (qmax > 0) ? 2.0f * static_cast<float>(PI) / qmax : a.GetDMin_A();
const float d_max = (qmin > 0) ? 2.0f * static_cast<float>(PI) / qmin : a.GetDMax_A();
a = ROIAzimuthal(sel, d_min, d_max, phimin, phimax);
changed = true; break;
}
} catch (const std::exception &) {
return;
}
if (changed)
emit roisChanged(rois_);
+61 -37
View File
@@ -230,9 +230,9 @@ QWidget *JFJochViewerSettingsDock::BuildMXPage() {
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);
cellAlpha_ = new NumberLineEdit(5.0, 179.0, 90.0, 2, "", page);
cellBeta_ = new NumberLineEdit(5.0, 179.0, 90.0, 2, "", page);
cellGamma_ = new NumberLineEdit(5.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);
@@ -682,12 +682,20 @@ QWidget *JFJochViewerSettingsDock::BuildAzIntSection() {
az->addRow("Azimuthal bins", azimBins);
azSection->setContentLayout(az);
// A combination can still be invalid (Low Q above High Q): the setters throw, and an exception
// escaping a slot terminates the viewer. Build on a copy and keep the previous settings instead.
auto emitAz = [=, this] {
azint_.QRange_recipA(static_cast<float>(lowQ->value()),
autoHighQ->isChecked() ? std::nullopt
: std::optional<float>(highQ->value()));
azint_.QSpacing_recipA(static_cast<float>(spacing->value()));
azint_.AzimuthalBinCount(azimBins->currentData().toInt());
AzimuthalIntegrationSettings s = azint_;
try {
s.QRange_recipA(static_cast<float>(lowQ->value()),
autoHighQ->isChecked() ? std::nullopt
: std::optional<float>(highQ->value()));
s.QSpacing_recipA(static_cast<float>(spacing->value()));
s.AzimuthalBinCount(azimBins->currentData().toInt());
} catch (const std::exception &) {
return;
}
azint_ = s;
emit azintChanged(azint_);
};
connect(lowQ, &SliderPlusBox::valueChanged, this, [emitAz] { emitAz(); });
@@ -792,7 +800,7 @@ QWidget *JFJochViewerSettingsDock::BuildBraggSection() {
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, "", this);
auto *r1 = new NumberLineEdit(1.0f, 20.0f, bragg_.GetR1(), 1, "", this);
auto *r2 = new NumberLineEdit(1.0f, 30.0f, bragg_.GetR2(), 1, "", this);
auto *r3 = new NumberLineEdit(1.0f, 30.0f, bragg_.GetR3(), 1, "", this);
auto *radii = new QHBoxLayout();
@@ -817,15 +825,23 @@ QWidget *JFJochViewerSettingsDock::BuildBraggSection() {
form->addRow(trimRow);
section->setContentLayout(form);
// The radii must grow r1 < r2 < r3, which a single typed value can break: the setters throw, and an
// exception escaping a slot terminates the viewer. Build on a copy and keep the previous settings.
auto emitBragg = [=, this] {
bragg_.Integrator(gaussian->isChecked() ? IntegratorMode::ProfileGaussian : IntegratorMode::BoxSum);
bragg_.R1(static_cast<float>(r1->value())).R2(static_cast<float>(r2->value()))
.R3(static_cast<float>(r3->value()));
// Either estimator clears the other, so unchecking has to put the default clip back explicitly.
if (bkgTrim->isChecked())
bragg_.BackgroundTrimFraction(static_cast<float>(bkgTrimFrac->value()));
else
bragg_.BackgroundClipNSigma(BraggIntegrationSettings().GetBackgroundClipNSigma());
BraggIntegrationSettings s = bragg_;
try {
s.Integrator(gaussian->isChecked() ? IntegratorMode::ProfileGaussian : IntegratorMode::BoxSum);
s.R1(static_cast<float>(r1->value())).R2(static_cast<float>(r2->value()))
.R3(static_cast<float>(r3->value()));
// Either estimator clears the other, so unchecking has to put the default clip back explicitly.
if (bkgTrim->isChecked())
s.BackgroundTrimFraction(static_cast<float>(bkgTrimFrac->value()));
else
s.BackgroundClipNSigma(BraggIntegrationSettings().GetBackgroundClipNSigma());
} catch (const std::exception &) {
return;
}
bragg_ = s;
emit braggChanged(bragg_);
};
connect(gaussian, &QCheckBox::toggled, this, [emitBragg] { emitBragg(); });
@@ -909,27 +925,35 @@ QWidget *JFJochViewerSettingsDock::BuildScalingSection() {
void JFJochViewerSettingsDock::EmitExperiment() {
if (!have_experiment_)
return;
experiment_.IncidentEnergy_keV(static_cast<float>(energy_->value()));
experiment_.DetectorDistance_mm(static_cast<float>(distance_->value()));
experiment_.BeamX_pxl(static_cast<float>(beamX_->value()));
experiment_.BeamY_pxl(static_cast<float>(beamY_->value()));
experiment_.PoniRot1_rad(static_cast<float>(rot1_->value() * PI / 180.0));
experiment_.PoniRot2_rad(static_cast<float>(rot2_->value() * PI / 180.0));
experiment_.PolarizationFactor(polarizationOn_->isChecked()
? std::optional<float>(static_cast<float>(polarization_->value())) : std::nullopt);
experiment_.Goniometer(GoniometerFromFields());
experiment_.GridScan(GridScanFromFields());
if (cellKnown_->isChecked()) {
experiment_.SetUnitCell(UnitCell{
static_cast<float>(cellA_->value()), static_cast<float>(cellB_->value()),
static_cast<float>(cellC_->value()), static_cast<float>(cellAlpha_->value()),
static_cast<float>(cellBeta_->value()), static_cast<float>(cellGamma_->value())});
const int sg = static_cast<int>(std::lround(spaceGroup_->value()));
experiment_.SpaceGroupNumber(sg > 0 ? std::optional<int64_t>(sg) : std::nullopt);
} else {
experiment_.SetUnitCell(std::nullopt);
experiment_.SpaceGroupNumber(std::nullopt);
// The setters validate and throw; an exception escaping a slot terminates the viewer, so
// an invalid entry keeps the previous experiment instead.
DiffractionExperiment e = experiment_;
try {
e.IncidentEnergy_keV(static_cast<float>(energy_->value()));
e.DetectorDistance_mm(static_cast<float>(distance_->value()));
e.BeamX_pxl(static_cast<float>(beamX_->value()));
e.BeamY_pxl(static_cast<float>(beamY_->value()));
e.PoniRot1_rad(static_cast<float>(rot1_->value() * PI / 180.0));
e.PoniRot2_rad(static_cast<float>(rot2_->value() * PI / 180.0));
e.PolarizationFactor(polarizationOn_->isChecked()
? std::optional<float>(static_cast<float>(polarization_->value())) : std::nullopt);
e.Goniometer(GoniometerFromFields());
e.GridScan(GridScanFromFields());
if (cellKnown_->isChecked()) {
e.SetUnitCell(UnitCell{
static_cast<float>(cellA_->value()), static_cast<float>(cellB_->value()),
static_cast<float>(cellC_->value()), static_cast<float>(cellAlpha_->value()),
static_cast<float>(cellBeta_->value()), static_cast<float>(cellGamma_->value())});
const int sg = static_cast<int>(std::lround(spaceGroup_->value()));
e.SpaceGroupNumber(sg > 0 ? std::optional<int64_t>(sg) : std::nullopt);
} else {
e.SetUnitCell(std::nullopt);
e.SpaceGroupNumber(std::nullopt);
}
} catch (const std::exception &) {
return;
}
experiment_ = e;
emit experimentChanged(experiment_);
}