diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f2d5423fe..d427dbb89 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## 1.0.0 ### 1.0.0-rc.166 +* `rugnux --mode calibration --no-refine-tilt`, and the "Refine detector tilt" tick box in the viewer's calibration panel, fit the beam centre and the distance with the detector tilt held fixed, for a calibration meant for a program that cannot express a tilted detector. * A miniCBF sweep takes the mounting from the imgCIF axis table its header carries: which laboratory direction the image's columns and rows run along, and which the spindle turns about. Where there is no table, a `+SLOW` on the `Oscillation_axis` line still says the spindle runs along the image's slow direction. All three were previously assumed, and two instruments are not what was assumed. * Naming a miniCBF frame with no directory - a frame in the working directory - finds its sweep instead of reporting that no images were found. * A detector swung out on a 2theta arm is placed where it stands, from the depends_on transformation chain of an NXmx master or the `Detector_2theta` line of a miniCBF header; both were previously read and then ignored. diff --git a/docs/CPU_DATA_ANALYSIS.md b/docs/CPU_DATA_ANALYSIS.md index 481c85a71..e4b5dd110 100644 --- a/docs/CPU_DATA_ANALYSIS.md +++ b/docs/CPU_DATA_ANALYSIS.md @@ -588,7 +588,7 @@ Only pass 2 is written, as the canonical `_*` output. Pass 1's merge exi Everything above fits the geometry to *Bragg* data, where the beam centre is the weakest parameter: it is gauge-coupled to the crystal orientation, which is why §7.5 restrains it toward the header value and commits only a sub-1 % move. A **powder ring has no orientation to be coupled to**. Where it falls on the detector depends on the geometry and on nothing else, which makes a calibrant — LaB₆, silver behenate, CeO₂, silicon — or even ice an independent constraint on exactly the quantity Bragg data cannot pin. -The ring positions are matched to the observed rings and the geometry is refined (Ceres, five parameters: beam centre, distance, and the two detector tilts) so that the $|s|$ predicted at each observed ring point matches the ring it belongs to. +The ring positions are matched to the observed rings and the geometry is refined (Ceres, five parameters: beam centre, distance, and the two detector tilts) so that the $|s|$ predicted at each observed ring point matches the ring it belongs to. The two tilts can be held fixed (`rugnux --no-refine-tilt`, the viewer's *Refine detector tilt* tick box), leaving a three-parameter fit: a tilt a downstream program cannot express is better left out of the fit than refined and then dropped, since the centre and the distance of a tilted fit have already absorbed it. **Calibrants.** LaB₆, silver behenate, CeO₂ and silicon are held as unit cells and their rings enumerated from them. Ice is held as the hexagonal-ice ring positions of §3.3 instead — measured to 1.522 Å, calculated below it — because hexagonal ice is $P6_3/mmc$ with oxygen on $4f$ and enumerating $hkl$ from its cell would emit rings the oxygen sublattice extinguishes. A calibrant is therefore a list of ring $q$ values throughout, not a cell. diff --git a/docs/JFJOCH_VIEWER.md b/docs/JFJOCH_VIEWER.md index 88afc5fd0..47d46602b 100644 --- a/docs/JFJOCH_VIEWER.md +++ b/docs/JFJOCH_VIEWER.md @@ -48,6 +48,10 @@ install it. so a fit that sits a couple of pixels off every ring can still report a small one. *Rings* needs the run to be integrated in azimuthal sectors — with the AzInt page's *Azimuthal bins* below 4 the calibration run raises it to 32, as `rugnux --mode calibration` does, and says so. + *Refine detector tilt* is ticked by default and fits the two tilts along with the centre and the + distance; unticking it holds them where they are, for a calibration meant for a program that + cannot express a tilted detector (`rugnux --no-refine-tilt`). It applies to both buttons and to + *Analyze dataset*. - **Settings** panel for the geometry, unit cell, spot finding, indexing, azimuthal integration, Bragg integration, scaling, powder calibration and a reference dataset — the same settings the CLI takes. diff --git a/docs/RUGNUX.md b/docs/RUGNUX.md index 72e29f436..361b79038 100644 --- a/docs/RUGNUX.md +++ b/docs/RUGNUX.md @@ -894,6 +894,12 @@ standard error that implies on the centre. That error is *formal*: it measures t points, not whether the rings themselves are trustworthy, so it stays small when a fit goes wrong for a structural reason — one visible ring, or ice that is textured rather than smooth. +`--no-refine-tilt` holds rot1/rot2 where the header put them and fits only the centre and the +distance. The tilt is real and worth measuring, but a program that has nowhere to put one — XDS +takes a detector normal to the beam — is better given a geometry that was measured with the tilt +pinned than one that was measured tilted and then flattened, because in the tilted fit the centre +and the distance have already absorbed the tilt. + Both the PONI (the point of normal incidence, which is what a `.poni` file stores) and the direct beam (where the beam lands, which is what most other programs call the beam centre) are printed. They differ by *distance* × tan(rot) once the detector is tilted, which on a 0.3° tilt at 300 mm is @@ -949,6 +955,7 @@ Calibration (`--mode calibration`): | --- | --- | | `--calibrant ` | Powder standard: `lab6` \| `agbh` \| `ceo2` \| `si` \| `ice` (default `lab6`, case-insensitive) | | `--calibration ` | How the rings are measured: `rings` \| `spots` (default `rings`; see above). `rings` defaults `--azim-phi-bins` to 32 | +| `--no-refine-tilt` | Do not refine the detector tilt: hold rot1/rot2 at the header value and fit only the beam centre and the distance, for a calibration handed to a program that cannot express a tilted detector (XDS) | Detector mask: diff --git a/image_analysis/geom_refinement/AssignSpotsToRings.cpp b/image_analysis/geom_refinement/AssignSpotsToRings.cpp index a53e2fd92..a99e669a7 100644 --- a/image_analysis/geom_refinement/AssignSpotsToRings.cpp +++ b/image_analysis/geom_refinement/AssignSpotsToRings.cpp @@ -239,7 +239,8 @@ std::vector GuessInitialGeometry(DiffractionGeometry &geom, const return cluster_annot; } -void GuessGeometry(DiffractionGeometry &geom, const std::vector &v, const std::vector &ring_q) { +void GuessGeometry(DiffractionGeometry &geom, const std::vector &v, const std::vector &ring_q, + bool refine_tilt) { if (ring_q.empty()) throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "No calibrant rings given"); auto cluster_annot = GuessInitialGeometry(geom, v, 2 * PI / ring_q[0]); @@ -273,7 +274,7 @@ void GuessGeometry(DiffractionGeometry &geom, const std::vector &v, } } - RingOptimizer optimizer(geom); + RingOptimizer optimizer(geom, refine_tilt); geom = optimizer.Run(optimizer_input); } @@ -294,7 +295,8 @@ std::vector AssignSpotsToRings(const DiffractionGeometry &ge return optimizer_input; } -void OptimizeGeometry(DiffractionGeometry &geom, const std::vector &v, const std::vector &ring_q) { - RingOptimizer optimizer(geom); +void OptimizeGeometry(DiffractionGeometry &geom, const std::vector &v, const std::vector &ring_q, + bool refine_tilt) { + RingOptimizer optimizer(geom, refine_tilt); geom = optimizer.Run(AssignSpotsToRings(geom, v, ring_q)); } diff --git a/image_analysis/geom_refinement/AssignSpotsToRings.h b/image_analysis/geom_refinement/AssignSpotsToRings.h index 08fa67870..0dc5dd6dd 100644 --- a/image_analysis/geom_refinement/AssignSpotsToRings.h +++ b/image_analysis/geom_refinement/AssignSpotsToRings.h @@ -50,8 +50,11 @@ float GuessDetectorDistance(const DiffractionGeometry& geom, float ring_radius_p // measured rather than enumerated from a cell - hexagonal ice - can be calibrated against as well. // GuessInitialGeometry needs only the outermost d-spacing, to seed the distance from the innermost ring. std::vector GuessInitialGeometry(DiffractionGeometry &geom, const std::vector &v, float largest_ring_d_A); -void GuessGeometry(DiffractionGeometry &geom, const std::vector &v, const std::vector &ring_q); -void OptimizeGeometry(DiffractionGeometry &geom, const std::vector &v, const std::vector &ring_q); +// refine_tilt = false pins rot1/rot2 where the geometry already has them (see RingOptimizer). +void GuessGeometry(DiffractionGeometry &geom, const std::vector &v, const std::vector &ring_q, + bool refine_tilt = true); +void OptimizeGeometry(DiffractionGeometry &geom, const std::vector &v, const std::vector &ring_q, + bool refine_tilt = true); // Each spot paired with the calibrant ring nearest its observed q, as the points RingOptimizer fits. // Spots more than 0.1 1/A from every ring are dropped rather than forced onto the closest one. diff --git a/image_analysis/geom_refinement/RingOptimizer.cpp b/image_analysis/geom_refinement/RingOptimizer.cpp index f29d32ed9..733cddc31 100644 --- a/image_analysis/geom_refinement/RingOptimizer.cpp +++ b/image_analysis/geom_refinement/RingOptimizer.cpp @@ -69,7 +69,8 @@ struct RingResidual { double det_m00, det_m01, det_m10, det_m11; }; -RingOptimizer::RingOptimizer(const DiffractionGeometry& geom) : reference(geom) {} +RingOptimizer::RingOptimizer(const DiffractionGeometry& geom, bool refine_tilt) + : reference(geom), refine_tilt(refine_tilt) {} DiffractionGeometry RingOptimizer::Run(const std::vector &input) { // Initial guess for the parameters @@ -107,7 +108,7 @@ DiffractionGeometry RingOptimizer::Run(const std::vector &in && std::all_of(input.begin(), input.end(), [&](const RingOptimizerInput &p) { return p.q_expected == input.front().q_expected; }); - if (single_ring) { + if (single_ring || !refine_tilt) { problem.SetParameterBlockConstant(&rot1); problem.SetParameterBlockConstant(&rot2); } diff --git a/image_analysis/geom_refinement/RingOptimizer.h b/image_analysis/geom_refinement/RingOptimizer.h index 59e646b1f..927cfd079 100644 --- a/image_analysis/geom_refinement/RingOptimizer.h +++ b/image_analysis/geom_refinement/RingOptimizer.h @@ -14,8 +14,13 @@ struct RingOptimizerInput { class RingOptimizer { DiffractionGeometry reference; + bool refine_tilt; public: - RingOptimizer(const DiffractionGeometry& geom); + // refine_tilt = false holds the detector tilt (rot1/rot2) at the value the geometry came in with + // and fits only the beam centre and the distance. A tilted PONI is exact but not every consumer + // accepts one - XDS has no place to put it - so a calibration meant for such a program is better + // measured with the tilt pinned than with it refined and then dropped. + RingOptimizer(const DiffractionGeometry& geom, bool refine_tilt = true); DiffractionGeometry Run(const std::vector &input); }; diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index aca39cbf0..7c09187da 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -2857,7 +2857,8 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b logger.Info("Powder calibration from {} pooled spots", calibration_spot_list.size()); result.calibration = CalibrateFromSpots(calibration_spot_list, experiment_.GetDiffractionGeometry(), - config_.calibrant_ring_q); + config_.calibrant_ring_q, + config_.calibration_refine_tilt); } else { const auto *summed = plots.GetAzIntProfileObject(); if (!summed) @@ -2865,7 +2866,8 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b "No azimuthal profile was accumulated"); result.calibration = CalibrateFromProfile(summed->GetResult(), mapping, experiment_.GetDiffractionGeometry(), - config_.calibrant_ring_q); + config_.calibrant_ring_q, + config_.calibration_refine_tilt); } } diff --git a/rugnux/Rugnux.h b/rugnux/Rugnux.h index 3b81f0805..064b7f21c 100644 --- a/rugnux/Rugnux.h +++ b/rugnux/Rugnux.h @@ -69,6 +69,10 @@ struct ProcessConfig { // The library takes the ring list, not a calibrant name - the CLI resolves it (CalibrantRings). CalibrationMethod calibration_method = CalibrationMethod::Rings; std::vector calibrant_ring_q; + // Fit the detector tilt (PONI rot1/rot2) along with the beam centre and the distance. False holds + // the tilt where the header put it, for a calibration destined for a program that cannot express + // one - XDS. + bool calibration_refine_tilt = true; // Measure the spot budget instead of taking experiment.GetMaxSpotCount() as given (FullAnalysis, // rotation only; the rugnux CLI sets it unless --max-spots pinned the budget). The first pass reads diff --git a/rugnux/RugnuxCalibration.cpp b/rugnux/RugnuxCalibration.cpp index 412135b70..6fe28d8ca 100644 --- a/rugnux/RugnuxCalibration.cpp +++ b/rugnux/RugnuxCalibration.cpp @@ -56,23 +56,25 @@ CalibrationResult Summarize(const DiffractionGeometry &fitted, CalibrationResult CalibrateFromProfile(const std::vector &profile, const AzimuthalIntegrationMapping &mapping, const DiffractionGeometry &geom, - const std::vector &calibrant_ring_q) { + const std::vector &calibrant_ring_q, + bool refine_tilt) { const auto points = RingsFromAzimuthalProfile(profile, mapping, geom, calibrant_ring_q); if (points.empty()) throw JFJochException(JFJochExceptionCategory::CalibrationError, "No powder ring found in the summed azimuthal profile"); - return Summarize(RingOptimizer(geom).Run(points), points); + return Summarize(RingOptimizer(geom, refine_tilt).Run(points), points); } CalibrationResult CalibrateFromSpots(const std::vector &spots, const DiffractionGeometry &geom, - const std::vector &calibrant_ring_q) { + const std::vector &calibrant_ring_q, + bool refine_tilt) { DiffractionGeometry fitted = geom; // From scratch (Hough circle centre + ring clustering), then refined: the guess pins the centre to a // whole pixel and only sees the spots its clustering kept, so the refine re-matches every spot at // that geometry. - GuessGeometry(fitted, spots, calibrant_ring_q); - OptimizeGeometry(fitted, spots, calibrant_ring_q); + GuessGeometry(fitted, spots, calibrant_ring_q, refine_tilt); + OptimizeGeometry(fitted, spots, calibrant_ring_q, refine_tilt); return Summarize(fitted, AssignSpotsToRings(fitted, spots, calibrant_ring_q)); } diff --git a/rugnux/RugnuxCalibration.h b/rugnux/RugnuxCalibration.h index 3f98d664e..0ab1a87e4 100644 --- a/rugnux/RugnuxCalibration.h +++ b/rugnux/RugnuxCalibration.h @@ -27,17 +27,24 @@ struct CalibrationResult { double beam_sigma_pxl = 0.0; }; +// Both fits take the detector tilt as a free parameter unless refine_tilt is false, which holds +// rot1/rot2 at the value `geom` came in with and fits only the beam centre and the distance. The +// tilt is real and a PONI carries it, but a program that has nowhere to put it - XDS - is better +// given a geometry measured with the tilt pinned than one measured tilted and then flattened. + // Fit the geometry to the rings found in a run-summed azimuthal profile (CalibrationMethod::Rings). CalibrationResult CalibrateFromProfile(const std::vector &profile, const AzimuthalIntegrationMapping &mapping, const DiffractionGeometry &geom, - const std::vector &calibrant_ring_q); + const std::vector &calibrant_ring_q, + bool refine_tilt = true); // Fit the geometry to a pooled spot list (CalibrationMethod::Spots): the beam centre from scratch off // the Hough circle centre, then the same ring fit. CalibrationResult CalibrateFromSpots(const std::vector &spots, const DiffractionGeometry &geom, - const std::vector &calibrant_ring_q); + const std::vector &calibrant_ring_q, + bool refine_tilt = true); // Write the geometry as a pyFAI PONI file, the interchange format every azimuthal-integration tool // reads. Throws if the file cannot be written. diff --git a/rugnux/RugnuxCommandLine.cpp b/rugnux/RugnuxCommandLine.cpp index 391a52e89..9f869732e 100644 --- a/rugnux/RugnuxCommandLine.cpp +++ b/rugnux/RugnuxCommandLine.cpp @@ -96,6 +96,8 @@ std::string RugnuxCommandLine(const ProcessConfig &config, if (!calibrant_name.empty()) add("--calibrant", calibrant_name); add("--calibration", config.calibration_method == CalibrationMethod::Rings ? "rings" : "spots"); + if (!config.calibration_refine_tilt) + args.emplace_back("--no-refine-tilt"); // The rings method reads the ring at every azimuth, and the GUI may have raised the sector // count for this run, so emit the value actually used rather than relying on the CLI default. if (config.calibration_method == CalibrationMethod::Rings) diff --git a/rugnux/rugnux_cli.cpp b/rugnux/rugnux_cli.cpp index 83a8d9fb2..12b18d939 100644 --- a/rugnux/rugnux_cli.cpp +++ b/rugnux/rugnux_cli.cpp @@ -100,6 +100,7 @@ void print_usage() { std::cout << " Calibration (--mode calibration)" << std::endl; std::cout << " --calibrant Powder standard: lab6|agbh|ceo2|si|ice (default: lab6, case-insensitive)" << std::endl; std::cout << " --calibration How the rings are measured: rings|spots (default: rings). rings sums the (q x azimuth) azimuthal profile over every processed image and fits the ring arcs in it; spots pools the found spots and fits those. -s/-e/--stride select the images; rings defaults --azim-phi-bins to 32" << std::endl; + std::cout << " --no-refine-tilt Do not refine the detector tilt: hold rot1/rot2 at the header value and fit only the beam centre and the distance. For a calibration handed to a program that cannot express a tilted detector, e.g. XDS" << std::endl; std::cout << std::endl; std::cout << " Detector mask" << std::endl; @@ -298,7 +299,8 @@ enum { OPT_DETECTOR_MIRROR_Y, OPT_DETECTOR_QUARTER_TURNS, OPT_FFT_MIN_UNIT_CELL, - OPT_POLARIZATION + OPT_POLARIZATION, + OPT_NO_REFINE_TILT }; static option long_options[] = { @@ -323,6 +325,7 @@ static option long_options[] = { {"mode", required_argument, nullptr, OPT_MODE}, {"calibrant", required_argument, nullptr, OPT_CALIBRANT}, {"calibration", required_argument, nullptr, OPT_CALIBRATION}, + {"no-refine-tilt", no_argument, nullptr, OPT_NO_REFINE_TILT}, {"no-merge", no_argument, nullptr, OPT_NO_MERGE}, {"no-p1-crosscheck", no_argument, nullptr, OPT_NO_P1_CROSSCHECK}, {"scale-fulls", no_argument, nullptr, OPT_SCALE_FULLS}, @@ -661,6 +664,7 @@ static int RunRugnux(int argc, char **argv) { RugnuxMode mode = RugnuxMode::MX; // --mode std::string calibrant = "lab6"; // --calibrant CalibrationMethod calibration_method = CalibrationMethod::Rings; // --calibration + bool calibration_refine_tilt = true; // --no-refine-tilt bool rotation_indexing = false; bool force_still = false; // --force-still: process a rotation dataset as stills (indexing + scaling) bool two_pass_rotation = true; @@ -1071,6 +1075,9 @@ static int RunRugnux(int argc, char **argv) { return 1; } break; + case OPT_NO_REFINE_TILT: + calibration_refine_tilt = false; + break; case OPT_NO_MERGE: run_scaling = false; break; @@ -1973,6 +1980,7 @@ static int RunRugnux(int argc, char **argv) { ProcessConfig config; config.mode = ProcessMode::Calibration; config.calibration_method = calibration_method; + config.calibration_refine_tilt = calibration_refine_tilt; config.calibrant_ring_q = CalibrantRings(calibrant); config.start_image = start_image; config.end_image = end_image; @@ -1999,10 +2007,11 @@ static int RunRugnux(int argc, char **argv) { config.spot_finding.low_resolution_limit = d_max_spot_finding; experiment.MaxSpotCount(max_spot_count_override.value_or(RUGNUX_MAX_SPOT_COUNT)); - logger.Info("Powder calibration: calibrant {} ({} rings), method {}, {} azimuthal bins", + logger.Info("Powder calibration: calibrant {} ({} rings), method {}, {} azimuthal bins, tilt {}", calibrant, config.calibrant_ring_q.size(), calibration_method == CalibrationMethod::Rings ? "rings" : "spots", - experiment.GetAzimuthalIntegrationSettings().GetAzimuthalBinCount()); + experiment.GetAzimuthalIntegrationSettings().GetAzimuthalBinCount(), + calibration_refine_tilt ? "refined" : "fixed"); Rugnux process(reader, experiment, *dataset->pixel_mask, config); g_active_process = &process; diff --git a/tests/DetGeomCalibTest.cpp b/tests/DetGeomCalibTest.cpp index b1ec526e5..2574f73f0 100644 --- a/tests/DetGeomCalibTest.cpp +++ b/tests/DetGeomCalibTest.cpp @@ -266,3 +266,39 @@ TEST_CASE("DetGeomCalib_OptimizeGeometry") { CHECK(geom_i.GetPoniRot1_rad() == Catch::Approx(geom.GetPoniRot1_rad())); CHECK(geom_i.GetPoniRot2_rad() == Catch::Approx(geom.GetPoniRot2_rad())); } + +// The tilt is held where it was given, and the beam centre and distance are still fitted. This is what +// a calibration destined for a program with no way to express a tilted detector asks for. +TEST_CASE("DetGeomCalib_RingOptimizerFixedTilt") { + DiffractionGeometry geom; + geom.Wavelength_A(1.0).BeamX_pxl(100.0).BeamY_pxl(200.0).DetectorDistance_mm(100); + + const float lab6_a = LAB6_CELL_A; + + std::vector spots; + for (int d = 1; d < 4; d++) { + for (int i = 0; i < 30; i++) { + auto [x, y] = geom.ResPhiToPxl(lab6_a / sqrtf(d), i * M_PI * 2.0 / 30.0); + spots.push_back(RingOptimizerInput{.x = x, .y = y, + .q_expected = sqrtf(d) * M_PI * 2.0 / lab6_a}); + } + } + + DiffractionGeometry geom_i; + geom_i.Wavelength_A(1.0).BeamX_pxl(105).BeamY_pxl(195).DetectorDistance_mm(110); + + DiffractionGeometry geom_o = RingOptimizer(geom_i, false).Run(spots); + + CHECK(geom_o.GetPoniRot1_rad() == 0.0f); + CHECK(geom_o.GetPoniRot2_rad() == 0.0f); + CHECK(geom_o.GetBeamX_pxl() == Catch::Approx(geom.GetBeamX_pxl())); + CHECK(geom_o.GetBeamY_pxl() == Catch::Approx(geom.GetBeamY_pxl())); + CHECK(geom_o.GetDetectorDistance_mm() == Catch::Approx(geom.GetDetectorDistance_mm())); + + // A tilt already in the geometry stays exactly where it is rather than being refined away. + DiffractionGeometry geom_tilted(geom_i); + geom_tilted.PoniRot1_rad(0.1).PoniRot2_rad(0.05); + DiffractionGeometry geom_t = RingOptimizer(geom_tilted, false).Run(spots); + CHECK(geom_t.GetPoniRot1_rad() == Catch::Approx(0.1)); + CHECK(geom_t.GetPoniRot2_rad() == Catch::Approx(0.05)); +} diff --git a/viewer/JFJochImageReadingWorker.cpp b/viewer/JFJochImageReadingWorker.cpp index c3f6e7f24..e1893f564 100644 --- a/viewer/JFJochImageReadingWorker.cpp +++ b/viewer/JFJochImageReadingWorker.cpp @@ -545,7 +545,7 @@ void JFJochImageReadingWorker::Analyze() { EmitImageLoaded_i(current_image_ptr); } -void JFJochImageReadingWorker::FindCenter(QVector ring_q, bool guess) { +void JFJochImageReadingWorker::FindCenter(QVector ring_q, bool guess, bool refine_tilt) { QMutexLocker locker(&m); if (!current_image_ptr) return; @@ -555,9 +555,9 @@ void JFJochImageReadingWorker::FindCenter(QVector ring_q, bool guess) { const std::vector ring_Q(ring_q.begin(), ring_q.end()); try { if (guess) - GuessGeometry(geom, current_image_ptr->ImageData().spots, ring_Q); + GuessGeometry(geom, current_image_ptr->ImageData().spots, ring_Q, refine_tilt); else - OptimizeGeometry(geom, current_image_ptr->ImageData().spots, ring_Q); + OptimizeGeometry(geom, current_image_ptr->ImageData().spots, ring_Q, refine_tilt); } catch (const JFJochException &e) { logger.ErrorException(e); return; diff --git a/viewer/JFJochImageReadingWorker.h b/viewer/JFJochImageReadingWorker.h index 210c8e899..6bb2b7f4f 100644 --- a/viewer/JFJochImageReadingWorker.h +++ b/viewer/JFJochImageReadingWorker.h @@ -232,7 +232,7 @@ public slots: // The calibrant as its ring q values (2*pi/d, 1/A) rather than a cell, so a standard whose rings // are measured rather than enumerated - hexagonal ice - can be calibrated against too. - void FindCenter(QVector ring_q, bool guess); + void FindCenter(QVector ring_q, bool guess, bool refine_tilt); void Analyze(); diff --git a/viewer/JFJochViewerSidePanel.h b/viewer/JFJochViewerSidePanel.h index 1215c6e13..938c9a9d2 100644 --- a/viewer/JFJochViewerSidePanel.h +++ b/viewer/JFJochViewerSidePanel.h @@ -42,7 +42,7 @@ signals: void downloadROIs(); void uploadROIs(); void maskFromROI(QString name, bool add); - void findBeamCenter(QVector ring_q, bool guess); + void findBeamCenter(QVector ring_q, bool guess, bool refine_tilt); void analyze(); void imageLoaded(std::shared_ptr image); diff --git a/viewer/widgets/JFJochViewerSettingsDock.h b/viewer/widgets/JFJochViewerSettingsDock.h index 00761a4fd..639846ebe 100644 --- a/viewer/widgets/JFJochViewerSettingsDock.h +++ b/viewer/widgets/JFJochViewerSettingsDock.h @@ -53,7 +53,7 @@ signals: void braggChanged(BraggIntegrationSettings settings); void scalingChanged(ScalingSettings settings); void experimentChanged(const DiffractionExperiment &experiment); - void findBeamCenter(QVector ring_q, bool guess); + void findBeamCenter(QVector ring_q, bool guess, bool refine_tilt); void ringsFromCalibration(QVector rings); void referenceSelected(QString path, QString column); // user picked a reference MTZ / column void reanalyzeImage(bool armed); // "Analyze image" toggle diff --git a/viewer/widgets/PowderCalibrationWidget.cpp b/viewer/widgets/PowderCalibrationWidget.cpp index 12b4081d7..14dd62208 100644 --- a/viewer/widgets/PowderCalibrationWidget.cpp +++ b/viewer/widgets/PowderCalibrationWidget.cpp @@ -25,6 +25,16 @@ PowderCalibrationWidget::PowderCalibrationWidget(QWidget *parent) : QWidget(pare "(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); @@ -48,6 +58,8 @@ PowderCalibrationWidget::PowderCalibrationWidget(QWidget *parent) : QWidget(pare 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); @@ -75,7 +87,8 @@ std::vector PowderCalibrationWidget::GetCalibrantRings() const { CalibrationSelection PowderCalibrationWidget::Selection() const { return {calibrantCombo->currentText(), GetCalibrantRings(), - static_cast(methodCombo->currentData().toInt())}; + static_cast(methodCombo->currentData().toInt()), + refineTiltCheck->isChecked()}; } void PowderCalibrationWidget::updateCalibrantList() { @@ -102,10 +115,10 @@ void PowderCalibrationWidget::loadImage(std::shared_ptr void PowderCalibrationWidget::findBeamCenterClicked() { const std::vector rings = GetCalibrantRings(); - emit findBeamCenter(QVector(rings.begin(), rings.end()), true); + emit findBeamCenter(QVector(rings.begin(), rings.end()), true, refineTiltCheck->isChecked()); } void PowderCalibrationWidget::optimizeBeamCenterClicked() { const std::vector rings = GetCalibrantRings(); - emit findBeamCenter(QVector(rings.begin(), rings.end()), false); + emit findBeamCenter(QVector(rings.begin(), rings.end()), false, refineTiltCheck->isChecked()); } diff --git a/viewer/widgets/PowderCalibrationWidget.h b/viewer/widgets/PowderCalibrationWidget.h index 217f30e89..8605edd40 100644 --- a/viewer/widgets/PowderCalibrationWidget.h +++ b/viewer/widgets/PowderCalibrationWidget.h @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include #include "../../common/UnitCell.h" @@ -16,6 +17,7 @@ struct CalibrationSelection { QString name; std::vector ring_q; // q = 2*pi/d [1/A], ascending CalibrationMethod method = CalibrationMethod::Rings; + bool refine_tilt = true; // false pins rot1/rot2 where they are (a calibration meant for XDS) }; // Detector geometry from a powder standard: the calibrant, and how its rings are measured. The two @@ -27,13 +29,14 @@ class PowderCalibrationWidget : public QWidget { QComboBox* calibrantCombo{nullptr}; // stores current calibrant selection QComboBox* methodCombo{nullptr}; // rings / spots, for the whole-dataset run + QCheckBox* refineTiltCheck{nullptr};// fit the detector tilt, or hold it where it is std::vector GetCalibrantRings() const; void updateCalibrantList(); signals: void ringsFromCalibration(QVector v); - void findBeamCenter(QVector ring_q, bool guess); + void findBeamCenter(QVector ring_q, bool guess, bool refine_tilt); public: PowderCalibrationWidget(QWidget* parent); CalibrationSelection Selection() const; diff --git a/viewer/windows/JFJochProcessingJobsWindow.cpp b/viewer/windows/JFJochProcessingJobsWindow.cpp index 4e9a436fe..57ba45a2f 100644 --- a/viewer/windows/JFJochProcessingJobsWindow.cpp +++ b/viewer/windows/JFJochProcessingJobsWindow.cpp @@ -301,6 +301,8 @@ int JFJochProcessingJobsWindow::askJob(const ReprocessingInputs &inputs, JobSpec .arg(spec.calibration.name) .arg(spec.calibration.ring_q.size()) .arg(rings ? "rings" : "spots"); + if (!spec.calibration.refine_tilt) + text += ", detector tilt held fixed"; if (rings) text += panel_bins < 4 ? QStringLiteral("\nAzimuthal bins: %1 is too few to locate a ring — using %2") @@ -376,6 +378,7 @@ ProcessConfig JFJochProcessingJobsWindow::buildConfig(const JobSpec &spec, const config.fit_spindle = spec.estimate_beam_center; if (spec.mode == ProcessMode::Calibration) { config.calibration_method = spec.calibration.method; + config.calibration_refine_tilt = spec.calibration.refine_tilt; config.calibrant_ring_q = spec.calibration.ring_q; // Spot finding for the spots method. Indexing is off: a calibration wants the spot positions and // nothing else, and a calibrant is a powder with no lattice to index.