Commit Graph
14 Commits
Author SHA1 Message Date
jungfrauandClaude Opus 5 20f1827550 Fit the modulation surface on a grid that spans the detector
The detector-frame modulation correction takes its 16x16 grid extent from a pass
over every full, but the surface is fitted only on the fulls that belong to an ASU
group. Those are two different populations, and the gap between them is whatever
was integrated past the resolution the merge uses.

That made the correction's fate depend on how far integration reached. Cut it back
and the grid contracts onto the merged disc while the cell count stays the same, so
each cell holds too few reflections, the surface over-fits, and cross-validation
throws it away - correctly, on a surface that should never have been fitted at that
scale. Varying only the integration limit on one rotation dataset, merged R_meas
came out 28.4 / 33.1 / 29.0 / 32.8 / 31.7 %, and the four-point spread is entirely
the correction switching on and off: every low value is a run where it was applied,
every high value one where it was refused, with no exceptions. Nothing else moved.

The grid now spans the detector. Cells with no observations in them keep a factor of
1 and cost nothing, and with integration running to the detector corner - the default
- the grid is the one it always was, so the common case is unchanged. On a crystal
carrying no resolution limit at all it takes merged R_meas from 39.7 % to 35.5 %.

This is a correctness fix in its own right. It also has to come first: without it, any
change that narrows the integrated resolution range trips the same over-fit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 18:04:23 -04:00
jungfrauandClaude Opus 5 088ba1cff8 Stop paying for workers and threads that do no work
Build Packages / build:viewer-tgz:cpu (push) Successful in 19m27s
Build Packages / build:windows:nocuda (push) Successful in 20m0s
Build Packages / build:viewer-tgz:cuda (push) Successful in 21m29s
Build Packages / build:rpm (rocky8) (push) Failing after 17s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 23m38s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 24m41s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 29m28s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 29m40s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 30m14s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 20m32s
Build Packages / XDS test (durin plugin) (push) Successful in 11m57s
Build Packages / build:windows:cuda (push) Successful in 21m56s
Build Packages / Generate python client (push) Successful in 31s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 12m34s
Build Packages / Create release (push) Skipped
Build Packages / build:rpm (rocky9) (push) Successful in 21m12s
Build Packages / Build documentation (push) Successful in 57s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 19m21s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 22m0s
Build Packages / DIALS test (push) Successful in 17m22s
Build Packages / XDS test (neggia plugin) (push) Successful in 7m53s
Build Packages / Unit tests (push) Successful in 1h17m43s
Four changes, all from measuring why this 48-core machine was SLOWER than a
quarter of itself on a single job.

The image loop now takes a worker count of its own. It is GPU-bound, every
worker builds a private analysis engine of tens of megabytes of device and
pinned memory - and every one of those allocations implicitly synchronises the
device - so past a handful per card another worker adds setup and contention and
no throughput. Measured at about 23 ms of pure setup per extra worker, which is
why the penalty is WORSE on short runs: 200 images cost 0.86 s of loop at 12
workers and 2.25 s at 48. Capped at four per GPU, floor of eight. Every other
phase still gets the full thread count, because each one starts its own workers.

Ingest built its array with a serial push_back over every observation of every
frame - 63 million of them on the largest crystal here. Each frame's block offset
is known before anything is written, so the frames convert together, each still
written by one thread in its own order.

The pass that buckets observations by h was the single most expensive thing in a
large run - 24% of all cycles, in five instructions. It strided an array of
80-byte observations to read one 4-byte field, and its store address depended on
the loaded value, so the store buffer could not retire and the misses stopped
overlapping. The sweep that already reads every observation now copies h out as
it goes, and the bucketing walks that instead.

And the post-refine passes took the raw thread count. One of them runs 134 times
inside the rotation-scale fit, starting 48 threads each time to divide 390k terms
among them; it is gated on the work now, like everything else.

Measured on one crystal, N=48: 12.80 s -> 11.45 s, which is what 12 threads used
to cost, and on the best-matched pair the two are now level. On the heaviest
crystal ingest goes 13.5 s -> 9.9 s and the run 77.8 s -> 69.4 s. Battery 9m01s
-> 8m24s, space group 21/24, no failures.

Also restores get_gpu_numa_node() - the sysfs lookup deleted with NUMAHWPolicy -
and an opt-in CPU pin to that node behind JFJOCH_PIN_CPU_TO_GPU_NODE. It is off
because it measured neutral here: all four GPUs hang off two of the four nodes,
so pinning to them costs a worker the other half of the machine. It is kept for
boxes whose GPUs are spread over every socket. The old lookup had a latent bug -
CUDA reports the PCI id with upper-case hex and sysfs paths are lower case, so on
three of this machine's four GPUs it would have silently returned "unknown".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 10:53:47 -04:00
jungfrauandClaude Opus 5 057fff98b2 Give each FFT direction a block and its histogram shared memory
The de-novo indexer projects every spot onto each of 16384 search directions and
bins the projections; the peak of each direction's spectrum is a reciprocal
lattice row spacing. One thread owned a whole direction, so neighbouring lanes
wrote 12.6 kB apart and every warp instruction touched 32 separate sectors of a
206 MB buffer with no chance of staying in a 4 MB L2. 160 million scattered
global read-modify-writes, at about 14% of the card's bandwidth.

