Commit Graph
1484 Commits
Author SHA1 Message Date
leonarski_fandClaude Opus 5.5 402b3d1293 ACKNOWLEDGEMENT: autoPROC and Clemens Vonrhein; DOIs verified; DISTL; tNCS credit in MR
- All 84 DOIs checked (82 on Crossref, 2 DataCite): title, journal, volume, pages and first author
  match; Matthews 1968 and Yeates 1997 links percent-encode their parentheses; the MOSFLM link
  points at a page that answers.
- autoPROC: an explicit acknowledgement of Clemens Vonrhein (Global Phasing) for the principle that a
  pipeline should verify the metadata it is given - beam centre, geometry, goniometer - and correct
  it where it is wrong; Vonrhein et al. (2011) Acta Cryst. D67, 293-302 (DOI checked).
- Zhang, Sauter et al. (2006) DISTL, already credited in SpotFindingSettings.h, gets its entry.
- Molecular replacement: tNCS looked for in the native Patterson as Read, Adams & McCoy do; the same
  credit at DetectTranslationalNcs (comment only).

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

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

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

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 08:27:36 +02:00
leonarski_fandClaude Opus 5.5 b736c4cefb RotationScaleMerge::DeviceBytes: the resident ingest's peak as measured
The ingest term was 90 bytes per reflection and 64 per run. Measured with cudaMemGetInfo at each
step of IngestUpload/IngestSort (relative to the free memory before the observation arrays):

- the second radix sort is the peak where runs are few: the 66 B of observation arrays, by_image,
  perm, key, key_sorted and cub's alternate key/value buffers of a plain SortPairs - 102.3-103.0 B
  per reflection on 7qij (51.1 M, 52.2 M, 78.1 M reflections) and 4nwv (32.7 M);
- the cut is the peak where runs are many: 98 B per reflection plus 56 per run (4nwv, 15.4 M runs:
  125.8 B per reflection, 4106 MB against 3923 MB from the old formula);
- the upload holds the 66 B beside the two 64 MB staging buffers (~150 MB measured).

Now max(103 B per reflection + 58 B per run, 66 B per reflection + 160 MB): 2-4% above every
measured peak, where the old formula was 10% under on 7qij (7170 MB against 7986 MB for the main
engine) and 4.5% under on 4nwv. Its only consumer is whether the all-observation arm goes beside
the main engine, which changes when that merge runs, not what it gives.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 08:27:36 +02:00
leonarski_fandClaude Opus 5.5 5e916fe120 docs: --model places by rigid body, then molecular replacement; --mr; <prefix>_mr files
RUGNUX_ADVANCED's --model page described the rigid body alone. It now says how the unified flow
places a model: an experimental model by the rigid body, checked against its null in the answer's
or the twin hypothesis' merge, and by molecular replacement where it is not placed; a prediction
straight to molecular replacement; --mr skipping the rigid body; MODEL_PLACEMENT. It says what a
placed model may change in <prefix>.mtz (the hand, and from the rigid body an indexing-ambiguity
reindex) and that another group or the twin subgroup the model chooses goes to <prefix>_mr.mtz/.cif
with <prefix>_mr_model.pdb/.cif. --mode scale places by the rigid body only (no molecular
replacement, --mr refused); a full run does molecular replacement on serial stills too (checked:
a stills run with a predicted model reports MODEL_PLACEMENT= MR). The options table gains --mr
and the --model row the fallback.

RUGNUX_INTEGRATION no longer says rugnux does no molecular replacement, and ACKNOWLEDGEMENT names
--model beside --mr for predicted models.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 08:07:44 +02:00
leonarski_fandClaude Opus 5.5 e53c3f50cd CalcBraggPredictionTest: skip the GPU cases on a machine without a card
None of the eight GPU test cases in the file (BraggPredictionGPU*, the CPU/GPU consistency
cases and BraggPredictionRotGPU_DeviceOrderMatchesHost) checked for a device, so a CUDA build on
a machine without one failed them instead of skipping. Same guard as the other GPU tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 07:54:56 +02:00
leonarski_fandClaude Opus 5.5 463cd8eec3 CBF byte-offset decoder: plain stores instead of SSE streaming stores
The host decoder wrote each pixel with _mm_stream_si32 and ended with _mm_sfence, to keep the
decoded frame out of the cache on its way to the GPU by DMA. The GPU now decodes CBF itself, so
the host decoder only serves the CPU engines and the tests, and the project rule is no SIMD
intrinsics. Plain stores; the values written are the same.

CBFDecoderGPU_MatchesHost, CBFDecoderGPU_ShortStreamIsRefused,
ImagePreprocessorGPU_StoredEncodingsMatchCPU and the CBF reader tests pass.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 07:54:56 +02:00
leonarski_fandClaude Opus 5.5 645d1cc1bb CHANGELOG: rc.175 says --model falls back to molecular replacement and takes predicted models; viewer refresh
The --mr line now says what the unified --model flow does for a user: a predicted model is
accepted, and a model the rigid body does not place goes to molecular replacement, with --mr
forcing it. One viewer line for the Files panel refresh (F5 and the poll) and the combo boxes
that no longer reset during live collection. Owner-requested edit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 07:41:37 +02:00
leonarski_fandClaude Opus 5.5 f8535968c6 Merge branch 'fix-9fhc' into rc175: marCCD frames copied into the engine's own pinned buffer (fixes a pre-scan race); ModelValidation test follows the null on every run
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 05:54:59 +02:00
leonarski_fandClaude Opus 5.5 2c6cd1ef38 ModelValidationTest: the null runs on every validation that judges its model
ModelValidation_ReindexesTheDataIntoTheModelsIndexing still expected no null for data already in
the model's indexing, as when the null ran only where a decision was pending. Since the unified
model validation it runs on every validation whose schedule asks for it (fit_test, the default),
and is skipped only where the verdict is another validation's; the test now checks that the null
ran and that the model fits.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 05:21:33 +02:00
leonarski_fandClaude Opus 5.5 34092a510f marCCD upload: stage the stored pixels instead of page-locking the frame in place
A UINT16_WIDENED frame (marCCD) was page-locked where it lay with PinInputRegion, which keeps the
registration until the engine moves on to another frame. The pre-scan hands its kept frames to the
spot measurement and to the defective-pixel search at once, each with its own engines, so one
engine could unregister a frame while another engine's copy from it was still in flight. That
takes the context down ("unspecified launch failure"), seen as a rare crash at the end of the
pre-scan on a 3072x3072 marCCD sweep (1 run in 30 reproduced it).

The stored bytes now go through the engine's own page-locked staging buffer, as the CBF and BSLZ4
decoders already do. The pixels decoded are unchanged.

Test: ImagePreprocessorGPU_SharedStoredFramesManyEngines - four engines upload four shared marCCD
frames concurrently; it aborted with the launch failure before the change and passes after it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 05:21:33 +02:00
leonarski_fandClaude Opus 5.5 59648587e3 Merge branch 'gpu-loops' into rc175: prediction ordered and selected on the GPU, CBF byte-offset decoded and marCCD widened on the GPU
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 03:28:19 +02:00
leonarski_fandClaude Opus 5.5 450266d6f7 Merge branch 'gpu-scaling' into rc175: merge ingest, delta-phi/partiality recompute and CSRs on the card; overload/ice in one parallel sweep; guard beside the main engine on one card
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 03:14:18 +02:00
leonarski_fandClaude Opus 5.5 11052bc376 CBFDecoderGPU: walk short parts from shared memory
The first device decoder walked 256-byte tiles, one thread each, straight from global memory: on a
PILATUS 6M frame (~6 MB of stream) that is ~24k threads walking alone, and the two walks took
~0.5 ms of the card per frame - with the CBF decode gone from the workers the card is what bounds
the 7qij loops (kernels 67-78 % busy). Now each thread walks a 68-byte part that its block has read
into shared memory together (68, not 64, so the threads of a warp do not read the same banks), the
scan runs over tiles of 8 parts whose maps are composed first, and the write kernel stages a block's
pixels in shared memory and writes them out coalesced.

Measured on 36 real frames of 9 CBF sets (nsys, average per frame): 0.41 ms against ~0.6 ms before.
Output unchanged: the same tests, and the same 36 frames identical to the host decoder.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 02:53:52 +02:00
leonarski_fandClaude Opus 5.5 9cb19753c7 Merge branch 'gpu-modelval' into rc175: --model validation's scale fits on the GPU, change-of-basis scoring gridded once in parallel
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 02:47:06 +02:00
leonarski_fandClaude Opus 5.5 eb7a65d051 rotation merge: the fulls' frame and group CSRs built on the card
Every merge downloaded the fulls' frame and group keys and built both CSRs by serial counting
sorts on the host - four passes over the fulls, two of them scattering into an array as long
as the distinct hkl (15 M on 4nwv) - and uploaded them again. They are now stable radix sorts
of the resident fulls on the card, with the counts summed there; the host gets the counts per
frame and the group CSR it walks itself. The same permutations (stable sorts list each frame's
and each group's fulls in index order, as the counting sorts did): p.hkl identical on cytc
and 4nwv.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 02:01:30 +02:00
leonarski_fandClaude Opus 5.5 85e714f072 Merge branch 'unified-model' into rc175: --model runs molecular replacement when the rigid body cannot place the model
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 01:58:38 +02:00
leonarski_fandClaude Opus 5.5 2a32c28cca battery nullmodel: a section title is '# N. TITLE', not any comment line starting with a digit
The CC(model, data) table's rows ('#      4.13 ...') were read as section titles, so every key after
the table in 5. MODEL VALIDATION counted as a decision outside it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-13 00:29:06 +02:00
leonarski_fandClaude Opus 5.5 8f8d0c47f5 rugnux: one sweep over the reflections for the overload counts and the ice flags
The two passes read every integrated reflection and are memory-bound once on all threads;
the ice flags are now set in the overload sweep. 7qij, 78 M reflections: 0.4 s for both
(1.2-1.3 s serial before this lever). Same counts, same flags.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 23:42:21 +02:00
leonarski_fandClaude Opus 5.5 095814baed rugnux: place the quality guard from the pre-pass engine's size, before either ingest
On a set too large for the quality guard's engine to sit beside the main one (8qaw: 123 M
observations; 8a1a), the main engine was ingested, found not to leave room, dropped and
ingested again after the guard - several seconds of ingest for nothing. The pre-pass's
engine merged the guard's very frames earlier in the run, so its card memory per reflection
is now kept (prepass_engine_bytes_ / prepass_engine_reflections_) and both engines are
reckoned from it before either ingests: the guard at its frames' reflections, the main
engine at all of them (observations add in proportion; rocking events and distinct hkl at
most in proportion). Beside where the two fit the card less the 3 GB reserve, otherwise
the guard first. The re-ingest path is gone.

7qij, 9v16, cytc: beside; 8qaw, 8a1a: guard first. Same merges, same numbers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 23:31:49 +02:00
leonarski_fandClaude Opus 5.5 5f7b4d06bc rugnux: the post-refinement gather starts once the scaling engine has ingested
The gather and the engine's ingest both read every reflection, and side by side they split
the memory bandwidth and the worker threads; the engine's ingest is what the chain waits for.
The gather now waits for it (or for the pass to end) and runs beside the first merges
instead, which leave the host mostly idle. 7qij main ingest: 2.9 -> 1.1 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 22:53:56 +02:00
leonarski_fandClaude Opus 5.5 d5bf32763f rotation merge: the resident ingest smooths the geometry on the card (last-bit change)
The rest of the resident ingest moves to the card: the delta_phi re-solve against each
frame's smoothed lattice, the one exact-Bragg angle per rocking event, the partiality
recomputed from the smoothed mosaicity (with corr brought to it) and the histogram of the
frames an event spans. The host keeps the per-frame work (the smoothed lattices, the
mosaicity window) and reads back only corr; nothing per observation crosses back, and the
host arrays the smoothing worked on are no longer built on this path.

