From 5b2b642e73cdc04d0c1d0a02fdfc941bed817e85 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sat, 26 Sep 2026 22:58:42 +0200 Subject: [PATCH] Rugnux: run the pre-scan's beam-centre capture beside the first pass The capture (about 1.5 s on a 16M detector) sat at the end of the pre-scan, but nothing reads its answer before the first pass's beam-centre check. It now runs on copies of the experiment, mask and projection it measured, and is taken (JoinBeamCenterCapture) wherever background_center_ or measured_beam_center_ is read. The first-pass memo key counts a capture still running as a centre present, which it will be by the time the pass reads it. Its log lines now come from its own thread, among the first pass's. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01D1G8gJVAy6gp1K5Dz3NE5C --- rugnux/Rugnux.cpp | 182 +++++++++++++++++++++++++--------------------- rugnux/Rugnux.h | 4 + 2 files changed, 105 insertions(+), 81 deletions(-) diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 93e1f9197..616f7ab1b 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -1383,91 +1383,99 @@ void Rugnux::PreScan(int start_image, int images_to_process, int frame_count, Ru if (want_hot) MaskDefectivePixels(start_image, shadow_sample, ring_centre); - if (want_shadow && config_.beam_center_check) { - const auto t0 = std::chrono::steady_clock::now(); - BeamCenterFFTResult capture; - background_center_ = FindBeamCenter(experiment_, pixel_mask_, finder.GetMeanProjection(), - config_.nthreads, &capture); - measured_beam_center_ = background_center_; - const auto capture_ms = std::chrono::duration_cast( - std::chrono::steady_clock::now() - t0).count(); - if (!capture.point.empty()) - logger.Info("Beam centre capture: the strongest point-symmetry centre on the WHOLE " - "detector is ({:.1f},{:.1f}), {:.1f} px from the file's; line mirrors put x " - "at {:.1f} and y at {:.1f}; margins {:.1f}%/{:.1f}%/{:.1f}% (point/x/y, " - "under ~3% the surface is flat). One transform set scores every candidate " - "centre, so this costs the same however wrong the file is - {} ms including " - "the walk that refines it", - capture.point[0].beam_x_pxl, capture.point[0].beam_y_pxl, - std::hypot(capture.point[0].beam_x_pxl - experiment_.GetBeamX_pxl(), - capture.point[0].beam_y_pxl - experiment_.GetBeamY_pxl()), - capture.line_x.empty() ? std::numeric_limits::quiet_NaN() - : capture.line_x[0].beam_x_pxl, - capture.line_y.empty() ? std::numeric_limits::quiet_NaN() - : capture.line_y[0].beam_y_pxl, - 100.0f * capture.margin_point, 100.0f * capture.margin_line_x, - 100.0f * capture.margin_line_y, capture_ms); - const float need = BeamCenterNeed_pxl(experiment_); - if (!background_center_) { - logger.Info("Beam centre check: neither the capture nor the walk places the centre on " - "this run - the file's ({:.2f},{:.2f}) is neither confirmed nor contradicted", - experiment_.GetBeamX_pxl(), experiment_.GetBeamY_pxl()); - } else { - // Against the DIRECT BEAM, not the PONI. Both the FFT capture and the walk read the - // centrosymmetry of the scattered background about the point the beam lands on; the - // geometry's beam_x_pxl is the PONI, which parts from it by distance*tan(rot)/pixel as - // soon as the detector is tilted. Measured on a calibrant at 110/150/200/300 mm the - // capture sits 0.5-1.5 px from the fitted direct beam and 5.0/6.8/8.4/17.9 px from the - // fitted PONI, so comparing it with the PONI reports a disagreement the data do not - // have - on the in-house files, whose header carries a 0.234 deg tilt, 3.3 px of it at - // 120 mm against a stated need of 0.7 px. - const auto [direct_x, direct_y] = experiment_.GetDiffractionGeometry().GetDirectBeam_pxl(); - const float dx = background_center_->beam_x_pxl - direct_x; - const float dy = background_center_->beam_y_pxl - direct_y; - const float moved = std::hypot(dx, dy); - const auto across = AcrossSpindle_pxl(experiment_, dx, dy); - // Everything a reader needs to judge the header, on one line: what the file says, what the - // background says, how far apart they are, how well the background knows its own answer, - // and what this particular geometry asks of the centre. The last is what turns a bare pixel - // count into a verdict - 3 px is nothing at 100 mm and a lost lattice at 500 mm. - logger.Info("Beam centre check: the file puts the direct beam at ({:.2f},{:.2f}), the " - "scattered background " - "puts it at ({:.2f},{:.2f}) +- {:.2f} px - a difference of {:.2f} px, against the " - "{:.2f} px this geometry asks the centre to be right to. Measured only; the " - "run keeps the centre in the file", - direct_x, direct_y, - background_center_->beam_x_pxl, background_center_->beam_y_pxl, - background_center_->sigma_pxl, moved, need); - // Three sigma, because the fit is a PRECISION and not an accuracy: on a background with no - // curvature there is nothing to separate shift from amplitude and it follows the noise in - // g', confidently. A move it cannot resolve is not evidence about the header either way. - if (moved <= 3.0f * background_center_->sigma_pxl) - logger.Info("Beam centre check: that is under three times the fit's own sigma, so the " - "file's centre is as good as this measurement can tell - the second first " - "pass below indexes it anyway, because a fraction of a pixel is enough to " - "decide a cell and no sigma small enough to gate on would notice"); - else if (across && *across > need) - // Info and not a warning: `need` is where the FFT amplitude at a 200 A axis has fallen - // to 0.70, which is a floor under what matters and not a threshold for failure - it is - // exceeded on two of three ordinary in-house sweeps whose lattice comes out right. What - // is worth a warning is what the second pass below then finds, not this. - logger.Info("Beam centre check: {:.2f} px of the difference is ACROSS the spindle, more " - "than the {:.2f} px this geometry absorbs, and a centre error is fixed in " - "the lab frame - it smears the accumulated reciprocal-space cloud, so the " - "first pass can lose peaks or take an axis harmonic. The second first pass " - "below indexes both centres and reports whether it did", *across, need); - else - logger.Info("Beam centre check: that is a real difference, and {} - the second first " - "pass below indexes both centres and compares what they give", - across ? "it is mostly along the spindle, where nothing announces itself" - : "this is not a rotation sweep, so there is no spindle to resolve it on"); - } - } + // It reads nothing the steps after it change, and nothing reads its answer before the first pass's + // beam-centre check (RunPipeline), so it runs beside them - on copies of what it measures - and is + // taken there (JoinBeamCenterCapture). It logs from its own thread. + if (want_shadow && config_.beam_center_check) + background_center_capture_ = std::async(std::launch::async, [x = experiment_, mask = pixel_mask_, + projection = finder.GetMeanProjection(), + nthreads = config_.nthreads] { + Logger logger("Rugnux"); + const auto t0 = std::chrono::steady_clock::now(); + BeamCenterFFTResult capture; + const std::optional background_center = + FindBeamCenter(x, mask, projection, nthreads, &capture); + const auto capture_ms = std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0).count(); + if (!capture.point.empty()) + logger.Info("Beam centre capture: the strongest point-symmetry centre on the WHOLE " + "detector is ({:.1f},{:.1f}), {:.1f} px from the file's; line mirrors put x " + "at {:.1f} and y at {:.1f}; margins {:.1f}%/{:.1f}%/{:.1f}% (point/x/y, " + "under ~3% the surface is flat). One transform set scores every candidate " + "centre, so this costs the same however wrong the file is - {} ms including " + "the walk that refines it", + capture.point[0].beam_x_pxl, capture.point[0].beam_y_pxl, + std::hypot(capture.point[0].beam_x_pxl - x.GetBeamX_pxl(), + capture.point[0].beam_y_pxl - x.GetBeamY_pxl()), + capture.line_x.empty() ? std::numeric_limits::quiet_NaN() + : capture.line_x[0].beam_x_pxl, + capture.line_y.empty() ? std::numeric_limits::quiet_NaN() + : capture.line_y[0].beam_y_pxl, + 100.0f * capture.margin_point, 100.0f * capture.margin_line_x, + 100.0f * capture.margin_line_y, capture_ms); + const float need = BeamCenterNeed_pxl(x); + if (!background_center) { + logger.Info("Beam centre check: neither the capture nor the walk places the centre on " + "this run - the file's ({:.2f},{:.2f}) is neither confirmed nor contradicted", + x.GetBeamX_pxl(), x.GetBeamY_pxl()); + } else { + // Against the DIRECT BEAM, not the PONI. Both the FFT capture and the walk read the + // centrosymmetry of the scattered background about the point the beam lands on; the + // geometry's beam_x_pxl is the PONI, which parts from it by distance*tan(rot)/pixel as + // soon as the detector is tilted. Measured on a calibrant at 110/150/200/300 mm the + // capture sits 0.5-1.5 px from the fitted direct beam and 5.0/6.8/8.4/17.9 px from the + // fitted PONI, so comparing it with the PONI reports a disagreement the data do not + // have - on the in-house files, whose header carries a 0.234 deg tilt, 3.3 px of it at + // 120 mm against a stated need of 0.7 px. + const auto [direct_x, direct_y] = x.GetDiffractionGeometry().GetDirectBeam_pxl(); + const float dx = background_center->beam_x_pxl - direct_x; + const float dy = background_center->beam_y_pxl - direct_y; + const float moved = std::hypot(dx, dy); + const auto across = AcrossSpindle_pxl(x, dx, dy); + // Everything a reader needs to judge the header, on one line: what the file says, what the + // background says, how far apart they are, how well the background knows its own answer, + // and what this particular geometry asks of the centre. The last is what turns a bare pixel + // count into a verdict - 3 px is nothing at 100 mm and a lost lattice at 500 mm. + logger.Info("Beam centre check: the file puts the direct beam at ({:.2f},{:.2f}), the " + "scattered background " + "puts it at ({:.2f},{:.2f}) +- {:.2f} px - a difference of {:.2f} px, against the " + "{:.2f} px this geometry asks the centre to be right to. Measured only; the " + "run keeps the centre in the file", + direct_x, direct_y, + background_center->beam_x_pxl, background_center->beam_y_pxl, + background_center->sigma_pxl, moved, need); + // Three sigma, because the fit is a PRECISION and not an accuracy: on a background with no + // curvature there is nothing to separate shift from amplitude and it follows the noise in + // g', confidently. A move it cannot resolve is not evidence about the header either way. + if (moved <= 3.0f * background_center->sigma_pxl) + logger.Info("Beam centre check: that is under three times the fit's own sigma, so the " + "file's centre is as good as this measurement can tell - the second first " + "pass below indexes it anyway, because a fraction of a pixel is enough to " + "decide a cell and no sigma small enough to gate on would notice"); + else if (across && *across > need) + // Info and not a warning: `need` is where the FFT amplitude at a 200 A axis has fallen + // to 0.70, which is a floor under what matters and not a threshold for failure - it is + // exceeded on two of three ordinary in-house sweeps whose lattice comes out right. What + // is worth a warning is what the second pass below then finds, not this. + logger.Info("Beam centre check: {:.2f} px of the difference is ACROSS the spindle, more " + "than the {:.2f} px this geometry absorbs, and a centre error is fixed in " + "the lab frame - it smears the accumulated reciprocal-space cloud, so the " + "first pass can lose peaks or take an axis harmonic. The second first pass " + "below indexes both centres and reports whether it did", *across, need); + else + logger.Info("Beam centre check: that is a real difference, and {} - the second first " + "pass below indexes both centres and compares what they give", + across ? "it is mostly along the spindle, where nothing announces itself" + : "this is not a rotation sweep, so there is no spindle to resolve it on"); + } + return background_center; + }).share(); finish_spot_measurement(); if (!want_beam_center) return; beam_center_placed_ = true; + JoinBeamCenterCapture(); // The spot symmetry first where the sweep supports it: it is exact geometry rather than a // property of the scattering, and it measures the sets the background cannot - a flat radial @@ -3111,6 +3119,14 @@ void Rugnux::StartSpeculativeGeometryProbe(const std::array &geometry) speculative_probe_ = std::move(sp); } +void Rugnux::JoinBeamCenterCapture() { + if (!background_center_capture_.valid()) + return; + background_center_ = background_center_capture_.get(); + measured_beam_center_ = background_center_; + background_center_capture_ = {}; +} + void Rugnux::CollectSpeculativeProbe() { if (!speculative_probe_) return; @@ -3178,7 +3194,8 @@ std::vector Rugnux::FirstPassInputKey() const { // Run state the first pass reads besides the experiment. k.insert(k.end(), {static_cast(lattice_angles_demoted_), prepass_rotation_scale_.value_or(-1.0f), static_cast(force_rotation_result_.has_value()), - static_cast(background_center_.has_value()), + // a capture still running will have run by the time the pass reads it + static_cast(background_center_.has_value() || background_center_capture_.valid()), static_cast(beam_center_alternative_state_.has_value())}); return k; } @@ -4476,6 +4493,7 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b // each error hides the other, both runs sit at 0/60 until both are moved, and moving both // takes them to 60/60 on the deposited lattice. Asking the pair costs one more first pass on // a run that has already failed twice. + JoinBeamCenterCapture(); if (!cancelled_ && config_.beam_center_check && background_center_) { // Consumed, not re-read: a two-pass rotation run comes back through here with the geometry // the first pass post-refined, and a pre-scan estimate of the FILE's centre says nothing @@ -4861,6 +4879,7 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b // 4*reach passes, so the same evidence is worth acting on here. Measured on a deposition // whose header centre lands on a masked pixel: 170 px away at 0.34 px sigma, 48 rungs, and // nothing adopted. + JoinBeamCenterCapture(); const bool centre_past_reach = measured_beam_center_.has_value() && std::hypot(measured_beam_center_->beam_x_pxl - beam_x, @@ -6447,6 +6466,7 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b // 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. + JoinBeamCenterCapture(); if (measured_beam_center_ && measured_beam_center_->sigma_pxl <= std::max(MAX_BEAM_CENTER_SIGMA_PXL, BeamCenterNeed_pxl(experiment_))) diff --git a/rugnux/Rugnux.h b/rugnux/Rugnux.h index 518d9624b..605e84491 100644 --- a/rugnux/Rugnux.h +++ b/rugnux/Rugnux.h @@ -645,6 +645,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 pre-scan's capture of it while it is still running beside what follows; JoinBeamCenterCapture + // waits for it and sets background_center_ and measured_beam_center_ from it, wherever they are read. + std::shared_future> background_center_capture_; + void JoinBeamCenterCapture(); // The centre the background measured, kept when the two centres index the SAME cell but a // different metric symmetry off it and the disagreement is by itself large enough to have decided // that symmetry (RunPipeline's beam-centre check). Set by the two-pass first pass and consumed by