Commit Graph
1532 Commits
Author SHA1 Message Date
leonarski_fandClaude Opus 5.5 bdb5aaa44b Indexing algorithm FFT without a GPU resolves to None: live indexing is off instead of the collection cancelled
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>
2026-10-14 13:14:25 +02:00
leonarski_fandClaude Opus 5.5 d0c9ee0410 Merge nogpu into rc175: rugnux --no-gpu keeps the process off the GPU; -X FFT is the algorithm (cuFFT on a GPU, FFTW otherwise), -X cuFFT the GPU implementation
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 13:07:08 +02:00
leonarski_fandClaude Opus 5.5 3d48b76b49 Merge stillsmerge into rc175: stills merge 2.3-2.5x faster (exact); scaling reference and tilt refinement over the merged crystals only; tilt post-refinement solved directly
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 12:46:11 +02:00
leonarski_fandClaude Opus 5.5 3c4d730f89 RESULT-CHANGING (owner decision): stills tilt post-refinement solved directly instead of by Ceres
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>
2026-10-14 11:59:09 +02:00
leonarski_fandClaude Opus 5.5 4936f9b408 RESULT-CHANGING (owner decision): stills scaling reference and tilt refinement only over the crystals the merge keeps
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>
2026-10-14 11:32:51 +02:00
leonarski_fandClaude Opus 5.5 593590b357 rugnux -X FFT is the algorithm: cuFFT on a GPU, FFTW without one; -X cuFFT names the GPU implementation
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>
2026-10-14 10:13:23 +02:00
leonarski_fandClaude Opus 5.5 bfd715b64d rugnux --no-gpu: run on the CPU only, without touching CUDA, on a machine with a GPU
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>
2026-10-14 10:13:23 +02:00
leonarski_fandClaude Opus 5.5 34d5bb5631 stills merge 2.3-2.5x faster, output unchanged: parallel observation passes, hash accumulator, no amplitudes on scaling references
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>
2026-10-14 10:09:23 +02:00
leonarski_fandClaude Opus 5.5 70276a69fe Merge stillskhz exact commit into rc175: stills image loop 3.6-3.9x faster, output unchanged
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 00:36:05 +02:00
leonarski_fandClaude Opus 5.5 9849154081 stills image loop 3.6-3.9x faster, output unchanged: indexer pool, candidate post-processing, peak search, per-frame indexer memo
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
2026-10-13 23:58:46 +02:00
leonarski_fandClaude Opus 5.5 6e9b891eb4 MR score-engine cache and merge buffers: no hang, no doubled wait; near-tie doc follows offline decide-late
Build Packages / Create release (push) Successful in 8s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 3m24s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 9m14s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 14m43s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 19m8s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 19m30s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m44s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 22m47s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 24m6s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 25m47s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 27m47s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 19m48s
Build Packages / Generate python client (push) Successful in 30s
Build Packages / Build documentation (push) Successful in 1m9s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 21m10s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 22m34s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 23m52s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 18m52s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 23m41s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 19m32s
Build Packages / Unit tests (push) Successful in 1h25m53s
Build Packages / Validation Battery (push) Successful in 31m9s
- 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>
2026-10-13 21:25:15 +02:00
leonarski_fandClaude Opus 5.5 ea597507df battery: workstation reference is the rc175 full battery at a45135926
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>
2026-10-13 21:17:39 +02:00
leonarski_fandClaude Opus 5.5 7ff2a55d65 Merge branch 'zmq-close-30s' into rc175: receivers wait 30 s (was 10 s) for the pusher to finalise its sends at the end of a collection
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 19:57:33 +02:00
leonarski_fandClaude Opus 5.5 186c78f56a receiver: allow 30 s instead of 10 s for the image pusher to return its buffers after END
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
2026-10-13 19:57:15 +02:00
leonarski_fandClaude Opus 5.5 a45135926c Merge branch 'pass-levers' into rc175: rotation-scale walk early stop; a replaced canonical pass stops after its search arms; the measured-centre beam-centre arm tries a block-strided third of the pre-pass first
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 19:43:02 +02:00
leonarski_fandClaude Opus 5.5 94a51ea1ae rugnux: the beam-centre arm at the measured centre integrates a block-strided third of the pre-pass first
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
2026-10-13 19:42:33 +02:00
leonarski_fandClaude Opus 5.5 7bef8d743c rugnux: a canonical pass the geometry walk replaces stops after its space-group search arms
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
2026-10-13 19:42:25 +02:00
leonarski_fandClaude Opus 5.5 8fa5b14bc0 rugnux: the rotation-scale walk stops where the next step would move the sweep end by under half a frame
Each refit reads a share of the error left, so the walk's steps contract by about the ratio of the
last two. Where the step that ratio predicts moves the end of the sweep by less than half a frame,
the refit (an integration of the pre-pass frames and a post-refinement) and the indexing probe after
it are not made; the scale the validation frames last preferred is adopted, as before.

Measured on the walk sets of the battery against rc175 edde4439c: p.hkl and MTZ data identical; it
fired on one open set (2 refits instead of 3, same adopted scale), about -7 s there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 19:42:25 +02:00
leonarski_fandClaude Opus 5.5 edde4439c0 Merge branch 'gpu-oom' into rc175: merge helpers wait for GPU work beside before failing; MR score-engine cache bounded per card; scaling engine released before MR on the predicted-model path
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 17:49:40 +02:00
leonarski_fandClaude Opus 5.5 1e42885f82 Merge branch 'offline-decide-late' into rc175: offline runs decide first-pass near ties on all frames (no re-check copies, no redo from them); live keeps early decisions and re-checks; --simulate-live
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 17:18:33 +02:00
leonarski_fandClaude Opus 5.5 62e64235df CI: the validation battery is a job of Build Packages, after the unit tests
Build Packages / Create release (push) Successful in 24s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 3m18s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 24m41s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 13m17s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 23m15s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 13m30s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 5m43s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 14m27s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 17m7s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 15m27s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 19m18s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 19m38s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 22m7s
Build Packages / Generate python client (push) Successful in 23s
Build Packages / Build documentation (push) Successful in 1m0s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 23m4s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 15m25s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 20m43s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 21m25s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 17m1s
Build Packages / Unit tests (push) Successful in 1h56m34s
Build Packages / Validation Battery (push) Successful in 31m53s
The unit-test job had joined the battery workflow's concurrency group to keep the two off the GPU at
the same time. A group keeps one pending run or job and cancels it for a newer one, cancel-in-progress
or not, so the battery waiting for its runner was cancelled by the same push's unit-test job, on
every push since 140739a69.

The battery is now a job in build_and_test.yml that needs the unit tests and runs whatever their
outcome, so within one run the two take turns; no concurrency group, so a merge to main always gets
both, and a manual release (create_release) runs neither. A manual subset or the full battery is a
Run workflow with battery_subset (empty = full). Two pushes close together can still overlap on the
GPU; a runner with capacity 1 for the GPU jobs is the later fix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 16:54:44 +02:00
leonarski_fandClaude Opus 5.5 fca28f2728 Merge branch 'erfc-partiality' into rc175: rotation partiality in the rocking-curve tail from erfc (CPU and GPU agree; last-bit change)
Validation Battery / Validation Battery (push) Canceled after 0s
Build Packages / Create release (push) Successful in 19s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 3m29s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m13s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 14m35s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 7m41s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 27m35s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 26m56s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 18m2s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 15m8s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 17m16s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 19m28s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 19m21s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 21m43s
Build Packages / Generate python client (push) Successful in 26s
Build Packages / Build documentation (push) Successful in 1m10s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 22m56s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 21m23s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 19m10s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 23m1s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 21m24s
Build Packages / Unit tests (push) Successful in 2h1m27s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 16:10:53 +02:00
leonarski_fandClaude Opus 5.5 0b86ef5dab Bragg prediction: rotation partiality in the tail from erfc, the same on CPU and GPU
Last-bit change, not bit-exact: every partial whose frame does not contain its exact diffracting
condition gets a partiality - and so a slice-centroid walk along its Debye ring - that differs in
the last bits.