The integer work is the host's to the bit (the event-span histogram). The floating-point
work differs in the last bits: every rounding on the card is explicit (no contraction, so
the same bits on any GPU), the host's compiler fuses some of the same products into sums,
and erf/atan2 come from the CUDA library. This is the one place in the lever where results
change; see the hand-back report for the sets and metrics it moves.

Measured on 7qij (main engine, 78 M reflections): the whole ingest 6.5 s host + 1.3 s device
before this lever -> 2.9 s, of which 2.4 s is the upload (memory-bound, beside the
post-refinement gather) and 0.25 s this smoothing. Same space group, error model,
resolution and reflection count; IMEAN differs at the 1e-7 level except on near-zero
intensities.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 22:46:16 +02:00
leonarski_fandClaude Opus 5.5 4158c48dfd rugnux: post-refinement gather beside the chain; quality guard beside the engine on one card
Two waits on the critical path of a canonical rotation pass go:
- the post-refinement gather (2-3.5 s on a 78 M-reflection sweep) now runs in the task that
  fits it, beside the chain; it reads only the reflections, and the chain waits for it before
  the space-group search, the first step that re-indexes them;
- with one GPU the quality guard's merge used to have the card to itself while the main
  engine waited. It now runs beside the main engine wherever both fit the card's total
  memory (the main engine's DeviceBytes plus its reflection share for the guard, plus the
  3 GB reserve the all-observation arm uses); otherwise it runs first as before and the
  main engine is ingested again after it. The main engine's first merge holds its
  per-frame write-back until the guard has read the outcomes (as the arm on a copy does),
  and the guard's numbers are recorded where the pass first needs them.

Same merges, same numbers; only when the guard and the gather run changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 22:38:34 +02:00
leonarski_fandClaude Opus 5.5 f88ff0440d rotation merge: the resident ingest sorts and cuts the reflections on the card
With the GPU resident, the host ingest swept the source reflections four times (background
and h histogram, sort keys, the in-range build, the device staging), bucket-sorted the keys
and cut the raw-hkl runs: 5-6 s of host time per engine on a 78 M-reflection sweep, run
twice beside the quality guard. Now the frames' reflections are copied once, through two
pinned buffers, to the card, where the per-frame background is summed, the reflections
are ordered by two stable radix passes ((image, index), then raw hkl - the one order the
host's total comparator gives), cut into runs by run-length encoding, filtered by the
resolution range and compacted. The observations, the runs and their order are the host
ingest's to the bit; a new GPU test checks the layout against the host's sort and cut.
The geometry smoothing still runs on the host in this step, on arrays read back.

The all-observation arm's engine is cloned after the main engine's device ingest (its
DeviceBytes are known only then) and copies the runs on the card as well. DeviceBytes now
also covers the ingest's own peak (90 bytes per reflection, 64 per run before the cut).

Measured: p.hkl and MTZ data identical on cytc and 7qij.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 22:38:25 +02:00
leonarski_fandClaude Opus 5.5 e24fb88b53 rugnux: count overloads and flag ice-ring reflections over the images on all threads
The two passes over every integrated reflection that open the scaling step ran on one
thread: 1.2-1.3 s on a 78 M-reflection sweep, on the critical path before the scaling
engine starts. One image per task now; the counts are integers and each reflection's
ice flag depends on itself alone, so the result is the same at any thread count.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 22:38:09 +02:00
leonarski_fandClaude Opus 5.5 dfd69fcb91 marCCD frames are widened on the device
The marCCD reader read each frame's 16-bit scanlines into scratch and widened them to int32 on the
worker thread (marccd::ReadInto, ~20 % of the image-loop worker CPU on 4nwv), and the 4-byte image was
uploaded. ReadRawImage now reads the scanlines straight into the image's buffer and hands them over as
a new read-only CompressionAlgorithm::UINT16_WIDENED (mode Int32): two bytes a pixel cross PCIe, from
page-locked memory, and the preprocessing kernel widens them itself.

It is not CompressedImageMode::Uint16, which calls 65535 an error pixel; a marCCD's 65535 is its
saturation marker. So the kernel compares each pixel with int32's error marker and the int32 saturation
cap - the comparisons are made on the pixel's value, in int64, rather than in the storage type - and
decides every pixel exactly as for the int32 image the host would have widened it to. The host widening
(JFJochDecompressPtr) is the CPU path.

Test: ImagePreprocessorGPU_StoredEncodingsMatchCPU, with a frame carrying 65535 at a saturation of
65535, against the CPU preprocessor on the host-widened image.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 22:37:47 +02:00
leonarski_fandClaude Opus 5.5 f4252ad39d miniCBF frames are decoded on the device
The CBF reader decoded every frame's x-CBF_BYTE_OFFSET stream on the worker thread - 19-30 % of the
image-loop worker CPU on CBF sets (7qij, 4mtk) - and the 4-byte image was then uploaded. ReadRawImage
now hands over the stream as the file stores it, in the image's own buffer, as a new read-only
CompressionAlgorithm::CBF_BYTE_OFFSET (mode Int32, what it decodes to), and ImagePreprocessorGPU
decodes it on the card into the same int32 image and runs the same preprocessing kernel on it.

The device decode (CBFDecoderGPU) cuts the stream into 256-byte tiles. A token is 1, 3, 7 or 15 bytes,
so a tile can be entered at 15 offsets; for each one thread works out where the decode leaves the
tile, how many pixels it emitted and their sum modulo 2^32 (the walks from different entries meet
within a few bytes, after which they are one walk). An exclusive scan composing these maps gives each
tile its true entry, first pixel and running value, and a second walk writes the pixels. A stream
that ends before the image is refused as on the host.

The host decoder moves to compression/ (DecodeCBFByteOffset) and is reached through
JFJochDecompressPtr, so every host consumer - the CPU build, the azimuthal-integration worker, the
pre-scan's shadow and gain passes - decodes exactly as before, now from the stream. It is the CPU
path, not a fallback: a frame the device refuses is not retried on the host. A process file written
from CBF input stores the decoded int32 image, as it always did. .cbf.gz is still inflated on the host.

Tests: CBFDecoderGPU_MatchesHost (every token size, 0x80 inside payloads, escape runs across tiles,
trailing CIF text), CBFDecoderGPU_ShortStreamIsRefused, ImagePreprocessorGPU_StoredEncodingsMatchCPU.
Checked locally against the host decoder on 36 real frames of 9 open-arm CBF sets (signed 32-bit,
signed and unsigned 16-bit, plain and gzip): identical.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 22:37:37 +02:00
leonarski_fandClaude Opus 5.5 e8409ef608 BraggPredictionRotGPU: order and select the predictions on the device
The rotation predictor used to download every predicted reflection into pinned memory, copy it into
the host vector, build sort keys, std::sort them, gather the 88-byte structs into scratch and copy
them back (OrderOutput), and - on a frame over output_limit - partial_sort the structs themselves
(TruncateToOutput). On a large cell that is a quarter to a third of the image-loop worker CPU
(profiles of 4nwv and 7qij: TruncateToOutput 12-16 %, OrderOutput 9-13 %, plus the memmoves).

Now the same order is built on the device with stable cub radix sorts of an index: delta_phi, then
the packed hkl (OrderOutput's h, k, l, delta_phi), then - over the cap only - partiality descending
and excitation error ascending (TruncateToOutput, whose hkl tie-break the stable sort keeps). Only
the kept rows are gathered and downloaded. Floats are keyed by an order-preserving integer map with
-0 made +0, so the key order is the comparators' order; the only possible difference from the host
is between two solutions of the SAME hkl with bit-identical partiality and excitation error, whose
relative order partial_sort leaves to its heap and the stable sort to delta_phi.

The device buffers grow with half again of slack, so a frame predicting a few more than the last
record (the supercell probe's frames above all) no longer reallocates and predicts again; the host
side is sized for what is returned, at most output_limit.

Test: BraggPredictionRotGPU_DeviceOrderMatchesHost holds the device output against OrderOutput's
order and against TruncateToOutput applied on the host, at four caps.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 22:36:31 +02:00
leonarski_fandClaude Opus 5.5 d0d225cb99 rugnux --model: molecular replacement where the rigid body does not place the model
One --model now does the right thing by itself:
- A prediction (AlphaFold/ModelCIF, read from the file) goes straight to molecular replacement.
- An experimental structure is placed by the rigid body as before. The null now runs on every
  --model run (no NOT_TESTED): "placed" = the fit beats its null (4.51 sd) in the answer's merge
  or in the twin hypothesis' merge. Placed: behaviour unchanged (hand and reindex may go into
  p.mtz); a subgroup the model fits and prefers is written as <prefix>_mr.
- Not placed (or no crystal cell): molecular replacement with the experimental-model path. It may
  relabel the hand into p.mtz past the anomalous veto, never reindexes; another group or the twin
  subgroup goes only to <prefix>_mr. Maps, R-free and CC come from validating the MR placement with
  every decision switched off; MODEL_FIT stays the rigid body's verdict.
- The twin hypothesis' merge is made in memory with a model where a metric re-ask promoted the
  group or the model's group has fewer rotations than the adopted one; written and reported only
  where the model chooses it.
- --mr is kept as "--model, searched for by MR straight away".
- Report: MODEL_PLACEMENT= RIGID_BODY|MR|NOT_PLACED, MODEL_DECISIONS_TAKEN gains MR_ENANTIOMORPH,
  MODEL_CHOICE_FILE_PREFIX/SPACE_GROUP; the MR keys move into 5. MODEL VALIDATION; MODEL_FIT absent
  where no rigid body ran. MR NOT_PLACED is never a warning.
- Battery wrong-model step: check (c) also fails a wrong model placed by MR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 22:20:36 +02:00
leonarski_fandClaude Opus 5.5 1fe73bab41 Model validation: scale fits on the GPU, change-of-basis probe gridded once per frame on all threads
The scale fit of every model-validation fit (fit_model: the real model, the indexing probe's relabellings
and every null replicate) was FitModelScale on the host - a long-double Levenberg-Marquardt per solvent grid
point, ~80% of the validation's CPU on a 400 A cell. It now runs on the GPU through ModelScaleGPU::FitSolvent,
the engine the rigid body and molecular replacement already use, from a small pool (ModelScaleGPUPool: up to
four engines with their own streams, sized on the card's total memory, at most an eighth of it). The grid,
the fits and the winner rule are FitModelScale's; the sums are taken in double on the device, so R-factors
agree with the host's to rounding. The CPU build, and a GPU build without a card, keep FitModelScale.

The change-of-basis probe gridded each candidate frame with gemmi's serial put_model_density_on_grid, once
for the data as merged and once more for each alternative indexing. It now grids each frame once, with
ModelGrid's parallel PutModelDensityOnGrid (the same grid, bit for bit), from threads outside the pool so the
gridding spreads over it, and scores every labelling on that F_calc. Exact.

4nwv (122k atoms, I222, 400 A cell): model validation 33.2 s -> 14.0 s; every logged R, sigma and decision
the same.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 21:40:54 +02:00
leonarski_fandClaude Opus 5.5 f567771d56 CHANGELOG: rc.175 names --mr, which takes predicted and experimental models
Validation Battery / Validation Battery (push) Canceled after 0s
Build Packages / Create release (push) Successful in 8s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 3m32s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 8m59s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 14m0s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 18m30s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 19m33s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 20m38s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 23m30s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 24m37s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 24m55s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 26m41s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 19m44s
Build Packages / Generate python client (push) Successful in 29s
Build Packages / Build documentation (push) Successful in 1m10s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 21m2s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 21m50s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 22m42s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 18m3s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 22m16s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 20m0s
Build Packages / Unit tests (push) Failing after 1h25m11s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 20:08:50 +02:00
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 3053455c1f Merge branch 'viewer-plot-combo' into rc175: jfjoch_viewer combo boxes no longer rebuilt on each live update
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 19:47:23 +02:00
leonarski_fandClaude Opus 5.5 9cf987cd4e jfjoch_viewer: live updates no longer rebuild open combo boxes
During a collection every dataset tick ran JFJochViewerDatasetInfo::UpdateLabels(), which
clear()ed the plot-type combo and re-added every item, so an open dropdown lost its hovered
row and scroll position on each tick and picking a plot type was a race against the next
update. The same clear-and-refill ran per tick or per image in the calibration window's
calibration combo, the ROI list combo and the powder calibrant combo; the last two also
threw away the user's choice (the calibrant was reset to the first entry on every image).

New LiveComboBox::SetItems() takes the full item list and leaves the combo untouched when the
list is unchanged. When it does change while the popup is open, the change is held back and
applied (queued) once the popup window hides, so a click that closes the popup lands first.
A replacement keeps the selection by item data, blocks signals, and a held-back one emits
itemsReplaced() so the owner can re-plot.

The calibration window now re-shows the selected calibration on a dataset tick instead of
always reloading the pixel mask over it.

Tested offscreen: a timer fires datasetLoaded() every 25 ms while the dropdown is open; the
popup and hovered row survive, newly available indexing metrics are held back until the popup
closes, and the picked type sticks and is what the chart plots. The rc175 code fails the
hover checks of the same harness.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 19:46:48 +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 83b1d85400 Merge branch 'viewer-refresh' into rc175: jfjoch_viewer Files panel reloads on F5 and polls every 3 s while visible
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 19:24:23 +02:00
leonarski_fandClaude Opus 5.5 c27282e577 jfjoch_viewer: F5 and a 3 s poll refresh the Files panel
The Files panel never showed files written after a directory was first
listed when they came from another host (a detector writer on NFS/GPFS/
Lustre), because QFileSystemModel only learns about changes through its
inotify watching, and there was no way to refresh it by hand.

F5 (window-wide, only while the panel is visible) and a refresh button
next to the root's browse button re-list the root and every expanded
directory. A timer does the same every 3 s while the panel is visible
and no mouse button is held, but re-lists only directories whose mtime
changed since the last listing, so a steady tick costs one stat per
expanded directory.

QFileSystemModel has no rescan call; index() of a path it does not know
adds that row in place, so only the names missing from the model are
passed (index() is ~10 us a call; a CBF sweep is thousands of files).
Selection and expansion are untouched, new rows are sorted into place,
and the first visible row is kept where it was when rows land above it.
Entries deleted by another host still stay listed; local deletes are
seen by the model's own watching as before.

Measured offscreen on a 5000-file directory: unchanged tick ~1 ms,
re-listing the changed 5000-file directory ~5 ms.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 19:23:44 +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 aef1c84bf1 rugnux pre-scan: build the spot measurement's mapping on the cores the readers leave free
The mapping is now built while the frames are read, and with every core it took them from the
readers - on a 16M EIGER the read went from 0.10 to 0.19 s. Built on AvailableCpus() minus the
readers it is back at 0.11 s. The mapping is per pixel, so the same whatever thread count builds it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
2026-10-12 18:16:23 +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 1b21958828 Merge branch 'mr-tncs-null' into rc175: --af-model null turns each copy about its own centroid, so tNCS no longer lets wrong models pass
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 16:48:36 +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 699bff1cb1 Merge branch 'sym-evidence' into rc175: POINTLESS/ZANUDA symmetry evidence for 18 open-arm sets, kept with the battery
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 16:02:51 +02:00