v1.0.0-rc.176 #86

Merged
leonarski_f merged 105 commits from rc176 into main 2026-10-19 07:03:29 +02:00
105 Commits
Author SHA1 Message Date
leonarski_fandClaude Opus 5.5 b95c6a5783 battery results: rc176 bc191bb80 - full batteries (workstation with REFMAC, server) and the timing benchmark on nine platforms
Build Packages / Create release (push) Successful in 8s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m6s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 11m5s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 15m47s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 21m8s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m55s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 22m42s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 26m25s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 27m20s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 28m50s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 22m18s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 33m57s
Build Packages / Generate python client (push) Successful in 23s
Build Packages / Build documentation (push) Successful in 1m16s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 25m13s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 28m1s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 26m27s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 21m43s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 27m30s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 21m55s
Build Packages / Unit tests (push) Successful in 1h34m31s
Build Packages / Validation Battery (push) Successful in 33m33s
- results/workstation/20261018-1828_bc191b_rc176-bc191bb-full-refmac: 254 sets, 244 pass / 5 fail / 5 unscored,
  REFMAC R-free ratio median 1.0009.
- results/server/20261018-2052_bc191b_srv-rc176-bc191b-full (4x T4): 244 / 5 / 5; against rc175 3r6o and 8c3e
  improve, no new failure.
- results/timing_platforms/rugnux_timing_platforms_bc191b.md (from platforms_rc176.json): the standard benchmark
  on the workstation (GPU and --no-gpu), the 4x T4 server, H200 / 2x L4 / 1x L4 / 4x A100 cluster nodes, a
  DGX Spark, a Windows laptop and a Mac mini (Metal and --no-gpu); every check passes everywhere.
- timing_table.py takes its sets from the benchmark the platforms ran (standard or inhouse); the rc175 report
  regenerates unchanged but for the sentence naming the benchmark.
- README: the workstation's measured times are rc176's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-19 03:45:01 +02:00
leonarski_fandClaude Opus 5.5 43928ec490 Build information: compiler and CUDA version in rugnux's log and banner, the broker's and writer's start, the viewer's About
GitInfo gains jfjoch_build_compiler() (CMake's compiler id and version), jfjoch_build_cuda() (the CUDA
toolkit version, empty without CUDA) and jfjoch_build_info(with_cuda), one line of compiler, CUDA and flags,
stamped like the git hash. rugnux logs it as "Build:" (beside the "Build flags:" line the timing table
reads) and prints it under its version; jfjoch_broker and jfjoch_writer (without CUDA, which it does not
use) log it at start; jfjoch_viewer shows it in About. Nothing is written into the output files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-19 03:18:23 +02:00
leonarski_fandClaude Opus 5.5 197ddef6a7 MolecularReplacement: the rotation and translation functions are sized for the GPU only when the search is on it
On a CUDA build with no usable GPU (none visible to the process) molecular replacement stopped with "CUDA
(GPU) error (Failed to read the CUDA device)": each rotation and translation function call worked out its
GPU working bytes before asking whether the search runs on the GPU at all, and sizing the translation
function's cuFFT plan asks the CUDA runtime for the current device. The bytes are now taken only where
the search is on the GPU. With CUDA_VISIBLE_DEVICES="" a --mr run now ends as with the GPU visible
(placed by MR, AGREES); output unchanged where a GPU is used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-19 03:14:54 +02:00
leonarski_fandClaude Opus 5.5 eb5605c820 Merge calib-prefix into rc176: a calibration run clears the previous collection's file prefix
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 19:58:33 +02:00
leonarski_fandClaude Opus 5.5 7601cd6a75 broker: clear file_prefix for the calibration sequence, so status no longer shows the previous run's prefix
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 19:56:43 +02:00
leonarski_fandClaude Opus 5.5 da9a7349d4 docs: rugnux is tested on Arm (DGX Spark) and on Apple Silicon
The install and release pages said the aarch64 build was not exercised on Arm hardware. rc176's timing
benchmark passes on a DGX Spark (GB10) with a native build of the same source, and on an Apple M-series
Mac mini on Metal and CPU-only; the pages now say so, keeping that the aarch64 archive itself is
cross-compiled in CI. The macOS row also names the model density among the Metal engines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 19:35:59 +02:00
leonarski_fandClaude Opus 5.5 bc191bb800 Merge cuda-density-align into rc176: the CUDA model density is the CPU walk's bit for bit (RESULT-CHANGING at the last bit on CUDA builds)
Build Packages / Create release (push) Successful in 8s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 4m46s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 10m38s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 16m6s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 20m58s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 21m38s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m49s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 26m26s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 26m37s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 27m53s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 31m46s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 23m17s
Build Packages / Generate python client (push) Successful in 35s
Build Packages / Build documentation (push) Successful in 1m20s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 27m31s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 27m38s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 22m28s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 29m29s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 28m15s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 23m53s
Build Packages / Unit tests (push) Successful in 1h33m58s
Build Packages / Validation Battery (push) Successful in 34m26s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 18:10:52 +02:00
leonarski_fandClaude Opus 5.5 b6392a3f58 RESULT-CHANGING (ulp level, CUDA builds): the CUDA model density is the CPU walk's, bit for bit
ModelDensityGPU's gather had arithmetic of its own: per-thread offsets in Cartesian, the atom's
nearest image to the brick, r2 = x x + y y + z z, the radius squared in float. It now does what
UseDensityBoxesByPlane + AtomDensity (ModelGrid.cpp) and ModelDensityMetal.metal do, in their order:
- each brick's atoms carry their box (c - d, wrapped start, 2 d); a point is in the box by a
  subtraction, and f = centre - (u, v, w) in the box's own unwrapped index;
- x = m00 fx + (m01 fy + m02 fz), y = m11 fy + m12 fz, z = m22 fz with gemmi's orth_n
  (orth * (1/n), as Grid sets it) in float; r2 = (y y + z z) + x x against max_r2 = float(r^2)
  rounded from the double radius (new ModelDensityAtom field);
- gemmi's expapprox, the five Gaussians in order, atoms in model order per point. Device code is
  already compiled with --fmad=false (top-level CMakeLists), so nothing is contracted.
- The centres are no longer wrapped into the cell (ModelStructureFactorsGPU, RigidBodyGPU's
  place_kernel): float(fractional * n), as MakeDensityBox holds them; the mask keeps wrapped
  fractional coordinates. The 'narrow' nearest-image branch is gone: Supports() already leaves
  only boxes narrower than the cell.
The target is PutModelDensityOnGrid() with no orbits to symmetrize (one copy of the content, which
is what the gather grids; symmetry stays composed in reciprocal space). A rounding tie in the box
centre c (float on the GPU, double on the CPU) changes no point: either box covers the sphere with
half a spacing to spare. New ModelDensityGPU_BitIdenticalToCPU ([ModelValidation][gpu]) memcmps the
grids: triclinic P1 and P 21 21 21 cells, 3000 atoms over -0.5..1.5 of the cell, a quarter
anisotropic - 0 points differ.

RTX 5080, alternating base (8d199cceb) / this, under the GPU lock:
- gather kernel (nsys): 4nwv --model median 2.06 -> 2.55 ms, total 0.79 -> 0.97 s;
  9s02 --mr median 0.30 -> 0.37 ms, total 0.94 -> 1.17 s (+~25%, the box test and per-point
  index arithmetic).
- wall, two each: 4nwv --model 76.0/68.9 -> 74.4/71.0 s, --mr 71.3/71.1 -> 70.9/71.6 s;
  9s02 --model 25.1/22.6 -> 23.3/23.5 s, --mr 23.3/23.6 -> 23.3/23.2 s; 5reo --model
  6.2/5.5 -> 5.5/5.7 s. Model validation 4nwv 12.4/12.3 -> 12.3/11.7 s; MR 4nwv 15.4/15.2 ->
  15.4/15.1 s, 9s02 2.3/2.4 -> 2.4/2.4 s: no change beyond noise.
- outputs (each build run-to-run identical): p.hkl identical on all; R-work/R-free and every
  decision unchanged (4nwv validation 0.2982/0.2969, 9s02 MR AGREES P 21 21 2 0.2503/0.2517,
  4nwv MR UNDECIDED I 2 2 2 / I 21 21 21). Moved: MODEL_FIT_SIGMA 9s02 99.21 -> 101.68, 5reo
  103.96 -> 104.98; MR placement Z 9s02 142.1 -> 145.6, 4nwv 46.7 -> 46.2; 4nwv anomalous site
  4.01 -> 4.02 sigma; 4nwv placed model 1322/125462 lines in the last digit (2fo-fc max |d| 0.04
  rms, CC 0.99999994); 9s02/5reo models identical, maps to 2e-5 rms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 18:10:23 +02:00
leonarski_fandClaude Opus 5.5 4f8d135c2a ModelScaleGPUTest: on a model's 6 A zone the GPU and gemmi fits are held to the same R
The 6 A zone of ModelScaleGPU_MatchesGemmiOnAModelsPoints holds few points, so its B minimum is flat:
since the model density's walk moved to single precision (the Metal density series) the last bits of the
structure factors move where gemmi's Levenberg-Marquardt stops by about twice the parameters' tolerance
(b_star 1.2e-6 against 6.4e-7 on the C 1 2 1 and P 21 21 21 cells). There the two fits are compared on
their R, as ModelScaleGPU_FitMatchesGemmi does; the 3.5 A zone keeps the parameter comparison.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 17:58:01 +02:00
leonarski_fandClaude Opus 5.5 7b293bade7 Merge tail-host-k1 into rc176: the tail's serial host passes made parallel or cheaper (anisotropy sort and join, window smoothing, merge scratch, decay terms), output unchanged
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 17:49:27 +02:00
leonarski_fandClaude Opus 5.5 d882cf8d32 Merge rc176-docs into rc176: CHANGELOG for rc.176 and documentation (Metal on the MR step, MR in the overview, live rugnux tab, polarization defaults)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 17:39:19 +02:00
leonarski_fandClaude Opus 5.5 f3bf945cca docs: rc176 - molecular replacement as a pipeline step, Metal and multi-GPU MR, header polarization, Rugnux live tab
- Pipeline figures: the structure/MR box carries the Metal badge.
- RUGNUX_OVERVIEW: a model/molecular-replacement step (triggers, outputs, GPUs).
- GPU_DATA_ANALYSIS: multi-card model validation and MR; the Apple GPU's model density and MR
  rotation function. Install/release/viewer/benchmarking/report/--no-gpu texts no longer say Bragg
  integration is the only Metal engine.
- --polarization default in RUGNUX_ADVANCED and the tutorial follows the usage text (miniCBF/d*TREK header).
- WEB_FRONTEND and JFJOCH_BROKER: the Rugnux live tab; the scan result's rotation lattice.
- battery README: a --model-check run records the REFMAC version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 17:38:49 +02:00
leonarski_fandClaude Opus 5.5 6a3d52ffa3 CHANGELOG: 1.0.0-rc.176
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 17:38:49 +02:00
leonarski_fandClaude Opus 5.5 bbb341a566 RotationScaleMerge: TIMING marks inside the surfaces and the merge, decay terms built once, merge scratch not zeroed on one thread
Prototype for the tail campaign (output unchanged, checked on 4nwv --mode scale --no-gpu: same .hkl, same
.mtz data):
- rsm/s and rsm/m TIMING sub-marks for each correction surface, the disposition and each stage of
  MergeAndStats (the pieces the tail is spent in had no marks of their own).
- RefineDecay takes the terms MeasureRadiationDamageB just built (nothing between them changes corr)
  instead of building them again, and evaluates its decay factor once per term where it used it twice.
- The merge's per-group and per-full scratch (MergeFields, FullsStaging, the accumulators, the R_meas sums
  and their staging, the ISa scatter) default-initialised: each is written in full before it is read, and
  std::vector zeroed - and faulted in - gigabytes of it on one thread per P1 merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 17:26:29 +02:00
leonarski_fandClaude Opus 5.5 8c560a3316 ScaledObservations and ComputeSmoothGWindow: the anisotropy sample sorted and joined on all threads, the scaling loop's window smoothing takes each frame's log once, output unchanged
ScaledObservations sorted its sample of partials (up to 8 M) with one std::sort on the
critical path of the Analyses step, and joined the per-image lists serially: 0.97 s of
the 1.22 s the step waits for the anisotropy on a median-sized triclinic set (4x T4 box).
The sort key now carries the image and the reflection's place in it, a total order any
sort leaves the same, so ParallelSort runs it on all threads.

ComputeSmoothGWindow took log(g[j]) once per window a frame falls in, on one thread, every
round of the scaling loop: about 0.7 s on the same set. Each frame's log is taken once;
the window sums add the same values in the same order.

Output identical (p.hkl, report) on three sets, live and offline.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CphfSakjtC9c4M35wQe6SP
2026-10-18 17:26:29 +02:00
leonarski_fandClaude Opus 5.5 8d199cceba ModelValidationTest: the CPU density is gemmi's to rounding, the same on any number of threads
The density's walk now places its points in single precision (the Metal density series), so it is no
longer gemmi's bit for bit: the two gridding tests compare it to gemmi within 1e-5 relative plus four
cutoff-sized terms (a point at a radius' edge can fall on the other side), and check that 1 and 4
threads give the same grid bit for bit. The solvent mask is still compared to gemmi's bit for bit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 17:07:13 +02:00
leonarski_fandClaude Opus 5.5 0e73543a20 Merge mac-mr-metal (density series) into rc176: Metal model density, bit-identical to the CPU; the CPU density walk in single precision (RESULT-CHANGING at the last bit); autorelease-pool leak fix in the Metal rotation function
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 17:04:34 +02:00
leonarski_fandClaude Opus 5.5 868ad0c8f2 ModelGrid: the model density on the Apple GPU (Metal), bit-identical to the CPU
On macOS, where a Metal device may be used (not under --no-gpu), PutModelDensityOnGrid puts the density
on the grid with a Metal gather (ModelDensityMetal), as ModelDensityGPU.cu does under CUDA: the grid in
bricks of 4^3 points, one threadgroup each; every point adds, in model order, each atom whose box holds
it and whose radius reaches it. No float atomics. The arithmetic is the CPU walk's of the previous commit,
spelled out in ModelDensityMetal.metal - the point's place in single precision in the same order, gemmi's
float expapprox ported as is, fast math and contraction off - so the grid is the CPU's bit for bit
(ModelDensityMetal_BitIdenticalToCPU, [portable][Metal]: a triclinic cell and a P 21 21 21 one, a quarter
of the atoms anisotropic, boxes wrapping at every face). The symmetrize stays on the CPU.

- Each brick's atoms: (brick, atom) pairs counted and filled on the CPU in parallel over slabs of w,
  each worker taking the atoms in order, so the lists keep model order; at most the atoms' boxes' bricks.
- The kernel writes straight into the grid FFTW reads where its pages can be handed to the GPU (large
  vectors are page-aligned on macOS), into its own buffer and copied otherwise.
- 4^3 bricks rather than CUDA's 8^3: every point tests each atom reaching its brick, and the smaller brick
  took a half to a third of the kernel time. The box test is a subtraction against a start the host wraps.
- Every grid goes to the GPU: giving it only grids of 256k points or more, or models of 8000 atoms or
  more, left 9s02's MR 0.8 s slower.
- A box wider than the cell (an atom reaching a point through two images) stays on the CPU, whose walk
  adds both. A Metal failure puts that grid and every later one on the CPU, with one warning.
- Each call runs in an autorelease pool of its own (see the rotation function's).

Apple M6 (16 GB), alternating with the rotation-function build (the previous commit but one), two each:
- 4nwv: wall 295.8/292.6 -> 275.2/277.8 s; model validation 48.9/49.0 -> 41.0/40.9 s; MR 130.2/130.3 ->
  121.4/122.5 s (rescoring 40.9/40.3 -> 35.6/36.5 s, null 31.7/32.1 -> 26.8/26.6 s); user CPU 1998/2011 ->
  1692/1681 s; peak footprint 17.8/18.5 -> 17.2/18.6 GB. The density's CPU time, sampled over a whole run,
  316 CPU-s (16% of the busy samples) -> none, the pairs' sort 31 CPU-s.
- 9s02: wall 76.7/76.5 -> 72.9/73.1 s; MR 16.9/16.8 -> 13.5/13.4 s.
- 9zlo: wall 70.4/70.7 -> 69.8/69.6 s; MR 3.1/3.1 -> 2.2/2.2 s.
p.hkl, the placed model and the maps are byte-identical to the previous commit's (density on the CPU) on
all three, and so are the MR and validation logs but for their timings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018My7tqt8a4LyKKjP3FhScT
2026-10-18 17:04:17 +02:00
leonarski_fandClaude Opus 5.5 fe58c5041c RESULT-CHANGING (ulp level): the model density's walk places its points in single precision
PutModelDensityOnGrid walked each atom's box as gemmi does, with every point's place relative to the
atom and its test against the atom's radius in double. The Apple GPU has no double, so a Metal gather
could only come to the same grid to rounding. The density now has its own walk, UseDensityBoxesByPlane:
the same planes, atoms, box and order as before, with the point's place in single precision in one
fixed order of operations (f = centre - (u, v, w); x = m00 fx + (m01 fy + m02 fz), y = m11 fy + m12 fz,
z = m22 fz with gemmi's upper-triangular orth_n; r2 = (y y + z z) + x x, taken where not above the
radius squared). A gather that spells the same arithmetic out then finds the same points with the same
bits, and gemmi's float expapprox and the Gaussian sums are already single precision. The solvent mask
keeps the double walk: its points are 0 or 1, and a point at the edge would flip.

The density differs from gemmi's in the last bits, so everything computed from it moves at that level.
9s02 on an Apple M6 against the previous commit: p.hkl unchanged; the MR decision (AGREES, P 21 21 2)
and every R-work/R-free within 0.001; validation R(all) 0.3719 -> 0.3718; the validation rigid body
took 29 evaluations instead of 22 to the same place (rotation 0.003 deg); Z of rejected candidates in
other groups by up to 0.7; the model and maps differ in their last digits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018My7tqt8a4LyKKjP3FhScT
2026-10-18 17:04:17 +02:00
leonarski_fandClaude Opus 5.5 f74df573cf MolecularReplacementMetal: each rotation-function call in an autorelease pool of its own
The calls run on the search's worker threads, which have no autorelease pool, so each call's autoreleased
command buffers and every buffer they held lived until the thread exited: about 7 MB per call. The Bragg
engine already runs its calls in a pool. On an Apple M6 this removes a 4-5 GB rise of 4nwv's peak
footprint seen with more Metal calls, and probably the rotation-function build's unexplained +1.7 GB; a
20-call worker-thread test grew 0.41 -> 0.55 GB before and stays flat after. Output unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018My7tqt8a4LyKKjP3FhScT
2026-10-18 17:04:17 +02:00
leonarski_fandClaude Opus 5.5 369b7679db battery: record the REFMAC version of a --model-check run and of a recheck
The manifest of a run made with --model-check now carries "refmac" (REFMAC's version as its banner states
it, e.g. "REFMAC 5.8.0431, CCP4 9.0.014"), as does every "rechecks" entry, and the report lists it beside
the rugnux binary, so a REFMAC R-free can be traced to the REFMAC that computed it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 17:03:58 +02:00
leonarski_fandClaude Opus 5.5 627d1c7ed0 Revert "Merge anderson-solver into rc176": Anderson-accelerated scaling loops taken out for this release
The full battery (rc176-1bdce7e56-full) traced three changes to it, all reproduced with the anderson-solver
binary alone: an in-house insulin set lost its cubic group (I 2 3 -> I 2 2 2, the cut moving 1.63 -> 1.75 A),
a weak split myoglobin set fell in CC1/2 (0.70 -> 0.60) and R_free (0.438 -> 0.456), and 9gdj took a third
first pass (+10 s): pushed further on the measured-centre arm's poor lattice, the scaling moved both arms'
statistics so the early beam-centre comparison no longer called the loss beyond its noise. Kept on branch
anderson-solver (747a30dba) to come back to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 17:02:18 +02:00
leonarski_fandClaude Opus 5.5 1bdce7e565 Merge anderson-solver into rc176: the per-frame scaling loops are Anderson-accelerated, 3-4x fewer iterations and closer to their fixed point (RESULT-CHANGING, ISa within 0.03 on the 44-set battery)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 14:07:00 +02:00
leonarski_fandClaude Opus 5.5 dc5cca7a45 Merge mac-tiff-rm into rc176: marCCD TIFF frames read without memory-mapping (macOS page faults), output unchanged
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 13:46:16 +02:00
leonarski_fandClaude Opus 5.5 90abe8670e MarCCD: read TIFF frames without memory-mapping, output unchanged
ReadRaw opened each frame with TIFFOpen(path, "r"), which memory-maps the file; on macOS the 32 MB
frame is then paged in under libtiff's copy out of it, and that copy (DumpModeDecode's memmove) was
23% of rugnux's busy CPU samples during 4nwv's image passes. With "rm" libtiff reads the file instead:
memmove 0.9% of the samples. The pixels are the same bytes either way.

Apple M6, 4nwv (the timing benchmark's moved model), alternating runs, two each:
- wall 291.1/285.9 s -> 283.4/284.5 s; system CPU 197/172 s -> 160/160 s
- pre-scan 3.9/3.8 -> 3.4/3.3 s, indexing 8.7/7.5 -> 6.8/6.9 s, metric-symmetry check 5.0/5.1 ->
  2.9/3.0 s; the integration passes, bound by the GPU, within 0.6 s
p.hkl and the placed model are byte-identical. (Reading by strips straight into the frame on top of
this, TIFFReadEncodedStrip: 282.2/273.6 s, not clearly apart from "rm" alone.) On Linux the three
read paths measured the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018My7tqt8a4LyKKjP3FhScT
2026-10-18 13:46:16 +02:00
leonarski_fandClaude Opus 5.5 747a30dbaa RotationScaleMerge: Anderson-accelerate the per-frame scaling loops (RESULT-CHANGING)
The partials and fulls scaling loops are alternating least squares that converge linearly:
over the rc176 battery's 2527 loops a round shrinks the step by a median 0.86 (partials) and
0.93 (fulls), up to 0.98, so the 1e-3 step tolerance stopped them 0.6% / 1.4% (median) and up to
17% (fulls p90) short of their fixed point, and 313 of them were stopped as "stalled" when they
were only slow - a stalled fulls loop also skipped ShrinkToRestrained.

Each round is now accelerated in log G by type-II Anderson mixing (Walker & Ni 2011), depth 5,
each frame weighted by its observation count as in the step test; frames below the credible floor
take the plain round's scale. The history is dropped when the weighted residual grows or the set
of fitted frames changes (new AndersonMixing class, unit-tested on a synthetic slow ALS loop). The
mixed scales are gauge-pinned again before the rescale, and the stop test is unchanged: it reads
the round's residual F(x) - x, so "stalled" now means a loop that does not settle under
acceleration either.

Measured (CPU, -N 8): 4nwv 311 -> 82 iterations over 9 loops, 9v16 76 iterations over 11 loops,
the same counts as the env-gated prototype on tail-solver; no history restart fired on either.
Depth 3/5/8 give 92/91/90 iterations on 4nwv. A restart on a change of the fulls loop's
cross-validated lambda was tried and gave nothing (83 vs 82). Same space group and merging
statistics on 4nwv and 9v16.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 13:04:54 +02:00
leonarski_fandClaude Opus 5.5 00fff118bc Merge failed-perimage into rc176: a rotation run that finds no lattice still reports every image (spots, resolution, background; not indexed), live and offline
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 13:00:17 +02:00
leonarski_fandClaude Opus 5.5 e2296a5e9d rugnux: the no-lattice per-image pass says its images did not index
The per-image pass of a run that found no lattice left indexing_result unset,
so the live scan result and the per-image arrays showed "not asked" rather
than "not indexed". Set it false there: GET /result/scan now gives index 0 for
every image and /entry/MX/imageIndexed is 0, as a collection without rugnux
reports a frame that failed to index.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 12:58:57 +02:00
leonarski_fandClaude Opus 5.5 c6b69d5bd5 Merge pol-header into rc176: the polarization a miniCBF or d*TREK header states is read; 0.99 only where the file states none (RESULT-CHANGING: lab-source d*TREK sets corrected as unpolarized)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 12:58:17 +02:00
leonarski_fandClaude Opus 5.5 421062be77 Merge mac-mr-metal into rc176: MR rotation function prefetched on the CPU (output unchanged) and on the Apple GPU where there is one (equal to rounding)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 12:51:58 +02:00
leonarski_fandClaude Opus 5.5 9a382c5ef4 Merge frontend-rugnux-fix into rc176: rugnux live gets a full-width dashboard row and a timeline that fits the run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 12:51:58 +02:00
leonarski_fandClaude Opus 5.5 c80471645c Merge scanresult-live into rc176: the live scan result carries rugnux's rotation lattice, cell and Bravais symbol
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 12:51:58 +02:00
leonarski_fandClaude Opus 5.5 04655bd8bf MolecularReplacement: the rotation function on the Apple GPU (Metal), equal to rounding
On macOS, where a Metal device may be used (not under --no-gpu), each merge's rotation stage - the sphere
and the peaks' refinement - runs on the GPU: the same trilinear interpolation of the same transform in
float, one threadgroup per orientation, every bin's sums of |F|^2, |F|^4 and |F|^2 (E^2 - 1) in a fixed
order, and the correlation made of them in double on the host. The GPU reads its own copy of the
transform in 2 x 2 x 2 bricks (1.3x faster than the grid's rows, as the eight corners then lie on fewer
cache lines), made for the rotation stage and given back before the translation function; the seeds'
orientations later on stay on the CPU. Where the GPU fails, the stage goes on on the CPU with a warning.
The kernel is MolecularReplacementMetal.metal, carried in the binary as the Bragg integration's are.
Elsewhere nothing changes.

Measured on an Apple M6 (16 GB), alternating with rc176 + the CPU prefetch, two runs each:
- 4nwv (the timing benchmark's moved model): rotation 78.9 -> 30.7 s, wall 344.5 -> 298.8 s; peak
  footprint 16.1-16.8 -> 18.5 GB.
- 9s02: rotation 2.99 -> 0.28 s, wall 79.2 -> 76.4 s.
p.hkl, the placed model and the maps are identical to the CPU's on both, and so is every line of the
molecular-replacement log (RF Z, TF Z, R values). The RF itself agrees with the CPU's to 1e-4, with the
same best orientation (MolecularReplacementMetal_MatchesCPU, [portable]).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018My7tqt8a4LyKKjP3FhScT
2026-10-18 12:51:49 +02:00
leonarski_fandClaude Opus 5.5 168a4e64a1 MolecularReplacement: prefetch the transform in the CPU rotation function, output unchanged
The CPU rotation function interpolates the molecular transform at points scattered over its grid, four
cache lines per point. Once the grid is far larger than the caches (835 MB for 4nwv: 16.9e9
interpolations over 17255 orientations) the loop waits on memory, at ~92 ns per interpolation per core
on an Apple M6. MolecularTransform::Prefetch() asks for the rows a point's corners lie on, 4
reflections ahead of the one being computed, through one portable helper (__builtin_prefetch under
GCC/Clang, nothing elsewhere). Same values, same order, so the output is bit-identical.

Only for a transform of 256 MB or more: a smaller one sits mostly in the caches, where the extra
address arithmetic only costs. In a standalone copy of the loop on the M6: 30% slower at 13-90 MB,
even at ~130-175 MB, 1.5x faster at 400 MB, 1.75-1.8x at 835 MB (prefetch distance 4 to 8 equally
good).

M6 Mac mini, 4nwv, alternating runs on a quiet machine, against the previous commit:
- rotation function 139.1/138.8 s -> 79.1/79.3/78.9 s
- wall 400.7/415.7 s -> 338.0/343.1/345.6 s
- --no-gpu: 450.4 s -> 385.4 s (rotation 140.3 -> 84.1 s)
- 9s02 (14k reflections, 13 MB transform, below the threshold): rotation 2.83 s -> 2.98 s, wall
  79.4 -> 80.4 s
p.hkl, p_model.cif/pdb, the maps and the MTZ/CIF reflection data are byte-identical in every run
(the files differ only in the git tag and command line in their headers). [MolecularReplacement]
tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018My7tqt8a4LyKKjP3FhScT
2026-10-18 12:51:48 +02:00
leonarski_fandClaude Opus 5.5 47132b7573 frontend: rugnux live polish - failed run steps take the full row, Supercell tile not highlighted
On the dashboard a run with no verdict, results or progress (a failed one) shows its step table
across the whole row instead of in the right half beside nothing. On the Rugnux tab the summary
tiles highlight "unit cell" rather than any label containing "cell". The image count drawn beside
the detector bar is opaque, so the "now" line passing behind it no longer crosses the text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 12:46:21 +02:00
leonarski_fandClaude Opus 5.5 39fff874e8 frontend: rugnux live gets a full-width dashboard row, a timeline that fits the run
Dashboard: the Rugnux card leaves the narrow right column for a full-width row between the first
plot and the second, with the timeline, the verdict and key results (space group, cell, resolution,
completeness, signal) beside the step table; it collapses to one column on a narrow screen and
renders nothing (no empty grid cell) when rugnux has not run. The measurement statistics row that
showed "-" is the cell given with the collection, now labelled "Unit cell (given)".

Timeline: the deadline sets the axis only while the analysis runs, so a finished run fills the
axis; marker labels that would overlap ("now" on "last image") move up a row; the image count goes
beside the detector bar when it does not fit in it; a step title too narrow to read is left to the
tooltip instead of being cut to one letter.

Rugnux tab: the failure message is a wrapped alert under the header, so the latency number stays
right-aligned; summary tiles are highlighted by label only, and only space group and cell share a
row, so no half-empty rows.

Status bar: with rugnux live on, the collection message names the analysis state alongside
("Data collection without problems · rugnux analysis: failed", raised to a warning when it failed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 12:25:18 +02:00
leonarski_fandClaude Opus 5.5 96a8ed3e4e rugnux: a run that finds no lattice still reports every image
A rotation run whose first pass found no lattice threw NoLatticeFound before
any canonical pass, so no image was ever reported as the canonical pass: the
live scan result and the master file's per-image arrays were empty, and the
CLI wrote nothing per image. Run() now catches it, runs one output pass over
every frame with spot finding and the per-image analysis but indexing off
(SpotFindingPass: header geometry, the run's spot finding, pre-scan mask kept;
the first pass, supercell probe, rotation finalize and merge skip a pass with
indexing off), reports it through the observer as the canonical pass, hands
the result back on the exception (NoLatticeFound::per_image) and rethrows -
the verdict and the report are unchanged. Skipped when cancelled and when Run
has no observer (the live lattice probe).

The CLI writes that result where a run that indexed nothing puts its
per-image analysis: the _process.h5 (streamed by the sink with --no-merge or
--write-process-h5, otherwise written from the end message) and the _plot.txt;
the exit status stays 1. An output pass with no sink now fills the end
message's per-image arrays (ScanResultGenerator) for that file, which it
claimed to carry but did not. The no-lattice hint no longer points at
--mode azint for the per-image analysis.

Measured offline on two 16M no-crystal controls (1800 frames): the extra pass
is 1.6 s, wall +1.2-2.3 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 12:23:35 +02:00
leonarski_fandClaude Opus 5.5 e3d6416ff3 broker rugnux live: the scan result carries rugnux's rotation lattice
The receiver's own rotation indexer sees no image in a rugnux-live collection, so the scan result
the broker serves (/result/scan) went without the rotation lattice, cell and Bravais symbol. They are
now taken from rugnux's end message, as the end message itself already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 11:16:24 +02:00
leonarski_fandClaude Opus 5.5 b17b643bd9 Readers: polarization from miniCBF and d*TREK headers (RESULT-CHANGING)
No reader parsed a polarization, and ApplyRugnuxExperimentDefaults then set
P = 0.99 (an undulator value) unconditionally, so laboratory-source data were
corrected as if from a synchrotron. Every d*TREK (Rigaku Saturn/A200) header in
the open arm states SOURCE_POLARZ=0.5 1 0 0, i.e. unpolarized; at Cu K-alpha and
2 A the wrong factor puts a +-33% azimuthal modulation on the intensities.

- d*TREK SMV: SOURCE_POLARZ is the fraction f followed by the plane normal (as
  dxtbx's FormatSMVRigaku reads it); stored as degree 2f - 1, negative where the
  normal lies along lab x (plane containing y).
- miniCBF: "# Polarization f" is the fraction in the horizontal plane; stored as
  2f - 1 ("None" or a missing line leave it unset).
- ApplyRugnuxExperimentDefaults sets 0.99 only where the file stated none;
  --polarization still overrides. ADSC SMV, marCCD and NXmx state nothing and
  keep 0.99.
- The "Header:" log line reports the stated polarization.

RESULT-CHANGING: d*TREK home-source sets (0.99 -> 0), one Rigaku miniCBF set
(0.99 -> 0), miniCBF sets stating 0.99 (0.99 -> 0.98).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 09:25:14 +02:00
leonarski_fandClaude Opus 5.5 14ee27f362 broker rugnux live: the analysis is cut 60 s after the last image by default
Build Packages / Create release (push) Successful in 8s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 4m43s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 9m36s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 14m44s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 20m29s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m41s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 21m58s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 26m12s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 26m10s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 27m17s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 27m41s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 22m48s
Build Packages / Generate python client (push) Successful in 28s
Build Packages / Build documentation (push) Successful in 1m8s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 23m57s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 27m18s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 26m18s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 21m27s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 24m10s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 27m39s
Build Packages / Unit tests (push) Successful in 1h32m56s
Build Packages / Validation Battery (push) Successful in 33m4s
deadline_s defaulted to 0 (no cutoff), so a difficult crystal could hold the end of a collection for
as long as the analysis took. 60 s bounds it; 0 still means no cutoff. The OpenAPI default, the
broker and receiver defaults and the docs say 60; the generated clients are regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 08:53:14 +02:00
leonarski_fandClaude Opus 5.5 6f361ccde5 HostWorkerPool: half the budget, MR placements reserve 16 B per grid point, output unchanged
Without a GPU (macOS), rc176's worker pool - 1/3 of the 12.9 GB budget on a 16 GB Mac - let only two
molecular-replacement placements run at once, as each reserved 32 B per grid point (2.1 GB on 4nwv)
against a measured ~0.95 GB. MR rescoring went from 38.6 s to 69 s and 4nwv's wall time from 402 s
(budget off) to 444 s.

On the M6 Mac mini (16 GB), 4nwv, two runs each in alternating order on a quiet machine:
- old build 234774ff0: 418.4 s, 20.0-21.3 GiB peak footprint, 576-682k swapouts
- rc176 as shipped: 444.2 s, 14.5-15.2 GiB, 39-66k swapouts
- this change: 402.4 s, 15.5-15.8 GiB, 66-93k swapouts
- budget switched off: 401.7 s, 21.8-23.3 GiB, 268-433k swapouts
9zlo/9s02/9gjx: 71.9/79.0/82.8 s (old build 73.6/82.2/83.6), no swap. p.hkl byte-identical in every
run: the pool decides only what runs at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018My7tqt8a4LyKKjP3FhScT
2026-10-18 08:42:02 +02:00
leonarski_fandClaude Opus 5.5 366079ca62 Merge twin-isa-9zmu into rc176: a twin-immune zone read on a control compressed beyond a perfect twin decides nothing, so a refused promotion no longer stands on it (RESULT-CHANGING: 9zmu P321 -> P6122)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 05:17:09 +02:00
leonarski_fandClaude Opus 5.5 ced0ac4bdd TwinZoneVerdict: no verdict where the control is compressed beyond a perfect twin
RESULT-CHANGING.

AnalyzeTwinImmuneZones flags zones_ambiguous when the acentric control reads more compressed
than a perfect twin's 0.541 by three standard errors: no twin fraction can do that, so something
else (overlapping spots, neighbour correlation) averages reflections with unrelated ones, it
compresses the zones too, and a genuine centric zone then reads acentric. SearchSpaceGroup's own
zone gate already skips such a reading, and the struct documents that no verdict is read from it
either way - but Rugnux's TwinZoneVerdict still returned -1 on it, only noting the ambiguity in
its text, and so let a refusal stand on a zone it had just called uninformative.

9zmu (deposited P6522, c ~493 A, 95% of predicted spots overlap a neighbour) regressed in rc176
from P6122 (enantiomorph, pass) to P321: its P1 <|L|> of 0.394 (overlap, not twinning - second
moment 2.23; the model's twin fraction 0.08 and R-free 0.287 twinned or not) put it in the
partial-twin band, so since 7c1befd48 the search asks the zone of the holohedral 622 too. On the
Lorentz-filtered merge the control read 0.552 +- 0.002 (just inside the twin range) and the zone
-1378 nats, so that arm refused 622; the all-observation arm confirmed it, and the arbitration
read the zone on the all-observation merge: control 0.513 (ambiguous), zone -319 nats, verdict
-1, refusal stands, P321. Now that reading is undecided, the rule stays as before the zone was
asked, and the all-observation 622 is taken: P6122 again, R-free 0.2825 (as rc175).

Changed: 9zmu P321 -> P6122 (pass). Unchanged against rc176 run 20261017-1924 (space group,
verdict, ISa, R-free identical): 3r6o I41, 8c3e P321, 3mc4 R3, and every other open set whose
rc176 log has REFUSED / twin-immune / "Twinning indicators BEFORE" (2wnn 4bwl 5ebi 5j23 6fid
6iu6 6iu8 6iu9 6p8j 6pxb 6pxc 6vww 6z9g 6zqr 7k1l 7kcn 8xte 8xtg 9fcf 9hnc 9hs7 9i80 cuhf2),
20 random passing open sets (2wnz 3p85 5jvn 6cdl 6f3p 6u7g 6zr0 7n2s 7orr 7os3 7ou1 7tcd 8egn
8xbp 8y74 9min 9rp9 9t6s 9v16 9yzk) and the five in-house sets with a TWIN_ZONE_DECISION.
In rc176 9zmu was the only set whose decisive zone verdict was an ambiguous one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-18 05:15:59 +02:00
leonarski_fandClaude Opus 5.5 0ee8897294 broker rugnux live: rugnux plans its host memory against what the image buffer leaves
The rugnux library plans what runs side by side against ProcessConfig::host_memory_budget_bytes,
by default 75% of the machine's memory. In the broker most of the machine is the image buffer,
which has to hold the whole collection for the live run, so that default would plan against
memory that is already taken. The receiver now sets the budget to the default less the image
buffer and a 16 GiB margin for the rest of the broker (never 0, which would mean the default;
at worst the run schedules in series), decided once and the same for the main run and the
60-degree probe, which share the process-wide HostWorkerPool. ImageBuffer gets a size getter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:03:07 +02:00
leonarski_fandClaude Opus 5.5 78c2a69267 Merge broker-rugnux-live into rc176: rugnux in jfjoch_broker as a live thread during a DECTRIS rotation collection (off by default), the rugnux library without file I/O (results in ProcessResult::outputs, the CLI writes them through JFJochRugnuxOutput), END carrying the merged table, map coefficients and placed model
Branch reviewed at e16db07ff (review: owner's decisions accepted - _maps.mtz instead of .ccp4 in the
viewer, -o "" computes everything incl. MR/validation, Ctrl+C/error paths may leave fewer files, the
writer links gemmi/zlib) and merged at 55184eb09 with its review fixes (END size guard, frame drop as
in Lite, outputs released before re-runs, pool priority, docs, frontend gating).

Conflicts: rugnux/Rugnux.h (kept rc176's host_memory_budget_bytes, output_prefix removed as on the
branch), rugnux/RugnuxPipeline.h (MtzTable future + rc176's GPUMerge pointer), MolecularReplacement.h/.cpp
(rc176's GPU cost model, packed transform and split functions with the branch's RugnuxModel signature;
search_model with rc176's other_budgets, result.model_name). tests/RugnuxLargeTest.cpp: rc176's
host-budget test writes its files through WriteRugnuxOutputs now that the library writes none.
Generated code (broker/gen, frontend client, Python client docs, Redoc) regenerated from the merged
jfjoch_api.yaml with update_version.sh (version unchanged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 21:01:38 +02:00
leonarski_fandClaude Opus 5.5 55184eb096 tests: the parallel unmerged MTZ table is compared bitwise again
UnmergedMtzTable's columns hold NaN for missing values, and vector<float> == compares NaN unequal to
itself, so the serial-vs-parallel check could fail spuriously. Each column is compared with memcmp,
as the test did before the MtzTable change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:51:08 +02:00
leonarski_fandClaude Opus 5.5 13a3d7c873 frontend: rugnux status polled only while rugnux live mode is on
The dashboard card and the Rugnux page polled GET /rugnux/status every second even with the mode
off, and on a token-protected dataset the dashboard showed a "Dataset token required" card for a
feature nobody had switched on. Both now read GET /config/rugnux first and enable the status query
only when `enable` is set: the dashboard reads the settings once on mount (the settings panel
invalidates them on a change) and shows nothing with the mode off; the Rugnux page says the mode is
off above its settings. Generated client unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:41:46 +02:00
leonarski_fandClaude Opus 5.5 06b0d445c1 broker rugnux live: docs and comments brought up to the in-memory design
- docs/CBOR.md: merged and map_coefficients are the generic MTZ table (title, cell, space group,
  history, sort order, datasets, columns with typed float values, batches), documented as the
  serializer encodes it; the placed_model row names every field's type.
- docs/JFJOCH_BROKER.md: the uploaded model is parsed and kept in memory, nothing on disk; the image
  buffer must hold the whole collection, else it is forwarded only, with a warning; the result files
  the writer makes and the END size limit; deadline_s and what /cancel and a cut do.
- JFJochServices.h: SetRugnuxModel keeps the model in memory, it writes no file.
- jfjoch_api.yaml: PUT /config/rugnux/model answers 400 also for a body that is not a model
  (description text only; generated code not regenerated here).
- rugnux_cli.cpp: --fast-live is in the usage text. Its adaptive_integration_radius = false stays: the
  CLI does not go through MakeRugnuxLiveSetup and ProcessConfig does not imply it from fast_live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:40:41 +02:00
leonarski_fandClaude Opus 5.5 91f6ac6749 broker rugnux live: the ParallelFor pool is made at the broker's priority
The pool is made by the first thread that uses it and its workers inherit that thread's nice value.
That thread was the nice-10 rugnux thread, so without CAP_SYS_NICE / RLIMIT_NICE the workers stayed
at nice 10 for the life of the broker - and the comment in RugnuxThread said the opposite.
JFJochReceiverRugnux's constructor now makes the pool first, at the broker's priority, so the
existing "lower it only where it can be raised back" logic is what decides. The pool constructor also
waits for every worker to record its thread id, so a SetNice right after construction reaches all of
them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:38:54 +02:00
leonarski_fandClaude Opus 5.5 78c3a0ba7e rugnux: a pass that is re-run lets go of its outputs before the re-run
ProcessResult::outputs (unmerged and merged tables, map coefficients) stayed alive while a re-run that
replaces the result ran, so the peak host memory held two sets. The old outputs are now released
before each re-run whose result replaces them: the near-tie re-run in Rugnux::Run, and in
RefinedPasses the demoted lattice arm, the geometry-walk rounds, the starved-background re-run, the
supercell-collapse re-run and the header-geometry redo. The doubled-cell hypothesis is left alone -
both cells' outputs are kept there. Nothing that is returned or written changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:38:12 +02:00
leonarski_fandClaude Opus 5.5 adc4ad3133 broker rugnux live: forward with JFJochReceiverLite's drop-on-overflow and start-message handling
JFJochReceiverRugnux::ForwardThread called SerializeImage directly, so an image whose CBOR metadata
overflowed its buffer slot threw: the collection was cancelled and the slot left InPreparation.
JFJochReceiverLite handles that by dropping the one image (SerializeImageOrDrop). Its Configure was
also copied by hand. Both are now static members of JFJochReceiverLite - SerializeImageOrDrop and
ApplyStartMessage, the former Configure moved verbatim - and JFJochReceiverRugnux calls them; Lite's
own behaviour is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:37:01 +02:00
leonarski_fandClaude Opus 5.5 cf2e0f7995 broker rugnux live: an END too large for the pushers' buffer goes without the result blocks
Every image pusher serialises END into its fixed MESSAGE_SIZE_FOR_START_END (256 MiB) buffer, and an
overflow there throws out of SendEndMessage: the writer never gets END and the collection errors. The
try in AddToEndMessage only covered filling the message. AddToEndMessage now serialises the finished
END once into a scratch buffer of the same size (uninitialised, so only the bytes written are
touched) and, where it does not fit, drops the merged data, map coefficients and placed model, with
an error in the log and a warning in the live status (RugnuxLiveStatus::Warning). The pusher then
serialises exactly the message that was checked.

The receiving side already takes an END this large: the TCP puller accepts payloads up to
JFJOCH_TCP_MAX_PAYLOAD_SIZE (1 GiB), the ZMQ sockets set no ZMQ_MAXMSGSIZE, and the puller's ring
buffer serves a message larger than its capacity from the heap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:36:23 +02:00
leonarski_fandClaude Opus 5.5 3e36fe9468 1.0.0-rc.176
Build Packages / Create release (push) Successful in 8s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 10m0s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 13m30s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 14m54s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 18m31s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 19m50s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m57s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 23m52s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 24m9s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 26m10s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 27m54s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 19m38s
Build Packages / Generate python client (push) Successful in 31s
Build Packages / Build documentation (push) Successful in 1m6s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 21m53s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 23m27s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 18m37s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 23m19s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 22m47s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 19m38s
Build Packages / Unit tests (push) Successful in 1h27m23s
Build Packages / Validation Battery (push) Successful in 33m5s
VERSION and update_version.sh: the version strings, and the clients regenerated from
broker/jfjoch_api.yaml - which also brings the C++ model, Python client docs, frontend client and
Redoc page up to the /live/events endpoint already in the spec - plus docs/THIRD_PARTY_NOTICES.md
regenerated from the root file (the timing benchmark's model entries).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 18:17:46 +02:00
leonarski_fandClaude Opus 5.5 e16db07ffd broker rugnux live: no files, END built from the result in memory, and review fixes
No files. The receiver no longer makes a /tmp scratch directory or reads
rugnux's files back: AddToEndMessage moves the merged MtzTable and the map
coefficients out of ProcessResult::outputs and converts the placed model with
PlacedModelFromStructure (now given the data cell, as WritePlacedModel is).
FillRugnuxResultBlocks and MtzTableFromFile are gone; their test now builds the
blocks from the same in-memory outputs and still checks the writer reproduces
rugnux's own files byte for byte. The live status report is built from the
result with the writer's file prefix as its label. AddToEndMessage never
throws: a failure is logged and the END goes without the blocks.

Review fixes:
- MakeRugnuxLiveSetup sets nthreads_auto, as a bare `rugnux` does, so the
  per-image loop keeps to GPUWorkersPerCard per card instead of starting one
  GPU worker per CPU thread; the 60-degree probe gets a quarter of the
  threads (at least 2).
- A failure setting rugnux up (start message, reader, thread start) marks the
  live status Failed and nothing else; the collection goes on as in Lite.
  MeasurementThread and ForwardThread also catch std::exception, so no
  exception skips the latch or the reader's Ended().
- The ParallelFor pool is lowered to nice 10 only where it can be raised back
  (CAP_SYS_NICE, or RLIMIT_NICE reaching 0); otherwise it is left alone and
  that is logged once.
- The preview and metadata sockets get the END without the result blocks.
- Receiver plots and scan result are fed only from the canonical pass
  (RugnuxObserver::OnImagePass): the pre-pass reports [0, E) again at the
  header geometry and the plots accumulate, so it would count those images
  twice. The canonical pass reads every image once.
- A collection the image buffer cannot hold is still not analysed, and now
  says so in the broker status (Warning) as well as the rugnux status.

With rugnux live off nothing changes: the extra END resets are of empty
blocks and analysis_warning is empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:13:23 +02:00
leonarski_fandClaude Opus 5.5 5028aac0ee jfjoch_writer: a placed-model block that describes no model is refused; END log line back to verbose
WritePlacedModel was handed the result of find_spacegroup_by_name unchecked,
and PlacedModelStructure indexed every per-atom vector by atom_name's length.
A malformed block from the wire could crash the writer. Now the per-atom
vectors must all have atom_name's length and aniso must be empty or six per
atom (PlacedModelStructure throws otherwise), and an unknown data space group
is an error; WriteRugnuxResultFiles logs either and writes nothing for the
model, as it already did for an MTZ table WriteMtz refuses.

StreamWriter: "Received end message" is logged only with -v again, exactly as
before this branch. The wall-clock times of the rugnux files are still logged
where they are written, which only happens when the END carries them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:13:12 +02:00
leonarski_fandClaude Opus 5.5 55f7254bab rugnux fast_live: a cut inside the canonical pass's image loop merges what it read
A /cancel or deadline cut while pass 2 was still integrating fell back to the
established pass 1 - a geometry pre-pass that merges nothing - so the END went
out without merged data. Live, pass 2 streams behind the frames, so this is
exactly where a cut during the collection lands.

Now, in fast_live, a cut inside the canonical pass's image loop stops it
reading further images, and the pass goes on as a partial sweep: its
integration outcomes are trimmed to the frames read (as a pre-pass trims its
own, so the per-frame smoothing in the merge does not run over an empty tail),
and it scales and merges them, making the normal outputs. The model stage is
left out after a cut, as before. Only a pass cut before it read any image (or
any other pass) still gives way to established_.

The CLI (fast_live false) is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:13:12 +02:00
leonarski_fandClaude Opus 5.5 d356cc5da5 ImageBuffer reader: one waiting thread at a time looks at the buffer
Every rugnux worker waiting for an image called ImageBuffer::GetImage every
millisecond, and GetStatus every 100 polls. Each call takes the buffer's mutex
and scans every slot, so sixteen waiting workers contended with the forward
threads for the lock they put the images in under (GetImageSlot, ReadyToSend).

Now one waiting thread at a time asks the buffer, at most every 2 ms, with a
single GetStatus, and notes the images it holds in a per-reader bitmap; the
other waiters sleep on a condition variable and are woken when it is
refreshed or the collection ends. GetImage is called once, for an image known
to be there (or given up on). MAX_IMAGE_LAG and Ended() mean what they did.
ImageBuffer itself is unchanged.

The test gains eight readers waiting at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:13:03 +02:00
leonarski_fandClaude Opus 5.5 9c9b987928 Merge host-mem-2 into rc176: a host-memory budget from the machine's total RAM decides what runs side by side, the CPU ingest reads the reflections in place (24 B per partial), merge scratch is really freed, and CPU MR/validation workers are bounded by their bytes; output unchanged
Peak live heap, CPU path -N10, before -> after: 4nwv scaling 14.8 -> 11.9 GiB, analyses 13.5 -> 7.3;
8qaw scaling 34.0 -> 27.1; 9v16 17.2 -> 13.5; 9zlo/9gjx/9s02 7.7/7.4/8.9 -> 6.3/6.3/7.5. With the
budget of a 16 GB machine, 4nwv validation 18.4 -> 10.8-12.3 and MR ~17 -> 12.5. GPU 4nwv 16.2 ->
14.8. Byte-identical to its base on GPU and --no-gpu (timing standard + inhouse, 4nwv/8qaw/9v16/
lyso_x06da_ref/cytc_x10sa), at the machine's budget and with a 16 GB one. Conflict in
RugnuxScaleMerge.cpp: perf-8gb-cpu's card-byte declaration of the merge and this branch's host-budget
decisions on the post-refinement and the all-observation arm, kept both, in that order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 17:59:30 +02:00
leonarski_fandClaude Opus 5.5 5f731db81d rugnux: the library returns its outputs in memory; the callers write the files
The rugnux library wrote its results itself: _process.h5 through a
FileWriter, the merged/twin/_mr reflection files, the unmerged MTZs, the
maps and their MTZ, the placed models, _detector.jpg and _plot.txt, and
on a doubled cell it renamed files on disk. It now writes none of them.

- MtzTable (common/MtzTable.h): an MTZ file's header, columns and batch
  headers with no file behind it. It replaces MergedReflectionsMessage
  and MapCoefficientsMessage in the END message, with one CBOR encoding.
  WriteMtz (writer/WriteRugnuxFiles.h) is the one serialiser, used by
  rugnux and jfjoch_writer alike; it refuses a table whose columns differ
  in length, name a missing dataset, carry a malformed batch header or
  name a space group gemmi does not know. MergedMtzTable and
  UnmergedMtzTable build the tables the old writers built.
- ProcessResult::outputs (RugnuxOutputs) holds, for the output pass only:
  the answer's merge (MTZ table plus what the .cif/.hkl are written from),
  the twin hypothesis' and the model's choice's merges, the unmerged and
  unmerged-partials tables, the maps (coefficients always; the grids only
  with ProcessConfig::keep_map_grids), the placed models, the detector
  JPEG and the per-image plot table, and the start message of the
  no-lattice _process.h5. Map RMS is taken from the grid in memory, as the
  CCP4 header stated it, whether or not the map is kept.
- The per-image analysis goes to a caller-supplied RugnuxImageSink;
  rugnux and the viewer give a FileWriter-based ProcessFileSink. The
  geometry pre-pass no longer makes a _01_process.h5.
- The doubled cell keeps both output sets: the answer's in outputs, the
  other's in DoubledCell::other_outputs with its suffix; the report names
  files by suffix after the caller's prefix.
- The output prefix, write_process_h5 and the observation dump path are
  gone from ProcessConfig; --dump-observations is a callback.
- What used to be gated on having files to write (model validation and
  MR, the twin-hypothesis merges, the finalist ledger, the scaled fulls,
  the unmerged tables) is gated on the output pass instead, so a caller
  with no prefix gets the same results.
- RugnuxOutputFiles (new library JFJochRugnuxOutput, linked by rugnux,
  the viewer and the tests) writes every file from the result with the
  names and formats rugnux has always used. The viewer writes its job's
  files the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 16:51:20 +02:00
leonarski_fandClaude Opus 5.5 6efc42360a ParseRugnuxModel: a file with no atom sites is not a model
gemmi reads any text it does not recognise as PDB and can hand back a
structure with an empty model in it, so 'no models' let garbage through.
Count the atom sites instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 16:51:07 +02:00
leonarski_fandClaude Opus 5.5 4bf41983ed rugnux: the model is parsed by the caller and handed in as RugnuxModel
The analysis read the --model file itself, four to five times per run
(the model kind, every validation, molecular replacement, the stills
reference intensities) and located the PAE sidecar from the file name.
The library is to do no file I/O, so the model now arrives in memory:

- RugnuxModel (image_analysis/structure_refinement/RugnuxModel.h): the
  gemmi structure as read, whether it is ModelCIF, the PAE matrix where a
  prediction came with one, and the name the report gives it.
- ParseRugnuxModel / ReadRugnuxModelFile (writer/RugnuxModelFile.h, in
  JFJochRugnuxFiles): bytes in, gzip by magic, PDB/mmCIF by content,
  _ma_qa_metric detection as before; the file reader takes the PAE JSON
  from the same place PaePathFor named.
- ProcessConfig::model replaces model_path; PrepareSearchModel,
  RunMolecularReplacement, ValidateAgainstModel and
  ModelReferenceIntensities take the model; result fields name it by
  model_name.
- rugnux reads the model before the run starts and stops with an error
  where the file holds no model (it used to run and report the model as
  not validated). The viewer reads it when the job is set up.
- The broker parses an uploaded model at once and answers 400 where it is
  not one; it keeps the parsed model in memory, with no temporary
  directory or file. It takes no PAE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 16:11:58 +02:00
leonarski_fandClaude Opus 5.5 b6f3e3b928 Merge sf-ceil into rc176: the GPU spot finder's local-box test, count threshold and sigma-clip bounds are the CPU's to the bit
RESULT-CHANGING in principle, unchanged in practice: diagnostic builds counted 0 disagreements of
the SNR and count tests over ~1.9e10 first-pass and 2.9e7 candidate tests on lyso_x06da_ref,
cytc_x10sa, myob_x10sa, yag_x10sa_20keV, 4nwv and thau_bl1a_3p8keV, and 2 clip-bound flips each on
lyso, yag and 4nwv; p.hkl bit-identical before and after on all six. New knife-edge parity test
fails on the old GPU code in both directions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 16:07:48 +02:00
leonarski_fandClaude Opus 5.5 0903e35f73 Merge perf-8gb-cpu into rc176: the geometry probe beside the merge is decided on bytes, the GPU rotation prediction walks only the l that can cross the sphere, and the mmCIF/SHELX numbers use std::to_chars; output unchanged
Byte-identical to an rc176 base on GPU and --no-gpu: standard and inhouse timing benchmarks and a
run of lyso_x06da_ref, cytc_x10sa, myob_x10sa, yag_x10sa_20keV, 4nwv. With the card faked to
8.55 GB the geometry check drops 0.81 -> 0.17 s (myob) and 0.44 -> 0.09 s (lyso); the GPU Bragg
prediction kernel on 4nwv 1968 -> 415 ms on an RTX 5080 (loaded box, kernel time). A merge above
~9 GB (8qaw class) now makes the probe wait for it on a 16 GB card too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 16:07:17 +02:00
leonarski_fandClaude Opus 5.5 7539f216dd AdaptiveSpotFinderGPU: sigma-clip bounds rounded as the CPU rounds them (RESULT-CHANGING)
RESULT-CHANGING for the GPU adaptive spot finder, only where a clip bound sat within its last bit of
an integer.

clip_keep formed mean -/+ clip_k * sigma as one fused multiply-add each; AdaptiveSpotFinderCPU::ClipRings
computes the product and then the sum (contraction is off project-wide). The two bounds differ in the
last bit, and where that crosses an integer pixel value every pixel of that value in the ring moves in
or out of the clip, so the ring mean/sigma - and the ring's threshold - differ. clip_keep now rounds
product then sum (__fmul_rn, __fsub_rn / __fadd_rn), still in one place for every kernel.

Measured: a diagnostic build evaluating both forms in every clip_keep found 2 differing tests each on
lyso_x06da_ref (of 4.7e7), yag_x10sa_20keV (of 2.2e8) and 4nwv (of 7.0e7), none on cytc_x10sa,
myob_x10sa and thau_bl1a_3p8keV. The battery GPU runs before and after give bit-identical p.hkl on
all six sets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 16:06:58 +02:00
leonarski_fandClaude Opus 5.5 e3f6dade6d battery: a --no-gpu run does not wait for the GPU lock
--gpulock serialises rugnux runs on the shared card; a run given --no-gpu (in --extra) has no card to
share, but queued behind every GPU run all the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 15:27:37 +02:00
leonarski_fandClaude Opus 5.5 c8c33b79e0 Merge twin-isa into rc176: the space-group search asks the twin-immune zone of a lattice-holohedral candidate and no longer reads centring absences as screws; reference_mismatch is a report note
RESULT-CHANGING. 3r6o I4122 -> I41 (fail -> pass; ISa 4.15 -> 7.77, R-free 0.303 -> 0.270);
8c3e P622 -> P321 (fail -> unscored: the c screw is undeterminable from its sweep, the point group
is right). Unchanged on the other 18 sets of a GPU battery (run 20261017-1241_520254_twin-isa-t2b
vs 20261016-0057) and on a CPU screen of 53 controls. The reference_mismatch key on 3mc4 and 3r6o
is a note in the report only: nothing is skipped or re-scored. The report.py conflict was the note
column, which now carries both the SHELXL R_free note and the reference note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 14:45:17 +02:00
leonarski_fandClaude Opus 5.5 af1bad8f47 MR on the CPU: placement refinements reserve their grids against the host worker pool
The first copies' placements are refined side by side, one thread each, and without a GPU each makes
its structure factors and rigid body on the CPU - about a gigabyte of grids on a large cell, five at
once at the 4nwv molecular-replacement peak (14.9 GiB live with a 16 GB machine's budget). Each now
reserves 32 B per point of its grid before it starts, as a null replicate does. What each computes,
and where its score goes, does not change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 14:40:41 +02:00
leonarski_fandClaude Opus 5.5 f215300a07 ImageBuffer reader: give up on an image MAX_IMAGE_LAG (100) behind the newest, and a unit test
Images reach the buffer in order give or take a few, so one still missing when the buffer holds an
image 100 past it is lost, not late: the read returns false instead of waiting for the end of the
collection. The newest image number comes from ImageBuffer::GetStatus, which scans every slot under
the buffer's lock, so it is asked only every 100 polls (~10 Hz).

The test puts CBOR images into an ImageBuffer the way the receiver does and checks read-back of the
pixels, waiting for an image not yet there, the lag limit, the end of the collection and the bounds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:27:10 +02:00
leonarski_fandClaude Opus 5.5 c346597a48 battery: reference_mismatch is an annotation, not a skip
Owner decision: an outlier that has an explanation stays in the battery, because it tells
something; removing it makes the results opaque. So 3mc4 and 3r6o run the REFMAC and
depositor-data checks like every other set again, and their numbers count in the plots and
statistics. The manifest's reference_mismatch note (the frames are a ~30% twinned crystal,
the deposition another, untwinned one) is printed in the per-set table's note column and
under the REFMAC R_free ratio plot. battery.py is back to its state before 52025483d.

Checked: battery recheck of 20261017-1139_e25876_twin-isa-mismatch - REFMAC ran on both
(R_free ratio 1.362 / 1.363, dep_cc_delta_all -0.163 / -0.319) and the report shows the note
beside both rows; test_score.py OK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 13:45:50 +02:00
leonarski_fandClaude Opus 5.5 2df46872c5 battery report: the plain R_free ratio says when the deposited R_free is a SHELXL one
The plain R_free ratio sets the R_free of rugnux's --model placement against the R_free the depositor
published; for an entry refined only in SHELXL (anisotropic, riding H, often atomic resolution) that
published number is lower for reasons of program, not data, so the ratio reads high (1.69 on the
0.6 A Z-DNA entry, where the like-for-like REFMAC ratio is ~1.07). The deposition's refinement
program (_software, classification 'refinement') is now recorded per set, and such a row's note and
its point in the R_free ratio plot say the ratio is not like-for-like and point at the REFMAC ratio.
An entry naming SHELXL beside another refinement program gets no note. In the open arm this marks
6cdl, 6jgh, 6jgj and 7atg; nothing is excluded or re-scored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 13:37:55 +02:00
leonarski_fandClaude Opus 5.5 9c4e22465a ImageSpotFinderGPU: the local-box test is the CPU's to the bit (RESULT-CHANGING)
RESULT-CHANGING for the GPU spot finder (classic and adaptive, which shares this kernel for its
local-box passes) - only on pixels where the two used to disagree.

Two differences between pixel_result and the CPU's StrongInWindow:

- SNR test. Both form T = float(var) * strong2 in float. The CPU compares the int64 square
  (in_minus_mean)^2 with (int64) ceil(T); the GPU compared it with T directly, which converts the
  square to float. Below 2^24 the two differ only when the square equals ceil(T) with T not an
  integer (an exact-integer strong2 such as 9 or 16 never gives that); past 2^24, where T is an
  integer, the GPU's rounded square can land on T and fail a pixel the CPU accepts. The GPU now
  uses the CPU's integer-vs-ceil comparison.
- Count threshold. At photon_count_threshold = 0 the CPU still requires val > 0; the GPU took 0 as
  "no threshold". It matters only for a pixel <= 0 above a negative local mean (a
  pedestal-subtracted detector); the adaptive finder always runs this pass with threshold 0. The GPU
  now applies val > threshold unconditionally, with the threshold carried as int64 like the setting.

Measured: a diagnostic build evaluating both semantics in every pixel_result found zero
disagreements in ~2e10 first-pass and ~2.9e7 candidate tests over lyso_x06da_ref, cytc_x10sa,
myob_x10sa, yag_x10sa_20keV, 4nwv and thau_bl1a_3p8keV (JUNGFRAU); the battery GPU runs before and
after give bit-identical p.hkl on all six sets. The new knife-edge test constructs both cases (a
checkerboard window whose square is var * 25 exactly and rounds onto the float threshold; a 0 above
a negative checkerboard) and failed on the GPU before this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 13:18:19 +02:00
leonarski_fandClaude Opus 5.5 7c1befd484 SearchSpaceGroup: ask the twin-immune zone of a lattice-holohedral candidate too, and do not read centring absences as screws
RESULT-CHANGING.

A tetragonal crystal in 4 twinned ~0.3 by k,h,-l (3r6o, I4_1) was merged in I4_122. Since the
beam-stop fix both search arms passed 422 (H ratio 1.51 < 1.85); before it the Lorentz-filtered
arm refused at 2.13x and was overridden by the all-observation arm. Either way the twin-immune
zone - the one reading a twin cannot fake - was never asked: the Stage A zone gate skipped any
candidate that holds every rotation of its lattice, on the argument that no twin law exists for
such a group. But the subgroup's twin law is one of the candidate's own operators, so the
holohedral promotion is exactly the commonest merohedral-twin case (4 -> 422, 3 -> 32 on a
rhombohedral lattice, 321 -> 622), and the L-test-under-merge gate leaves such a twin in the
higher group by design. The gate now runs whenever the P1 merge reads twinned
(<|L|> in [0.375, 0.44)), holohedral or not. 3r6o reads 422 over 4 at -58 nats (acentric).

With 422 refused, the all-observation arm took 222 over 4: the same-order tie-break credits a
group's axial two-folds with the screw evidence of their rows, and on a body-centred crystal
the I centring kills the odd class of every principal row (30 nats each), so a 222 of the real
2-fold plus two twin laws beat the crystal's own 4. Rows whose odd class a centring extinguishes
are no longer counted where the merge reads that centring on its general reflections (the
centring class at < 0.25 of the rest's mean E^2; measured 0.11-0.13 for I on 3r6o, 0.97-1.00
for the absent centrings).

Changed (CPU screen of 53 sets against the kept rc175 run 20261016-0057, plus 3r6o):
- 3r6o: I4_122 -> I4_1 (deposited I4_1), TWINNING INDICATED, twin law h,-k,-l ~0.32;
  ISa 4.15 -> 7.77, R_meas 0.218 -> 0.146, CC1/2 0.989 -> 0.993.
- 8c3e: P622 -> P321 with P3_121 | P3_221 as alternatives, screw on c undetermined (deposited
  P3_121, twinned): 622 refused at -70.8 nats; ISa 5.69 -> 6.91.
Unchanged: 3mc4, 5j23, 6toc, 6oww, 6iu5/6/9, 2wnn, 2xfw, 4bwl, 5ebi, 2wnq, 6vww, 7k1l, 6pxb,
8xte, 8xtg, 9i80, 5uth, 6h5t, 8k1g, 6moj, 7kcn, 5vml, 3p85, 7rji, 8xtf, 9h0q, 7l84, 6v2r, 7t5t,
5m17, 6p8p, 3meb, 5cc8, 9lxl, 8v4j, 6zqr, 9fcf, 5src, 11if, cuhf2, lyso_x06da_ref,
lyso_x10sa_90deg_1, thau_x10sa_16keV, thau_bl1a_6p5keV, insu_H_x06da_twin/notwin,
insu_I_x06da_ref, cytc_x06da_1, myob_x06da_split, kdp_x10sa_20keV (space group identical,
ISa within run noise; 7l84/5src/11if differ only by the model-adopted enantiomorph label).

Test: a body-centred 4 twinned at 0.45, given its cell, stays in 4 (222 without the centring
rule). [SearchSpaceGroup], [twinning], [twin] pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 13:01:11 +02:00
leonarski_fandClaude Opus 5.5 f117484592 GPU rotation prediction walks only the l of each column that can cross the Ewald sphere
bragg_rot_kernel_3d ran one thread for every (h, k, l) of the box on every frame, while the CPU
predictor already skips the l of an (h, k) column whose phi solution cannot lie within the
rocking-curve test's reach (BraggPredictionRot: f(l) = |p0|^2 + 2 S0.p0 bounded by f_max, at
most two intervals of l). The GPU predictor now walks the same l:

- RotationColumnRange.h: the CPU predictor's phi limit and column bound, moved into a
  host/device header shared by both, unchanged in arithmetic except that the four divisions
  by 2 C*.C* are a product with its reciprocal (each end is widened by a whole index, which
  covers the rounding).
- column_range_kernel: the l range of every column, one thread a column, in double; the
  column's b and c are evaluated as polynomials in h and k from coefficients set up on the
  host, and the blocks are 32 threads, because a consumer card runs double at a small
  fraction of the float rate on a unit the warps of a multiprocessor share (a warp per column
  with the range on lane 0 was slower than the whole box).
- bragg_rot_kernel_columns: a warp per column, its l spread over the lanes; per (h, k, l) the
  same compute_reflections_rot. The output is sorted afterwards (OrderAndSelect), so the
  predictions and their order are those of the whole box.

Kernel time on the RTX 5080 (nsys, whole run): 4nwv 1968 -> 415 ms (bragg 374 + ranges 41),
myob 45.5 -> 49.9 ms (small box: the ranges cost more than they save there, ~4 ms a run). On
the Windows laptop bragg_rot_kernel_3d was 9.4 s of 4nwv.

BraggPredictionRotGPU_ColumnRangeKeepsEveryReflection: on 300 random lattices, geometries and
rocking widths the pruned walk predicts bit for bit what the whole box does (the GPU twin of
the CPU test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 12:43:20 +02:00
leonarski_fandClaude Opus 5.5 dccbf8b1da Reflection files: the mmCIF and SHELX numbers formatted with std::to_chars
Writing the merged reflections - p.mtz, p.cif, p.hkl, ~250 MB for 4nwv - takes 5.3 s on the
Windows laptop against 0.46 s on Linux. The mmCIF rows (twelve "%.4f" fields each) and the
HKLF 4 records (two "%.2f") are formatted with snprintf, ~50 M calls for 4nwv; the likely
Windows cost is the MSVC runtime's printf (slower float formatting, locale handling per call) -
not measured there yet. Fmt and the HKLF 4 number now go through std::to_chars(fixed, precision) where the
standard library has it for floating point (__cpp_lib_to_chars: libstdc++ 11+, MSVC); libc++
before macOS 13.3 does not, and keeps snprintf.

The same digits: both round the exact binary value correctly, halfway cases to even. Checked
against glibc on 20 M float-valued doubles at 4 and 2 decimals and on 8 M exact binary
fractions (every tie), 0 differences. On this machine (gcc-toolset-13) 52 against 194 ns a
field. p.hkl, the MTZ data and p.cif (apart from its provenance lines) byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 12:42:09 +02:00
leonarski_fandClaude Opus 5.5 0d7e176b3c Speculative geometry probe beside the merge decided on bytes, not on a 12 GB card size
The speculative geometry probe waited for the main line's merge on every card below 12 GB
(gpu_takes_turns), so on an 8 GB laptop card it always ran after the merge, as a serial
"Geometry check" at the end of the run (Windows RTX PRO 2000: myob 1.4-1.7 s, lyso 0.6 s,
4nwv 3.2-4.4 s, against ~0.15 s where it overlaps). Now it is decided from bytes, up front and
never from free memory:

- GPUMerge::Holds(bytes): the main line says what its merge holds of the card once the scaling
  engine is ingested - the engine's DeviceBytes (an upper bound), twice where the all-observation
  arm gets an engine of its own - before the post-refinement that may start the probe is let go.
  A merge that has not said yet counts as leaving no room. Before the model stage it says the
  whole card: model validation and MR budget 80% of the card for themselves.
- GPUWorkBeside(after_merges, bytes): the probe waits for the merges only where its own bytes
  and theirs exceed 80% of the first card's total (the same 80% the merge's and the model's GPU
  budgets use; on WDDM the rest is the paging margin). The image-pass / probe-pass turn-taking
  below 12 GB is unchanged.
- Rugnux::IndexingProbeGPUBytes: the probe's first-pass spot workers at 24 bytes a pixel
  (GPUWorkersPerCard's figure) plus 0.65 GB per FFT indexer (1.24 GB measured for two).
  The spot-worker count is now one function (FirstPassSpotWorkers) for both.
- The model stage waits for GPU work beside to end before it plans its GPU engines.

Estimates on an 8.55 GB card: myob probe 4.1 GB + merge 0.6 GB -> beside (was: after);
4nwv probe 3.9 GB + merge 4.2 GB -> still after. On the RTX 5080 (16.3 GB): myob 4.8 + 0.6,
4nwv 4.5 + 8.4 = 13.0 of 13.04 GB -> beside, as before.

Concurrency only: what each pass computes is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 12:41:45 +02:00
leonarski_fandClaude Opus 5.5 2f3885259f Merge mr-gpu-r2 into rc176: MR and model validation spread their rigid-body engines over the cards, and MR packs the transform where its cost model expects it faster; output unchanged
Validated on the 4x T4 server (round 2): p.hkl, p_model.pdb, MR_* and the candidate table
identical on 9zlo/9s02/9gjx/4nwv against round 1 on 1 and 4 cards; 4nwv on 4 cards 140.8 -> 127.9 s
wall (MR 34.0 -> 21.2 s; rotation 18.4 -> 4.8 s), 9zlo/9s02/9gjx 1-3% faster. Conflicts were the
crash fix both branches carry: the branch's side taken (the per-card Reserve of the device test).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 12:35:42 +02:00
leonarski_fandClaude Opus 5.5 52025483d8 battery: a per-set reference_mismatch key skips the REFMAC and depositor-data checks
The frames of 3mc4 and 3r6o are a crystal twinned ~30% by k,h,-l - XDS on the same
frames reads the same H-test (0.29/0.27) as rugnux (0.29/0.29), in every 60-frame wedge
and in P1 - while the deposited structure factors read untwinned (H ~0). The deposited
data are another crystal of the same form, so the REFMAC R-free ratio and dep_cc on these
two sets measured the mismatch, not rugnux.

open.json carries the reason as "reference_mismatch"; run and recheck then record
refmac_status/dep_status "skipped" with it, and the report lists both skips (the
depositor-data skip list is shown even when no set was compared). Space group, cell and
d_min are still scored against the deposition.

Checked: battery run --only 3mc4,3r6o --model-check --gpulock
(20261017-1139_e25876_twin-isa-mismatch) - both sets skipped with the reason;
test_score.py OK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 12:21:34 +02:00
leonarski_fandClaude Opus 5.5 4e6d7c33b3 rugnux live: END carries the merged data, map coefficients and placed model rugnux wrote
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:20:50 +02:00
leonarski_f 6e89bb2c26 Merge branch 'broker-rugnux-writer' into broker-rugnux-live 2026-10-17 12:20:38 +02:00
leonarski_fandClaude Opus 5.5 3a44779d79 broker: rugnux live - rugnux runs inside the broker while a DECTRIS rotation collection arrives (demo prototype)
A DECTRIS rotation collection, with the mode switched on (PUT /config/rugnux), goes to
JFJochReceiverRugnux instead of JFJochReceiverLite: the images are forwarded as before, without the
per-image analysis, and one rugnux thread reads them back from the image buffer
(JFJochImageBufferReader) as they arrive. END waits for rugnux and carries its lattice, refined
geometry and per-image results. A small second run on the first 60 degrees reports early whether
there is a lattice. The live state, steps and result are at GET /rugnux/status and on a new
"Rugnux live" frontend page.

rugnux: ProcessConfig::fast_live takes every decision where it is first made and does nothing again
once the sweep is in (near-tie re-checks and the run made again, rotation-scale walk, metric-symmetry
arms, geometry walk, the supercell and quality guards, the doubled cell), so the run ends a fixed
time after the last image. A cut (/cancel, or deadline_s after the last image) hands over what is
established. Off by default: the CLI and the battery are unchanged (--fast-live is hidden, with
--simulate-live).

With the mode off the broker is unchanged. Not built yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:20:38 +02:00
leonarski_fandClaude Opus 5.5 aca27be862 jfjoch_writer writes rugnux's .mtz, _maps.mtz and _model.cif from the END blocks
New gemmi-only library JFJochRugnuxFiles (writer/), linked by JFJochWriter, so
the writer needs no analysis library and builds with JFJOCH_WRITER_ONLY:
- WriteMergedMtz / WriteMapCoefficientsMtz / PlacedModelStructure make the
  files from the structured blocks; WriteRugnuxResultFiles writes whichever
  blocks END carries beside the master, each under a temporary name and renamed
  into place, an existing file replaced only when the run's overwrite is set.
  A file that fails costs only itself (logged), not the run.
- FillRugnuxResultBlocks reads the blocks back from the files rugnux wrote at
  a prefix (p.mtz, p_maps.mtz, p_model.cif), for the broker to fill END.

WriteMtzReflections now goes through WriteMergedMtz (MergedReflectionsForWriter
builds the block); its output is byte-identical to before, checked by md5 on a
synthetic set in both the Friedel-merged and the anomalous layout.

StreamWriter writes the files after the master and before the END ACK, so the
ACK means they are in place; END receipt and each rename are logged at INFO
with the wall-clock time, for last-frame-to-files latency. The broker's direct
HDF5 pusher writes them too.

Test: rugnux's three files -> blocks -> CBOR -> writer reproduces all of them
(and _model.pdb) byte for byte; a ~25k-reflection END is 2.7 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:19:51 +02:00
leonarski_fandClaude Opus 5.5 91f6563953 END message: optional merged / map_coefficients / placed_model blocks
Structured results of an analysis run inside the broker, for jfjoch_writer to
turn into <file_prefix>.mtz, _maps.mtz and _model.cif itself: the merged
reflections and the map coefficients one typed array per MTZ column, the placed
model one array per atom field (coordinates as float64). Each block is a nested
CBOR map under its own optional END key; a reader that does not know a key
skips it, as for every other END key, and unknown keys inside a block are
skipped the same way. Space groups travel as gemmi's extended H-M name, which
round-trips to the same table entry for all 564 settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:10:35 +02:00
leonarski_fandClaude Opus 5.5 e8e5c4aecc battery model check: a synthetic free set where the deposited structure factors carry none
An entry refined in SHELXL often deposits its structure factors with every reflection 'o' and no
pdbx_r_free_flag, so the depositor-data baseline was skipped and the set had no REFMAC ratio; the
report then showed only the plain R_free ratio, REFMAC on our data against the depositors' SHELXL
R-free, which is a comparison of programs. Such an entry now gets a 5% free set from a fixed hash
of the asymmetric-unit hkl (order-independent, the same reflections free in both data sets and
every run), and baseline_note - shown as refmac_reason - records that it is synthetic.

On the 0.6 A Z-DNA entry of the open arm (7atg, the only such set in the last full run) the
first-cycle REFMAC ratio is 1.067 (0.171 vs 0.160) and the refined shared-free-set ratio 1.084
(0.166 vs 0.153, 2629 shared free reflections), where the plain R_free ratio read 1.69. Entries
with a deposited free set take the unchanged path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 11:57:43 +02:00
Filip LeonarskiandClaude Opus 5.5 a0a9a4b99c battery.py: macOS core tiers from every perflevel (an M6 is 2S+4P+6E, not 2P+4E)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018My7tqt8a4LyKKjP3FhScT
2026-10-17 11:44:26 +02:00
leonarski_fandClaude Opus 5.5 bc5577d32c Merge mr-multigpu-crash-fix into rc176: rigid-body engine calls restore the caller's card and the MR GPU functions run on their own card, fixing an illegal memory access in MR with engines on other cards; output unchanged on one card
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 23m23s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 27m42s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m56s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 27m8s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m35s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 17m8s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 30m6s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 20m19s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 13m19s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 16m43s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 35m34s
Build Packages / Create release (push) Successful in 7s
Build Packages / Generate python client (push) Successful in 11s
Build Packages / Build documentation (push) Successful in 45s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 24m56s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 28m38s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 29m12s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 29m13s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 30m13s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 30m37s
Build Packages / Unit tests (push) Successful in 1h12m25s
Build Packages / Validation Battery (push) Successful in 33m41s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 11:00:42 +02:00
leonarski_fandClaude Opus 5.5 a47f030074 Merge singular-fix into rc176: the model-scale fit no longer stops on a singular normal matrix (a reflection whose |Fcalc| the solvent cancels exactly; an underflowed Levenberg-Marquardt step), output unchanged where it ran before
Fixes the GPU-path crash after MR on 8sqo, 8v4o and 9zmu (all three now place in the deposited group) and the CPU-only ModelValidation_CCModelUnrelatedModelConfirmsNothing failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 10:51:05 +02:00
leonarski_fandClaude Opus 5.5 210664f454 Model scale fit: no log ratio at |Fcalc| = 0, and no LM step once the model has underflowed
Two ways the scale fit's normal matrix went singular and gemmi's jordan_solve threw "Trying to
reverse singular matrix. Column 0 is zeroed.", ending the run:

1. The isotropic start (fit_isotropic_b_approximately) takes log(Fobs/|Fcalc|) over the strong
   reflections. At a phase-restricted reflection whose Fcalc and Fmask share a phase, some solvent
   pair of the k_sol/b_sol grid can cancel the model exactly in float, |Fcalc + k_sol
   exp(-b_sol s^2) Fmask| = 0. log(Fobs/0) = inf made that grid point's k_overall and B NaN, every
   sum of the Levenberg-Marquardt NaN, and the solve failed. This is what stopped three open-arm
   sets in the output pass after MR on the GPU (the scale fit after model validation's rigid body):
   one strong reflection, one grid point. The CPU path has the same exposure; which grid point
   hits an exact zero depends on the last bits of Fcalc, so the --no-gpu run of the same set went
   through. Such a reflection has no log ratio and is now left out of the start - in the GPU kernel,
   in FixedSolventFit::Start and, as a marked local change, in gemmi's own
   fit_isotropic_b_approximately, so the three stay equal and the held-solvent fits are covered too.

2. In FitModelScale's CPU Levenberg-Marquardt (ModelValidation_CCModelUnrelatedModelConfirmsNothing
   in a CUDA=OFF build): on 66 reflections of an unrelated model the fit walked to k_overall -424
   and a B where k exp(-B s^2) |F| underflows at every reflection; the squares in J^T J were all 0
   while J^T r kept a residue near 1e-210, which jordan_solve rejects. The fit now ends there, at
   its best so far, as it would at the lambda limit; that grid point then loses on R. The GPU fit
   takes the derivatives in float and their squares in double, so it cannot reach this state.

Both changes only touch fits that threw before. Byte identity checked against an rc176 build on
the standard and inhouse timing benchmarks and on 4nwv, 8tyy, 6oel (p.hkl, p_model.pdb, MTZ data,
MR_* report keys).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 10:48:36 +02:00
leonarski_fandClaude Opus 5.5 e2bc7a3870 Rotation scaling: the fulls and the merge's working arrays go with each arm, and are really freed
ReleaseMergeScratch and ReleaseAllButPerFrameScale assigned `= {}` to std::vector members, which is
the initializer-list assignment: it empties a vector and keeps its memory, so only the struct-typed
members were ever handed back. They now free (Free(v): v = T()). And every RunArm hands back the
fulls, their staging and group CSR and the merge's working arrays when it returns - each is rebuilt
before it is read - instead of keeping them beside the next arm's combine and through the analyses:
4nwv on the CPU at 12 threads, live peak in scaling 13.0 -> 11.9 GiB, in the analyses 11.7 -> 7.3.
Output unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 10:45:04 +02:00
leonarski_fandClaude Opus 5.5 80f634b641 MR GPU: engines spread over the cards; the transform packed where it is expected faster
Build Packages / Create release (push) Successful in 8s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m31s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 12m45s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 37m25s
Build Packages / build:portable:linux-x86_64:cuda (push) Canceled after 0s
Build Packages / build:portable:linux-x86_64:nocuda (push) Canceled after 0s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Canceled after 0s
Build Packages / Validation Battery (push) Canceled after 0s
Build Packages / Unit tests (push) Canceled after 48m13s
Build Packages / build:jfjoch:rocky8:cuda (push) Canceled after 0s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Canceled after 0s
Build Packages / build:jfjoch:rocky8:nocuda (push) Canceled after 0s
Build Packages / build:jfjoch:rocky9:cuda (push) Canceled after 0s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Canceled after 0s
Build Packages / build:jfjoch:rocky9:nocuda (push) Canceled after 0s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Canceled after 0s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Canceled after 0s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Canceled after 0s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Canceled after 0s
Build Packages / Generate python client (push) Canceled after 0s
Build Packages / Build documentation (push) Canceled after 0s
Build Packages / build:portable:windows-x86_64:cuda (push) Canceled after 21m47s
Engine placement: the rigid-body engines past this card's first go one card after the other in turn, from
the next card and this one last, each within half of its budget. A small cell on four cards (24 engines of
0.09 GB) had put all of them on this card, which has room for them, where rc176 spread them; now 6/6/6/6.
A large cell on four 15.6 GB cards keeps T+1/T+2/T+2/T+2.

Layout: packing was refused wherever it cost any card an engine, which kept 4nwv plain on four T4s although
the packed rotation function on ONE card (15.1 s) already beat plain over four (18.5 s). Now the two layouts'
plans are compared on a small cost model, decided once before the search from sizes and counts alone:
  seconds = rotation interpolations (orientations x reflections x operators) x s/interpolation / cards with a copy
          + rigid-body fits at once x engine GB x s/(fit GB) / engines
Constants measured on 4nwv on Tesla T4 (17255 x 244183 x 4 interpolations): 75.8 s plain and 15.1 s packed on
one card; rescoring + null 28.5 s with one engine and 11.6 s with seven, 9 fits of 2.10 GB, so 19.7 s shared
among the engines. Packing must still keep the search, the scores and (where plain has it) the rigid body on
the GPU, and the budget must hold both grids while the packed one is made. Expected on four T4s: packed
T+1/T+1/T+1/T+1, 8.7 s against 21.8 s plain. One card is unchanged (the engines are the same either way, so
packed). The layout line now logs both expected times and the search's size.

Layout and engine count change where the work runs, never a number.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 10:34:09 +02:00
leonarski_fandClaude Opus 5.5 3fa17b9385 Model validation: the rigid-body engines on every card in turn, even where this card has room for all
The engines past this card's first now go one card after the other, from the next card and this one last,
each within half of its own budget, as MR's do. Before, this card took every extra engine it had room for
and the other cards only the rest, so a small cell on four cards ran all its fits on one. Where this card
has no room for a second engine (a large cell on 15.6 GB cards) the plan is unchanged: 1/3/3/3. This card's
scale fits take the room its engines leave. Engines are interchangeable and deterministic, so only where
fits run changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 10:34:05 +02:00
leonarski_fandClaude Opus 5.5 46c44e04d9 Rotation scaling: a host-memory budget decides what runs side by side
The run's budget (ProcessConfig::host_memory_budget_bytes, else 75% of the machine's RAM) is decided
once and logged, and sizes the host worker pool. Three concurrencies of the scaling chain are
decided on it from counts known before each starts (RotationScaleMerge::HostIngestBytes,
HostMergeBytes, HostBytesBound), each logged where it serialises:
- the quality guard's merge beside the main ingest: where they would not fit together, the guard
  merges first;
- the canonical pass's post-refinement beside the merge chain: done before the first merge;
- the all-observation P1 search merge beside the main engine's (GPU path): started where the search
  first reads it, after the main engine's merges and the release of their scratch.
None of these reads what the other writes while it runs, so only the order changes: a [large] test
runs the rotation set at a 1 TB and a 1 MB budget and compares the .hkl byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 09:31:24 +02:00
leonarski_fandClaude Opus 5.5 71afe748b3 Rotation scaling on the host reads the reflections in place: 24 B per partial instead of 84
Without a GPU (and for the observation dump) RotationScaleMerge kept a full Obs copy of every
partial - 84 B, 9.8-10.9 GiB on a 123 M-partial set, 2.6-2.9 GiB on 4nwv - beside the reflections it
was copied from, which the run keeps anyway. A partial is now a PartialObs: a pointer to its
reflection plus frame, prescaling_corr (with the flux divided out), the recomputed partiality and the
working corr. Every other field is read from the reflection, which nothing changes after integration
but h,k,l (read only at the ingest). Fields the CPU never read on a partial (h,k,l, delta_phi,
var_per_I, capture, group) are gone. Same values in the same arithmetic order: output unchanged.

The geometry pre-pass handed each image's reflections back right after the ingest; an engine that
reads them (ReadsReflections) now has the post-refinement view gathered once it is reset, after the
pre-pass merge - the gather reads only reflection fields, so it is the same gather.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 09:31:15 +02:00
leonarski_fandClaude Opus 5.5 43e836573d MR and model validation on the CPU: workers bounded by their bytes, not the thread count
- The CPU translation function runs as many rotations at once as the host worker pool holds (8 B per
  grid point each, 550 MiB on a 400 A cell; 33 at once at 32 threads was 18 GiB), at most nthreads.
  Counted, not reserved: the rotations run on ParallelFor's pool, whose workers must not wait on memory.
- Null replicates (model validation's and MR's) whose structure factors are made on the CPU reserve
  32 B per point of their grid (ModelGridPoints) against the pool before they start, so the nine
  replicates, and nulls run side by side, hold at most the pool between them.
Every replicate and rotation computes what it did and is stored by index: no result depends on how
many run at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 09:31:05 +02:00
leonarski_fandClaude Opus 5.5 64a87cdf52 CPU beam-centre FFT: two transforms at a time
PointSurfaces held four padded (2h x 2w) r2c workspaces and three spectra at once - ~4.2 GiB on an
18 Mpx detector at any thread count, the Mac's peak on small proteins. Two workspaces now, the image's
spectrum handed back before the squared image's is made: 24 instead of 44 bytes per padded point.
Each transform is built and run as before (own buffers, own plan), so the surfaces are bit-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 09:31:04 +02:00
leonarski_fandClaude Opus 5.5 3ebf085e4c Host memory: the machine's total, a budget decided from it, and a pool for large per-worker buffers
get_host_total_memory (sysconf / hw.memsize / GlobalMemoryStatusEx), DefaultHostMemoryBudget at 75% of
the total - decided from the total, never from what is free, as the GPU plans are - and HostWorkerPool,
a process-wide pool (a third of the budget) that workers reserve their large buffers against before
making them, so how many run at once is decided by bytes rather than threads. Only scheduling reads
any of it. [HostMemory] tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 09:31:04 +02:00
leonarski_fandClaude Opus 5.5 b004a846dd RigidBodyGPU_CallsLeaveTheCallerOnItsCard: one engine on each card with the per-card Reserve
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 09:24:40 +02:00
leonarski_fandClaude Opus 5.5 fbdf036e6f Rigid-body engines leave the calling thread on its own card; the MR search runs on its transform's card
Every RigidBodyGPUEngine call did cudaSetDevice(engine's card) and never set the caller back. Molecular
replacement's group threads refine copies on the pool's engines - on any card where every engine on this one
is taken - and then run the next translation function on the same thread. MolecularReplacementGPU ran its
calls on whatever card the thread was on, so that function's buffers and cuFFT plan were made on the
engine's card, beside the transform and the stream on the search's card: an illegal memory access, 9s02 with
four cards visible, every run (the further-copies loop: refine a trial on another card's engine, then
translation_function on the group thread).

- Each engine call (and the constructor) now holds a DeviceGuard: on the engine's card for the call, back on
  the caller's after it.
- MolecularReplacementGPU remembers the card its transform was uploaded to and runs every call (and its
  destructor) there, whichever card the calling thread is on.
- RigidBodyGPU_CallsLeaveTheCallerOnItsCard: one engine per card, all taken from one thread; the thread's card
  is unchanged after each call (meaningful with two or more cards).

No number changes: where every call runs is the same card as before for each object.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 09:24:25 +02:00
leonarski_fandClaude Opus 5.5 a3f30d03c1 Rigid-body engines leave the calling thread on its own card; the MR search runs on its transform's card
Every RigidBodyGPUEngine call did cudaSetDevice(engine's card) and never set the caller back. Molecular
replacement's group threads refine copies on the pool's engines - on any card where every engine on this one
is taken - and then run the next translation function on the same thread. MolecularReplacementGPU ran its
calls on whatever card the thread was on, so that function's buffers and cuFFT plan were made on the
engine's card, beside the transform and the stream on the search's card: an illegal memory access, 9s02 with
four cards visible, every run (the further-copies loop: refine a trial on another card's engine, then
translation_function on the group thread).

- Each engine call (and the constructor) now holds a DeviceGuard: on the engine's card for the call, back on
  the caller's after it.
- MolecularReplacementGPU remembers the card its transform was uploaded to and runs every call (and its
  destructor) there, whichever card the calling thread is on.
- RigidBodyGPU_CallsLeaveTheCallerOnItsCard: one engine per card, all taken from one thread; the thread's card
  is unchanged after each call (meaningful with two or more cards).

No number changes: where every call runs is the same card as before for each object.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 09:24:16 +02:00
leonarski_fandClaude Opus 5.5 5d8c6f4504 battery: the wrong-model null report lists pairs that were never scored
A pair that crashed, exited non-zero or had no coordinates carries only set,
model, verdict and reason; section() read the scored fields directly, so a
single crashed pair raised KeyError 'data_same' while the report was written.
The step then died with a traceback instead of its own pass/fail message, and
the following "Keep the report" step found no report. Those rows now show '-'
in the scored columns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 09:13:28 +02:00
leonarski_fandClaude Opus 5.5 38e75c8e5b HDF5Writer_Socket: wait for the subscription before the first file is announced
The test connected its SUB socket and wrote at once; file 1 is closed and
announced on the PUB socket right after the second image, and PUB drops
whatever it sends before the subscription has reached it (slow joiner). On a
loaded CI runner the first notification was lost and the test read file 2
where it expected file 1. Wait 2 s after subscribing, as the preview and
metadata socket tests already do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-17 09:06:54 +02:00
leonarski_fandClaude Opus 5.5 0f461a65a2 Merge mr-transform-layout: the corner-packed molecular transform beside MR on every card
Build Packages / Create release (push) Successful in 7s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 4m38s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 8m58s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 14m20s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 18m0s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 21m31s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m55s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 21m49s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 13m41s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m30s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 26m7s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 15m22s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 21m54s
Build Packages / Generate python client (push) Successful in 31s
Build Packages / Build documentation (push) Successful in 1m13s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 18m45s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 23m54s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 25m56s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 25m47s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 20m33s
Build Packages / Unit tests (push) Failing after 1h28m5s
Build Packages / Validation Battery (push) Successful in 34m34s
Combines mr-multigpu (per-card engine budgets, a transform copy on each other
card, the rotation/translation functions split over the cards) with
mr-transform-layout (the GPU transform optionally held corner-packed, same
bits, 2.2x faster rotation function).

Conflict (MolecularReplacement.cpp): the two new functions side by side
(PackMolecularTransformGPU next to SplitRotationFunction/SplitTranslationFunction).
On top of the textual resolution:
- One layout decision for all cards, not one per card: the functions are split
  into equal slices, so a mix of packed and plain cards would run at the speed
  of the plain ones; either layout gives the same bits.
- PackMolecularTransformGPU now plans with the other cards' budgets too and packs
  only where every card keeps what the plain plan gives it: this card's
  transform, engines and score engine, and on every other card its copy and its
  engine count. A packed copy is made after the card's engines are reserved;
  copy and engines within half of that card's budget leave room for the plain
  grid it is made from.
- The copies on the other cards are made in the chosen layout, and the per-card
  plan (copies, engines, room) is made with the bytes actually placed.
- Test: four 15.6 GB cards stay plain (packed, each other card would fall from
  T+2 to T+1); with 24 GB cards beside, packed.

So on a 4x T4 4nwv runs split and plain, on one card packed; whether giving up
3 of 7 engines for the packed rotation function would pay there is left to a
server measurement.

Workstation (1x RTX 5080, --gpulock), against rc176: battery timing
--benchmark standard (9zlo, 9s02, 9gjx, 4nwv) and --benchmark inhouse (myob,
cytc, 4nwv) on GPU, inhouse also --no-gpu: p.hkl, p_model.pdb and the MR_
report block byte-identical on every set. 4nwv GPU: rotation 5.53 -> 2.50 s,
MR step 20.1 -> 16.5 s (packed, 2.28 GB in place of 0.84 GB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-16 18:46:58 +02:00
leonarski_fandClaude Opus 5.5 2b7907d767 Merge mr-multigpu: MR and model validation use every card's memory, MR's functions split over the cards, output unchanged
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-16 17:56:21 +02:00
leonarski_fandClaude Opus 5.5 3aa67a6059 MR GPU: corner-packed molecular transform for the rotation function
Each sample of the molecular transform interpolates over 8 grid corners that
lie in four u-rows of the half-l grid, 4-5 32-byte sectors per sample. On the
GPU the transform can now be held packed: every point (i, j, l) stores its four
(v, w) corners, so the two neighbouring records hold all eight, read from two
places. The packed grid is cut to |u| <= reach as the plain one already is in
v, and has the conjugate plane l = -1 in front; below l = -1 the eight corners
are read mirrored about -i0 - 1 and conjugated. Same floats, same accumulation
order: rotation and translation functions give the same bits (checked in
MolecularReplacement_GPUMatchesCPU).

The layout is chosen up front from the budget (PackMolecularTransformGPU):
packed where the answer merge's plan holds every part beside it that it holds
beside the plain transform, and the budget holds both grids while the packed
one is made from the plain one on the device; logged either way. The upload no
longer stages a host copy of the grid. The CPU At() is unchanged: a
microbenchmark gained 1.3x single-threaded and ~1.0-1.1x at 16-32 threads,
not worth 2.7x the host memory.

4nwv (moved model, timing benchmark), RTX 5080: rotation 5.56 -> 2.48 s, MR
step 20.9 -> 16.7 s; transform on the card 0.84 -> 2.28 GB. p.hkl, p_model.pdb
and the MR_ report block byte-identical on the timing sets (GPU and --no-gpu)
and on 4nwv, 8tyy, 6oel, 3inp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-16 17:55:30 +02:00
leonarski_fandClaude Opus 5.5 bfc6ca8487 MR and model validation: use every card's memory, split MR's functions over the cards
On a multi-GPU host the structure stage ran essentially on one card: the rotation
function's launches all went to the search's card, and the rigid-body engines of
model validation and MR were admitted only within half of that one card's budget,
although RigidBodyGPUPool already placed engines on other cards. On a 4x T4 server
4nwv's MR rotation function took 75.8 s on one card, validation queued its 10 fits
on one engine and MR's rescoring and null got one engine.

- PlanModelValidationGPU / PlanMolecularReplacementGPU: this card is planned
  exactly as on a machine with it alone (GPU-vs-CPU placement unchanged: 80% of the
  total, less what is resident). The rigid-body engines it has no room for go on the
  other cards in turn, each against its own budget (80% of its total, less the
  resident bytes, counted on every card) within half of it. MR also puts a copy of
  the molecular transform on each other card within half of its budget.
- RigidBodyGPUPool::Reserve takes per-card engine counts.
- Validation forwards the most it holds on any card as gpu_bytes to a validation
  beside, and may run one beside only where twice that fits every card's budget.
- MR splits every rotation and translation function into contiguous slices of
  the orientation list over the cards holding a transform copy (and room for the
  call's buffers), one thread per card, gathered back in order. Each orientation
  is scored by itself (one block / one map), nothing is summed across
  orientations, so N cards give the bytes of one.
- ScoreEngines keeps a room per card; score engines always go on the search's
  own card; function buffers are held on the card they run on.

Byte-identical at N=1 (workstation, one RTX 5080): timing benchmark (myob, cytc,
4nwv) and 4nwv, 8tyy, 6oel, 3inp with --model give the same p.hkl, MTZ data,
p_model.pdb, maps and report against rc176. New tests: per-card plans on four
15.6 GB cards (validation 1/3/3/3 engines, MR T+1/T+2/T+2/T+2), and the split
driven over 1-4 virtual cards on one device, bitwise equal to one card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-16 17:16:28 +02:00
leonarski_fandClaude Opus 5.5 234774ff09 battery: a "standard" timing benchmark on open data, with every model committed
Build Packages / Create release (push) Successful in 7s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 4m30s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 11m43s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 7m21s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 14m56s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 20m57s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m54s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 15m14s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 11m35s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 26m0s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 17m21s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 15m35s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 20m31s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 20m41s
Build Packages / Generate python client (push) Successful in 28s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 22m5s
Build Packages / Build documentation (push) Successful in 1m0s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 14m33s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 21m10s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 15m21s
Build Packages / Unit tests (push) Successful in 1h26m10s
Build Packages / Validation Battery (push) Failing after 1h6m19s
battery.py timing --benchmark standard (now the default) runs 9zlo, 9s02 and
9gjx from their AlphaFold DB models (AF-P17090-F1, AF-P32322-F1, AF-Q65MG6-F1
v6, with their PAE, as the 2026-10-16 AlphaFold MR scan used them; all three
GREEN there) and 4nwv from its deposited model moved by the existing fixed
transform. --benchmark inhouse keeps the earlier myob_x10sa / cytc_x10sa /
4nwv list. Same checks; expected groups are the deposited ones, R-free <= 0.50
for the predictions (0.35 for 4nwv), placement CC >= 0.30.

So that a machine needs only the raw data:
- the deposited coordinates of 9zlo, 9s02, 9gjx, 4nwv and 4dc8 (the inhouse
  myoglobin reference) are committed in tools/battery/models/pdb/ as the
  compressed mmCIF RCSB serves (CC0; 50-660 kB each, 1.6 MB together), and
  model_check.deposition reads an entry there before the pdb_cache, taking
  its refinement record from the file itself - no cache JSON, no network;
- open/inhouse arms default their manifest and reference, so a site config
  is just runs_root plus the data roots (site.timing.example.json);
- baseline_timing is now per benchmark ({"standard": RUN, "inhouse": RUN}).

Verified on the workstation through the GPU lock with a site config whose
pdb_cache is an empty directory (still empty afterwards): standard 4/4 pass
(9zlo 19 s, 9s02 30 s, 9gjx 21 s, 4nwv 89 s), inhouse 3/3 pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-16 16:49:04 +02:00