The partiality was (erf(b) - erf(a)) / 2 in float. Where the frame's slice lies in one tail of the
rocking curve both erf are near +-1, and their difference keeps only a few ulps of 1: glibc erf and
CUDA erff, one ulp apart, gave partialities ~1e-3 apart (relative) at p ~ 3e-5. The slice centroid
(RockingSliceCentroid_rad) divides by that partiality, and at zeta ~ 0.05 the walk it scales is
hundreds of pixels, so CPU and GPU placed such a reflection up to 0.1 px apart -
BraggPrediction_CPU_GPU_consistency_large_cell failed on one of them once --fmad=false (7ed970503)
moved the ulps. A slice in one tail now takes the same difference from erfcf, which keeps its relative
precision there; one helper (RockingSlicePartiality) serves both predictors.

Reflections placed more than 0.01 px apart by CPU and GPU in that test: 65 -> 0. BraggPrediction*,
BraggPredictionRot*, BraggPredictionRotGPU*, RockingSlice* pass. 7qij image passes 5.68/7.90 s ->
5.26/7.50 s (one lock hold; no cost).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 16:10:38 +02:00
leonarski_fandClaude Opus 5.5 7c622f9e7f rugnux: the scaling engine's card memory goes back before molecular replacement
The rotation merge engine is released before model validation decides whether its GPU path fits,
but a predicted model (or --mr) skips that validation and went into molecular replacement with the
engine still on the card - about 10 GB on the AF 9v16 run, which then failed with 0.0 GB free on
some runs and on the others got fewer rigid-body engines. It is now released before the search
too, on the same condition (no finalist-ledger merge still running on it). Card peak of that run
15.75 -> 14.05 GB; MR keys identical.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 16:07:33 +02:00
leonarski_fandClaude Opus 5.5 df14a1e631 molecular replacement: the score engines keep within a quarter of the card together
Molecular replacement's structure-factor score engines were made one per group, copy count and
resolution, each admitted against a quarter of the card's total, and all kept to the end of the
search. On a large cell they summed to well over the card: traced with nsys on the rotated 4nwv
model (which failed on every run, base included), seven engines of 1.36 GB were held beside the
two rigid-body pools (5.9 GB) when a 288 MB buffer did not fit.

Each engine is still admitted, or the search scored on the CPU, exactly as before, on the card's
total memory. Now the engines kept on a card also stay within that quarter together: an engine
that does not fit beside them takes the place of the least recently used one no score is using,
and waits for a score to finish where all are in use; one taken out is made again when next asked
for. A score holds its engine for as long as it uses it (shared_ptr lease). Engines compute the
same numbers whenever they are made, so only when the card holds them changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 16:07:33 +02:00
leonarski_fandClaude Opus 5.5 b6f34db258 rotation merge: helper buffers wait for the probe pass beside them, as the engine's own do
The intermittent "out of GPU memory" on the 3600-frame cbf.gz set (about one run in three on a 16 GB
card, base included) was caught with allocation backtraces: FitErrorModelGPU's 42.8 MB in
MergeAndStats failed while the speculative geometry probe (GPUWorkBeside) held its FFT indexers on
the same card. The engine's own arrays (RotationScaleMergeGPU Impl::Alloc) wait for such work to end
and ask again; the merge's helpers - ErrorModelGPU, WilsonOutliersGPU, PairSortGPU and the cub
scratch of the engine itself - allocated directly and failed at once.

CudaAlloc::SynchronousAfterWorkBeside: Synchronous, and where the card is full after the pool is
trimmed, it waits for the GPU work beside the main line (wait_for_gpu_work_beside, 10 min as for
the engine) and asks once more. The four merge files use it. Only the probe passes register
GPUWorkBeside and they index only, so none of them merges and nothing waits for itself. When a
buffer is allocated is all that changes; what is computed is not.

Catch2 CudaDevicePtr_AfterWorkBesideWaitsForItsMemory: with the card filled by work beside, a
Synchronous buffer fails and a SynchronousAfterWorkBeside one gets its memory once the work ends.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 16:07:33 +02:00
leonarski_fandClaude Opus 5.5 7d64a62872 Merge branch 'plots-disposition' into rc175: <prefix>_plot.txt gains a per-frame disposition column (1 merged, 0.5 downgraded, 0 rejected)
Validation Battery / Validation Battery (push) Canceled after 0s
Build Packages / Create release (push) Successful in 21s
Build Packages / Unit tests (push) Canceled after 0s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 3m16s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m11s
Build Packages / build:portable:windows-x86_64:cuda (push) Failing after 9m16s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 12m55s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 9m2s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 14m52s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 17m44s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 21m18s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 19m30s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 23m31s
Build Packages / Generate python client (push) Successful in 31s
Build Packages / Build documentation (push) Successful in 1m8s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 23m42s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 26m45s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 29m4s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 28m4s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 27m39s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 22m45s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 28m13s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 15:44:34 +02:00
leonarski_fandClaude Opus 5.5 913dabed92 Merge branch 'model-subgroup' into rc175: --model tests the twin hypothesis merged in the model's own subgroup (p_mr only); a screw one axial reflection could not confirm is reported open
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 15:44:10 +02:00
leonarski_fandClaude Opus 5.5 5901c75547 rugnux: _plot.txt gets a graded per-frame disposition column
A new last column, "disposition", gives each frame's merge disposition as
a number to plot directly: 1 merged, 0.5 downgraded, 0 rejected; nan for
a frame that never got a scale (never reached the merge) and on a run
that merged nothing, as the file writes every value nothing measured.
Existing columns keep their indices.

0.5 is a marker, not a weight. "Downgraded" is a label applied after the
merge (RotationScaleMerge::CountSweepDisposition marks the merged frames
inside a flagged sweep-quality range); the merge applies no factor of its
own to them - they are carried at whatever weight their own scale and
sigmas give - so there is no fixed or per-frame reduction factor that
could honestly be written instead.

Writer-only change: on three open-arm sets with downgraded and rejected
frames p.hkl is identical to the same commit without it, the first ten
columns are unchanged, and the column's 1/0.5/0 counts equal the report's
merged/downgraded/rejected counts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 15:44:04 +02:00
leonarski_fandClaude Opus 5.5 e80735a261 Merge branch 'review-fixes-rc175' into rc175: review fixes (quality guard serial on one card, DeviceBytes as measured, CBF plain stores, GPU test guards), docs brought up to the code, GPU_DATA_ANALYSIS, acknowledgements, CHANGELOG
Validation Battery / Validation Battery (push) Canceled after 0s
Build Packages / Unit tests (push) Canceled after 0s
Build Packages / Create release (push) Successful in 21s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 3m13s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 26m28s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 12m56s
Build Packages / build:portable:windows-x86_64:cuda (push) Failing after 7m35s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 17m6s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 8m21s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 20m33s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 18m25s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 18m4s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 22m34s
Build Packages / Generate python client (push) Successful in 33s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 22m51s
Build Packages / Build documentation (push) Successful in 1m55s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 27m22s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 28m8s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 30m11s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 22m54s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 30m51s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 24m50s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 15:31:56 +02:00
leonarski_fandClaude Opus 5.5 563ad27af6 battery: workstation reference is the rc175 full battery at f853596
Validation Battery / Validation Battery (push) Canceled after 0s
Build Packages / Unit tests (push) Canceled after 0s
Build Packages / Create release (push) Successful in 21s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 3m26s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 12m18s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m1s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 26m20s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 7m33s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 15m9s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 28m8s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 16m39s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 16m6s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 19m20s
Build Packages / Generate python client (push) Successful in 27s
Build Packages / Build documentation (push) Successful in 1m24s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 19m41s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 26m9s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 27m16s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 24m33s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 28m29s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 30m40s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 27m4s
Replaces the kept 57fef1c run with 20261013-0557_f85359_rc175-f853596-full (open + inhouse; the
private arm is never kept).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 15:31:44 +02:00
leonarski_fandClaude Opus 5.5 da87160b07 Merge branch 'fix-mr-segfault' into rc175: an atom whose density at the starting radius is exactly the cutoff no longer gets a NaN radius (MR model-density overflow)
Validation Battery / Validation Battery (push) Canceled after 0s
Build Packages / Create release (push) Successful in 6s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 3m26s
Build Packages / Unit tests (push) Canceled after 0s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 8m16s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 12m20s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 16m29s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 17m53s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m25s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 22m3s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 22m35s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 25m30s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 26m0s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 19m23s
Build Packages / Generate python client (push) Successful in 27s
Build Packages / Build documentation (push) Successful in 1m8s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 20m46s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 23m2s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 22m40s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 21m39s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 26m20s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 25m4s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 15:25:51 +02:00
leonarski_fandClaude Opus 5.5 e5b69647c5 Space group: a screw one axial reflection could not confirm is reported open
A screw candidate whose absent class no reflection contradicts, refused only because its
evidence could not reach min_screw_absence_evidence even with every predicted-absent axial
reflection at the floor (one such reflection reaches 6.9 nats; a row without a control class
reaches nothing), left the symmorphic group adopted and the report saying the group follows from
the absences. Such a row is now SPACE_GROUP_SCREW_UNDETERMINED, with the screw group in
SPACE_GROUP_ALTERNATIVES. Report only: the result is kept apart (unreached_screws) and folded in
after every decision, so the written group and files do not change.

