Commit Graph
303 Commits
Author SHA1 Message Date
leonarski_fandClaude Opus 5.5 07eecb3285 Merge branch 'mr-7qij' into rc175: --mr (predicted and experimental models), MR tNCS half-shift check, P1 single copy, carry rule, MR 2-4x faster
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 20:08:43 +02:00
leonarski_fandClaude Opus 5.5 30c7bee15f rugnux: --af-model becomes --mr, for predictions and experimental structures alike
- The option is --mr <file>. What kind of model it is is read from the file: a prediction where it
  is ModelCIF (_ma_qa_metric) or has no cell and one B value per residue in 0-100 (AlphaFold PDB/mmCIF);
  an experimental structure otherwise. A prediction is handled as before (pLDDT >= 70 trim, B from
  pLDDT, PAE domains where the AlphaFold DB's PAE file sits beside it). An experimental structure is
  taken as its polymer - no waters, ligands, hydrogens or alternative conformers, occupancy 1, isotropic
  B as refined - with a coordinate error of 0.67 A (a prediction's at pLDDT 90) for sigma_A; a B column
  above 100 no longer stops the search.
- The report section is MOLECULAR REPLACEMENT (EXPERIMENTAL), its keys MR_* (MR, MR_MODEL_FILE,
  MR_MODEL_KIND, MR_DECISION, MR_SPACE_GROUP, MR_COPIES, MR_HAND_CHECK, MR_SECONDS, ...); the <prefix>_mr
  outputs are unchanged.
- The twin hypothesis' merge that is made only for the model to test (a metric re-ask promoted the
  indexed lattice's group) is written and reported only where the model chooses the subgroup.

Raw deposited coordinates of nine control sets, unconverted: every one placed and confirmed against the
deposition (all chains as one body), the groups the same as with the prediction or the prepared model.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 19:30:22 +02:00
leonarski_fandClaude Opus 5.5 e1f49dd729 rugnux --af-model: the further copies' translation function prepared once per group, the known copies' likelihood beside it
From the nsys/perf profiles of the copy chain (GPU busy 39% between copies, the thread mostly in host
work and syncs):
- The known-structure translation function's lists, device arrays and cuFFT plan are made once per
  group (MolecularReplacementGPU::Prepare) instead of on every copy (~260 MB of uploads and a plan
  per call before).
- The known copies' CopyFactors and likelihood, which rank the peaks, are taken while the GPU runs
  the translation function.
- The structure-factor engines are made per entry, without one lock held across all groups' creation.
- The transform's index wrap divides only for an index outside the grid (CPU and GPU).
All exact: same arrays, same kernels, same order. 4mtk MR, alternating 3 pairs: 9.5 -> 9.0 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 19:30:22 +02:00
leonarski_fandClaude Opus 5.5 4f5e419d79 Merge branch 'spot-width' into rc175: pre-scan spot measurement on per-spot tiles, engines built during the read, tiers looked ahead, repeated pre-scans memoised
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 18:18:19 +02:00
leonarski_fandClaude Opus 5.5 9da57ba559 rugnux --af-model: beside tNCS, a poorly fitting placement cannot rule out a group by carrying its copies there
Drops the rule of af796d0a6 that the merge's own group searched to fewer copies always leaves the
choice UNDECIDED: it also caught a correct SPACE_GROUP decision (a target set; the half-vector move
check already catches the wrong one it was meant for). Instead, the chosen copies carried into a
tNCS-related group rule that group out only where the chosen placement fits the data itself
(R-free below 0.50): a perfect deposited model placed in the wrong group at R-free 0.520 had ruled
out the deposited group by 0.036; it is UNDECIDED now. The carries that decide in the survey all
start from R-free 0.36-0.47 and lead by 0.04-0.10.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 18:06:23 +02:00
leonarski_fandClaude Opus 5.5 af796d0a6a rugnux --af-model: tNCS half-vector moves and P1's first copy decided on the evidence that can tell them
Completes c6912e2a0 (whose message was a placeholder). What the two commits do:

- tNCS by half a lattice vector v: copies v apart weaken the reflections with h.v half-integral, and a
  placement moved by (v + o)/2 (o an origin shift of the group) fits the strong ones exactly as well.
  The candidate the choice falls on is compared with every such move of itself (rigid body refined,
  packing) by R over the weakened reflections only; a move better by 0.01 is taken instead, and where
  the two best are within 0.01 the merge is UNDECIDED.
- With tNCS, the group the merge was made in, searched to fewer copies than the chosen group, keeps
  the merge UNDECIDED (not for its enantiomorph, which the hand check decides): its first copy alone
  can sit at half the vector, and no placement of the chosen group, which may sit there itself, rules
  it out.
- P1: the first copy sits at the origin, which no function of its position can single out (the
  translation function is flat), so one copy stands out where its rotation-function Z reaches 8; the
  null then judges it as before.

Checked against the deposited coordinates (placement comparison, symmetry/origin/setting matched):
two half-vector-shifted placements that had been decided (a SPACE_GROUP in the wrong setting, an
AGREES off by a/4) are UNDECIDED; two UNDECIDED tNCS sets are now AGREES with confirmed placements;
the nine confirmed tNCS placements and the ENANTIOMORPH stay; a P1 single copy with a perfect-match
prediction is placed (confirmed), a wrong PAE-domain placement in P1 (RF Z 6.4) is not.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 17:34:28 +02:00
leonarski_fandClaude Opus 5.5 6ee167df9a Merge rc175 (the null turns each copy about its own centroid) into mr-7qij
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 16:49:20 +02:00
leonarski_fandClaude Opus 5.5 c6912e2a06 rugnux --af-model: tNCS half-vector moves checked, the merge's own group not ruled out beside tNCS, P1's first copy judged by its orientation (WIP, validation pending)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 16:49:02 +02:00
leonarski_fandClaude Opus 5.5 babff6d8bf rugnux --af-model: the null keeps the copies' arrangement
The null a placement must beat turned the whole assembly about its common
centroid, so with several copies it also moved them off the arrangement the
search found. Under tNCS, two parallel copies a tNCS vector apart fit the data
whatever the model is, and the null threw that fit away: wrong deposited models
reached TF Z 14-47 and 8-34 sd over the null on tNCS crystals, and two of them
decided the space group (SPACE_GROUP, a reflection file in the wrong group).

Each copy is now turned about its own centroid, all by the same rotation, so
only the model's own orientation is random. On a control of 38 open-arm sets
(deposited chain randomly re-oriented as search model, 3 unrelated deposited
models and a fold-scrambled decoy per set) the wrong models' null falls to
1.4-4.2 sd on the tNCS sets (one at 7.2, left UNDECIDED), both wrong decisions
become NOT_PLACED, and no right-model or AlphaFold-model decision changes
(nulls stay 15-107 sd). The 193-set AF survey replay loses no GREEN (two
UNDECIDED become NOT_PLACED); the four twin/tNCS targets stay correct.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 16:47:07 +02:00
leonarski_fandClaude Opus 5.5 6c8ead466a rugnux --af-model: molecular replacement faster - the score's scale fit on the GPU, the expected LLG gate, the serial parts side by side
The search was CPU-bound: on a 54-chain partial complex ~80% of all CPU samples were the score's
FitModelScale grid (and libm's exp under it), with the GPU busy 13 of 37 s.

- score_placement fits its scale on the GPU where its structure factors are made there
  (ModelScaleGPU's FitSolvent / Fit, as the rigid body already does); equal to rounding, the CPU
  build unchanged.
- A model whose expected log-likelihood gain (sum sigma_A^4 / 2, McCoy et al. 2017; sigma_A^2 the
  part of the scattering one copy is by Matthews, times Luzzati's D^2) is below 10 is not searched
  for. Over the 193-set survey the lowest eLLG of a placed model is 26; every PAE domain below 24
  placed nowhere, so no decision changes.
- The likelihood setups made where first needed, one serving both where the reflections are the same.
- After a candidate fails its null, the other placed candidates' nulls side by side.
- The first copy's placements refined side by side on an engine each; the groups' translation
  functions side by side; CopyFactors and KnownStructureFactors over threads; the further copies'
  CopyFactors and gains for the peaks walked all at once; the known-structure TF setup made while the
  first copy is refined. All exact.

Survey replay (193 sets): no decision changes; two R values differ in the 3rd decimal (GPU scale).
Alternating runs, MODEL_PRIOR_SECONDS base -> new: 54-chain complex 38.3 -> 14.7, 4mtk 18.3 -> 9.7,
9v16 13.7 -> 8.3 (shared machine, indicative).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 16:39:03 +02:00
leonarski_fandClaude Opus 5.5 545cf3b13e rugnux pre-scan: spot width on per-spot squares, engines built during the read, tiers looked ahead, measurements memoised
The spot-width stage of the pre-scan copied every preprocessed frame back to the host (72 MB a
frame on a 16M detector, page-locked host mirrors per worker) for measurements that read a few
pixels about each spot. Now:

- MeasureSpotsOnTiles: the flux curves and the footprint read (2*24+1)^2 squares about each spot,
  gathered on the card (ImagePreprocessorBufferGPU::GatherTiles) - no copy_image_to_host, no host
  mirror. The flux curves are bounded at R_BKG_OUT = 20 px. The footprint keeps its arithmetic; a
  per-iteration check that its window is inside the square, and a spot whose window walks off it is
  measured again from scratch on an 80 px square, then on the whole frame. Where the squares would
  be more pixels than the frame, the frame is read. Same pixels in the same order, so the same bits.
  Harness over 34 rotation sets (~1500 frames): 0 differences; misses at 24 on a few spots of most
  frames, at 54 on up to 80% of frames of a few sets, at 80 on none.
- The azimuthal mapping and the per-worker spot engines are built while the frames are read.
- The tiers no longer wait for each other: workers run on through the visit order, each tier's
  answer is drawn from exactly its own frames, and frames measured past the tier the answer stops
  at are thrown away - a frame is measured the same whichever tier it is read for.
- PreScanMemo keeps the spot measurement, the defective pixels and the background gain under the
  pre-scan key plus the spot-finding and azimuthal settings, so a copy of the run that pre-scans
  again on the same inputs reads no frames.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 16:11:14 +02:00
leonarski_fandClaude Opus 5.5 fa79d7587b Merge branch 'af-mr4' into rc175: --af-model likelihood rescoring, PAE domain search, twin comparison on the subgroup merge, refined tNCS rule
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 13:45:46 +02:00
leonarski_fandClaude Opus 5.5 b28dff47e4 Merge branch 'merge-layout' into rc175: merge engine observations as a packed 16-byte record, the scale preparation folded into the per-frame fit
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 13:37:40 +02:00
leonarski_fandClaude Opus 5.5 0d33d7a40a rugnux --af-model: likelihood judging, PAE domains, twin comparison, tNCS carry test, faster multi-copy search
- Likelihood (Read 2001; Z-score over random positions as McCoy et al. 2005): log-likelihood gain of
  Rice/Woolfson E distributions, sigma_A from the pLDDT coordinate error with its scale refined, to
  2.5 A (or the 20000 lowest-resolution reflections). Further-copy TF peaks ranked by the gain on the
  search's reflections (peaks that lower it dropped); a copy whose TF Z is below 8 also stands out where
  its refined likelihood Z-score reaches 8 - only where the copies placed are as many as Matthews makes
  most likely. The likelihood is computed only where a translation function does not stand out.
  Copy structure factors exact from one FFT of the cell's grid, or read off the molecular transform
  where it reaches the likelihood's resolution.
- PAE domains: the AlphaFold DB's predicted_aligned_error JSON beside the model (same name pattern) is
  read automatically; average linkage on the symmetrised PAE (10 A, domains >= 40 residues). Where the
  whole model is placed nowhere, each domain is searched by itself side by side; placed domains must
  agree, and a domain alone may only confirm the answer's group.
- Twin comparison: where the answer's merge chose and the twin hypothesis' merge exists, the chosen
  placement read as a crystal in the subgroup (its symmetry images as copies, the subgroup's setting
  found from the two cells) against the model searched for afresh in the subgroup, both against the
  data twinned by the law (fraction refined on the working set, Yeates 1997); the subgroup is taken by
  a 0.03 R-free lead plus its own null/margin -> TWIN_SUBGROUP, written to p_mr with law and fraction.
  With --af-model rugnux makes the subgroup merge wherever a metric re-ask promoted the indexed group.
- tNCS rule: a group with fewer copies beside tNCS (screws bridged by the vector, or the enantiomorph)
  no longer leaves the choice undecided where the chosen copies, carried into it and refined, fit
  worse by 0.03 in R-free.
- Speed: the molecular transform's FFT on the GPU (cuFFT, serialised), no rigid body for trials that
  lower the likelihood, re-search only with tNCS, PAE domains searched in parallel.

Survey (193 sets, MR replay on stored merges): single GREEN 58 -> 68, multi 48 -> 58, partial 8 -> 9;
4bwl (and, in full runs, 2wnq and a withdrawn entry of the same protein) TWIN_SUBGROUP P21 instead of
a wrong AGREES; a P21212 set with tNCS now SPACE_GROUP P21212 instead of UNDECIDED; no wrong call.
CPU and GPU builds make the same decisions on 18 demo/multi-copy/twin sets.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 13:21:51 +02:00
leonarski_fandClaude Opus 5.5 7d656c13de rugnux merge engine: the immutable fields the scaling gathers in one 16-byte record
The group reduction reaches every observation through the group permutation, and so fetched one
32-byte sector per field for I, sigma, partiality and corr. I, sigma, partiality and prescaling_corr
now sit together in one aligned record, so it fetches two sectors per observation; the grouping
kernels' finiteness test and the combine read the same record. The per-observation fit coefficient,
its flag and 1/sigma are no longer stored: the per-frame fit forms them as it reads each observation
(the same expressions, so the same bits), which removes PrepScaleObsKernel and 13 bytes per
observation of device memory. DeviceBytes' per-observation figure drops by those 13 bytes.

Every sum keeps its order: p.hkl md5 identical to the base on cytc, 8a1a and 8qaw.
RTX 5080, nsys, merge-engine kernels: 8a1a 2459 -> 1924 ms, 8qaw 3624 -> 3025 ms, cytc 501 -> 476 ms
(ReduceGroupMeans 1.5-1.6x faster, PrepScaleObs gone, FitPerFrameG absorbs it).

Measured and not taken: storing all per-observation arrays in group order makes the reduction 3-4x
faster on its own, but the per-frame fit then has to gather, and moving 15 arrays at every grouping
costs as much as the reduction saves (net 0 on the 5080); a record for the combine-only fields
did not speed the combine.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 11:21:49 +02:00
leonarski_fandClaude Opus 5.5 70e11d9ab8 rugnux pre-scan: projection and defective pixels per card, the sample read once, beam stop memoised
The beam-stop projection (ShadowAccumulatorGPU) and the defective-pixel sums (HotPixelFinderGPU) are
now kept one per card, fed by workers pinned t % gpus like the other pre-scan workers, and folded
into the first card when they are read (cudaMemcpyPeer + an integer add / max-with-count kernel).
All of them are integer sums or a maximum with the first-value rule, so the fold is order-free and
the result is identical to one card's. With one card nothing is folded.

The pre-scan sample is read once and kept (compressed as stored; up to 2 GiB, a frame past that is
read again where it is wanted) for the spot measurement and the defective pixels, which read every
frame again each before - a CBF/marCCD/SMV frame was decoded on the host three times.

A beam-stop detection that finds no shadow leaves the mask as it was, so every probe copy of the run
repeated it on the same inputs. Its answer (shadow, ring centre, mean projection) is now kept in a
memo shared with the copies, keyed on the sampled frames, the geometry the rings are drawn in, the
polarization and the binary mask; up to 4 entries.

Debug TIMING lines for the pre-scan's parts (read and projection, beam stop, defective pixels, spot
measurement). New tests: two accumulators/finders folded match one fed every frame.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 08:11:05 +02:00
leonarski_fandClaude Opus 5.5 fcbe92042b Merge branch 'af-mr3' into rc175: --af-model searches several copies per asymmetric unit (Matthews probability, sequential translation function, fixed-copy rigid body, tNCS)
Build Packages / Create release (push) Successful in 7s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 3m23s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 10m24s
Build Packages / build:portable:linux-x86_64:cuda (push) Failing after 12m20s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 13m30s
Build Packages / build:portable:windows-x86_64:nocuda (push) Failing after 16m11s
Build Packages / build:portable:windows-x86_64:cuda (push) Failing after 19m38s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 22m18s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 23m27s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 17m12s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 27m57s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 20m3s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 23m18s
Build Packages / Generate python client (push) Successful in 39s
Build Packages / Build documentation (push) Successful in 1m18s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 22m7s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 18m24s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 23m53s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 22m44s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 18m58s
Validation Battery / Validation Battery (push) Successful in 59m36s
Build Packages / Unit tests (push) Successful in 1h30m17s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 01:36:26 +02:00
leonarski_fandClaude Opus 5.5 63e0a85467 rugnux --af-model: several copies in the asymmetric unit, tNCS, a search-matched null, faster
Copy number. The hard "Vm >= 4: MULTI_COPY, no decision" rule is gone. Copies
are searched one after another up to the largest copy number with a Matthews
probability of 1% (a Gumbel distribution with Kantardjieff & Rupp's 2003 mode
and mean, no resolution dependence); COPIES_LIKELY reports the most probable.

Sequential search. Copy n's translation function carries copies 1..n-1 as a
known partial structure: the cross term 2 Re(conj(F_known) F(t)) goes on the
same FFT grid (CPU FFTW and GPU cuFFT; KnownStructureFactors, AssemblyCorrelation).
Its peaks, past those that clash, are ranked by the correlation of the whole
assembly with E^2 (the TF itself, linear in |F|^2, rewards copying a placed
copy onto an origin shift), the first that stands out (TF Z >= 8), packs and
lowers R-free by 0.01 is kept; each copy is refined beside the others held
fixed (RefineRigidBody's fixed copies), and the first copy again beside the
second, the earlier ones once more at the end.

Packing: ClashFraction counts C-alpha clashes with other copies and all their
mates, binned (7mzt: 51 s of clash checks -> 1.9 s).

Search quality: rotation-function peaks hill-climbed off the 6 deg sampling
(6ze4: RF Z 10.4 -> 16.0, 3.3 deg off the deposited orientation -> 0.2); the
first copy goes down the TF list until three placements that pack are
refined, so a group is CLASH only where no offered orientation packs; groups
are searched side by side, and every group the best one leads is searched
again with the best one's copy orientations tried first, the better search kept.

tNCS: the native Patterson of E^2 (5-10 A) off the lattice; placed copies'
orientations join every further copy's search; and where the tNCS vector turns
one group's screw axes into another's (TncsRelatesGroups), a group whose search
stopped at fewer copies does not lose to the other - UNDECIDED. Measured on an
in-house tNCS crystal (vector c/3 along the 3-fold): both hands fit with three
copies to within 0.01 in R-free, and where one hand's search failed the other
was taken as ENANTIOMORPH; now UNDECIDED.

Null: the replicates are placed by the translation function in their own
orientation, as the search placed the real one (a null at the real centroid
reads high), and the gate is 4.51 = t(8) sqrt(1 + 1/9), 3 sd of a nine-draw
null, the gate --model's MODEL_FIT is now read at.

Speed: placements scored on the GPU structure-factor engines; further copies'
trials score with the first copy's bulk solvent held, the assembly refitted at
the end. Survey of 193 sets (jfjoch_mr_replay on stored merges, RTX 5080
shared with other jobs): MR median 1.15 s, max 24 s; demos 0.8-1.1 s.

Report: COPIES and COPIES_LIKELY columns, MODEL_PRIOR_COPIES.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 00:27:17 +02:00
leonarski_fandClaude Opus 5.5 dcb01100e7 RefineRigidBody: a body refined beside fixed copies of it
An optional `fixed` model: other atoms of the crystal held where they are -
the other copies of a molecular-replacement search - whose Fcalc is added to
the body's at every evaluation and whose atoms the bulk-solvent mask covers
together with the body's (computed once per zone, with the body where the
zone starts, and held). With them the origin is no longer free, so the gauge
projection is skipped. On the CPU the fixed part is gridded once per zone; on
the GPU the fixed atoms must be copies of the body, and each one's placement
is read off by Kabsch superposition and composed on the device with the
engine's own density, transform and mask (SetFixedCopies), so a fit beside
three copies of a 90 kDa model costs what a fit alone does.

Without `fixed` nothing changes (the composition kernel's extra term is null).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 00:26:49 +02:00
leonarski_fandClaude Opus 5.5 ee350f1f74 rugnux --model: a wrong model confirms no shell, fits no hand, and fits the GPU reserve
Three defects the battery's wrong-model null found (5 sets x 5 deposited models of unrelated proteins):

CC(model, data) significance. The per-shell sigma was Fisher's transform on independent reflections,
but the data and a scaled model share the falloff across a shell, the anisotropic B fitted to these
very data, the bulk solvent and the symmetry enhancement epsilon - whatever the model is. 21 of 24
wrong models read CC_MODEL_CONFIRMED_TO_D_MIN to the data's d_min (10-15 sigma in most shells).
The correlation is now formed on I/epsilon and |F_model|^2/epsilon, and its null is empirical: the
data at h against the model at h + d for the 26 nearest lattice offsets, which keeps everything smooth
in reciprocal space and loses what the model knows of h. SIGMA = (z - mean) / max(sd, 1/sqrt(N-3)) in
Fisher z over those 26, a new CC_NULL column shows the null, and a shell confirms only beside a coarser
shell that also clears 3 sigma (one shell alone did so once in 216 wrong-model shells). Prototyped on
the p_maps.mtz of a full open-arm run and the null runs: wrong models confirm 0/24 (was 21/24), the
deposited models confirm 197/200 (was 198/200 in the prototype's reading; the one lost has CC_MODEL
0.05-0.13 in every shell). The report no longer says a model cannot agree by accident.

MODEL_FIT gate. Accepted at 3.0 sample sds of a nine-draw null, as if the sd were known; an unrelated
P1 helical model on a P3(1) helical crystal was ACCEPTED at +3.05. Against 48 draws it reads +2.09 and
the best random placement fits as well as it does, and drawing the null about the cell axes instead
changes nothing (+2.15): the cause is the nine-draw sd, not the orientation. The gate is now 3 sigma
allowing for that: t(8) * sqrt(1 + 1/9) = 4.51. The deposited models of the open arm read +7.0 and up.
The indexing lead stays at 3 sigma (INDEXING_MARGIN_SIGMA): it is read only behind the fit gate, and
a deposited model settles one at +4.15.

Rigid-body GPU reserve. The finest zone's observations were counted by the data's d, but the zones
count them by the d of the index in the model's ASU, and a model's group need not keep the data's
metric (an I2(1)3 model on I222 data: 2320 in the zone against 1712 reserved), so the validation died
with "zone over the engine's reserve". Counted the zones' way, under every indexing probed.

Tests: ModelValidation_CCModelUnrelatedModelConfirmsNothing (Fisher alone reads +4.7 on an unrelated
model with an anisotropic falloff; the new sigma stays under 1), ModelValidation_WrongModelInAGroupOffTheDataMetric
(fails with the old count), ResultReport_ModelValidationSection (CC_NULL column, lone-shell rule).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-11 21:30:34 +02:00
leonarski_fandClaude Opus 5.5 f800669839 jfjoch_mr_replay: --af-model's molecular replacement re-run on a run's merged MTZ files
A developer utility, not installed: the search, the decision and the hand
check run on the answer's p.mtz (and the twin hypothesis' p_twin_<group>.mtz)
that a rugnux run wrote, so a survey of the search over every dataset with a
predicted model takes minutes instead of a reprocessing per set. The hand
check of an ENANTIOMORPH decision moves from the rugnux driver into
CheckEnantiomorphHand, which both call; unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-11 20:50:47 +02:00
leonarski_fandClaude Opus 5.5 323c5253de rugnux --af-model: each candidate space group once, not again on a shifted origin
gemmi's table lists some groups a second time on another origin ("P 42 21 2a", "P 21212(a)",
"I 2 2 2a", "C 1 21 1"). SameRotationGroups kept them as separate candidates, so the true group tied
with itself in R-free and the choice came out UNDECIDED: five single-copy sets of the --af-model
survey (and one partial-model set) ended there. A group equal to one already listed up to an origin
shift on the twelfths is now left out.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 19:09:27 +02:00
leonarski_fandClaude Opus 5.5 50e3b59fb7 rugnux --af-model: hand check reads only the anomalous map, on MR's placement and solvent
The ENANTIOMORPH hand check ran a whole ValidateAgainstModel on the placed model: model read and frame
probe, a rigid body, a 9-replicate null (~0.5 s of the ~0.6-0.9 s), sigma_A, three maps written. The
decision needs only --model's rule: the model fits its null (the MR choice has already required its
own null of the same construction) and the anomalous map at the model's atoms is not inverted.

ReadModelAnomalousMap (ModelValidation) computes F_calc and F_mask to the data's resolution (GPU engine
where there is one), carries them through the bulk solvent MR scored the placement with (the overall
scale and anisotropic B do not move phases), and reads the anomalous map with the same code as the
validation: its coefficient construction and its reading at the atoms are factored out of Validate
(anomalous_coefficients, read_anomalous_map) and used by both, so plain --model is unchanged.

Hand check 0.6/0.6/0.9 s -> 0.027/0.028/0.040 s on 11if/7ris/9zmu; same ADOPTED/VETOED on all seven
cases tried (the three demos, two anomalous lysozyme sets and their mirrored-detector runs), with the
scatterer means and deepest troughs within 0.1 sigma of the full validation's.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 18:11:01 +02:00
leonarski_fandClaude Opus 5.5 2a3d60fb82 rugnux --af-model: anomalous veto on the enantiomorph, search on the GPU, cheaper rescoring
- An MR ENANTIOMORPH decision is taken into the answer's label only as --model takes a model's
  enantiomorph: ValidateAgainstModel runs on the placed model (in a scratch directory, removed after)
  and its adopted_model_enantiomorph - fit over its null, anomalous map at the atoms not inverted -
  decides; otherwise ENANTIOMORPH_VETOED. The hand-check numbers go to the MODEL PRIOR section.
- Rotation and translation functions on the GPU (MolecularReplacementGPU.cu): one block per
  orientation for the rotation function; per orientation the copies, the bin weights, a gather of the
  Crowther-Blow coefficients (CSR lists, fixed order, no atomics) and a cuFFT C2R, block-reduced
  statistics finished on the host in a fixed order. The CPU path (FFTW) is unchanged in what it
  computes; the orientation-independent part of the translation function (TranslationSetup) is now
  built once per group on either path.
- Rescoring: a placement clashing with its mates over twice the limit is not refined, one rigid-body
  engine per group, and the null is built only for the candidate the choice falls on.

Measured MR step on the RTX 5080 (before -> after, seconds; hand check extra where it runs):
3r6o 1.77 -> 0.67, neg 2.14 -> 0.37, 11if 1.20 -> 0.51 (+0.6 hand check), 7ris 1.46 -> 0.56 (+0.6),
9zmu 4.71 -> 1.07 (+0.9). Rotation 0.50 -> 0.02 s and translation 1.17 -> 0.13 s on 9zmu.
Mirrored-detector runs of two anomalous lysozyme sets: the hand check vetoes (map inverted at the
model's atoms); the unmirrored runs adopt.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 17:10:48 +02:00
leonarski_fandClaude Opus 5.5 57fef1cf44 Merge branch 'af-mr' into rc175: experimental --af-model, single-copy molecular replacement deciding open symmetry hypotheses into p_mr.mtz
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 14:38:08 +02:00
leonarski_fandClaude Opus 5.5 62b7d8b2b9 Merge branch 'u5-ingest' into rc175: merge ingest reads the reflections 4 times instead of 7; the all-observation arm copies the staged observations on the card
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 14:26:01 +02:00
leonarski_fandClaude Opus 5.5 8dbaa9f8d7 Merge branch 'fft-admit' into rc175: FFT indexer construction waits for GPU work beside it before failing
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-11 14:06:12 +02:00
leonarski_fandClaude Opus 5.5 136b1f24e9 Merge branch 'decode-bounds' into rc175: decode kernel launch bounds allow five resident blocks on sm_120
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-11 13:45:14 +02:00
leonarski_fandClaude Opus 5.5 d14e32b19a decode kernel: __launch_bounds__(256, 5), so sm_120 keeps the five blocks its shared memory allows
The compiler's register choice held the fused decode/un-transpose/preprocess kernel to four
resident blocks per SM on the RTX 5080, where its shared buffer allows five. Asking for five
(48 registers, a small spill) takes it from ~391 to ~313 us per 18 Mpx frame (myob, -N 1, nsys,
repeated). sm_75 cannot hold five 256-thread blocks, so its code is unchanged (55 registers).
Output identical: ci tier base vs new, compare 0 changed, p.hkl identical on 26 sets, MTZ
reflection arrays identical on 49 files; [ImagePreprocessorGPU] passes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-11 13:44:30 +02:00
leonarski_fandClaude Opus 5.5 60a8bf3981 rugnux --af-model (experimental): single-copy molecular replacement of a predicted model
A predicted model (AlphaFold mmCIF, pLDDT in the B column) is trimmed at pLDDT 70, given B-factors
from the pLDDT (Oeffner et al. 2022) and placed by molecular replacement, one copy in the ASU:

- rotation function on a uniform 6 deg sampling of the point group's rotation ASU, each orientation
  scored by the correlation of E^2 with the symmetry-summed |F|^2 read off the model's molecular
  transform, computed once by FFT and interpolated (Navaza 1994);
- full-symmetry Crowther & Blow translation function (one FFT per orientation and group, through
  MapFromFPhi/FFTW) for the 50 best orientations in every group with the merge's rotations;
- the best three placements per group through RefineRigidBody (GPU where there is one), bulk solvent
  and R-free to 3 A, a C-alpha packing check against symmetry mates, Matthews, and the
  random-orientation null for a placed candidate.

Decision: PLACED = TF Z >= 8, clash <= 5%, Vm in [1.5, 4), null >= 3 sd. Within the answer's merge
the placed group with the lowest R-free is chosen where it leads every other packing group by 0.03;
the twin hypothesis' merge is searched, and chosen, only where no group of the answer's merge can
hold the model at all (Matthews, packing). An enantiomorph relabels the answer as --model does;
another group goes to <prefix>_mr.mtz/.cif plus <prefix>_mr_model.pdb; the MODEL PRIOR report
section carries the table. The search runs on the CPU in both builds; without --af-model nothing
changes.

Measured (RTX 5080, MR step): 3r6o twin I41 chosen over I4122 (Matthews + clash) in 1.8 s, solution
0.09 A CA rmsd from Phaser's; 11if P41 -> P43 1.2 s; 7ris P3121 -> P3221 1.7 s; 9zmu P6122 -> P6522
4.7 s; 11if's model against 3r6o's data NOT_PLACED.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-11 13:35:05 +02:00
leonarski_fandClaude Opus 5.5 35f0cffaa4 Merge branch 'b2-radius-early' into rc175: the canonical pass picks its integration radius before it runs
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-11 13:02:07 +02:00
leonarski_fandClaude Opus 5.5 a609f6ba1c rugnux two-pass: the integration radius decided on the pre-pass frames, before the canonical pass
Where the measured spot width widened the integration radius, the canonical pass
integrated the whole sweep at the widened radius, and only then did the starvation
guard read how many background rings the neighbouring predictions crowd; over the
bound (1.13%) the pass was thrown away and run again at the fixed radius.

The geometry pre-pass now counts that same quantity at the widened radii without
integrating there (BraggIntegrationEngineCPU::CountNeighbourCrowding: the neighbour
mask and the ring pixels the run's mask leaves readable, no image) on 20 frames
spread over the pre-pass, with the standard error of the ratio over those frames.
Over the bound, the canonical pass integrates at the fixed radius from the outset.
A count within NEAR_TIE_SIGMA of the bound is a near tie in the part-B sense: the
canonical pass counts the widened radii again on 20 frames over the whole sweep,
and where that puts them under the bound the run is made again deciding late. A
run deciding late counts nothing early and decides as before. Under the bound the
canonical pass integrates at the widened radius and the measured guard stays as it
was, so a pre-pass count that is too low (its prediction holds fewer tails than
the canonical pass's, or it stood on another lattice class) costs only the pass
it cost before. No new threshold.

Battery against 46be3f647 (the 14 open sets that re-ran at the fixed radius, the
9 open/in-house sets closest under the bound, the ci tier, 2 private sets):
- p.hkl and MTZ data identical on all 51 open/in-house sets and on one private set.
- The 14 re-running sets: rugnux wall 534 -> 447 s (7qij -27, 4nwv -12, 7yzx -10,
  9fhc -9, 5src -6 s, on a shared machine). 8sqq and 8xtf count under the bound on
  the pre-pass and re-run as before. 4nwv (1.69 +- 0.28%) and one other set were
  near ties, re-checked on the whole sweep (1.72%, 1.61%), ending where they did.
- One private, densely crowded set: same space group and d_min, R_meas 28.6 ->
  39.3%. Its old re-run re-indexed starting from the spot budget the thrown-away
  pass had measured (152 of 1000). With that budget given (--max-spots 152), the
  new run gives 30.6%. The difference comes from that path, not from the radius.
- The count costs ~13 CPU-s on the heaviest set (65k predictions a frame). Over
  the 22 ci sets that count, pre-pass integration took 45.2 -> 45.8 s.
- A build forcing the near tie and its overturn on 4nwv re-checks on the whole
  sweep (1.80% against the engine's 1.81%) and makes the run again deciding late.
  It ends with the same p.hkl.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-11 13:01:23 +02:00
leonarski_fandClaude Opus 5.5 e55750e1e0 rugnux merge ingest: keep d beside zeta only under an energy bandwidth
The partiality recompute reads d only to broaden the rocking curve by the energy bandwidth. On a
large set the final ingest sits at the process's peak resident memory (8a1a: max RSS rose by the
array's 4 bytes x 68 M observations), so the array is kept only where a bandwidth is set; with
zeta in and the predicted-partiality copy out, the ingest's peak is then where it was.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-11 12:38:04 +02:00
leonarski_fandClaude Opus 5.5 3befddd649 rugnux: an FFT indexer that does not fit waits for the GPU work beside it, then is built again
Several pipeline copies of one run build FFT indexer pools on the card at once - the
rotation-scale walk's indexing probes start as the output pass begins its image loop,
and the speculative geometry probe runs beside the merge - and each indexer is a cuFFT
plan plus 0.6-0.9 GB of buffers. Construction had no admission control: a cufftPlanMany
or device allocation that found the card full failed straight through, and since
IndexerResult carries the exception that ends the run. With 7.2 GB of the card held by
another process, a small-molecule rotation set failed 4 of 4 runs this way although
each pass fits on its own.

Construction now does what the merge's allocations do: hand back the pool's unused
memory, wait for the work beside it to end, and build again. Who waits for whom is
fixed so that nothing waits in a circle:
- the main line waits for the probes (GPUWorkBeside, as the merge already does);
- a probe waits for the main line's image pass (new GPUImagePass), never for a probe,
  itself included. The image pass waits for no probe, so it ends.
The walk's indexing probes now hold a GPUWorkBeside too (they are exactly that work),
and a run carries gpu_work_beside_ so its pools know which side they are on. Only when
an indexer is built changes, never what it computes; with nothing to wait for it still
fails at once.

Same run with the 7.2 GB hog: 5/5 complete (also at 7.6 and 8.0 GB), p.hkl identical
to the unhogged run; a card that cannot hold the run still fails in 0.5 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-11 11:11:42 +02:00
leonarski_fandClaude Opus 5.5 b46b4a4657 rugnux all-observation arm: observations copied on the card, not staged from the reflections again
The all-observation arm of the space-group search merges on a copy of the engine (CloneIngest), and
its IngestDevice staged every observation from the ~92-byte source reflections a second time -
concurrently with the main engine's own staging of the same reflections. Now the main engine
stages, and the copy takes its card arrays with cudaMemcpyPeer (a device copy when both engines
share the card, as on a one-card machine). CloneIngest no longer copies the predicted partiality,
which only that staging read.

Output identical (cytc p.hkl md5, and with --dump-observations the dump md5).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-11 10:52:57 +02:00
leonarski_fandClaude Opus 5.5 09c41d067d Pinned host memory from a process-wide pool; the compressed chunk uploaded from pinned staging
The GPU engines are built per worker per step, and each took its page-locked host buffers with
cudaHostAlloc / cudaHostRegister and gave them back with cudaFreeHost / cudaHostUnregister. Those
calls map and lock pages under the process's memory-map lock, so all workers queue behind each
other: on the 5080 workstation a myoglobin run made ~3400 of them (6-22 thread-s, depending on the
load), 9gdj ~10400 (124 thread-s); on a four-socket host they cost 20-30 ms each.

- CudaHostPtr takes its memory from a pool: a freed buffer is kept in a power-of-two size class for
  the next engine, up to 512 MiB in all; a buffer above 64 MiB is never kept. Rugnux::Run hands the
  pool back at its end.
- PinnedVector (std::vector with the pool's allocator) replaces the registered vectors of
  AdaptiveSpotFinderGPU and ImagePreprocessorGPU; the Bragg predictors download into a pooled
  pinned buffer and copy into their output, instead of registering it.
- BSLZ4DecoderGPU copies the compressed chunk into a pooled pinned buffer before the upload, so it
  is one DMA instead of the driver's pageable staging (9gdj: 163 GB pageable H2D, 52 s of copy
  time, -> 147 GB pinned, 13 s).

Measured with nsys on the workstation: myob 3398 -> 1396 calls (674 of them the release at the
end), cytc 2986 -> 1270, 9gdj 10428 -> 1748 (124 -> 3.7 thread-s). Output identical (p.hkl md5 on
myob, cytc, 9gdj; CI tier battery).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-11 10:37:23 +02:00
leonarski_fandClaude Opus 5.5 42ed7261d3 rugnux merge ingest: four sweeps of the source reflections instead of seven
The ingest read every ~92-byte Reflection seven times per engine: the background/h-range sweep, the
h histogram, the key scatter, the observation build, the delta_phi re-solve against the smoothed
geometry, the partiality recompute, and the device staging. On a box with several sockets these are
remote, latency-bound loads (the owner's 4-socket profile puts RotationScaleMerge's ingest lambdas
at the top of the CPU time on most large sets). Now:

- the histogram of h is counted in the background sweep, per chunk of frames over the whole int16
  range the sort key holds, so the h range needs no sweep of its own;
- delta_phi is re-solved against the smoothed per-frame lattice while each kept observation is built
  (SmoothedFrameGeometry computes the per-frame bases first, SmoothedDeltaPhi solves one);
- the build keeps zeta and d beside delta_phi, so the partiality recompute reads compact arrays, and
  it brings corr_ingested to the recomputed partiality in the same loop, which retires the
  observation-length copy of the predicted partiality and its separate pass.

Output identical (p.hkl md5 on 9gdj and cytc; cytc also with --dump-observations, dump md5 equal,
and CPU-only). Transient host memory: +4 bytes per kept observation inside IngestHost (zeta and d
in, the predicted-partiality copy out) and nthreads x 256 KiB of histograms.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-11 09:42:00 +02:00
leonarski_fandClaude Opus 5.5 54f8e07591 Merge branch 'b3-merge-mem' into rc175: merge-engine device memory estimated from the layout's counts
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-11 08:31:30 +02:00
leonarski_fandClaude Opus 5.5 9d6a3f1e04 rugnux merge engines: count-based device-memory estimate for the all-observation arm beside
The gate for merging the space-group search's all-observation arm on a copy of the engine, beside
the first arm, judged an engine at a fixed 256 B per observation. It now counts what the ingest
knows - observations, rocking events (at most one full each) and distinct hkl (at most one ASU
group each) - at 80 / 260 / 200 B, the envelope of the measured per-engine peaks (helpers
included: error-model fit, Wilson outlier test, pair sort), plus a fifth, and keeps 3 GB of the
card for what else stays resident (context, the run's indexers: measured up to 2.3 GB at the gate
with no speculative pass running).

Measured per-engine peaks (scratch build tracking each engine's device allocations): 4nwv 7.98 GB,
8a1a 8.15, 8tyy 9.36, 8qaw 13.3 - two engines do not fit a 16 GB card on any of these, so they stay
serial. The old constant was not an over-estimate there (fulls cost ~260 B each at the surface
fit). What changes: 9 battery sets of 33-53 M observations at high multiplicity (8s38, 8sqt,
7ph1, 9min, 9i80, 9sl0, 9h0q, 8v4o, 6ze4) now merge the arm beside; none goes the other way.
Their merge-step VRAM peak is 11.0-14.1 GiB of 15.9, no out-of-memory, no failed speculative probe.
Every engine peak measured on 11 sets is at most 81% of the estimate.

Output identical (p.hkl md5 and MTZ data, 40 sets incl. ci tier). Gain is small: the two merges
share one GPU, so the arm beside slows the main P1 merge by ~0.5 s while saving ~0.7-1 s of wait
at the search; alternating A/B on 5 of the sets: -1.1 s output pass in total.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-11 08:09:58 +02:00
leonarski_fandClaude Opus 5.5 bcb059591b rugnux hypothesis arms read their residual on the pre-pass frames
The demoted arm of the metric-symmetry check and the rotation-scale walk's
refits integrated and post-refined every frame of the sweep, only to read a
held-out residual or a scale fitted on top. They now integrate only the frames
the geometry pre-pass read (prepass_frames_only_, PrepassEnd), as the pre-pass
asked its own fit: the run decides early on the evidence it has.

Metric-symmetry check: the canonical pass, the promoted arm, fits its
comparison post-refinement on the same frames (GatherPostRefineObservations
takes a frame limit). That fit only measures and leaves nothing on the run;
the canonical pass then measures its own fit over every frame where it
measures one anyway, as any canonical pass does. A decision whose margin from
the HeldOutResidualFell line lies within NEAR_TIE_SIGMA of its noise is a near
tie in the part-B sense: the canonical pass also fits over every frame while
its reflections are in hand, and after the run the demoted arm runs again on
every frame (deciding late) and is judged against that fit. Where the check
ends elsewhere the run is made again deciding late, which asks both arms on
all the frames as before. No new threshold.

Rotation-scale walk: a refit proposes the next scale; the validation frames of
the whole sweep still decide whether it is taken, so it needs no re-check.

Battery, 31 open/in-house metric-check and scale-walk sets + 3 private,
against 56bad5fdb:
- p.hkl and MTZ arrays identical on 32 of 34. Every metric-symmetry decision
  ends as before. 6yqf decided a near tie the other way on 1200 of 2000
  frames, the re-check on all frames overturned it and the run made again
  gives base's output (24 -> 55 s). 6cdl and 9vyb were near ties the re-check
  confirmed.
- 9gdj: the scale walk takes one more refit and adopts 0.97020 instead of
  0.97045; R_meas 11.41 -> 11.36 %, R_free 0.1709 -> 0.1708, d_min, SG same.
- One private scale-walk set: the pre-pass refit pointed up where the
  all-frame one pointed down; the whole-sweep validation frames prefer the
  higher scale, and the merge improves on every statistic.
- ci tier: 0 sets changed beyond noise (only 9gdj's data differ).
- rugnux wall over the 31 sets 1059 -> 993 s (with 6yqf's re-run), metric
  check step 129 -> 85 s; ci tier 640 -> 626 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-11 07:12:05 +02:00
leonarski_fandClaude Opus 5.5 60ca872bc2 GPU adaptive spot finder: same spots, about half the kernel time per frame
Measured on an RTX 5080, one worker (nsys), 16M EIGER rotation set, per frame:
spot-finding kernels 757 -> ~383 us. Output is bit-identical: integer sums
and a deterministic candidate/spot order throughout.

- flag_strong reads the image only where a strong pixel can be: the plain
  ring pass notes each 32-pixel word's largest value, a geometry table gives
  the rings each word touches, and a word below the lowest of their
  thresholds is skipped. One thread per word writes the whole word, so the
  ring buffer needs no clearing or atomics. 128 -> 12 us.
- analyze_pixel with candidates uses 16-row tiles (only tiles near a
  candidate are worked, and each warp's serial chain of rows is short) and
  issues the entering/leaving row loads before the window scans; the window
  count is summed in 32 bits. 254 -> 72 us. The dense path keeps its tiling.
- analyze_candidates reads its column window with unconditional, unrolled
  loads; through value_at each load waited for its previous-pass bit and
  the row before. 74 -> 34 us.
- scatter_bits emits a block's range with the whole warp (prefix sum over
  the words' bit counts, same order). 32 -> 6 us.
- finish_components split into label (one block), sum (all blocks, integer
  atomics) and filter/compact (one block); a component's sums no longer walk
  the list from its first member to its last. 26 -> 12 us.

Tried and not kept (slower on sm_120): warp-aggregated ring sums and a
low-value histogram fast path in reduce_rings_shared, a precomputed
tile-needed map, unrolling analyze_pixel's row loop.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-11 04:29:41 +02:00
leonarski_fandClaude Opus 5.5 40fcaa985c Global Wilson B: minimum fit window, no upper cap
CalcGlobalWilsonB fits ln<I> against 1/d^2 from 4 A to d_min. With d_min just under 4 A that window is a
sliver, and after the shells past the signal limit are dropped it held 0-2 shells, so the estimate was
NaN (or a fit over almost no range); and any B above 200 A^2 was discarded, which is every crystal
diffracting to 4-7 A.

Now the window is never narrower in 1/d^2 than the 4-3 A window a 3 A data set gets: a narrower one is
widened towards low resolution (d_min 3.15 A -> from 4.39 A, 3.61 A -> 5.96 A, 3.87 A -> 7.40 A; data
coarser than ~4.4 A use everything, as before). The 200 A^2 cap is dropped for the dataset-wide
estimate (kept for the per-image one). The window is logged, printed with the statistics and reported
(WILSON_B_RANGE_A, developer key); the correlation is reported as before. Data with d_min <= 3 A are
unchanged.

Report only: the value feeds the statistics text, the log, the report and the mmCIF
_reflns.B_iso_Wilson_estimate; nothing in scaling, merging or the MTZ reads it.

Measured on the open arm (replica on the a897b08 merged data, confirmed by rugnux runs): 12 sets
change, 7 previously NaN now finite - 5nw5 315, 9z44 493, 6r72 302 A^2 (were above the cap; model mean
B 328/310/114, deposited Wilson 388/322/179), 9yzk 210 (model 383, ctruncate 243, xtriage 104), 7qij 104
(model 194, ctruncate 150), 9yl4 78 (model 146), 8qq7 35 at 4 shells (model 197; its signal ends well
inside the written 3.15 A). 6yqf 132 (corr 0.51) -> 96 (corr 0.69; model 57, ctruncate 145,
xtriage 114); 9rcs 69 -> 148 (model 74, ctruncate 131). At 3-4 A the estimators themselves disagree by
1.5-3x, so these are better-defined numbers, not exact ones. p.hkl and MTZ data identical to the base
(e34a5fb88) on every set compared (ci tier, private arm, the Wilson sets).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 02:21:32 +02:00
leonarski_fandClaude Opus 5.5 fe181ca042 Anisotropy verdict: the directional limits are a second hypothesis beside the tensor
The verdict gated only deltaB_linear, the Debye-Waller part of the tensor, against the data set's own
systematic floor. A deficit that does not follow s^2 (a FLAT signature) can stop the signal far earlier
in one direction and still read NOT_DETECTED: one open-arm set reported directional limits of
3.18/3.18/2.14 A beside NOT_DETECTED. The cone limits (<I/sigma> = 2 in 20 deg cones) were computed but
never consulted.

Now each statement is decided on its own evidence and either establishes anisotropy:
- tensor: unchanged gate;
- directional limits: at the best direction's limit the weakest cone's <I/sigma> is below the best
  cone's by more than 3.5 of their joint standard error (both read linearly in s^2 between shell
  centres, as the limits are), the weakest limit is measured (not censored at the data edge), and the
  limits are at least 0.25 A apart - the spread the report already calls "noticeable".

Why the magnitude term: with the standard errors alone (the counting statistics of the cones) the
weakest cone is "significantly" below the best on 172 of 216 battery sets, down to limits 0.5% apart
(measured with a replica of ConeLimits on the a897b08 battery's merged data). The cones differ
systematically (multiplicity and coverage depend on direction), so significance alone made DETECTED
near-universal. A refusal (CANNOT_DETERMINE: dose ramp, short sweep, triclinic, no unmerged data) is not
overruled - its reasons bias the cones too - but now reports the measured directional limits.

Report: ANISOTROPY_DETECTED_BY (TENSOR/DIRECTIONAL/BOTH/NONE); developer keys for the two cones'
<I/sigma> and errors; the summary row names the test that carried the verdict, and CANNOT_DETERMINE
rows give the directional limits; the mmCIF verdict comment says so. Report only: no intensity, flag or
decision downstream reads the verdict (the French-Wilson prior uses the tensor regardless).

Measured (battery replica, 225 sets with limits): directional statement established on 95; 23
NOT_DETECTED -> DETECTED (directional ratio 1.14-1.59); of the 17 sets with ratio > 1.2 and an
uncensored weakest limit, 11 flip, 5 stay CANNOT_DETERMINE with limits reported, 1 (sparse, noisy
cones) stays NOT_DETECTED. ANISOTROPY warnings (spread > 0.5 A) appear on 6 more sets. Confirmed with
rugnux runs on the ci tier, those 17 and the private arm: p.hkl and MTZ data identical to the base
(e34a5fb88) on every set compared.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 02:21:32 +02:00
leonarski_fandClaude Opus 5.5 1420908885 --model: the bulk-solvent b_sol ceiling follows the model's atomic B
The model scale searched b_sol only over 10-80 A^2. That is the range
Fokine & Urzhumtsev report for models at ordinary B, but on low-resolution
entries whose deposited B is 200-400 A^2 the fit sat on the 80 A^2 ceiling
(and k_sol on its 0.1 floor): a solvent edge far sharper than the atoms it
lies against, reaching too deep in resolution. R-free came out 0.05-0.10
above REFMAC (0 cycles) and phenix.model_vs_data on the same MTZ, model
and flags.

The ceiling is now max(80, 2 x the model's mean atomic B) (SolventBMax),
for the reported fit and for the rigid-body placement's per-zone solvent
(CPU, GPU and the host fallback alike). The grid keeps eight b_sol values
across a wider box, so it costs no more points; the default box, and so
every model with mean B <= 40 A^2, is bit-identical to before.

Measured (10 open-arm sets with --model, GPU build, vs the same base):
  high-B set, 7.0 A:  R-free 0.462 -> 0.364, b_sol 80 -> 379 (REFMAC 0.366,
                      REFMAC's own b_sol ~371; model_vs_data 0.291);
                      the rigid-body step now commits
  high-B set, 3.9 A:  0.410 -> 0.331, b_sol 80 -> 388, k_sol 0.10 -> 0.225
                      (REFMAC 0.328, model_vs_data 0.314); null 10.1 -> 14.2 sigma
  B~200 set, 3.2 A:   0.408 -> 0.402, b_sol 80 -> 120; enantiomorph label
                      decision unchanged (27.2 -> 25.8 sigma)
  7 other sets (mean B 16-57): bit-identical R, k_sol, b_sol, placement.
p.hkl md5 and the MTZ reflection data are identical to the base on all 10.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 20:47:04 +02:00
leonarski_fandClaude Opus 5.5 89a7853916 A failed cuFFT plan gets the GPU-memory advice whatever its code
In a stress run (two rugnux on one 16 GB card) the indexer's cufftPlanMany failed three times with
CUFFT_INTERNAL_ERROR (5), not CUFFT_ALLOC_FAILED, so the message carried neither the free memory
nor the remedy. Every plan failure is now reported like a failed allocation: free memory on the
card, the two usual causes and the CPU remedy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 19:34:51 +02:00
leonarski_fandClaude Opus 5.5 c6f6396e46 Indexer failures are thrown, not read as "no lattice"; GPU memory errors say what to do
A CUDA out-of-memory in an indexer worker was turned into an IndexerResult carrying only the
message string. The rotation indexer then returned no lattice, the first pass treated that as a
genuine negative and the rescues acted on it: in a stress run (two rugnux on one card) a run had
both first-pass schemes fail with out-of-memory, the axis-sign rescue compared 0 frames at the
flipped sign with the -1 "no lattice" sentinel, adopted the flipped sign, and the beam-centre rescue
then moved the centre - a failure of the machine used as evidence about the crystal. The CUDA
category was lost on the way, so the fail-fast check (IsFatalResourceError) never saw it.

- IndexerResult::error is now the std::exception_ptr the worker caught. RotationIndexer::RunIndexing
  rethrows it before touching any state, so nothing is memoised for a failed run and the exception
  keeps its category: rugnux stops with the CUDA message at once (verified with a forced indexer
  failure: exit 1 after 1.9 s, "CUDA (GPU) error (...)"). FirstPass::indexer_error,
  RotationIndexer::GetIndexerError and RequireLattice's machine-failure branch are gone.
- The beam-centre arm in RunAllPasses rethrows fatal resource errors like its sibling catches; it
  had caught the out-of-memory and carried on at the file's centre.
- IndexAndRefine keeps per-frame indexer failures apart from frames that did not index (count +
  first exception). The frame stays "not attempted" as before; the receiver logs the count and the
  first error at the end of a collection (no cancel), rugnux's image pass and stills geometry
  refinement rethrow it and stop. A rotation-indexer failure inside IndexAndRefine (online rotation
  indexing, FinalizeRotationIndexing) is recorded the same way, so a live collection is not
  cancelled by it.
- A failed device allocation or cuFFT plan now says how much was asked, how much of the card is
  free, and - when the card is merely full - that another program is using the GPU or the data set
  needs more than the card has, with the CPU remedy; previously "Failed to allocate device memory
  (out of memory)" / "Failed to create CUFFT plan with cufftPlanMany".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 18:56:00 +02:00
leonarski_fandClaude Opus 5.5 a897b08a7d Merge branch 'doubled-cell' into rc175: a doubled-cell hypothesis for a half-translation pseudo-symmetry that breaks down with resolution
Build Packages / Create release (push) Successful in 7s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 10m58s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 11m43s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 4m50s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 15m10s
Build Packages / build:portable:windows-x86_64:nocuda (push) Failing after 16m20s
Build Packages / build:portable:windows-x86_64:cuda (push) Failing after 19m2s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 19m20s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 20m9s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m0s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 16m10s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 17m46s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 18m52s
Build Packages / Generate python client (push) Successful in 29s
Build Packages / Build documentation (push) Successful in 1m5s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 20m46s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 22m24s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 16m8s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 19m46s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 17m29s
Validation Battery / Validation Battery (push) Successful in 48m17s
Build Packages / Unit tests (push) Failing after 1h24m43s
Narrow, guarded: the doubled pass runs only where the added class grows with resolution and is resolved; the doubled
cell is adopted only where one pseudo-translation explains the added class in every shell and the point group does not
rise. Fixes the one remaining lattice failure (half cell -> deposited cell, R-free 0.574 -> 0.275); p.hkl identical on
the ci tier, the supercell gate set, 78 further supercell-candidate sets and all private sets.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-10 15:18:14 +02:00
leonarski_fandClaude Opus 5.5 49384c4aa8 Merge branch 'mv-ncs' into rc175: models expanded by their strict NCS operators; the written cell follows the model's indexing
Exact: p.hkl md5 and MTZ reflection data identical on 18 sets. 4nwv R-free 0.545 -> 0.297 and cell in the depositor's
frame; written cells corrected on 9rcs, 9b22, 9qw8, myob_x06da_powder_1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-10 13:36:31 +02:00
leonarski_fandClaude Opus 5.5 1f27fba1f3 Model validation expands strict NCS; the written cell follows the model's indexing
- ModelValidation reads the model and, where it carries non-given NCS operators
  (_struct_ncs_oper 'generate' / MTRIX without the given flag), expands them
  (gemmi expand_ncs, vendored assembly.cpp from GEMMI 0.7.5, MPL-2.0 as the rest
  of gemmi_gph). Same in the battery's model_check.py and depdata_check.py.
  An icosahedral-virus entry deposited as 3 of the 45 chains of its ASU was
  scored against a fifteenth of its atoms: R-free 0.545 -> 0.297.
- The indexing the model chooses (AdoptModelFrame) now moves the cell with the
  indices (reseat_integrated in the main path; CellInBasis in the forecast,
  twin-file and stills/scale paths). A near-symmetry law (b within 2% of c, beta
  a degree off 90, a~b in P1) relabelled the reflections only, so the files were
  written with two axes swapped or beta flipped relative to the indices. Where
  the law moved the cell, the validation is remade on the moved cell.
  Measured on 18 open/in-house sets with models: p.hkl and MTZ reflection data
  bit-identical on all; cell and model R unchanged where the law is exact; the
  cell now matches the deposited one on the 6 pseudo-merohedral cases (orthorhombic
  a/c swap, three monoclinic beta flips, a P1 alpha/beta swap), R-free within
  +-0.001 there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-10 13:35:49 +02:00
leonarski_fandClaude Opus 5.5 48201d35ec WIP: rugnux doubled cell decided late on one pseudo-translation; screw trim reads the measured intensity
Doubled-cell hypothesis: the run now always merges the doubled cell under <prefix>_doubled first and
decides with both merges in hand. Adopted only where (1) the probe's class grows significantly with
resolution (three standard errors between the 20-8 A shell and the finest measured one) and (2) in
the doubled merge one pseudo-translation - half a cell plus eps, fitted over the class - modulates at
least half of the added class in every shell (FitDoubledClassTranslation: A sin^2(pi h.eps) + B per
shell, share 1 - B/mean). Where adopted, every <prefix>_doubled file takes the plain name and the
probed cell's file of that name moves to <prefix>_halfcell. Replaces the HALF_CELL_BREAKS_DOWN bound.

Measured on the forced doubled merges: modulated share per shell 0.80-1.0 out to 1.9 A on the crystal
deposited in the doubled cell; on the ones deposited in the half cell it falls to 0.01-0.34 by
2.1-2.8 A (the rising class with twinning), or stays at -0.3-0.48 at every resolution (three others);
the one class it fully explains does not rise (eps 0.1 of a cell, flat at 0.1 of the lattice).

Space-group search: the trim of a screw zone's largest absence exempted a violation "at its row's
mean" on the de-modulated intensity, so under a deep pseudo-translation a reflection at 0.015 of its
row (de-modulated by 1/0.002) could never be trimmed and alone decided the row against the screw.
The exemption now reads the measured intensity, at both sites.

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