rugnux: say where the scattered background puts the beam, on every run

A beam centre in a file is the metadata field everyone jokes about and nobody measures. rugnux
already has an estimator that can measure it - the isotropy of the scattered background, whose
leverage is the curvature of the water ring - and it already builds, on every run, the projection
that estimator needs: --detect-beam-stop makes one to find the shadow. So the measurement is a fit
over a mean image that already exists, no frames of its own, and it can simply be done always.

--beam-center-check, on by default, does that and reports it: what the file claims, what the
background says, how far apart the two are, how well the fit knows its own answer, and how right
this particular geometry needs the centre to be. That last term is what makes the line a verdict
rather than a pixel count - 3 px is nothing at 100 mm and a lost lattice at 500 mm. On the two
public depositions in the test corpus whose header is the geometric centre of the detector, the line
reads 73.49 px and 70.96 px against a 1.57 px and 1.82 px tolerance, on runs that until now said
nothing at all about it.

Nothing is committed. The run keeps the centre it was given, and where the fit cannot place the
centre - a flat background has nothing to separate a radial shift from an amplitude - it says so
rather than guessing. Measured on a 0.3 Mpx detector the fit takes 8 ms.

The fall-through in --estimate-beam-center now reads this same result instead of repeating the fit;
it is the same call on the same projection with the same mask, so that flag's answer is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3yNBXk4wKdMZy1ak2NY7f
This commit is contained in:
2026-08-30 08:53:05 +02:00
co-authored by Claude Opus 5
parent a2f451cf12
commit 0644404286
3 changed files with 83 additions and 1 deletions
+63 -1
View File
@@ -851,6 +851,64 @@ void Rugnux::PreScan(int start_image, int images_to_process, int frame_count, Ru
// The beam centre comes after, so that the shadow is in the mask by the time it is placed and
// the holder arm - the largest azimuthal asymmetry on the detector - is out of the way of the
// background the centre is read from.
// What the scattered background makes the centre, measured on EVERY run and committed on none.
// The fit reads the projection the beam-stop pre-pass has just built and no frame of its own, so
// it costs a fraction of a second on top of a pass that has already run - which is what makes it
// affordable to do always, and doing it always is the point. A header beam centre is the metadata
// field the field jokes about, and nothing in any package says how wrong a given one is: on a run
// that indexes and merges perfectly well this line still reports that the file claims a centre the
// data put 73 px away. The run is not touched by it. What consumes the number is the second first
// pass in RunPipeline, and --estimate-beam-center's fall-through below, which is the same fit.
//
// Only where the projection exists: with --detect-beam-stop=off nothing accumulates it, and
// projecting every frame for this alone is the largest per-frame cost of the pre-scan. On the
// second pass of a two-pass run the shadow is already in the mask, so this does not run again -
// by then the centre has been post-refined and the file's value is no longer what is in it.
if (want_shadow && config_.beam_center_check) {
background_center_ = FindBeamCenterFromBackground(experiment_, pixel_mask_,
finder.GetMeanProjection());
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 "
"run - the file's ({:.2f},{:.2f}) is neither confirmed nor contradicted",
experiment_.GetBeamX_pxl(), experiment_.GetBeamY_pxl());
} else {
const float dx = background_center_->beam_x_pxl - experiment_.GetBeamX_pxl();
const float dy = background_center_->beam_y_pxl - experiment_.GetBeamY_pxl();
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 says ({:.2f},{:.2f}), the scattered background "
"says ({:.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",
experiment_.GetBeamX_pxl(), experiment_.GetBeamY_pxl(),
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");
else if (across && *across > need)
logger.Warning("Beam centre check: {:.2f} px of the difference is ACROSS the spindle, "
"more than the {:.2f} px this geometry absorbs. A centre error is fixed "
"in the lab frame, so it smears the accumulated reciprocal-space cloud "
"and the first pass loses peaks or takes an axis harmonic - expect the "
"cell below to be wrong", *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");
}
}
if (!want_beam_center)
return;
beam_center_placed_ = true;
@@ -966,7 +1024,11 @@ void Rugnux::PreScan(int start_image, int images_to_process, int frame_count, Ru
logger.Info("Beam centre: the spot symmetry does not come out on this sweep - falling "
"through to the background");
source = "background";
estimate = FindBeamCenterFromBackground(experiment_, pixel_mask_, finder.GetMeanProjection());
// The same fit the check above has already done wherever the projection exists - same mask,
// same geometry, same projection - so this asks for it again only where it was not run.
estimate = background_center_
? background_center_
: FindBeamCenterFromBackground(experiment_, pixel_mask_, finder.GetMeanProjection());
}
if (!estimate) {
logger.Info("Beam centre: not measurable, keeping ({:.2f},{:.2f})",
+10
View File
@@ -21,6 +21,7 @@
#include "ModelValidation.h" // ModelValidationResult
#include "../image_analysis/scale_merge/SearchSpaceGroup.h" // SearchSpaceGroupResult
#include "../image_analysis/geom_refinement/PostRefine.h" // PostRefineResult
#include "../image_analysis/geom_refinement/BeamCenterFromBackground.h" // BeamCenterEstimate
#include "../image_analysis/rotation_indexer/RotationIndexer.h" // RotationIndexerResult
#include "RugnuxCalibration.h" // CalibrationMethod, CalibrationResult
@@ -107,6 +108,12 @@ struct ProcessConfig {
// rung that indexes a majority. The value is how many pixels it reaches in each direction; 0 is off.
int beam_center_search_pxl = 12;
// Beam centre from the scattered background, measured on EVERY run and reported (--beam-center-check,
// on by default). The fit reads the projection --detect-beam-stop has already built and no frame of
// its own, so it is close to free, and its result is worth having on a run that comes out right:
// it is what lets a job reprocessing a database say a deposited header is 73 px out.
bool beam_center_check = true;
// Adaptive integration radius (--adaptive-integration-radius). When set, the pre-scan measures how
// wide the recorded spots are - r80, the radius holding 80 % of a spot's flux, over a fixed
// aperture that owes nothing to the integrator (SpotWidth.h) - and sets r1 from it. r1 is the
@@ -330,6 +337,9 @@ class Rugnux {
// re-asks a question already answered, and where the answer was "no centre works" it doubles the
// cost of the failure.
bool beam_center_searched_ = false;
// What the scattered background makes the beam centre, measured in the pre-scan (--beam-center-check)
// and never committed there: the run keeps the centre it was given.
std::optional<BeamCenterEstimate> background_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.
+10
View File
@@ -106,6 +106,7 @@ void print_usage() {
std::cout << " --detect-beam-stop[=N|off] Find the beam stop and its holder in a projection of N images and add them to the pixel mask (bit 9), so nothing shadowed by them is integrated. ON by default (60 images); =off disables. Reflections behind the stop are attenuated but not flagged, so they are integrated low with a plausible sigma and no existing rejection catches them" << std::endl;
std::cout << " --estimate-beam-center Place the beam centre before anything is indexed, and use it in place of the header value when it is measured precisely enough. On a sweep that reaches half a turn it comes from the symmetry of the spot positions - the frames 180 deg apart are each other's mirror image, and every reflection is recorded twice - and where the sweep does not reach that far, from the isotropy of the scattered background, which needs only a few frames. It reads frames of its own, chosen as pairs half a turn apart, so it does not change the mask --detect-beam-stop finds. Ignored when --beam-x/--beam-y are given, and when a stills geometry refinement has already placed the centre from indexed spots" << std::endl;
std::cout << " --beam-center-search[=N|off] After a first pass that indexes fewer than half the validation frames, try the beam centre a pixel at a time out to N px along each detector axis and keep the first one that indexes a majority. A beam-centre error is fixed in the lab frame, so it smears the accumulated reciprocal-space cloud and the FFT takes an axis harmonic instead of the true axis; nothing downstream repairs that. ON by default (12 px); =off disables. It runs only after a pass that has already failed, so a run that indexes never pays for it" << std::endl;
std::cout << " --beam-center-check[=off] Measure the beam centre from the isotropy of the scattered background on EVERY run, report how far the file's value is from it, and index a second first pass with it to see whether the two centres give the same lattice. The fit reads the projection --detect-beam-stop already builds, so it costs no extra frames. Nothing is committed on a run that indexes: the measured centre is adopted only where the file's indexes nothing and the measured one indexes a majority. ON by default; =off disables" << std::endl;
std::cout << " --no-fit-spindle Take the goniometer axis from the file rather than measuring the spindle's rotation about the beam from the spots. Measuring it is the DEFAULT and the file is never right: every master writes an exact lab axis and no goniometer is one. Both mirror lines of the beam-centre estimator turn with the spindle, so an axis a few tenths of a milliradian out smears the vote until a neighbouring tooth wins. Only has an effect with --estimate-beam-center" << std::endl;
std::cout << std::endl;
@@ -237,6 +238,7 @@ enum {
OPT_REFINE_GEOMETRY,
OPT_DETECT_BEAM_STOP,
OPT_BEAM_CENTER_SEARCH,
OPT_BEAM_CENTER_CHECK,
OPT_ESTIMATE_BEAM_CENTER,
OPT_FIT_SPINDLE,
OPT_NO_FIT_SPINDLE,
@@ -354,6 +356,7 @@ static option long_options[] = {
{"refine-geometry", optional_argument, nullptr, OPT_REFINE_GEOMETRY},
{"detect-beam-stop", optional_argument, nullptr, OPT_DETECT_BEAM_STOP},
{"beam-center-search", optional_argument, nullptr, OPT_BEAM_CENTER_SEARCH},
{"beam-center-check", optional_argument, nullptr, OPT_BEAM_CENTER_CHECK},
{"estimate-beam-center", no_argument, nullptr, OPT_ESTIMATE_BEAM_CENTER},
{"fit-spindle", no_argument, nullptr, OPT_FIT_SPINDLE},
{"no-fit-spindle", no_argument, nullptr, OPT_NO_FIT_SPINDLE},
@@ -716,6 +719,7 @@ static int RunRugnux(int argc, char **argv) {
std::optional<int> detect_beam_stop = 60; // --detect-beam-stop[=N|off]; on by default
bool estimate_beam_center = false; // --estimate-beam-center
int beam_center_search = 12; // --beam-center-search[=N|off]; on by default
bool beam_center_check = true; // --beam-center-check[=off]; on by default
bool fit_spindle = true; // --fit-spindle / --no-fit-spindle
std::optional<int> refine_geometry; // --refine-geometry[=N]: stills global geometry-refinement pass
bool refine_geometry_disabled = false; // --refine-geometry=off: opt out of the stills default-on
@@ -833,6 +837,9 @@ static int RunRugnux(int argc, char **argv) {
? parse_number_arg<int>(optarg, "--beam-center-search", logger, 1, 1000)
: 12;
break;
case OPT_BEAM_CENTER_CHECK:
beam_center_check = !optarg || std::string(optarg) != "off";
break;
case OPT_ESTIMATE_BEAM_CENTER:
estimate_beam_center = true;
break;
@@ -1921,6 +1928,7 @@ static int RunRugnux(int argc, char **argv) {
config.output_prefix = output_prefix;
config.detect_beam_stop = detect_beam_stop;
config.estimate_beam_center = estimate_beam_center;
config.beam_center_check = beam_center_check;
config.fit_spindle = fit_spindle;
Rugnux process(reader, experiment, *dataset->pixel_mask, config);
@@ -1966,6 +1974,7 @@ static int RunRugnux(int argc, char **argv) {
config.output_prefix = output_prefix;
config.detect_beam_stop = detect_beam_stop;
config.estimate_beam_center = estimate_beam_center;
config.beam_center_check = beam_center_check;
config.fit_spindle = fit_spindle;
config.write_process_h5 = false; // the .poni below is the output of this mode
@@ -2391,6 +2400,7 @@ static int RunRugnux(int argc, char **argv) {
config.detect_beam_stop = detect_beam_stop;
config.estimate_beam_center = estimate_beam_center;
config.beam_center_search_pxl = beam_center_search;
config.beam_center_check = beam_center_check;
config.fit_spindle = fit_spindle;
config.adaptive_integration_radius = adaptive_integration_radius;
config.rotation_postrefine_geometry = rotation_postrefine_geometry;