One block per direction now, with the bins in shared memory. They are counts, so
they are held as integers: an integer atomicAdd is a real shared-memory
instruction where the float one compiles to a compare-and-swap retry loop, and a
count below 2^24 converts to float exactly, so the output is bit for bit what the
repeated += 1.0 produced. Above 48 kB of bins the old kernel still runs.

245.75 ms per launch -> 4.08 ms, so 0.98 s of the run -> 0.016 s. This machine
runs two of them at once on two cards, so it is worth about half a second here
and about a second on the single-GPU machines the viewer and the broker run on.
The FFT it feeds takes 3.3 ms; preparing its input took 70x longer than
transforming it.

Alongside it, the per-frame scale fit divided by k^2 once per observation per
IRLS iteration, and a loop-invariant divisor does not get hoisted out of a double
division - ptxas emits the whole Newton refinement of the reciprocal every time.
Hoisted, as 1/sigma already is a few lines above; the same expression in the
three CPU scale paths went with it so the two stay algebraically identical.
54.92 ms per launch -> 44.56 ms, 1.65 s -> 1.34 s.

That one is not bit-identical - a multiply by a rounded reciprocal differs from a
correctly rounded quotient in the last place - so it can move a frame that sits
on the convergence tolerance. Battery: 21/24 space groups, no failures, and 17 of
24 crystals identical to the previous run, against a floor of 13 of 24 for the
same binary run twice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 06:29:48 -04:00
jungfrauandClaude Opus 5 8ea3076f96 Take the serial passes out of the combine phase
Four passes over all ten million partials and one over all four million fulls,
each of them a single thread walking a fat struct: the snapshot of corr taken
before the pass filters, the zeta filter, the frame rejection, the extraction of
corr for the upload, and the scatter of the downloaded fulls arrays back into
Obs. All are independent per element, so all are chunked now, and the split is
gated on the amount of data rather than the core count.

Measured on the heaviest crystal, summed over the run: corr snapshot 0.14 s ->
0.05, zeta filter 0.20 -> 0.04, corr upload 0.16 -> 0.07, fulls scatter 0.27 ->
0.06.

Two things were tried in the same phase and are NOT here, both measured on the
way past. Spreading the combine over the other three GPUs: the whole
gpu_->Combine() call is 1.27 s over the entire run, so four cards could save
about a second at best, but the partials would have to live on every device -
1.7 GB of transfer at the 1.5 GB/s this machine gets - and the fulls would have
to be gathered back for the merge, which reads them where they are. The
replication alone costs more than the best case saves. And staging the fulls
download through a page-locked block, which reads like the textbook fix for
copies running at 1.5 GB/s, measured 0.76 s -> 0.80 s: whatever limits them
here, it is not the driver's bounce buffer.

Battery 9m23s, space group 21/24, no failures; the crystals that moved are the
two already known to sit on knife edges.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 05:59:16 -04:00
jungfrauandClaude Opus 5 a1325637d2 Spread the scaling corrections and the space-group search over the cores
Build Packages / build:viewer-tgz:cpu (push) Successful in 19m41s
Build Packages / build:viewer-tgz:cuda (push) Successful in 22m26s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 24m17s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 25m22s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 28m12s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 28m41s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 29m1s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 20m8s
Build Packages / XDS test (durin plugin) (push) Successful in 11m39s
Build Packages / build:rpm (rocky9) (push) Successful in 21m51s
Build Packages / Generate python client (push) Successful in 34s
Build Packages / Build documentation (push) Successful in 1m24s
Build Packages / Create release (push) Skipped
Build Packages / build:rpm (rocky8) (push) Successful in 26m38s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 22m0s
Build Packages / DIALS test (push) Successful in 21m28s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 25m22s
Build Packages / XDS test (neggia plugin) (push) Successful in 10m14s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 11m19s
Build Packages / Unit tests (push) Successful in 1h19m35s
Build Packages / build:windows:nocuda (push) Successful in 59m3s
Build Packages / build:windows:cuda (push) Successful in 1h2m17s
Two thirds of a rotation run is one thread. The image loop is not the problem -
on the heaviest crystal of the battery it is 1.8 s of 40 - and neither GPU nor
CPU is saturated, because while the corrections and the space-group search run
there is one core working and 47 idle. Mean occupancy over the whole run: 3.9 of
48.

In the correction surfaces (absorption in the goniometer frame, detector-plane
modulation, absorption against time and detector position - all one function):
the per-cell accumulation, the score reduction and the final apply are now
chunked, as are the three loops that assign a full to its cell, one of which
spends a sine and a cosine per full de-rotating it into the crystal frame. Two
full sorts of four million floats went with them: only the nine bin edges are
wanted, so they are selected instead, each selection starting where the last one
left off.

The per-group pass is deliberately left serial. The terms of one group are
spread all over the list, so the only way to give a thread groups of its own is
to walk in group order, and that trades a near-sequential read of the fulls for
a random one over a few hundred megabytes - the trade that already lost once in
the combine kernel.

The space-group search scores each candidate rotation by correlating I(h)
against I(Rh) over the whole merge. Every operator it can ask about comes from a
fixed list and none of them depend on each other, so they are scored up front,
in parallel, and the search reads the cache. The scratch that stops a pair being
counted twice is now per worker rather than shared.

Worker counts are gated on how much work there is, not on how many cores the
machine has (ThreadsForWork). Both parallel helpers start a thread per chunk, so
a small dataset on a large node would otherwise pay for 48 thread starts to sum
a few thousand terms - and this runs on 8-core laptops as well as on this node.

Measured on the heaviest crystal, idle machine, two runs each, summed over both
passes: those phases go 7.88 s -> 5.19 s. Whole-run wall time is the wrong ruler
for it - it moves +-4 s between identical runs. Battery 9m45s -> 9m23s, space
group 21/24, no failures; 16 of 24 crystals bit-identical to the previous run
and the rest inside the noise floor of running one binary twice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 02:38:20 -04:00
jungfrauandClaude Opus 5 3aa239fce8 Parallelise the incident-flux divide, drop a redundant sync
DivideOutIncidentFlux was still the last fully serial pass in Ingest: a sweep
over every observation to take each frame's mean background, and another to
divide every rlp by its frame's flux. Ten gigabytes of traffic on one thread.

The per-frame means go a frame at a time rather than an observation at a time,
so each frame's running sum stays in one thread and in the order it had -
splitting by observation would cut a frame across two threads and the partial
sums would have to be recombined, which is a different sequence of roundings.
The divide is per-element and splits anywhere.

The adaptive spot finder synchronised after flagging strong pixels. The
extractor that reads those pixels runs on the same stream, so the ordering
already guaranteed the flagging had finished; the wait only idled the host,
once per image.

Measured on a crystal with 66 million partial observations: Ingest 8.5 s and
7.7 s -> 7.1 s and 6.6 s, whole crystal 1m24s -> 1m17s. Merged statistics
unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 22:57:24 -04:00
jungfrauandClaude Opus 5 f36349060a Sort the partials and the post-refine events in buckets, in parallel
Both were one std::sort on one thread over tens of millions of elements, and
together they were a third of a crowded crystal's run.

Bucketing by h first makes them parallel. h is the comparator's leading key, so
the sorted array is exactly the buckets laid end to end, and each bucket sorts
on its own thread. In Ingest the keys are built straight into their bucket slot,
so this replaces the build pass rather than adding one and the packed-key array
is never duplicated; the extra memory is a few hundred kilobytes of histograms.
Buckets are taken largest first, because the tail of the phase is whichever
bucket finishes last.

The run split falls out of the same structure for free: a run of equal (h,k,l)
never crosses an h boundary, so each bucket counts its own runs, a scan over the
buckets gives the offsets, and the arrays are sized exactly - which also removes
the repeated growth the push_backs were paying for. The h range comes from the
finiteness pass, which already reads every observation.

The partials order became total in an earlier commit, when the observation index
was added as the last key. That is what makes this safe rather than merely fast:
the permutation is uniquely determined, so a bucket sort produces the same one a
single sort would.

Measured on a crystal with 66 million partial observations: Ingest 15.2 s and
14.3 s -> 8.3 s and 7.4 s, the post-refine event sort out of the top ten gaps
entirely, the whole crystal 2m22s -> 1m24s. Battery 15m32s -> 10m05s. Same space
group on all 24 crystals, none failed, and no crystal's R_meas moved by more
than 0.3 points.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 22:31:05 -04:00
jungfrauandClaude Opus 5 56512414aa Make the partials order total, and hoist 1/sigma out of the IRLS loop
The sort that orders every observation by (h,k,l,image_number) was not a total
order: two observations can genuinely share all four. The predictor emits BOTH
intersections of a reflection's rotation circle with the Ewald sphere, and near
the blind region - where zeta is smallest - the two are close enough in angle
that both are accepted on the same frame. Which of them came first was then
whatever the sort happened to produce.

That was observable. The combine takes on_ice from the FIRST member of a
rocking event, so the order decided whether a full was flagged as ice at all,
and its per-event sums are floating point, so it moved intensities in their
last bits. The observation's own index is now the final key, which orders them
by arrival - and, more usefully, makes the order unique, so it no longer
depends on which algorithm sorted it.

sigma never changes once it is uploaded, so 1/sigma is the same in all thirty
IRLS iterations of all three scaling iterations of all five scaling passes. It
was being recomputed every time: a 64-bit reciprocal is a hardware estimate
plus five refinement steps, and the profile put the three divisions in that
loop at 21 of its 31 double-precision instructions. It is computed once now, in
the pass that already streams every observation. The CPU has always hoisted it;
this is the GPU catching up. Same expression on the same operand, so the value
is what the loop used to compute, bit for bit.

Also: PrepScaleObsKernel is not a grid-stride loop, but the scale-fulls path
capped its grid at 65535 blocks like the grid-stride kernels around it. Above
16.8 million fulls that silently left the tail of sco_coeff/sco_ok stale. No
dataset here reaches it; the cap is simply wrong for that kernel.

And the AoS-to-SoA staging that feeds the GPU - the widest pass in Ingest,
reading an 80-byte struct and writing fourteen arrays out of it - ran on one
thread.

Full 24-crystal battery: same space group on all 24, none failed, one crystal
moved R_meas by 0.8 points with CC unchanged (it moves by that much between
runs of an identical binary). 15m32s -> 13m35s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 18:56:26 -04:00
leonarski_f 538f3504d3 v1.0.0.rc-161 (#71)
Build Packages / build:windows:nocuda (push) Successful in 20m4s
Build Packages / Unit tests (push) Skipped
Build Packages / build:viewer-tgz:cpu (push) Successful in 16m5s
Build Packages / build:viewer-tgz:cuda (push) Successful in 17m26s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 27m46s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 20m17s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 26m13s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 23m17s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 28m11s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m30s
Build Packages / build:rpm (rocky8) (push) Successful in 24m34s
Build Packages / build:rpm (rocky9) (push) Successful in 21m30s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 23m33s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 20m18s
Build Packages / DIALS test (push) Successful in 18m23s
Build Packages / XDS test (durin plugin) (push) Successful in 11m30s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 10m16s
Build Packages / XDS test (neggia plugin) (push) Successful in 8m2s
Build Packages / Generate python client (push) Successful in 49s
Build Packages / Build documentation (push) Successful in 1m21s
Build Packages / Create release (push) Skipped
Build Packages / build:windows:cuda (push) Successful in 29m45s
This is an UNSTABLE release. It includes many experimental features, as well as many AI generated fixes. We recommend using rc.152 for production use.

* **rugnux: significantly better quality of results, and faster.** A large rework of integration, scaling, merging, geometry refinement and space-group determination, together with measurements the program previously made no attempt at - the direct beam before indexing, the beam stop, the goniometer rotation scale, and the stretches of a sweep the crystal did not deliver. A rotation dataset typically gains observations at better <I/sigma> and R_meas, and every `mx` and `scale` run writes a `<prefix>_report.txt` results report modelled on XDS's `CORRECT.LP`. Many defaults moved with it: spot detection is self-calibrating, beam-stop detection and rotation geometry post-refinement are on, resolution limits default to as far as the detector reaches, and ice-ring handling engages only where the crystal is measured to have ice.
* **jfjoch_viewer:** the beam-stop shadow, the detector calibration and the beam-centre measurement are reachable from "Analyze dataset"; the settings panel reports how the sample moved and how polarized the beam was; image rendering and interaction are faster.
* **Performance:** bitshuffle+LZ4 images are decoded on the GPU rather than on the host, with the bitshuffle inverse fused into preprocessing so the decompressed frame is never held in device memory.
* **Broker, writer, packaging and build:** image-slot lifetime and locking fixes, per-image datasets sized by the images actually written, the Debian/Ubuntu broker package renamed to `jfjoch`, and `image_analysis` compiling under MSVC again.

**Breaking change to the rugnux command line:**
* `--azint-only` and `--scale` are **removed**, replaced by `--mode azint` and `--mode scale`; the full pipeline is `--mode mx` and remains the default. A script passing the old flags now fails with the list of valid modes rather than silently running the wrong one.
* `-t`/`--stride` is **refused on rotation data**: skipping frames cuts every reflection's rocking curve, so the combined fulls and their partiality would be measured over frames the sweep never recorded. Select a contiguous range with `-s`/`-e` instead. `--mode azint` and `--force-still` still take a stride.

**Breaking changes to OpenAPI** - regenerate the client (`jfjoch-client` 1.0.0-rc.161, `frontend/src/client`) or read the affected fields as optional:
* `image_scale_b` is removed from the `plot_type` enum, so a client requesting that plot now gets an error rather than a curve.
* `azim_int_settings.high_q_recipA`, `spot_finding_settings.high_resolution_limit` and `spot_finding_settings.low_resolution_limit` are no longer `required`. All three mean "no limit at that end" when unset and are omitted from the response instead of carrying a placeholder value, which raises in a client generated from an rc.160-or-earlier spec. A value of 0 is still accepted and means the same thing.

**Breaking changes to the stored formats** - a consumer reading these fields must treat them as optional:
* The per-image image-scale B factor is no longer computed, so `/entry/MX/imageScaleBFactor` is absent from newly written HDF5 files and the corresponding key is absent from the CBOR DataMessage and END blocks. Files written by rc.160 and earlier still contain it and still open; nothing in the pipeline reads it any more.
* `_reflns.jfjoch_diffrn_ISa` now carries the whole-range `1/sqrt(a*b)` that XDS's ISa denotes, and the error-model `a` and `b` are reported in XDS's convention; the strong-reflection asymptote moves to `_reflns.jfjoch_diffrn_ISa_asymptotic`. **A file written by an earlier version carries the asymptote under the plain `ISa` name.**

Reviewed-on: #71
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-08-13 17:03:10 +02:00
leonarski_f 67dca388bd v1.0.0-rc.160 (#70)
Build Packages / Unit tests (push) Skipped
Build Packages / build:windows:cuda (push) Successful in 18m44s
Build Packages / build:viewer-tgz:cpu (push) Successful in 6m11s
Build Packages / build:viewer-tgz:cuda (push) Successful in 6m54s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 9m40s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 10m41s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 10m10s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 10m4s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 11m5s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 12m23s
Build Packages / build:rpm (rocky8) (push) Successful in 11m30s
Build Packages / build:rpm (rocky9) (push) Successful in 12m51s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 12m8s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 11m21s
Build Packages / DIALS test (push) Successful in 13m22s
Build Packages / XDS test (durin plugin) (push) Successful in 9m2s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 7m55s
Build Packages / XDS test (neggia plugin) (push) Successful in 5m57s
Build Packages / Generate python client (push) Successful in 23s
Build Packages / Build documentation (push) Successful in 57s
Build Packages / Create release (push) Skipped
Build Packages / build:windows:nocuda (push) Successful in 10m24s
This is an UNSTABLE release. It includes many experimental features, as well as many AI generated fixes. We recommend using rc.152 for production use.

* rugnux: Add `--model model.pdb` - score the merged data against an atomic model and compute initial maps. It reports R-work/R-free (scaling the model to the observed amplitudes with an overall scale, an anisotropic B and a flat bulk solvent - the standard few-parameter model, so a batch of maps stays directly comparable) and writes 2Fo-Fc / Fo-Fc electron-density maps (CCP4) plus a map-coefficient MTZ. The structure itself is not refined; the model is only re-fractionalised into the data cell.
* rugnux: The merged reflection output now carries French-Wilson amplitudes (|F| and its sigma) next to the intensities - MTZ `F`/`SIGF`, mmCIF `_refln.F_meas_au`, and the text HKL - computed with the correct centric/acentric Wilson prior and epsilon multiplicity, so a downstream program (e.g. phenix.refine) can refine against amplitudes. The intensity columns are unchanged.
* rugnux: R-free test-set flags are now assigned deterministically and consistently across symmetry - a Bijvoet pair I(+)/I(-) is never split between the work and free sets, and the assignment is a reproducible per-hkl hash that depends only on the reflection index, so every dataset of one crystal form gets the same ~5% free set (what a multi-dataset campaign such as PanDDA needs). On small data the fraction is floored so the test set stays large enough for a stable R-free (~500 reflections, capped at 10%); it stays flat at 5% on ordinary data. When a reference MTZ carries a `FreeR_flag` column its test set is imported instead, letting a whole campaign inherit one shared free set.
* rugnux: A reference MTZ (`--reference-mtz`) can now fix the space group and cell for rotation data too (previously rejected), without being used to scale - the rotation merge stays self-consistent. When the crystal has an indexing (merohedral) ambiguity - a lattice symmetry higher than its Laue symmetry, e.g. P3/P4/P6/C2 - the reference also resolves it: each candidate reindexing (identity plus the twin-law cosets of the metric symmetry) is scored by its intensity correlation against the reference and the data are re-merged in the best-correlating one. This is a metric-preserving relabelling of hkl (the cell is unchanged) and a no-op for a holohedral crystal such as lysozyme.
* rugnux: `--model` validation now aligns the data to the model before scoring - the observed reflections are reindexed into the model's enantiomorph when the two differ only by hand (indistinguishable from merged intensities). A merohedral indexing ambiguity is resolved against the reference MTZ when one is given (so a whole campaign shares one indexing convention); only with a model and no reference does validation fall back to fitting each candidate reindexing and keeping the lowest R-free.
* rugnux: De-novo symmetry - recover a genuine high-symmetry group whose data are imperfectly scaled. Such a merge's within-orbit chi² lands just past the self-consistency bound (each real symmetry step adds a little systematic scatter), right where a merohedral twin also lands, so the chi² ratio alone cannot separate them. The candidate is now rescued when the extra intensity-proportional systematic error it invokes stays small relative to the confirmed subgroup - a genuine symmetry step gains multiplicity without inflating the merge error model's b, whereas a twin forces non-equivalent reflections together and b balloons. Fixes cubic insulin (I23 instead of I222) with no change to any other crystal in the test battery, including the twins that must stay in their lower symmetry.
* Docs: Document the French-Wilson amplitude estimation, R-free flagging, reference-based space-group/ambiguity resolution, and model-based validation/maps in CPU_DATA_ANALYSIS.md.
* Frontend: The status-bar pill now shows a progress bar during detector calibration (previously only during measurement), and the calibration state and its button are labelled "Calibration"/"CALIBRATE" (the internal `Pedestal` state name is unchanged for back-compatibility).Reviewed-on: #70

Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-07-19 09:39:28 +02:00
leonarski_f dd0bffb283 v1.0.0-rc.159 (#69)
Build Packages / Unit tests (push) Skipped
Build Packages / build:windows:nocuda (push) Successful in 11m6s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 10m27s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 10m54s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 9m25s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 10m5s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 11m33s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 11m19s
Build Packages / build:rpm (rocky8) (push) Successful in 12m23s
Build Packages / build:rpm (rocky9) (push) Successful in 13m21s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 12m30s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 11m55s
Build Packages / DIALS test (push) Successful in 13m42s
Build Packages / XDS test (durin plugin) (push) Successful in 9m26s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 6m41s
Build Packages / XDS test (neggia plugin) (push) Successful in 6m12s
Build Packages / Generate python client (push) Successful in 19s
Build Packages / Build documentation (push) Successful in 52s
Build Packages / Create release (push) Skipped
Build Packages / build:viewer-tgz:cpu (push) Successful in 5m29s
Build Packages / build:viewer-tgz:cuda (push) Successful in 6m12s
Build Packages / build:windows:cuda (push) Successful in 18m36s
This is an UNSTABLE release. It includes many experimental features, as well as many AI generated fixes. We recommend using rc.152 for production use.

* rugnux: Add `--model model.pdb` - score the merged data against an atomic model and compute initial maps. It reports R-work/R-free (scaling the model to the observed amplitudes with an overall scale, an anisotropic B and a flat bulk solvent - the standard few-parameter model, so a batch of maps stays directly comparable) and writes 2Fo-Fc / Fo-Fc electron-density maps (CCP4) plus a map-coefficient MTZ. The structure itself is not refined; the model is only re-fractionalised into the data cell.
* rugnux: The merged reflection output now carries French-Wilson amplitudes (|F| and its sigma) next to the intensities - MTZ `F`/`SIGF`, mmCIF `_refln.F_meas_au`, and the text HKL - computed with the correct centric/acentric Wilson prior and epsilon multiplicity, so a downstream program (e.g. phenix.refine) can refine against amplitudes. The intensity columns are unchanged.
* rugnux: R-free test-set flags are now assigned deterministically and consistently across symmetry - a Bijvoet pair I(+)/I(-) is never split between the work and free sets, and the assignment is a reproducible per-hkl hash that depends only on the reflection index, so every dataset of one crystal form gets the same ~5% free set (what a multi-dataset campaign such as PanDDA needs). On small data the fraction is floored so the test set stays large enough for a stable R-free (~500 reflections, capped at 10%); it stays flat at 5% on ordinary data. When a reference MTZ carries a `FreeR_flag` column its test set is imported instead, letting a whole campaign inherit one shared free set.
* rugnux: A reference MTZ (`--reference-mtz`) can now fix the space group and cell for rotation data too (previously rejected), without being used to scale - the rotation merge stays self-consistent. When the crystal has an indexing (merohedral) ambiguity - a lattice symmetry higher than its Laue symmetry, e.g. P3/P4/P6/C2 - the reference also resolves it: each candidate reindexing (identity plus the twin-law cosets of the metric symmetry) is scored by its intensity correlation against the reference and the data are re-merged in the best-correlating one. This is a metric-preserving relabelling of hkl (the cell is unchanged) and a no-op for a holohedral crystal such as lysozyme.
* rugnux: `--model` validation now aligns the data to the model before scoring - the observed reflections are reindexed into the model's enantiomorph when the two differ only by hand (indistinguishable from merged intensities). A merohedral indexing ambiguity is resolved against the reference MTZ when one is given (so a whole campaign shares one indexing convention); only with a model and no reference does validation fall back to fitting each candidate reindexing and keeping the lowest R-free.
* rugnux: De-novo symmetry - recover a genuine high-symmetry group whose data are imperfectly scaled. Such a merge's within-orbit chi² lands just past the self-consistency bound (each real symmetry step adds a little systematic scatter), right where a merohedral twin also lands, so the chi² ratio alone cannot separate them. The candidate is now rescued when the extra intensity-proportional systematic error it invokes stays small relative to the confirmed subgroup - a genuine symmetry step gains multiplicity without inflating the merge error model's b, whereas a twin forces non-equivalent reflections together and b balloons. Fixes cubic insulin (I23 instead of I222) with no change to any other crystal in the test battery, including the twins that must stay in their lower symmetry.
* Docs: Document the French-Wilson amplitude estimation, R-free flagging, reference-based space-group/ambiguity resolution, and model-based validation/maps in CPU_DATA_ANALYSIS.md.
* Frontend: The status-bar pill now shows a progress bar during detector calibration (previously only during measurement), and the calibration state and its button are labelled "Calibration"/"CALIBRATE" (the internal `Pedestal` state name is unchanged for back-compatibility).Reviewed-on: #69

Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-07-13 13:54:03 +02:00
leonarski_f 451310f43d v1.0.0-rc.158 (#68)
Build Packages / Unit tests (push) Successful in 1h32m35s
Build Packages / build:windows:cuda (push) Successful in 18m0s
Build Packages / build:viewer-tgz:cpu (push) Successful in 7m37s
Build Packages / build:viewer-tgz:cuda (push) Successful in 8m55s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 14m13s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 14m11s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 14m35s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 13m57s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 14m23s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 12m45s
Build Packages / build:rpm (rocky8) (push) Successful in 11m39s
Build Packages / build:rpm (rocky9) (push) Successful in 14m0s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 13m42s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 12m38s
Build Packages / DIALS test (push) Successful in 14m55s
Build Packages / XDS test (durin plugin) (push) Successful in 7m11s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 9m7s
Build Packages / XDS test (neggia plugin) (push) Successful in 8m34s
Build Packages / Generate python client (push) Successful in 28s
Build Packages / Build documentation (push) Successful in 1m3s
Build Packages / Create release (push) Skipped
Build Packages / build:windows:nocuda (push) Successful in 9m55s
This is an UNSTABLE release. It includes many experimental features, as well as many AI generated fixes. We recommend using rc.152 for production use.

* Analysis: The azimuthal-integration solid-angle correction now follows the incidence angle to the detector normal (`cos^3` of that angle) instead of `cos^3(2*theta)`, so it is correct for a tilted detector and matches PyFAI `solidAngleArray` and MAX IV azint (unchanged for an untilted detector). Crystal geometry refinement (`XtalOptimizer`) no longer silently ignores an imported PONI `rot3` (rotation about the beam): it is applied as a fixed rotation in the residual so refinement stays consistent with the rest of the pipeline. Polarization and azimuthal binning already honoured `rot3` through the full PONI rotation.
* jfjoch_viewer: Open datasets on the WSL2/UNC filesystem (paths starting `\\`); write processing outputs next to the input file, with a Browse button and independent `_process.h5` / merged `.mtz`/`.cif` toggles; and show the determined space group in the merge-statistics window.
* rugnux: Accept an absolute `-o` output prefix in offline processing.
* Packaging: The self-contained Linux viewer `.tgz` now bundles cuFFT, so it runs without a system CUDA toolkit (`.deb`/`.rpm` are unchanged, distro-managed).
* Docs: Bring the analysis references up to date with the code. `docs/CPU_DATA_ANALYSIS.md` now reflects the unified profile-fit Bragg integration engine, multi-lattice indexing, azimuthal phi binning, the radial parallax/bandwidth profile with sub-pixel centring, the rot3d capture-fraction handling and the automatic CC1/2 resolution cutoff, and drops the descriptions of features that were never implemented (French-Wilson amplitudes, the still excitation-error partiality model); `docs/RUGNUX.md` documents the new `--resolution-cutoff`/`--resolution-cc-target`/`--resolution-shells`, `--min-captured-fraction`, `--mosaicity`, `--reference-column`, the azimuthal correction toggles and the geometry-override options, and corrects the `-N` default. The outdated in-source design notes (ICE_RING_DETECTION, BRAGG_INTEGRATION_ENGINE, NEXTGEN_INTEGRATOR) are removed.Reviewed-on: #68

Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-07-12 19:42:29 +02:00
leonarski_f 54c0100e8e v1.0.0-rc.157 (#67)
Build Packages / Unit tests (push) Successful in 1h28m28s
Build Packages / build:windows:nocuda (push) Successful in 14m45s
Build Packages / build:windows:cuda (push) Successful in 13m13s
Build Packages / build:viewer-tgz:cpu (push) Successful in 6m47s
Build Packages / build:viewer-tgz:cuda (push) Successful in 7m22s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 13m52s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 14m16s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 13m19s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 12m50s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 14m40s
Build Packages / build:rpm (rocky8) (push) Successful in 11m18s
Build Packages / build:rpm (rocky9) (push) Successful in 12m4s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 11m55s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 11m22s
Build Packages / DIALS test (push) Successful in 13m37s
Build Packages / XDS test (durin plugin) (push) Successful in 8m47s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 9m4s
Build Packages / XDS test (neggia plugin) (push) Successful in 7m45s
Build Packages / Generate python client (push) Successful in 34s
Build Packages / Build documentation (push) Successful in 1m4s
Build Packages / Create release (push) Skipped
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 7m16s
This is an UNSTABLE release. It includes many experimental features, as well as many AI generated fixes. We recommend using rc.152 for production use.

* rugnux: Rebrand the offline data-processing subsystem as `rugnux` and consolidate all offline analysis into the single `rugnux` binary - `jfjoch_process` is now `rugnux`, the former `jfjoch_azint` is now `rugnux --azint-only`, and `jfjoch_scale` is now `rugnux --scale` (see the new docs/NAMING.md and docs/RUGNUX.md). Scaling and merging are on by default for rotation and stills (`--no-merge` disables them), replacing the previous opt-in `-M, --scale-merge`.
* rugnux: CLI fixes - default `-N` to all hardware threads, parse numeric option arguments strictly (reject non-numeric or trailing input instead of silently yielding 0), require `--wavelength > 0`, and correct the reproduced command line and `--scale` reference-cell handling.
* rugnux: De-novo space-group improvements - recover genuine high symmetry and centred Bravais lattices from intensities, add an automatic CC1/2 high-resolution cutoff, and report L-test twinning statistics.
* rugnux: Index weakly-diffracting low-resolution rotation data that previously failed (e.g. F-cubic crystals that diffract only to ~4 A on a detector reaching ~1.5 A). The per-frame indexing gate now measures the indexed fraction only within the resolution range the lattice actually diffracts to, so the many sub-diffraction ice/noise spots no longer make the fraction floor unreachable; the two-pass first pass tries several image-sampling schemes (spread across the whole rotation vs a consecutive wedge whose native stride keeps a reflection's rocking curve continuous, letting the FFT resolve a long axis) and keeps the one that indexes the most frames; and the de-novo space-group search no longer discards all reflections (and crashes) when every resolution shell falls below <I/sigma> = 1.
* rugnux: Lower the low-resolution R-meas for strongly-diffracting rotation data - drop edge-of-sweep truncated fulls whose rocking curve was captured below `--min-captured-fraction` (default 0.7 for rotation), and report R-meas only over the observations kept by outlier rejection (matching XDS). The 0.7 default also strips the partiality-extrapolated fulls that dominate the intensity second moment on weakly-diffracting crystals, so the de-novo space-group search is no longer starved by the error-model I/sigma floor and recovers the correct symmetry (e.g. the F-cubic Benas crystals: Benas_3 -> F432, Benas_7 -> P6122, instead of P4/P1); on the reference battery every other crystal keeps its space group.
* rugnux: Write the refined geometry (beam, tilt, axis) to _process.h5 and place non-standard mmCIF items under a reserved `jfjoch` prefix.
* jfjoch_broker: Ordinary acquisition failures (receiver/writer/analysis problems, missed packets, writer disconnect) now return to the Idle state with an Error-severity message, so a run can be retried without an expensive re-initialisation; only failures that leave the detector in an undefined state (new JFJochCriticalException, e.g. PCIe/FPGA faults) go to the Error state and force re-initialisation.
* jfjoch_broker: A synchronous /start now reports its failure to the HTTP caller instead of returning HTTP 200, and an incomplete or truncated dataset (missing packets, writer disconnect) is reported as an error rather than a "reduce frame rate" warning.
* jfjoch_broker: Drop uncollected placeholder rows (number = -1) from the scan_result REST endpoint.
* jfjoch_broker: Fix the inverted per-image compression ratio reported by the Lite receiver (was compressed/uncompressed instead of uncompressed/compressed).
* jfjoch_broker: Bragg integration adds a quantization-noise variance floor with a box-sum fallback, and treats the type-maximum marker as an invalid pixel for unsigned image types.
* jfjoch_writer: Detect file-overwrite conflicts at start for back-channel transports, and reset the writer when end-of-collection finalisation fails.
* jfjoch_viewer: Preview overlays follow the geometry (resolution/ROI arcs, true beam centre, predictions, coral secondary-lattice spots, legend), add save-as-JPEG, and fix an HTTP live-follow memory leak.
* Frontend: Improved aesthetics and usability, and added in-browser pixel-mask and JUNGFRAU-pedestal visualisation.
* CI: Name the Windows installer jfjoch-viewer-* instead of jfjoch-*.Reviewed-on: #67

Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-07-11 07:19:11 +02:00
leonarski_f d6389e12da v1.0.0-rc.156 (#66)
Build Packages / Unit tests (push) Skipped
Build Packages / build:windows:nocuda (push) Successful in 15m31s
Build Packages / build:viewer-tgz:cpu (push) Successful in 5m46s
Build Packages / build:viewer-tgz:cuda (push) Successful in 6m9s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 9m25s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 10m21s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 9m41s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 9m18s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 10m26s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 11m33s
Build Packages / build:rpm (rocky8) (push) Successful in 10m32s
Build Packages / build:rpm (rocky9) (push) Successful in 12m23s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 10m50s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 10m12s
Build Packages / DIALS test (push) Successful in 12m6s
Build Packages / XDS test (durin plugin) (push) Successful in 8m15s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 7m12s
Build Packages / XDS test (neggia plugin) (push) Successful in 5m35s
Build Packages / Generate python client (push) Successful in 27s
Build Packages / Build documentation (push) Successful in 54s
Build Packages / Create release (push) Skipped
Build Packages / build:windows:cuda (push) Successful in 12m37s
This is an UNSTABLE release. It includes many experimental features, as well as many AI generated fixes. We recommend using rc.152 for production use.

* jfjoch_process: Major rotation (rot3d) data processing overhaul - robust profile-fit integration, Cauchy-loss scaling with optional absorption surface, de-novo indexing and space-group/centering determination fixes, and merging statistics + ISa in the mmCIF output.
* jfjoch_process: Add EXPERIMENTAL ice-ring detection (--detect-ice-rings) that excludes ice reflections from scaling.
* Compression: Add BSHUF_ZSTD_RLE_HUFF, make compression size-aware (drop frames that don't fit rather than aborting), and add the jfjoch_recompress tool.
* jfjoch_viewer: Report "Multiple lattices detected" and grey out "Analyze dataset" on a live connection.
* jfjoch_broker: Write smargon chi/phi goniometer positions to NXmx; read sensor thickness/material from HDF5 metadata.
* CI: Build Windows (CUDA and non-CUDA) installers.Reviewed-on: #66

Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-07-03 19:18:56 +02:00