Measured on a monoclinic set whose merge holds five 0k0 reflections at noise level, one of them
predicted absent: P2 was reported as decided by the absences against a deposited P21; it is now
P2 with the b screw open and P21 offered. Over the full battery's candidate tables the condition
holds on that set and one private set (the same P2/P21 case) only. ci tier: no change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 14:43:44 +02:00
leonarski_fandClaude Opus 5.5 52136a74f0 --model: the twin hypothesis merged in the model's own subgroup
Where the model's group, carried onto the data's axes (CellMappingOperators), is an index-2
subgroup of the adopted group, the merge the model is asked about the twin hypothesis on is the
one in that subgroup: a merge in another subgroup averages reflections the model's symmetry does
not relate. Where the data's own twin hypothesis (p_twin_*) is another subgroup, the model's merge
is made beside it, for the model only, and written - as <prefix>_mr - only where the model is
placed there and fits it better (the existing rule); p_twin_* and every data file are untouched.
A p_mr made from a subgroup merge carries the model's own group where it has the subgroup's
rotations (its screws and hand, unless the anomalous map at its atoms is inverted), without the
reflections that group extinguishes.

Measured: a hexagonal set whose deposited model is P3121 adopted P622 on twin-like data; its
data hypothesis is P6, and the model was asked about P6 and chose it (p_mr P6). Now it is asked
about P321 (twin R-free 0.296 at a fraction 0.45, answer's merge 0.398) and p_mr is P3121, the
deposited group. ci tier 28/28 and 14 named twin/screw sets: p.hkl, p.mtz data and p_twin data
identical; no other report key changed. AF models via --model (8 sets): decisions identical.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 14:43:44 +02:00
leonarski_fandClaude Opus 5.5 29e4887efa gemmi dencalc: an atom whose density at the starting radius is exactly the cutoff gets a radius, not NaN
determine_cutoff_radius() starts at the IT92 approximation of the radius. When the density there is
exactly the float cutoff (1e-5f) - which happens for a few B per element, e.g. a nitrogen at
B + blur = 277.64563 - neither loop runs, x1 == x2, and the interpolation returns 0/0 = NaN. Upstream
gemmi master has the same code.

A NaN radius cast to int gave MakeAtomBox() INT_MIN half-widths, so UseAtomBoxesByPlane() sized its
point buffer from a wrapped product (one point) and then wrote past it: the heap overflow (SIGSEGV, or
a crash later in FFTW) that killed molecular replacement after the rigid body of a large deposited
model, turned to imitate another crystal form, had failed (2 of 73476 atoms, in the CPU rigid body of
an MR placement). The GPU density path took the same NaN radius into its box sizes.

The outward search now steps at least once, so that case returns the point at the cutoff. Every other
B gives the same radius bit for bit (checked over ten elements, every 8th float B in 2-600 A^2: 3 NaN
before, none after, no finite radius changed).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 14:28:51 +02:00
leonarski_fandClaude Opus 5.5 53c81dc5df CHANGELOG: rc.175 gains a jfjoch_writer line for the writer-end-catch work
Bounded memory and efficient closing of old data files on long runs, a failure ending the run in
an error state the broker hears about, and the pusher's log line when a stalled writer makes it
drop images; the master-file ordering moves here from the Fixes line. Owner-requested edit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 13:30:21 +02:00
leonarski_f 7b4dcce480 Merge rc175 into review-fixes-rc175 2026-10-13 13:30:00 +02:00
leonarski_fandClaude Opus 5.5 9da3344639 ZeroMQ puller: the message region without a ring takes the message's read-only bytes
Validation Battery / Validation Battery (push) Canceled after 0s
Build Packages / Create release (push) Successful in 7s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 3m37s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 9m31s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 14m16s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 19m12s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 19m31s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m54s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 23m44s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 24m54s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 26m5s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 26m42s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 19m29s
Build Packages / Generate python client (push) Successful in 25s
Build Packages / Build documentation (push) Successful in 1m8s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 20m43s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 22m56s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 23m45s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 18m33s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 22m46s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 20m11s
Build Packages / Unit tests (push) Failing after 2h15m46s
ZMQMessage::data() returns const uint8_t *, and GCC refuses the implicit conversion to the
region's uint8_t *. In this mode the region is only read (deserialised and republished), never
written, so the cast is safe.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 13:17:48 +02:00
leonarski_fandClaude Opus 5.5 ecdfd16033 Merge branch 'writer-end-catch' into rc175: writer ends any failure in error state; received images in a byte-bounded ring; CloseOldFiles walks open files only; ZeroMQ pusher logs the switch to dropping
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 13:00:55 +02:00
leonarski_fandClaude Fable 5.1 a5b4529f9c ZeroMQ pusher: say so when a stalled writer switches the socket to dropping
A send that blocked for over a second drops that image and turns the socket non-blocking for
the rest of the run, so every later image that finds the queue full is dropped too - without a
word in the log. Log the moment it happens, once per run, with the image number.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 12:51:34 +02:00
leonarski_fandClaude Fable 5.1 6202d36268 Writer: CloseOldFiles walks the open data files, not every file slot of the run
The slots of closed files stay in `files` (empty) for the whole run, and CloseOldFiles visited
all of them on every image: a thousand slots per image for a million-image run. Keep the open
file numbers in an ordered set and walk that; being ordered, the walk ends at the first file
that is not old enough to close.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 12:51:34 +02:00
leonarski_fandClaude Fable 5.1 1be6093429 Writer: received images live in a byte-bounded ring buffer until written and republished
Every stage between the socket and the HDF5 write was bounded by a message count and never by
bytes: ZeroMQ receive watermark 100, cbor_fifo 200, outside_fifo 1000, repub_fifo 200 - up to
some 1500 images in flight per writer process, each a whole compressed image, so 6-12 GB for a
JUNGFRAU 4M and 30-60 GB for a 9M, and the writer's memory was a quantity nobody had chosen.

ReceiveRingBuffer is a fixed ring of bytes owned by the ImagePuller base class, so the ZeroMQ
and TCP pullers share it. A message is received straight into it (TCP reads the payload into
the ring; ZeroMQ copies the message in) and travels as a shared_ptr region in ImagePullerOutput;
the deserialised image points into that region, republish sends it as is, and the bytes come
back when the last holder lets go. Space is reclaimed in arrival order: a region released out
of turn - republish finishes with messages in its own order - is marked and waits for the ones
in front of it. Because of that, what republish may hold is capped at a quarter of the ring,
and an image over the cap is not republished, exactly as one that finds the republish queue
full; otherwise a stalled republish consumer would pin the ring and slow the writer to its
rate. A message larger than the whole ring (a START or END of a very long run) goes on the heap,
so it cannot wedge the receiver.

A full ring stalls the receive thread, and what that does depends on the stream. On TCP the
pusher waits behind the writer's BUSY heartbeats and gives up only after a minute in which no
image at all was written. On ZeroMQ the pusher's send blocks for one second, then drops the
image and sends without waiting for the rest of the run - so on ZeroMQ a ring that is smaller
than the writer's stalls costs images, where the old 1500-message queues absorbed them. That
is the trade: the budget is now a number, and it has to be chosen. jfjoch_writer sets it with
-m/--rcv_ring_mb, default 2048 MB per writer process; the ZeroMQ receive watermark default goes
from 100 to 10 messages, since it now only counts what sits beyond the ring. The ring's peak
use is in the end-of-run log line, and docs/JFJOCH_WRITER.md has a Memory section with the
sizing rule.

The puller's own default is no ring at all, which keeps every other user as it was: the
broker's DECTRIS stream2 puller (JFJochServices, JFJochReceiverService) stays zero-copy with
the ZeroMQ message as the buffer, and the broker's direct HDF5 writer (HDF5FilePusher ->
FileWriter) never goes through a puller. The puller threads now also catch std::exception, as
the writer does since the previous commit: deserialising the END message of a million-image
run allocates a vector per per-image array, and a bad_alloc leaving the thread ended the process.

