Pure refactor, no behaviour change. The 2460-line function with ~25 lambdas
becomes Rugnux::FirstPassRun (RugnuxFirstPass.h): the state the steps share
(spot cache, validation frames, schemes, committed spot-list state, the
speculative passes) as members, and one member function per step taking
and returning the standing lattice (FirstPass). FirstPassRotationIndexing is
now the ordered list of decisions:
PrefetchCertainFrames, IndexSchemes (+ centre/short-axis speculation),
AxisSignRescue, BeamCentreCheck (IndexAtMeasuredCentre,
CentreWhenFileFails, CentreByPooledSpots, CentreWhenBothIndex,
CentresDisagree), LongAxisRescue, AddEndWedge, ReleaseSpotEngines,
RequireLattice, ShortAxisHypothesis, SubLatticeHypothesis, Pass1Fallback,
last-resort FirstPassLadder, MeasureVerdict, RejectChanceLattice,
AdoptLattice, StartLeftoverCensus, ReportTiltWalk, MeasureSpotBudget,
Pass1SupercellGuard.
Files: RugnuxFirstPass.cpp (order + shared machinery: spots, scoring,
schemes, ladder), RugnuxFirstPassRescues.cpp, RugnuxFirstPassAccept.cpp.
Bodies are moved verbatim (Rugnux members reached through rx.); every log
line is unchanged. Literal duplicates folded: the two MAX_SPOT_KEYS
constants, and the feed-every-scheme / run-every-indexer loops of the
long-axis rescue and the short-axis fallback (now FeedSchemes/RunIndexing,
the same calls in the same order). The leftover census, which outlives the
pass, captures what it needs explicitly instead of [&].
Verified against the rc175 0b2dde214 binary on 16 sets (7kcn, 8v4o, 5nw5,
9rci, 6z9g, 9min, nothing_1/2, 7atg, myob/cytc_x10sa, 8a1a, kdp, insu low
ISa, 6p8j, 9qw8) and -N 8 on 7kcn/8v4o/9rci: p.mtz md5-identical, logs
identical as multisets apart from timings.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
With two cards or more, the speculation's spot-finding workers took card t % cards, so some of them
shared card 0 with the first pass they run beside. They now take the other cards, worker t on card
1 + t % (cards - 1): on two cards all of them on card 1, as before the previous commit, and on more
cards spread over cards 1..n-1. Spots are a pure function of the frame, so the card cannot change
them; on one card the speculation does not start. Not measured on more than one card.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
The speculation that spot-finds and indexes at the measured beam centre beside the first pass
(only with two GPUs or more) put all its spot-finding workers on card 1. The file centre's spot
finding ends long before the speculation's, so the speculation is what the first pass waits on
(T4 server, kdp: main pass spot finding done at 1.3 s, speculation at 5.6 s). Its workers now
take card t % cards, as the first pass's own workers do; worker t builds and frees its engine on
the same card, since the destructor's OnWorkers maps t the same way.
Spots are a pure function of the frame, so which card finds them cannot change them. The
prototype measured kdp 25.9 -> 23.6 s on the 4-card T4 with md5 unchanged on 5 sets. On a
one-card machine the speculation does not start (get_gpu_count() < 2), so nothing changes there:
p.mtz md5 identical to rc175-next 41fab6156 on myob_x10sa, cytc_x10sa, thau_x10sa_16keV, 8a1a,
11if, 7qis, 6h5t, kdp_x10sa_20keV at default -N, -N 8 and CUDA_VISIBLE_DEVICES=0 (5080
workstation). The multi-card gain and the 2-card risk (speculation workers now share card 0 with
the main pass) are not measured here.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
79ec61c14 asked the 'end wedge' scheme inside pick_best, so every pass the
axis-sign, beam-centre and long-axis rescues run carried it, and its count went
into the pass those rescues are gated on. A consecutive wedge indexes part of
the validation frames at the WRONG rotation-axis sign as well: on 7kcn (CBF,
P4_122, the file's axis sign wrong for pass 2) the spread and the wedge gave
0/60, the end wedge 33/60, which lifted the pass over the sign rescue's
majority gate - so the flip, where the spread indexes 60/60, was never asked.
Pass 2 then integrated on the end wedge's lattice (33-49/60 frames): ISa 11.8
-> 4.7, R_meas 7.9 -> 15.3 %, wall 27 -> 43 s. The same mechanism let it
defend a file beam centre the measured one beats on 6p8j.
The end wedge is now asked once, after the rescues, at the geometry they
settled on, and only where nothing has indexed a majority; it is judged against
the standing pass as before. It can no longer displace a pass that indexes a
majority, and the passes the rescues run no longer pay for it.
Battery subset vs e99dd4949 (GPU): 7kcn back to 7c9a6c (p.mtz md5-identical
to 7c9a6c, ISa 11.8, R_meas 7.9 %); 5nw5 still pass (P212121, d 7.10, ISa
8.9); 6p8j first pass back to 7c9a6c's (measured centre, spread 60/60).
p.mtz md5-identical to e99dd4949 on 3mc4, 5ky6, 5reo, 7atg, 8v4o, 9qw8, 9rci,
cuhf2, cytidine, myob_x06da_powder_1/2, myob_x06da_split/sparse,
insu_I_x06da_low_isa, kdp/lcystine/yag/thau inhouse sets, nothing_1 (no
lattice) and six private sets.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
This reverts commit c5a13d212.
The second-domain test fixed its target (a low-ISa insulin set) but was
evaluated 1132 times on 212 battery sets and adopted a "second domain"
on 50 of them; three sets that passed went to fail (5j23, 9qw2 and one
private set: F432 -> P432). It never asks whether the other domain is a
different orientation, so near-ties at 54:46 decide it. A narrowed
version (regr2-domain) still costs about 120-200 s battery-wide for one
difficult set. It is parked for rc176, where near-tie domains are to be
carried as two hypotheses and decided late.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
The arbiter between two first-pass schemes whose cells differ by an integer volume factor
adopted the larger cell when it accounted for more validation spots than the smaller one. Each
count is made on the cloud derotated through that scheme's OWN refined geometry, so the
comparison is also one of two geometries, and the scheme that refined on frames spread over the
sweep wins it whichever cell it found. With the decisions before pass 2 taken on the first part
of a rotation sweep (5b9144410), 'spread' samples 120 deg instead of 360 and on a crystal with a
very long axis it returned a 3x harmonic of the I-centred orthorhombic cell while 'wedge' found the
true one. The 3x cell accounted for more spots than the true cell - and none of its
spots lay off its (0,1,0) sub-lattice, so the class it adds was empty. It was adopted, and the
run merged in C2 at 3x the primitive volume (I/sigma and R_meas both far worse). On the full sweep
(before part B) the roles were reversed: 'spread' found the true cell and 'wedge' a 2x harmonic,
and the count, again won by 'spread', happened to point the right way.
Now the larger cell must also account for more spots than its own sub-lattice - the lattice with
the emptiest class's translation added - counted on the same cloud. On one geometry the two
cells differ only in the added class, which is the comparison the arbiter's design comment
describes. The added class is now measured on the primitive basis (a centred basis's own
absences read as an empty class), and both cells are counted on Niggli-reduced bases, since a
fractional-Miller tolerance on a skewed basis makes the sub-lattice miss spots it contains.
The log reports both counts. The ladder's harmonic check keeps its spot-count rule.
Measured (GPU, against rc175 7c9a6c1 + the OOM fix):
a large-cell I-centred orthorhombic sweep: the 3x C2 harmonic is no longer adopted; it merges
in I222 again, with I/sigma and R_meas back to the night before part B
6h2p_native first pass no longer takes a 3x cell with an empty class (24436 vs
18613 spots); final unchanged: C2221, d 1.32, ISa 19.75, R_meas 13.6 %
p.mtz md5-identical: 5nw5, 8v4o, 9rci, 5ky6, insu_I_x06da_low_isa, nothing_1, nothing_2,
myob_x06da_powder_2, 7atg, 9qw8, 5reo, 6ze4, 9w3y, 6z9g, cuhf2, lcystine_x10sa_20keV,
myob_x06da_sparse, lyso_x06da_half_image, 8a1a.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
Since the decisions before pass 2 read only the first part of a rotation sweep
(5b9144410), the first pass samples [start, PrepassEnd) with its two schemes:
'spread' (100 frames over it) and 'wedge' (consecutive frames from the start up to
the accumulation cap). Which orientations a sweep passes through first is chance,
and a sparse pattern can open on frames that do not show the long axis: on 5nw5
(P212121, 92 x 170 x 390 A, 10-25 spots a frame, 0.1 deg frames) the spread over
the first 120 deg returned a C-centred 955 A cell at every density tried (60-200
frames: 0-10/60 validation frames) and the wedge a wrong one (0/60); the FFT never
offered the true cell. The run kept the 5/60 cell as a sparse lattice and merged
P2_1 at 2.43x the deposited primitive volume, ISa 2.7. The spread over 0-90, 30-150,
60-180 or 0-180 deg indexes 57-59/60, and consecutive frames from 60 deg on 60/60:
the second half of the first part carries the lattice.
Where the two schemes index no majority of the validation frames, pick_best now
also indexes 'end wedge' - the first part read back from its end up to the
accumulation cap - and judges it against them as any later scheme is (clearly more
frames, or the axis-harmonic arbiter). It stays inside [start, PrepassEnd), so
nothing before pass 2 waits for the rest of the sweep. It is not asked on a run the
schemes index: offered there unconditionally it only added a lattice for the
axis-harmonic arbiter, which took a 3x hexagonal supercell over an I-cubic crystal's
cell on a spot count. The measured-centre arm of the beam-centre check takes it the
same way, whether its schemes were indexed speculatively or not.
Battery subset vs rc175 7c9a6c (GPU): 5nw5 fail -> pass (P212121, vol 0.996, d 7.04,
ISa 8.42, R-free(model) 0.462; baseline 7.06 / 7.76 / 0.464); myob_x06da_powder_2
d 2.26 -> 1.47 A (baseline 1.47), ISa 2.11 -> 2.34; p.mtz md5-identical on 8v4o,
9rci, 5ky6, insu_I_x06da_low_isa, nothing_1, nothing_2, 7atg, 9qw8, 5reo.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
The first pass can only choose among the cells the FFT builds, and two
lattices seen together - a twin domain, or a second crystal whose reciprocal
lattice shares an index-2 sub-lattice with this one's - build the cell that
contains both, twice the volume of either. That cell indexes both domains'
spots and wins every comparison of FFT candidates; the true cell is never
offered, so nothing downstream can pick it.
Each first pass now scores the standing cell's seven index-2 sub-lattices
(same orientation and geometry) on the validation frames. The frame count
only nominates (a sub-lattice must index a majority of the frames and no
fewer than the standing cell); the decision is the pooled-spot comparison
already used between a cell and its axis harmonic (ValidationEvidencePrefers,
each lattice against its own wrong-spindle-angle null). A real superstructure
keeps its cell there because its added class holds reflections; a two-domain
union cell loses because it must place each domain's spots twice as finely
with an orientation fitted to both.
9rci (P1, deposited ~36/39/101 A): from rc173 on rugnux had merged a c-doubled
cell (b+2c, ~199 A; R-free vs model 0.57). The deposited cell was reached only
between 0f9f3a and the part-B commit (5b9144410), because pass 1's orthorhombic-
constrained post-refinement walked the tilt to -0.46 deg and the whole-sweep
re-index happened to land on the short cell there. Part B did not cause the
wrong choice; it removed that accident (rc175 with --prepass-fraction 1 still
gives the doubled cell). Now, on the early range at the file's geometry, the
(0,1/2,1/2) sub-lattice indexes 57/60 frames against 41/60 and puts 1915/5659
pooled spots on itself against 1191 (30 and 35 by chance): adopted. Result:
cell 35.90 39.17 100.78 98.6 90.3 89.9, R-free (model) 0.270, d 1.69 A,
CC1/2 0.988 (was 199 A cell, 0.569, 1.81 A, 0.952).
Battery against rc175 7c9a6c (GPU): p.mtz md5-identical on 5nw5, 8v4o, 5ky6,
insu_I_x06da_low_isa, nothing_1, nothing_2, myob_x06da_powder_2, 7atg, 9qw8,
5reo, 6ze4, 9min, 8v2t, lyso_x06da_ref, insu_H_x06da_twin, myob_x06da_split;
only 9rci changes (now the deposited cell; the verdict read pass before only because P1 matched P1). Cost: tens of ms per first pass.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
When neither the file's nor the background-measured beam centre indexes a majority of the
validation frames, the first pass chose between them on the pooled spots each centre's lattice
puts on itself over chance, and only then asked the long-axis rescue - at the chosen centre alone.
The lattices compared there are, by construction, not the crystal's, and the comparison is close
to a coin toss. With the decisions before pass 2 taken from the first part of the sweep (part B,
5b9144410), 8v4o (P6122, c = 545 A) tipped from the file's centre to the measured one, 3.6 px out
across the spindle, on 16.7% against 15.5%; the rescue there could not see the 545 A axis, and the
run shipped a P1 sub-cell (primitive volume 0.35x). At the file's centre the same rescue on the
same early frames indexes 60/60 (checked with --beam-center-check=off on the rc175 binary).
The rescue is now a lambda, asked at both centres in that branch so each centre is judged on the
best lattice it gives, and asked as before after the beam-centre check where that branch did not
already ask it. The early-frame principle of part B is unchanged.
8v4o: P1 2.62 A, CC1/2 0.920, R_model 0.553 -> P6122 2.12 A (pass), the file's centre kept.
p.mtz md5-identical to rc175 7c9a6c7d1 (GPU) on 5nw5, 5ky6, 5reo, 7atg, 9qw8, 9rci, cuhf2,
cytidine, insu_I_x06da_low_isa, kdp_x10sa_20keV, lcystine_x10sa_20keV/25keV, lyso_x06da_half_image,
myob_x06da_powder_2, myob_x06da_sparse, nothing_1, nothing_2 and the two private sets that reach
the same branch. 9rci (doubled-c alternative under tNCS) is a different mechanism: rc175 with
--prepass-fraction 1 also returns the doubled cell.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
Two twin domains, or two crystals of one cell, each index every validation frame, so the frame
count the first pass picks its lattice by cannot tell them apart, and the FFT returns whichever
its sample favours. Since the decisions before pass 2 read only the first part of the sweep
(5b9144410), that sample is smaller and it can be the domain there is less of. On an I-centred
cubic crystal with a second domain at 60 deg about [111] pass 1 took the smaller domain; pass 2's
re-index then found the hexagonal P cell both domains share (3x the primitive volume), the
supercell guard handed it back to pass 1's lattice, and the run integrated the smaller domain
over the whole sweep. Every reflection the two share then carried the larger domain's intensity,
which broke the cubic 2-folds (operator CC 0.48 against 0.89 for the 3-fold about the twin axis),
the added-operator R gate refused 23 and the run merged in R3 on the cubic axes - the I centring
undeclared, primitive volume 2x.
Now the spots the chosen lattice leaves on the spread frames are indexed again, as the
leftover-lattice census does, and where they give the same lattice (Niggli-reduced primitive
edges within 2 %) in another orientation, the two are judged on the validation spots each puts
on itself over a wrong spindle angle (ValidationEvidencePrefers); the larger domain is kept. It
is decided on the same early frames, so part B's principle stands.
Measured (GPU, battery, against rc175 7c9a6c1):
insu_I_x06da_low_isa R 3:R, d 1.59, R_model 0.315 -> I 2 3, d 1.43, R_model 0.226 (= pre-B)
5reo adopts the larger domain (86.5 % against 68.4 % of validation spots):
ISa 23.0 -> 34.3, R_meas 23.6 -> 23.3 %, R_model 0.205 -> 0.202,
d 1.64 -> 1.69 A, still C2
5nw5, 8v4o, 9rci, 5ky6, 7atg, 9qw8, myob_x06da_powder_2, nothing_1, nothing_2: p.mtz
md5-identical (the check ran on most of them and kept the lattice).
Result-changing only where a second domain is adopted.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
The first pass's last gate - is the lattice this crystal's at all - compared the validation spots
on the lattice with the median of five displaced-spindle-angle nulls, against the null's binomial
noise floored at one spot. On the two in-house no-crystal controls it is fed a handful of genuine
stray reflections from something rotating with the sample (the same detector positions, a pair
symmetric about the beam 198 frames apart, in both sweeps, offset by the 15-frame start difference),
plus one reflection near the spindle axis that stays in diffraction for ~46 frames. With the
decisions taken from the first part of the sweep the 60 validation frames are 3 deg apart instead
of 6, so that one reflection is counted on three validation frames, and the gate passed on
4 spots of 151 (null median 0) and 7 of 72, where the whole-sweep flow had seen 3 and 3.
Now the evidence unit is the distinct Miller index - a reflection on several validation frames
is one piece of evidence - and the test uses every displaced angle's count: with no crystal the
recorded angles are one of six exchangeable arrangements, so the gate is the binomial tail of the
recorded arrangement's share at p = 1/6, at the same 0.001 significance (SPOT_BUDGET_SIGNIFICANCE_Z).
No threshold is added; the median and the one-spot floor go from the gate. The comparisons between
lattices (ValidationEvidencePrefers) are unchanged.
no-crystal control 1: 4 distinct vs 2 over 5 wrong angles, tail 0.009 -> no lattice (was P 1)
no-crystal control 2: 5 distinct vs 2, tail 0.002 -> no lattice (was P 1)
p.mtz md5 identical to rc175 7c9a6c7d1 (GPU) on 5nw5, 5reo, 7atg, 8v4o, 9qw8, 9rci, 5ky6,
insu_I_x06da_low_isa, myob_x06da_powder_2; cytidine and myob_x06da_split pass. The weakest real
lattice in the open/in-house/private battery logs has 117 spots against a null of 0.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
The beam-centre check runs a second first pass at the centre measured from
the background, and reads the first pass's answer only once that pass is
over. With two GPUs or more, its spot finding (on the second card), scheme
feeding and FFT indexing now start beside the file centre's pass, on a
copy of the experiment at the measured centre with workers, a mapping and a
spot list of their own (SpotFindingWorkers). The check takes them only
where the experiment (rotation angles included), SpotFindingKey at the
measured centre and the spot budget are still what the run has there, and
scores the schemes on the run's own indexer as before; otherwise it runs
that pass itself as before. The spots at the two centres are not shared:
the spot list depends on the centre (ring background, resolution, budget).
With one GPU, and in the CPU build, nothing changes: on one card the two
passes share the card and the reader, and side by side they saved nothing.
FindFirstPassSpots is the first pass's per-frame spot finding, now shared;
ExperimentKey and SpotFindingKey take an experiment other than the run's.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
The ladder's rung says which spots let the FFT find the lattice; until now it
also fixed how deep the whole run reads each frame (the spot-finding floor),
so which floor's FFT happened to succeed decided the run's depth. On a hard
pattern that is close to a coin toss: with the decisions taken from the first
part of the sweep, a sub-angstrom crystal (7atg) tied at 60/60 on a 1.13 A
and a 1.60 A floor, kept 1.60 A for the run and lost half its validation
spots. Now the winning lattice is itself scored at each deeper floor, deepest
first, and the run reads to the deepest at which it indexes as many
validation frames as at its own. A contaminant past a floor costs the lattice
frames there and keeps the floor.
Measured (GPU unless noted), against c240690a1:
7atg ISa 21.0 -> 23.1, R_meas 5.4 -> 5.1 %, R-free(model) 0.220 -> 0.160
(whole-sweep decisions, af3ea9: 22.9, 5.2 %, 0.178)
myob_x06da_split reads to 1.92 A instead of 3.00 A: ISa 2.2 -> 2.7, R_meas 87 -> 78 %,
CC1/2 81 -> 70 %, R-free 0.382 -> 0.402 (a contaminated split crystal)
9qw8, 9zmu, 8v4o, myob_x06da_powder_1/2 (CPU): unchanged; 8v2t: p.mtz bit-identical.
Result-changing only where the ladder adopts a rung.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
The pre-scan sample (beam-stop shadow, defective pixels, spot width, the background beam centre)
and the first pass's frames (the spread and wedge schemes, the validation frames, the prefetch) now
come from [start, PrepassEnd) - the part of the sweep the geometry pre-pass integrates - instead of
the whole sweep. Rugnux::PrepassEnd is the one place that range is defined, for the pre-pass and for
these decisions alike, so any change to the pre-pass range (such as a minimum rotation) moves both.
Stills keep the whole range.
With rugnux following a collection while it is written, nothing before pass 2 then waits for the
second part of the sweep: the pre-scan, both first passes and the pre-pass run while it is
collected, and pass 2 starts reading as soon as they are done. An offline run takes the same
decisions from the same frames, so the streamed result still equals the offline one by md5 (in-house
JUNGFRAU 4M thaumatin replayed at one data file per second, also at -N 8 and with the replay paused
and resumed): 2.4 s from the last data file to the result, against 8.0 s for the offline run.
Result-changing. p.mtz md5 at af3ea9cca -> here (with the 120 deg minimum of the previous commit),
GPU: myob de2ab820 -> 9bdd6916, cytc 3e5d2f91 -> 62e744a1, thau bc5e3de3 -> 3ec88c34,
kdp cf83ee5d -> cdc2c068, 8a1a 50ab659e -> 309ea9a8, 8qaw d3287922 -> c2849d52; CPU: myob 3f9d12d8 ->
18c7ac2f, cytc d30c9969 -> 40e17d97, thau 4cdbdd94 -> f14e52da, kdp f1d801f3 -> eb930714. Sweeps of
120 deg or less are unchanged by this commit (their early range is the whole sweep). Needs the
battery before it is merged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
The goniometer rotation-scale walk ran between the geometry pre-pass and the
refined (output) pass, on the critical path: two indexing probes, at the stored
angles and at the pass-1 fit, each with its own first-pass spot finding.
Adopting a scale other than the stored one is rare, so the refined passes now
start at once at the stored angles, on a copy of the run, while the walk runs on
the run itself. The walk starts once the output pass has indexed, so its probe at
the stored angles finds that first pass already done (spot store, rotation-indexer
memo), and its probe at the fit takes the frames' spots from the store as well:
the spot store's key no longer carries the goniometer's angles, and the spindle
angle each stored spot carries is stamped again for the angles in hand, exactly
as DiffractionSpot::Export stamps it (verify_first_pass_memo still recomputes and
compares). The output pass waits for the walk's answer only before it writes
(the superseded flag); where the walk adopts a scale, or leaves the spot-finding
settings changed behind it, the copy's passes are thrown away and the refined
passes run again on the run itself, which is then exactly where the sequential
walk left it. A refit integrates and only follows a move off the stored angles,
so it first calls the copy off (a cancel flag of its own) and waits for it to
end: two integrations never share the card.
WHEN, not WHAT: the adopted scale and the surviving output pass's inputs are
those of the sequential flow. The console shows the check as a step only where a
scale is adopted.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
The body of RunPipeline's two-pass branch becomes the private member
FirstPassRotationIndexing(PipelineLocals &), unchanged (de-indented one level) behind the lines that
bind the locals it reads. Its one early exit, the indexing probe's `return result;`, becomes
`return true;`, and the call site returns the result there. The helpers only it reads (SameSpots,
IndexedMillerIndices, AddedClassOccupancy) move into the new file's anonymous namespace.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi