Commit Graph
1192 Commits
Author SHA1 Message Date
leonarski_fandClaude Opus 4.8 fccf9b83e7 RotationScaleMerge: drop profiling + env gates, honour forced mosaicity
Remove the [rsm] per-stage lap timing and the JFJOCH_RSM_NO_GPU / JFJOCH_RSM_CPU_COMBINE
env gates now that the GPU-resident path is the validated default (it runs whenever a GPU
is present, with the CPU loops as the bit-parity fallback; the diagnostic-dump path still
uses the CPU combine).

Honour a fixed (forced) mosaicity: SmoothMosaicityAndPartiality now overrides every frame
with GetForcedMosaicity() when set, instead of always reading the per-frame integration
value - so the caller can route the --mosaicity case through RotationScaleMerge (its
partiality recompute makes it a natural fit) rather than a separate path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 11:41:31 +02:00
leonarski_fandClaude Opus 4.8 34b3c3c4e7 RotationScaleMerge: GPU merge + error-model reductions over the resident fulls
Port the four fulls-walking reductions of MergeAndStats to the GPU, over the fulls
group-CSR already resident from scale-fulls: the per-group inv-var mean + leverage-
corrected error-model samples, the merge accumulate (inv-var sums + deterministic
half-sets, error-model-corrected sigma, with outlier rejection), and R_meas + the
per-shell usable count. The host keeps the parts that don't parallelise cleanly or are
tiny: the I2-sort + 16-bin (a,b) median fit, the per-group reject median (a per-group
median is awkward on the GPU - cheap on the host from the GPU cnt), the merged export,
the shells and the gemmi completeness. Only per-group arrays (~55k) + the samples
(~n_fulls, for the fit) come back - the fulls are not re-walked on the host.

Device HalfForImage (splitmix64) + IceRingIndex mirror the host; the corrected-sigma
uses (b*I_for_b)^2 (not b^2*I^2) to match the host rounding; the R_meas usable count
requires finite d (the host counts only fulls with a valid shell, and a group's fulls
share d, so the shell is assigned per group). Gated on fulls_resident (GPU
combine+scale-fulls active); reject is fully supported so it runs for the default
rot3d command.

merge+stats ~0.49 -> ~0.37s, taking RSM on lyso to ~0.78s (was ~0.91). Validated across
the battery: 15/15 deterministic crystals bit-identical to the CPU path (SG / ISa /
CC1.2 / completeness / total-obs, and the exact outlier-reject count), only EP_cs_01-24
noise wobbles. The em-sort + a,b fit are the remaining host floor. Non-CUDA build
unaffected (use_gpu_merge is always false there).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 10:38:36 +02:00
leonarski_fandClaude Opus 4.8 786edf9929 RotationScaleMerge: GPU smooth-G, corr kept resident across the pass
Apply smooth-G's corr adjustment on the device (a small kernel: corr[i] *=
ratio[frame[i]] for flagged frames, double-then-float, matching host SmoothG) so the
per-image corr never leaves the GPU: it now stays resident through scaling ->
smooth-G -> per-frame CC -> combine, and across the two space-group passes exactly as
the old host round-trip did. The host only builds the tiny per-frame ratio (g/g_smooth
via the extracted ComputeSmoothGWindow) and refreshes host partials[].corr solely for
the CPU-combine path (JFJOCH_RSM_CPU_COMBINE or the diagnostic dump).

This drops the post-scale GetCorr and the two SetCorr re-uploads (~3x25MB/pass) plus the
6.3M host corr-adjust loop: scale-partials ~0.21->~0.10s and the smooth+combine region
shrinks, taking RSM on lyso to ~0.91s (was ~1.47s with phase-1-only, ~1.71s full-CPU) -
under the 1s target for this crystal; merge+stats (~0.49s) is now the dominant chunk.

Bit-identical (GPU smooth-G == host SmoothG on the resident corr); validated across the
battery (15/15 deterministic crystals bit-identical to CPU across default / CPU-combine /
NO_GPU, only EP_cs_01-24 noise wobbles). Non-CUDA build unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:49:31 +02:00
leonarski_fandClaude Opus 4.8 87c70195f8 RotationScaleMerge: faster group-hkl via flat group ids + skip Obs stamp
Profiling showed the per-space-group "group hkl" step (~0.30s/2-pass on lyso) is not
gemmi-bound (the ASU keying is ~6ms) but memory-bandwidth-bound: stamping the group id
onto, and reading it back from, the `group` field scattered across the 56-byte Obs
struct touches the whole ~350MB partials array twice per pass.

Precompute the per-obs AcceptReflection finiteness once (immutable) into a flat 1-byte
array, then stamp the ASU-group id from rawrun_group + that flat array into a flat
group_ids vector for the GPU, and build the group CSR (a stable counting sort, now
parallel) from group_ids - all sequential/flat reads. The Obs.group field is written
only when a CPU stage will read it (no GPU: scaling/CC/combine otherwise use
group_ids / rawrun_group, never partials.group), so the default path skips the strided
Obs pass entirely. group hkl ~0.31 -> ~0.20 s/2-pass on lyso.

Output is bit-identical (group_ids values and the obs-index-ordered gperm are unchanged),
so the merged results are unchanged; validated across the battery (15/15 deterministic
crystals bit-identical to the CPU path, only EP_cs_01-24 noise keeps its benign wobble).
Non-CUDA build unaffected (need_obs_group is always true there).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:25:31 +02:00
leonarski_fandClaude Opus 4.8 7d804eb799 RotationScaleMerge: GPU post-smooth group means + per-frame CC
The single-threaded ReduceGroupMeans over the 6.3M partials (~0.07s/2-pass) and the
per-frame diagnostic CC now run on the resident partials on the GPU: after SmoothG,
the smoothed corr is uploaded once (and left resident for the combine, dropping the
combine's redundant re-upload), then the post-smooth group means (reusing the scaling
reduce) and the per-frame Pearson CC (a new one-block-per-frame kernel) run there and
only the tiny per-frame cc/cc_n come back. FinalizePerFrameScale is split into
ComputePerFrameCC (host reference) + the writeback; the GPU path uses ComputePartialCC.

The per-frame CC is diagnostic only (the per-image scaling table), so the tree
reduction's ~ulp difference from the CPU is immaterial and it does not touch merged
intensities. smooth+CC region ~0.10s GPU vs ~0.15s CPU on lyso. Validated across the
battery: 15/15 deterministic crystals run-to-run deterministic and merged output
bit-identical to the CPU path (only EP_cs_01-24, unindexable noise, keeps its benign
error-model-b wobble). CPU fallbacks (JFJOCH_RSM_CPU_COMBINE / _NO_GPU) unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 08:58:26 +02:00
leonarski_fandClaude Opus 4.8 617184041f RotationScaleMerge: enable GPU combine + scale-fulls by default
Steps 1-2 (GPU 3D-combine + resident scale-fulls) are validated bit-parity and
run-to-run deterministic against the CPU path across the rotation battery, and cut
the combine+scale-fulls region from ~0.46s to ~0.32s on lyso, so make them the
default when a GPU is present (consistent with phase-1 partial scaling already being
default-on). JFJOCH_RSM_CPU_COMBINE forces the CPU combine/scale-fulls for A/B or
debugging; JFJOCH_RSM_NO_GPU still disables the whole GPU path.

The only battery crystal whose reported metrics move is EP_cs_01-24 (CC1/2 2%,
unindexable noise) whose upstream integration is itself nondeterministic; its merged
intensities/CC/completeness are unchanged, only the ill-conditioned error-model b.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 08:35:23 +02:00
leonarski_fandClaude Opus 4.8 ced85bcd9d RotationScaleMerge: GPU scale-fulls, fulls kept resident (phase 2 step 2)
Scale the combined fulls (Unity model) on the device so they no longer round-trip
between the combine and the merge: after the GPU combine, build the fulls' per-frame
and per-ASU-group CSRs on the host from just the small key arrays (f_frame/f_group)
with a deterministic counting sort - no GPU stable-sort - then scale in place and
download once.

The four scaling kernels are reused unchanged except FitPerFrameGKernel, which gains
an optional `perm` argument (null for the partials, whose arrays are already
frame-contiguous; a frame-grouping permutation for the emit-ordered fulls) so the
fulls are scaled without a physical reorder. The Unity model falls out of giving the
fulls all-ones partiality/rlp/zeta (coeff = mean), so no other kernel changes and the
committed phase-1 partial-scaling path is bit-identical (perm == null -> idx == i).

Validated across the rotation battery (JFJOCH_RSM_GPU_COMBINE=1): all 15 deterministic
crystals stay run-to-run deterministic and their merged output is bit-identical to the
CPU path (SG/ISa/CC1.2/completeness). The lone exception is EP_cs_01-24 (CC1/2 2%,
R_meas 379% - unindexable noise): merged intensities/CC/completeness match exactly, but
the ill-conditioned 16-bin error-model b fit amplifies the ~1e-7 scale-fulls rounding
to ISa 10.6 vs 10.8 - benign, same class as the accepted phase-1 GPU rounding. The 3
upstream-nondeterministic crystals vary as before (GPU-prediction overflow, not this).

Scale-fulls drops from ~0.09s to ~0 across the two passes; combine+scale-fulls region
~0.32s GPU vs ~0.46s CPU on lyso. Still opt-in (fulls are downloaded for the host merge;
the win grows once the merge/error-model also stay resident).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 07:51:29 +02:00
leonarski_fandClaude Opus 4.8 2c928b27cd RotationScaleMerge: GPU 3D-combine (CUDA port, phase 2 step 1)
Port Combine() (partials->fulls) to CUDA, mirroring process_rawrun bit-for-bit:
one thread per raw-hkl run splits its usable partials into rocking events (frame
gap <= 2), pools background, seeds F, runs 3 de-biased Poisson reweights and the
capture-uncertainty term. Emission is deterministic - a count pass, a host
exclusive prefix sum for per-run offsets, then an emit pass at those offsets - so
fulls come out in raw-run-major/event order, identical to the CPU path; both pass
instantiations share the same arithmetic so count == emit exactly. Dmax/Dmin/Fmax
reproduce std::max/min NaN semantics (not fmax) for parity.

Validated across the 18-crystal rotation battery: all 15 deterministic crystals
(P1/P2/C2/H3/I23/P41212/P222/P422) match the CPU combine exactly on SG/ISa/CC1.2/
completeness and run-to-run (fulls count bit-identical); the 3 upstream-nondet
crystals vary from GPU-prediction overflow, not the combine.

Gated opt-in behind JFJOCH_RSM_GPU_COMBINE (default = CPU combine): combine alone
is timing-neutral because the shared 1.2M SortFullsByFrame std::sort dominates and
the fulls round-trip adds a copy - it only pays off once the fulls stay resident
for scale-fulls + merge. Also add JFJOCH_RSM_NO_GPU master switch to force the CPU
fallback (incl. phase-1 scaling) from one binary for A/B parity. SortFullsByFrame
extracted from the Combine tail and shared by both paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 07:27:33 +02:00
leonarski_fandClaude Opus 4.8 dd25de461d RotationScaleMerge: GPU partial-scaling loop (CUDA port, phase 1)
First stage of moving the rotation scale/merge onto the GPU. The per-frame partial-scaling loop
(inverse-variance group-mean reduction -> robust per-frame IRLS G -> corr update, x scaling_iter)
now runs in RotationScaleMergeGPU (.cu) when a GPU is present; the CPU loops remain the fallback.

The host keeps the one-time raw-hkl sort and the per-space-group gemmi ASU keying, and hands the
GPU a group-ordered permutation + CSR so the per-group reduction is a DETERMINISTIC segmented
reduction (one thread per group, fixed order, no atomics) - preserving the run-to-run determinism
just won on the CPU path (a float atomicAdd reduction would have re-introduced jitter). Reduction is
one-thread-per-group (groups average tens of obs, so a block-per-group wastes threads); the IRLS is
one block per frame with a deterministic shared-memory reduction.

Validated: bit-identical to the CPU path and deterministic run-to-run on lyso/cytC/Ins_H/pding
(P41212 ISa 7.8 CC1/2 99.7%, etc.). The scaling kernels are ~7x faster than the CPU compute
(~36 ms for 3 iters vs ~0.28 s); end-to-end scale/merge ~2.0 -> ~1.5 s. The remaining gap to the
<1 s target is the per-pass host round-trip (corr down/upload for the CPU combine + per-SG group-CSR
rebuild); phase 2 keeps the data resident by moving the 3D combine and the merge/error-model onto
the GPU too, so nothing round-trips.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 22:26:29 +02:00
leonarski_fandClaude Opus 4.8 29c8ba6112 rotation: deterministic frame-order mosaicity smoothing + partiality recompute
Prediction applied a mosaicity/profile-radius moving average (RotationParameters) over the
last N *processed* frames. Under the parallel per-image loop that window is thread-arrival
order, so the smoothed value - and hence which reflections are predicted/integrated - was
non-deterministic run-to-run, swinging CC1/2 (and even the space group) on marginal crystals.
`-N 1` was deterministic; `-N 32` was not.

Fix (as designed with FL): prediction now uses each frame's OWN mosaicity/profile-radius
(image-local, deterministic membership - a reflection on the cutoff contributes ~nothing).
The smoothing that actually matters is moved into RotationScaleMerge and done in FRAME order
(deterministic): per-frame mosaicity is smoothed with the same window as smooth-G, then every
partial's partiality is recomputed from it BEFORE the 3D combine. This is the mosaicity analogue
of smooth-G: combining a reflection's per-frame partials only tiles the rocking curve correctly
(captured fractions summing toward 1) if neighbouring frames share a consistent mosaicity.

Battery (18 crystals, /data/rotation_test, 2 runs each): 15/18 now bit-identical run-to-run
(the good crystals unchanged - lyso P41212 ISa 7.8 CC1/2 99.7%). The 3 residual crystals
(EcwtAL500, EcwtCQ066S, pding4_003 - all large/triclinic cells) still jitter ~0.002%, traced
to a SEPARATE, benign cause: the GPU prediction buffer overflow (BraggPredictionRotGPU
max_reflections=10000 with a racy atomicAdd/atomicSub) on dense frames - cell/space group stay
stable; to be addressed in the GPU prediction/integration rework (naively raising the cap also
changes prediction quality, so it is not a one-line bump). Minor label refinements from the
recomputed partiality: cytC_2 P321 -> P3121 (now consistent with cytC_3), Ins_I_2/3 report the
honest I23/I213 screw-axis ambiguity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:58:10 +02:00
leonarski_fandClaude Opus 4.8 b5d9167bf4 RotationScaleMerge: chunked parallelism -> scale/merge phase ~2s
The per-observation corr update (7.6M items) ran through a work-stealing ParallelFor that
does one atomic fetch_add PER item - pure contention for trivial work (measured: update 0.60s
vs reduce 0.15s / fit 0.13s in the scale-partials loop). Add ParallelChunks (one contiguous
range per worker, no per-item sync) and use it for UpdateCorr, and parallelise the ASU keying
(gemmi reduction per distinct raw hkl - HKLKeyGenerator is const, safe to read concurrently)
and the group-stamping over disjoint raw-hkl runs.

scale-partials 0.90 -> 0.28s, group-hkl 0.20 -> 0.09s, per-pass warm 0.83s, whole scale/merge
phase ~3.3 -> ~2.0s. Bit-identical output (same space group, ISa, CC1/2). ParallelChunks is
the CPU stand-in for a flat CUDA grid-stride kernel; ParallelFor stays for the heavy, uneven
per-frame fits where the atomic amortises and work-stealing balances the load.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:31:41 +02:00
leonarski_fandClaude Opus 4.8 29c5410328 rotation scaling: dedicated allocate-once RotationScaleMerge, ~3.4x faster scale/merge
The rot3d post-pass (scale -> smooth-G -> 3D combine -> scale-fulls -> merge ->
error model -> stats) dominated offline wall clock because ScaleOnTheFly + MergeAll
+ CombineRotationObservations + MergeOnTheFly rebuild a std::map keyed by hkl on every
scaling iteration and every merge (7-14 map rebuilds per space-group pass), each re-walking
and re-keying millions of per-frame partials.

