v1.0.0-rc.169 #79

Merged
leonarski_f merged 83 commits from rc169 into main 2026-09-15 17:09:31 +02:00
83 Commits
Author SHA1 Message Date
leonarski_fandClaude Opus 5 0fccbe21b5 symmetry: the cell a group is adopted on is refined under that group
Build Packages / Create release (push) Successful in 15s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 7m21s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 8m28s
Build Packages / build:viewer-tgz:cpu (push) Successful in 9m53s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m57s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 15m12s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 15m50s
Build Packages / build:windows:nocuda (push) Successful in 17m15s
Build Packages / build:windows:cuda (push) Successful in 19m50s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 23m14s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 19m9s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 19m12s
Build Packages / build:rugnux:windows (push) Successful in 10m50s
Build Packages / Generate python client (push) Successful in 44s
Build Packages / Build documentation (push) Successful in 1m23s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 20m10s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m23s
Build Packages / build:rpm (rocky8) (push) Successful in 18m2s
Build Packages / build:rpm (rocky9) (push) Successful in 18m22s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 13m46s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 12m25s
Build Packages / Unit tests (push) Successful in 1h12m4s
A space group confirmed from the intensities AFTER integration is one no
constrained fit has produced. The Bravais class is decided on the unrefined
indexing candidate, so a two-fold the spot positions never offered leaves the
freely refined metric standing in the group's own setting - a C 1 2 1 whose
alpha is 88.5 - and that cell goes to the report, the master file and the MTZ.

At adoption, re-refine the lattice under the group's constraint against the
accumulated rotation spots, at the geometry the images were integrated at, and
keep it when the spots do - the bar the indexer's own pseudo-symmetry guard
uses. Where they refuse it, report the nearest metric the group fixes and say
that a deviation that size is not refinement noise. A cell whose violation is
above MAX_METRIC_VIOLATION is the WRONG cell for its group rather than an
unconstrained one, and nothing here touches it: a visible mismatch must not
become a plausible-looking one.

The projection is applied as a change of basis, not as three rebuilt vectors:
the three-Coord constructor enforces a right-handed basis, and a left-handed
lattice came back with an axis flipped and its free angle replaced by the
supplement, which failed the merge outright.

Battery over 151 datasets, base against this: 144 byte-identical, 7 cells moved
onto their group's metric, no space group changed, no run gained or lost, open
arm 94/99 both ways. Every merge statistic of the seven is unchanged except
completeness, which rises on four and falls 0.1 % on one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KWkZ1o2aoQ9EimF2wtzBky
2026-09-15 15:57:12 +02:00
leonarski_fandClaude Opus 5 22b4700c1e docs: the analysis chapters describe what rc169 actually does
Build Packages / Create release (push) Successful in 44s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 9m57s
Build Packages / build:viewer-tgz:cpu (push) Successful in 11m14s
Build Packages / build:viewer-tgz:cuda (push) Successful in 12m13s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 6m6s
Build Packages / build:windows:nocuda (push) Successful in 16m48s
Build Packages / build:windows:cuda (push) Successful in 19m18s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 23m56s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 13m58s
Build Packages / build:rugnux:windows (push) Successful in 10m46s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 15m38s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 16m0s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 17m3s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 14m19s
Build Packages / Generate python client (push) Successful in 20s
Build Packages / Build documentation (push) Successful in 1m21s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 16m8s
Build Packages / build:rpm (rocky8) (push) Successful in 15m47s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 15m46s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 15m49s
Build Packages / build:rpm (rocky9) (push) Successful in 16m41s
Build Packages / Unit tests (push) Successful in 1h14m45s
The branch changed the beam centre, the beam stop, rotation acceptance,
post-refinement, the space-group gates and the resolution cut, and left the
three chapters that describe them untouched. Several passages had stopped being
out of date and become wrong: the beam stop is described flooding from a centre
it no longer floods from, at a ratio it no longer cuts at; the space-group
section lists a veto that has been removed; the pseudo-symmetry control is the
one that was refuted and replaced; and post-refinement is bounded by a rule it
no longer applies.

Images and geometry. The whole-detector FFT capture is new here in full - the
self-convolution that scores every candidate centre at once, the three surfaces,
the shortlist-and-margin rather than a centre, and the walk that refines it -
along with the check comparing against the direct beam rather than the PONI, the
per-trial spot cache, and the ladder asked at both centres with its harmonic
refusal. The beam-stop section is rewritten around what it now does: a Poisson
significance beside the ratio, rings drawn about a fitted centre in the order fit
- mask - fit, the polarization divided out before the ring comparison, and size
rather than connection to the beam as what makes a low region a shadow. The
claim that an empty mask means the seeds fell in a gap is gone with the seeds.
Powder rings measured from the run's own spots are described in the ice section.

Indexing. A rotation lattice is accepted on the spots it explains against the
same spots at other frames' angles; the leaner, shallower retry and its harmonic
arbiter; the refinement chain committing its best round on the wide gate; and
post-refinement's one per cent as a trust region on a step, with the walk, the
never-settled refusal and the re-index ratification. The pass guard is compared
on the signal each pass measured.

Decisions. The error-model b test is a rescue and not a veto, with why the veto
went; a confirmed promotion that is refused is decided by merging under it; the
modulation null is the shell-wise permutation; and the resolution cut keeps ice
out of its fit and is not quoted where the shell table refutes it.

Also: the ice flag reaching merged reflections on the rotation path, a report
section for the powder keys, --min-indexed-spots matching the usage message it
had already been corrected in, and one typo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-14 19:54:24 +02:00
leonarski_fandClaude Opus 5 d80a9c429e docs: the rc169 changelog is seven lines a user can read
Build Packages / Create release (push) Successful in 15s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 8m17s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 9m28s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m40s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m43s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 14m52s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 15m57s
Build Packages / build:windows:nocuda (push) Successful in 17m24s
Build Packages / build:windows:cuda (push) Successful in 19m58s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 23m32s
Build Packages / build:rugnux:windows (push) Successful in 10m47s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 19m2s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 21m26s
Build Packages / Generate python client (push) Successful in 59s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 20m58s
Build Packages / Build documentation (push) Successful in 1m25s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 20m49s
Build Packages / build:rpm (rocky8) (push) Successful in 18m18s
Build Packages / build:rpm (rocky9) (push) Successful in 18m2s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 20m0s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 17m42s
Build Packages / Unit tests (push) Successful in 1h37m22s
The branch accumulated 44 bullets, one per landing, written by whoever
landed it: individual fixes, mechanisms and investigation outcomes that a
user cannot act on. Replaced by seven entries covering the whole branch -
build dependencies, offline processing quality, frame rejection and the
resolution cut, the machine-readable results report, the viewer, and the
broker fixes. Rationale and measurements stay in the commit messages,
which already carry them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-14 19:21:08 +02:00
leonarski_fandClaude Opus 5 0bc1c4b7e0 scale/merge: a stretch is removed frame by frame, not on its average
The delta-CC1/2 stage asks an independent channel to corroborate a conviction
before any frame leaves the merge, and the commit that landed the acting
behaviour said the frames must disagree with the merge on their own per-image
correlation. The code took the mean over the stretch, and a mean cannot see a
healthy arc lying beside a dead one: on a sweep with two dead arcs half a turn
apart, 38 frames at 0.97 of the run's scale and 0.91 of its CC, averaged with the
47 at 0.11 and 0.21 beside them, read 0.52 - below the margin - and came out with
them. At matched resolution that pair took CC1/2 from 0.80 to -0.14, where
removing the healthy 38 on their own left it at 0.90.

The corroboration is now taken frame by frame, on the same running medians
MeasureSweepQuality segments the ledger on, so both stages call the same frames
bad and one odd frame decides nothing. Each edge is first pulled in off any frame
that is at the run's typical scale AND at its typical agreement, and what is left
is removed only if it holds no such frame at all and still carries the conviction
on its own. A stretch that cannot be taken without healthy frames has more than
one thing in it, and refusing it is the honest answer. No new constant: both
margins and the median window are MeasureSweepQuality's own. Normal means up on
BOTH channels, because the largest measured gains are stretches whose scale has
collapsed while their per-image CC stays high, and a CC-only rule would refuse
those.

Measured on the reproducer in both compiler-flag builds. The rejected row reading
0.97 of the run's scale and 0.91 of its CC - 1.07 and 1.01 when the same source
is built without -march, frames that diffracted better than the run average - is
gone, and only frames at 6-13% of the run's scale and CC are removed: R_meas
1.3568 -> 1.2840, unique 19381 -> 20302, multiplicity 8.27 -> 9.61, I/sigma
2.23 -> 2.36, d_min 3.227 -> 3.178, CC1/2 -0.0009; in the unflagged build every
statistic improves. The two largest corpus gains are byte-identical. The damage
controls hold: two byte-identical, one keeping every unique reflection and its
resolution shell, and on the cleanest sweep in the corpus three false positives
on 10-frame blocks at 0.62-0.92 of the run's scale are refused.

This is not a strict tightening, and the risk should not be read as one-sided.
The removal loop is iterative, so refusing one candidate lets another be
convicted against a reference that still holds the refused stretch - six frames
are now removed that the mean kept - and a stretch whose scale has collapsed
while its CC stays near the run's is refused by the mean and passed here.

What is still placed by an argmin of delta-CC1/2, on a surface the code's own
comment calls very nearly flat at the outside of a long defect, is the outer edge
inside a defect. Letting the per-frame channel carry that edge to the end of the
run of frames both channels call bad would settle it and would take a defect
whole rather than in pieces, but it expands removals on about ten datasets and
needs its own evidence before it lands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-14 19:16:57 +02:00
leonarski_fandClaude Opus 5 c53139281a resolution: rotation data now keeps ice out of the cutoff fit too
The ice flag that excludes powder-ring reflections from the CC1/2 resolution
fit was carried only through MergeOnTheFly, which is the stills merge. Rotation
data merges through RotationScaleMerge's own accumulator, which had no ice
field, so MergedReflection::on_ice_ring was left false on every merged
reflection and the exclusion never fired on the workflow it was written for.

Carry it through the rotation merge: an ice-flagged observation marks its
merged reflection, on the host loop and in the device MergeAccum kernel (a new
per-group output and staging array - the device path writes every accumulator
field itself, so a host-only change would have kept the old behaviour there).
The intensity is still merged, written and counted; only the cutoff fit skips
it.

Measured on a rotation dataset with 24% of its reflections on ice rings, with a
probe counting the flag at the top of the cutoff fit: 0 of 52180 merged
reflections flagged before, 12881 of 52180 after, and the written resolution
moves from 1.410 A (fit crossing CC1/2 0.30 at 1.48 A) to 1.372 A (1.44 A).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-14 19:05:28 +02:00
leonarski_fandClaude Opus 5 6a87d097b1 rotation indexing: the chain commits its best round, not the round it stops on
The post-selection refinement chain solves, re-accumulates the reciprocal-space
cloud under the refined geometry and solves again, up to twenty times, and then
commits whatever round it stopped on. Which round that is, is decided by a test
on the detector-tilt step - and the unrestrained perpendicular component of that
step is scatter at the size of its own bound, taking values over three decades
with no trend. So the round a run commits is settled by the last bits, and the
compiler flags settle it: on a C-centred monoclinic crystal refined free, the
two primitive axes the lattice makes equal walk from 0.84 % apart one way,
through equality, to 1.15 % apart the other, and the plain-Release and
-march=x86-64-v3 builds stopped seventeen rounds apart on that one walk.

The walk is an overfit, and that is the larger half. Every solve ends by fitting
only the spots inside its tightest gate, so a free cell whose metric is near a
Bravais class can slide along the one direction the data barely constrain,
pulling a core of spots tighter while the periphery falls out of the fit
altogether. Over those seventeen rounds the spots inside the tight gate rise
0.295 -> 0.327 while the spots inside the widest gate peak at round three
(0.754) and fall to 0.730 - and round three is the round that merges ISa 11.0
against 6.3 and R_meas 0.148 against 0.213, and the one that agrees with the
archived reference cell. Nothing the run consulted could see it: indexed
fraction, validation frames, validation spots and the tight-gate count all
prefer the overfitted end.

So score every round on the widest gate - the one XtalOptimizer's first pass
selects on and its last pass does not fit, which makes it the population a
converged solve is not optimising - and commit the best-scoring round. Two
conditions keep that from acting on noise. The score is a COUNT of spots, and a
lead of fewer than sqrt(count) of them is inside the count's own noise, so a
smaller lead leaves the last round standing; at the default accumulation cap
that bound is about 0.9 percentage points, against the 2.4 the case above leads
by. And the round taken has to be the less distorted lattice as well as the
better-fitting one: LatticeSearch is re-asked every round to MEASURE the drift
from the class the metric matches - imposing that class is measured fatal, the
snap puts almost everything outside the refinement's own gate - and the earlier
round is taken only when it matched the same class and sits closer to it.

Same class is a precondition, not a precaution. The deviation is a fraction of
the tolerance of whichever class the round matched, so two classes' deviations
are not the same quantity; and a round that matched no class reports 0, which
means "nothing was asserted", not "undistorted". Comparing that against a
class's deviation reads the absence of a constraint as the absence of
distortion, and switches the veto off - measured, on 11 of 1132 traced chains,
four of them against a round with no class at all - which leaves the spot count
deciding alone. That configuration was measured: it costs another crystal
R_meas 0.2553 -> 0.3181 by firing on a symmetry-constrained chain whose cell
moves 0.10 % over twenty rounds and which is not drifting anywhere.

Measured over 31 datasets and 914 chains: an earlier round scores higher on
44 % of chains, but the rule changes the committed round on 1 of 54. Both
conditions are why - a chain that has settled scores its rounds within a spot or
two of each other, and a symmetry-constrained solve holds its distortion at zero
throughout, so the rule cannot fire on either. Twelve paired datasets are
byte-identical, including five where it fires on a chain whose result the run
does not use, and including the tilt-critical sweeps whose committed chains peak
at their last round 19 times out of 19. Both builds now agree on the crystal
above at ISa 11.00 against 11.01, and the second flag-dependent crystal is
byte-identical in both builds. Deterministic across -N 6/8/32.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-14 18:47:43 +02:00
leonarski_fandClaude Opus 5 6ba95a84c4 symmetry: a refused point group is decided by merging under it
Stage A of the space-group search can confirm a higher point group from the
operator correlations and then refuse to adopt it. Every gate that does so is a
ratio - the added operators' disagreement H against a parent's, or their R
against the best-agreeing operator anywhere - and both references are properties
of how the crystal was mounted rather than of its symmetry. A 2-fold within a
degree of the spindle records its mates on the same detector pixel half a turn
later, so it carries no geometry-dependent systematic at all and by being that
clean makes every other operator look bad against it; and the best-agreeing
operator can be one of the operators under test, which collapses the ratio on
worse data and raises it on better. On 6fid the reference operator's R sits
below the crystal's own half-set noise floor; on a cubic-metric case the same
gate reads 1.59 on worse data and 2.2 on better.

So where a confirmed promotion is refused, ask the merge instead of the ratio.
Both groups are scaled and merged over one pinned resolution range - the adopted
group's own cut - and the promotion is taken only if neither R_meas nor ISa gets
worse. R_meas is multiplicity-corrected, so folding non-equivalent reflections
together has to inflate it, and the error model is refitted per merge, so ISa
says whether the extra multiplicity was bought with systematic disagreement.
CC1/2 and <I/sigma> cannot arbitrate this: both RISE on a false promotion too.

