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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1G8gJVAy6gp1K5Dz3NE5C
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-09-26 22:58:42 +02:00
1 parent 7e1be959a6
commit 5b2b642e73
2 files changed
+105 -81

No files matched your search

+101 -81
View File
@@ -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::milliseconds>(
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<float>::quiet_NaN()
: capture.line_x[0].beam_x_pxl,
capture.line_y.empty() ? std::numeric_limits<float>::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<BeamCenterEstimate> background_center =
FindBeamCenter(x, mask, projection, nthreads, &capture);
const auto capture_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
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<float>::quiet_NaN()
: capture.line_x[0].beam_x_pxl,
capture.line_y.empty() ? std::numeric_limits<float>::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<float, 5> &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<double> Rugnux::FirstPassInputKey() const {
// Run state the first pass reads besides the experiment.
k.insert(k.end(), {static_cast<double>(lattice_angles_demoted_), prepass_rotation_scale_.value_or(-1.0f),
static_cast<double>(force_rotation_result_.has_value()),
static_cast<double>(background_center_.has_value()),
// a capture still running will have run by the time the pass reads it
static_cast<double>(background_center_.has_value() || background_center_capture_.valid()),
static_cast<double>(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_)))
+4
View File
@@ -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<BeamCenterEstimate> 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<std::optional<BeamCenterEstimate>> 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