Tests: ReceiveRingBufferTest (reuse, wrap-around, full ring, out-of-order release, heap
fallback, zero size, stop, a region outliving its ring); ZMQImageCommTest_SmallRingBuffer (64
one-megabyte images through a three-image ring, nothing lost, peak within the bound); one of
the two writers in TCPImageCommTest_2Writers_WithAck runs on a three-image ring.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 12:51:34 +02:00
leonarski_fandClaude Fable 5.1 5f6f257998 Writer: a failure that is not a JFJochException ends the run in error state too
ProcessDataImage, ProcessEndMessage and NotifyReceiverOnFinalizedWrite caught only
JFJochException, while ProcessStartMessage and FinalizeDataCollection already caught
std::exception. Anything else - std::bad_alloc from a per-image array, filesystem_error from
a path check, the JSON library refusing a string that is not UTF-8 while publishing a
file-done notification - left CollectImages, and Run() logged "Exception not properly handled"
and returned with the state never reaching Finalized: no END acknowledgement, no writer
notification, the master's .tmp left behind until the next START. On the TCP image stream the
broker then waited for the END acknowledgement indefinitely, because the writer's heartbeat
thread keeps reporting it alive whatever state it is in.

Now the same catch as at the other sites: error state, fatal ACK, END processed, Finalized.
The new StreamWriter test provokes the JSON case through a sample name that is not UTF-8 with
the file-done socket enabled, and checks the run ends Finalized with no master written.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-13 12:27:08 +02:00
leonarski_fandClaude Opus 5.5 23da53d2c1 Merge branch 'model-doc' into rc175: a page on model and reference-data options and which decisions they can touch
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 09:05:54 +02:00
leonarski_fandClaude Opus 5.5 8b43cce9eb docs: Models and reference data - what a model or reference can and cannot change
A new page, RUGNUX_MODEL_AND_REFERENCE.md, beside the other rugnux pages in the toctree and
linked from RUGNUX.md and the --model section of RUGNUX_ADVANCED.md. It lists every option that
brings in a model or reference data (--model, --mr, the PAE file beside a prediction, -z and
--reference-column, -S, -C, --force-rotation-lattice, --report-resolution), sorts every decision
into data-only, model-informed label in <prefix>.mtz (hand past the anomalous veto, indexing
ambiguity, setting), model-driven separate files (<prefix>_mr) and report-only statistics, walks a
--model run step by step, says exactly where a model can touch the space group and what guards it
(the null, the anomalous veto, the indexing margin, the MR rules, the wrong-model battery step),
and describes the free set and model bias (a deposited model's R-free on rugnux's own test set is
not cross-validated; -z with the PDB's structure factors inherits the depositor's set). Includes the
one pre-merge use of a model: serial stills with -C/-S, where it breaks the per-image indexing
ambiguity. Documentation only; behaviour as merged in rc175 (7ed970503).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 09:05:18 +02:00
leonarski_fandClaude Opus 5.5 402b3d1293 ACKNOWLEDGEMENT: autoPROC and Clemens Vonrhein; DOIs verified; DISTL; tNCS credit in MR
- All 84 DOIs checked (82 on Crossref, 2 DataCite): title, journal, volume, pages and first author
  match; Matthews 1968 and Yeates 1997 links percent-encode their parentheses; the MOSFLM link
  points at a page that answers.
