A transform launched on a non-blocking stream straight after cufftPlanMany could read the
plan before its setup, still queued on the NULL stream, had finished: Xid 31, a read at a
fixed near-null address, "an illegal memory access" in the indexer threads. It showed at
transform length 3072, in 3 of 4 runs of one rotation set where a pool built its FFT
indexers at the start of integration (6 of 28 ci-tier sets, 9 of 25 null-model runs).
It went away under CUDA_LAUNCH_BLOCKING=1 and with a cudaStreamSynchronize(NULL) after
the plan (0 of 11 runs). The 3142 length the indexer used so far never hit it, but
nothing ordered it either.
Plans are built once per indexer, so the wait costs nothing measurable. The other cuFFT
users (beam-centre FFT, model structure factors, rigid body) create raw plans and are not
covered by this wrapper.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
JFJOCH_MACOS_SIGNING_IDENTITY (the certificate's SHA-1) makes the install step sign the shipped
program with hardened runtime and secure timestamp: macdeployqt's -sign-for-notarization for the
viewer bundle, a codesign INSTALL(CODE) for rugnux. tools/notarize_macos.sh then submits the cpack
artefact to Apple, waits for the verdict and staples the ticket to the .dmg; the rugnux binary
inside the .tgz is submitted as a zip, as a bare executable cannot carry a ticket. Both macOS CI
jobs resolve the identity from the runner's keychain, check for the hardened runtime and notarize
on every push, so a rejection shows up with the change that caused it.
The identity is passed as the hash, not the name: Qt's generated deploy script writes
DEPLOY_TOOL_OPTIONS unquoted, so a name with spaces splits into several macdeployqt arguments. The
notarytool profile has to live in the login keychain file (store-credentials --keychain): the
default data-protection keychain becomes unreadable once the screen locks, which an unattended
runner's does.
Verified end to end on the runner: both artefacts accepted by Apple, the stapled image and a
quarantined copy of the app and of rugnux pass Gatekeeper as Notarized Developer ID, signing and
the credential lookup work from a launchd-started process with the console locked.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This reverts commit 64a8d8234. Measured on non-lysozyme sets (one search per frame scheme, 5000
frames each, with -C -S): on a body-centred orthorhombic crystal with a 235 A axis the single call
indexed 14.4% of frames against 15.9% with the 30/80/all escalation (79.7k -> 63.1k observations);
the escalation restored the frame count exactly (15.9%) on that set, on a P212121 set (73.6%) and on
lysozyme (55.0%). The lean seed matters for ffbidx on a long cell.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With a known cell the min-pix 1 list is searched again whatever the min-pix 2 list holds. On sparse
serial data the frames with almost no spot of two pixels are real hits: a weakly diffracting
hexagonal run (20000 frames, -C -S) lost half its indexed frames and observations to the gate
(6.5 -> 3.4%, 273k -> 120k observations, CC1/2 .83 -> .74); with the gate off for a known cell it
matches the original scheme (6.5%, 274k, .83). De novo the gate stays: there the cells a free search
fits to single pixels were unrelated to the crystal's.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The carried indexer answer now brings its spindle severity along, so with a goniometer (--force-still)
no severity-only pass runs per carried list. The min-pix 1 search gate counted ice-ring spots the
indexer leaves out; it now counts the spots indexing would use.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The peak search gave each of the 16384 directions to one thread, a chain of dependent
FP64 additions over 1572 bins - latency-bound on a card that holds far more threads.
Each direction's spectrum is now split into four consecutive runs of bins, one thread
each, and the runs' first-best peaks are combined in order with the same strict
comparison, so the first bin of highest prominence is still the one returned.
A run after the first sums its starting window afresh rather than carrying the running
sum in. The double sums of float magnitudes are exact unless a magnitude is below
~1e-9 of its window sum, so the background is the same double: 0 of 491520
directions differ in a synthetic still/rotation harness, and the hkl of 3000 stills
frames (-X fft) is byte-identical.
Kernel (isolated, RTX 5080): 0.87 -> 0.65 ms per call. The stills de-novo loop gains
~4% (128 -> 133 frames/s, ABAB, load ~10): the FFT chain as a whole is bound by GPU
memory bandwidth under the indexer pool's concurrency, so even a background in float
(6x faster kernel, not adopted) gives only ~9%. Rotation first pass: FFT kernels are
~3% of the indexing step.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A still indexed on few spots often gets a lattice that explains them as well as the true one: an axis
multiple, whose reciprocal lattice holds every true node plus the nodes between, or the true lattice in
another setting. Its cell is then not the run's, the merge leaves it out, and its reflections at the true
nodes - as good as any - are lost. On two 25k-frame XFEL runs of a tetragonal crystal these were 26-30%
of the indexed frames; for those holding the true cell as a sublattice, the reflections at true nodes had
the I/sigma of accepted frames and those between them none.
RelabelOnSublattice (new, scale_merge/SublatticeRelabel) compares lattices, not cell parameters: it
looks for integer combinations of the frame's reciprocal axes whose full metric is the run's cell within
the merge's own tolerances, volume ratio 1-4 (another setting, or a 2x-4x supercell). Where several such
bases exist - a nearly symmetric supercell fits along more than one axis - the one with the strongest
mean I/sigma at its nodes is taken, since only the true lattice has signal there. The frame's indices
are re-derived on that basis (h = h' M^-1), reflections that are not nodes are dropped, as are those
finer than any frame already on the run's cell reaches (the wrong lattice was also predicted to a wrong
resolution), and the frame gets the run's basis in its own orientation. It runs before the first stills
merge, and in --mode scale before the resolutions are recomputed from the cell.
--mode scale, both runs (indicative; the mx pairs are queued): 1060 and 920 crystals put back,
observations +8%, I/sigma 10.3 -> 10.6 and 10.7 -> 11.0, CC1/2 0.966 -> 0.969 and 0.970 -> 0.966.
Tests: SublatticeRelabel_DoubledAxisGoesBackToTheTrueCell, _OtherSettingAndUnrelatedLattice,
_SignalPicksTheAxisThatWasDoubled.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The unmerged MTZ took every integrated still. A still whose cell is not close to the run's - the same
test, with the same tolerances, the merge applies - was indexed in another lattice (on few spots,
typically an axis multiple), so its H K L do not name the reflections a reading program takes them
for: aimless or careless would scale them in as wrong reflections. On two 25k-frame XFEL runs such
frames carried 60% of the integrated reflections, and the 824 MB file was the largest single cost
after the merge (about 7 s in --mode scale). Rotation runs are unchanged (one lattice).
The orientation matrix in the batch headers is likewise taken from the first crystal on the run's cell.
Test: Unmerged MTZ: on stills, only the crystals on the run's cell.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The broker cancelled the whole data collection on the first indexed image when its algorithm was FFT and no
GPU was visible (the indexer pool refuses FFT there, and the receiver cancels on the exception). FFTW is too
slow for live stills, so it is not switched to; with None the pool reports the frame as not indexed and the
collection runs on. rugnux resolves -X FFT itself (cuFFT or FFTW) and never reaches this case.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ExportReflections ran French-Wilson single-threaded: about 1 s on the P1 search merge of a 25k-frame
stills run (180k reflections). It now uses the merge's thread count, as AssignRfreeFlags does; both
give the same result at any worker count by construction. mtz/unmerged-mtz data, hkl, cif and report
identical (--mode scale, 25k-frame XFEL run).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With the main image pass already waited for, a simulated 8 GB card with 7.0 GB free (the display's
share taken) still ran out of memory: the rotation-scale walk's index-only probe and the speculative
geometry probe ran beside each other during the merge, and the walk's cuFFT plan failed. On a small
card a probe pass now also waits, before it starts, for every other probe pass to end. Scheduling
only; nothing changes on a 16 GB card.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The defective-pixel finder ran only on scanning rotation data. On stills, a pixel that holds the
detector's error value on every frame but is missing from the file's static mask is read by the
integrator as a per-frame unreadable pixel, so every reflection whose signal disk touches it is
flagged overloaded and the merge drops it. On a SwissFEL JUNGFRAU stills run, 14k such pixels made
3.9% of all integrated reflections "overloaded" (mostly finer than 1.6 A, where they are scarcest)
and raised a false OVERLOADS warning; with them masked, 0.05% remain flagged and the profile fit
keeps the reflections whose disk stays at least MINPK readable.
On stills only the error-value and dead rules apply: the hot-pixel rule counts a reflection's stay on
a pixel in rotation, so spacing is passed as 0 there, which HotPixelFinder already treats as "no
hot-pixel test". Rotation is unchanged (same gate, same oscillation and spacing).
Result-changing for stills; merged-statistics A/B on full runs and the battery ci check still to do.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A worker of the full-analysis image loop holds about 24 bytes a detector pixel on its card (sixteen
held 6.3 GB on a 16.8 Mpx detector, measured with nsys). The workers now keep to half of the card's
total memory: on a 16 GB card that is still the sixteen of before for detectors up to ~21 Mpx; on
an 8 GB card with a 16.8 Mpx detector it is ten, where sixteen ran out of memory with ~7 GB free.
How many workers there are changes only the loop's speed. An explicit -N is obeyed as before.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The device scaling's peak, measured with nsys on four large sets (33-123 M observations, 4-16 M
distinct hkl), is 81 bytes an observation plus 291 bytes a distinct hkl to within 5%. Once the
ingest has counted both, an engine whose estimate is over 80% of the card's total memory scales on
the CPU instead, with one warning line saying why; a resident ingest is made again on the host,
which the CPU path needs. Decided once, before anything is scaled, on the card's total memory: the
answer depends on the card, not on the moment.
On a simulated 8 GB card (total clamped to 8.2e9, 7.75 GB free) a 33 M-observation, 15 M-hkl set
whose final merge alone needs ~7.5 GB on the GPU ran out of memory there; it now completes, and its
merged MTZ matches the 16 GB GPU run to float rounding (max relative difference 9e-7 on any
intensity column, same reflections, cell, space group and resolution cut). The largest estimate on
the battery is 12.45 GB, under 80% of a 16 GB card, so nothing changes there.
The all-observations arm is copied from the engine's card arrays only when that engine is on the
GPU.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each crystal's (psi_x, psi_y) tilt was refined by a Ceres problem rebuilt for every inner
iteration: one auto-differentiated residual block and one Cauchy loss per reflection. With the
scaling reference cleaned up it was 70% of the stills scale/merge. The same objective - Cauchy
on the sigma-weighted intensity residuals plus the Gaussian prior on the tilt, the tilt bounded -
is now minimised by a small Levenberg-Marquardt on the rho'-reweighted 2x2 normal equations,
the tilt applied to all reflections through one rotation matrix and the Jacobian taken by
central differences.
It reaches the same answer to optimizer precision: on the new synthetic test
(StillsPartialityRefine_RecoversTheTilt) partiality error 0.0559 -> 0.03019 with Ceres and
0.03016 with this, mean tilt 0.02065 vs 0.02067 deg; on two 25k-frame XFEL runs (--mode scale)
CC1/2, I/sigma, R_pim and the CC against a deposited lysozyme model agree to 0.0005, mean
|dpsi| 0.023 deg on both. Scale+merge 6.9 -> 3.0 s (--mode scale, loaded box).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The final stills merge drops every crystal whose cell is not close to the consensus cell
(MergeOnTheFly::Mask), but the scaling reference (MergeAll) and the partiality post-refinement
were made over all of them. On two 25k-frame XFEL runs of a tetragonal crystal those were 26-30%
of the indexed frames and 60% of the integrated reflections: weak frames (median 8 indexed
spots) indexed in a 2x/3x/4x supercell or an unrelated lattice, whose supercell-only reflections
are pure noise and whose indices do not name the true reflections. They went into the
reference every merged crystal was scaled against, and through the tilt refinement.
MergeAll and StillsPartialityRefine::Run now take the reference cell and use only the crystals
it accepts - the same test, with the same tolerances, as the merge.
Measured (both runs, mx path): CC1/2 0.9599 -> 0.9598 and 0.9645 -> 0.9644, I/sigma +0.05 and
+0.02, R_pim unchanged, space group and resolution unchanged; --mode scale against a deposited
tetragonal lysozyme model CC 0.9452 -> 0.9477 and 0.9461 -> 0.9473. Scale+merge 2.1x faster on
--mode scale (14.6 -> 6.9 s, loaded box).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On a 16 GB card a probe pass (GPUWorkBeside) runs its image work beside the main line's image pass
(GPUImagePass). Measured on a 16 Mpx detector with nsys, the two together held 8.6 GB of device
buffers (two sets of 24 per-worker 67 MB image buffers plus five 0.6 GB FFT indexers), so on an
8 GB card the refined-geometry pass ran out of memory ("Allocating GPU memory (53.0 MB) failed").
Below 12 GB of card memory each of the two now waits, before it starts, for the other kind to end.
Only when each runs depends on the card, never what it computes; neither waits once running, so
nothing waits in a circle. On a 16 GB card nothing changes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Changes results - not for merging without the owner's approval.
The 30/80/all seed escalation exists for the de-novo search, which a flooded frame's full spot list
derails. ffbidx fits a known cell with a trimmed score that already discounts off-lattice spots, so
for it the escalation only multiplied calls: up to three per searched list, ~2.2 per indexed frame
with a fixed min-pix 2. The ffbidx path is GPU-bound (nsys: card 98.5% busy, ffbidx kernels 86%), so
calls are the loop rate. Stills only: rotation never resolves to ffbidx.
Measured on top of the one-search min-pix scheme (parent), two full 24960-frame serial runs, P41212
merge (box loaded by other agents, load 13-17):
run A: 1130 -> 1532 frames/s; obs 5.30M -> 5.47M; I/sig 13.02 -> 13.12; R_pim 6.46 -> 6.43%;
CC1/2 .973 -> .976; indexing rate 56.9 -> 57.3% (original scheme: 785 frames/s, 5.37M,
13.09, 6.48%, .970)
run B: 1017 -> 1488 frames/s; obs 4.89M -> 4.99M; I/sig 11.95 -> 11.94; R_pim 6.47 -> 6.42%;
CC1/2 .970 -> .972; indexing rate 52.4 -> 52.9% (original: 750, 4.91M, 12.00, 6.50%, .971)
Warm 6000-frame ABAB in one lock hold: 1453/1419 -> 1734/1810 frames/s, same indexing rate.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Changes results - not for merging without the owner's approval.
The per-image min-pix scheme indexed every frame at min-pix 3, 2 and 1, each with the 30/80/all seed
escalation, then indexed the winner again: 3.9 indexer calls per frame. Measured per frame on a
serial lysozyme run (2000 frames, de novo and against the known cell):
- where several lists index, the searches return the same lattice (97-98% of pairs);
- the gain over a fixed min-pix 2 has two sources: (A) frames min-pix 2 indexes but whose min-pix
1/3 list indexes ~26 more spots (a better per-frame refinement), and (B) frames with almost no
spot of two pixels (min-pix 3/2 lists of 1-3 spots, ~50 single pixels at min-pix 1) on which the
min-pix 1 search fits 8-9 spots. De novo, 189 of 194 such B lattices had cells unrelated to the
crystal's; against the known cell they index, but leaving them out raises I/sigma and lowers R_pim.
Now: the lattice is searched for on the min-pix 2 list (min-pix 3 where that does not index); the
first list that indexes hands its indexer answer to the others, which are only refined and scored
(FrameIndexerCalls::lattice_found). The min-pix 1 list is searched on its own only where the min-pix
2 list holds at least ViableCellMinSpots spots. Ties keep the larger min-pix, as before.
Measured on two full 24960-frame serial runs (RTX 5080, box loaded by other agents, load 9-20; loop
rate is frames/s of the image loop; I/sig, R_pim, observations of the P41212 merge):
known cell (ffbidx, -C): base 785 / 750 -> 1130 / 1017 frames/s (warm 6000-frame ABAB 860 -> 1453)
run A: obs 5.37M -> 5.30M, I/sig 13.09 -> 13.02, R_pim 6.48 -> 6.46%, CC1/2 .970 -> .973
run B: obs 4.91M -> 4.89M, I/sig 12.00 -> 11.95, R_pim 6.50 -> 6.47%, CC1/2 .971 -> .970
de novo (FFT): base 89 / 91 -> 174 / 195 frames/s
run A: obs 3.96M -> 3.91M, I/sig 10.18 -> 10.12, R_pim 6.94 -> 6.98%, CC1/2 .960 -> .967
run B: obs 3.91M -> 3.89M, I/sig 10.53 -> 10.52, R_pim 7.01 -> 7.03%, CC1/2 .965 -> .961
fixed min-pix 2, for scale: ffbidx 1496 / 1316 frames/s, obs 4.94M / 4.54M, I/sig 12.62 / 11.77.
The indexing rate drops (ffbidx 59.5 -> 56.9%, 55.2 -> 52.4%; FFT 60.5 -> 52.1%, 60.2 -> 47.9%):
the frames that go are the spurious B lattices; observations stay within 1.3%. Indexer calls per
frame 3.89 -> 1.67 (de novo, 2000 frames); the min-pix 1 search is skipped on 58% of frames.
De novo, 23 of 691 frames whose lattice was the crystal's now take a superlattice (13, same spots
indexed) or a cell fitted to 6-12 spots of a weak frame (10) from the min-pix 2 search.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
calculate_fft_result (the per-direction prominence peak search) was 61-65% of the GPU kernel time of
a de-novo stills run, and the card is what limits that loop. It is one dependent chain per direction,
latency-bound. Two exact changes, taken where the spectrum holds a whole background window (any
FFT max cell above ~30 A; the general loop stays for the rest):
- the sliding window written out as its three phases (clamped at the start, centred, clamped at the
end): the same additions and subtractions in the same order, so the running double sum is bit for
bit the same, but the loads no longer sit behind data-dependent while loops and are issued ahead;
- the window mean divides by a constant width, so the double division (a long FP64 iteration on a
consumer card) is replaced by the reciprocal multiply plus one correction by the exact remainder,
which returns the correctly rounded quotient (Markstein). Checked on the CPU against the division
for 4e8 random dividends over all odd widths 3-401: 0 differences.
Measured (RTX 5080, box loaded by other builds, load 12-25): kernel minimum 1.50 -> 0.83 ms, mean
under the 8-stream pool 4.7 -> 2.8 ms; de-novo stills 2000 frames ABAB 113/120 -> 127/138 frames/s.
p.hkl byte-identical (2000 and 3000 frames, base and new min-pix schemes).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
In rugnux people choose the algorithm, not where it runs. -X fft (and the
old -F) now resolves at parse time to the GPU FFT indexer where a GPU is
visible and --no-gpu is not given, and to FFTW otherwise. -X cufft is the
GPU implementation alone, as -X fft was. -X cufft and -X ffbidx without a
GPU now stop at once with a clear message instead of failing later in the
indexer pool. Auto is unchanged and not redundant: on stills with a known
cell and a GPU it takes FFBIDX.
Only rugnux's CLI spelling changes. The internal IndexingAlgorithmEnum, the
broker and its OpenAPI enum keep FFT = the GPU implementation; the CLI
maps onto it, and the command line the viewer writes for a run (RugnuxCommandLine)
gives the enum's FFT back as -X cufft, which re-parses to the same thing.
A run without -X, and -X fft on a machine with a GPU, process exactly as before.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
hide_gpus() in CUDAWrapper makes get_gpu_count() answer 0 without calling
cudaGetDeviceCount, so every GPU decision in the process (indexer Auto ->
FFTW, preprocessing/spot finding/decode, pre-scan, scaling engine, model
validation, MR) takes the path it takes on a machine without a GPU, and no
CUDA context is created. rugnux reads --no-gpu in main, ahead of the parser,
because set_gpu_blocking_sync() already asks CUDA for the devices. The
banner says 'CPU only (--no-gpu)'. The 'run it on the CPU' advice in the
out-of-GPU-memory messages and in the install docs now names --no-gpu
instead of CUDA_VISIBLE_DEVICES=.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The stills scale/merge after the image loop was almost entirely serial:
- every scaling-reference merge (MergeAll: the per-image scaling reference and the two
partiality post-refinement references) ran French-Wilson single-threaded on its export,
about 3 s each, and assigned free-R flags - neither is read by a reference, which uses I
only. MergeAll now exports intensities alone (MergeOnTheFly::ExportIntensities).
- the merge accumulated into a std::map; it is now an unordered_map exported in key order,
which is the std::map order.
- AddImage, the error-model collection and the R_meas pass of MergeStats did the
per-observation work (systematic-absence test, ASU key, filters, error-model sigma,
outlier test, merged-I lookup) in one serial loop. They now reduce a block of images in
parallel and consume it in image order (ReduceImagesInOrder), so every floating-point
sum, half-set choice and hash-map insertion is made in the serial order.
Measured on a 25k-frame XFEL stills run (box loaded, load 9-14): merge P1 19.0 -> 7.1 s,
final-group merge 27.7 -> 13.2 s; on a second run 23.4 -> 7.7 s and 29.7 -> 13.6 s.
mtz/unmerged mtz data, hkl, cif and report identical on both runs, mx and --mode scale.
New test MergeOnTheFly_AddImagesIsTheSerialMerge checks the parallel merge against the
serial loop across a block boundary.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The virus capsid (I222, ~400 A cell, marCCD) passed with the deposited group in every rc175 full
run: 71 s on the workstation, 159 s and again in 20261014-0141 on the server. It adds a second
large set to the regression check, about a minute of rugnux on the RTX 5080.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Open 196/203 (97%; the 20261011 server run had 195/205), inhouse 47/47, no crash. Kept as
results/server/20261014-0141_6e9b89_1.0.0-rc.175 (report.md, results.json, manifest.json as
written); no private arm on this site.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The stills per-image loop spent 99% of its time indexing (28.4 of 28.7 ms per frame on a 16 Mpx
serial set; read, decode, spot finding and integration together 0.33 ms), and only four threads
were ever busy:
- The 16 image workers each block on IndexerThreadPool, which had the default 4 threads, while the
FFT candidate post-processing is CPU work. Stills now size the pool to min(-N, 8); 8 gets 94% of
what 16 does (83 / 125 / 133 frames/s at 4 / 8 / 16 with the changes below) at half the card
memory - each GPU FFT indexer holds ~0.5 GB, 16 of them took the run to 12.7 GB.
- FFTIndexer::ReduceResults (widened path, taken on ~2/3 of calls) deduplicated ~2900 candidate
cells per call by a linear is_close scan: 46% of all CPU cycles. The accepted cells are now kept
ordered by a and only the 2%-window of a is asked; the answer (any close cell) is the same.
- FilterFFTResults built a 16k-entry multimap per call (11% of cycles); now a partial sort of
indices in the multimap's reverse order (magnitude descending, later direction first on ties).
- calculate_fft_result reads magnitudes from a bin-major transposed copy (in the histogram buffer,
no new memory): neighbouring lanes no longer read a spectrum apart. Bit-identical results
(checked against the old kernel on 4000+ calls, 0 mismatches); not faster on an idle card, but
67 -> 98 frames/s with the pool's streams sharing it.
- The stills min-pix choice indexes a frame at 3/2/1 and then the winner again; the seed of
strongest spots is often the same list each time (31% of indexer calls repeated an earlier call
of the same frame). IndexAndRefine answers a repeated list from a per-frame FrameIndexerCalls
memo kept by MXAnalysisWithoutFPGA for the duration of one frame.
Measured (RTX 5080, 32 threads, data in page cache), first 2000 frames, ABAB: 35/35 -> 127/127
frames/s, p.hkl md5 identical. Full 24960-frame run: loop 1081 s -> 277 s, wall 1132 s -> 328 s,
p.hkl md5 identical (P41212, 33234 unique); with a pool of 16 the loop is 222 s (the full run has
more spots per frame than its first 2000 frames, so the CPU side weighs more there). With a known cell (ffbidx path) 715 -> 920 frames/s on
2000 frames, p.hkl identical. Tests: jfjoch_test [Indexing] pass.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
- MolecularReplacement: an engine whose making throws anything (std::bad_alloc, a gemmi error), not
only a JFJochException, is refused and its reservation given back, so a thread waiting for the
same engine is woken instead of waiting forever.
- RotationScaleMergeGPU::Alloc waits for the GPU work beside itself (GPU_BUSY_TIMEOUT), so its
buffers are allocated plainly Synchronous; SynchronousAfterWorkBeside added a second 10-minute wait
in front of it.
- CPU_DATA_ANALYSIS_INDEXING: an offline run decides its near ties on the whole sweep from the start;
only a live run (broker URL or --simulate-live) decides early and re-checks.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Replaces the kept f853596 run with 20261013-1944_a45135_rc175-a451359-full (open + inhouse; the
private arm is never kept). Against f853596: no verdict changes beyond 5ky6 fail -> unscored (screw
undeterminable), sum 4145 -> 4088 s; the other moves are the last bits of device code without FMA
contraction (8xtf ISa 7.7 -> 7.9) and anomalous sigma.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
After the END message the receiver waits for every image buffer the pusher still holds (for
ZeroMQ: zero-copy messages freed once libzmq has transmitted them) before calling Finalize();
on expiry the collection fails with "Send commands not finalized". The limit was a 10 s literal
repeated in the FPGA and Lite receivers; it is now one constant, SendFinalizeTimeout, in the
JFJochReceiver base class.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
The measured-centre arm is a whole geometry pre-pass. It now integrates first a third of the pre-pass
frames, in 3k + 1 blocks of about 10 deg over the whole pre-pass arc with every third kept, and the
file's centre merges the same frames (its outcomes for the other frames are moved out while that
engine ingests). Where the measured centre loses on that subset on every line MeasuredCentreWins draws
by more than NEAR_TIE_SIGMA times the line's noise (Poisson for the strong counts, the Fisher standard
error for a CC1/2), it has lost and no full arm is run. Anything else, a near tie or a win, puts the
run back as the file's centre left it and runs the arm over all the pre-pass frames, deciding as before.
The run made again (decide_all_late_) runs the arm on all frames.
The same subset was tried for the metric-symmetry demoted arm and the walk refits and is not used
there: the held-out residual's standard error does not grow on a third of the frames, so a subset
margin overstates the evidence (one set's metric call flipped at 2.07 sigma), and a subset refit moved
one walk to a worse fixed point.
Against rc175 edde4439c, ci + 49 extra + 9 private sets: p.hkl and MTZ data identical on all 86. The
arm lost on the subset on 22 sets (no full arm), was a tie/win on 9 (full arm as before, +0.3-1.7 s).
Alternated runs: one set 62.1 -> 55.2 s, another 46.2 -> 42.4 s.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
The walk's indexing probes start as the canonical pass's post-refinement fit ends and run beside its
merge and space-group search. The pass asked whether the walk takes a round only before the search
and before its analyses; it now also asks after the search's two arms, before the space-group
decisions and the merge in the adopted group, without waiting for anything. A pass the walk is going
to replace stops there instead of finishing the search. (Waiting for the probes before the search was
measured and dropped: it cost 0.2-3.6 s on sets where the walk took no round.)
Exact by construction (only a pass that is thrown away is cut short); p.hkl and MTZ identical on the
ci tier and 49 extra sets. Fired on one geometry-walk set, alternated runs 33.8 -> 31.6 s.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3