The first pass of two-pass rotation indexing was the slow "getting there" phase.
Profiling RunIndexing showed the cost is the serial Ceres candidate refinement
(up to 4 FFT candidate cells x 2 XtalOptimizer solves each), not spot finding or
the cuFFT kernel.
- XtalOptimizer: add a num_threads arg (default 1) so a caller running a few
refinements concurrently can give each several cores.
- RotationIndexer::RunIndexing: seed the candidates serially, run their up-to-8
XtalOptimizer solves concurrently (4 Ceres threads each), select serially in
candidate order (identical result). ~10-13x on the candidate loop.
- Rugnux: feed both first-pass schemes (spread + wedge) serially, then run their
RunIndexing() passes in parallel - overlapping one scheme's cuFFT with the
other's Ceres. Size the FFT indexer pool to 2 for the rotation path (it fires
only twice) instead of the default 4, avoiding wasted cuFFT-plan init.
First-pass wall 2.1-6.5x faster across a 12-crystal battery; whole lysoC run
17.7->10.5s. All 12 crystals bit-identical (scheme, validation count, space group).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>