- autoPROC: an explicit acknowledgement of Clemens Vonrhein (Global Phasing) for the principle that a
  pipeline should verify the metadata it is given - beam centre, geometry, goniometer - and correct
  it where it is wrong; Vonrhein et al. (2011) Acta Cryst. D67, 293-302 (DOI checked).
- Zhang, Sauter et al. (2006) DISTL, already credited in SpotFindingSettings.h, gets its entry.
- Molecular replacement: tNCS looked for in the native Patterson as Read, Adams & McCoy do; the same
  credit at DetectTranslationalNcs (comment only).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 09:03:32 +02:00
leonarski_fandClaude Opus 5.5 1bd251cdd1 docs: GPU_DATA_ANALYSIS, every GPU engine of the crystallographic pipeline
One page listing the CUDA engines by pipeline stage - decode and preprocessing, per-image analysis
(azimuthal and ROI integration, spot finders, extractor), pre-scan accumulators, indexing,
prediction and integration, the scaling engine, model validation and molecular replacement - each
with what it computes, how closely it agrees with the CPU, and a link to the section describing the
method. Added to the toctree after CPU_DATA_ANALYSIS_DECISIONS.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 09:03:31 +02:00
leonarski_fandClaude Opus 5.5 30cf5856d4 docs: bring the rugnux and analysis pages up to the code (MR fallback, GPU decode, live runs)
- CPU_DATA_ANALYSIS_IMAGE §0: CBF byte-offset and marCCD widening are decoded on the card; the host
  decoders are the CPU build's path.