Two details the comparison turns on. The refused point group's representative is
primitive and symmorphic, so the arm merged under it has to take the ADOPTED
group's centring - merged in the bare representative a centred crystal is handed
its centring-absent class as data, and reads R_meas 0.1819 where the right group
reads 0.1153, which inverts the decision. And the group is asked of Stage B on
the adopted merge, so both arms carry the same absence classes and differ by the
added rotations alone.

Six refused promotions, complete sign separation:

  6fid   P2      -> P2(1)2(1)2(1)  R_meas 0.1215 -> 0.1027  ISa  8.00 -> 10.88  adopt
  cubic  I222    -> I23            R_meas 0.1155 -> 0.1153  ISa 17.34 -> 17.46  adopt
  6iu8   P3(1)   -> 32             R_meas 0.1357 -> 0.1762  ISa  7.03 ->  5.40  keep
  7k1l   P6      -> 622            R_meas 0.1538 -> 0.3062  ISa  8.86 ->  2.91  keep
  9hnc   P2      -> 422            R_meas 0.4982 -> 0.6497  ISa  1.40 ->  1.13  keep
  9hs7   P6(1)   -> 622            R_meas 0.0849 -> 0.3411  ISa 12.02 ->  1.93  keep

6fid gains the deposited space group and the second gains the one XDS reports;
the four that keep their group come out unchanged. The cost is two extra merges,
and only on a run that records a refusal - a few in a hundred on the open corpus.
6fid is unchanged across -N 6/8/32 and between a plain Release build and one
built with -march=x86-64-v3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-14 18:23:00 +02:00
leonarski_fandClaude Opus 5 d00175c8b4 resolution: a cut the shell table refutes is not quoted
One run was written to 1.089 A where I/sigma was zero and R_meas 1776 per cent,
and the summary said only "Merged to 1.09 A". Two things let that through.

The logistic's crossing was taken wherever the fitted curve met the target, even
when that point lay past every bin the curve was fitted through - an extrapolation
of a fall-off the data never showed, quoted as a measurement. The crossing now has
to lie inside the fitted bins, with the one-shell extension still there to spare;
outside them the number is read off the bins themselves instead.

And the shipped guard asked only for the finest shell whose CC1/2 still reached the
target, with no requirement that the curve get there monotonically. A noise shell
that climbs back over the bar therefore became the edge of the data. It is the
climbing back that disqualifies it, and the program already noticed - it printed
"CC1/2 is not monotone with resolution" and then cut there anyway, because the test
was developer-only and decided nothing. It decides now, in the report everyone
reads, and the duplicate is gone so there is one such test rather than two.

The dataset above is written to 1.527 A: CC1/2 0.209 to 0.688, I/sigma 0.40 to
1.26, R_meas 245 to 145 per cent. Forty-four of fifty-one reports are unchanged to
the character; of the seven that move, five are cut coarser and every headline
number of all five improves, one loses a fit that was an extrapolation without
changing anything written, and one gains a quotable fit and loses a warning.

This is a guard, not the cause. On four of those five the reflections doing the
damage are ice, which the resolution fit now leaves out for its own reasons; the
guard still earns its place, because on those four removing the ice alone does not
stop the cut being quoted past what the shells support.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-14 14:17:39 +02:00
leonarski_fandClaude Opus 5 5f1292ca3b resolution: the cut is decided on the crystal's own diffraction, not on the ice
Build Packages / Create release (push) Successful in 18s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 6m52s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 9m4s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m37s
Build Packages / build:viewer-tgz:cuda (push) Successful in 12m6s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 15m3s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 15m20s
Build Packages / build:windows:nocuda (push) Successful in 17m9s
Build Packages / build:windows:cuda (push) Successful in 19m45s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 17m54s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m21s
Build Packages / build:rugnux:windows (push) Successful in 11m3s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 19m46s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 18m49s
Build Packages / Generate python client (push) Successful in 1m4s
Build Packages / Build documentation (push) Successful in 1m29s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m3s
Build Packages / build:rpm (rocky8) (push) Successful in 17m23s
Build Packages / build:rpm (rocky9) (push) Successful in 18m10s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 13m21s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 13m22s
Build Packages / Unit tests (push) Successful in 1h13m48s
A powder ring is reproducible. Past the crystal's own limit the ice is still there
and still the same in both half-sets, so the two halves agree about the ice, and a
Pearson correlation cannot tell that agreement from diffraction. Shells have been
measured at CC1/2 0.765 where <I/sigma> is -0.1 and R_meas is 470 per cent, and at
0.828 where R_meas is 560 - inside the written range, because the logistic was
fitted through them. Against archived references a run whose frames carry ice is
written a mean twelve per cent finer than the reference and one without four, and
every over-claim past twenty per cent has rings.

The same ring drags the curve the other way where the data are good: one shell of
forty falls to CC1/2 0.51 because the two strongest ice lines cross it, and the
shell either side reads 0.99. That is a real defect of those reflections, but it is
narrower than the shell it is reported in, so it defames three quarters of a shell
that is fine.

Both signs are the same cause, and both leave the fit here. Scaling, the error
model and the space-group search already keep ice out of their own fits - what
changes is only that the resolution DECISION now reads the curve those three read.
The reflections are still merged, still written, still counted in the shell table:
the ring contaminates an intensity, it does not make it absent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-14 13:03:06 +02:00
leonarski_fandClaude Opus 5 154f4f8895 scale/merge: a stretch of a sweep whose removal helps the merge is removed
Build Packages / Unit tests (push) Successful in 1h37m59s
Build Packages / build:windows:nocuda (push) Successful in 16m1s
Build Packages / build:windows:cuda (push) Successful in 18m38s
Build Packages / build:viewer-tgz:cpu (push) Successful in 7m16s
Build Packages / build:viewer-tgz:cuda (push) Successful in 8m11s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 6m46s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 6m12s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 14m15s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 15m18s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 16m19s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 15m42s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 17m11s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 17m50s
Build Packages / build:rpm (rocky8) (push) Successful in 15m11s
Build Packages / build:rpm (rocky9) (push) Successful in 16m7s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 13m18s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 13m0s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 23m19s
Build Packages / Generate python client (push) Successful in 38s
Build Packages / Build documentation (push) Successful in 1m5s
Build Packages / Create release (push) Successful in 26s
Build Packages / build:rugnux:windows (push) Successful in 16m1s
The measurement landed report-only because the code could not say which frames it
was talking about. It can now.

A candidate is scanned at the batch width and at that width doubled and doubled
again, so a defect far longer than one batch is judged against a reference that
does not still contain it - on one sweep a 137 degree stretch measured minus
0.0352 at 59 sigma while its worst single batch read minus 0.008 and was refused.
Its edges are then found by moving them a frame at a time with the WHOLE stretch
re-measured at each position, rather than by halving it: the reflection count the
verdict rests on never shrinks as the edge sharpens, which is what used to drive
the harm onto one frame. Nothing narrower than one rocking event is ever removed,
that width taken from the run's own combine, because a single frame is below what
the statistic can resolve.

And delta-CC1/2 may confirm a removal, never propose one. The frames must also
disagree with the merge on their own per-image correlation, computed on the
partials and independent of the statistic being confirmed - otherwise the test
selects on the very quantity it then reports as improved, which flatters any
removal whatsoever. Measured: on a sweep where dropping 450 frames takes R_meas
from 0.28 to 0.21, dropping 450 frames of the same sizes chosen by position makes
every number worse.

Over a hundred and forty-eight datasets: no space group and no cell changes, no
single-frame removals anywhere, and the ledger no longer calls a frame inconsistent
with a merge it agrees with better than average. Eighty-eight are byte-identical.
Where it acts it is worth R_meas 0.82 to 0.23, ISa 9.5 to 14.1, a resolution shell.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-14 09:52:49 +02:00
leonarski_fandClaude Opus 5 926b68c658 indexing: a rotation lattice is accepted on the spots it explains, not the frames
The acceptance test counted frames: a frame indexed if at least six of its spots
and a fifth of them fell on the lattice, and a run was refused if fewer than ten of
sixty sampled frames did. That test comes from serial crystallography, where each
image is its own experiment. A rotation sweep is one crystal and one orientation
matrix, and its frames are not independent of each other - so what the count mostly
measured was how many spots happen to land on a frame. A sweep carrying four spots
an image cannot reach six on three frames in four however right the lattice is, and
one such run found the correct cell and threw it away at three frames of sixty.

Count the spots instead: the fraction of all spots in the sampled frames that the
lattice explains, against what the same lattice explains when each frame's spots are
put at another frame's angle. Same lattice, same spots, same detector, same
refinement - only the claim that these spots were seen at these angles is removed.
That difference is the evidence, and it carries no spots-per-frame number anywhere,
so nothing has to be chosen for a crystal that diffracts weakly.

Measured on the sparse sweeps this was found with: ninety-three per cent of spots
explained against nought point nought by chance, and they merge to 0.58 A with the
deposited cell. Over a hundred and three datasets the chance level never exceeds
2.6 per cent and the smallest true margin is seventeen points; ninety-nine of them
are byte-identical, because only the refusal is decided this way - every rescue and
every arbiter still counts frames.

The floor on --min-indexed-spots goes from six to four. Six was the serial gate's
number; four is where a lattice stops being fitted by any three spots.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-14 00:20:01 +02:00
leonarski_fandClaude Opus 5 5d643498c6 post-refinement: a one per cent bound on a step, not on the answer
The fit was refused outright if it wanted to move the detector distance or a cell
edge by one per cent or more. The bound exists to stop a runaway, but it was
applied to the whole move rather than to a step, so a geometry that was genuinely
several per cent out could never be reached - on one dataset the refused fit was
310.000 -> 305.692 mm with a cell within 0.06 per cent of the deposited one, and it
was thrown away for being too large a correction.

A move of one per cent or more is now re-fitted as a walk of one per cent steps,
each seeded where the last arrived and each required to lower the held-out
split-half residual, and a walk of more than one step is ratified by re-indexing
at where it arrived. That is what the bound was really for: a second lattice does
not index better at the new geometry, and a real distance error does - on the
dataset this was found with, pass-1 frames go 37/60 to 60/60, R_meas 0.74 to
0.081, ISa 0.98 to 15.0, and the walk stops at a minimum bracketed on both sides
without ever being told what the answer should be. A walk that uses every step it
is allowed has not settled and is refused; that test is what stops the runaway
this replaces the veto with.

The first solve is untouched. An earlier version that also widened its box was
measurably not inert - two microns on one control moved a 694-reflection merge by
three points of R_meas - so a move inside the bound still commits exactly as before.

Refusals now say so in the report rather than passing silently. Four of ninety-nine
open datasets carry a uniform cell-scale error; ninety-eight of a hundred and three
are byte-identical or differ only by the new line, and no run on forty-eight
datasets with calibrated headers moves at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-14 00:18:32 +02:00
leonarski_fandClaude Fable 5 33c0124833 viewer: the direct beam reads as x and y rows, like the PONI
Build Packages / Create release (push) Successful in 20s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 8m48s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 10m2s
Build Packages / build:viewer-tgz:cpu (push) Successful in 11m31s
Build Packages / build:viewer-tgz:cuda (push) Successful in 12m17s
Build Packages / build:windows:nocuda (push) Successful in 17m20s
Build Packages / build:windows:cuda (push) Successful in 19m52s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 14m50s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 24m30s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 15m12s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m41s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 14m28s
Build Packages / build:rugnux:windows (push) Successful in 11m4s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 17m26s
Build Packages / Generate python client (push) Successful in 37s
Build Packages / build:rpm (rocky8) (push) Successful in 18m50s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m10s
Build Packages / Build documentation (push) Successful in 1m59s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 18m38s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 18m36s
Build Packages / build:rpm (rocky9) (push) Successful in 19m20s
Build Packages / Unit tests (push) Successful in 1h36m23s
One row holding both coordinates sat oddly under a PONI split over two,
so the direct beam gets the same treatment - and with it a real "From
header" column (the header geometry's own direct beam, the file's beam
centre while the header is untilted) and a Change column that says how
far the beam actually moved, where the combined row could only show
dashes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vRwzF8PLaaa2novAKYorq
2026-09-13 23:08:48 +02:00
leonarski_fandClaude Fable 5 03883924d9 viewer: the analysis window carries the rugnux report
The merge-analysis window gets a fourth view beside the plot, the table
and the ISa diagnostic: the rugnux result report, the same text the
command line writes as <prefix>_report.txt, in a fixed-width read-only
page. The viewer's in-process run never wrote that file, so the
controller renders it from the run's own results right after the
pipeline finishes and hands it along with the finished signal; as with
WriteResultReport, a report failure leaves the report empty rather than
losing the run. The view buttons now map to page widgets instead of
stack indices, so an absent page (no ISa scatter, no report) cannot
shift the pages behind it.

The calibration result window already opens itself when a calibration
job finishes (rc-161); verified against a LaB6 dataset, unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vRwzF8PLaaa2novAKYorq
2026-09-13 23:06:12 +02:00
leonarski_fandClaude Fable 5 52d37bd673 viewer: Spots + background is two stacked plots, and the angle axis carries its unit
Qt Charts caps each axis' layout area at a fraction of the chart size,
so in a dock this short a horizontal axis title is elided to nothing at
any window size - which is also why the resolution plot's vertical
"d (A)" was the only axis title that ever drew. The angle axis now
carries the degree sign on every tick instead of a title that cannot
render, and the lone "d (A)" goes: its labels each carry the unit
already, and no other plot titles its y axis.

"Spots + background" becomes two half-height plots stacked in the
same slot - the spot count above, the background below - each named by
a short bold label in its series' colour, its y axis reduced to the two
range ends, the lower plot carrying the x axis for both. Clicking
either jumps to the image, and Reset zoom resets both. This replaces
the twin-axis single chart, whose machinery (the secondary series and
its second y axis) is removed: two scales in one plot made the reader
match colours across the frame to know which curve reads on which axis.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vRwzF8PLaaa2novAKYorq
2026-09-13 21:50:13 +02:00
leonarski_fandClaude Fable 5 941c6e45cc viewer: ring labels in plain type at one screen size, and a cell that holds still
Build Packages / Create release (push) Successful in 59s
Build Packages / build:viewer-tgz:cpu (push) Successful in 11m44s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 9m55s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m51s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 5m3s
Build Packages / build:windows:nocuda (push) Successful in 16m58s
Build Packages / build:windows:cuda (push) Successful in 19m23s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 23m30s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 15m12s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 14m46s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 15m2s
Build Packages / build:rugnux:windows (push) Successful in 10m52s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m16s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 14m53s
Build Packages / Generate python client (push) Successful in 33s
Build Packages / Build documentation (push) Successful in 1m18s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 16m27s
Build Packages / build:rpm (rocky8) (push) Successful in 16m46s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 16m47s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 15m58s
Build Packages / build:rpm (rocky9) (push) Successful in 17m35s
Build Packages / Unit tests (push) Successful in 1h14m23s
The cased white label read as an outline artefact, and its scene-space
font - scaled by 1/zoom to hold an apparent size - broke down at high
magnification, where a sub-point font rasterises into a white block.
The label is now drawn in device coordinates (ItemIgnoresTransformations)
at 1.6x the application font, bold, with no outline, so it is the same
size at every zoom by construction. The colour is picked from what ends
up under it: near-black normally - the label usually sits outside the
detector image, on the canvas that is white under every colour map -
and white only over the image of a dark map. Not the ring's magenta:
where the label meets its own dashes the two blend and the glyphs
dissolve.

