diff --git a/reader/JFJochReader.cpp b/reader/JFJochReader.cpp index 0ab19292..fb49e358 100644 --- a/reader/JFJochReader.cpp +++ b/reader/JFJochReader.cpp @@ -84,6 +84,9 @@ void JFJochReader::UpdateGeomMetadata(const DiffractionExperiment &experiment) { new_dataset->experiment.PoniRot3_rad(experiment.GetDatasetSettings().GetPoniRot3_rad()); new_dataset->experiment.SetUnitCell(experiment.GetUnitCell()); new_dataset->experiment.SpaceGroupNumber(experiment.GetSpaceGroupNumber()); + new_dataset->experiment.PolarizationFactor(experiment.GetPolarizationFactor()); + new_dataset->experiment.Goniometer(experiment.GetGoniometer()); + new_dataset->experiment.GridScan(experiment.GetGridScan()); new_dataset->experiment.ImportIndexingSettings(experiment.GetIndexingSettings()); new_dataset->experiment.ImportBraggIntegrationSettings(experiment.GetBraggIntegrationSettings()); new_dataset->experiment.DetectIceRings(experiment.IsDetectIceRings()); diff --git a/viewer/JFJochImageReadingWorker.cpp b/viewer/JFJochImageReadingWorker.cpp index e132d3e5..11282d2a 100644 --- a/viewer/JFJochImageReadingWorker.cpp +++ b/viewer/JFJochImageReadingWorker.cpp @@ -286,6 +286,7 @@ void JFJochImageReadingWorker::LoadFile_i(const QString &filename, qint64 image_ if (dataset) { curr_experiment = dataset->experiment; roi_override_.reset(); // new file: use its ROIs, forget any earlier edits + file_goniometer_ = dataset->experiment.GetGoniometer(); // before the panel edits it curr_experiment.ImportIndexingSettings(indexing_settings); curr_experiment.ImportAzimuthalIntegrationSettings(azint_settings); curr_experiment.ImportBraggIntegrationSettings(bragg_settings); @@ -576,7 +577,7 @@ void JFJochImageReadingWorker::UpdateSpotFindingSettings(const SpotFindingSettin indexing_settings.Algorithm(indexing.GetAlgorithm()); indexing_settings.GeomRefinementAlgorithm(indexing.GetGeomRefinementAlgorithm()); - indexing_settings.RotationIndexing(indexing.GetRotationIndexing()); // "Process as stills" drives this + indexing_settings.RotationIndexing(indexing.GetRotationIndexing()); // the rotation axis drives this indexing_settings.Tolerance(indexing.GetTolerance()); indexing_settings.ViableCellMinSpots(indexing.GetViableCellMinSpots()); indexing_settings.IndexIceRings(indexing.GetIndexIceRings()); @@ -1055,6 +1056,7 @@ ReprocessingInputs JFJochImageReadingWorker::GetReprocessingInputs() const { in.experiment = curr_experiment; in.pixel_mask = *current_image_ptr->Dataset().pixel_mask; in.spot_finding = spot_finding_settings; + in.file_goniometer = file_goniometer_; if (reference_) in.reference_data = reference_->reflections; in.valid = true; diff --git a/viewer/JFJochImageReadingWorker.h b/viewer/JFJochImageReadingWorker.h index 28c7180c..8ca1795b 100644 --- a/viewer/JFJochImageReadingWorker.h +++ b/viewer/JFJochImageReadingWorker.h @@ -51,6 +51,9 @@ struct ReprocessingInputs { PixelMask pixel_mask; SpotFindingSettings spot_finding; std::vector reference_data; // empty unless a reference MTZ is loaded + // The rotation axis the file was opened with, before the settings panel had a say. `experiment` + // carries the panel's, which may have cleared it; this is what a fresh `rugnux` run would see. + std::optional file_goniometer; }; #include "RunData.h" @@ -78,6 +81,9 @@ private: // Once the user edits ROIs they override whatever the file carried, for this and // every subsequently loaded image, until a new file is opened. std::optional roi_override_; + // The rotation axis of the open file, captured before the settings panel can change it — see + // ReprocessingInputs::file_goniometer. + std::optional file_goniometer_; std::map run_labels_; // snapshot id -> editable display label IndexingSettings indexing_settings; AzimuthalIntegrationSettings azint_settings; diff --git a/viewer/JFJochViewerWindow.cpp b/viewer/JFJochViewerWindow.cpp index 939ffafa..3c8cfabb 100644 --- a/viewer/JFJochViewerWindow.cpp +++ b/viewer/JFJochViewerWindow.cpp @@ -23,6 +23,7 @@ #include "widgets/JFJochViewerImageStrip.h" #include "JFJochViewerStatusBar.h" #include "../common/CUDAWrapper.h" +#include "../rugnux/RugnuxDefaults.h" #include "windows/JFJochViewerImageListWindow.h" #include "windows/JFJochViewerMetadataWindow.h" #ifdef JFJOCH_VIEWER_DBUS @@ -105,6 +106,10 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString DiffractionExperiment experiment; experiment.ImportIndexingSettings(indexing_settings); experiment.DetectIceRings(true); + // Files carry no polarization factor, so without this the interactive analysis would integrate + // without the Lp correction that a processing run applies. Start from the same policy `rugnux` + // does; the settings panel shows it and can change it per dataset. + ApplyRugnuxExperimentDefaults(experiment); // Central area: the diffraction image. Everything else (inspector, plots, processing) is a // dock, so the layout can be rearranged, saved, and switched between perspectives. diff --git a/viewer/widgets/JFJochViewerSettingsDock.cpp b/viewer/widgets/JFJochViewerSettingsDock.cpp index 41c16b31..6bd9ef5c 100644 --- a/viewer/widgets/JFJochViewerSettingsDock.cpp +++ b/viewer/widgets/JFJochViewerSettingsDock.cpp @@ -11,7 +11,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -30,6 +32,17 @@ #include "../../gemmi_gph/gemmi/symmetry.hpp" namespace { + // What the goniometer fields offer when the file describes no sample motion, so picking a mode + // on a still file starts from something sensible: the usual single rotation axis, and a raster + // of the size beamlines commonly use. + const char *DEFAULT_AXIS_NAME = "omega"; + constexpr float DEFAULT_AXIS[3] = {-1.0f, 0.0f, 0.0f}; + constexpr float DEFAULT_INCREMENT_DEG = 0.1f; + constexpr float DEFAULT_GRID_N_FAST = 10.0f; + constexpr float DEFAULT_GRID_STEP_UM = 20.0f; + // Polarized fraction of a synchrotron beam - the same default a bare `rugnux` run applies. + constexpr float DEFAULT_POLARIZATION = 0.99f; + // 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) { @@ -143,6 +156,17 @@ QWidget *JFJochViewerSettingsDock::BuildGeometrySection() { 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); + // Both are overwritten from the experiment as soon as a dataset is open; this is what they show + // until then. + polarizationOn_ = new QCheckBox("Polarization correction", this); + polarizationOn_->setChecked(true); + polarization_ = new NumberLineEdit(-1.0, 1.0, DEFAULT_POLARIZATION, 3, "", this); + const QString polarizationTip = + "Polarized fraction of the beam in the horizontal plane (XDS FRACTION_OF_POLARIZATION): 0.99 " + "for a synchrotron, 0 for an unpolarized source. Divides out the Lp modulation in both the " + "azimuthal profile and the integrated Bragg intensities; unchecked = no correction at all."; + polarizationOn_->setToolTip(polarizationTip); + polarization_->setToolTip(polarizationTip); // 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. @@ -161,14 +185,22 @@ QWidget *JFJochViewerSettingsDock::BuildGeometrySection() { auto *tilt = new QHBoxLayout(); tilt->addWidget(rot1_); tilt->addWidget(rot2_); + auto *polarization = new QHBoxLayout(); + polarization->addWidget(polarizationOn_); + polarization->addWidget(polarization_, 1); geom->addRow("Photon energy", energy_); geom->addRow("Detector distance", distance_); geom->addRow("Beam origin", beam); geom->addRow("Detector tilt", tilt); + geom->addRow("", polarization); - for (auto *f : {energy_, distance_, beamX_, beamY_, rot1_, rot2_}) + for (auto *f : {energy_, distance_, beamX_, beamY_, rot1_, rot2_, polarization_}) connect(f, &NumberLineEdit::newValue, this, [this] { EmitExperiment(); }); + connect(polarizationOn_, &QCheckBox::toggled, this, [this](bool on) { + polarization_->setEnabled(on); + EmitExperiment(); + }); section->setContentLayout(geom); return section; @@ -226,6 +258,8 @@ QWidget *JFJochViewerSettingsDock::BuildMXPage() { EmitExperiment(); }); + layout->addWidget(BuildGoniometerSection()); + // --- Spot finding --- auto *spotSection = new CollapsibleSection("Spot finding", page); auto *spot = new QFormLayout(); @@ -334,18 +368,9 @@ QWidget *JFJochViewerSettingsDock::BuildMXPage() { // (multi-lattice integration). refine->addItem("Flex (best per-image refinement)", static_cast(GeomRefinementAlgorithmEnum::Flex)); 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); @@ -358,7 +383,6 @@ QWidget *JFJochViewerSettingsDock::BuildMXPage() { indexing_.GeomRefinementAlgorithm(static_cast(refine->currentData().toInt())); EmitSpotFinding(); }); - connect(stills_, &QCheckBox::toggled, this, [this] { ApplyProcessingMode(); }); UpdateAlgorithmDescription(); layout->addWidget(BuildBraggSection()); @@ -369,6 +393,170 @@ QWidget *JFJochViewerSettingsDock::BuildMXPage() { return page; } +QWidget *JFJochViewerSettingsDock::BuildGoniometerSection() { + // How the sample moved between images, in the same three cases a file stores: a rotation axis + // (/entry/sample/transformations/omega), a grid scan (/entry/sample/grid_scan), or neither. The + // mode IS the rotation/stills switch, so there is no second "process as stills" control, and it + // is not tied to what the file says: a still file can be given an axis or a grid, and a rotation + // file can be processed as stills by choosing Still. + auto *section = new CollapsibleSection("Goniometer", this); + auto *form = new QFormLayout(); + form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + + // Exclusive, so radio buttons rather than checkboxes: a dataset is one of the three, never two. + modeStill_ = new QRadioButton("Still", this); + modeStill_->setToolTip("The sample does not move between images: every image is an independent " + "still (per-frame indexing, fixed partiality)."); + modeRotation_ = new QRadioButton("Rotation", this); + modeRotation_->setToolTip("The sample rotates between images: rotation indexing, Ewald-crossing " + "partiality and 3D rotation scaling."); + modeGrid_ = new QRadioButton("Grid scan", this); + modeGrid_->setToolTip("The sample translates between images on a raster. Processed as stills; the " + "grid is what the dataset-info panel maps the per-image results onto."); + auto *modes = new QButtonGroup(this); + modes->setExclusive(true); + for (auto *b : {modeStill_, modeRotation_, modeGrid_}) + modes->addButton(b); + modeStill_->setChecked(true); + + axisName_ = new QLineEdit(DEFAULT_AXIS_NAME, this); + axisName_->setToolTip("Name of the rotated goniometer axis, as it is written to the output."); + axisX_ = new NumberLineEdit(-1.0, 1.0, DEFAULT_AXIS[0], 3, "", this); + axisY_ = new NumberLineEdit(-1.0, 1.0, DEFAULT_AXIS[1], 3, "", this); + axisZ_ = new NumberLineEdit(-1.0, 1.0, DEFAULT_AXIS[2], 3, "", this); + const QString axisTip = "Rotation axis in the laboratory frame (x, y, z); normalised on use."; + for (auto *f : {axisX_, axisY_, axisZ_}) + f->setToolTip(axisTip); + rotStart_ = new NumberLineEdit(-3600.0, 3600.0, 0.0, 3, "°", this); + rotStart_->setToolTip("Angle of the first image."); + rotIncrement_ = new NumberLineEdit(-10.0, 10.0, DEFAULT_INCREMENT_DEG, 3, "°", this); + rotIncrement_->setToolTip("Angle rotated per image."); + + // Only the fast axis has a point count: the slow one is however many rows the images fill, which + // is also why the file stores n_fast alone. The steps are signed - the sign is the direction the + // scan runs in - so only zero is rejected. + gridNFast_ = new NumberLineEdit(1.0, 100000.0, DEFAULT_GRID_N_FAST, 0, "", this); + gridNFast_->setToolTip("Points along the fast axis. The number of slow rows follows from the " + "number of images."); + gridStepX_ = new NumberLineEdit(-10000.0, 10000.0, DEFAULT_GRID_STEP_UM, 1, "μm", this); + gridStepY_ = new NumberLineEdit(-10000.0, 10000.0, DEFAULT_GRID_STEP_UM, 1, "μm", this); + const QString stepTip = "Distance between neighbouring points, in x and y. Negative = the scan " + "runs towards decreasing x / y."; + gridStepX_->setToolTip(stepTip); + gridStepY_->setToolTip(stepTip); + gridVertical_ = new QCheckBox("Fast axis vertical", this); + gridVertical_->setToolTip("The scan fills a column before moving to the next one (fast axis = y) " + "instead of filling a row (fast axis = x)."); + gridSnake_ = new QCheckBox("Snake scan", this); + gridSnake_->setToolTip("Every second row/column is collected in the reverse direction (boustrophedon) " + "instead of returning to the start of the next one."); + gridSummary_ = new QLabel(this); + gridSummary_->setStyleSheet("color: gray;"); + + auto *axis = new QHBoxLayout(); + axis->addWidget(axisX_); axis->addWidget(axisY_); axis->addWidget(axisZ_); + auto *step = new QHBoxLayout(); + step->addWidget(gridStepX_); step->addWidget(gridStepY_); + auto *gridFlags = new QHBoxLayout(); + gridFlags->addWidget(gridVertical_); gridFlags->addWidget(gridSnake_); + + form->addRow("", modeStill_); + form->addRow("", modeRotation_); + form->addRow("Axis name", axisName_); + form->addRow("Axis vector", axis); + form->addRow("Start angle", rotStart_); + form->addRow("Increment", rotIncrement_); + form->addRow("", modeGrid_); + form->addRow("Points (fast)", gridNFast_); + form->addRow("Step x, y", step); + form->addRow("", gridFlags); + form->addRow("", gridSummary_); + section->setContentLayout(form); + + EnableRotationFields(false); // datasetLoaded picks the mode the file describes + EnableGridFields(false); + // One handler for the three: whichever is now on decides which fields are live, what the + // experiment carries, and whether the run is a rotation or stills one. + for (auto *b : {modeStill_, modeRotation_, modeGrid_}) + connect(b, &QRadioButton::toggled, this, [this](bool on) { + if (!on) + return; // the button being switched off reports first; act once, on the new mode + EnableRotationFields(modeRotation_->isChecked()); + EnableGridFields(modeGrid_->isChecked()); + UpdateGridSummary(); + ApplyProcessingMode(); + EmitExperiment(); + }); + connect(axisName_, &QLineEdit::editingFinished, this, [this] { EmitExperiment(); }); + for (auto *f : {axisX_, axisY_, axisZ_, rotStart_, rotIncrement_}) + connect(f, &NumberLineEdit::newValue, this, [this] { EmitExperiment(); }); + for (auto *f : {gridNFast_, gridStepX_, gridStepY_}) + connect(f, &NumberLineEdit::newValue, this, [this] { UpdateGridSummary(); EmitExperiment(); }); + for (auto *b : {gridVertical_, gridSnake_}) + connect(b, &QCheckBox::toggled, this, [this] { UpdateGridSummary(); EmitExperiment(); }); + + return section; +} + +void JFJochViewerSettingsDock::EnableRotationFields(bool on) { + axisName_->setEnabled(on); + for (auto *f : {axisX_, axisY_, axisZ_, rotStart_, rotIncrement_}) + f->setEnabled(on); +} + +void JFJochViewerSettingsDock::EnableGridFields(bool on) { + for (auto *f : {gridNFast_, gridStepX_, gridStepY_}) + f->setEnabled(on); + gridVertical_->setEnabled(on); + gridSnake_->setEnabled(on); +} + +void JFJochViewerSettingsDock::UpdateGridSummary() { + const auto grid = GridScanFromFields(); + gridSummary_->setText(grid ? QStringLiteral("%1 x %2 points for %3 images") + .arg(grid->GetGridSizeX_step()) + .arg(grid->GetGridSizeY_step()) + .arg(experiment_.GetImageNum()) + : QString()); +} + +std::optional JFJochViewerSettingsDock::GoniometerFromFields() const { + if (!modeRotation_->isChecked()) + return std::nullopt; + // GoniometerAxis throws on an empty name or a zero-length axis, and an exception thrown out of a + // widget signal aborts the viewer - fall back to the defaults for a half-typed entry. + std::string name = axisName_->text().trimmed().toStdString(); + if (name.empty()) + name = DEFAULT_AXIS_NAME; + Coord axis(static_cast(axisX_->value()), static_cast(axisY_->value()), + static_cast(axisZ_->value())); + if (axis.Length() == 0.0f) + axis = Coord(DEFAULT_AXIS); + GoniometerAxis gonio(name, static_cast(rotStart_->value()), + static_cast(rotIncrement_->value()), axis, gonioHelicalStep_); + gonio.ScreeningWedge(gonioScreeningWedge_); + return gonio; +} + +std::optional JFJochViewerSettingsDock::GridScanFromFields() const { + if (!modeGrid_->isChecked()) + return std::nullopt; + // GridScanSettings throws on a zero step, and an exception thrown out of a widget signal aborts + // the viewer - fall back to the default step for a half-typed entry. + float step_x = static_cast(gridStepX_->value()); + float step_y = static_cast(gridStepY_->value()); + if (step_x == 0.0f) + step_x = DEFAULT_GRID_STEP_UM; + if (step_y == 0.0f) + step_y = DEFAULT_GRID_STEP_UM; + GridScanSettings grid(std::llround(gridNFast_->value()), step_x, step_y, + gridSnake_->isChecked(), gridVertical_->isChecked()); + // The rows are however many the images fill, exactly as the reader does when a file carries a + // grid: the file stores n_fast alone. + grid.ImageNum(experiment_.GetImageNum()); + return grid; +} + 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 @@ -600,7 +788,7 @@ QWidget *JFJochViewerSettingsDock::BuildBraggSection() { } QWidget *JFJochViewerSettingsDock::BuildScalingSection() { - // The partiality model + rot3d combine + scale-fulls are driven by "Process as stills" (indexing + // The partiality model + rot3d combine + scale-fulls are driven by the rotation axis (goniometer // 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(); @@ -678,6 +866,10 @@ void JFJochViewerSettingsDock::EmitExperiment() { 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)); + experiment_.PolarizationFactor(polarizationOn_->isChecked() + ? std::optional(static_cast(polarization_->value())) : std::nullopt); + experiment_.Goniometer(GoniometerFromFields()); + experiment_.GridScan(GridScanFromFields()); if (cellKnown_->isChecked()) { experiment_.SetUnitCell(UnitCell{ static_cast(cellA_->value()), static_cast(cellB_->value()), @@ -702,6 +894,44 @@ void JFJochViewerSettingsDock::RefreshGeometryFields() { rot1_->setValue(experiment_.GetPoniRot1_rad() * 180.0 / PI); rot2_->setValue(experiment_.GetPoniRot2_rad() * 180.0 / PI); + const auto polarization = experiment_.GetPolarizationFactor(); + QSignalBlocker blockPolarization(polarizationOn_); + polarizationOn_->setChecked(polarization.has_value()); + polarization_->setEnabled(polarization.has_value()); + if (polarization) + polarization_->setValue(polarization.value()); + + // The mode follows what the experiment describes, and the described one's fields are filled from + // it. The other two keep what they show - the generic defaults, or what the user last entered - + // so switching away and back does not lose an axis or a grid. + const auto gonio = experiment_.GetGoniometer(); + const auto grid = experiment_.GetGridScan(); + QSignalBlocker blockStill(modeStill_), blockRotation(modeRotation_), blockGrid(modeGrid_); + modeRotation_->setChecked(gonio.has_value()); + modeGrid_->setChecked(!gonio.has_value() && grid.has_value()); + modeStill_->setChecked(!gonio.has_value() && !grid.has_value()); + EnableRotationFields(gonio.has_value()); + EnableGridFields(modeGrid_->isChecked()); + if (gonio) { + axisName_->setText(QString::fromStdString(gonio->GetName())); + axisX_->setValue(gonio->GetAxis().x); + axisY_->setValue(gonio->GetAxis().y); + axisZ_->setValue(gonio->GetAxis().z); + rotStart_->setValue(gonio->GetStart_deg()); + rotIncrement_->setValue(gonio->GetIncrement_deg()); + gonioHelicalStep_ = gonio->GetHelicalStep(); + gonioScreeningWedge_ = gonio->GetScreeningWedge(); + } + if (grid) { + gridNFast_->setValue(grid->GetNFast()); + gridStepX_->setValue(grid->GetGridStepX_um()); + gridStepY_->setValue(grid->GetGridStepY_um()); + QSignalBlocker blockVertical(gridVertical_), blockSnake(gridSnake_); + gridVertical_->setChecked(grid->IsVerticalScan()); + gridSnake_->setChecked(grid->IsSnakeScan()); + } + UpdateGridSummary(); + const auto cell = experiment_.GetUnitCell(); QSignalBlocker blockKnown(cellKnown_); cellKnown_->setChecked(cell.has_value()); @@ -739,19 +969,14 @@ void JFJochViewerSettingsDock::datasetLoaded(std::shared_ptrexperiment; 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(); - } + ApplyProcessingMode(); // the mode just read back decides rotation vs stills } 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(); + // Rotation is the only mode that changes how the data is processed: the rotation good-path + // (rotation indexing + Ewald partiality + rot3d combine + scale-fulls). A grid scan is a raster + // of stills, so it runs exactly as Still does. + const bool rotation_mode = modeRotation_ && modeRotation_->isChecked(); indexing_.RotationIndexing(rotation_mode); // rotation indexing -> rotation scaling/merge downstream scaling_.ScaleFulls(rotation_mode); EmitSpotFinding(); // carries indexing_ (incl. RotationIndexing) to the worker diff --git a/viewer/widgets/JFJochViewerSettingsDock.h b/viewer/widgets/JFJochViewerSettingsDock.h index dca2908a..fb7f658f 100644 --- a/viewer/widgets/JFJochViewerSettingsDock.h +++ b/viewer/widgets/JFJochViewerSettingsDock.h @@ -22,7 +22,9 @@ class QStackedWidget; class QCheckBox; class QComboBox; class QLabel; +class QLineEdit; class QPushButton; +class QRadioButton; class SliderPlusBox; class NumberLineEdit; @@ -81,9 +83,6 @@ private: // Indexing-algorithm combo + the description line under it (kept so the Auto resolution refreshes). QComboBox *algo_ = nullptr; QLabel *algoDesc_ = nullptr; - // "Process as stills" — kept so datasetLoaded can enable it only when the dataset has a goniometer - // (a rotation dataset); it drives indexing/scaling mode via ApplyProcessingMode(). - QCheckBox *stills_ = nullptr; // Geometry fields (shared by both MX and AzInt, since both need the same geometry) NumberLineEdit *energy_ = nullptr; @@ -92,6 +91,32 @@ private: NumberLineEdit *beamY_ = nullptr; NumberLineEdit *rot1_ = nullptr; // detector tilt (PONI rot1), deg NumberLineEdit *rot2_ = nullptr; // detector tilt (PONI rot2), deg + // Polarization factor, also shared: it corrects the azimuthal profile and the Bragg intensities + // alike. Unchecked = no polarization correction (the experiment's factor is unset). + QCheckBox *polarizationOn_ = nullptr; + NumberLineEdit *polarization_ = nullptr; + + // How the sample moves between images — the three cases a dataset can be, exactly as a file + // stores them (a rotation axis, a grid scan, or neither). The choice sets or clears the + // experiment's goniometer / grid scan, and ApplyProcessingMode() turns it into the + // indexing/scaling mode. Each group keeps its values while another mode is selected. + QRadioButton *modeStill_ = nullptr; + QRadioButton *modeRotation_ = nullptr; + QRadioButton *modeGrid_ = nullptr; + QLineEdit *axisName_ = nullptr; + NumberLineEdit *axisX_ = nullptr, *axisY_ = nullptr, *axisZ_ = nullptr; + NumberLineEdit *rotStart_ = nullptr, *rotIncrement_ = nullptr; + // Not editable here, but part of the file's goniometer — kept so rebuilding the axis from the + // fields does not drop them. + std::optional gonioHelicalStep_; + std::optional gonioScreeningWedge_; + // Grid scan. Only the fast-axis point count is a free parameter: the number of slow rows follows + // from the image count (GridScanSettings::ImageNum), which is why there is no second count field + // and why gridSummary_ spells out the grid the settings actually make. + NumberLineEdit *gridNFast_ = nullptr; + NumberLineEdit *gridStepX_ = nullptr, *gridStepY_ = nullptr; + QCheckBox *gridVertical_ = nullptr, *gridSnake_ = nullptr; + QLabel *gridSummary_ = nullptr; // Crystal fields QCheckBox *cellKnown_ = nullptr; NumberLineEdit *cellA_ = nullptr, *cellB_ = nullptr, *cellC_ = nullptr; @@ -111,6 +136,7 @@ private: QWidget *BuildGeometrySection(); QWidget *BuildMXPage(); + QWidget *BuildGoniometerSection(); QWidget *BuildBraggSection(); QWidget *BuildScalingSection(); QWidget *BuildReferenceSection(); @@ -119,8 +145,13 @@ private: void SyncMinPix(); void EmitSpotFinding(); void EmitExperiment(); - void ApplyProcessingMode(); // "Process as stills" -> indexing + scaling rotation/stills mode + void ApplyProcessingMode(); // sample-motion mode -> indexing + scaling rotation/stills mode void UpdateAlgorithmDescription(); void RefreshGeometryFields(); void UpdateSpaceGroupName(); + void EnableRotationFields(bool on); + void EnableGridFields(bool on); + void UpdateGridSummary(); + [[nodiscard]] std::optional GoniometerFromFields() const; + [[nodiscard]] std::optional GridScanFromFields() const; }; diff --git a/viewer/windows/JFJochProcessingJobsWindow.cpp b/viewer/windows/JFJochProcessingJobsWindow.cpp index a2bb326a..9ab1bf35 100644 --- a/viewer/windows/JFJochProcessingJobsWindow.cpp +++ b/viewer/windows/JFJochProcessingJobsWindow.cpp @@ -197,8 +197,9 @@ int JFJochProcessingJobsWindow::askJob(const ReprocessingInputs &inputs, JobSpec prefixRowLayout->addWidget(prefix); prefixRowLayout->addWidget(browse); - // Rotation-vs-stills mode (rotation indexing + partiality + rot3d) is set in the settings panel via - // "Process as stills"; the dialog only collects run options. Scaling applies to MX full analysis. + // Rotation-vs-stills mode (rotation indexing + partiality + rot3d) is set in the settings panel by + // the goniometer rotation axis; the dialog only collects run options. Scaling applies to MX full + // analysis. auto *scaling = new QCheckBox("Scale && merge", &dlg); scaling->setChecked(true); scaling->setEnabled(!azint && !calibration); @@ -362,7 +363,7 @@ ProcessConfig JFJochProcessingJobsWindow::buildConfig(const JobSpec &spec, const config.spot_finding.indexing = false; } if (spec.mode == ProcessMode::FullAnalysis) { - // Rotation indexing follows the panel's "Process as stills" (= the experiment's indexing + // Rotation indexing follows the panel's rotation axis (= the experiment's indexing // setting); a rotation run uses 60 first-pass images to find the lattice. config.rotation_indexing = inputs.experiment.GetIndexingSettings().GetRotationIndexing(); config.two_pass_rotation = true; @@ -396,10 +397,11 @@ void JFJochProcessingJobsWindow::newJob(ProcessMode mode, CalibrationSelection c const ProcessConfig config = buildConfig(spec, inputs); - // The experiment carries the panel's indexing settings — including RotationIndexing set by "Process - // as stills" (needed so IndexAndRefine builds a rotation indexer). On top of that it gets exactly - // the defaults `rugnux` with no options would apply, so the two front ends agree: the analysis - // policy the panel does not expose (the polarization factor) and the rotation scaling defaults. + // The experiment carries the panel's indexing settings — including RotationIndexing set by the + // rotation axis (needed so IndexAndRefine builds a rotation indexer). On top of that it gets + // exactly the defaults `rugnux` with no options would apply, so the two front ends agree on the + // analysis policy — except where the panel states its own, which wins (the polarization factor, + // and the scaling fields below). // // The cell and space group come from the settings panel and nowhere else: with "Unit cell known" // ticked they are indexed and merged with, unticked the panel clears both and the run determines @@ -408,7 +410,9 @@ void JFJochProcessingJobsWindow::newJob(ProcessMode mode, CalibrationSelection c // job's _process.h5 becomes the active snapshot, where a run that ended in P1 shows P1 and can be // cleared, rather than silently pinning every later run to it. DiffractionExperiment experiment = inputs.experiment; + const auto polarization = experiment.GetPolarizationFactor(); ApplyRugnuxExperimentDefaults(experiment); + experiment.PolarizationFactor(polarization); // the panel's setting, not the rugnux default // Calibrating from the run-summed profile needs the profile to be binned in azimuth; the panel's // bin count is an azimuthal-integration setting and defaults to a plain radial profile, which // carries no information about where the ring centre is. Same fallback as the rugnux CLI. @@ -431,8 +435,15 @@ void JFJochProcessingJobsWindow::newJob(ProcessMode mode, CalibrationSelection c } if (action == 2) { // copy command line + // The command line is run against the file, so it has to state what the panel changed. The + // one thing it cannot state is the rotation axis (rugnux has no flag for it) - but switching + // the axis off is `--force-still`, which RugnuxCommandLine emits from the axis the file has, + // not the one the panel cleared. Put the file's back for the command line only. + DiffractionExperiment cmdline_experiment = experiment; + if (!cmdline_experiment.GetGoniometer().has_value()) + cmdline_experiment.Goniometer(inputs.file_goniometer); const QString cmd = QString::fromStdString( - RugnuxCommandLine(config, experiment, inputs.file.toStdString(), + RugnuxCommandLine(config, cmdline_experiment, inputs.file.toStdString(), spec.calibration.name.toStdString())); QApplication::clipboard()->setText(cmd); QMessageBox::information(this, "Command line", cmd + "\n\n(copied to clipboard)");