- CPU_DATA_ANALYSIS_DECISIONS §14: the null runs for every model the rigid body is tried on; the
  device choice is made once on the card's total (structure factors and rigid body a quarter, scale
  fits an eighth) and a CUDA failure ends the validation, deciding nothing; a subgroup the model fits
  is written as <prefix>_mr; new §14.10 on molecular replacement (model kind, rotation and
  translation functions, likelihood, Matthews, tNCS and P1 rules, null, what it may change).
- CPU_DATA_ANALYSIS: scope and references gain molecular replacement.
- CPU_DATA_ANALYSIS_INDEXING: the near tie decided early and re-checked once every frame is in.
- RUGNUX: merging is the default and _process.h5 optional; live following of a collection with a
  broker URL; --model's MR fallback.
- RUGNUX_INSTALL, RUGNUX_OVERVIEW, RUGNUX_TUTORIAL, RUGNUX_REPORT: GPU work list, <prefix>_mr outputs,
  MR in --mode mx for rotation and stills, rigid body only in --mode scale.
- RUGNUX_ADVANCED: -N default as the usage states it.
- battery README: the ci tier has 28 sets.
- ModelValidation.cpp: a comment still said a GPU failure reran the validation on the CPU (comment only).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 09:03:31 +02:00
leonarski_fandClaude Opus 5.5 ec57236ae5 rugnux: offline, the first passes decide their near ties on all the frames from the start
A stored sweep is all there when the run starts, so a first pass now decides a near tie on its
validation frames and the later frames together at once, instead of deciding on the validation frames,
re-checking the pass after the run and making the run again where the re-check ends elsewhere. Live
(following a collection from the broker) keeps that early path unchanged: ProcessConfig::live.

The pre-pass decisions - the rotation-scale walk's refits, the demoted metric-symmetry arm, the
integration radius - stay on the pre-pass frames offline too, with their near-tie re-checks after the
run; only the run made again decides them on all the frames (decide_all_late_, split from decide_late_).

--simulate-live <ms> reads a stored sweep as the collection it was while being written - image i
readable (i+1)*ms after the run starts, 0 = all at once - and takes the live path, so that path can be
tested and run in the battery without a broker.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 08:36:35 +02:00
leonarski_fandClaude Opus 5.5 cf082b2531 rugnux: the quality guard no longer runs beside the main engine on one card
Owner decision: about a second slower is preferred over 2.5-4 GB more VRAM. With one GPU the
quality guard's merge is launched ahead again, beside the post-refinement and the main engine's
host ingest, and joined before the main engine's device ingest, as before 4158c48df/095814bae;
the per-reflection placement estimate (prepass_engine_bytes_/_reflections_) and its share in the
all-observation arm's memory check go. Kept from those commits, being independent of where the
guard sits on one card: the post-refinement gather in the fit's own task, the first merge holding
its per-frame write-back (rather than waiting) for a guard on a second card, and the guard's
numbers recorded where the pass first needs them.

Measured (RTX 5080 16 GB, base = f853596, alternated, 2 runs each; machine load ~13 on 32 cores):
  VRAM peak  7qij 14.7/15.2 -> 13.3/12.2 GB, 9v16 15.1/14.6 -> 12.3/12.8 GB
  wall       7qij 47.1/39.0 -> 44.1/42.5 s,  9v16 40.3/37.4 -> 36.8/38.2 s (within the noise)
  guard merge alone 4.4-4.7 -> 1.8-1.9 s (7qij), 3.7-3.8 -> 1.6 s (9v16)
p.hkl identical on both.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 08:27:36 +02:00