The unit-cell rows pad every number to three integer digits with figure
spaces, so 95.0 and 102.3 occupy the same width and the columns do not
shift as values change magnitude; no axis reaches 1000 A and no angle
1000 degrees.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vRwzF8PLaaa2novAKYorq
2026-09-13 21:03:14 +02:00
leonarski_fandClaude Fable 5 53d6aa4ddf viewer: ring labels that stay in view, and an inspector that reads at a glance
Build Packages / Create release (push) Successful in 17s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 7m35s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 8m55s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m40s
Build Packages / build:viewer-tgz:cuda (push) Successful in 12m1s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 15m35s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m9s
Build Packages / build:windows:nocuda (push) Successful in 17m10s
Build Packages / build:windows:cuda (push) Successful in 19m39s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 14m39s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m5s
Build Packages / build:rugnux:windows (push) Successful in 10m49s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 19m3s
Build Packages / Generate python client (push) Successful in 44s
Build Packages / Build documentation (push) Successful in 1m15s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 19m33s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m18s
Build Packages / build:rpm (rocky8) (push) Successful in 17m17s
Build Packages / build:rpm (rocky9) (push) Successful in 18m28s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 21m45s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 19m38s
Build Packages / Unit tests (push) Successful in 1h38m36s
The resolution-ring label was placed at one of four fixed azimuths with
its corner on the ring line, in a hardcoded 16 pt Arial, in the ring's
own magenta: it clipped at the viewport edge, ignored the font size,
and vanished into busy patterns. It now walks the ring's cached trace,
centres on the first point where the whole text fits in the viewport
(failing that, clamps the nearest visible point inside), keeps a
constant on-screen size of 1.2x the application font, and is cased like
the spot markers so it reads on any colour map. In the same function
visibleRect now maps viewport()->rect(), which is what mapToScene
expects.

The dataset plot's angle axis snaps its ticks to round angles -
multiples of 90 degrees when the sweep is wide enough - instead of
whatever angle the evenly-spaced image ticks landed on. The inspector
prints the indexed cell as two aligned rows (alpha below a, beta below
b, gamma below c), mosaicity with two decimals, and "No lattice" in
grey, keeping red for actual trouble. The armed "Analyze image"
button stays navy and is marked by a coral outline instead of turning
into a differently-coloured button. The dataset navigation keys leave
the image's keyPressEvent - the window-wide filter of the previous
commit sees them first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vRwzF8PLaaa2novAKYorq
2026-09-13 20:20:54 +02:00
leonarski_fandClaude Fable 5 702d8f2a1e viewer: the background is a control, and shortcuts work from every panel
The display mapping always had a black point; it was just pinned to
zero with no way to move it. It gets the second slider in the display
toolbar (0 = off, as before; both sliders are width-capped so the strip
stays tight), a logarithmic scale that starts at a true zero, and the
wheel with B held, mirroring F for the foreground. The white point now
floors at the black point plus one.

F, B, A and Home/End/PageUp/PageDown used to reach their targets only
with the image focused, because they lived in keyPressEvent. They move
into an application-wide event filter that steps aside when the focused
widget genuinely uses the key: text entry gets everything, the file
tree keeps its navigation keys, and open menus are untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vRwzF8PLaaa2novAKYorq
2026-09-13 20:20:42 +02:00
leonarski_fandClaude Fable 5 7801f309cc viewer: the file panel is called Files, not File manager
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vRwzF8PLaaa2novAKYorq
2026-09-13 19:29:36 +02:00
leonarski_fandClaude Fable 5 89272850fb viewer: the splitter is a cased centre grip, not a full coral line
The 10 px coral band gave the resize handles the 3:1 contrast that made
them findable, but three of them frame the image panel and dominated the
view. The separator keeps its 10 px grab area and is painted in the
window colour; what marks it is a short dark-amber pill (4.4:1 on
salmon) at its midpoint, cased in a near-white rim like the spot markers
so the amber does not bleed into the salmon, and taking the brand coral
under the cursor. Drawn by a QProxyStyle over Fusion, since a stylesheet
can only fill the whole band.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015vRwzF8PLaaa2novAKYorq
2026-09-13 19:27:39 +02:00
leonarski_fandClaude Opus 5 2c1a459195 build: zlib and Eigen come from the build, not from the host
Build Packages / Create release (push) Successful in 16s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 8m15s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 8m18s
Build Packages / build:viewer-tgz:cpu (push) Successful in 9m16s
Build Packages / build:viewer-tgz:cuda (push) Successful in 10m48s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 12m48s
Build Packages / build:windows:nocuda (push) Successful in 17m20s
Build Packages / build:windows:cuda (push) Successful in 19m43s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 22m47s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 16m6s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 17m17s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 18m25s
Build Packages / Generate python client (push) Successful in 37s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 16m37s
Build Packages / Build documentation (push) Successful in 46s
Build Packages / build:rugnux:windows (push) Successful in 10m50s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 16m8s
Build Packages / build:rpm (rocky8) (push) Successful in 15m39s
Build Packages / build:rpm (rocky9) (push) Successful in 16m1s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 14m2s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 14m22s
Build Packages / Unit tests (push) Successful in 1h13m32s
They were the last two dependencies a machine had to supply itself. Everything
else - spdlog, zstd, HDF5, Catch2, libzmq, libtiff, FFTW, Ceres, the indexer,
libcurl, libjpeg-turbo - the build fetches, so a checkout and a compiler were
almost enough and then were not. Now they are: a plain Release configure needs
neither zlib-devel nor eigen3-devel.

Eigen is fetched as headers and never added as a subdirectory; a two-file config
package is written into the build tree and Eigen3_DIR points at it, so Ceres and
the indexer resolve their own find_package(Eigen3) through the ordinary config
path. This is what makes it safe: OVERRIDE_FIND_PACKAGE remains banned, for the
reason the note in CMakeLists has always given - it segfaults the CMake that ships
with Visual Studio, one configure in three and every configure with Ceres CUDA on -
and the shim never enters that code path at all, which the empty pkgRedirects
directory in a configured build shows. The version file uses CMake's own
AnyNewerVersion rather than Eigen's same-major rule, which is what declined Ceres'
cross-major range and cost us a mixed-Eigen hunt in August.

zlib is zlib-ng in compat mode, built during the configure into a prefix that
FindZLIB is pointed at ahead of the system one. Built, not added as a
subdirectory: libcurl puts ZLIB::ZLIB in CMAKE_REQUIRED_LIBRARIES and try_compile
against an alias of an in-tree target is a hard error, so the viewer build breaks.
A real archive has no such problem. It is forced position-independent, which the
distro archive is and a default zlib-ng build is not - the xds plugin does not
link otherwise.

An existing installation still wins when it is asked for: -DZLIB_ROOT= and
-DEigen3_DIR= configure as before and skip the fetches. About eighteen seconds on
a first configure, nothing on a rebuild, and nothing added to the build itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-13 17:50:59 +02:00
leonarski_fandClaude Opus 5 dc0d7e65d8 space group: the b-veto goes
It refused a higher point group when the added operator cost too much extra error
in the scaling model, relative to the parent's. It was guarded by "H could not be
computed" and followed by the H refusal, so it could only ever act where H - the
one statistic that discriminates here - is blind. Over 1213 candidate rows on 148
datasets it is reached seven times, every one of them with H not a number, fires
twice on a single dataset, and changes no result anywhere.

What it does do is make a space group depend on the compiler flags. On that one
dataset a plain Release build refuses a correct P2_12_12_1 and reports P2, while
the same source built for x86-64-v3 does not, because the quantity it compares is
not a measurement at all - it is the bisection's own ceiling, over a parent whose
error term is larger than the signal it is added to. A decision that moves with
the flags is worse than one that is consistently wrong, and no bound repairs this
one: genuine promotions now measure up to 5.8 where the bound was calibrated at
1.62 against a twin at 2.1, so the two populations have swapped sides.

The case it was meant to catch - a merohedral twin too sparse for H - is already
answered below it by the added-operator R gate, whose reference is the global best
rather than the parent, so it still speaks where H does not.

No space group and no cell changes on 148 datasets: 100 open, 34 in-house, 14
private.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-13 16:13:48 +02:00
leonarski_fandClaude Opus 5 beae384bb7 scale/merge: what each stretch of a sweep costs the merged data, measured
Build Packages / Create release (push) Successful in 32s
Build Packages / build:viewer-tgz:cpu (push) Successful in 7m40s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 6m54s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 7m7s
Build Packages / build:viewer-tgz:cuda (push) Successful in 8m29s
Build Packages / build:windows:nocuda (push) Successful in 16m47s
Build Packages / build:windows:cuda (push) Successful in 19m18s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 21m1s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 11m53s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 14m3s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 12m48s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 14m34s
Build Packages / build:rugnux:windows (push) Successful in 10m19s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 15m38s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 16m35s
Build Packages / Generate python client (push) Successful in 47s
Build Packages / Build documentation (push) Successful in 1m19s
Build Packages / build:rpm (rocky8) (push) Successful in 15m59s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 15m34s
Build Packages / build:rpm (rocky9) (push) Successful in 16m26s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 9m48s
Build Packages / Unit tests (push) Successful in 1h34m17s
The sweep-quality diagnostic says a range is weak; it cannot say whether that
weakness costs anything. A 60 degree stretch of poor diffraction and a radiation
damage tail look alike in scale and correlation, and on one dataset they measure
-0.0758 +- 0.0005 and -0.0019: one is 150 sigma of real harm, the other is
nothing once the corrections have done their work.

So measure it, per batch of the sweep, as the change in CC1/2 when that batch is
left out - Assmann, Brehm & Diederichs (2016) Acta Cryst. D72, 1071-1081. In the
sigma-tau form, so no half-set is drawn and nothing depends on a seed; against the
observed scatter of each reflection's own observations rather than the error
model's sigmas, which would invert the sign and make a bad batch read as added
precision; and on the 10 degree batch the damage monitor already uses, because a
single frame is far below what the statistic can resolve - the precision goes as
one over the root of the reflection count, and 0.1 degrees of rotation does not
have the reflections.

Report only. Nothing is dropped, no observation is weighted differently, and every
existing key of every dataset tested is unchanged to the digit. The disposition
column says merged for all of them; acting on it needs a frame attribution this
code does not yet have, since a full is currently labelled with its peak frame
rather than with the frames its partials came from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-13 16:10:44 +02:00
leonarski_fandClaude Opus 5 c367e88261 viewer: spot markers are cased in black, so they can be seen on any colour map
Build Packages / Create release (push) Successful in 22s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 20m57s
Build Packages / build:windows:nocuda (push) Successful in 15m48s
Build Packages / build:windows:cuda (push) Successful in 20m58s
Build Packages / build:viewer-tgz:cpu (push) Successful in 8m30s
Build Packages / build:rugnux:windows (push) Successful in 15m40s
Build Packages / build:viewer-tgz:cuda (push) Successful in 9m40s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 6m23s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 5m44s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 10m51s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 13m15s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 12m18s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 14m41s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 14m35s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 14m59s
Build Packages / Generate python client (push) Successful in 30s
Build Packages / Build documentation (push) Successful in 1m18s
Build Packages / build:rpm (rocky8) (push) Successful in 15m39s
Build Packages / build:rpm (rocky9) (push) Successful in 16m24s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 14m52s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 15m15s
Build Packages / Unit tests (push) Successful in 2h9m29s
A green marker on the default colour map measured 1.37:1 against its background
and a cyan one 1.25:1 - invisible to anyone, not only to a viewer who is colour
blind. Two markers were also indistinguishable from what they sat on: a
second-lattice marker on the coral beam stop at 1.10:1, and a feature box on a
magenta bad pixel at 1.00:1, which is to say a pixel row through it contains one
colour and the marker is simply not there.

Draw each one twice, a black pen a little wider underneath. No hue moves: magenta
stays the strong colour no colour map contains, cyan stays the colour of ice.
Against the light maps every marker reaches 21:1 and the two collisions become
8.40:1 and 6.70:1; against a dark map the bright core already carried it and
nothing changes, so one mechanism serves both without asking which map is loaded.

Spots only. The predicted-reflection markers have the same complaint with a
different cause - dark red on a black-low map - which a black casing cannot mend,
and casing them cost 55% of the overlay rebuild for no benefit: 16.0 to 34.3 ms
with them, 16.0 to 19.1 ms without. Both overlays are off by default and a
frame's spots are counted in hundreds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-13 15:08:04 +02:00
leonarski_fandClaude Opus 5 7e186d6d51 rugnux: the beam-centre check compares against the direct beam, not the PONI
Build Packages / Create release (push) Successful in 31s
Build Packages / build:viewer-tgz:cpu (push) Successful in 9m40s
Build Packages / build:viewer-tgz:cuda (push) Successful in 10m14s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 5m45s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 5m8s
Build Packages / build:windows:nocuda (push) Successful in 16m33s
Build Packages / build:windows:cuda (push) Successful in 18m57s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 20m41s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 9m24s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 10m6s
Build Packages / build:rugnux:windows (push) Successful in 11m13s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 13m12s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 12m38s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 14m9s
Build Packages / build:rpm (rocky8) (push) Successful in 14m25s
Build Packages / Generate python client (push) Successful in 15s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 15m22s
Build Packages / Build documentation (push) Successful in 1m17s
Build Packages / build:rpm (rocky9) (push) Successful in 15m26s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 16m6s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 14m1s
Build Packages / Unit tests (push) Successful in 2h5m11s
Both the FFT capture and the background walk measure where the beam lands - the
centrosymmetry of the scattered background is about that point. The geometry's
beam_x_pxl is the PONI, which parts from it by distance*tan(rot)/pixel the moment
the detector is tilted. Comparing the two reported a disagreement the data do not
have: 3.3 px at 120 mm on the in-house files, whose header carries a 0.234 degree
tilt, against a stated need of 0.7 px.

Measured on a calibrant at 110/150/200/300 mm, the capture sits 0.5-1.5 px from
the fitted direct beam and 5.0 to 17.9 px from the fitted PONI. With the tilt at
zero the two are the same point and the line is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-13 15:02:12 +02:00
leonarski_fandClaude Opus 5 e3da313c84 reader: a goniometer whose inner circle is inclined says so in its axis table
A fixed-chi stage carries phi at a standing angle to the base circle, and states
that angle in the CBF _axis table and nowhere else - its "# Chi" line reads
0.0000, because no chi circle is driven. ParseAxisTable kept only rotation axes
hanging off nothing, so the inclination was discarded and RotationAxis() fell
back to that zero: a phi scan was run about the base axis, tens of degrees from
the one the crystal actually turned about.

Measured on a magic-angle head 54.74 degrees off omega: the axis goes from
1 0 0 to 0.577382 -0.707087 -0.408237, against dxtbx's 0.5774 -0.7071 -0.4083,
and the first pass from a P-centred triclinic 27.2 58.1 126.2 that indexes
nothing to P-orthorhombic 5.16 14.05 14.81, which is the cell DIALS and XDS both
reach on these frames. The mirrored inclination returns to nothing, so it is that
angle and not any perturbation of the axis.