RotationScaleMerge ingests the per-frame partials ONCE into flat vectors and reuses them
across both space-group passes: the raw-hkl ordering is sorted a single time at ingest, so
the per-pass 3D combine only re-splits events (no sort) and the ASU grouping is one gemmi
reduction per distinct raw hkl (~10x fewer) rather than per observation, reusing that order.
Every hot step is a flat loop (segmented reduction + per-frame robust IRLS + parallel
per-run combine) that also maps directly onto CUDA kernels. CC1/2 and the per-image CC are
computed once at the end, not every iteration.

It is a distinct path from ScaleOnTheFly, used only for the self-scaling rotation case
(Rotation partiality + combine3d, per-image G, no B refinement, no external reference, no
absorption surface, no wedge/mosaicity override). Stills, B-factor refinement, reference
scaling and the absorption surface stay on the classic path.

Numerically equivalent to the classic path (same robust per-frame G, same 3D combine, same
XDS-order scale-fulls, same global error model, same merge statistics), validated on the
18-crystal /data/rotation_test battery: 16/18 bit-identical in space group / ISa / CC1/2;
the 2 differing crystals are ice-heavy / marginal ones on which the classic path is equally
non-deterministic run-to-run (a pre-existing upstream integration race). Scale/merge wall
time drops ~3.4x (median 9.8 -> 4.1 s), making higher --scaling-iterations cheap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:00:12 +02:00
leonarski_fandClaude Opus 4.8 711216e116 scan_result: short "ice" field + global Bravais lattice type
Two per-object scan_result API tweaks (C++ internal names unchanged):

- Rename the per-image scan_result field ice_ring_score -> ice (it is
  serialized once per image, so keep it short). Only the OpenAPI property and
  its regenerated C++/TS clients change; the DataMessage/EndMessage/CBOR/HDF5
  field stays ice_ring_score, and the ice_ring_score plot_type enum is untouched.

- Add a global rotation_bravais string (crystal-system letter + centering, e.g.
  "tP", "cF", "hR") to scan_result, alongside rotation_unit_cell /
  rotation_crystal_lattice. It comes from the RotationIndexer result
  (LatticeSearchResult system+centering) via the receiver, formatted by a new
  BravaisSymbol() helper in ScanResult.h.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:00:12 +02:00
leonarski_fandClaude Opus 4.8 ddddfb6ffc bragg_integration: GPU box + profile-fit integrator (standalone engine)
Build Packages / build:windows:nocuda (pull_request) Successful in 14m41s
Build Packages / build:windows:cuda (pull_request) Successful in 16m48s
Build Packages / build:rpm (ubuntu2404_nocuda) (pull_request) Successful in 11m15s
Build Packages / build:rpm (rocky8_nocuda) (pull_request) Successful in 12m46s
Build Packages / build:rpm (ubuntu2204_nocuda) (pull_request) Successful in 12m38s
Build Packages / build:rpm (rocky9_nocuda) (pull_request) Successful in 13m11s
Build Packages / build:rpm (rocky8_sls9) (pull_request) Successful in 12m20s
Build Packages / build:rpm (rocky9_sls9) (pull_request) Successful in 12m22s
Build Packages / build:rpm (ubuntu2404) (pull_request) Successful in 11m7s
Build Packages / build:rpm (ubuntu2204) (pull_request) Successful in 11m55s
Build Packages / build:rpm (rocky8) (pull_request) Successful in 12m56s
Build Packages / Generate python client (pull_request) Successful in 14s
Build Packages / build:rpm (rocky9) (pull_request) Successful in 13m15s
Build Packages / Create release (pull_request) Skipped
Build Packages / Build documentation (pull_request) Successful in 41s
Build Packages / XDS test (durin plugin) (pull_request) Successful in 10m3s
Build Packages / DIALS test (pull_request) Successful in 13m6s
Build Packages / XDS test (neggia plugin) (pull_request) Successful in 6m58s
Build Packages / XDS test (JFJoch plugin) (pull_request) Successful in 7m30s
Build Packages / Unit tests (pull_request) Successful in 58m5s
Build Packages / Unit tests (push) Successful in 1h12m36s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 14m52s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 15m35s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 15m29s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 13m35s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 15m25s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 16m5s
Build Packages / build:rpm (rocky8) (push) Successful in 15m11s
Build Packages / build:rpm (rocky9) (push) Successful in 13m35s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 11m59s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 12m14s
Build Packages / DIALS test (push) Successful in 14m29s
Build Packages / XDS test (durin plugin) (push) Successful in 9m56s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 10m23s
Build Packages / XDS test (neggia plugin) (push) Successful in 9m3s
Build Packages / Generate python client (push) Successful in 20s
Build Packages / Build documentation (push) Successful in 1m10s
Build Packages / Create release (push) Skipped
Build Packages / build:windows:nocuda (push) Successful in 16m39s
Build Packages / build:windows:cuda (push) Successful in 18m40s
Reimplement BraggIntegrate2D (box sum) and ProfileIntegrate2D (Kabsch
profile fit) under one roof as a base + CPU + GPU engine, mirroring the
AzIntEngine / ROIIntegration pattern. Reads the preprocessed int32
ImagePreprocessorBuffer (masked=INT32_MIN, saturated=INT32_MAX), the same
buffer AzIntEngineGPU/ROIIntegrationGPU consume.

The CUDA engine runs one block per reflection with shared-memory
reductions across six kernels (reset, mask, box-sum, profile learning,
profile build, Kabsch fit); the resolution shell is computed inline. The
learning/fit hot path is single precision (FP64 is throttled on consumer
GPUs; reproduces the double CPU path to ~1e-4). Collapsing the per-frame
CUDA API calls into one reset kernel keeps launch-latency overhead low.

Standalone for now: NOT wired into IndexAndRefine. See
BRAGG_INTEGRATION_ENGINE.md for the design and the binding steps.
BraggIntegrationEngineGPUTest checks GPU == CPU across all three modes
(box/gaussian/empirical) within numeric tolerance, plus a [bragg_bench]
perf sweep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:59:45 +02:00
leonarski_fandClaude Fable 5 347228d008 jfjoch_process: azimuthal-integration CLI + default 0.01 1/A q-spacing
Build Packages / build:windows:nocuda (pull_request) Successful in 14m25s
Build Packages / build:windows:cuda (pull_request) Successful in 16m48s
Build Packages / build:rpm (rocky8_nocuda) (pull_request) Successful in 12m0s
Build Packages / build:rpm (ubuntu2204_nocuda) (pull_request) Successful in 11m27s
Build Packages / build:rpm (rocky9_nocuda) (pull_request) Successful in 12m24s
Build Packages / build:rpm (ubuntu2404_nocuda) (pull_request) Successful in 11m13s
Build Packages / build:rpm (rocky8_sls9) (pull_request) Successful in 12m9s
Build Packages / build:rpm (rocky9_sls9) (pull_request) Successful in 12m25s
Build Packages / build:rpm (rocky8) (pull_request) Successful in 11m8s
Build Packages / build:rpm (ubuntu2204) (pull_request) Successful in 10m48s
Build Packages / XDS test (durin plugin) (pull_request) Successful in 8m44s
Build Packages / Generate python client (pull_request) Successful in 13s
Build Packages / build:rpm (rocky9) (pull_request) Successful in 11m57s
Build Packages / Create release (pull_request) Skipped
Build Packages / Build documentation (pull_request) Successful in 43s
Build Packages / build:rpm (ubuntu2404) (pull_request) Successful in 11m54s
Build Packages / DIALS test (pull_request) Successful in 13m47s
Build Packages / XDS test (neggia plugin) (pull_request) Successful in 6m15s
Build Packages / XDS test (JFJoch plugin) (pull_request) Successful in 6m51s
Build Packages / Unit tests (pull_request) Successful in 59m10s
Build Packages / Unit tests (push) Successful in 1h13m8s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 14m54s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 15m34s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 15m28s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 14m20s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 15m11s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 16m0s
Build Packages / build:rpm (rocky8) (push) Successful in 15m14s
Build Packages / build:rpm (rocky9) (push) Successful in 13m20s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 12m37s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 11m1s
Build Packages / DIALS test (push) Successful in 12m23s
Build Packages / XDS test (durin plugin) (push) Successful in 8m1s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 8m16s
Build Packages / XDS test (neggia plugin) (push) Successful in 8m57s
Build Packages / Generate python client (push) Successful in 27s
Build Packages / Build documentation (push) Successful in 1m36s
Build Packages / Create release (push) Skipped
Build Packages / build:windows:nocuda (push) Successful in 15m25s
Build Packages / build:windows:cuda (push) Successful in 17m38s
Add -q/--azim-q-spacing, --azim-min-q, --azim-max-q, --azim-phi-bins (mirroring
jfjoch_azint) so offline processing can set the radial binning, applied before
the azint mapping is built. Set the AzimuthalIntegrationSettings default spacing
to 0.01 1/A (was 0.05): the coarse default barely resolved the narrow ice rings,
diluting the ice-ring score. Finer binning sharpens it a lot with no effect on
processing - EP_cs_01-17 ice score 4.6->7.3 (max 11->23), clean cytC stays ~1.0,
and space group / cell / ISa / completeness are unchanged (cytC, InsI3, MyoB,
pding4_001 verified full-image). Documented in JFJOCH_PROCESS.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 17:24:05 +02:00
leonarski_fandClaude Fable 5 5737d2e1d0 ice score: measure against a smooth whole-profile background, not shoulders
The score's baseline was two adjacent shoulder bins with a bin-overlap bug - the
ring's edge bins were counted in both the ring and the shoulder, since
GetMeanValueOfBins is inclusive. At the typical (coarse) azint binning (dq ~ 0.05
in q, wider than the 0.03 ring half-width) a shoulder is only ~1 bin, so the ratio
was noisy and poorly separated. Replace it with the ring intensity over a smooth
whole-profile background: a running median of the non-ice bins, interpolated under
each ring.

Clean crystals now sit at ~1.0 and ice separates far more cleanly on
/data/rotation_test: cytC 1.06->1.03, lysoC 1.23->2.77, EP_cs_01-17 1.67->4.51
(max 11.4). A z-score / abnormality probability was tried but is uninformative
here - with many photons any real ice ring is highly significant, so the useful
discriminator is the ice magnitude (this ratio), noted in CPU_DATA_ANALYSIS.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 17:05:39 +02:00
leonarski_fandClaude Fable 5 65c7eb6366 docs: complete the per-image-quantity recipe with the HDF5 read-back path
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:43:02 +02:00
leonarski_fandClaude Fable 5 0399d1799d ice score: read back from stored HDF5 (viewer) + write to the NXmx master
The per-image ice_ring_score was written to the data files (HDF5DataFilePluginMX)
but the master-file write and the stored-file read path were missing, so opening a
processed HDF5 (e.g. in jfjoch_viewer) would not surface it. Mirror bkg_estimate:
write /entry/MX/iceRingScore in the NXmx master (HDF5NXmx, from EndMessage), and
read it back in HDF5MetadataSource into the dataset (master + data-file reads) and
per image into the message. Verified the write; the read is a byte-for-byte mirror
of the working bkgEstimate round-trip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:42:17 +02:00
leonarski_fandClaude Fable 5 f422988cd2 ice score: per-image ice-ring indicator plumbed through all layers
A single per-image ice_ring_score - the strongest hexagonal-ice ring band/shoulder
intensity ratio from the azimuthal profile (1 = no ice) - computed in the CPU and
FPGA analysis paths and the offline azint worker, then plumbed through every layer:
DataMessage/EndMessage, CBOR (frame_serialize), HDF5 (/entry/MX/iceRingScore),
ScanResult, receiver plots (PlotType::IceRingScore), the OpenAPI spec (plot_type +
scan_result schema, with regenerated broker/gen and frontend client) and
OpenAPIConvert, the reader + Qt viewer, and the React frontend plot. Documented in
docs/CBOR.md, docs/HDF5.md and docs/CPU_DATA_ANALYSIS.md, with the general
"add a per-image quantity" recipe added to CLAUDE.md.

Verified in HDF5: lysoC (weak ice) mean 1.23, EP_cs_01-17 (heavy ice) mean 1.67 /
max 2.23. This is a monitoring quantity - it does not gate scaling (which already
excludes all ice rings) or merging (handled by the CC1/2 ring mask).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:37:12 +02:00
leonarski_fandClaude Fable 5 6ffb16f142 merge: mask ice rings whose CC1/2 collapses below their resolution neighbours
An ice-contaminated ring decorrelates its reflections, so its merged half-set
CC1/2 falls well below the Bragg trend of its resolution shoulders (e.g.
EP_cs_01-17 2.25A ring CC1/2 0.12 vs shoulders 0.89). After the final merge,
compute per-hexagonal-ice-ring CC1/2 in the ring band (+/- ice width in q=2pi/d)
against the shoulders either side, mask any ring more than 0.05 below its
shoulders (reliable shoulder CC1/2 > 0.5, >=20 reflections each) and re-merge
without them. Weak/absent rings track their neighbours and stay, so clean
crystals are untouched.

Data-driven and robust on /data/rotation_test: 16/18 crystals mask nothing and
are unchanged (every clean crystal, plus icy-but-fine EP_cs_02-10); EP_cs_01-17
masks 6 rings, CC1/2 6.9% -> 28%; CQ066/pding4_003 mask one marginal ring each.
A well-scaled CC1/2 test, replacing the fragile per-image azimuthal cutoffs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:06:39 +02:00
leonarski_fandClaude Fable 5 cdbd0bed5d partiality: fix the rotation-prediction wedge, use the stored value in scaling
BraggPredictionRot halves settings.wedge_deg to the +/- half-wedge of the
partiality erf pair, but IndexAndRefine already passed GetWedge_deg()/2, so the
two /2 compounded to a half-wedge of increment/4 - half the correct Kabsch value
(increment/2, which ScaleOnTheFly's RotationPartiality already used). Pass the
full increment so prediction's partiality matches scaling.

With prediction correct, ScaleOnTheFly now uses the stored r.partiality directly
(the value the reflection was integrated with) rather than recomputing the erf
pair per reflection - recomputing only when scaling overrides the geometry
(-w wedge refinement, --mosaicity, or a scaling wedge override). Output-neutral
on the /data/rotation_test battery (SG/cell/completeness identical, ISa/CC1/2
within run noise on the stable crystals).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 14:57:16 +02:00
leonarski_fandClaude Fable 5 8c084021e6 ice rings: exclude from scaling, keep in the final merge (recover completeness)
--detect-ice-rings previously dropped ice-ring reflections outright before
scaling, which also removed them from the merge and gutted low/mid-resolution
completeness on crystals that merge them fine (e.g. lysoC 80% -> the ice bands
show as jagged shell completeness). Instead flag them (new Reflection.on_ice_ring,
set in JFJochProcess, carried through the 3D combine) and exclude the flag only
where a model is fit: the per-image scale (ScaleOnTheFly::Accept + the per-image
CC), and - for the de-novo P1 pass - the space-group search and error model
(MergeOnTheFly::ExcludeIceRings, on for for_search). The final in-symmetry merge
and its statistics keep them, so completeness is preserved.

/data/rotation_test battery vs the previous drop-from-merge behaviour: space
group correct on all 18; completeness recovered broadly with CC1/2 and ISa held
(cytC_2 82->99.7%, cytC_3 73->99.7%, InsI3 76->99.5%, lysoC 80->99.7%, MyoB
80->99.7%, InsH3 78->99%). Excluding ice from the P1 search merge is what keeps
the space group correct: without it InsI3 flipped I23->P1 and EP_cs_01-17 P2->P1.

Known limitation: on heavy-ice crystals (EP_cs_01-17) the strong ice is garbage
and keeping it in the final merge collapses CC1/2 in the ice shells (91.7->6.9%).
Distinguishing strong vs weak/absent rings per crystal needs data-driven,
per-ring ice detection (azimuthal radial profile) - the planned next step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 14:29:37 +02:00
leonarski_fandClaude Fable 5 1ca5a69054 ScaleOnTheFly: solve the per-image scale by IRLS instead of Ceres
In the default rotation (rot3d) path only G is refined - B is fixed, mosaicity
is pinned and the wedge is not refined - so the predicted intensity G*coeff is
linear in G and the robust (Cauchy) per-image scale is a 1-D M-estimate. Solve
it directly by iteratively reweighted least squares (a few closed-form weighted
ratios) instead of building a Ceres problem per image. Ceres is kept for the
cases that are genuinely nonlinear: refining the B-factor or the rotation wedge.

Same Cauchy objective as the Ceres path, but ~4x faster at scaling and ~30%
faster overall on the /data/rotation_test battery, with space group, cell, ISa,
completeness and CC1/2 matching across all 18 crystals (the two that look
different, EP_cs_01-17 and EcwtAL500, are run-to-run unstable for both solvers).
lyso_ref scaling 25.2->4.3s, cytC_2 15.2->2.6s, battery total 468->316s.

Also drop the per-image G/B regularizers (gated by GetScalingRegularize, which
nothing enables) and the now-unused RegularizationResidual.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 13:55:04 +02:00
leonarski_fandClaude Fable 5 5c9175688a jfjoch_process: let --detect-ice-rings=on|off override the dataset
The master file's detectorSpecific/detect_ice_rings key is read back by
HDF5MetadataSource onto the experiment, so a dataset collected with ice-ring
detection on was reprocessed with exclusion forced on (dropping ~9% of
reflections, gutting low/mid-res completeness on clean crystals) and
jfjoch_process had no way to turn it off. Make --detect-ice-rings an
optional<bool> taking =on|off (bare = on, back-compatible) and apply it
after the dataset value is copied in, so an explicit CLI choice wins.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 13:18:09 +02:00
leonarski_fandClaude Opus 4.8 ed9f6ac9eb integration: remove --reciprocal-profile (proven per-frame dead-end)
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 14m21s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 15m0s
Build Packages / build:rpm (rocky8) (push) Successful in 14m56s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 15m9s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 15m19s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 15m28s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 15m58s
Build Packages / build:windows:nocuda (push) Successful in 16m12s
Build Packages / build:windows:cuda (push) Successful in 18m22s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 7m52s
Build Packages / XDS test (durin plugin) (push) Successful in 8m8s
Build Packages / Generate python client (push) Successful in 29s
Build Packages / Create release (push) Skipped
Build Packages / XDS test (neggia plugin) (push) Successful in 8m10s
Build Packages / Build documentation (push) Successful in 1m3s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 11m42s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 12m25s
Build Packages / build:rpm (rocky9) (push) Successful in 13m12s
Build Packages / DIALS test (push) Successful in 13m48s
Build Packages / Unit tests (push) Successful in 1h16m3s
Build Packages / Unit tests (pull_request) Successful in 56m24s
Build Packages / build:windows:cuda (pull_request) Successful in 11m16s
Build Packages / build:rpm (rocky8_nocuda) (pull_request) Successful in 9m55s
Build Packages / build:rpm (rocky9_nocuda) (pull_request) Successful in 10m45s
Build Packages / build:rpm (ubuntu2204_nocuda) (pull_request) Successful in 11m17s
Build Packages / build:rpm (ubuntu2404_nocuda) (pull_request) Successful in 10m15s
Build Packages / build:rpm (rocky8_sls9) (pull_request) Successful in 11m5s
Build Packages / build:rpm (rocky9_sls9) (pull_request) Successful in 11m57s
Build Packages / build:rpm (rocky8) (pull_request) Successful in 10m48s
Build Packages / build:rpm (rocky9) (pull_request) Successful in 12m13s
Build Packages / build:rpm (ubuntu2204) (pull_request) Successful in 11m22s
Build Packages / build:rpm (ubuntu2404) (pull_request) Successful in 10m34s
Build Packages / DIALS test (pull_request) Successful in 12m24s
Build Packages / XDS test (durin plugin) (pull_request) Successful in 8m45s
Build Packages / XDS test (JFJoch plugin) (pull_request) Successful in 7m23s
Build Packages / XDS test (neggia plugin) (pull_request) Successful in 6m1s
Build Packages / Generate python client (pull_request) Successful in 13s
Build Packages / Build documentation (pull_request) Successful in 47s
Build Packages / Create release (pull_request) Skipped
Build Packages / build:windows:nocuda (pull_request) Successful in 10m4s
An 11-crystal mosaicity-stratified re-test (/data/rotation_test, off vs on
vs a de-contaminated variant, plus a per-frame dump of the fitted widths)
showed the dial is net-negative and cannot work in the per-frame paradigm:

- The C|q|^2 mosaicity term - the whole point - is unfittable per-frame: the
  fitted curvature a2 comes out ~0 (often negative) on every crystal, with zero
  correlation to the XDS mosaicity (0.09..0.42 deg). Strong spots sit at low q
  where eta^2 q^2 is invisible; the curvature only appears at high q where there
  are ~0 strong spots. The law degenerates to a straight line.
- With a2~0 the high-res width becomes a blind 1/cos^2(2theta) extrapolation,
  2-4x wider than per-shell. The per-shell path's high-res "starvation" (flat
  narrow fallback) is accidentally correct: weak, crowded high-res spots want a
  narrow aperture, not the true wide spot shape.
- The over-wide profile pulls background into weak spots -> R-meas rises, CC1/2
  drops in reliable high-multiplicity shells (pding4_001, pding4_003, MyoB,
  EcwtCQ066). A cap at the widest well-sampled per-shell width recovers the
  regression, confirming over-widening is the harm. No crystal reliably wins;
  the apparent overall-CC gains were all in noise shells (mult 2-3, CC<20%).

Delete the CLI flag, the BraggIntegrationSettings::reciprocal_profile setting,
and the per-frame fit block. Default (per-shell) integration is byte-identical.
NEXTGEN_INTEGRATOR.md records the finding as a dead-end for posterity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:39:09 +02:00
leonarski_fandClaude Fable 5 7e3ad84a5b jfjoch_process/scale: default to mmCIF, auto-detect rotation, rot3d partiality
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 13m31s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 15m1s
Build Packages / build:rpm (rocky8) (push) Successful in 14m56s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 15m10s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 15m16s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 15m14s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 15m49s
Build Packages / build:windows:nocuda (push) Successful in 15m5s
Build Packages / build:windows:cuda (push) Successful in 17m15s
Build Packages / XDS test (durin plugin) (push) Successful in 8m51s
Build Packages / XDS test (neggia plugin) (push) Successful in 8m12s
Build Packages / Generate python client (push) Successful in 24s
Build Packages / Create release (push) Skipped
Build Packages / XDS test (JFJoch plugin) (push) Successful in 9m14s
Build Packages / Build documentation (push) Successful in 1m5s
Build Packages / build:windows:nocuda (pull_request) Successful in 9m29s
Build Packages / build:rpm (rocky9) (push) Successful in 13m8s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 11m44s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 12m56s
Build Packages / DIALS test (push) Successful in 13m57s
Build Packages / build:windows:cuda (pull_request) Successful in 16m22s
Build Packages / build:rpm (rocky8_nocuda) (pull_request) Successful in 10m19s
Build Packages / build:rpm (ubuntu2404_nocuda) (pull_request) Successful in 10m45s
Build Packages / build:rpm (rocky9_nocuda) (pull_request) Successful in 13m23s
Build Packages / build:rpm (ubuntu2204_nocuda) (pull_request) Successful in 11m55s
Build Packages / build:rpm (rocky8_sls9) (pull_request) Successful in 11m55s
Build Packages / build:rpm (rocky9_sls9) (pull_request) Successful in 12m40s
Build Packages / build:rpm (rocky8) (pull_request) Successful in 10m46s
Build Packages / build:rpm (ubuntu2404) (pull_request) Successful in 11m3s
Build Packages / build:rpm (ubuntu2204) (pull_request) Successful in 11m48s
Build Packages / build:rpm (rocky9) (pull_request) Successful in 12m57s
Build Packages / Generate python client (pull_request) Successful in 17s
Build Packages / Create release (pull_request) Skipped
Build Packages / XDS test (durin plugin) (pull_request) Successful in 9m11s
Build Packages / Build documentation (pull_request) Successful in 46s
Build Packages / DIALS test (pull_request) Successful in 12m41s
Build Packages / XDS test (JFJoch plugin) (pull_request) Successful in 7m36s
Build Packages / XDS test (neggia plugin) (pull_request) Successful in 5m42s
Build Packages / Unit tests (push) Successful in 1h11m43s
Build Packages / Unit tests (pull_request) Successful in 57m54s
- Default reflection format is now mmCIF (ScalingSettings.h, the single source of
  truth). jfjoch_process and jfjoch_scale no longer hard-code a local MTZ default;
  they override only when --scaling-output is given. jfjoch_viewer inherits this.
- A dataset with a rotation goniometer axis is processed as rotation data (two-pass
  indexing) by default; add --process-as-stills to force per-frame stills. -R still
  tunes the first-pass image count.
- rot3d is the default partiality model for rotation processing (fixed for stills)
  when no explicit -P is given.
- Update docs/JFJOCH_PROCESS.md: new de-novo rotation/stills examples, corrected
  -R/-P/--scaling-output defaults, --process-as-stills, and a real Integration table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 08:34:50 +02:00
leonarski_fandClaude Opus 4.8 4b393082d4 ci: drop requests dependency and use PowerShell for the Windows release upload
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 12m29s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 13m26s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 13m39s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 13m48s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 14m6s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 14m8s
Build Packages / build:windows:nocuda (push) Successful in 14m26s
Build Packages / build:windows:cuda (push) Successful in 17m26s
Build Packages / XDS test (durin plugin) (push) Successful in 7m20s
Build Packages / build:rpm (rocky8) (push) Successful in 11m40s
Build Packages / build:windows:nocuda (pull_request) Successful in 9m34s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 11m18s
Build Packages / Generate python client (push) Successful in 17s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 11m58s
Build Packages / Create release (push) Skipped
Build Packages / Build documentation (push) Successful in 1m8s
Build Packages / build:rpm (rocky9) (push) Successful in 13m19s
Build Packages / DIALS test (push) Successful in 13m23s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 7m32s
Build Packages / XDS test (neggia plugin) (push) Successful in 6m16s
Build Packages / build:windows:cuda (pull_request) Successful in 16m40s
Build Packages / build:rpm (ubuntu2204_nocuda) (pull_request) Successful in 9m55s
Build Packages / build:rpm (rocky8_nocuda) (pull_request) Successful in 10m55s
Build Packages / build:rpm (rocky9_nocuda) (pull_request) Successful in 11m12s
Build Packages / build:rpm (ubuntu2404_nocuda) (pull_request) Successful in 9m35s
Build Packages / build:rpm (rocky8_sls9) (pull_request) Successful in 10m36s
Build Packages / build:rpm (ubuntu2204) (pull_request) Successful in 9m58s
Build Packages / build:rpm (rocky8) (pull_request) Successful in 11m15s
Build Packages / build:rpm (rocky9_sls9) (pull_request) Successful in 12m7s
Build Packages / build:rpm (rocky9) (pull_request) Successful in 11m42s
Build Packages / build:rpm (ubuntu2404) (pull_request) Successful in 10m21s
Build Packages / Generate python client (pull_request) Successful in 25s
Build Packages / Build documentation (pull_request) Successful in 57s
Build Packages / Create release (pull_request) Skipped
Build Packages / XDS test (neggia plugin) (pull_request) Successful in 6m12s
Build Packages / XDS test (durin plugin) (pull_request) Successful in 7m24s
Build Packages / XDS test (JFJoch plugin) (pull_request) Successful in 6m58s
Build Packages / DIALS test (pull_request) Successful in 10m41s
Build Packages / Unit tests (push) Successful in 1h11m21s
Build Packages / Unit tests (pull_request) Successful in 58m33s
The Windows viewer runner has Python but not the 'requests' package, and does
not necessarily have bash. So:
- rewrite gitea_upload_file.py to use only the Python stdlib (urllib), which
  works with a bare interpreter on both the Linux package runners and Windows;
  also drop the file's unused create_release() (gitea_create_release.py owns that);
- run the Windows 'Upload installer to release' step in PowerShell (always present)
  instead of bash, globbing the NSIS .exe with Get-ChildItem.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 21:51:54 +02:00
leonarski_fandClaude Opus 4.8 ae1ea41151 ci: matrix the Windows build and upload installers to the release on tag
The two Windows jobs (build-windows / build-windows-nocuda) differed only in
-DJFJOCH_USE_CUDA=ON/OFF, so collapse them into one matrixed job (variant
cuda/nocuda), mirroring the build-rpm matrix.

On a tag, upload the NSIS installer (jfjoch-<version>-win64-{cuda<major>|cpu}.exe,
named in CMakeLists.txt) to the release via gitea_upload_file.py, the same helper
the RPM/DEB nocuda variants use to attach viewer/writer artifacts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 21:47:01 +02:00
leonarski_fandClaude Opus 4.8 48a7a67283 docs: publish third-party notices as a documentation page
The THIRD_PARTY_NOTICES.md manifest lived only at the repo root and was
referenced from docs/SOFTWARE.md via a ../ link that escapes the Sphinx
source tree, so it never rendered in the published docs and was absent
from the navigation.

Add docs/THIRD_PARTY_NOTICES.md to the General toctree and fix the
SOFTWARE.md link. The docs page is generated from the canonical root file
by update_version.sh (like the python-client docs): licenses/*.txt links
point at the repository, and the project-license links point at the
in-docs LICENSE / FPGA_LICENSE pages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 21:42:22 +02:00
leonarski_f ec904bc21d Remove Gitlab remains 2026-07-01 21:36:36 +02:00
leonarski_f 91cae946a9 VERSION: 1.0.0-rc.156 2026-07-01 21:33:37 +02:00
leonarski_fandClaude Opus 4.8 0f327e4550 docs: add rc.156 changelog entry
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 21:32:51 +02:00
leonarski_fandClaude Opus 4.8 fa121f0079 jfjoch_viewer: show "Multiple lattices detected" in image statistics
Add a row after the Indexing solution that flags images with more than
one indexed lattice (indexing_lattice_count > 1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 21:30:10 +02:00
leonarski_fandClaude Opus 4.8 00821e32ef rotation indexer: refine candidate cells and pick the best after refinement
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 14m24s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 14m32s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 14m40s
Build Packages / build:rpm (rocky8) (push) Successful in 15m8s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 15m12s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 15m25s
Build Packages / build:windows:nocuda (push) Successful in 15m47s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 15m50s
Build Packages / build:windows:cuda (push) Successful in 17m29s
Build Packages / XDS test (durin plugin) (push) Successful in 8m31s
Build Packages / Generate python client (push) Successful in 32s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 9m3s
Build Packages / Create release (push) Skipped
Build Packages / XDS test (neggia plugin) (push) Successful in 8m31s
Build Packages / Build documentation (push) Successful in 1m6s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 11m23s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 12m16s
Build Packages / build:rpm (rocky9) (push) Successful in 12m56s
Build Packages / DIALS test (push) Successful in 12m42s
Build Packages / Unit tests (push) Successful in 1h34m10s
De-novo two-pass indexing failed on large-cell / superstructure / modulated
crystals (EcwtAL500 0%, EcwtCQ034 0%) and mis-handled a pseudo-symmetric one
(EcwtCQ066 14%). The common cause: the choice of unit cell was made too early,
on the raw pre-refinement spot fraction, which is an unreliable discriminator
(a correct hexagonal cell indexes only ~13% of the un-refined accumulated
spots, while a wrong larger cell can index more).

Move the decision to after geometry refinement, where it is reliable:

- FFTIndexer now OFFERS widened candidate cells instead of deciding. ReduceResults
  gains a `widen` flag: the standard path (9 shortest vectors) is unchanged; the
  widened path anchors the two short axes and lets the third range over all
  filtered vectors (+dedup) to reach the long axis of an elongated cell.
  FilterFFTResults takes the peak count as a parameter (30 standard, 60 widened).
  RunInternal appends widened candidates only when its standard best indexes
  poorly, so compact crystals are untouched.

- RotationIndexer fully refines the top few candidates and keeps the one that
  indexes the most spots under its own refined geometry (IndexedFraction). Each
  refine is length-bounded (1.2x the found cell) so a free triclinic refine cannot
  drift onto a pseudo-translation / modulation supercell (CQ034's satellites). The
  earlier (primary) candidate is preferred: a later one is adopted only if it
  indexes clearly more and reasonably well in absolute terms, so a twin's noisy
  near-tie cannot displace it. Extra/twin lattices are only searched when the
  chosen cell is the FFT primary (lattice[0]), since MultiLatticeSearch's
  rotations are derived from that primary.

- The pseudo-symmetry guard (de-novo only - a user-fixed space group is always
  honored) is a ratio of refined fractions: refine the primitive as triclinic and
  drop to P only if the constrained cell indexes less than half of it. A false
  promotion indexes badly under its constraint (CQ066 ratio ~0.1) while genuine
  higher symmetry, including R-centred, indexes comparably (Ins_H R3 ratio ~0.7)
  and is kept.

Validated on the full /data/rotation_test battery: AL500 0->89% (C2), CQ034
0->99%, CQ066 14->93% (ISa 7.2->13.7); the other 15 crystals keep their exact
cell, space group, indexing rate and ISa (no regressions).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 21:17:53 +02:00
leonarski_fandClaude Opus 4.8 dfc6703f4a jfjoch_process: don't double-smooth per-frame G on the reference path
SmoothImageScaleG rewrites the partials in place (image_scale_corr and
image_scale_g). On the no-reference path that is harmless: each scaling
pass recomputes G from scratch via ScaleAllImages, so smoothing always
runs on freshly-refined G. On the reference path the scaling loop is
skipped, so G is computed once and stays; running scale_and_merge twice
(P1 then the adopted space group) smoothed the already-smoothed G a
second time, compounding into a ~2x wider effective kernel than the
configured --smooth-g and biasing the merged intensities.

Smooth only on the first pass of the reference path (G is unchanged
afterwards, and the smoothed partials persist into the second pass's
combine3D). The no-reference path is unchanged.

Verified on lyso (600 frames, -P rot3d -z ref.mtz -M): the reference run
now logs the smoothing once instead of twice, and the merged MTZ changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:24:05 +02:00
leonarski_f ab2176960f OpenAPI: Fix smargon descriptions 2026-07-01 14:12:09 +02:00
leonarski_fandClaude Opus 4.8 c67ea2fa42 receiver: guard size_t underflow in compression buffer sizing
When per-image CBOR metadata comes within 32 bytes of the buffer slot
size, slot_size - (metadata_size + 32) wrapped around (size_t), passing a
huge output_size to CompressImage. That defeated the buffer-too-small
guard and let the compressor write the full image past the end of the
slot, corrupting adjacent memory; AppendImage then threw a plain
JFJochException that aborted the whole collection after the fact.

Detect metadata_size + 32 >= slot_size explicitly and throw
CompressionBufferTooSmallException, so the existing catch drops just this
frame gracefully - the case the change was meant to handle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:05:20 +02:00
leonarski_fandClaude Opus 4.8 b7613c559c smargon: write chi/phi goniometer position into NXmx transformations
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 14m15s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 15m13s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 15m9s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 15m11s
Build Packages / build:rpm (rocky8) (push) Successful in 15m14s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 15m30s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 15m52s
Build Packages / build:windows:nocuda (push) Successful in 15m54s
Build Packages / build:windows:cuda (push) Successful in 17m32s
Build Packages / XDS test (neggia plugin) (push) Successful in 7m30s
Build Packages / Generate python client (push) Successful in 30s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 8m31s
Build Packages / Create release (push) Skipped
Build Packages / XDS test (durin plugin) (push) Successful in 8m47s
Build Packages / Build documentation (push) Successful in 58s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 11m37s
Build Packages / build:rpm (rocky9) (push) Successful in 12m37s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 12m44s
Build Packages / DIALS test (push) Successful in 12m55s
Build Packages / Unit tests (push) Successful in 1h0m54s
Add an optional Smargon static positioner (chi/phi angles + rotation axes)
that is reconstructed into the NXmx sample transformation chain. Chi/phi are
appended at the innermost end of the chain (closest to the sample) for both
the goniometer and grid-scan branches, with axes defaulting to chi {0,0,1}
and phi = omega default {1,0,0}.

- SmargonPosition gains chi_axis/phi_axis (common/JFJochMessages.h)
- OpenAPI: optional phi_axis/chi_axis arrays; clients regenerated
- OpenAPIConvert wires Dataset_settings.smargon -> DatasetSettings
- CBOR serializer/deserializer round-trip the axes
- tests: CBORSerialize_Start_Smargon

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:45:08 +02:00
leonarski_fandClaude Opus 4.8 35607057d9 CC1/2 merge: assign half-sets by deterministic per-image hash
Build Packages / build:windows:cuda (push) Failing after 4m30s
Build Packages / build:windows:nocuda (push) Successful in 10m0s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 13m43s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 14m46s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 14m53s
Build Packages / build:rpm (rocky8) (push) Successful in 14m54s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 15m10s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 15m34s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 15m37s
Build Packages / XDS test (durin plugin) (push) Successful in 8m5s
Build Packages / Generate python client (push) Successful in 31s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 8m50s
Build Packages / Create release (push) Skipped
Build Packages / XDS test (neggia plugin) (push) Successful in 8m44s
Build Packages / Build documentation (push) Successful in 1m7s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 10m45s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 12m5s
Build Packages / build:rpm (rocky9) (push) Successful in 13m8s
Build Packages / DIALS test (push) Successful in 12m47s
Build Packages / Unit tests (push) Successful in 1h0m40s
MergeOnTheFly::AddImage picked each image's CC1/2 half from a shared
mt19937 drawn in call order (and before Mask), so the split depended on
iteration/thread order and on how many images were masked. The class is
mutex-guarded for concurrent "on-the-fly" use, so any parallel merge would
make CC1/2 non-reproducible - a latent race.

Assign the half as a splitmix64 hash of the image's stable index instead,
computed after Mask. The split is now reproducible run-to-run, independent
of AddImage order, parallel-safe, and decoupled from masking. Callers pass
the outcome's vector index as the image id.

Verified: lyso_ref two-pass -M -P rot3d gives identical CC1/2 across runs
(overall 99.6%, P41212); hash split is balanced ~50/50.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 07:25:34 +02:00
leonarski_fandClaude Opus 4.8 a773aaa6a9 WriteReflections: write merging statistics + ISa to the mmCIF output
Build Packages / build:windows:cuda (push) Failing after 2m40s
Build Packages / build:windows:nocuda (push) Failing after 2m40s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 13m47s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 14m42s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 14m46s
Build Packages / build:rpm (rocky8) (push) Successful in 14m52s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 15m10s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 15m5s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 15m47s
Build Packages / Unit tests (push) Failing after 19m1s
Build Packages / Generate python client (push) Successful in 32s
Build Packages / Build documentation (push) Successful in 1m0s
Build Packages / Create release (push) Skipped
Build Packages / XDS test (JFJoch plugin) (push) Successful in 7m54s
Build Packages / XDS test (durin plugin) (push) Successful in 8m7s
Build Packages / XDS test (neggia plugin) (push) Successful in 8m24s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 11m5s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 12m4s
Build Packages / build:rpm (rocky9) (push) Successful in 12m58s
Build Packages / DIALS test (push) Successful in 12m32s
The .cif (--scaling-output cif) now carries the per-shell and overall merging
statistics in the standard _reflns / _reflns_shell categories (resolution, redundancy,
completeness, <I/sigma>, R-rim/R-meas, CC1/2) plus the Diederichs asymptotic I/sigma
(ISa) as a free-text _reflns.pdbx_diffrn_ISa item (no standard CIF tag exists). The
MergeStatistics and the ISa string are threaded through WriteReflections to the mmCIF
writer; jfjoch_process and jfjoch_scale pass them. Values match the text statistics table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 22:29:45 +02:00
leonarski_fandClaude Opus 4.8 ff91ca5e3a jfjoch_process: default --reject-outliers 6 for rot3d (XDS-like)
Per-observation merge outlier rejection at 6 sigma is neutral-or-better across every test
crystal (CC1/2 up or flat: lysoC 99.7->99.9, MyoB 98.9->99.6, EP0210 97.6->98.8,
cytC 99.7->100.0, InsI2 99.2->99.7; R-meas slightly lower everywhere; ISa unchanged), and
it is the lever for radiation-damaged / ice data (EP0117 reaches CC1/2 ~95% with it). Make
it the rot3d default, like scale-fulls and smooth-g; --reject-outliers 0 disables it. Off
for the non-rot3d partiality models, which were not benchmarked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 22:04:36 +02:00
leonarski_fandClaude Opus 4.8 636ed1e180 space group: use merge reduced chi^2 (not R-meas) for the consistency check
The R-meas-ratio over-symmetry guard (commit 7ade6d9) missed a near-perfect
pseudo-2-fold: InsH2 (XDS R3) has a false 2-fold at operator CC 0.85 whose merged
intensities still correlate well, so R-meas inflated only ~1.3x and R32 was accepted
(ISa collapsing to 3.9). The reduced chi^2 (within-orbit scatter / sigma^2) is the
right signal - false equivalents disagree by many sigma even when they correlate well.

Measured chi^2 ratio (candidate / best subgroup) across the test set: true point groups
<= 1.63 (a cubic merge of a noisy crystal, InsI3, is the worst real case), false >= 2.26
(InsH2) and 6.0 (InsH3); threshold 2.0 separates them. InsH2 now resolves to R3 (ISa 6.1
from the false R32's 3.9); InsH3/InsI2/InsI3/cytC/lysoC/MyoB/EP0210 unchanged and correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 21:54:51 +02:00
leonarski_fandClaude Opus 4.8 fea2790a41 rotation_indexer: detect hexagonal metric de-novo (fixes trigonal cytC indexing)
The hexagonal lattice metric (two equal axes at 60/120 deg, both perpendicular to the
third) is also satisfied by its ortho-hexagonal C-centred supercell, so the
geometry-keyed LatticeSearch lands a metrically-hexagonal lattice on the C-orthorhombic
setting. The -S path already re-expresses it (HexagonalConventional) keyed on the
supplied space group; do the same de-novo, keyed on the metric of the reduced PRIMITIVE
cell (rhombohedral lattices have a rhombohedral primitive cell and are unaffected).

cytC (P3121) now indexes de-novo as hexagonal 83.8/83.8/88.6 gamma=120 and merges in
P3121 (CC1/2 99.7%, ISa 13.1), for both test crystals, instead of the orthohexagonal
C2/P1. lysoC/InsI3/InsH3 unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 21:31:20 +02:00
leonarski_fandClaude Opus 4.8 52248b8f5c docs: design for auto-gated, data-driven ice-ring detection
Records the validated z-score detector (on the azint radial background profile) and
position/width estimation, for a future --detect-ice-rings=auto. Not yet implemented.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 21:22:31 +02:00
leonarski_fandClaude Opus 4.8 f3c51b0266 jfjoch_process: --min-image-cc is in percent, not a fraction (fix crash)
--min-image-cc <num> is documented in percent and the per-image CC is a fraction in
[0,1], but the value was passed straight to MinCCForImage (which requires [0,1]), so
the documented usage (e.g. --min-image-cc 30) threw an uncaught JFJochException and
aborted. Divide the percent argument by 100.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 21:09:18 +02:00
leonarski_fandClaude Opus 4.8 73d9e1de39 ice rings: widen the exclusion band to 0.03 q (measured ring FWHM)
The hexagonal-ice ring FWHM measured on JUNGFRAU data (azimuthal radial background
profile) is ~0.06 q, so the exclusion half-width should be ~0.03; the previous 0.02
under-covered the strong low-res rings. On EP0117 with --detect-ice-rings this lifts
CC1/2 37->50%, and combined with --reject-outliers 3 (which down-weights the
radiation-damaged late frames) reaches ~94% (XDS 98.5%). Only active with
--detect-ice-rings, so default behaviour is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 21:09:18 +02:00
leonarski_fandClaude Opus 4.8 a74b8767de ice rings: add --detect-ice-rings and exclude ice-ring reflections from scaling
jfjoch_process gains --detect-ice-rings, which (a) activates the existing spot-finder
ice flagging (ice spots de-prioritised in indexing) and (b) drops reflections sitting
on a hexagonal-ice powder ring from scaling/combine/merge/stats, via a new shared
IsOnIceRing() helper over ICE_RING_RES_A using the spot-finder's q half-width. Their
integrated intensity is contaminated by the strong, variable ice background, so leaving
them in mis-scales the whole frame and inflates the error model.

On EP0117 (ice-ring crystal): de-novo space-group determination recovers from P1 to P2
and CC1/2 improves (31->37%). Off by default; a no-op without the flag. This is the
first, non-controversial step - the residual gap needs ice-aware background/integration
(follow-up).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 20:40:51 +02:00
leonarski_fandClaude Opus 4.8 7ade6d9906 space group: reject over-symmetrised point groups by merge R-meas consistency
Per-operator CC alone cannot separate a real weak symmetry operator from a false
moderate one: a noisy crystal's genuine cubic 2-folds (InsI2 I23, CC ~0.51) score
BELOW a pseudo-symmetric crystal's false 2-fold (InsH3, CC 0.64), so no CC threshold
works. Add a self-consistency test: a candidate point group is accepted only if
merging the intensities under it does not inflate R-meas past max_rmeas_ratio (1.5x)
the most-consistent candidate. A false operator forces non-equivalent reflections
together and blows R-meas up; a real one leaves it flat.

Fixes InsH3: was over-called R32 (ISa collapsed to 2.2 from the forced merge), now
correctly R3/H3 (ISa 10.2, matching XDS). InsI2 stays I23, and lysoC/InsI3/MyoB/
EP0210/cytC/lyso_ref are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 20:32:03 +02:00
leonarski_fandClaude Opus 4.8 23d58b6202 integration: robust, well-conditioned per-frame --reciprocal-profile fit
Fit the reciprocal tangential-width model y(q)=a0+a1*t+a2*t^2 in a centered,
standardized variable t=(q-qbar)/qscale instead of the raw {1,q,q^2} monomials:
the raw normal matrix went near-singular when the strong spots span a narrow
q-range (small cell / sparse still), letting tiny per-frame jitter swing the
curvature into a wild over-wide profile. Adds IRLS (Huber) robustness, a ridge
on the curvature (sharp-crystal prior), and clamps the applied width to the
fitted q-range (no extrapolation). Stays strictly per-frame (no dataset pooling),
so it works online and for stills. Neutral on rotation data (cytC high-res CC1/2
win preserved 66.8 vs 65.6%).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 20:01:05 +02:00
leonarski_fandClaude Opus 4.8 0783af247c scaling: express rot3d smooth-g as a rotation range in degrees (XDS DELPHI-like)
--smooth-g now takes a rotation range in degrees (default 5) instead of a frame
count, converted to an odd frame window from the oscillation step inside
JFJochProcess. This makes the per-frame scale-G smoothing physical and
independent of the frame slicing. (Note: G-smoothing is not the cure for the
low-symmetry stochastic collapse - the per-image scale Cauchy loss is - but a
degree-based window is the correct parameterization regardless.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 20:00:53 +02:00