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:
1 parent
7e1be959a6
commit
5b2b642e73
2 files changed
+105
-81
No files matched your search
+101
-81
@@ -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_)))
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user