Every other header in the corpus states one rotation axis, which is every
Eulerian head, and is untouched. The two that share this table were written three
days later by a template that puts the same angle on the "# Chi" line, agreeing
to 0.03 degrees - they read identically either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-13 15:02:04 +02:00
leonarski_fandClaude Opus 5 b243fa21d3 docs: a Linux jfjoch_viewer build needs OpenSSL 3 and the krb5 headers
Build Packages / Create release (push) Successful in 25s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 7m9s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 7m59s
Build Packages / build:viewer-tgz:cpu (push) Successful in 8m23s
Build Packages / build:viewer-tgz:cuda (push) Successful in 9m50s
Build Packages / build:windows:nocuda (push) Successful in 16m46s
Build Packages / build:windows:cuda (push) Successful in 19m12s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 20m6s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 14m4s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 13m38s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 13m4s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 14m58s
Build Packages / build:rugnux:windows (push) Successful in 10m20s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 17m24s
Build Packages / Generate python client (push) Successful in 44s
Build Packages / Build documentation (push) Successful in 1m46s
Build Packages / build:rpm (rocky8) (push) Successful in 20m2s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 18m27s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 18m37s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 20m57s
Build Packages / build:rpm (rocky9) (push) Successful in 20m16s
Build Packages / Unit tests (push) Successful in 2h7m25s
The fetched libcurl 8.22 refuses to compile against OpenSSL 1.1 ("OpenSSL 3.0.0
or greater required"), so a viewer build on an older distro fails deep inside a
dependency with nothing in the requirements list to explain it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-13 14:34:12 +02:00
leonarski_fandClaude Opus 5 94c5437885 writer: the temporary file name is defined when the arm date is missing
tmp_suffix was left uninitialised and only assigned inside the `if
(!arm_date.empty())` body or in the catch handler. An empty arm_date takes
neither path, so the `.tmp` file name was formatted from an indeterminate value.
Initialise it to the same wall-clock fallback the catch handler already uses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-13 14:34:12 +02:00
leonarski_fandClaude Opus 5 7dc749fb31 tests: the synthetic merged reflections are built without narrowing conversions
MergedReflection's I, sigma and d are floats; the designated initialisers fed
them doubles, which is a narrowing conversion in a braced initialiser and five
of the nine default-visible warnings the project emitted. The casts are what the
compiler was doing anyway - the values are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-13 14:34:12 +02:00
leonarski_fandClaude Opus 5 167af290c2 build: Release CUDA objects carry -DNDEBUG, like every other Release object
CMAKE_CUDA_FLAGS_RELEASE replaced CMake's default (`-O3 -DNDEBUG`) instead of
adding to it, so `.cu` translation units were the only ones in a Release build
compiled with assertions live. No project `.cu` uses a runtime `assert` today -
the one assertion in ImageSpotFinderGPU.cu is a `static_assert`, unaffected -
but the headers a `.cu` pulls in do, and Eigen's `eigen_assert` in particular
was being expanded differently in `.cu` and `.cpp` translation units of the same
build.

Costs a full `.cu` rebuild once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-13 14:34:12 +02:00
leonarski_fandClaude Opus 5 b3c9987ed7 build: the vendored HLS headers no longer warn, and three stale CMake rules go
Four unrelated but equally small build-tree fixes, none of which changes what
any binary does:

* fpga/hls: `fpga/include/` holds the vendored Xilinx `ap_int`/`ap_fixed`
  headers. Included with a plain `-I` they emit 790 default-visible
  `-Wdeprecated-enum-enum-conversion` warnings across the targets that consume
  them - every warning a developer sees in a build of the receiver, the broker,
  the tests or the HLS simulation comes from that one header. `SYSTEM`
  propagates as `-isystem`, silencing the vendored header while leaving warnings
  in our own HLS sources visible. Measured on one HLS source: 78 -> 0 at default
  flags, 289 -> 56 under -Wall.

* compression: `zstd/lib` was the old in-tree vendored location, gone from the
  tree since zstd moved to FetchContent. It stays harmless only while nothing
  sits there; a checkout with a leftover `compression/zstd/` puts that copy's
  internal headers (`common/huf.h`, `common/mem.h`) ahead of the fetched zstd
  the library actually links. The fetched zstd already exports its `lib/` root,
  so dropping the entry is all that is needed.

* xds-plugin: `target_link_options(... --exclude-libs,ALL)` was applied twice,
  once unconditionally and once guarded by `if(UNIX AND NOT APPLE)`. Keep the
  guarded one.

* xds-plugin's install rule uses `CMAKE_INSTALL_LIBDIR`, which was only ever set
  as a side effect of a fetched dependency including GNUInstallDirs first.
  Include it at the top level, where it belongs.

* broker: `redoc-static.html` was installed to `<prefix>/jfjoch/frontend/`, a
  path nothing serves - the broker mounts its frontend from
  `share/jfjoch/frontend`, whose copy of the API reference is `dist/openapi.html`
  written by `npm run redocly`, while `redoc-static.html` itself reaches the
  bundle through Sphinx's `html_static_path`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-13 14:34:12 +02:00
leonarski_fandClaude Opus 5 5546c165d7 viewer: text the eye can follow, and a font size the user can set
Build Packages / Create release (push) Successful in 23s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 7m30s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 7m41s
Build Packages / build:viewer-tgz:cpu (push) Successful in 8m9s
Build Packages / build:viewer-tgz:cuda (push) Successful in 9m19s
Build Packages / build:windows:nocuda (push) Successful in 16m37s
Build Packages / build:windows:cuda (push) Successful in 19m3s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 21m50s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 17m19s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 16m7s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 17m53s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 17m51s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 16m21s
Build Packages / Generate python client (push) Successful in 31s
Build Packages / build:rugnux:windows (push) Successful in 10m4s
Build Packages / Build documentation (push) Successful in 47s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 14m44s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 16m11s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 16m22s
Build Packages / build:rpm (rocky8) (push) Successful in 16m33s
Build Packages / build:rpm (rocky9) (push) Successful in 17m12s
Build Packages / Unit tests (push) Successful in 1h54m27s
Written for the person this program is mostly used by: someone reading a
diffraction pattern on a 4K monitor with eyesight that is no longer 25.

The rotation-angle axis of the dataset plot showed four ellipses and nothing
else. Qt Charts has truncated axis labels by default since 6.2, so the plot has
been unreadable along that axis for as long as we have shipped it; turning
truncation off then exposed the last label clipped by the widget edge, hence the
right margin that now follows the font instead of a hard 8 px.

The splitters were 10 px of the window's own salmon - a 1.00:1 contrast ratio,
which is to say invisible. They are now a darker coral band, 3.31:1, past the 3:1
that WCAG asks of a control's boundary, with the brand coral as the hover state.
The brand colour itself is 2.38:1 on salmon and cannot carry the idle band.

View > Font size, Ctrl+plus and Ctrl+minus, 100/125/150 %. It multiplies whatever
the desktop already set rather than replacing it, so a session already scaled by
GNOME steps from there and steps back exactly. Qt does not deliver a runtime
application font to widgets that already exist, so the change is broadcast to
them; per-widget attributes survive it.

The Inspector is the panel people read all day, and its value column was capped at
a flat 600 px, which clipped as soon as the text grew. The bounds are character
widths now, chosen to be exactly 600 at the default font and to grow with it. The
dataset plot's curves are a fifth of the font height rather than 2 px, for the
same reason: a curve is read at the distance the labels beside it are.

Also the four QChart::axisX/axisY calls deprecated since 6.0, replaced by what
they do internally, and the two button rows the larger fonts clipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-13 13:54:56 +02:00
leonarski_f b673a05176 space group: the modulation's control is drawn from data with the modulation removed
Build Packages / Create release (push) Successful in 21s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 7m17s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 8m19s
Build Packages / build:viewer-tgz:cpu (push) Successful in 9m39s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m13s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 12m16s
Build Packages / build:windows:nocuda (push) Successful in 16m42s
Build Packages / build:windows:cuda (push) Successful in 19m4s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 21m36s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 17m3s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 18m14s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 16m16s
Build Packages / build:rugnux:windows (push) Successful in 10m29s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 16m5s
Build Packages / Generate python client (push) Successful in 17s
Build Packages / Build documentation (push) Successful in 1m25s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 16m41s
Build Packages / build:rpm (rocky8) (push) Successful in 14m1s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 14m7s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 13m54s
Build Packages / build:rpm (rocky9) (push) Successful in 15m5s
Build Packages / Unit tests (push) Successful in 1h52m20s
The pseudo-translation test has two halves: a Patterson peak, judged against a
permutation null, and an intensity modulation along that peak's vector, judged
against a control. The control was the same greedy search run from random starting
vectors over the SAME intensities - so it measured the search AND whatever
modulation the crystal carries, because a greedy search started anywhere walks
into a real modulation's basin. Measured over 110 merged datasets, single draws of
that control reached 199x and 6162x on crystals whose own modulation reads 4.0x and
67x, and on 28 of the 110 the control came out at or above the signal it is
subtracted from.

Shuffle the intensities inside each resolution shell instead - the permutation the
Patterson null in the same function already uses - and start the search where the
measurement starts. The reflection count, the E^2 distribution and the phase-bin
populations are untouched; the only thing removed is the correlation between a
reflection's intensity and h.u, which is the thing the modulation claims to see.

No threshold moved and no verdict changed: over those 110 datasets the same 8 are
called, none gained, none lost. What goes away is a control that reads its own
signal, and with it the one route by which something upstream can move the control
while the modulation stands still.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
(cherry picked from commit bae480b9202b403629fcad5dc3408aec4998bd6a)
2026-09-13 12:43:58 +02:00
leonarski_fandClaude Opus 5 54968a245f viewer: the file manager starts where the data is, and follows a dataset opened elsewhere
Build Packages / Create release (push) Successful in 18s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 7m41s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 8m36s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m51s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m28s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 15m37s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 15m40s
Build Packages / build:windows:nocuda (push) Successful in 16m50s
Build Packages / build:windows:cuda (push) Successful in 19m6s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 23m20s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 15m57s
Build Packages / build:rugnux:windows (push) Successful in 10m23s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 19m21s
Build Packages / Generate python client (push) Successful in 48s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 19m11s
Build Packages / Build documentation (push) Successful in 1m23s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m36s
Build Packages / build:rpm (rocky8) (push) Successful in 17m45s
Build Packages / build:rpm (rocky9) (push) Successful in 19m12s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 14m20s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 15m11s
Build Packages / Unit tests (push) Successful in 1h42m33s
Two things a beamline user did every session by hand. The browser's default root
now resolves JUNGFRAUJOCH_DATA_ROOT, then the root last used, then - for an SLS
account, which is eNNNNN against a pNNNNN group - /sls/mx/data/pNNNNN/raw, then
home. The environment variable stays the general mechanism and keeps precedence;
the account rule is there because it needs nothing rolled out to benefit from it.
Both go through DefaultRoot(), so a reset resolves them the same way a first run
does.

A dataset opened from the File menu or over D-Bus now expands and selects itself
in the browser when it lies inside the current root. Containment is tested on
resolved paths, so a sibling directory sharing a prefix is not inside it.

Neither says anything when it does not apply: an account that is not a beamline
one, a directory that is not there, a dataset outside the root - each is a bare
return, because for everyone who is not at this one facility the condition should
be invisible. Following never writes the root, so the location the user chose is
still the location they get back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-13 12:21:59 +02:00
leonarski_f f4a838afb9 merge viewer-x11-perf: composite dataset plot, per-kind plot choices, spot-count band
Build Packages / Create release (push) Successful in 29s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 7m44s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 9m4s
Build Packages / build:viewer-tgz:cpu (push) Successful in 9m32s
Build Packages / build:viewer-tgz:cuda (push) Successful in 10m52s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 12m37s
Build Packages / build:windows:nocuda (push) Successful in 16m29s
Build Packages / build:windows:cuda (push) Successful in 19m2s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 21m46s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 16m7s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 17m55s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 16m38s
Build Packages / build:rugnux:windows (push) Successful in 10m30s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 16m18s
Build Packages / Generate python client (push) Successful in 14s
Build Packages / Build documentation (push) Successful in 1m29s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 16m13s
Build Packages / build:rpm (rocky8) (push) Successful in 14m23s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 13m39s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 13m43s
Build Packages / build:rpm (rocky9) (push) Successful in 14m42s
Build Packages / Unit tests (push) Successful in 1h12m43s
2026-09-13 10:30:10 +02:00
leonarski_fandClaude Fable 5 8c4d991447 viewer: the spot count takes the lower band of the composite plot
Swapped which curve occupies which half of the spots + background plot: the
spot count now sits in the lower band with its axis floor at its natural
minimum (zero), and the background in the upper band with its axis floor
pushed down instead. A spot count below zero is nonsense and its axis never
shows one now; a background below zero is at least conceivable on an
integrating detector, so that is the axis that carries the inflated range.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WbjnQhutnboy4qbxwmgDAX
2026-09-13 10:24:36 +02:00
leonarski_fandClaude Fable 5 7d872fd762 viewer: plot choices persist per dataset kind, and settings reset live
The dataset-info plot remembers the selected quantity and the Grid/Per-image
toggle states across restarts, with grid scans and standard scans each
keeping their own favourite (QSettings groups datasetInfoPlot/gridScan and
/standard). Choices are stored as the combo item's data code, not its index,
so a saved choice survives the plot list changing and a code no longer
offered falls back to the first item. Favourites are saved on every explicit
user action and applied whenever a dataset of the other kind loads; loads of
the same kind keep the in-session state, and the live-source default still
wins until the user picks. The unconditional Grid auto-check on grid scans
moved into the favourite's default, so unchecking Grid on a grid dataset now
sticks.

View > "Reset all settings to defaults" clears the whole settings store after
confirmation and brings the running session to a first-start state in place:
default layout, window size, display settings (Indigo, Auto off, HDR off),
plot choices, and the file manager root. Because the live state then equals
the defaults, saving on close stays enabled and later changes persist as
usual.

File > Quit now closes the main window instead of calling
QApplication::quit(), so closeEvent runs and the persistent settings are
saved on that path too - previously they were saved only when the window was
closed by the window manager.

The file manager root persistence was audited and left unchanged: saved on
every successful change, restored behind JUNGFRAUJOCH_DATA_ROOT, and a
vanished directory falls back to the home directory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WbjnQhutnboy4qbxwmgDAX
2026-09-13 10:16:55 +02:00
leonarski_fandClaude Fable 5 337ea76fda viewer: the composite plot reads on two coloured axes, and fluorescence goes
The "Spots + background" dataset plot now scales each quantity on its own
axis - background on the left, spot count on the right - with each axis's
labels and line coloured like its curve, so which axis reads which quantity
is visible at a glance. Each axis is ranged from its own series alone, so
live growth of one quantity never rescales the other; on top of that the
background's range is inflated upward and the spot count's downward, each by
its own span, so the two curves occupy separate halves of the plot instead of
crossing. The spot count stays the primary series (current-image marker,
hover, click-to-load, overlays). Trade-off accepted: the spots axis shows
ticks below zero in its empty lower band.

The per-image plot selector no longer offers the X-ray fluorescence spectrum;
the reader-side fluorescence plumbing is untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WbjnQhutnboy4qbxwmgDAX
2026-09-13 09:33:38 +02:00
leonarski_f 51f2b306a2 two-pass: pass-1's cell is scaled to the distance it is integrated at
Build Packages / Create release (push) Successful in 15s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 7m49s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 9m8s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m23s
Build Packages / build:viewer-tgz:cuda (push) Successful in 12m16s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 15m37s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m14s
Build Packages / build:windows:nocuda (push) Successful in 16m45s
Build Packages / build:windows:cuda (push) Successful in 19m15s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 16m20s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m33s
Build Packages / build:rugnux:windows (push) Successful in 10m12s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 18m35s
Build Packages / Generate python client (push) Successful in 57s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 18m42s
Build Packages / Build documentation (push) Successful in 1m47s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m18s
Build Packages / build:rpm (rocky8) (push) Successful in 16m30s
Build Packages / build:rpm (rocky9) (push) Successful in 16m54s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 13m31s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 14m34s
Build Packages / Unit tests (push) Successful in 1h37m31s
When the refined-geometry re-index indexes too few frames, the run falls back to
pass 1's lattice and integrates it at the post-refined geometry. That lattice was
fitted at pass 1's detector distance. A real-space cell is measured against the
distance the spots were seen at, so carrying it across a distance change scales
the whole cell by the ratio of the two - the code re-scored it at the new
geometry but never re-fitted it.

Measured on a 545 A axis where post-refinement moved the distance 225 -> 226 mm:
the shipped cell came out 0.4 per cent small, exactly 225/226, against a
deposited value the run's own post-refinement had already matched to 0.02 per
cent. The same build given the refined distance to start with shipped the right
cell, which is what identified the stale pairing. Two datasets in a hundred reach
this fallback; on the other the distance moves 0.08 per cent, so nobody saw it.

The fallback now scales the cell to the distance it will be used at. The
orientation is untouched.
2026-09-13 09:31:54 +02:00
leonarski_f a5ba1f8e95 docs: what a rugnux run costs in host and device memory
Measured on rc.169 over rotation sweeps from 2.5 to 18 megapixels: both peaks
fall in scaling and merging, and both track the number of integrated
observations rather than the detector size - two sweeps on the same detector
differ five-fold. Device memory is linear in -N at about 8 bytes per detector
pixel per worker over a fixed floor, so the 16 GB card that fits four light
runs fits one heavy one. --no-merge and -N lower the peak; --no-export-unmerged
was measured and does not - it saves disk, not RAM.

Also records what happens when the card is too small: device decode, the
beam-stop projection and an indexer thread degrade to the host, everything else
fails the run with a non-zero exit rather than returning a short dataset.
2026-09-13 07:37:14 +02:00
leonarski_f 0f11897615 beam centre: the combination and the search follow the transforms onto the device
With the transforms on cuFFT the capture's cost was no longer the transforms: on a 16 Mpixel
detector they are 26 ms, while bringing the four convolution surfaces back is 0.9 s, the
masked Pearson over them is 0.4 s and the greedy search is 1.4 s. All three now run where the
surfaces already are, and only the shortlist crosses PCIe.

The engine interface gains one virtual, PointShortlist, whose default is exactly what the
code did before - PointSurfaces, then BeamCenterPointScore, then BeamCenterShortlist2D on the
host - so the CPU path is unchanged and an engine that has nothing to gain overrides nothing.
Those two functions stop being file-local and become the documented reference the device
kernels are held to.

Memory: three 2h x 2w surfaces, not four. The last inverse's output is read where it lies in
the transform buffer, which nothing overwrites afterwards. Peak on a 16 Mpixel detector goes
from 1.68 GB to 2.55 GB, and DeviceMemoryNeeded counts the surfaces so the fit check and the
CPU fallback still cover it.

The parity test is widened to compensate for what the two paths no longer share. It now
compares the four convolutions, the scored surface (the device kernel against
BeamCenterPointScore, which is what PointScoreSurface exists for), the shortlist (the device
search against BeamCenterShortlist2D on one surface, exactly - the device path is
deterministic), and the whole score. Where the surfaces are compared the tolerance is stated
against the shortlist's reach: r is a ratio of two cancellations, so a weak centre carries
1e-4 of the transform's 1e-6 whichever path computed it, and at a peak - the only part of the
surface anything reads - the two agree to 7.5e-6.
2026-09-13 07:37:05 +02:00
leonarski_f 6326c8b301 beam centre: the capture's transforms run on the GPU, and the CPU stays the fallback
BeamCenterFFTScore is split the way the FFT indexer is: an engine interface with a cuFFT
implementation and an fftw3f one, chosen by whether CUDA is compiled in, a device is visible
and the card has room for the image. Everything that decides anything - the preparation, the
masked Pearson, the shortlist and the margins - is shared, so the engines can differ only in
how the four convolutions are computed, and the parity test compares two shortlists rather
than two answers.

The whole-detector transform was the entire added cost of the capture (4-15 s a run on CPU,
all of it the transforms). On the device it is milliseconds, so the composition is now
cheaper than the walk it replaced rather than dearer - which is what makes 2x2 binning, the
other way out, unnecessary: full resolution is affordable and the binned capture was measured
to pick a neighbouring peak on one dataset of 51.

Device discipline, because the card is shared with the run's own analysis workers: the four
convolution surfaces are brought back to the host and combined there, so the device holds
only one real buffer and three spectra; the spectrum of the image is reused for its square;
plans and buffers are created inside the call that needs them and freed when it returns; and
an image that would not fit is scored on the CPU instead.

Tests, none of which need a GPU or a dataset: the autoconvolution identity (an exactly
symmetric image is recovered at integer and half-pixel centres, scoring exactly 1), shift
equivariance, the four convolutions against brute force, the no-variance overlap that
VARIANCE_FLOOR exists for, the smooth pad, GPU against CPU, and the composition - a walk
seeded at the capture where the walk alone declines, the fallback to the capture alone at
BEAM_CENTER_CAPTURE_SIGMA_PXL, and the beam stop being blanked out of the scored image.
2026-09-13 07:37:05 +02:00
leonarski_f d75102b9b4 beam centre: the FFT captures the whole detector, the walk refines what it captured 2026-09-13 07:37:05 +02:00
leonarski_fandClaude Opus 5 1d1d7fd686 rugnux: whole-detector FFT beam-centre capture, measured on every run, consumed by none
The centrosymmetry score of the pre-scan projection is a self-convolution, so one FFT set
scores every candidate centre on the detector at half-pixel spacing - the cost is O(N log N)
and independent of how far the header centre is from the truth, where every existing search
pays per pixel of error. BeamCenterFFT computes the 2D point-inversion score (a background
measurement, the physics FindBeamCenterFromBackground fits locally) and the two 1D line-mirror
scores (for the along-spindle coordinate this is exact Friedel physics, sharp exactly where
the indexing count is blind), and returns a non-maximum-suppressed shortlist plus a
peak-to-runner-up margin per surface - a shortlist and a margin, never a centre: measured on
17 datasets the surface can be locally flat (~25 px), and the margin is what says so.

Measured on the gross-header cases that motivate it (offline, float64 reference): a header
351 px wrong is captured at rank 1 within 1.0 px from 30 frames, and still from a 30 deg
wedge; two 73 px placeholder headers at rank 1 within 0.5 px; a dataset whose header is
~170 px wrong but which no centre can index is flagged by the lowest margin of the set
(0.8 %) instead of being answered confidently. Capture survives 30-120 deg wedges on all
four sets tried (union of 8 candidates within 6 px everywhere).

Numerics: fftwf (the tree's FFTW is single precision) with the valid-pixel mean subtracted
before the transform - the masked Pearson is exactly invariant under a global shift, and the
subtraction removes the large-term cancellation - and the per-element combination done in
double. Against the float64 numpy reference the shortlist positions are identical and the
margins agree to 3e-6 absolute on the two controls whose margins are 0.3 %. An overlap with
no image variance (a mirrored empty region) carries no evidence and minted r values of 4-274
in float64 as much as float32; such centres are now not scored (VARIANCE_FLOOR).

Wired report-only into the pre-scan next to the background estimate: one log line with the
strongest candidates, the margins and the wall time. Nothing consumes it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-13 07:37:04 +02:00
leonarski_fandClaude Opus 5 a7ed4e2d34 beam stop: a bright ring is not a beam stop
Inside the stop a whole ring is blocked, so its own median is blocked too and the per-pixel
comparison has nothing to work with. The walk that covers that case declared a ring to be inside the
stop when its background fell below a third of the LARGEST background of any ring further out - and
on a sample whose background peaks in a strong ring away from the beam, the ordinary background
inside that ring is legitimately below a third of the peak. The walk then runs out to the ring and
returns a filled disk of good detector: on one corpus dataset 16 % of the area, with diffraction
rings plainly visible inside the disk it masked, and on another 2.5 %.

The defect predates the branch; what changed is that the flood from the beam centre used to discard
the disk whenever its seeds landed on invalid pixels, which is how the first of those two datasets
came back with an empty mask instead of a wrong one. Dropping that anchor was right, and it made
this visible.

A ring is now compared against what this detector's background typically is - the median over the
rings the walk is willing to judge - which is robust to a bright ring and to a corner ring of a
handful of pixels alike, and is less code. Evaluated over 151 corpus datasets: the number returning
a disk larger than 0.2 % of the detector falls from 12 to 3, the two pathological cases collapse
(radius 974 -> 68 px and 188 -> 12 px), and genuine stops move by a few pixels at most (152 -> 132,
122 -> 120, 70 -> 64). The new test fails on the old walk and passes on this one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 07:36:57 +02:00
leonarski_fandClaude Opus 5 99910e187e beam stop: a ring is only flat once the polarization is divided out
The shadow test compares a pixel with the median of its ring, which assumes the background is flat
around a ring with nothing in the beam. It is not: a polarized source suppresses the background in
its own plane by 1 - sin^2(2 theta), a factor of three at 2 theta = 55 degrees and four at 70. That
is several times the dip the test is looking for, so on a short-distance geometry the two horizontal
lobes of every outer ring read as shadow. Measured over the corpus it cost one dataset 5.1 % of its
detector, and another 11.2 %, with nothing visible under either mask.

The mean projection is therefore divided by the Kahn factor before the ring comparison, and the
Poisson deficit multiplies it back in so the significance is still the significance of the counts
that were recorded. The factor is the geometry's own CalcAzIntPolarizationCorr, evaluated about the
centre the caller named rather than the one in the file: the azimuth is the whole point of this
correction, and the geometry is what knows where the polarization plane lies once the detector is
tilted, the stored image quarter-turned or the detector rotated in its own plane. Six corpus
datasets are quarter-turned, and a flat-detector formula on the stored image's own axes gets them
90 degrees out of phase - measured, it takes one of them from 16.5 % masked to 22.8 % where the
geometry takes it to 4.4 %. Polarization is also the only correction a ring carries that varies
along it; solid angle, detector and air absorption are functions of 2 theta and the ring median
absorbs them.

Measured, mask fraction of the detector: a clean 110 mm sweep 0.82 -> 0.51 %, a clean 200 mm 18 Mpx
sweep 0.94 -> 0.80 %, a 150 mm sweep whose header centre is 351 px out 5.05 -> 0.74 %, and the
sweep with a third of the detector behind a pin 32.22 -> 32.83 %. Clean detectors lose mask, a real
obstruction gains it. Across 151 corpus datasets nothing gains more than a tenth of a percent from
the correction and 26 lose some, 6 of them by more than 3 percentage points.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 07:36:57 +02:00
leonarski_fandClaude Opus 5 dd95704d7b beam stop: the rings are drawn about the centre the data measures, not the one the file claims
Every pixel is compared against the ring it sits on, and the rings were drawn about the
beam centre in the file. Displacing that centre costs nothing for tens of pixels and a
great deal beyond: on clean sweeps 50 px leaves the mask where it was, 150 px returns a
tenth of the detector as shadow with nothing blocking it. Header centres are wrong by that
much - one sweep in the corpus states one 351 px from where the data put it, on a run that
otherwise indexes every frame and merges at CC1/2 0.999 - so a shadow search that trusts
the header is a live hazard now that an empty mask is no longer the fail-safe it used to be.

The centre is therefore fitted from the projection BEFORE the mask is read, and named to
the finder with BeamCenter(). It costs no frame of its own: the fit reads the same
per-pixel mean the mask is computed from. Measured on the phase boundary, the pre-scan
grows by 0.4-0.6 s on a 2.5 Mpx detector and by 3-4 s on an 18 Mpx one.

The other half of the circle is that this fit is itself biased by a shadow it has not yet
masked - on a sweep with a third of the detector behind a pin it put the centre 5 px from
the truth and called it 0.40 px, ten of its own sigmas out. It does not have to be unbiased
here. The ring comparison does not notice tens of pixels and the bias is a few, and the
centre the run REPORTS and consumes is not this one: it is the fit that already ran after
the mask was loaded, unchanged, with the shadow out of the way. The order is fit, mask,
fit, and only the second answer leaves the function.

Two rounds are enough, measured rather than assumed: on four clean sweeps the fit before
the mask and the fit after it agree to 0.03 px, so a third round would draw the same rings.
The centre estimator is not made robust to a shadow either - it already robustifies across
azimuthal sectors, three IRLS rounds on the per-sector shifts, and that is what returned
5 px at 0.40 px, because a third of the azimuth is a second population and not an outlier.
And no guard refuses to mask when the fitted centre disagrees with the header: a header
351 px out is precisely the case this has to survive.

Mask, before -> after (pixels, and per cent of the detector):

  clean, 2.5 Mpx            20851 (0.82%) ->  20670 (0.82%)
  clean, 2.5 Mpx            33308 (1.32%) ->  32730 (1.29%)
  clean, 18 Mpx, low bkg   173804 (0.96%) -> 170031 (0.94%)
  clean, 18 Mpx            250196 (1.38%) -> 249915 (1.38%)
  a third behind a pin     818184 (32.32%) -> 815699 (32.22%)
  header 351 px out         54183 (0.87%) -> 314592 (5.05%)

The clean sweeps do not move; on all four the measured centre is 1-9 px from the file's and
the mask shrinks by under 3 %. The pinned sweep does not move either - its header centre
happens to be right, so the rings were already where they belong - and its merge is
unchanged to the third decimal.

The last row goes the other way and is not the ordering: with the rings finally about the
beam, this geometry reaches 2 theta = 68 deg, where the polarization of the source modulates
the background around a ring by a factor of three. That is four times the threshold this
test cuts at, so the horizontal lobes read as shadow. The commit that follows removes that
confound; with it the same row reads 39 k px (0.63 %), which is the holder arm and nothing
else. Even uncorrected the run's answer is unchanged - same space group, same cell to the
third decimal, 12658 unique reflections either way, R_meas 9.06 -> 9.10 %, and 1 % of the
multiplicity lost.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 07:36:57 +02:00
leonarski_fandClaude Opus 5 d412306c77 beam stop: a shadow does not have to touch the direct beam
The shadow finder kept only the low region connected to the beam centre, flooding
outward from seeds within 4 px of it. That is written for the beam stop and its
holder arm, an object that touches the direct beam. Hardware that stands in the
beam further out - a pin, a loop - casts a shadow that begins some way out in
radius, with lit detector between it and the stop, and no bridge crosses that gap.
On a sweep where such a shadow covers 38 % of the detector the per-ring test found
the whole of it and connectivity then discarded 950 k pixels, leaving the beam-stop
disk alone; those pixels went into integration as measured-and-near-zero.

What makes the test specific instead is size: keep the connected components that
hold at least MIN_SHADOW_PIXELS core pixels. A shadow is cast by something physical
and is correspondingly large, while the background wanders a pixel or two at a time.
Measured on five clean in-house sweeps spanning 0.05 to 9.5 counts/px/frame of
background, every one returns exactly one such component - the beam stop - and the
largest spurious candidate anywhere is 74 pixels, a 27x margin.

MIN_EXPECTED_COUNTS becomes a Poisson significance. A ratio says nothing when the
background behind it is a handful of photons, so the deficit is now required to be
significant against its own scatter, sqrt(2 (E - N + N ln(N/E))) over the pooled
counts. This is what makes the comparison scale-free rather than tuned to one
exposure: rebuilt from six frames of a low-background sweep, the same thresholds
without it mask three quarters of the detector and with it mask none of it. The
index of dispersion, measured over the frames, does NOT do this job - it is 1.0
inside the shadow and 1.0 outside it, because a shadowed pixel is Poisson at a low
rate and a lit one is Poisson at a high rate. Only the rate relative to the ring
separates them.

The rest follows: the core ratio moves 0.35 -> 0.50 and the half-shadow reach 0.72
within 14 px -> 0.75 within 30 px, since a pin's penumbra is much wider than a beam
stop edge's; the ring walk that finds the rings lying wholly inside the stop keeps
its own threshold so the central disk does not move; and those rings join the region
after the size filter rather than being asked to be large themselves.

On the shadowed sweep this takes the mask from 16,587 px (0.66 %) to 818,184 px
(32.32 %), against 956,705 px (37.79 %) for the reference implementation, at a
precision of 0.92 against it. Clean sweeps move from 0.60 % to 0.82 %, the width of
the same arm.

One thing this costs: an empty mask used to be a statement about the beam centre,
because the seeds were placed at it. It no longer is, and a centre wrong by a couple
of hundred pixels now draws the rings across the background's own fall-off and
returns a large spurious mask instead of an empty one. The log message says so. The
fix is ordering - the centre is measured from this same projection a moment later -
and is not attempted here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 07:36:57 +02:00
leonarski_f 591c8cbc4b beam centre: the depth question is asked at both centres
The ladder inside the beam-centre check ran only at the MEASURED centre, and
its count was then set against the whole spot list at the file's. Two things
differ between those two numbers - where the beam is and how much of each frame
the pass reads - so the count cannot say which of them the gain came from, and
the run attributed all of it to the centre.

Measured on one crystal: at the measured centre, 15 px and 2.8 sigma from the
file's, the ladder reached 40/60 on a cell five times the deposited primitive
volume; at the file's own centre it reaches 49/60 on the deposited cell. The
run moved the centre, post-refined the supercell, and - standing at 40/60 -
skipped the beam-centre walk that finds the crystal's own centre six pixels the
other way. It shipped a C-centred monoclinic cell of the right volume in the
wrong lattice, on 43 per cent of the unique reflections.

The ladder now runs at both centres and the centre moves only when it is the
centre that pays. An exact tie keeps the file's centre. Where the file's centre
wins, the run takes that rung and says the depth, not the centre, was the
problem.

No arbiter can be asked instead here: at the measured centre the true cell is
not among the rungs, so no rung-versus-rung comparison contains the right
answer, and the standing pass the winner would be judged against is itself a
wrong 18 thousand cubic Angstrom cell 32 times smaller.
2026-09-13 07:36:48 +02:00
leonarski_fandClaude Opus 5 a751aec32c beam centre: a walk that is still travelling is not at its fixed point
The background fit stops early when two consecutive steps make an obtuse angle, which was meant
to catch the period-2 limit cycle a damped walk falls into at its fixed point. A travelling walk
wobbles too, and the test cannot tell the two apart: on one sweep whose centre is 170 px from the
header, a single 0.56 px step between 2 px ones turned twice in passing and ended the fit 118 px
short, with a last step of 4.96 px - i.e. while still moving at 2.5 px an iteration. Which of the
two happens is set by the last ulp of the per-sector regression, so the same source measured
(1231.55,1311.77) built with -march=x86-64-v3 and (1224.37,1430.16) without it, and the run
indexed every frame of the sweep in one build and refused to index at all in the other.

A reversal now counts only where the pair CANCELS - the two steps together move the centre less
than the smaller of them would alone - which is what a limit cycle does and what a wobble does
not. The travel budget goes from 100 iterations to 300, because this walk needs 107-115 to reach
CONVERGED_PXL and 100 cut it off in the last few pixels of its approach, leaving where it stopped
to the build as well.

Both builds now converge on the same centre to 0.047 px, and both recover the sweep.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-13 07:36:48 +02:00
leonarski_f 3d9c8b878b beam-centre rescue: a rung is scored at its own centre, and not adopted on a harmonic
The ladder set a trial centre and then scored it on spots found at the starting
centre. Raw centroids do not move with the beam centre, but which spots are in
the list does: the resolution limit, the ice-ring flag, the beam-stop mask and
the strongest-N ranking are all computed against a radius. Every rung was
therefore judged on the starting centre's evidence, and the count it produced
was not a function of the centre it named.

A trial centre now gets its own azimuthal mapping, spot engines and spot cache;
the starting centre's cache is parked and restored, so a ladder that adopts
nothing leaves the run exactly as it was. Measured against standalone runs
pinned at the same centres, the ladder's score goes from a mean absolute error
of 21.8 validation frames to 0.43, and at nine pinned centres the patched and
unpatched binaries agree exactly - the change is confined to the ladder.

Scoring honestly is not by itself an improvement: with the count honest, one
crystal adopted a rung at a real 56/60 whose cell is a four-fold axis harmonic
of the standing one, and merged the supercell. A longer cell indexes more,
which is why the harmonic arbiter exists elsewhere in this file. A rung whose
primitive volume is an integer or sqrt(3) multiple of the standing cell is now
refused, and that crystal walks past both harmonics to a 60/60 rung.

Over a hundred rotation datasets no space group, cell or run status changes.
Eight merges move: one gains 126 per cent of its unique reflections, one 3.7,
five move by less than half a per cent, and one loses 3.2 - that last was
attributed separately and is the difference between two failed merges, both
peaking at a half-dataset correlation of 5 per cent, with the honest run the
slightly better of the two. Wall time rises 1.7 per cent, the cost of finding
spots again at each trial centre.

On a corpus processed from a deliberately destroyed beam centre the same change
recovers eight datasets outright on their deposited space group and cell,
restores a ninth's space-group call by returning its axial absence counts to the
values the correct geometry gives, and withdraws one adoption that had shipped a
wrong cell in the wrong crystal system at a half-dataset correlation of 0.10.

Note that only the spot-cache half of this does the work. Reading the indexer's
geometry per call rather than from construction was measured to change nothing
on a rotation ladder, because the per-frame validation dispatches to a path that
takes its geometry from the forced indexer result; it is kept because the copy
was misleading to read, not because it moves a number.
2026-09-13 07:36:48 +02:00
leonarski_f 7a62032765 rugnux: the first-pass ladder does not adopt an axis sub-multiple
The ladder picks its rung on the validation-frame count, and that count cannot
arbitrate an axis harmonic: a cell twice as long has to place every spot twice
as accurately to score the same, which is why the scheme comparison already
refuses to decide such a pair on it. Across rungs the bias is worse, because
the leanest and shallowest rung - the one that scores highest on a harmonic -
is also the one a smaller cell is easiest on, so the two biases compound.

Measured on one crystal: the "30 spots/image, stop at 4.91 A" rung indexed
48/60 frames on a halved c axis while the deeper rungs found 2x, 3x and 4x of
the true one. The ladder adopted the halved cell together with a beam centre
15 px from the file's, which pre-empted the beam-centre walk that otherwise
moves 6 px and finds the crystal's own lattice at 42/60; the run ended in a
different Bravais class on a cell half as long.

So the winner is now put through the same axis-harmonic arbiter the scheme
comparison uses - which of the two cells accounts for more of the validation
frames' spots - against every rung that cleared the caller's bar and whose
primitive volume is a near-integer multiple of the winner's. A winner that
loses that question is a sub-multiple and the ladder adopts nothing; promoting
the rival instead would be no better, since on the measured crystal the rival
is a multiple of the true axis in its own right. The comparison is made on the
spot list the run stands on, so both cells are counted against the same spots.

Measured: the crystal above returns to the cell and space group it reaches with
no ladder at all, and the three crystals the ladder was built for are unchanged
(two of them never reach the guard, the third meets it and keeps its cell, the
doubled rival accounting for 923 spots against its 929).
2026-09-13 07:36:48 +02:00
leonarski_f de839021dc rugnux: the whole-spot-list rung does not overflow the cloud's reserve
The rung that reads every spot of a frame passes SIZE_MAX as the per-image cap, and the
accumulated cloud reserved max_spots_per_image * images - which overflows and throws
length_error out of vector::reserve, killing the run. The count held is already tracked
exactly, so reserve that.
2026-09-13 07:36:48 +02:00
leonarski_f 1b28dd2072 rugnux: a first pass that finds no lattice tries a leaner, shallower one
A crystal sitting in a crystalline powder floods the first pass with spots that are not its
own. Two in-house crystals gave 2112 spots a frame against the 80 a clean crystal on the same
beamline gives, and no scheme indexed a single validation frame; the FFT took a 10-30 A cell
out of the shells. A third dataset, with no powder at all, failed for the neighbouring reason:
its file beam centre is 170 px out, and the measured centre was tested with the full spot list,
indexed nothing there either, and was thrown away.

Powder rings are now MEASURED from each run's own pre-scan spots rather than read off the fixed
hexagonal-ice list, and reported on every run (POWDER_*) whether or not anything acted on them -
hexagonal ice is the only phase that can be named in advance, and 16 of the 24 rings on one of
these crystals are ice while the rest are not.

A first pass that ends with no usable lattice then retries over how much of each frame it reads:
the strongest 30, 80, 200 or all spots an image, each at the file's resolution and at the
resolutions a quarter and a half of this sample's own spots lie coarser than, with the measured
rings set aside where there are any. Decided late on the validation-frame count, the way the
rotation-axis sign already is, and adopted only on a win of a sixth of the frames. The same
ladder is asked inside the beam-centre check, where a centre that is wrong and a spot list that
is too deep otherwise hide each other.

Measured over 146 datasets: unchanged on all of them - the open arm scores 91/99 either way and
the 46 SLS datasets are identical on every scientific field - while the three that failed now
index. The deposited cell is recovered to 0.1% on the beam-centre case, at 100% of frames and
0.597 A.
2026-09-13 07:36:48 +02:00
leonarski_f ea37056fe9 rugnux: judge the two-pass on the signal each pass measured, not on a pooled CC1/2
The rotation two-pass reverts to the header geometry when the refined pass
looks worse. It decided that on the search merge's pooled, uncut,
pre-correction P1 CC1/2 - a number that reads 0.13 on a crystal whose data
merge at 0.995, and that moves 0.93 to 0.76 between two runs whose merged
data agree to 0.7% of R_meas. Two crystals were sent back to a geometry that
ships 22% fewer reflections than the one they refused.

Compare instead what each pass MEASURED: reflections merged at I/sigma >= 2.
Against an external arbiter that objective agrees with which geometry is the
more accurate one on 23 of 27 arm-dataset pairs, where self-consistency
measures agree on 13 of 27. Count it on both sides over the centring-allowed
reflections, so a pass that finds a centring the other did not is not judged
on the extinguished half its P1 merge holds as noise (two crystals compared
merges holding 2.0x the reflections of the other), and ask the axial-row arm
only where the two passes are in the same setting, which is the only case
where the two counts are counts of the same reflections.

The bar is a decisive loss rather than a bare inequality: the two passes
never integrate the same way - the canonical pass integrates at the measured
spot width and predicts with the pre-pass's smoothed mosaicity - so a
difference that small is not evidence about the geometry either way.
2026-09-13 07:36:19 +02:00
leonarski_fandClaude Fable 5 7b868fb9f5 viewer: raster window composition and remote-display repaint throttling
Over "ssh -X" the viewer was near unusable, and the July finding that every
repaint uploads the whole window (5.3 MiB at the default size) regardless of
how small the damage is turns out to have a single cause: the reciprocal-space
window constructed a QOpenGLWidget at startup, and one dormant QOpenGLWidget -
unmapped, in a window nobody ever opened - switches Qt's window composition
onto the OpenGL swapchain path. That path re-presents the entire window on
every flush (and is also the llvmpipe CPU burn seen on headless boxes). The
reciprocal-space window was a placeholder feature and is removed; with it gone
Qt flushes through the raster backing store, which sends only the damaged
region.

On top of that, interactions that repainted once per input event are held to
the hover cadence (15 Hz) on remote sessions only: panning accumulates its
pixel delta, wheel zoom its steps, a recolour burst its final value, live
playback keeps only the newest frame (the worker ack stays per-frame), and the
toolbar counter/slider readbacks coalesce the same way. Each throttle applies
inline with a single-shot tail, the same shape as the existing hover limiter,
so the view always lands exactly where the gesture put it. Local sessions are
unchanged - every event still applies inline.

Remote sessions are detected once at startup (xcb platform and a DISPLAY with
a host part, i.e. "localhost:10.0" from SSH forwarding or "host:0"); the
JFJOCH_VIEWER_REMOTE environment variable and a View-menu toggle override the
detection in either direction.

Measured through a byte-counting X relay against Xvfb with MIT-SHM disabled
(client-to-server bytes are what the SSH link carries), same scripted
interaction sequence, 1200x1100 window, KiB:

                     rc169    removal only   removal+throttles
  hover, 50 motions  82088    2040           2040
  pan, 50 motions    290046   12087          4778
  wheel zoom x10     71143    12603          7649
  ctrl-wheel x6      65672    7194           4797
  frame step x10     284572   21454          21454

Removing the reciprocal-space window from the source measured byte-identical
to only deferring its GL view to first show, confirming the cost is the live
QOpenGLWidget, not the code being compiled in. The image panel renders
pixel-identically (AE=0) to rc169 after identical zoom+pan gestures, with the
throttles on and off.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WbjnQhutnboy4qbxwmgDAX
2026-09-12 22:38:45 +02:00
leonarski_f 483cba0aaa report: authorship and the acknowledgement open the report
They were at the foot, where a reader who stops after the first screen never
reaches them. A results file travels further than the person who produced it, so
who wrote the program, what its licence permits and whose methods it implements
belong before the data rather than after.

The header's explanatory paragraph goes with them. It described what a
`KEY= value` line is and what --developer does, which a reader works out
without being told.
2026-09-12 22:38:32 +02:00
leonarski_f db16f06e0f report: the header says what the lines are, and stops there
The opening paragraph explained that the keys are a stable interface, that
REPORT_VERSION records when it changed, and that --developer relocates rather
than loses the long explanations. All true, none of it what a reader opening a
results file needs in the first six lines, and the register gave it away as
something no crystallographer had written. Two sentences remain: what the three
kinds of line are, and what --developer adds.
2026-09-12 22:38:32 +02:00
leonarski_f d1d1748fed docs: typographical fixes in the hardware and deployment pages
Jungfruajoch, frotend, allways, statisitics, pixed point, "the two has to be
purchases separately", "servers are the moment", "how it processed". These pages
are published to Read The Docs.
2026-09-12 22:38:32 +02:00
leonarski_fandClaude Opus 5 50a23c2945 docs: six instruction-shaped sentences become plain statements
A prose pass over docs/ for text that reads as written by, or addressed
to, a language model. The documentation is almost entirely in the house
voice already; what needed changing was six hollow directives in the
older infrastructure pages ('it is important to ensure...', 'extra care
has to be taken...', a leftover 'Be explicit about the gaps' addressed
to the document's own author). Each is rewritten as a statement of fact
with the technical content untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-12 22:38:32 +02:00
leonarski_f 43decf99a8 report: REPORT_VERSION counts releases, not format changes
The constant went to 14 on the reasoning that the format history above it
listed thirteen entries while the number still read 7. That is backwards. A
reader only ever meets the report that was built from main, so a bump inside a
release branch numbers a format nobody received, and several of those entries
shipped together in one release. Seven is what main ships; this release changes
the format once, so it is eight.

The convention is now written down beside the constant and in the report
documentation, because this is the second time it has been read the other way.
2026-09-12 22:38:32 +02:00
leonarski_fandClaude Opus 5 f8763ca875 rugnux report: parsable comments, honest build provenance, terms of use
Every line of <prefix>_report.txt is now KEY= value, a # comment, or blank
(REPORT_VERSION 14; warnings print as "# WARNING:"), so a consumer keeps the
data by dropping the # lines. The header records the exact build: the git
commit, stamped at BUILD time by common/GitInfoStamp.cmake so it cannot go
stale in a reconfigured tree, with -dirty marking uncommitted changes; the
CMAKE_CXX_FLAGS of the build, because two builds of one commit can differ by
flags alone; and the release page of exactly this version, whose tag is the
version string. The foot states authorship and terms of use: GPLv3, free for
academic institutions and commercial companies alike.

Every sentence phrased as a directive to the reader ("Read the shell table
rather than quoting a single number", "Do not refine against the written
reflections", "Treat the reported cell as a supercell candidate", "believe
the _BEFORE_SEARCH one") is rewritten as a statement of fact about the
measurement: a report describes the data, it does not instruct whoever - or
whatever - reads it. Also fixed: REPORT_VERSION had stayed at 7 while its own
history comment reached 13; ANISOTROPY_D_MIN_* printed "nan" against the
report's own no-placeholder rule; "1 condition(s) need attention"; "rises by"
on a signed quantity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-12 22:38:32 +02:00
leonarski_fandClaude Opus 5 df1536db7f unmerged export: the comments name the FLIGHT column they forgot
The header and the two in-function comments describing what the unmerged
MTZ carries still said "two columns" and "raw counts are I / LP * QE",
written before the flight-path term and its FLIGHT column were added.
The code, the test and docs/RUGNUX_INTEGRATION.md all say I / LP * QE *
FLIGHT; only these comments did not. Also drops the self-referential
note in Reflection.h about a comment having been overtaken, and states
the flight factor's symbols and the medium rather than assuming air.

No behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011GxZqDiFP3KqriBhNdcR56
2026-09-12 22:38:32 +02:00
jungfrauandClaude Opus 5 051e2aef46 ci: pin every job on the 2609 images
Build Packages / Create release (push) Successful in 29s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 8m4s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 9m20s
Build Packages / build:viewer-tgz:cpu (push) Successful in 9m59s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m53s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 15m39s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 15m54s
Build Packages / build:windows:nocuda (push) Successful in 16m32s
Build Packages / build:windows:cuda (push) Successful in 19m0s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 23m16s
Build Packages / build:rugnux:windows (push) Successful in 10m0s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 18m37s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 20m9s
Build Packages / Generate python client (push) Successful in 44s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 19m26s
Build Packages / Build documentation (push) Successful in 1m49s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m21s
Build Packages / build:rpm (rocky8) (push) Successful in 17m28s
Build Packages / build:rpm (rocky9) (push) Successful in 17m33s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 13m57s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 10m53s
Build Packages / Unit tests (push) Successful in 1h11m53s
One tag for all four images, replacing the 2607b/2608b split. The 2609 images carry
Qt 6.11.1, CMake 3.31.8 and Ninja 1.13.2 identically, so a build no longer depends
on which distro's tooling a job happened to land on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6iekpDa2xiKobEj1R5xnp
2026-09-12 16:00:24 -04:00
jungfrauandClaude Opus 5 090a01e3cf docker: Qt 6.11.1, with the pinned ninja and xcb package it needs
Qt 6.11 did not build on either Rocky image, for two unrelated reasons:

rocky8 carried ninja 1.8.2, which cannot parse what Qt 6.11 generates -- "multiple
outputs aren't (yet?) supported by depslog", a ninja limitation lifted in 1.10.
Ninja is now pinned at 1.13.2 from the upstream release binary, for the same reason
CMake is: the distro versions spanned 1.8.2 to 1.11.1 and moved under us.

rocky9 installed xcb-util-*-devel, a glob that does not match the plain
xcb-util-devel package, so the XCB::UTIL target Qt 6.11's xcb platform plugin
requires was absent. Qt 6.9 did not ask for it, which is why it appeared only now.

build-qt.sh also derives the download path's series from the version, so a bump no
longer 404s against the old 6.9 directory.

Built on all four images, and jfjoch_viewer links against the static Qt 6.11.1 on
both rockys (2708/2708 targets, Qt fully static -- only system X/GL remain dynamic).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6iekpDa2xiKobEj1R5xnp
2026-09-12 16:00:24 -04:00
jungfrauandClaude Opus 5 cc4ded5aad ci: build a release in one run instead of two
Releasing took two full runs of the matrix. The manual dispatch built every
package and threw them away -- the uploads were gated on being on a tag -- then
created the tag, whose push re-ran the whole workflow only to rebuild the same
packages and upload them. That is where ">3 hours from push to release" came from,
and the second run tested nothing the first had not.

Now the run that creates the release also fills it. create-release runs first and
every uploading job waits for it, so the uploads can target the release directly;
they are gated on the same dispatch input that creates it rather than on ref_type.
Tags no longer trigger the workflow at all, because nothing is left for that run
to do. The release is still created before its assets exist, exactly as it was
when the tag run populated it.

create-release now runs on every event, because a skipped job skips its dependents.
On an ordinary push it is a bare checkout: both the LFS pull and the release call
are gated, so nothing is fetched or created.

Also: the pull_request trigger is gone. Branches live in this repository, so the
push trigger had already run the whole workflow on the identical commit. And the
HDF5 consumer tests no longer repeat on a tag run, which no longer exists anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6iekpDa2xiKobEj1R5xnp
2026-09-12 14:52:26 -04:00
jungfrauandClaude Opus 5 c94fe9e740 docker: one copy of the shared build steps, and a pinned CMake
OpenSSL, libdbus, Qt and Eigen were built from byte-identical shell in all four
images. They now live once in docker/common/ and each Dockerfile runs them, which
is why the build context is docker/ rather than docker/<variant>/. The four copies
are what let the git HTTP/1.1 fix land in both Ubuntu images and neither Rocky one.

CMake is pinned and installed from cmake.org instead of coming from the distro.
The base images ship three different versions (EL8 3.26.5, EL9 3.31.8, Jammy 3.22
via a Kitware repo held at 3.26, Noble 3.28.3) and they move at every base refresh;
that spread is what makes a configure fail on one image and pass on the next, as
the CMP0169 deprecation that only appears on 3.31 did. The Kitware apt repo that
existed solely to hold CMake at 3.26 is gone with it.

Also: duplicate zlib-static (both rockys) and libxcb-devel (rocky9) package
entries, and an ENV PATH=/usr/bin that never changed anything (ubuntu2204).

Dockerfiles drop from 872 to 684 lines against ~110 lines of shared script. All
four build clean through the eigen step; Qt is unchanged text and is covered by
the image rebuild.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6iekpDa2xiKobEj1R5xnp
2026-09-12 14:33:10 -04:00
jungfrauandClaude Opus 5 cb55a60002 ci: one job for the HDF5 consumer tests, pinned images, one -j
The DIALS and three XDS jobs each configured and built the same jfjoch_hdf5_test
on the same rocky9 image -- four identical builds for twelve short test cases.
They are now one job that builds once and runs all twelve, and every case runs
even when an earlier one fails: each is a subshell under `set -e`, so a case stops
at its own first bad command without taking the rest down, and a summary at the end
decides the job's result. Each XDS case also clears its directory first, which two
of them did not do before their first run -- on a runner that reuses its workspace
volume, that let XDS index a previous run's files.

Every job now names its image instead of taking whatever the runner label points
at. Two jobs had pinned much older images (rocky8:2511, ubuntu2404:2508), so the
unit tests did not run on the toolchain that builds the packages. The rockys stay
on 2607b because no 2608 was ever built for them.

Unit tests build with -j16 like everything else; -j48 from one job starves the
other seven at runner capacity 8.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6iekpDa2xiKobEj1R5xnp
2026-09-12 14:17:11 -04:00
jungfrauandClaude Opus 5 9efe72f9b1 licenses: list libcurl in the notices
libcurl is fetched and statically linked into the shipped jfjoch_viewer, but it
had no row, no licence text and no COLLECT.sh entry. Its collection is the one
that only resolves after a viewer-enabled configure, which is noted at the entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6iekpDa2xiKobEj1R5xnp
2026-09-12 14:00:17 -04:00
jungfrauandClaude Opus 5 4f3d2f85b6 licenses: follow the dependency updates in the notices
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 7m30s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 10m7s
Build Packages / build:viewer-tgz:cpu (push) Successful in 12m2s
Build Packages / build:viewer-tgz:cuda (push) Successful in 12m52s
Build Packages / build:windows:nocuda (push) Successful in 16m58s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 18m35s
Build Packages / build:windows:cuda (push) Successful in 19m12s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 20m57s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 21m54s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 19m26s
Build Packages / build:rugnux:windows (push) Successful in 10m27s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 22m32s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 21m23s
Build Packages / build:rpm (rocky8) (push) Successful in 22m7s
Build Packages / build:rpm (rocky9) (push) Successful in 19m55s
Build Packages / Generate python client (push) Successful in 33s
Build Packages / Build documentation (push) Successful in 1m21s
Build Packages / Create release (push) Skipped
Build Packages / XDS test (durin plugin) (push) Successful in 9m25s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 9m33s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 20m52s
Build Packages / XDS test (neggia plugin) (push) Successful in 8m19s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 22m21s
Build Packages / DIALS test (push) Successful in 19m20s
Build Packages / Unit tests (push) Successful in 1h18m13s
Versions in the fetched-at-build-time table, and the two licence texts that
changed with them: HDF5 2.2.0 appends the Apache-2.0 text to its BSD-3-Clause-style
licence, and libjpeg-turbo 3.2.0 rewrote its licence sections (the licence set is
unchanged: IJG + BSD-3-Clause + zlib).

Abseil gets a row and a licence text of its own. It was always linked in, arriving
as Ceres' git submodule; now that the build fetches it explicitly it is listed
rather than left implicit.

docs/THIRD_PARTY_NOTICES.md regenerated by the update_version.sh recipe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6iekpDa2xiKobEj1R5xnp
2026-09-12 13:30:14 -04:00
jungfrauandClaude Opus 5 2b30850d1a build: update fetched dependencies to current releases
zstd 1.5.6 (a commit pin) -> 1.5.7, hdf5 2.1.0 -> 2.2.0, Catch2 3.13.0 -> 3.16.0,
cpp-httplib 0.39.0 -> 0.56.0, curl 8.11.1 -> 8.22.0, libtiff 4.7.1 -> 4.7.2,
libjpeg-turbo 3.0.4 -> 3.2.0.

spdlog and libzmq were already current. Ceres stays on its pinned commit, which is
newer than the 2.2.0 tag. sls and fast-feedback-indexer are unchanged.

Built clean on all four CI images -- rocky8, rocky9, ubuntu2204, ubuntu2404 --
with the CI flags and CUDA on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6iekpDa2xiKobEj1R5xnp
2026-09-12 13:26:36 -04:00
jungfrauandClaude Opus 5 2bd6ddd577 build: fetch dependencies as pinned tarballs instead of git clones
GitHub throttles unauthenticated git-upload-pack and selects targets by TLS
fingerprint (x-github-edge-protection: ja4-campaign-git-error). A clone gets 200
for the ref advertisement and then 401 for the fetch, so git asks for a password,
finds no terminal, and the configure stops with no error and hangs to the CI
timeout. Whether an image is hit follows its OpenSSL version: rocky8 (1.1.1)
passed 5/5 while rocky9 (3.5.5) and ubuntu2204 (3.0.2) failed 5/5, which is why
it presented as two broken build images.

The archive endpoint is not part of the campaign, and a tarball is a fraction of
the transfer -- hdf5 is 40 MB against a 271 MB shallow clone. Every URL is pinned
by tag or commit and its bytes by URL_HASH.

Ceres is the exception: it carries abseil as a git submodule, so it is populated
first, the abseil commit its submodule points at is unpacked into
third_party/abseil-cpp, and the subdirectory is added only then.

Versions are unchanged; the archives are the same trees the clones checked out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6iekpDa2xiKobEj1R5xnp
2026-09-12 13:11:10 -04:00
jungfrauandClaude Opus 5 ff91fd0c0c docker: clone over HTTP/1.1 in the Ubuntu build images
The HTTP/2 multiplexing libcurl uses by default makes clones from some servers
fail mid-transfer with "RPC failed; curl 92 HTTP/2 stream not closed cleanly".
Every source dependency in the image, and the project's own FetchContent step,
clones over HTTPS, so pin HTTP/1.1 system-wide.

Orthogonal to GitHub's unauthenticated-download throttling, which answers
git-upload-pack with 401 regardless of the HTTP version.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6iekpDa2xiKobEj1R5xnp
2026-09-12 12:43:24 -04:00
leonarski_fandClaude Opus 5 0ca6b251e5 jfjoch_viewer: no settings section is wider than the panel it sits in
Build Packages / build:windows:nocuda (push) Successful in 17m28s
Build Packages / build:windows:cuda (push) Successful in 19m59s
Build Packages / build:rugnux:windows (push) Successful in 10m55s
Build Packages / Unit tests (push) Successful in 47m41s
Build Packages / build:viewer-tgz:cpu (push) Successful in 7m17s
Build Packages / build:viewer-tgz:cuda (push) Successful in 8m8s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 7m2s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 4m57s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 10m21s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 9m10s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 10m14s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 8m17s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 10m57s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 10m9s
Build Packages / build:rpm (rocky8) (push) Successful in 11m10s
Build Packages / build:rpm (rocky9) (push) Successful in 10m17s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 10m55s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 8m58s
Build Packages / Create release (push) Skipped
Build Packages / Generate python client (push) Successful in 14s
Build Packages / Build documentation (push) Successful in 43s
Build Packages / DIALS test (push) Successful in 10m29s
Build Packages / XDS test (neggia plugin) (push) Failing after 3h12m41s
Build Packages / XDS test (JFJoch plugin) (push) Failing after 3h12m43s
Build Packages / XDS test (durin plugin) (push) Failing after 3h12m45s
Measured every section's content minimum width against the 330 px the
settings dock gives it (the powder one, fixed yesterday, sat at 278):

  geometry 346, unit cell 355, goniometer 322, spot finding 370,
  indexing 321, bragg 287, scaling 297, azint 243, reference 149

Five over. Three causes, none of them about the section:

- addRow("", w) reserves the label column and puts the widget in the
  field column, so a checkbox's whole width was added to the widest
  label. addRow(w) spans both columns instead, which is what a
  label-less row means anyway. Spot finding 370 -> 251.
- A spin box is never narrower than its longest possible value, suffix
  included, and these rows hold two or three abreast. The unit moves to
  the row label, which is how the panel names units elsewhere
  ("High resolution [Å]"): "a, b, c [Å]", "Beam origin [px]",
  "Detector tilt [°]", "Step x, y [μm]", "Radii r1/r2/r3 [px]".
  A cell length can still be four digits and three decimals, so those
  six fields also divide the row rather than demand their own width.
- A combo is never narrower than its longest entry - the same thing
  that pinned the powder panel. "Flex (best per-image refinement)" set
  the indexing section's width; it elides now, as the calibrant does.

Every section is now 291 or less, and none of them moves the dock.

Two inspector changes in the same panel:

- An axis that is present but does not turn covers no angular range, so
  the image angle is stated as one number rather than "-145° + 0°".
- Sample-head angles that stay put during a run - an SLS Smargon writes
  chi and phi - get a row of their own, "chi 0° / phi 0°". Both its
  labels stay empty for a file that carries none, so it costs nothing
  where it means nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oVDk7aukYqfPZBLHn5nGg
2026-09-12 07:31:54 +02:00
leonarski_fandClaude Opus 5 a7a4c28925 jfjoch_viewer: the inspector states the numbers it used to hide
Build Packages / Unit tests (push) Successful in 49m15s
Build Packages / build:windows:nocuda (push) Successful in 18m5s
Build Packages / build:windows:cuda (push) Successful in 20m26s
Build Packages / build:viewer-tgz:cpu (push) Successful in 7m17s
Build Packages / build:viewer-tgz:cuda (push) Successful in 8m15s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 6m57s
Build Packages / build:rugnux:windows (push) Successful in 10m56s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 5m0s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 10m21s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 8m22s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 11m49s
Build Packages / build:rpm (rocky8) (push) Successful in 11m57s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 9m5s
Build Packages / XDS test (neggia plugin) (push) Successful in 5m45s
Build Packages / Generate python client (push) Successful in 10s
Build Packages / Build documentation (push) Successful in 36s
Build Packages / Create release (push) Skipped
Build Packages / XDS test (JFJoch plugin) (push) Failing after 3h12m36s
Build Packages / XDS test (durin plugin) (push) Failing after 3h12m38s
Build Packages / DIALS test (push) Failing after 3h12m40s
Build Packages / build:rpm (ubuntu2204) (push) Failing after 3h12m42s
Build Packages / build:rpm (rocky9) (push) Failing after 3h12m44s
Build Packages / build:rpm (rocky9_sls9) (push) Failing after 3h12m46s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Failing after 3h12m48s
Build Packages / build:rpm (rocky9_nocuda) (push) Failing after 3h12m50s
Six changes to the desktop viewer, all in what it shows and what it is
called:

- The left-hand reprocessing panel is titled "Processing" (it sits next
  to "File manager"); the jobs panel that held that name is "Jobs".
- The file manager lists *.cbf beside the HDF5 datasets. A CBF is one
  frame per file and opening any of them opens its sweep, which is what
  the File > Open dialog already did.
- Image statistics: collection time, detector distance, beam centre and
  wavelength were in the dataset row's tooltip, where a number nobody
  hovers over is a number nobody reads. They are rows of their own, in
  the order a person asks for them - what the dataset is, how it was
  collected, then what this image contains. Source/instrument and sample
  are bold like every other value. The B-factor row is gone.
- A "Spots + background" plot: spot count on the left axis, background
  per pixel on the right, since a common scale would flatten one of
  them. The axis labels take the colour of their line - a legend costs
  exactly the vertical room those labels need at this dock height. It is
  what a live (HTTP) connection selects by default, until the user picks
  something else.
- Expanding the powder-calibration section no longer widens the settings
  panel past the space it has. Two buttons side by side cannot be
  narrower than both their labels, and a combo is never narrower than
  its longest entry - a 40-character cell in the calibrant list set the
  minimum width of the whole dock. One button per row, the cell in a
  tooltip, and both combos allowed to elide.
- The grid/per-image toggles both rebuild the metric selector now. The
  grid button did not, so leaving per-image mode through it left the
  per-image plot types listed - X-ray fluorescence offered as a metric
  of a grid scan - and their values were then read as per-dataset
  metrics. Each mode also remembers its own selection instead of
  carrying an index across two unrelated lists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oVDk7aukYqfPZBLHn5nGg
2026-09-11 22:07:54 +02:00
leonarski_fandClaude Opus 5 75eb6ce59c jfjoch_viewer: a dataset opened over HTTP keeps its source and instrument
Build Packages / Unit tests (push) Successful in 1h8m44s
Build Packages / build:windows:nocuda (push) Successful in 16m57s
Build Packages / build:windows:cuda (push) Successful in 20m12s
Build Packages / build:viewer-tgz:cpu (push) Successful in 16m28s
Build Packages / build:viewer-tgz:cuda (push) Successful in 17m5s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 14m54s
Build Packages / build:rugnux:windows (push) Successful in 11m7s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 8m31s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 19m32s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 14m0s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 15m23s
Build Packages / build:rpm (rocky8) (push) Successful in 14m28s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 9m2s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 6m23s
Build Packages / XDS test (neggia plugin) (push) Successful in 5m44s
Build Packages / Generate python client (push) Successful in 10s
Build Packages / Build documentation (push) Successful in 34s
Build Packages / Create release (push) Skipped
Build Packages / XDS test (durin plugin) (push) Failing after 3h12m36s
Build Packages / DIALS test (push) Failing after 3h12m38s
Build Packages / build:rpm (ubuntu2204) (push) Failing after 3h12m40s
Build Packages / build:rpm (rocky9) (push) Failing after 3h12m43s
Build Packages / build:rpm (rocky9_sls9) (push) Failing after 3h12m45s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Failing after 3h12m47s
Build Packages / build:rpm (rocky9_nocuda) (push) Failing after 3h12m49s
The start message carries source_name, source_type and instrument_name, and the
broker fills all three from its config, but the HTTP reader never imported them
into the dataset's experiment - so the Inspector showed "-" for
Source / Instrument on a live connection while the same file opened from disk
showed it. Total flux and attenuator transmission, which the tooltip on that
line reports, were dropped the same way; ring current already came through.

Sample name was never affected and is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PogHY7bWXV4bpctDPdyuDN
2026-09-11 19:23:49 +02:00
leonarski_fandClaude Opus 5 a59be89192 receiver: bound the wait for a start message that never arrives
JFJochReceiverLite::MeasurementThread polls for the start message with no way out other than
the cancelled flag, and JFJochServices::Stop cancels the receiver silently once the detector
reads back idle - a silent cancel deliberately does not set cancelled, since it reports that
the detector has finished rather than asking for an abort. A run whose start message never came
therefore left Stop() waiting on this thread indefinitely, and only an operator pressing cancel
got the broker back.

Cancel(bool) is overridden to record that the detector has finished, and the pre-start poll
gives up five seconds later. The grace is there because the detector can also read back as idle
for a moment just after it was armed, so a run is not abandoned the instant the silent cancel
arrives; the flag is consulted nowhere else, so images already being processed are untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bew392LTGP2fkhfRJsMcB
2026-09-11 19:19:45 +02:00
leonarski_fandClaude Opus 5 db5068b2f8 tests: a puller whose queue filled must recover, not go silent
Covers the half of the deadlock fix that decides whether a collection poisons the ones after
it. outside_fifo fills whenever the detector keeps streaming past the point where the run
stopped consuming - the analysis threads have gone, and Suspend() only arrives once Stop() has
finished with the writer - and the CBOR thread then parks in PutBlocking on the full queue.
Suspend() cannot release it, since the flag is tested before the put, so ResumeAndClear at the
next start has to; nothing else ever would, because a Get on an empty queue notifies no
producer. Without that notify the puller is dead for good: cbor_fifo fills behind it, the
puller thread blocks in turn, and every later collection receives nothing at all - no images
and no start message - while the puller itself looks healthy.

The test fails with the c_full notify in ThreadSafeFIFO::Clear removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bew392LTGP2fkhfRJsMcB
2026-09-11 19:19:35 +02:00
leonarski_fandClaude Opus 5 1da94bdfe2 broker: a cancelled dark-mask collection is not a finished calibration
DarkMaskAnalysis::GetMask always returns a full-size array - a pixel that saw no frame reads as
good - so the dark_mask_result.size() check in TakeDarkMaskInternal accepted a mask measured on
nothing. The mask was adopted through LoadDarkBadPixelMask, the state went to Idle and the
sequence logged "Calibration sequence done" on a calibration the operator had just cancelled.
The dark mask is a single step, so unlike a pedestal sequence there is no later cancel_sequence
check to catch it.

Refuse it after Stop() the way the check at the top of the same function does: Inactive with the
"Mask sequence cancelled" error, matching what the pedestal steps report.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bew392LTGP2fkhfRJsMcB
2026-09-11 18:53:53 +02:00
leonarski_fandClaude Opus 5 c40e0eb11d broker: fix deadlock when re-initialising after a DECTRIS run that never started
A re-initialisation left the previous run's ZMQImagePuller connected: JFJochServices::On
replaces its own shared_ptr, but JFJochReceiverService keeps a second reference to the puller
of the last run. Two PULL sockets on one PUSH peer share messages round-robin, so the orphaned
puller silently took half of the DECTRIS stream - the start message included - and the receiver
then waited for a start message that had already been discarded.

Once such a run was cancelled, nothing drained the orphaned puller's outside_fifo any more. Its
CBOR thread parked in PutBlocking on the full queue (suspend is only tested before the put), the
puller thread backed up behind it in cbor_fifo, and neither could reach the disconnect flag
again. The next JFJochReceiverService::Start dropped the last reference to that puller, so
~ZMQImagePuller joined two threads that could never exit - while holding state_mutex, inside a
calibration sequence that itself holds the state machine's mutex. The whole control plane froze
with no way to cancel; only a restart got out of it.

* ThreadSafeFIFO gains Stop(), which releases every waiter and makes further blocking operations
  return at once. Clear() now notifies c_full as well: clearing a full queue used to leave the
  blocked producer asleep, since the next Get on an empty queue notifies no one.
* ZMQImagePuller::Disconnect and TCPImagePuller::Disconnect stop their queues before joining, so
  a puller whose consumer is gone can always shut down.
* JFJochServices::On and ::Off disconnect the previous puller explicitly rather than relying on
  the shared_ptr going away, so two readers never share the detector stream.

ZMQImagePuller_DisconnectWithFullQueue covers the shutdown; it hangs on the previous code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012bew392LTGP2fkhfRJsMcB
2026-09-11 18:53:39 +02:00
leonarski_fandClaude Sonnet 5 d24cdfcff4 jfjoch_viewer: navigation keys, one-shot auto-contrast, file manager panel, cleanup
* Dataset-info plot hover shows a horizontal crosshair alongside the existing
  vertical one, so a run of hovers reads as a level or a drift at a glance.
* `A` applies auto-contrast once instead of switching on persistent Auto mode
  (a new `oneShotAutoForeground()`, distinct from the toolbar's Auto toggle);
  pressing it a second time on the same image switches Auto on for good, and
  pressing it while Auto is already on leaves it on. The value itself comes
  from one `AutoForegroundValue()` shared with the continuous Auto path.
* `Home`/`End`/`Page Up`/`Page Down` navigate the dataset (first/last image,
  one image forward/back); claimed in the diffraction view's keyPressEvent
  before QGraphicsView's default handling, which otherwise eats them to
  scroll the viewport.
* Alt+wheel dataset navigation is removed - some window managers already
  took it before the app ever saw it, per the caveat that used to sit next
  to its entry in the shortcuts list.
* The image strip dock and its dedicated thumbnail-rendering machinery in
  the reading worker (SetThumbnail*, RenderThumbnail_i) are removed; it cost
  a lot and nothing else used any of it.
* Toolbar text: "Colour" -> "Color".
* Color map, Auto and HDR mode now persist across sessions, the same way the
  window layout already does.
* The Inspector's "Dataset:" path wraps at '/' (a zero-width space after
  each one) instead of being cut off when it doesn't fit one line.
* LoadFile no longer reopens an already-open file - it forwards straight to
  LoadImage instead - which was the likely cause of a D-Bus client's per-image
  navigation lagging behind the same navigation done via the grid-scan hover.
* New file-manager dock (left, tabbed with Settings via tabifyDockWidget):
  a directory tree filtered to *_master.h5/*_process.h5, rooted at
  JUNGFRAUJOCH_DATA_ROOT if set, else the last-browsed root, else the home
  directory. Most beamline users care about opening images, not
  reprocessing settings, so it's the tab raised by default. Its filter box
  narrows files only - a directory always passes - because filtering the
  directories too hid the matching files under any directory whose own name
  did not match, and hid the root's ancestors, which left the tree with no
  valid root index until the root was set again by hand.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PogHY7bWXV4bpctDPdyuDN
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 18:45:20 +02:00
leonarski_f 40dd6f25fd release: version 1.0.0-rc.169
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 9m39s
Build Packages / build:windows:nocuda (push) Successful in 17m21s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 18m31s
Build Packages / build:windows:cuda (push) Successful in 20m26s
Build Packages / build:viewer-tgz:cpu (push) Successful in 22m32s
Build Packages / build:viewer-tgz:cuda (push) Successful in 22m51s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 23m1s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 27m58s
Build Packages / build:rugnux:windows (push) Successful in 11m10s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 28m43s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 19m44s
Build Packages / build:rpm (rocky9) (push) Successful in 23m4s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 28m23s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 24m37s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 23m9s
Build Packages / build:rpm (rocky8) (push) Successful in 29m39s
Build Packages / Generate python client (push) Successful in 38s
Build Packages / Create release (push) Skipped
Build Packages / Build documentation (push) Successful in 1m10s
Build Packages / DIALS test (push) Successful in 25m36s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 28m18s
Build Packages / XDS test (durin plugin) (push) Successful in 10m24s
Build Packages / XDS test (neggia plugin) (push) Successful in 9m40s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 10m17s
Build Packages / Unit tests (push) Successful in 1h27m12s
2026-09-10 17:26:55 +02:00
leonarski_f 92c441d4f2 space group: the operators the data confirm are asked whether they are a group
Stage A admits a point group only when every one of its operators clears the
correlation bar, so admission is a conjunction over an enumerated list: one
operator reading low deletes the group, and the operators that did clear the bar
can then generate a group no candidate names. That is a contradiction in the
operator table, not a low symmetry. The generated group is now offered as a
candidate as well, and judged by the same chi^2, systematic-b, H and
added-operator-R tests as every other; like a widened rung it does not set
chi2_ref and is not a parent, so offering it cannot make another promotion
harder. Exactly the generated group, never a supergroup of it - proposing the
smallest candidate that merely CONTAINS the generated set was measured and
over-calls where only one operator is confirmed.

Measured on a tetragonal crystal recorded in two unequally exposed arcs, where
the search merge is scaled in P1 and no P1 operator ties the two halves
together: the four operators that exchange h and k read 0.11-0.14 against a bar
of 0.30 while the five that do not read 0.28-0.82 and generate all of 422. The
run adopted an orthorhombic subgroup and wrote 0.98 A of unusable data; it now
adopts the tetragonal group and writes 1.51 A at CC1/2 0.96 and 99.7 per cent
completeness.

A measured pseudo-translation on an axial row no longer revokes the screw
deferral by itself. It is already paid for quantitatively - the absent class is
divided by the modulation depth before either the evidence or the violation
count reads it - and revoking the deferral as well charges one measurement
twice, categorically, on an estimate that moves with anything upstream that
changes which reflections enter the cone. Masking a beam-stop arm, 2.3 per cent
of the detector, took one row's depth across the bound and cost an orthorhombic
crystal both of its screws while its zone evidence rose. The per-reflection rate
test keeps the deferral honest where a modulation really does hide a row.

No threshold changed value. Over 146 datasets - 100 open, 32 in-house, 14
private - no row's space group or cell moves except the crystal above.
2026-09-10 16:30:33 +02:00