rugnux: remove the beam-centre ladder (--beam-center-search)

The ladder stepped the beam centre a pixel at a time after a rotation first pass that indexed fewer
than half the validation frames. A battery with it switched off changed no output, so it goes: the
option, its usage text, ProcessConfig::beam_center_search_pxl, the once-per-run flag that kept
probes, walks and later passes from re-running it, and the docs. The background centre
measurement, the beam-centre check that indexes at the measured centre, the merge-judged arms and
the post-refinement of the centre are unchanged. rugnux now rejects --beam-center-search as an
unknown option.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
This commit is contained in:
2026-10-07 12:35:40 +02:00
co-authored by Claude Opus 5.5
parent 8c7c222e7d
commit 082f77eaa8
8 changed files with 22 additions and 244 deletions
+2 -2
View File
@@ -14,8 +14,8 @@
// out - so a mapping for the same geometry under another mask takes them from here and blanks its own
// masked pixels, instead of evaluating the geometry of every pixel again. Building a mapping is that
// evaluation, and on a 16 Mpx detector it is a tenth of a second on every core. A run alternates
// between a handful of centres - the file's, the measured one, the post-refined one, the beam-centre
// ladder's trial centres, each built once to index and again to score - so several are kept, the
// between a handful of centres - the file's, the measured one, the post-refined one, each built once
// to index and again to score - so several are kept, the
// least recently used going first (~170 MB each at 16 Mpx). A mapping being built holds the cache,
// so two built at once for the same geometry evaluate it once.
class AzimuthalIntegrationGeometryCache {
+3 -18
View File
@@ -143,7 +143,6 @@ for it. The steps, in the order they run:
| `--estimate-beam-center` | only when asked | replaces the file's before indexing, where measured precisely enough |
| Second first pass at the measured centre | rotation | adopts the measured centre in the cases listed below |
| Both centres judged on the merge | rotation, two-pass | adopts the measured centre where its first pass merges better |
| `--beam-center-search` | rotation, after a first pass that indexes under half the validation frames | steps the centre a pixel at a time |
| Post-refinement (§7.5) | rotation, two-pass | refines the centre from the integrated reflections, within 15 px |
On most runs the measured centre is reported, the second first pass finds the same lattice at both
@@ -257,9 +256,9 @@ are `Beam centre check: running the first pass again at the measured centre …`
Over the validation battery about one rotation run in ten runs this second arm, and about one in ten
ends on the measured centre by one of the routes above.
**After a first pass that failed.** A rotation sweep leaves two further things the input often cannot
settle, both decided on the same count the rest of the first pass uses — the right answer indexes and
the wrong one does not.
**After a first pass that failed.** A rotation sweep leaves one further thing the input often cannot
settle, decided on the same count the rest of the first pass uses — the right answer indexes and the
wrong one does not.
* **The rotation-axis sign.** A miniCBF header names the axis but gives no direction, and an NXmx
vector is only meaningful together with the detector mounting. So after a poor first pass the
@@ -269,19 +268,6 @@ the wrong one does not.
**axis**, not the angles, because prediction reads the axis too. It runs before the second first
pass above and before the long-axis rescue, since with the sign wrong every candidate lattice is
wrong.
* **The beam-centre search (`--beam-center-search[=N|off]`, on by default, N = 12 px).** Where the
first pass, after the second first pass above, still indexes fewer than half the validation frames,
the centre is stepped a pixel at a time out to N px along **both** detector axes, each rung with its
own spots, and the first rung that indexes a majority and beats the starting count is adopted
(`Beam centre from indexing: …`). Both directions are searched deliberately: an error *across* the
spindle collapses the indexed fraction and announces itself, while an error *along* it leaves the
transform's peaks sharp, holds nearly every frame indexed and lets the lattice fit commit to an axis
harmonic. A rung whose primitive volume is an integer or $\sqrt{3}$ multiple of the starting cell's is
refused for that reason. The search is skipped where the background measurement places the centre
further away than both N px and three σ, since no rung could reach it, and it stops after two rings
where no rung has reached a third of the majority. The step is a flat pixel: derived from the $J_0$
law it would have to use the cell the *failed* pass returned, which can be a small spurious sub-cell
and asks for a step that jumps the lobe being looked for.
**Post-refinement.** On a two-pass rotation run, whatever centre the first pass ends on is refined
together with the distance, cell, orientation and axis by the post-refinement of §7.5, and the second
@@ -350,7 +336,6 @@ why, is in the run log only.
`--beam-center-check=off` to hold a typed centre until post-refinement.
- `--beam-center-check=off` skips the background measurement and the second first pass, and removes the
measured centre from the post-refinement bound.
- `--beam-center-search=off` or `=N` turns off or resizes the search after a failed pass.
- `--estimate-beam-center` measures and commits the centre before indexing; `--no-fit-spindle` keeps the
spindle direction from the file in that estimate.
- `--detect-beam-stop=off` removes the projection the background measurement is read from.
-1
View File
@@ -284,7 +284,6 @@ Geometry:
| Option | Description |
| --- | --- |
| `--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 (against how right this geometry needs it to be), and on rotation data index a **second first pass** at the measured centre. The fit reads the projection `--detect-beam-stop` already builds, so it costs no extra frames. **On by default**; `=off` disables it, and with it the measured centre's part in the post-refinement bound. The measured centre is adopted where the file's centre indexes nothing and the measured one indexes a majority; where the two return cells related by an integer volume factor (2 to 4) and the measured centre's cell, larger or smaller, carries materially more of the pooled validation spots against its own chance level; and, on a two-pass run, where the first pass is run at both centres and the measured one merges better — done where the two give the same cell in different metric symmetries, where they agree only once less of each frame is read, and where they agree but lie further apart than the geometry absorbs and the file's centre merges inconsistently in the lowest-resolution shell. Otherwise the file's centre is kept. Still runs when `--beam-x`/`--beam-y` are given. See [§1.4](CPU_DATA_ANALYSIS_IMAGE.md) |
| `--beam-center-search[=N\|off]` | After a rotation first pass that indexes fewer than half the validation frames, step the centre a pixel at a time out to N px along **each** detector axis, re-finding the spots at every rung, and keep the first rung that indexes a majority. **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. Both detector directions are searched: a centre error *across* the spindle collapses the indexed fraction and announces itself, while one *along* it holds the frame count up and quietly returns an axis harmonic, so a rung whose cell is an integer or √3 volume multiple of the starting one is refused. Skipped where the background places the centre further away than N px |
| `--estimate-beam-center` | Measure the beam centre before indexing and use it in place of the file's: from the symmetry of the spot positions where the sweep reaches half a turn, from the scattered background otherwise. Adopted only where its sigma is within the larger of 1 px and what this geometry needs, and the move is over three sigma; otherwise the file's value is kept. Ignored with `--beam-x`/`--beam-y`. Off by default |
| `--no-fit-spindle` | With `--estimate-beam-center`, keep the rotation axis given in the file instead of fitting its skew about the beam |
+1 -1
View File
@@ -33,7 +33,7 @@ shortest accepted axis lowered from 10 to 5 Å, for small-molecule cells. The fi
second time at the beam centre the pre-scan measured; the file's centre is kept unless it indexes
nothing, gives an axis harmonic of the measured centre's lattice, or loses to it when both first passes
are merged ([§1.4](CPU_DATA_ANALYSIS_IMAGE.md)). A failed pass triggers the discrete rescues — the
rotation-axis sign, the beam-centre search — before anything is given up on.
rotation-axis sign, the first-pass depth ladder, the long-axis rescue — before anything is given up on.
**First integration pass.** At the geometry in the file, every frame of the first half of the sweep
(`--prepass-fraction`) is predicted
+3 -3
View File
@@ -162,9 +162,9 @@ IndexAndRefine::IndexingOutcome IndexAndRefine::DetermineLatticeAndSymmetry(Data
const bool index_ice_rings = experiment.GetIndexingSettings().GetIndexIceRings();
// Read per call, for the same reason: a beam-centre hypothesis moves the geometry after this
// object is built, and a spot mapped into reciprocal space through the centre the run started at
// is not the spot the trial centre sees. Held at construction, this scored every hypothesis of the
// beam-centre ladder against the file's geometry - which is why a centre scored 42/60 inside the
// ladder and 5/60 when the same value was given on the command line.
// is not the spot the trial centre sees. Held at construction, this scored every beam-centre
// hypothesis against the file's geometry - which is why a centre once scored 42/60 as a trial and
// 5/60 when the same value was given on the command line.
const DiffractionGeometry geom = experiment.GetDiffractionGeometry();
for (size_t seed_cap : {size_t{30}, size_t{80}, std::numeric_limits<size_t>::max()}) {
std::vector<Coord> recip;
+13 -188
View File
@@ -2575,21 +2575,17 @@ ProcessResult Rugnux::RunAllPasses(RugnuxObserver *observer) {
// in part (see WalkRotationScale), so it is walked to the fit's fixed point here and adopted
// only where the validation frames of the whole sweep prefer it. The walk runs at the detector
// geometry the second pass will, so every hypothesis, the stored angles included, is scored
// there, and at that geometry only: a probe whose angles lose the lattice must score what it
// scores, not start the beam-centre search that a poor first pass otherwise opens. Its passes
// there, and at that geometry only. Its passes
// only measure - an indexing probe stops once the lattice is scored, a refit stops once the
// post-refinement has measured - and they leave nothing behind: the experiment, the geometry
// the second pass is to run at, pass 1's mosaicity (a width in degrees fitted against the
// stored angles) and whether the run has searched for the beam centre are put back when the
// walk ends.
// the second pass is to run at and pass 1's mosaicity (a width in degrees fitted against the
// stored angles) are put back when the walk ends.
std::string rotation_scale_walk;
if (!cancelled_ && gonio_snapshot && pass1.post_refine && !config_.rotation_scale) {
step("Rotation-scale check");
const DiffractionExperiment before_walk = experiment_;
const auto geometry_before_walk = prepass_detector_geometry_;
const auto mosaicity_before_walk = prepass_mosaicity_;
const bool searched_before_walk = beam_center_searched_;
beam_center_searched_ = true;
// A probe pass at scale k, run on r - this run, or a copy of it (see below).
const auto probe_on = [&](Rugnux &r, RugnuxObserver *obs, float k, bool index_only) {
r.experiment_ = before_walk;
@@ -2662,7 +2658,6 @@ ProcessResult Rugnux::RunAllPasses(RugnuxObserver *observer) {
experiment_ = before_walk;
prepass_detector_geometry_ = geometry_before_walk;
prepass_mosaicity_ = mosaicity_before_walk;
beam_center_searched_ = searched_before_walk;
prepass_rotation_scale_.reset();
if (!walk.trail.empty()) {
rotation_scale_walk = fmt::format(
@@ -2941,8 +2936,6 @@ ProcessResult Rugnux::RunAllPasses(RugnuxObserver *observer) {
const auto index_at_geometry = [&](const std::array<float, 5> &g) {
const DiffractionExperiment before_probe = experiment_;
const bool probe_was = postrefine_probe_;
const bool searched_before_probe = beam_center_searched_;
beam_center_searched_ = true; // a probe scores the geometry it is given, nothing else
set_geometry(g);
CollectSpeculativeProbe();
indexing_probe_only_ = true;
@@ -2957,7 +2950,6 @@ ProcessResult Rugnux::RunAllPasses(RugnuxObserver *observer) {
}
indexing_probe_only_ = false;
postrefine_probe_ = probe_was;
beam_center_searched_ = searched_before_probe;
experiment_ = before_probe;
++arm_passes;
return r.validation_evidence;
@@ -2981,10 +2973,9 @@ ProcessResult Rugnux::RunAllPasses(RugnuxObserver *observer) {
std::optional<ValidationSpotEvidence> probe_at_best; // the indexing probe at best_geometry, once run
bool large_walk = false; // a round of this walk was started by a move of a step or more
// What the last round started from: a pass can leave the run changed behind it (a spot budget
// it shortened, an axis sign it flipped, a beam-centre search it ran), so a round is written
// it shortened, an axis sign it flipped), so a round is written
// from here, not from where its own probe left the run.
DiffractionExperiment last_round_start = experiment_;
bool last_round_searched = beam_center_searched_;
bool walk_shown = false;
while (!cancelled_ && prepass_detector_geometry_
&& pass2.post_refine && pass2.post_refine->detector_refined) {
@@ -3025,7 +3016,6 @@ ProcessResult Rugnux::RunAllPasses(RugnuxObserver *observer) {
fit.distance_after_mm, geometry_rounds + 1, MAX_GEOMETRY_ROUNDS);
set_geometry(g);
last_round_start = experiment_;
last_round_searched = beam_center_searched_;
// A round is a probe pass: the walk reads only its fit and its validation evidence, both in
// hand before the merge, so the files are written once, below, at the round the walk keeps.
speculate_geometry_probe_ = !large_walk;
@@ -3079,7 +3069,6 @@ ProcessResult Rugnux::RunAllPasses(RugnuxObserver *observer) {
logger.Info("Two-pass: writing round {} of the geometry walk (distance {:.3f} mm)",
best_round, best_geometry[2]);
experiment_ = last_round_start;
beam_center_searched_ = last_round_searched;
} else {
logger.Info("Two-pass: going back to round {} of the geometry walk (distance {:.3f} mm), "
"the last one that lowered the realised residual", best_round, best_geometry[2]);
@@ -3441,7 +3430,6 @@ void Rugnux::StartSpeculativeGeometryProbe(const std::array<float, 5> &geometry)
run->speculate_geometry_probe_ = false;
run->experiment_.BeamX_pxl(geometry[0]).BeamY_pxl(geometry[1]).DetectorDistance_mm(geometry[2])
.PoniRot1_rad(geometry[3]).PoniRot2_rad(geometry[4]);
run->beam_center_searched_ = true;
run->indexing_probe_only_ = true;
run->postrefine_probe_ = false;
run->postrefine_probe_only_ = false;
@@ -3835,7 +3823,6 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
[&](const FirstPassMemo &m) { return m.key == first_pass_key; });
if (indexing_probe_only_ && memo != first_pass_memo_.end())
memo_hit = memo->evidence;
bool beam_center_ladder_ran = false;
if (observer)
observer->OnPhase("Rotation indexing (first pass)");
@@ -4010,8 +3997,8 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
// the spot LIST does: every spot's d, the ice-ring flag, the resolution mask the finder
// applies, the high-resolution gap cut and the strongest-N budget are all radial, and the
// adaptive finder's ring background is binned about the centre. Scoring a trial centre against
// spots found at the file's put the ladder in a world the run never enters: measured, a centre
// scored 42/60 inside the ladder and 5/60 when that same value was given on the command line.
// spots found at the file's puts the trial in a world the run never enters: measured, a centre
// scored 42/60 as a trial and 5/60 when that same value was given on the command line.
//
// A trial therefore gets its own azimuthal mapping, its own engines and its own cache, and the
// starting centre's spots are PARKED rather than thrown away - a hypothesis that is not adopted
@@ -4854,8 +4841,8 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
//
// The estimate itself is free - it is read off the projection --detect-beam-stop has already
// built - and a first pass re-uses the spots it has already found, so indexing the run a
// second time at the measured centre costs about what one rung of the ladder below costs.
// That buys the comparison the on-failure ladder structurally cannot make: a centre wrong
// second time at the measured centre costs about one more first pass.
// That buys the comparison an on-failure search structurally cannot make: a centre wrong
// ALONG the spindle does not fail. It keeps 96-100 % of frames indexed and quietly returns a
// 2x, 3x or sqrt(3) axis harmonic, so no failure trigger ever fires on it. Indexing both
// centres and comparing the two ANSWERS is what can see it.
@@ -5036,10 +5023,9 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
alt.score, static_cast<int>(validation.size()));
best = alt;
} else if (!deeper_at_home) {
// Nothing worked. Put the file's centre back, so the ladder below searches
// around the value the run started with rather than around a fit that has just
// been tested and found no better - and so a run that fails for some other
// reason fails at the geometry it was given.
// Nothing worked. Put the file's centre back, so a run that fails for some
// other reason fails at the geometry it was given rather than at a fit that has
// just been tested and found no better.
//
// Unless the frame counts are both at the floor and the POOLED spots do decide:
// a crystal whose spots are a minority of every frame indexes 0/60 at any centre,
@@ -5249,166 +5235,6 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
}
}
// Beam centre as an INDEXING HYPOTHESIS. A beam-centre error is not repairable downstream: it
// is fixed in the LAB frame, so accumulating a sweep smears every reciprocal-lattice point
// around a circle and the FFT amplitude at an axis of length a is multiplied by
// J0(2 pi delta p a/(D lambda)). Past the first zero the true axis is gone and its harmonic
// wins, which is what a first-pass axis doubling usually is. It is decidable from the data on
// exactly the count the scheme choice and the axis-sign rescue already use: the right centre
// indexes and the wrong one does not.
//
// BOTH detector directions are searched, and that is the one thing to keep. The J0 law is
// about the FFT AMPLITUDE, which is translation-invariant, so a centre error along the spindle
// is free for the transform - but the step after it, fitting a lattice whose origin is the
// beam, is not, and a rigidly shifted lattice is fitted by a finer one. Measured by injection
// on two crystals: across the spindle the cell stays right and the indexed fraction collapses
// (99 % to 25 %), while along it the run keeps 96-100 % indexed and quietly adopts a 2x, 3x or
// sqrt(3) supercell. The direction that announces itself is not the dangerous one.
//
// Only after a poor pass, so a correct header costs nothing. A rung is adopted only after it
// has re-found its spots at its own centre (try_beam_center): the positions do not move with
// the centre but the list does, and a rung scored on the file centre's spot list is not a rung
// the run can adopt. That list is still good enough to say which rungs are worth re-finding.
if (!cancelled_ && !beam_center_searched_ && config_.beam_center_search_pxl > 0
&& best.score < 0.5 * static_cast<double>(validation.size())) {
beam_center_searched_ = true;
beam_center_ladder_ran = true;
const float beam_x = experiment_.GetBeamX_pxl(), beam_y = experiment_.GetBeamY_pxl();
const int reach = config_.beam_center_search_pxl;
// The background centre is measured off the whole projection rather than off a lattice, so
// where it is significant it BOUNDS where the beam can be, and a ladder that reaches
// `reach` px cannot arrive at a centre further away than that: every rung it can try is one
// the measurement has already excluded. The check above indexes the measured centre once
// and is right not to gate on significance for that - one pass is cheap. This ladder is
// 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,
measured_beam_center_->beam_y_pxl - beam_y)
> std::max(3.0f * measured_beam_center_->sigma_pxl, static_cast<float>(reach));
if (centre_past_reach)
logger.Info("Beam-centre hypothesis: the background places the centre {:.2f} px away "
"(+- {:.2f} px), past the {} px this ladder reaches - no centre it can try "
"is the beam, so it is not walked",
std::hypot(measured_beam_center_->beam_x_pxl - beam_x,
measured_beam_center_->beam_y_pxl - beam_y),
measured_beam_center_->sigma_pxl, reach);
logger.Info("Beam-centre hypothesis: {}/{} validation frames at ({:.2f},{:.2f}); trying "
"the centre +-{} px along each detector axis, a pixel at a time",
best.score, static_cast<int>(validation.size()), beam_x, beam_y, reach);
// Adopt only a hypothesis that INDEXES - the same majority the guards above test - rather
// than whichever rung scores best. Taking any improvement was tried and is wrong: on a
// dataset where no centre works the ladder wandered to the far end of its range on 10/60
// against 5/60 and reported a 30 px move, which is the count's noise floor.
if (best.result)
logger.Info("Beam-centre hypothesis: the starting lattice (vol {:.0f}) refined the centre "
"to ({:.2f},{:.2f})", best.vol, best.result->geom.GetBeamX_pxl(),
best.result->geom.GetBeamY_pxl());
const int adopt_bar = static_cast<int>(validation.size()) / 2;
bool adopted = false;
float won_x = beam_x, won_y = beam_y;
// The count is the only thing the ladder steers on, so a ladder that has not seen a THIRD
// of its own bar anywhere in two complete rings has nothing to steer by and is walking the
// noise floor. Measured over the beam-centre walks on the open battery: after two rings the
// two runs that went on to adopt stood at 17/60 and 59/60, and the three that adopted
// nothing stood at 4/60 or below - the two populations do not overlap.
int seen = best.score;
// One centre, fed, indexed and scored. HONEST: under its own mapping and its own re-found
// spots (try_beam_center) - the only evaluation a centre is adopted on. SCREEN: the starting
// centre's spots reprojected at the trial centre, with no detection at all. Re-finding the
// spots is nearly all a rung costs (the wedge scheme reads over a thousand frames), while
// the spot POSITIONS do not move with the centre; what does - each spot's d, its ice flag,
// the resolution mask, the ring background - changes which spots are on the list only at
// the margins. Measured over 225 walk rungs on seven sets, the screen gives the honest
// count to a few frames, and exactly where it matters (43/60 on both, the rung a genuine
// adoption was made on). So every rung is screened, and only a rung the screen says could
// be adopted is evaluated honestly.
struct Rung { FirstPass fp; bool harmonic = false; };
const auto evaluate = [&](float trial_x, float trial_y, bool honest) {
if (honest)
try_beam_center(trial_x, trial_y);
else
experiment_.BeamX_pxl(trial_x).BeamY_pxl(trial_y);
SchemeIndexers ris = feed_schemes(*indexer_pool);
std::vector<RotationIndexer *> ris_ptrs;
for (const auto &ri : ris)
ris_ptrs.push_back(ri.get());
run_indexing(ris_ptrs);
Rung r;
r.fp = score_schemes(ris, *indexer);
if (!honest)
experiment_.BeamX_pxl(beam_x).BeamY_pxl(beam_y);
// A rung is not adopted on the count alone. A centre error along the spindle
// translates the derotated cloud rigidly, which the FFT amplitude cannot see, so
// the indexer commits to an axis HARMONIC of the true cell - and a cell twice as
// long indexes MORE frames, not fewer, so the count rewards exactly the answer a
// centre error produces. The same test the scheme comparison already makes:
// primitive volumes differing by a small integer or by sqrt(3). Measured with the
// rungs scored honestly, a rung 1 px away indexed 56/60 on a 4x supercell of the
// lattice the starting centre had already found at 4/60.
const double ratio = best.vol > 0.0 && r.fp.vol > best.vol ? r.fp.vol / best.vol : 0.0;
const double nearest = std::round(ratio);
r.harmonic = (nearest >= 2.0 && std::abs(ratio - nearest) < 0.15)
|| std::abs(ratio - std::sqrt(3.0)) < 0.15;
logger.Info("Beam-centre hypothesis{} ({:.2f},{:.2f}): {}/{} frames, cell volume {:.0f}, "
"centre refined to ({:.2f},{:.2f}){}", honest ? "" : " screen", trial_x, trial_y,
r.fp.score, static_cast<int>(validation.size()), r.fp.vol,
r.fp.result ? r.fp.result->geom.GetBeamX_pxl() : NAN,
r.fp.result ? r.fp.result->geom.GetBeamY_pxl() : NAN,
r.harmonic ? fmt::format(" - {:.2f}x the volume of the starting centre's cell, "
"an axis harmonic, not adopted", ratio) : "");
return r;
};
const auto adoptable = [&](const Rung &r) {
return !r.harmonic && r.fp.result.has_value() && r.fp.score > adopt_bar
&& r.fp.score > best.score;
};
for (int k = 1; k <= reach && !cancelled_ && !adopted && !centre_past_reach; k++) {
const std::array<std::pair<int, int>, 4> ring = {{{-k, 0}, {k, 0}, {0, -k}, {0, k}}};
for (size_t h = 0; h < ring.size() && !cancelled_ && !adopted; h++) {
const float trial_x = beam_x + static_cast<float>(ring[h].first);
const float trial_y = beam_y + static_cast<float>(ring[h].second);
const Rung screen = evaluate(trial_x, trial_y, false);
seen = std::max(seen, screen.fp.score);
if (!adoptable(screen))
continue;
const Rung r = evaluate(trial_x, trial_y, true);
if (adoptable(r)) {
best = r.fp;
won_x = trial_x;
won_y = trial_y;
adopted = true;
} else {
// Back to the starting centre's spots, which the next screen reprojects.
restore_beam_center(beam_x, beam_y);
}
}
if (k == 2 && !adopted && seen < adopt_bar / 3) {
logger.Info("Beam-centre hypothesis: the best of two rings is {}/{} against the "
"{}/{} a centre has to index to be adopted - the ladder is at the "
"count's noise floor and is stopped here", seen,
static_cast<int>(validation.size()), adopt_bar + 1,
static_cast<int>(validation.size()));
break;
}
}
if (!adopted)
restore_beam_center(won_x, won_y);
if (adopted) {
logger.Info("Beam centre from indexing: ({:.2f},{:.2f}) -> ({:.2f},{:.2f}), moved "
"{:.2f} px, {}/{} validation frames", beam_x, beam_y, won_x, won_y,
std::hypot(won_x - beam_x, won_y - beam_y), best.score,
static_cast<int>(validation.size()));
Note(fmt::format("beam centre searched: moved {:.1f} px, it indexes there",
std::hypot(won_x - beam_x, won_y - beam_y)));
}
}
// Long-axis rescue. When the de-novo cell indexes few validation frames, a long, finely-spaced
// axis was likely lost: the unconstrained FFT either collapsed it to a short sub-multiple or let
// a denser supercell over-fit the accumulated cloud (a small global-orientation error throws the
@@ -5799,9 +5625,8 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
throw MemoMismatch(fmt::format("First-pass memo mismatch: stored {}/{}/{}, recomputed {}/{}/{}",
memo_hit->on_lattice, memo_hit->spots, memo_hit->by_chance,
evidence.on_lattice, evidence.spots, evidence.by_chance));
// Kept only where the pass ended on the inputs it started from, and did not take the
// beam-centre ladder, which a probe (beam_center_searched_) never would.
if (!geometry_prepass && !beam_center_ladder_ran && FirstPassInputKey() == first_pass_key)
// Kept only where the pass ended on the inputs it started from.
if (!geometry_prepass && FirstPassInputKey() == first_pass_key)
AddFirstPassMemo(FirstPassMemo{first_pass_key, evidence});
// A pass run only to score a lattice has its score, whatever it is: a lattice that does not
// beat chance is a result for the comparison that asked, not a reason to stop the run.
-11
View File
@@ -123,12 +123,6 @@ struct ProcessConfig {
bool estimate_beam_center = false;
bool fit_spindle = false;
// Beam centre as an INDEXING HYPOTHESIS (--beam-center-search). Runs only after a first pass that
// indexes under half the validation frames, so it costs nothing on a run that works, and it is a
// repair path rather than a measurement: it steps the centre a pixel at a time and keeps the first
// 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:
@@ -685,11 +679,6 @@ class Rugnux {
// a stills bundle adjustment, or an earlier pre-scan of this same run. The pre-scan estimate is
// a starting point for indexing, so it must not overwrite either of those.
bool beam_center_placed_ = false;
// Whether the beam-centre hypothesis ladder has already run in this RUN, not in this pass: the
// second pass starts from the geometry the first one post-refined, so searching again there
// 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. RunPipeline consumes it for
// the second first pass, and clears it, so the two-pass loop does not re-ask on a post-refined
-20
View File
@@ -124,7 +124,6 @@ void print_usage() {
std::cout << " Detector mask" << std::endl;
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. On a run that indexes, the measured centre is adopted where the file's centre indexes nothing and the measured one indexes a majority, and where the two centres return cells related by an integer volume factor, in either direction, and the measured centre's lattice carries materially more of the pooled spots against its own chance level - the axis harmonic a centre error along the spindle produces, which nothing downstream repairs. Where the indexing cannot tell the two centres apart - the same cell with a different metric symmetry, or the same lattice found at both only by reading less of each frame - the first pass is run at both and the run keeps the one that merges better (on the same lattice: the one that measures more reflections at I/sigma >= 2). 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;
@@ -265,7 +264,6 @@ enum {
OPT_BACKGROUND_RADIAL,
OPT_REFINE_GEOMETRY,
OPT_DETECT_BEAM_STOP,
OPT_BEAM_CENTER_SEARCH,
OPT_BEAM_CENTER_CHECK,
OPT_ESTIMATE_BEAM_CENTER,
OPT_FIT_SPINDLE,
@@ -395,7 +393,6 @@ static option long_options[] = {
{"rotation-scale", required_argument, nullptr, OPT_ROTATION_SCALE},
{"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},
@@ -933,7 +930,6 @@ static int RunRugnux(int argc, char **argv) {
std::optional<bool> background_radial_arg; // set = force on/off, unset = auto (the default)
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
@@ -1041,21 +1037,6 @@ static int RunRugnux(int argc, char **argv) {
? parse_number_arg<int>(optarg, "--detect-beam-stop", logger, 1, 1000000)
: 60;
break;
case OPT_BEAM_CENTER_SEARCH:
// How far the ladder reaches, in pixels, in each direction. The step is one pixel, flat:
// a header beam centre is written in pixels and is wrong by pixels. Deriving the step from
// the J0 law was tried and is wrong, because the only cell available at that point is the
// one the FAILED pass returned - on a dataset whose failed cell was a small spurious
// sub-cell the formula asked for a 6 px step, which steps clean over the lobe it is
// looking for.
if (optarg && std::string(optarg) == "off") {
beam_center_search = 0;
break;
}
beam_center_search = optarg
? 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;
@@ -3004,7 +2985,6 @@ static int RunRugnux(int argc, char **argv) {
config.two_pass_rotation = two_pass_rotation;
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;