diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ae1f9add3..2a4c3dc24 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -18,6 +18,7 @@ * The `rugnux` report gives the detector tilt and the direct beam beside the beam centre, and carries the tilt in the `dataset_settings` block. * The `rugnux` report gives the detector tilt rotation indexing measured, so it can be checked against a powder calibration. * `--no-refine-tilt` holds the detector tilt at the value in the file, instead of zeroing it, when the calibration starts from the spots. +* A post-refined beam centre is judged against the measurement the run makes for itself, so a badly-placed header no longer caps how far it may be corrected. * The `jfjoch_viewer` grid scan view draws the cells in the proportion of the scan steps, so the map has the shape of the scanned area. ### 1.0.0-rc.165 diff --git a/image_analysis/geom_refinement/PostRefine.cpp b/image_analysis/geom_refinement/PostRefine.cpp index c16755fa0..a900a9499 100644 --- a/image_analysis/geom_refinement/PostRefine.cpp +++ b/image_analysis/geom_refinement/PostRefine.cpp @@ -20,6 +20,20 @@ namespace { +// How far post-refinement's step B may move the beam centre away from a value something else already +// believes. The bound is not about how large a real correction can be - it is the backstop for a fit +// corrupted by something every cross-validation fold shares (a second lattice, most often), which the +// relative "the held-out residual improved" gate cannot see. The absolute size of the move is what +// separates that from a genuine header correction. +// +// It is measured from whichever of the nominal centre and the run's own MEASUREMENT of the centre +// (PostRefineSettings::measured_beam_px) is nearer. Anchored on the nominal centre alone it caps the +// correction at exactly the header's own error, which is the opposite of its job: the header is most +// worth correcting when it is most wrong. Measured on a rotation crystal whose header centre is 22.1 px +// out - a value the same run's beam-centre check had already placed to +-0.30 px and then discarded - +// the old bound rejected a fit that improved the held-out positional residual nine-fold. +constexpr double BEAM_BOUND_PXL = 15.0; + // One integrated partial, flattened across all images. Kept as narrow as the sort and the event // split allow: on a large cell this array is gigabytes, and the scatter and every level of the // per-bucket sort move all of it. The goniometer angle is not stored - it is a function of the @@ -693,8 +707,14 @@ PostRefineResult PostRefineRotationGeometry(const std::vector::infinity(); const bool in_bounds = std::fabs(dist_fit - dist0) < 0.01 * dist0 - && std::hypot(beam_fit[0] - beam_x0, beam_fit[1] - beam_y0) < 15.0; + && std::min(from_nominal, from_measured) < BEAM_BOUND_PXL; result.detector_refined = convB && cvB_ref < 0.98 * cvB_nom && in_bounds; if (result.detector_refined) { double bo[2]; solve_detector(ALL, bo, dist); beam[0] = bo[0]; beam[1] = bo[1]; } + // Name which test refused it. Three different things reject here and the geometry that + // comes out is the same in all three, so a run that silently keeps its header geometry + // says nothing about whether the fit was bad, the improvement too small, or the move + // too large for the bound - which is the one case where the number worth reading is the + // one that was thrown away. + const char *verdict = + result.detector_refined ? "COMMIT" + : !convB ? "reject (the fit did not converge; kept nominal detector)" + : !(cvB_ref < 0.98 * cvB_nom) ? "reject (the held-out residual did not improve enough; " + "kept nominal detector)" + : std::fabs(dist_fit - dist0) >= 0.01 * dist0 + ? "reject (the distance moved more than 1 %; kept nominal detector)" + : "reject (the beam moved further than the bound from every " + "centre anything believes; kept nominal detector)"; logger.Info("Post-refine GEOM step B (distance/beam): dist {:.3f} -> {:.3f} mm, beam " "({:.2f},{:.2f}) -> ({:.2f},{:.2f}), held-out pos {:.3e} -> {:.3e} => {}", dist0, result.detector_refined ? dist : dist0, beam_x0, beam_y0, result.detector_refined ? beam[0] : beam_x0, result.detector_refined ? beam[1] : beam_y0, - cvB_nom, cvB_ref, result.detector_refined ? "COMMIT" : "reject (kept nominal detector)"); + cvB_nom, cvB_ref, verdict); } else { logger.Info("Post-refine GEOM step B: only {} positional observations - skipped", n_obs); } diff --git a/image_analysis/geom_refinement/PostRefine.h b/image_analysis/geom_refinement/PostRefine.h index 09799d8f6..ab29c6c6c 100644 --- a/image_analysis/geom_refinement/PostRefine.h +++ b/image_analysis/geom_refinement/PostRefine.h @@ -3,6 +3,8 @@ #pragma once +#include +#include #include #include "../../common/DiffractionGeometry.h" @@ -52,6 +54,11 @@ struct PostRefineSettings { double excitation_weight = 1.0; // weight of the phi/excitation residual vs the positional one int min_events = 50; int num_threads = 1; + // An independent measurement of the beam centre in pixels, where the run has one (rugnux's pre-scan + // fit to the isotropy of the scattered background, which runs on every rotation run). Step B bounds + // how far it may move the beam from whichever of this and the nominal centre is NEARER; see + // BEAM_BOUND_PXL in PostRefine.cpp for why the nominal centre alone is not enough to bound it. + std::optional> measured_beam_px; }; // nominal_geom / reference_latt: the current detector geometry and the phi=0 reference lattice (orientation diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index b51a8b1b2..2fa789964 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -1046,6 +1046,7 @@ void Rugnux::PreScan(int start_image, int images_to_process, int frame_count, Ru if (want_shadow && config_.beam_center_check) { background_center_ = FindBeamCenterFromBackground(experiment_, pixel_mask_, finder.GetMeanProjection()); + measured_beam_center_ = background_center_; const float need = BeamCenterNeed_pxl(experiment_); if (!background_center_) { logger.Info("Beam centre check: the background is too flat to place the centre on this " @@ -3512,6 +3513,16 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b prs.crystal_system = rot->search_result.system; prs.num_threads = static_cast(config_.nthreads); prs.refine_geometry = true; + // The centre the pre-scan measured, where the fit was precise enough to be used as + // a centre at all (the same ceiling --estimate-beam-center adopts one on). It does + // not move the geometry - it only tells the bound on step B's beam move where else + // the centre is independently believed to be. + if (measured_beam_center_ + && measured_beam_center_->sigma_pxl <= std::max(MAX_BEAM_CENTER_SIGMA_PXL, + BeamCenterNeed_pxl(experiment_))) + prs.measured_beam_px = std::array{ + static_cast(measured_beam_center_->beam_x_pxl), + static_cast(measured_beam_center_->beam_y_pxl)}; const auto pr = PostRefineRotationGeometry(outcomes, *rot->axis, experiment_.GetDiffractionGeometry(), rot->lattice, prs, logger); result.post_refine = pr; diff --git a/rugnux/Rugnux.h b/rugnux/Rugnux.h index 3ed88f6c7..dc15d680d 100644 --- a/rugnux/Rugnux.h +++ b/rugnux/Rugnux.h @@ -380,6 +380,10 @@ class Rugnux { // the second first pass, and clears it, so the two-pass loop does not re-ask on a post-refined // geometry the pre-scan estimate says nothing about. std::optional background_center_; + // The same pre-scan measurement, kept for the whole run instead of being consumed by the first + // pass. Post-refinement measures how far it may move the beam from the nearer of the nominal + // centre and this one, so a header that is far out does not cap the correction at its own error. + std::optional measured_beam_center_; // The recorded spot width the pre-scan measured (config_.adaptive_integration_radius), kept so the // two-pass rotation run measures it once and both passes integrate at the same radius.