diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 641e8a9c6..75e6275de 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -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})", diff --git a/rugnux/Rugnux.h b/rugnux/Rugnux.h index e9577b311..7d1b25771 100644 --- a/rugnux/Rugnux.h +++ b/rugnux/Rugnux.h @@ -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 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. diff --git a/rugnux/rugnux_cli.cpp b/rugnux/rugnux_cli.cpp index fbfcf6408..b7072f581 100644 --- a/rugnux/rugnux_cli.cpp +++ b/rugnux/rugnux_cli.cpp @@ -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 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 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(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;