v1.0.0-rc.186 #97

Merged
leonarski_f merged 119 commits from rc186-merge2 into main 2026-11-13 20:13:44 +01:00
Owner
  • jfjoch_broker (experimental): Live Rugnux covers serial collections from DECTRIS detectors; a rotation collection too large for the image buffer falls back to standard per-image processing with a warning, and a stream that ends without its end message finishes the collection instead of hanging it.
  • jfjoch_broker (experimental): Live Rugnux settings to build the solvent during refinement and to process a rotation collection as stills, a Clear action for the last result, a _report.txt for live rotation runs, R-free reported for the refined model, and ice-ring detection on by default for rotation as in the command line.
  • jfjoch_broker (experimental): More complete and reproducible live serial merges, analysed for twinning, pseudo-translation and anisotropy, with merging statistics by resolution shell in the live status.
  • jfjoch_writer: Live results with large models are written reliably; update jfjoch_writer together with jfjoch_broker.
  • Rugnux: Several masters can be processed together with --force-still, including grid scans; more robust and faster merging of several sweeps; improved twinning, pseudo-translation, anisotropy, anomalous-signal and space-group decisions.
  • Rugnux: Molecular replacement places the copies of a multi-copy solution together; the pseudo-translation warning quotes the Patterson peak and is not raised once a model is placed; the report advises on extending the rotation or reorienting the crystal where the coverage would gain from it.
  • Web frontend: Light and dark modes, a phone layout, a hideable page list, a redesigned Rugnux live page with step-through of the Fo-Fc peaks in the structure view.
  • jfjoch_viewer: A broker URL given on the command line follows the running collection.
* jfjoch_broker (experimental): Live Rugnux covers serial collections from DECTRIS detectors; a rotation collection too large for the image buffer falls back to standard per-image processing with a warning, and a stream that ends without its end message finishes the collection instead of hanging it. * jfjoch_broker (experimental): Live Rugnux settings to build the solvent during refinement and to process a rotation collection as stills, a Clear action for the last result, a `_report.txt` for live rotation runs, R-free reported for the refined model, and ice-ring detection on by default for rotation as in the command line. * jfjoch_broker (experimental): More complete and reproducible live serial merges, analysed for twinning, pseudo-translation and anisotropy, with merging statistics by resolution shell in the live status. * jfjoch_writer: Live results with large models are written reliably; update jfjoch_writer together with jfjoch_broker. * Rugnux: Several masters can be processed together with `--force-still`, including grid scans; more robust and faster merging of several sweeps; improved twinning, pseudo-translation, anisotropy, anomalous-signal and space-group decisions. * Rugnux: Molecular replacement places the copies of a multi-copy solution together; the pseudo-translation warning quotes the Patterson peak and is not raised once a model is placed; the report advises on extending the rotation or reorienting the crystal where the coverage would gain from it. * Web frontend: Light and dark modes, a phone layout, a hideable page list, a redesigned Rugnux live page with step-through of the Fo-Fc peaks in the structure view. * jfjoch_viewer: A broker URL given on the command line follows the running collection.
leonarski_f added 119 commits 2026-11-13 20:13:28 +01:00
The 44-character table is read on the single Niggli-reduced cell. A lattice whose
shortest vectors are degenerate has several reduced cells: an F-centred cubic one
reduces to the 60/60/60 rhombohedral cell or to a 60/60/90 one (also the reduced
cell of an I-tetragonal lattice with c = a*sqrt(2)) according to the sign of its
strain, and only the first carries character 1. On an F-cubic small-molecule
crystal the indexing therefore named the lattice I-tetragonal, the space-group
search never got the cubic operators, -C with the cubic cell ended in P1 and
-S Fm-3m was refused as "no setting of that lattice carries the group's".

LatticeSearch now runs LePageLattice (basis-independent, already used by the
space-group stage) on the same lattice and takes its class where it is richer
than the table's and its conventional cell meets that class's metric within the
caller's own length and angle tolerances (obliquity at most min(angle tolerance,
1 deg)). A tetragonal or cubic class refused only on its length equality is
returned as the orthorhombic lattice of the same centring, so the demoted
hypothesis (dist_tolerance 0) gets oF/oI/oP rather than triclinic.

Measured on synthetic lattices, 300 per Bravais type, strain 0.05-0.5%: cF is
now right 296-300/300 (was 155-159); hP 291-300 (105-108); oC, oI, tI, hR also
gain, the monoclinic under-calls of centred orthorhombic lattices mostly go.
Costs: a few more promotions within the existing tolerances (mP -> oC 39 -> 50 of
300), which the first pass's constrained-vs-free guard and the metric-symmetry
check judge on the data.

ci tier vs the rc184 full run: 29/29 pass, the null passes, 27 sets identical;
myob_x10sa changes slightly (its first-pass candidate is pseudo-oC at 0.3 deg
obliquity, now promoted and then dropped by the guard; P21, R_free 0.246 ->
0.245), 6iu5 d_min 2.12 -> 2.11.

LatticePointGroupOrder moves from RotationIndexer to LatticeSearch; the class
metric departure is shared with MetricDeviation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Where the file's centre and the measured one index lattices whose primitive
volumes differ by an integer factor (2-4), the pooled excess over chance decided
between them, and where it did not prefer the measured centre the file's was kept
outright. On a harmonic pair that excess is often a near tie - the larger cell's
extra nodes catch the spots a wrong centre displaces and its own null grows with
them. Measured on an F-cubic small-molecule crystal whose file centre sits 4 px
out across the spindle: 12.8x over chance on the doubled cell at the file's
centre against 13.9x on the true cell at the measured one; the file's centre was
kept, the first pass fell to the triclinic cell, the distance refined 2.9 mm long
and the run ended on a cell 1.6 % long.

Now, when the pooled spots do not adopt the measured centre, it is carried as
beam_center_alternative_ - the mechanism the same-cell case already uses - and
RunAllPasses runs the first pass again there and adopts whichever arm merges
better. On that crystal: CC1/2 before corrections 0.913 at the measured centre
against 0.564, cubic F at the true cell; R_meas 16.2 %, CC1/2 0.998.

Costs one more first pass on runs whose two centres give a harmonic pair.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Where the file's centre and the background-walk centre index lattices whose
primitive volumes differ by an integer factor (2-4), the decision was made on the
pooled excess of spots over chance - a comparison of two working indexings, which
on a harmonic pair can favour the wrong centre: on the battery it chose the
centre holding the larger, false cell in 3 of 9 such runs, and on an F-centred
cubic small-molecule crystal the file centre (4 px out across the spindle) was
kept, the first pass fell to triclinic and the cell ended 1.6 % long.

The pre-scan already measures the centre a second way, as the image's strongest
point-symmetry centre, and only stills used it. Against each run's own final
refined beam over the rotation runs of the battery its median error is 0.26 px
(walk 1.17, file 2.89), and "the centre nearer the capture" picks the better of
file and walk in 167 of 180 runs where they differ. It is now asked first on a
harmonic pair: where its margin is at least 1.5 % (below ~1 % its surface is flat
and it is uninformative) the centre nearer it is taken with its cell; the long-
axis and sub-lattice hypotheses then test that cell on its own spots as before.
Without an informative capture the pooled spots decide, and where they keep the
file's centre the measured one is still carried to the merge (previous commit).

Forced-centre runs of the ten harmonic-pair sets: the measured centre is the
one that merges in five (four collapse at the file's centre: ISa 0.6-2.6 against
13-31), the file's in two - one where the capture is uninformative, unchanged, and
one unscored P1 set that merges poorly at either centre, where the capture now
takes the measured one - and the centre makes no difference in the rest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Taken from rc186 (merge 2250e4019, net of 7f2ebd820) as the stream receivers need them:
- ImagePuller::GetDroppedImages (15266b21e), and the puller comments as rc186 has them
  (0d26ecd13, 3047ccd13); ReceiveRingBuffer's mutex renamed (0d26ecd13).
- ImageBuffer::GetImageSlot(keep_free), for JFJochReceiverRugnuxStills, and the unused slots_idle
  counter dropped (8bad85f9f, b0db94658). The default keeps every existing call as it was.
- MXAnalysisWithoutFPGA::AnalyzeStreamImage, which keeps the saturated and error counts a stream
  states (4c3a7852e).
- DarkMaskAnalysis::AnalyzeImage takes its buffer by reference (5d1fb1a48).
- The device memory the indexers hold: Indexer/FFTIndexerGPU/IndexerThreadPool::DeviceBytes
  (0df766328), read by the live stills model step.

Left out: nothing of these files; their FPGA-side callers keep their calls and behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Taken from rc186 (270ae432d, net of merge 2250e4019): rugnux/ModelStep, the rotation pipeline's
model validation and refinement calls moved onto its parts (RugnuxScaleMerge.cpp), StillsLiveMerge
::SpaceGroup, SecondsSince in common/time_utc.h, and the two ModelStep cases of
ModelValidationTest. 270ae432d states rotation output with a model is unchanged by the move
(identical maps, models and blobs end to end).

Left out: nothing of these files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
rc186 reworks StillsGPUAnalysis for the stream receivers (3b09e721a: a sink instead of four
callbacks, no bounded Hold, a model step, every frame integrated) and takes live stills out of
JFJochReceiverFPGA (8bad85f9f). On this branch the FPGA path stays as it is at rc.185, so its
analysis is kept unchanged under a name of its own: the class, its tests and the replay tool are
renamed, nothing else. The rc186 StillsGPUAnalysis comes with the stream receivers in the next
commit. The FPGA-side step that follows later removes this copy with the FPGA live stills.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The stream-receiver side of rc186 (merge 2250e4019, net of 7f2ebd820), for testing on a DECTRIS
system first:
- JFJochReceiverStream, one parent for the receivers that take a collection from an ImagePuller
  (d87508c6b, c2a20a222, 659b5da52, 16f3b0270, 3047ccd13, 0d26ecd13).
- JFJochReceiverLite on it: indexing only, no integration (5f8e26e3d, d279827a8), its own indexer
  (97cfae1fd), its thread-count rule (89fb963bd), the stream's saturated/error counts kept
  (4c3a7852e), stream calibration messages no longer forwarded (25ac0071b).
- JFJochReceiverDarkMask for the DECTRIS mask sequence, sending nothing on the preview and
  metadata sockets (5d1fb1a48, 72310c2ee).
- JFJochReceiverRugnuxRotation and JFJochReceiverRugnuxStills replace JFJochReceiverRugnux
  (1ea363876, 8bad85f9f, af5850090): DECTRIS serial collections get the live stills analysis; a
  cancelled rotation run reports CANCELLED (8f3ba8d38); live stills integrate every frame
  (11eb76093).
- StillsGPUAnalysis as rc186 has it (3b09e721a, 270ae432d), with StillsPrePass, RugnuxLiveStatus
  (CANCELLED, steps capped after a cancel, LiveModel moved here) and RugnuxStructure (shared END
  helpers) - 8f3ba8d38, 270ae432d, 0d26ecd13.
- JFJochReceiver base: one send block, CountImage/StampImage/AddToAverages/ForwardImage for the
  stream receivers (c2a20a222), images_dropped (15266b21e), the indexer moved out of the base into
  the receivers that index (97cfae1fd); dead members removed (51b2ee880, receiver side).
  rc.185's "a given cell is what the master file records" (2b284e11b) is kept, now in
  AddIndexingToEndMessage.
- JFJochReceiverService: the DECTRIS dispatch of rc186 - dark mask, rotation, stills, Lite - and a
  rotation run the image buffer cannot hold gets the per-image analysis with a warning
  (2978a9f92, 29afe06a7).
- Tests: JFJochReceiverRugnuxTest (59a0afaf8, 1a39d6f29 and the receivers' own), the rc186
  StillsGPUAnalysisTest and RugnuxLiveStatusTest; the replay tool on the new StillsGPUAnalysis.

The FPGA path is left as at rc.185: JFJochReceiverFPGA still takes every PSI run (pedestal
included) and its live stills (StillsGPUAnalysisFPGA). Its only changes are the ones the base move
forces, as 97cfae1fd makes them: it owns its IndexAndRefine, built with the same arguments the
base used, and adds the consensus cell and indexer errors through AddIndexingToEndMessage before
its live stills overwrite the cell, as before.

Left out (FPGA side, a later step): FPGAImagePuller, JFJochReceiverPedestal, MockAcquisitionDevice
and the tests on it, "FPGA receiver: index only" (68ec8a7f9), MXAnalysisAfterFPGA/ImageMetadata
cleanups (b0db94658, 51b2ee880 FPGA part), the internal-generator memory check (25542a9af),
JFJochReceiverService::UseFPGAImagePuller, the JFJochHLSSimulation link removal, and
JFJochReceiverFPGA_RugnuxOnStaysPerImage (rugnux live still analyses FPGA serial runs here).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
From rc186 b86b99691 and 8f3ba8d38: the verdict's description lists CANCELLED, in
jfjoch_api.yaml and, as the generator writes them, broker/gen/model/Rugnux_live_status.h,
frontend/src/client/{types,zod}.gen.ts and docs/python_client/docs/RugnuxLiveStatus.md. The
frontend shows a CANCELLED verdict as info, and for serial data shows the model section while the
collection runs (RugnuxParts/RugnuxPage/RugnuxStills.tsx as in rc186).

Adapted, not copied: rc186's rugnux_settings text says the FPGA workflow is not affected by the
mode, which is not true here - JFJochReceiverFPGA still analyses serial runs live. The descriptions
of rugnux_settings, its `enable` and rugnux_live_stills say instead that serial collections from a
DECTRIS detector or converted by the FPGA are analysed; the generated files carry the same text
(edited by hand, as only comments change; VERSION stays 1.0.0-rc.185). The frontend switch reads
"Process collections with rugnux live".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
From rc186 2feef4b02 (JFJOCH_BROKER.md, IMAGE_STREAM.md), for what this branch has: the table of
the receiver each run gets, the DECTRIS serial live mode (pre-pass, analysis, live merge, model
step, CANCELLED after a cancel, a rotation run the buffer cannot hold getting the per-image
analysis), no calibration forwarded from a DECTRIS stream, nothing on the preview and metadata
streams for the DECTRIS mask sequence.

Adapted: JFJochReceiverFPGA takes every PSI run, pedestal included, and still analyses serial runs
live with rugnux on; a paragraph says how that differs (frames wait in half the image buffer, no
model step). Left out: the broker_pipeline figure (it shows JFJochReceiverPedestal and the FPGA
receiver without live stills), the internal-generator memory check (25542a9af, not ported) and
docs/TESTS.md (the card mock, not ported).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
DiscardSlot (ZeroCopyReturnValue::discard) returns a slot that is in
preparation or already readable (Sending) to the free list and clears its
image number, so that no request for that image finds it again; ReleaseSlot
keeps the number, and an idle copy could shadow a newer slot carrying the same
image. A held stills frame now goes back this way. For a frame still in
preparation (the FPGA path) the outcome is what release() did with the number
set to -1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Until now a frame held for the pre-pass sat as raw bytes in a slot in
preparation, which the image buffer does not serve, and the start message was
saved only after the pre-pass: /image_buffer/* answered 404 and jfjoch_viewer
and the web preview showed nothing for the whole pre-pass.

Each held frame is now put in its slot as an image message without analysis
(the image appended after the metadata, the analysis reads it there in place)
and marked ready, but not sent: the writer and the ZeroMQ streams still get
each image once, after its analysis. A start message on the stated geometry is
saved to the image buffer at the start (not sent anywhere); the one after the
pre-pass replaces it. The analysed copy is put in its own slot before the held
one is discarded, so a reader sees the image without or with its analysis,
never neither, and afterwards only the analysed one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
jfjoch_viewer follows a collection by the image buffer's change counter
(/image_buffer/status current_counter): only when it moves does it read the
start message, the plots and the newest image again. Rugnux fills the plots and
the scan result from its canonical pass, which runs behind the images and mostly
after the last one, when the counter no longer moves - so the viewer kept the
empty plots of the collection for good (seen with a broker on a replayed
16M rotation: plot.bin answered with 150 entries while the counter stood still).

ImageBuffer::ResultsChanged counts a change of the results kept beside the
images as a change of the buffer; the rotation receiver calls it for every
image of the canonical pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The constructor started the receive and CBOR threads before it set the socket options and
connected. Options were set and the endpoint connected while PullerThread was already receiving
on the same socket (ZeroMQ sockets are not thread-safe, and only Receive/Send take the wrapper's
lock), and a throwing Connect or repub Bind - a malformed endpoint, a busy repub port - unwound
the constructor with joinable std::thread members, so the process called std::terminate: in the
broker that is a crash in JFJochServices::On instead of an initialisation error.

The sockets are now set up first and the threads started after them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Suspend() only set a flag that the CBOR thread tests before its blocking put. When a run ends
while the detector still streams, outside_fifo fills (1000 messages) before the suspend, and the
CBOR thread stays parked in PutBlocking with a frame that already passed the check; cbor_fifo,
the socket and the sender's queue fill behind it. Nothing moved until ResumeAndClear at the next
Start: it cleared the queue, which woke the parked put, and then everything still queued behind
it - frames, an END, and a START of a series that no receiver took - flowed into the new run.
The receiver drops everything before a START, but a stale START is taken as the new run's own.
The test sends 2000 images, suspends, sends a START, waits 2 s and resumes: 1001 stale messages,
the START among them, reached the next run; now none do. It also kept up to ~1200 frames (GBs on
a 16M) in memory between runs.

Suspend() now clears outside_fifo, which releases the parked put, and from then on every message
is dropped as it arrives. The broker suspends the puller also when the receiver's Stop ended in
an error, and after a failed detector start, where it did not before - in both cases the stream
was left with nobody consuming it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Image threads left PollImages only on a cancel or on the END message. The silent cancel the
broker sends once the detector reads idle bounded only the wait for START, so an END that did not
deserialise (the CBOR thread logs and drops it) or a series the detector ended without one kept
the run in Measuring - JFJochServices::Stop waiting in receiver->Stop() - until an operator
cancelled it. The test feeds START and five images, no END, and reports the detector finished:
Stop did not return within 60 s; now it returns after the grace with all five images written and
the run not cancelled.

Once the detector has finished, a stream silent for 10 s (counted from the later of the last
message and the detector finishing, so a detector still draining its buffer is not cut) is taken
as the end: the error is logged once and the collection finishes with the images it has, so
missing images show as efficiency below 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
A small-molecule standard measured at X10SA (EIGER2 16M, 20 keV, 360 deg in 0.2 deg frames,
100 K): a tiny cubic F cell (a 5.60 A) in a crystal of several grains 5-10 deg apart, with
99% of the spots on powder rings and strong extinction.

- inhouse.json: the row, with the XDS reference read by `battery.py refs --write` from the
  CORRECT.LP beside the data (F 4 3 2, a 5.608, ISa 2.5, R_meas 0.38, CC1/2 0.97, to 0.78 A,
  Friedel pairs merged). XDS indexed only from its 150 strongest spots with the cell and
  F 4 3 2 given, and integrated with DELPHI=10; README_XDS beside the data records the steps.
  ref_override F m -3 m (225) from COD 2108652 (Tsirelson & Stash, Acta Cryst. B76 (2020)
  769-778), which is also the structure the SHELXL check refines.
- smoke tier: a known hard case - rc184 named the lattice tI 3.96/5.60 and merged in I422
  (sym_under); the current branch merges in F 4 3 2, which passes as an
  indistinguishable_alternative of F m -3 m (SHELXL R1 0.072 against the COD structure).
- README: NaCl named in the Arms table, smoke count 22; doc_check maps the id prefix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
StillsLiveResult makes the ProcessResult the report is read from out of a
merge of StillsLiveMerge: cell and group merged in, the merge's statistics
and error model, frames analysed and indexed. ProcessResult::powder_measured
(true everywhere else) lets that result say powder rings were not looked
for, so the SUMMARY lists Powder as not measured instead of "no rings
detected". PathologyIsSerious names the codes behind the FAILED/UNUSABLE
verdicts (NO_LATTICE, UNUSABLE_MERGE).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
GET /rugnux/status gains an optional `triage` array (rugnux_triage_item:
label, codes, severity ok|attention|serious|not_measured, text, detail): the
SUMMARY checklist of _report.txt (ReportDocument::checks) line for line,
coloured from the same flags - absent = ok, fired = attention, or serious
for NO_LATTICE/UNUSABLE_MERGE, undetermined = not_measured. Rotation sets it
with the verdict when the run is over (the CANCELLED line in the words the
live warnings use); serial at every provisional merge and at the end, from
BuildReportDocument on that merge (StillsLiveResult).

Tests: the live list equals the report's checks; an unusable merge is
serious; a live serial merge leaves powder/ice/twinning/tNCS/anisotropy
not measured; OpenAPI conversion; receiver rotation and stills runs carry it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
RugnuxTriage (self-contained, takes the status' triage array): conditions
that fired one per line (red, then orange) with the report's words, the
fine ones in packed green columns, the unmeasured ones named in one grey
line; the report's warnings and codes on hover. Mounted under the probe
alert in RugnuxPage; client regenerated from the API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The placed, scaled model's R-free is now also formed per shell of the merge
statistics (report_shell_d_min), over the free reflections of each shell and
with the same Fmodel the overall R-free uses. The report's CC(model, data)
table gains an R_FREE column (a dash where a shell has no free reflection).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The live status carries the shell table of the latest merge (RugnuxLiveShell:
d range, N_obs, N_unique, completeness, multiplicity, <I/sigma>, R_meas on
rotation, CC1/2, CCref, R_split on stills, R-free of the placed model), filled
by LiveShells from the same MergeStatistics the report prints, with the same
rows. Rotation sets it when the run is over; stills at each provisional merge
and at the end, with the R-free added once the model step on that same merge
returns. A quantity the run did not measure is left unset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
New optional rugnux_live_shell schema and the rugnux_live_status properties
shells / shells_merge (back-compatible). Regenerated the C++ server model
(openapi-generator 7.20.0), the TypeScript client, the python client docs and
the Redoc page; OpenAPIConvert fills them from the live status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Four panels - multiplicity, <I/sigma>, CC1/2 with CCref, and R_meas / R_split /
R-free - drawn at 1/d^2 of each shell's d_min with the axis labelled in d,
updating with the live status. A quantity the run did not measure has no line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Three warnings and one report line read wrongly on a tiny high-symmetry cell with a few dozen unique
reflections (a cubic F cell of ~44 A^3 primitive volume, 22 unique in its Laue class):

- The twinning statistics were read from one L-test pair and one second-moment reflection (<|L|> = 0.000,
  <I^2>/<I>^2 = 1.000) and the run was warned that its point group was "promoted although the intensities
  read at least as twin-like as a perfect twin's". <|L|> over N pairs is known to 0.29/sqrt(N), so below
  about 50 pairs neither statistic separates the untwinned 0.5 from a perfect twin's 0.375: the verdict is
  now NOT_READABLE below 50 pairs and 50 reflections (MIN_TWIN_STATISTIC_PAIRS), the prose says so, and no
  warning is read from them. The promoted-group warning rests on the final merge's own L-test, the one it
  quotes, not on the pre-search merge's.
- The POWDER_RINGS warning named "a crystalline phase other than the crystal" for rings that all lie at the
  indexed cell's own d-spacings - a tiny cell puts every reflection of the crystal, of its other grains and of
  any powder of the same phase on a few d-spacings. POWDER_RINGS_ON_OWN_LATTICE counts the rings within 1% of a
  d-spacing the lattice (with its centring) diffracts at, POWDER_RINGS_LATTICE_COVERAGE says how much of the
  rings' d range those d-spacings cover at that tolerance (an ordinary small-molecule cell: ~0.8-1, every
  ring matches by construction; the cubic salt: 0.28), and where at least 3 and 90% of the rings match at a
  coverage under 0.5 the warning names the crystal's own phase (more crystals, not a different one). The flag
  itself is unchanged.
- A Sohncke group is written whenever nothing extinguished demands more, and the non-Sohncke groups with the
  same absences were never named anywhere (SPACE_GROUP_ALTERNATIVES stays the search's own list, which the
  battery reads). SPACE_GROUP_ACHIRAL_ALTERNATIVES now lists them, read off the adopted group itself (same
  Laue class, centring and absences over an hkl box): F432 -> F-43m | Fm-3m, the group a salt or a small
  molecule is refined in.

Report only: no decision changes. docs/RUGNUX_REPORT.md describes the three.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
A small-molecule set's shx_* numbers only say something against what another program makes of the
same frames. shelx_check.py --xds <XDS_ASCII.HKL> <COD id> writes the p.hkl/p_report.txt the check
reads from CORRECT's corrected unmerged intensities (misfits with a negative sigma dropped, as XDSCONV
drops them; scaled by a power of ten where the strongest would overflow HKLF 4's columns) and runs the
unchanged recipe on them. On the in-house small molecules this puts rugnux level with or ahead of XDS on
the organics (R1 0.031-0.038 both) and behind it on the two extinction-dominated inorganic crystals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
`jfjoch_viewer http://<broker>` opened image 0, and for an HTTP source any image number
>= 0 sets the autoload mode to None (JFJochImageReadingWorker::LoadFile_i), so the viewer
showed the first image and never updated - no new images, plots or image count, with every
receiver. The URL dialog already asks for image -1 (follow live); the command line now does
the same for http:// and https://, and still opens a file at its first image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The live status carried the per-shell table as numbers but the whole-merge completeness,
multiplicity, I/sigma, CC1/2, R_meas/R_split and CCref only as prose in the summary rows, so the
frontend had to parse text for its headline numbers. `overall` (a `rugnux_live_shell`, optional,
present with `shells`) is now the "Overall" row of the same table, converted by the same code as the
shell rows (LiveShell), with the overall R-free of the model validated on that merge added where
the per-shell R-free is. Rotation sets it with the merge of the collection, serial at each
provisional merge and at the end. The test compares it with the "Overall" line `_report.txt` prints.

Regenerated: broker/gen (Rugnux_live_status), the frontend client and the python client docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The live page no longer scrolls on a desktop screen: a dark stage the height of the window
holds, top to bottom, the phase (collecting, processing, done with the verdict's colour) with
the two progress bars - the detector's images against the collection and the step rugnux is on
against its own pass - and the clock the run is judged by (since the first image while it runs,
last image to all written when it is over, with the deadline counting down in between); one row
of eight hero numbers (resolution, CC1/2, I/sigma, completeness, R_meas or R_split, ISa or the
serial indexing rate, space group with the cell, the model's R-free); and the stage proper - the
placed model in Mol* on the left, filling its panel, the triage list and the merging statistics
by shell on the right. While there is no model the left half lists the run's steps with the
running one's progress. Serial data put the pre-pass's decisions where rotation data put the
lattice probe. The hero numbers come out of the summary's Signal and Completeness rows, which
is where the status carries them.

The timeline, the steps with their notes, the summary rows, the warnings, the model scores and
the serial pre-pass geometry move under a "Details" fold below the stage, on the page's light
background; the settings stay below that. The shell plots take the colours of the enclosing
theme and a height, the structure viewer can fill its parent, and the wide page may use up to
3400 px so a 4k screen is not left half empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The structure viewer's side panel becomes a toolbar laid over the top of the canvas: the representation
and waters as pills, each map as a chip showing its colour and level that opens the switch and the
sigma slider in a popover, the unexplained blobs as one toggle, and the mouse controls behind a "?".
The molecule has the whole panel.

The triage keeps a line only for a condition that needs attention; the ones looked for and clean are
badges with their names (the report's words on hover), the unmeasured ones greyed among them.

The stage drops its nested cards for hairline divisions: the phase word with the two progress bars
side by side under it, thin, and the clock at the right; the hero numbers as a rail down the left
edge, the model (or the run's steps, the running one large with its progress) filling the centre, the
triage over the shell plots at the right. The stage's top edge carries the phase's colour. The shell
plots' type and tick count follow the panel's size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The stage says nothing in sentences any more: the phase word stands alone (the run's message only where
it failed), the bars carry counts and the step's title, the clock its caption, the hero tiles a one-word
caption and the number, the triage a chip per condition that fired and a count of the clean and the
unmeasured ones, the model panel "Model · refined". The verdict, the lattice probe, the step notes and
every explanation are in the tooltips or under Details.

The hero reads the whole-merge statistics (d_min, CC1/2, I/sigma, completeness, R_meas or R_split,
multiplicity) from the status's `overall` row where the broker sends one, and only otherwise out of the
summary's words; multiplicity joins the tiles.

The structure viewer is a glance at the density, not a viewer to work in: cartoon, 2mFo-DFc at 1 sigma,
mFo-DFc at +-3 sigma, the anomalous map where there is signal, and one toggle for the difference map.
The representation, waters, blob and level controls and the mouse legend are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The detector's and rugnux's progress stand one under the other again, on one axis of seconds since the
first image: the detector's bar fills as images arrive up to the collection's expected end; rugnux's bar
is its steps, one segment each as wide as the step took, the running one growing to now, titled where
the segment is wide enough and on hover otherwise. The last image and the deadline are lines across both
bars. A strip under the bars names every step with its duration (the running one with its progress), so
what takes how long is read at a glance during the run and after it.

The triage panel loses its lead line; the chips stand alone. The hero reads the whole-merge statistics
from the status's `overall` row now that the API carries it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The step segments on rugnux's bar and the chips under it carry a tag of a few letters for the kind of step
(Pre, Spots, Idx, Int, Scale, MR, Ref, Out, ...), from a small table on the step's title - a title the table
does not know gives its first word - and the full title on hover; no counts, no pass numbers. Every kind
has a colour of its own (Okabe-Ito, the blue lightened for the dark stage; a kind outside the table takes
the next spare one), the same on every pass and on its chip; consecutive passes of one kind are one chip
with their time together and, on the bar, segments parted by a thin divider with the tag on the first.
The centre panel shows only the step rugnux is on while there is no model; the steps behind it are on the
strip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
A merge on a centred cell that holds none of the reflections the centring
extinguishes cannot test it: the centred group's absent class is empty
(centering_untested), it ties with the primitive group on the same cell
on every absence, and the lower number won - P2 on a C-centred cell,
with half the reflections "missing" (completeness ~47%). This is what
every joint merge of sweeps on a C-centred lattice did: each sweep's own
merge dropped its centring absences, so the joint merge never saw them.

On that tie the lattice's own centring, decided where those reflections
were integrated, is preferred over the number. The primitive group stays
among the alternatives the data cannot separate. Test: a C2 merge with
its centring-absent class removed, on a C lattice, comes out C 1 2 1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Since LatticeSearch takes the Bravais class from the metric two-folds (LePage)
where the 44-character table misses it, a metrically hexagonal cell that the
table read as its ortho-hexagonal C cell comes back as hP in LePage's
conventional basis - chosen from the strained cell, so one of the twelve
equivalent hexagonal settings, and not necessarily the same one in the two
passes. Before, the rotation indexer re-expressed that C cell itself, always
from the reduced cell (HexagonalConventional).

The rotation path is not invariant under that choice. The refinement and
indexed-fraction gates measure the miss in fractional Miller units with an
identity metric, which for a and b at 120 degrees changes with the setting, and
the two-pass quality guard counts h00/0k0/00l rows of each pass's P1 merge,
which are not the same rows in two settings. Measured:
- six equivalent settings of one hexagonal small-molecule crystal (same binary,
  basis relabelled by a 622 operation) gave SHELXL R1 0.097-0.144 and a
  first-pass beam centre spread of 2.4 px;
- on a P6122 protein set the refined pass's search merge counted 0 low-order
  axial reflections against the header pass's 28 in the LePage setting (28
  against 28 in the reduced-cell one), the guard reverted to the header
  distance and the cell came out 0.44 % short (0.06 % before).

A primitive hexagonal result is now written with HexagonalConventional of its
reduced cell, whichever route named the class; the class itself still comes
from LePage, so its gains (cubic F, the other classes, stills) stay.

Subset vs the rc185 build (the commit's parent): the nine hexagonal and
trigonal rotation sets and the in-house small molecules are back to their
rc185 numbers (two hexagonal small-molecule sets SHELXL R1 0.110 -> 0.097 and
0.129 -> 0.116; ISa and cell recovered on 6pb3, 8a1a, 8v4o); the tetragonal
small-molecule cuhf2 still differs (ISa 45.6 -> 41.8, not hexagonal). Against
rc186 without it, the three F-cubic sets of the subset (3inp, 5epe, 6oel) are
unchanged.

The setting dependence itself is not fixed here: an invariant miss metric for
the hexagonal gates and a setting-independent axial-row count would make the
choice irrelevant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
A model placed as given on a free set drawn by the run was flagged on nearly every run with a
model (an orange triage row on 238 of 262 battery rotation sets), although it says nothing about
the data. The warning and its code are gone; the refinement still does not let the as-given model
compete on such a free set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
OperatorScorer rounded each entry of the Miller-index matrix to an integer before
building the gemmi operator, so a pseudo-merohedral law of a centred lattice - whose
entries in the lattice's own basis are halves, such as the hexagonal two-folds of a
C-centred cell with a = sqrt(3) b - was scored as a different operator altogether: on a
crystal twinned by it at about 0.3 its pairs read unrelated (H 0.61, CC 0.02) where
phenix.xtriage reads a Britton fraction of 0.33 for the same law, and the twin was
missed. The entries are now kept in gemmi's 1/DEN units, and ScoreOperator skips an index
whose image under the operator is not integral (the centring-absent class) instead of
letting apply_to_hkl truncate it onto a reflection it does not relate.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
TWIN_LAW= named the first operator the refused group added and quoted beside it the
mean H over all the operators the refusal added, or failing that the L-test's fraction -
neither the headlined law's own number. A 3-fold crystal twinned at 0.4 by one of its
three laws was headlined at 0.20 while TWIN_LAW_1, the same law, read H 0.10, i.e. 0.40
(the deposition's 0.39, xtriage's 0.38). The headline is now the law of the TWIN_LAW_n
list whose pairs agree best, at 0.5 - its H, so the two lines cannot disagree.

The TWIN_LAW_n operators are printed as Miller-index operators (as TWIN_ZONE and TWIN_LAW
print theirs): find_twin_laws returns real-space operations, and the two differ for every
law that is not symmetric - a pseudo-merohedral law of a centred lattice read literally as
hkl relates unrelated reflections.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The L-test and the second moment admitted every resolution shell whose mean I/sigma
reached 1, which includes the noise tail past the fitted resolution. Noise moves <|L|>
the way a twin does: a pair of noisy intensities kept at I > 0 reads below the untwinned
0.5 (simulated on Wilson intensities, 0.474 at sigma/<I> = 0.1, 0.449 at 0.3, 0.437 at
0.5, 0.426 at 1), so five untwinned crystals read 0.413-0.440 over their whole range -
against 0.448-0.492 over the shells phenix.xtriage reads - and were called twins, while
the twinned ones read within 0.01 either way. A shell now enters only when at least 85%
of its reflections are at I/sigma above 3, xtriage's cut for its twin tests; replicated
on the battery's merges the two tools then agree within 0.01.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The small / noticeable / STRONG grade (and the ANISOTROPY warning) was read on the spread of
the directional diffraction limits in A, 0.25 and 0.5 A, so the same anisotropy graded
differently at every resolution: a 0.5 A spread is a 12% loss of resolution at 4 A and a
33% one at 1.5 A, and the battery had 4 A crystals losing 15% of their reach graded STRONG
beside 1 A crystals losing a third of it graded noticeable. The grade is now read on the
ratio of the weakest limit to the best (new key ANISOTROPY_D_MIN_RATIO; 1.125 and 1.25,
the former spreads read at 2 A), and so is the directional hypothesis of the verdict. The
headline prose quotes the factor.

ANISOTROPY_DELTA_B is unchanged. Its difference from phenix.xtriage's B_cart (a quarter
larger on a typical crystal, several-fold on one whose weakest direction stops diffracting
inside the measured range) was traced to xtriage's positivity cut on amplitudes and its
single Wilson curve over the written range - restricting the model-free fall-off of <I> in
cones to I > 0 reproduces xtriage's number, and xtriage's range grows as the data are
truncated at the weak direction's limit - while rugnux's tensor follows the model-free
fall-off; the report and the docs now say what to expect from amplitude-based tools. The
directional limits reproduce the ellipsoids of STARANISO-processed depositions of the same
crystals to about 0.1 A per axis. The log's strongest/weakest factor is capped at 1000x as
the report's key already was.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The row said "no usable signal (CC_anom < 0.15)" on data whose model-phased
anomalous difference map shows the sulfurs at 6-17 sigma, and "signal present"
on data with no anomalous scatterer at all. Over the battery CC_anom tracks the
map poorly (Spearman 0.38 over 237 sets) for two reasons that are properties of
the statistic, reproduced with phenix.merging_statistics on the XDS references:
a systematic Friedel difference (absorption, geometry) reproduces between the
half-sets exactly as an anomalous one does (0.85-0.96 on centrosymmetric small
molecules, where no anomalous difference can exist), and mates measured on
neighbouring images share an error that independent per-hand halves put on
opposite sides of the two differences half the time, never the same side, so
strong systematic-error-dominated data read negative (-0.58 in every low shell
of a set whose map shows 6-9 sigma sulfurs; -0.43 on unmerged XDS data with
random halves, -0.10 with 10-frame-block halves). The overall value also
averages the low-resolution signal away with the outer shells' noise.

With a placed model the map at it now decides, in both directions: its mean
over the model's anomalous scatterers (>= 4 sigma), or its strongest site or
unexplained peak (>= 6 sigma; an iodide soak, a SeMet, one Yb among many
sulfurs) says "signal present", 2.5 / 5 "weak signal", else "no usable
signal", and the row quotes the readings. Without a model the lowest shell's
SigAno decides against its floor sqrt(2/pi) = 0.80 (>= 1.0 present, >= 0.9
weak). Both forms still quote CC_anom. Against the ground truth of an
independent model-phased map (f_calc phases, cctbx) on the 215 battery sets
with a model: before 25/81 strong-map sets called present, 1 false alarm;
after 81/81, 4 false alarms (sets rugnux's own map reads 6-7 sigma where the
independent one reads 3). The model-free leg agrees with xtriage's
measurability > 0.05 on 274/280 sets. The live status's anomalous_signal
follows the row instead of repeating the CC_anom test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
A rotation run now leaves two more things in `GET /rugnux/status` once it is over: `frame_scales`, the
per-image scale factor of the merge (one entry per image, 0 where the image got none), and
`frame_ranges`, the stretches of the sweep the merge downgraded or rejected, each with its disposition
and the reason the sweep-quality diagnostic saw there - which frames rugnux left out, and why. Both
come from what the run already has (the end message's scale vector and SweepQuality's per-image
dispositions, run-length encoded with the reason of the range they lie in); nothing is computed anew.

The C++ model, the Redoc page, the TypeScript client and the python client docs are regenerated from
the YAML. RugnuxLiveFrames_OpenAPI covers the encoding and the conversion.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The app's theme now comes in two modes, both indigo and lime: white by day, a navy stage by night, with
every colour chosen for its mode rather than inverted (theme.ts). The mode follows the system until the
sun/moon button in the top bar sets it; the choice is kept per browser. The success, warning and error
tokens are the three verdict colours the Rugnux live page uses, so the whole app agrees on them, and the
dashboard plots take the theme's colours so they read at night too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The live page follows the app's mode instead of bringing its own dark theme, and shows everything at
once: left to right the hero numbers, the diffraction image the broker is on (the preview JPEG with
spots, beam centre and resolution estimate, polled while images come in; wheel to zoom, three pills:
last/indexed, spots, predictions), the placed model, and the triage badges over the per-shell plots.
Under the image and the model, the collection image by image: spots and background from the receiver's
plots, and the per-image scale with the frames the merge downgraded or rejected as amber and red bands
labelled with the reason (serial data: the indexing rate instead).

livePalette.ts holds the page's colours for both modes, all drawn from the app's indigo and lime: the
two phases, the three verdicts (the theme's own), the kinds of step (Okabe-Ito, apart for colour-blind
readers), the plot traces and the frame bands. The rail, the image and the type of the strip scale with
the screen so a 4k display gets no 11 px labels.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
CCanom read strongly negative on strong rotation data (-58% in a shell at I/sigma 70, the more
negative the stronger the shell) with a model-phased anomalous map of 6-16 sigma at the model's
scatterers. The halves of a Bijvoet pair were dealt out one observation at a time, as the CC1/2
halves are. The two mates of a pair are often measured together - with a symmetry axis along the
spindle the mirror image of a spot across the plane through the beam and the spindle lies on the
same image and is of the opposite hand - and share the image's error, which is as large as the
anomalous difference or larger; split across the halves half of the time, that shared error enters
the two half-set differences with opposite signs. Measured on the unmerged observations: the
residuals of opposite-hand observations of a pair within a few to a hundred frames of each other
correlate at +0.5 to +0.8, same-hand ones at about zero; the same structure is in XDS's own scaled
observations of the same crystals, and a per-observation split of XDS's data goes negative too.

The halves of a pair are now formed from Bijvoet couples - an I(+) with an I(-), the closest in
time first - dealt out whole, alternately in time order (AssignAnomalousHalves): balanced, and the
shared error cancels inside each half's own I(+)-I(-). A synthetic sweep with a planted 5%
difference under an 8% error shared by the mates reports CCanom above 0.5 where it read near zero.
On the rerun subset the strong low-resolution shells go from -24/-22% to +9/-8% (insulin), -16/-14%
to +7/-1% (a 11-sigma map set), 0.10 to 0.30 (a 6.6-sigma one); a 200-image set with no scatterer
drops from +33% to +10% over 105 pairs.

The summary's Anomalous row (decided on the anomalous map since the previous commit) now quotes
CCanom shell by shell from the low-resolution end - the resolution to which it holds above 0.15
(ANOMALOUS_SIGNAL_D_MIN), else the lowest shell's value against its number of pairs - with the
number of pairs behind the overall value (CC_ANOM_PAIRS) and phenix.xtriage's measurability
(ANOM_MEASURABILITY), where it quoted the overall value that the noise shells dilute. The merged
data are unchanged: the halves are a report statistic only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The native-Patterson detector called 7.5-12% peaks that no deposited model has a translation-related
pair for, missed 24-35% peaks that every model has, and on a crystal whose files were put on a model's
axes reported the vector on the axes the group was determined on.

- The gate statistic was the max/min ratio of twelve phase-bin means, owned by its thinnest bin. A
  vector a little off a special position sorts the reflections by the order of h along an axis, and
  spherically normalised E^2 depends on direction under anisotropy, so the ratio read 6.5x where the
  amplitude along the vector was 0.06, and 4x at a C-centred crystal's own centring vector. The gate
  is now the correlation of E^2 with cos 2 pi h.u (the full-data ladder's objective), refined and
  judged against the same search on intensities shuffled within (shell x nearest reciprocal axis)
  bins, in which E^2 is also normalised; the search stays 15 A from every origin-equivalent point.
- The ratio-2.5 gate refused real modulations whose control read 1.3-1.7x. Significance is now five
  spreads of the null, on the acentric reflections beyond 4 A or the 2000 lowest-resolution ones
  where a small cell has fewer - which also ends the INCONCLUSIVE verdict on peptide cells.
- A vector that modulates is a fact about the crystal; the pseudo-translation reported is the one
  molecular replacement has to be told about, which the programs correct for from a Patterson peak
  of a fifth of the origin. TNCS_DETECTED= TRUE needs that height in the 20-5 A band or as the
  modulation's depth over all the data (TNCS_MODULATION_DEPTH_PCT: a half-integer pseudo-centring
  modulating every shell to atomic resolution reads a fraction of its height on a 190-reflection
  band). A smaller modulation is noted (TNCS_MODULATES, TNCS_CORRELATION) and not flagged. A peak of
  that size the data are too few to establish (a 30 A cell has ~150-240 reflections in the band,
  where the permutation floor is itself 12-19%) is carried as TNCS_DETECTED= POSSIBLE, warned under
  PSEUDO_TRANSLATION with the count and the floor, instead of dropped.
- The averaged-classes hypothesis runs beside the vector's wherever the peak is significant and is
  taken where its correlation is the higher, rather than only where the vector's test failed.
- ChangeBasis carries the result's vectors across every relabelling of the output axes.

Over the deposited models of the 200-dataset open corpus (the model's own E^2 Patterson over 20-5 A,
and chain pairs related by a rotation under 10 degrees) the two populations divide at the level
chosen: 18-80% with a pseudo-translation, 4-15% without. Agreement with phenix.xtriage's flag over
the battery: open and in-house 96% -> 97% (the remaining disagreements are three undeclared lattice
translations rugnux reports as the stronger finding, a 12% model peak xtriage calls on a thin band,
and two sets within 1% of the level), private 91% -> 100%.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Two findings from the triage battery, where phenix.xtriage reported ice on sets whose images
show no ring and rugnux reported none on sets whose merges carry a ring-shaped bias.

Ice in the intensities (MeasureIceRingIntensity, on every written merge whatever the image-side
gate found): each hexagonal-ice ring's core and flanks are compared with a Wilson line fitted to
the off-ring reflections within 0.15 A^-1 of the ring, so anisotropy and the curvature of the
Wilson plot cancel - xtriage's z-score of a shell against the whole Wilson curve is what flagged
broad features of the plot on some thirty flat-profile crystals. The rings are fitted together as
one ring system in proportion to ice's line strengths, twice: cores against the line (the size of
the bias, ICE_INTENSITY_BIAS_Z) and cores against their own flanks (ICE_INTENSITY_Z, which
decides): a ring is narrower than the flank offset, a broad shell is not, and the integration can
bend a ring either way (a ring under the spots lifts the cores; a ring in the background annulus
beside them depresses the cores and lifts the flanks - both seen), so the decision is on the
contrast's size at 5 sigma over at least two rings. Reported per ring, carried on the Ice triage
row, and an ICE_RINGS warning where it fires on a run whose images showed no ring. Nothing is set
aside after the fact.

Ice-ring verdicts: a ring whose window had no shell at mean I/sigma above 1 - every ring near the
resolution limit of a weak dataset, and all but the first ring of a 3 A one - got no Wilson line,
no verdict and no log line, and was merged as it was, biased by up to 14x in the cases found.
The band verdict now reads its bands against a line fitted through the window's own equal-count
shells with no I/sigma cut (flat at the window's mean failing that), shared with the measurement
above, and a band is counted and reported whether or not it could be judged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Reverts the Bijvoet-coupled half split (AssignAnomalousHalves), its tests and the resolution limit
built on it (ANOMALOUS_SIGNAL_D_MIN). The coupling removed a real sign bias of CC_anom on strong
data whose Bijvoet mates are recorded together, but measured against the independent model-phased
map over 241 sets the standard lowest-shell CC_anom (Spearman 0.48, AUC 0.83 strong vs none) is
already below measurability (0.60, 0.90) and SigAno (0.50, 0.84), and on the 32 hard sets the
coupled one (0.11, 0.61) stayed below both as well. Nothing decides on CC_anom any more - the
summary's Anomalous row is the anomalous map's - so a reported statistic that is not the standard
AIMLESS/phenix definition buys nothing; the definition stays standard and the bias is explained
in the report prose and the docs instead.

Kept: phenix.xtriage's measurability per shell and overall (ANOM_MEASURABILITY; Zwart 2005
acknowledged), the best of the pair statistics against the map, and CC_ANOM_PAIRS, the number of
pairs behind the correlation, both quoted in the summary's Anomalous row.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The reference sweep was the one whose own merge reached the finest
resolution. A lattice that integrated a few hundred reflections of
noise out to the detector edge reaches the finest resolution of all
with 2% of the data merged, became the reference, and the real sweeps
were then judged against it: they shared too few reflections with it
and were left out, and the joint merge held one lattice of noise. Such
a lattice's error model resolves no ISa, and that is the whole change
to the rule: of the sweeps with a measured ISa, the one reaching the
finest resolution, ties by ISa. Two other merits were tried against the
multi-sweep rows and rejected: a sweep's own CC1/2 saturates on a small
cell (the smallest of seven kappa runs won by noise, and nothing could
be measured against it) and reads precision (a long sweep of a poor
crystal won over the best one); coverage times ISa rewards a sweep
merged in too high a symmetry and a minority twin domain with the
higher ISa.

A doubtful sweep (isomorphism ratio in the band, or not measurable) was
kept only where the held-out residual of the merge was shown not to
rise (two standard errors up), or where it added coverage. On a cell of
a few dozen unique reflections no statistic over reflections resolves
anything, so a second sweep of the same crystal was left out on a
residual change of +1.6% +- 1.3% - and a merge of both was clearly the
better one. It now joins unless adding it is shown to raise the
residual by more than the allowance, two standard errors down; one the
merge cannot measure at all joins on its correlation with the reference.
The added-coverage clause is subsumed and goes. On seven kappa runs of a
small molecule this joins one run more than before, with a better
refinement R1.

A sweep whose intensities correlate with the reference's on no axes
(best CC below MIN_REFERENCE_CC) is left out before the isomorphism
test, as the header always said; it used to reach the band on a ratio
measured by chance. The ratio's reflection count is now reported where
there were too few to measure it, instead of 0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
A ring's shoulder reaches past the 0.01 A^-1 core into the flank; judged together with the clean
outer part of the flank (out to the 0.03 flag width) it was diluted into a merge, and a 5x shoulder
stayed in one written merge next to a rejected core, a 2.8x one in another. Each side now has an
inner flank (0.01-0.02) and an outer one, each judged on its own: on those two crystals the merged
intensities on the rings come out level with their neighbours (core-flank contrast 11.5 -> 2.5 and
13.6 -> 3.8 sigma), R-free level or better, nothing moves on clean crystals.

Also the not-detected wording of the intensity-side reading, which read "agree" at 4.9 sigma.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The live serial merge (StillsLiveMerge, the broker's provisional merges at 25/50/75% and the one after the
last image) ran none of the analyses the full pipeline makes on its final merge, so the live triage showed
twinning, pseudo-symmetry and anisotropy as "not measured" on every serial collection. The merge now
analyses itself (AnalyseStillsMerge) where it has the outcomes at hand - the cold MergeStills and the live
Finish - in the order the pipeline uses (the pseudo-translation first, since the twin verdict must know
whether it biased the L-test), and StillsLiveResult carries the results into the report the triage is read
from. --mode scale on stills takes the merge's own analyses instead of recomputing them, and now writes the
tNCS and AMBIGUITY_* keys the full pipeline writes.

The indexing ambiguity's resolution (ReferenceFreeReindexResult: the crystals that took part, those
reindexed, the median margin, the share decided at over 3 sigma) is kept on StillsMergeOutput - in a live
run, of the first checkpoint, which every later crystal was decided against - and the report's checklist
gets an "Indexing ambiguity" row for stills: the evidence, or UNRESOLVED where the hands are mixed in the
merge. Crystals an ambiguity leaves mis-indexed read in the merge exactly as a twinned crystal population
does, and one still cannot tell them apart, so a twin indication on stills points at that row.

Cost, measured on a replay (--live-checkpoints, 8 threads): 0.2-1.4 s per merge for 3k-14k crystals.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Measuring the anisotropy's systematic-error floor was nearly all of what the stills merge analyses cost -
5.6 s of 5.7 on the 16M observations of a 54k-crystal merge (8 threads), linear in their number - where a live
merge of 160k images is to be reported within seconds. The floor is a systematic scale that does not improve
with the count (the analysis says so itself), so the first crystals' observations up to 4M are what it is
measured on: on that run the floor read 0.76 A^2 on them and 0.78 A^2 on all 16M, every verdict and deltaB
unchanged, and the analyses take 1.3-2.1 s per merge instead of 1.0-5.7. The log now splits the time between
the three analyses.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Several sweeps of one crystal of a very large cell (~600 A axis) lost up to half of their sweeps
from the joint merge: de novo indexing of a single sweep missed the lattice (an axis harmonic, a
P1 supercell, a 5 A row, or no lattice at all), the sweep merged nothing or nothing that correlates
with the reference, and was left out. Given the cell the other sweeps measured (-C), the same
sweeps index, integrate and merge (checked on five of them, 5 A, ISa 5-10).

So once a sweep has merged with a resolved ISa, its cell is given to the sweeps after it as the
reference cell; a sweep processed before that which still finds no lattice of the crystal is run
again given the shared-cell target's cell. Whether any of them joins is decided on its intensities
as before. A user's -C is given to every sweep as before and replaces nothing.

Also cheaper relating of the sweeps: the held-out merge of the joined sweeps is built once for all
the doubtful sweeps (and once for a re-run sweep's before/after pair) instead of once per sweep,
with identical results; the relation is made a second time only where a sweep's run was replaced;
TIMING marks for its stages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
With a pseudo-translation declared, ReadZone split the zone into the two cos(2 pi h.u) classes and
normalised each in bins of 100 reflections. The zone a refused promotion is decided on is read on the
adopted group's merge and can hold only a couple of hundred reflections, so each class got a single
bin spanning the whole resolution range: the fall-off read as spread and the zone read centric. A
twinned 321 crystal (8c3e) whose pseudo-translation is now detected read its 622-over-32 zone at +0.9
nats as one population and +21.4 split, past the 20-nat bound, and its twin law 622 was adopted
(single sweep and multisweep). The modulation is now divided out of every strong reflection per
resolution shell, by its class's mean over the shell's mean, both measured on the acentric
reflections, and the zone and control are then read as one population, as without a
pseudo-translation.

Synthetic test: a 321 twin (30%, 622 law) with a 0.6 modulation at (2/3,1/3,1/2), 248 zone
reflections: -11.2 nats before, -25.8 after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The previous commit gave the first measured sweep's cell to every sweep after it. On a crystal of a
~600 A cell with a pseudo-centring that moved sweeps which index well de novo into another setting
or centring (two of twenty lost, two gained: still 13 joined), so the cell now goes only to a sweep
that found no lattice of the crystal on its own - it merged nothing, or nothing that correlates with
the reference on any axes - and that sweep is processed once more given the shared-cell target's
cell. A space group given as well (-S) was tried and refused by the metric check on such sweeps.

The isomorphism ratios and the held-out gains of the sweeps are independent of each other and are
now measured at once, one task per sweep (the held-out merge of the joined sweeps still built once).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Three rules, each given only where the evidence is firm, reported in the
SUMMARY as ADVICE= (codes, NONE) and an Advice line per rule - the summary
rows the CLI and the broker's live status already carry, so no new API object:

- EXTEND_ROTATION: the sweep's exact geometric coverage of the unique
  reflections to the signal limit, in the indexed orientation and point group
  (SweepCoverage), against a full turn; advised when a full turn adds >= 5%,
  with the degrees that come within 1% of it.
- REORIENT_CRYSTAL: SPINDLE_LOST_UNIQUE_FRACTION >= 0.05; with the least tilt
  that leaves < 1% in the blind cone in any direction.
- REDUCE_DOSE: relative B >= 3x the 2.5 d^2 at which the merged I/sigma at the
  fitted resolution peaks; the factor brings the sweep to that optimum.
  Witness: merging only the first 1/factor of the sweep resolves as far as the
  whole sweep on most fired sets, while undamaged sweeps cut to a third lose
  resolution.

Single rotation sweeps only. Report-only: nothing in processing changes.
The battery records ADVICE (not scored).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
3c001eb7a let the metric two-folds (Le Page, obliquity up to 1 deg) override the
Niggli character table wherever they found a richer class. The motivating case -
an F-cubic lattice whose degenerate shortest vectors give the table a reduced cell
it reads as I-tetragonal - is a class that equates lengths. For a class carried by
its angles alone (monoclinic, orthorhombic) the override instead promoted
pseudo-symmetric metrics the table had refused:

- a monoclinic small-molecule set (twin-composite pass): mC/mP -> oC at 0.3-0.9 deg
  obliquity; ISa 46.4 -> 16.7, R_meas 4.2 -> 4.9 %, SHELXL R1 0.049 -> 0.066
- a monoclinic protein, two in-house sets: mP -> oC at 0.2-1.0 deg; the beam-centre
  check then compared different classes at the two centres; ISa 3.6 -> 2.4,
  CC1/2 0.98 -> 0.45 on one, R_meas 68 -> 135 % on the other
- a triclinic crystal with a pseudo-I-orthorhombic metric: aP -> oI at 0.5 deg
  at the file's centre, and the run merged at a markedly lower ISa and resolution

Obliquity does not separate these from the F-cubic case (its unrefined cells
promote at up to 1 deg too), so the line is drawn on the class: Le Page's answer
is taken only where its class is tetragonal, trigonal, hexagonal or cubic. The
F-cubic set keeps F432 (ISa 7.7, R_meas 13.2 %, unchanged); the three regressions
return to their rc184 numbers.

Guard sets (open + in-house, 26) against rc186-merge: 20 identical; cytidine,
2wnn and myob_x10sa return to their rc184 numbers (differences in the third
digit); 2wnq returns to rc184's C2221 (R-free 0.547) from P21212 (0.444) - the
deposition is P21, so both fail.

The synthetic gains 3c001eb7a measured for oC/oI (monoclinic under-calls of
centred orthorhombic lattices) are given back; cF, hP, hR, tI are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
- tNCS: the modulation search reads at most the 200,000 coarsest reflections of its band, and the
  ladder that refines the class vector climbs to at most the 1,000,000 lowest orders. Both grew with
  the merge: on a ~600 A cell merged in P1 (2-3M reflections) the pre-promotion analysis took 38-46 s
  per sweep, 23-26 s of it on the critical path; now ~3 s + the ladder on 1M. The tNCS truth sets
  read up to ~110,000 and ~960,000 and are unchanged (9ea5, 8c3e, 6yqf, 5ebi, 2wnq, 5j23, 6pxb, 9i80,
  four cytc sets: identical rows); a 6 M-reflection open set (4nwv) keeps its result, 131 -> 96 s.
- A sweep of a joint run skips the final tNCS analysis, which is reporting only and which it does not
  report.
- The joint merge searches the space group on one arm: its fulls come in pre-combined at zeta 1, so
  the arm of only the well-measured observations read all of them again (one P1 merge, ~90 s on
  20 sweeps of a large cell, and its engine's memory).
- A sweep of a joint run that ends on a JFJochException (its lattice merged nothing, say) is logged
  as that sweep leaving the joint merge, a warning, not "Processing failed": the joint run goes on and
  reports it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The members' fulls were "released" with `= {}`, an assignment from an empty initializer list that
keeps the vectors' memory (about 0.6 GB per sweep of a ~600 A cell, ~11 GB for 20 sweeps, held
through the joint merge); they are now assigned empty vectors. The joint merge's input grew each
frame's reflections one by one, up to twice their size in spare capacity; each frame is now reserved
at its count first. Nothing the merge reads changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Several rotation masters with --force-still were refused twice: the sweep
plan sent them to the several-sweeps path, which refuses --force-still, and
the multi-master reader refuses any input whose goniometer turns. The flag
dropped the goniometer only for a single input.

Under --force-still, masters now skip the sweep plan and are read together
as one stills run; JFJochMultiReader takes an as_stills flag that accepts
rotation inputs and gives the run no goniometer. The geometry check
(detector, wavelength, distance, beam centre) still applies. Several sweeps
of frame files with --force-still are still refused, since their frames
name one sweep each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
rugnux: advice on the next collection keeps the two geometry rules only
Build Packages / Create release (push) Successful in 8s
Build Packages / Build documentation (push) Successful in 48s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m24s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 11m16s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 18m59s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 23m24s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 24m58s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 26m59s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 28m57s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 30m44s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 32m15s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 33m19s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 28m24s
Build Packages / Generate python client (push) Successful in 33s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 31m10s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 34m28s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 26m30s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 33m44s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 33m9s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 31m30s
Build Packages / Unit tests (push) Successful in 1h47m14s
Build Packages / Validation Battery (push) Failing after 1h21m28s
340e02072a
The dose rule (REDUCE_DOSE) is left out of the merge: it reads a threshold on the
relative B, and the truncated-sweep witness found it wrong on one fired set in four.
The two coverage rules are exact geometry and their forecasts matched the measured
gains on every set checked.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
rugnux: several grid-scan masters read as one run keep no grid
Build Packages / Create release (push) Successful in 9s
Build Packages / Build documentation (push) Successful in 47s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 26m57s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m28s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 11m22s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 15m45s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 21m27s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 28m30s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 29m47s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 30m46s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 30m59s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 21m12s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 24m58s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 29m20s
Build Packages / Generate python client (push) Successful in 31s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 28m52s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 35m23s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 34m45s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 35m58s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 28m18s
Build Packages / Unit tests (push) Failing after 1h50m10s
Build Packages / Validation Battery (push) Failing after 46m38s
ed655d0c4d
Several fixed-target masters given together failed in the integration
step with "Input parameter invalid (Image out of bounds)". The combined
dataset of JFJochMultiReader is a copy of the first input's, grid scan
included, while the run's image numbers are global over all inputs and
rugnux sets the image count to the total without re-deriving the grid.
ScanResultGenerator::Add then placed every image on the first input's
raster and threw on the first image past its last element; the NXmx
writer's grid_scan_x/y columns would have thrown the same way.

Several collections are not one grid, so the multi reader now drops the
grid scan from the run, next to where as_stills drops the goniometer. The
grid is used only for the grid map and the per-coordinate plots, not for
processing, so the run indexes, integrates and merges every chip as
before and simply carries no grid map. A grid map per chip is a possible
later feature.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Dark mode: the panel title band kept the light mode's pale indigo, so the light
text on it was invisible. It is now the drawer's selected-entry tint at night.

Phone width (390 px): Plotly plots were drawn to the box they had when the
drawer was open and never redrawn, as `responsive` follows the window only - the
Plot component now resizes them with a ResizeObserver on their own box. The
dashboard plot toolbar's 500 px minimum width made the page overflow; the
toolbar wraps and the card takes its own height below md. On the rugnux page
the By-resolution and Collection plots took their height from boxes that below
xl take their height from the plots, so the plots overflowed onto the strip:
fixed heights below xl, single column collection plots and hero tiles two or
three to a row, the progress details under the bars. The top bar's three
action buttons go to a second dense row and the wordmark is hidden below sm.
The preview and data-collection pages stack to one column.

During a rotation collection with rugnux live the per-image plots are empty by
design (the values come with rugnux's first pass): the empty plot now says so
instead of showing autoscaled axes, and the statistics card shows the
collection's progress in percent while measuring, as the efficiency is over the
images expected and read like a fault.

The rugnux page header no longer shows "Done ·" when a run has no verdict, and
the Collection strip says how many images are held for the serial pre-pass (or
are in, for a rotation run) instead of "waiting for images".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The live status's images_received, the frame rate and the end latency of a
stills collection ran from StillsGPUAnalysis::Hold, which only sees the frames
that got a slot of the image buffer: a frame dropped at a full buffer was not
counted, so after a loss the "N / M · Hz" header and the latency figure ran
from the last frame rugnux kept, not the last frame the detector sent. The
receiver now reports the frame to the live status in OnImage, before the slot
is taken, as the rotation receiver does; the drop itself is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
A rotation sweep run live had ice-ring detection off unless /start asked for
it: the dataset settings held the flag as a plain bool (default false), the
start message always carried it, and MakeRugnuxLiveSetup took it from there
as the file's value - where a bare `rugnux` on the same images defaults it
on for a rotation sweep. The flag is now optional through the dataset
settings and the start message (which was already std::optional, as is the
OpenAPI model, so neither the CBOR stream nor the API schema changes): left
out of /start, the start message carries no detect_ice_rings, rugnux live
decides as the CLI would (on for rotation, off for stills - unchanged there),
and the master file carries no detect_ice_rings key, so an offline `rugnux`
on the written file takes the same default. Given on /start, true or false,
it is honoured everywhere as before. The broker's own spot finding (SpotUtils)
keeps treating an unset flag as off.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Every Logger construction printed the version line, and a rugnux run makes a
logger per FFT indexer of its first-pass ladder: about 180 version lines in a
20 s live run. The line now goes out the first time a service name is seen.
The set is a function-local static, as jfjoch_writer's logger is a global
made before this file's own namespace-scope statics.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The lattice probe's "no lattice on the first 60 deg" stayed on the dashboard
while the main rotation run indexed the sweep. When the run reports its
lattice (the first pass's "lattice ..." note), the probe banner becomes
"lattice on the sweep: ..." and a probe answer that comes later is not shown.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
A model that was tried and rejected still has its anomalous difference map computed, as the negative
result, and the summary's Anomalous row took that map's verdict as if the model had been placed. The
row then differed from the one a run without the model writes, which the wrong-model null in CI
caught on every pair whose wrong model has a compatible cell and is scored (4 of 25 pairs: "2 report
line(s) moved outside MODEL VALIDATION"). The map decides only where the model was placed
(ModelPlaced(), as the anomalous-hands warning already does); otherwise the row is the no-model one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Tests: the hot-pixel GPU tests copy each frame on the frame's stream
Build Packages / Create release (push) Successful in 8s
Build Packages / Build documentation (push) Successful in 47s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m28s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 9m49s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 17m43s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 22m26s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 26m32s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 28m18s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 31m13s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 30m22s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 33m34s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 34m34s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 28m54s
Build Packages / Generate python client (push) Successful in 34s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 31m58s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 35m19s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 25m51s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 31m23s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 32m19s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 29m43s
Build Packages / Unit tests (push) Successful in 1h50m0s
Build Packages / Validation Battery (push) Successful in 46m37s
0968981658
Both tests uploaded a frame with a synchronous cudaMemcpy from pageable memory and then launched the
finder's kernels on the frame's stream, which is a non-blocking stream: cudaMemcpy returns once the
data is in the staging buffer while the DMA to the device may still be landing, and a non-blocking
stream is not ordered after it. Under GPU contention on the CI runner the folded-cards test read a
frame that had only partly landed and the folded counts differed from the one-card run in the lower
rings. cudaMemcpyAsync on the frame's stream orders the copy before the kernels.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The drawer with the page list takes 240 px that the rugnux live page is
better off having. The menu button in the top bar already hid it, but
nothing said so: it now has a tooltip and its icon shows the state (the
open-menu glyph while the list is shown), and the choice is kept per
browser in localStorage, so a screen set up for the live page stays that
way across reloads. The plots and the Mol* stage already redraw to their
box through their ResizeObservers.

On a phone the permanent drawer squeezed the page to 150 px until it was
closed: below the sm breakpoint it is now a temporary drawer that starts
closed, lies over the page when opened, and closes on a tap outside or on
choosing a page; the kept choice applies to wider screens only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
A pixel lit on more frames than one reflection or chance explains is a defect on stills as on rotation:
a still's reflection is on a pixel for one frame, so two frames are already more than one explains, and
the chance bound measured on the ring covers a lattice that recurs. The rule was switched off without a
rotation (spacing 0), so a live or offline stills run whose input mask was incomplete analysed every
frame with its hot pixels: on a JUNGFRAU 8M stills set with no detector mask ~350 of them made ~350
fixed false spots on every frame, junk lattices on the empty frames and a merge far below one with the
mask. The error-value, dead and unstable rules are unchanged; the GPU candidate pass no longer takes
the spacing flag it only tested.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The per-image analysis of a live serial collection ran on the broker's bare experiment and took only the
beam centre, distance and mask from the pre-pass, so it lost what the pre-pass itself integrated with:
the polarization (no Lp correction at all), the stills integration radii and the background ring's
radial correction as the pre-scan measured them, and the run's rocking width - every frame predicted
through twice its own profile radius, which on the same frames integrated 13% fewer reflections than
the run's window and left the merge thousands of far-tail observations (13723 under a partiality of
0.001 against 77 offline). The pre-pass result now carries them (StillsPrePassResult), the analysis
experiment imports them, the indexer gets the window and the live merge the run width.

The per-frame refinement was bounded by the wall clock (real_time), which the code itself documents as
making the answer depend on machine load: the same 1122 frames gave 521 to 567 crystals from run to
run. The frames wait in the buffer, so it is bounded by the iteration caps as offline (50 and 25
iterations, LMSolver); measured offline that refinement takes 0.2 ms a frame against the 40 ms budget.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The provisional merges' notes compared overall numbers at different automatic cuts, which read as a
later merge being worse than an earlier one when it was the cut that had moved; and where the CC1/2
fit found no fall-off at all - a few hundred crystals rarely show one - the note printed the data's
edge as the resolution (1.19 A at <I/sigma> 0.9). The note now says the cut, or that no fall-off was
found and how far the data reach, and gives the lowest shell's CC1/2 and R_split beside the overall
ones. A checkpoint is labelled by its image count: its fraction is of the images the collection
declared, which a cut or lost frames make fewer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The incremental merge prepares each crystal after the first checkpoint once, against the checkpoint's
merge, and never scales the run as a whole; replayed on a stored run the loss scales with the share of
crystals prepared that way (R_split 0.244 cold, 0.288 with a checkpoint at half the frames, 0.443 at a
tenth; CC1/2 0.854 / 0.773 / 0.736 at one cut). StillsLiveMerge::Final can now scale and post-refine
every crystal over the whole run instead - the cold merge, at the cold merge's cost after the last
frame - and the broker's merge does so (FINAL_FULL_SCALING), a prototype against the live budget for
the owner to keep or revert. --live-final-full replays it in --mode scale --live-checkpoints, with the
time of the merge after the last frame in the log.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The checkpoint notes read "6 of 12 images" since the merge labels moved
from fractions to counts; the receiver test still looked for the fractions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
A 20 keV L-glutamic acid sweep (P 21 21 21 per XDS) on which rugnux currently chooses a different space
group. ref filled by `battery.py refs --arm inhouse --write` from the CORRECT.LP beside the input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
battery: MOPS, an in-house monoclinic small-molecule set
Build Packages / Build documentation (push) Successful in 47s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 17m22s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 30m51s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 11m10s
Build Packages / Unit tests (push) Successful in 1h45m39s
Build Packages / Validation Battery (push) Successful in 46m26s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 34m3s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 34m8s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 28m30s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 32m21s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 17m2s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 22m36s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 22m16s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 26m52s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 27m4s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 32m58s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 30m52s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 30m32s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 25m28s
Build Packages / Create release (push) Successful in 8s
Build Packages / Generate python client (push) Successful in 27s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m32s
75114c47a1
P 21/c with Z'=2 and beta near 90 (COD 2206999); the XDS reference reports the Sohncke group P 21, so the
literature space group goes in ref_override. rc184 indexes it in a doubled cell (C 2/c, primitive volume
twice the true one): the sub-lattice hypothesis keeps the standing cell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The second NaCl sweep at X10SA, same setup as nacl_x10sa_20keV (EIGER2 16M, 20 keV, 155 mm,
360 deg in 0.2 deg frames, 100 K), recorded a minute after it: a tiny cubic F cell in a crystal
of several grains, most spots on powder rings.

- inhouse.json: the row, with the XDS reference read by `battery.py refs --write` from the
  CORRECT.LP beside the data (F 4 3 2, a 5.606, ISa 2.7, R_meas 0.33, CC1/2 0.99, 22 unique,
  Friedel pairs merged, XDS range 0.78 A). Processed like the first sweep: IDXREF on the 150
  strongest spots with the cell and F 4 3 2 given (76/150); INTEGRATE's automatic spot-size
  determination failed even at DELPHI=20, so the spot-size parameters were fixed at the values
  XDS refined for the first sweep; README_XDS beside the data records the steps. Most shells
  hold 2-4 unique reflections and carry no significant CC1/2, so the CC1/2 rule puts the
  reference d_min at 1.86 A rather than at XDS's 0.78 A. ref_override F m -3 m from COD 2108652,
  as for the first sweep; smoke tier like it.
- README: smoke count 23.

Battery (rc186-merge2, --no-refine): pass, F 4 3 2 (indistinguishable alternative of F m -3 m),
to 0.68 A, ISa 12.1, SHELXL R1 0.055 against the COD structure; nacl_x10sa_20keV unchanged (pass).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The search places each further copy wherever its translation function peaks,
and every symmetry image of that position (an operator of the group plus a
lattice shift) peaks alike, so the copies of a multi-copy solution came out
at arbitrary images, often 50-80 A apart and in different cells, although the
crystal they build is right. CompactCopies replaces each copy, in order, by
the image whose centroid is nearest the mean centroid of the copies before
it, and moves the whole by one lattice shift that puts the mean of the
centroids in [0, 1). It runs once the decision is taken, on every candidate:
within its own group the crystal is unchanged, while the check that carries
the chosen copies into another group reads them as they stand, so compacting
earlier would change that check's input.

On a three-copy P 32 2 1 solution of an in-house cytochrome C set the copies
went from 52-78 A apart to 26 A apart, each still matching a reference chain
to 0.3-0.4 A C-alpha rms; the search's decisions were unchanged, and the
held-out R-free before the validation's rigid body was identical (0.4331).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
gemmi's flood fill marks the asymmetric-unit mates of a blob's points as it goes, but a difference map from
a Fourier transform is symmetric only to ~0.06 sigma: an edge point of the mate that is just above the cutoff
while its partner is just below it stays unmarked, seeds a second flood fill, and the whole mate is reported
as a blob of its own. Moved beside the model, the two copies coincide. On a cytochrome C crystal with an
AlphaFold model one of the three Fe sites was listed twice (21.5 sigma, 0.05 A apart); 11 of 19 runs of that
data set carried one to three such pairs. A blob whose placed centroid is within 1.5 A of a stronger one is
now left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
A bar over the Mol* stage carries previous/next arrows through the refined model's difference-map blobs
(already in the live result, highest peak first), with the peak's index, height and reading, and a button
back to the whole model; the Fo-Fc toggle moves into the same bar. At a peak the camera looks at it from the
side with the fewest atoms in front, 26 A away, with the clip planes 5 A either side of it; the maps are drawn
uncarved within 10 A of it (the carved maps about the model would hide unmodelled density such as a missing
cofactor) and the model as sticks over the cartoon.

Smaller view fixes: stick carbons take the chain colours instead of Mol*'s default greens, which read as
Fo-Fc density; riding hydrogens are not drawn; the 2Fo-Fc mesh is dimmed (alpha 0.35 over the whole model,
0.5 at a peak) so the cartoon and the difference density show through it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
/config/rugnux gains two settings, both off by default and turned off by
POST /initialize like enable and the model:

- refine_solvent: the refinement of a placed model builds the solvent, as
  rugnux --refine-solvent (ProcessConfig::refine_solvent on the rotation path,
  ModelStepSettings::refine_solvent -> RefinementSettings::solvent on the
  stills path). model.refinement in /rugnux/status carries waters_added and
  waters_removed where the build ran.
- force_still: a DECTRIS rotation collection goes to
  JFJochReceiverRugnuxStills, as rugnux --force-still takes it. The analysis
  gets the experiment and start message without the goniometer; the start
  message to the writer keeps it. The FPGA path is unchanged.

POST /rugnux/clear puts the live status back to Idle with nothing of the last
run (RugnuxLiveStatus::Clear), refused during a collection with the
WrongDAQState error of POST /image_buffer/clear.

C++ model, python client docs, TypeScript client and redoc regenerated.
Tests: JFJochBrokerHttp_RugnuxLiveOptions, JFJochReceiverRugnux_Rotation_ForceStill,
JFJochReceiverRugnux_ClearStatus, JFJochStateMachine_Initialize_DisablesRugnux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The "Live processing" panel gets two switches with a line of help each, beside
the rugnux live switch. The rugnux live page gets a Clear button next to the
phase once a run is over and nothing is measured: POST /rugnux/clear, after
which the page is the empty state. The model scores show the waters built
where the solvent was refined.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The "Automatic processing as an expert system" section that opened the
methods part was given over to one company - a paragraph on Global Phasing
Ltd, one on autoPROC and one on STARANISO, ahead of every other package -
which made that group look far more prominent than what was taken from it.
The section is gone and its three pieces now sit where their subject is:
autoPROC under a new first subsection, "Experimental geometry and physical
corrections", that also takes over the sensor-absorption, polarization and
ice-ring entries of the former "Physical corrections and calibration"
section at the end (they are image-level physics, so they belong before
spot finding in the pipeline order the page follows); STARANISO in the
"Diffraction anisotropy" entry it inspired; the Bricogne (1986) credit was
already under Indexing and stays there alone. The umbrella paragraph only
summarised those three and is dropped; the autoPROC entry keeps every
statement it made (header metadata verified against the images, the beam
centre found from them, the expert-system aim, the "loss of centring" wording)
in the same tone and length as the entries around it.

Two further moves for balance: the per-crystal post-refinement of stills
(PRIME, Sauter et al.) moves from Spot finding to the serial entries of the
scaling section, and the single ninety-line "Structure refinement"
paragraph is split into entries (gradient, minimiser, geometry restraints,
B restraints, weights, model scaling, metal bonds, anisotropic
displacements) with no change of wording.

No acknowledged work, citation or statement of what was taken is dropped:
all 151 DOIs appear exactly as before, and every sentence outside the three
rewritten Global Phasing entries is verbatim.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Second pass on the acknowledgements for readability. Every entry now says
what was taken, from whom, and gives the citation; the detail of how rugnux
uses a method, which belongs in CPU_DATA_ANALYSIS.md, is gone. The two
long sections are split: "Scaling, merging and data quality" (174 lines)
into "Scaling and merging", "Serial data" and "Data quality and
anisotropy"; "Model-based analysis and maps" (285 lines) into "Model
placement and molecular replacement", "Structure refinement" and "Solvent,
ions and unmodelled density", with the former single refinement paragraph
and the solvent-audit paragraph broken into one entry per method. The
longest section is now 123 lines, the longest entry 22.

Descriptions corrected where the code differs: rugnux prints "loss of
centring", not autoPROC's full phrase; the twin fraction is re-fitted
between cycles, not refined in the same least squares as SHELXL's BASF;
the coincidence-lattice test covers reticular (pseudo-)merohedry up to
index 10 and also follows Herbst-Irmer & Sheldrick (1998); the water
build widens Coot's 2.4-3.2 A band rather than using it as is; the random
rotations also serve the molecular-replacement null; IRRMC supplies the
largest share of the public sets, not most of them; the PDB also supplies
the refinement battery's coordinates and structure factors.

No citation lost: all 151 DOIs appear with the same counts as before, and
every DOI-less work is kept.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The PSEUDO_TRANSLATION warning and the summary's pseudo-symmetry line printed
max(Patterson peak, modulation depth) under the label "Patterson off-origin peak".
With TNCS_CLASSES= LATTICE_AVERAGED the modulation depth is fitted over the class
averaged across the lattice's rotations, so where the crystal's own symmetry carries
the vector to other images it also counts the copy pairs there and reads above the
Patterson at the vector (about 3x for a pseudo-translation off the axes of a trigonal
crystal: 88.6% against a 27.8% band peak on a P3221 cytochrome C with three copies,
which the deposited-type model confirms are related by near-pure translations,
rotations 1-3 deg). In the persisted rc186-merge2 runs 17 of the 35 warnings printed
a number other than TNCS_PATTERSON_PEAK_PCT. They now quote the Patterson peak and
the modulation the detection stands on (TNCS_MODULATION or _AVERAGED), each by its
own name; the docs no longer call the depth a Patterson height in the averaged case.

The warning asks for the vector to be declared to molecular replacement. Where the
run placed a model (MODEL_PLACEMENT= MR or RIGID_BODY) that has been done, so the
finding is reported in section 4 and on the summary line, which say how the model
was placed, and raises no flag - as a noticeable anisotropy does. In those runs 32 of
35 warnings were given with a model already placed, and 5 sets owed their WARNINGS
verdict to this flag alone. Detection itself is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
/rugnux/status overall.r_free reported the model as placed (R_FREE of the
report) even where rugnux had refined it, while model.refinement.r_free and
the page's tile showed the refined one. The overall row now carries the
R-free of the model the run ended with: the refined model's where the
refinement ran (as model.refinement.r_free, REFINE_R_FREE of the report),
else the placed model's. The shell rows stay the placed model's - the
refinement measures no shell. Serial runs get the same through ShellRFree.
The OpenAPI descriptions say so; C++ model, frontend client and Redoc
regenerated.

Live runs now write <file_prefix>_report.txt. The receiver already built the
report document for the live status; its text (RenderReportText, the same
rendering the CLI writes) goes to the writer as a new optional END message
field `report`, written beside the other result files under a temporary name
and renamed into place. No file I/O in the broker. The CBOR string decoder
was capped at 16 kB; it now sizes the buffer to the string (a report is tens
of kB, a model's metadata block can exceed the cap as well).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
Third pass on docs/ACKNOWLEDGEMENT.md. Entries added for the works the
code credits but the page did not: the Cornish-Fisher expansion behind the
Poisson spot threshold; Grosse-Kunstleve (1999) for the step from Le Page's
two-folds to a conventional cell; the silver behenate cell (Huang et al.
1993) and the NIST SRM 674b / SRM 640 calibrants; the GPU spot finder by
H.-C. Stadler; phenix.xtriage as the model of the twinning test (with the
2019 Phenix paper); Phaser's default for declaring a pseudo-translation;
Sultana et al. (2007) for the six-domain twin; Luzzati's D in the
likelihood of a placement; the Oren-Luenberger / Shanno-Phua self-scaling
of the L-BFGS initial Hessian; the riding-hydrogen model (Liebschner et
al. 2020) and the QCP superposition (Theobald 2005; Liu et al. 2010), both
through GEMMI; the PILATUS count-rate clipping (Trueb et al. 2015) the CBF
reader relies on; Mol* itself (Sehnal et al. 2021) and the SGI trackball
port behind the viewer; the Crystallography Open Database (Gražulis et al.
2009, 2012) as the source of the small-molecule reference cells; and the
HKL-2000 manual's wording for a weak stretch of a sweep, now in an entry
of its own beside autoPROC's.

Every DOI on the page, the 151 that were there and the 15 added, was
checked against Crossref (title, journal and year read back) or, for the
two DataCite ones, doi.org; all resolve. The same papers go into the
References list of docs/CPU_DATA_ANALYSIS.md where that page describes
the method.

The reverse direction: works the page credits that had no comment at the
algorithm now carry the one-line credit there - Wilson (1949) at the
centre-of-symmetry statistics; Sheldrick (2015) for SIMU/ISOR; Lunin,
Afonine & Urzhumtsev (2002) at the likelihood parameters; CrystFEL's
three-ring region at the CPU and GPU stencils; MOSFLM's post-refinement
practice; Kleywegt / Uson / Headd at the NCS decision; Herbst-Irmer &
Sheldrick (1998) at the twin-fraction fit; Jumper / Varadi at the PAE
domains; Hubbell & Seltzer's database name; Matplotlib at the colormaps;
PRIME / Sauter at the stills post-refinement. Comments only, no code
change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
# Conflicts:
#	broker/redoc-static.html
A live run with a large uploaded model lost its whole END message at
the writer: "Error processing end message, key placed_model: out of memory/need
more memory". The model's metadata block (entities, sequences, TLS, connections)
is 36 kB of mmCIF, and the CBOR string decoder copied strings into a fixed 16 kB
buffer - tinycbor's CborErrorOutOfMemory. Reproduced on 125003d69 with that
model; the decoder sized to the string (7fbbe449f, already on this branch) fixes
it. Nothing else on the path has a fixed limit below the END message budget:
byte strings are read in place, the TCP frame limit is 1 GiB, ZeroMQ has none,
the writer's receive ring falls back to the heap.

The long texts of END - the models' metadata_mmcif and the report - now travel
gzipped, under the compression tag the images use: tag 56500 ["gzip", 1,
bytes]. A new reader still takes a plain text string there; an old one refuses
the message ("String expected") rather than writing files without the text.
docs/CBOR.md says so. On that mmCIF gzip gives 3.3x (2.0 MB -> 0.6 MB,
29 ms); on its metadata block 3.0x (36 kB -> 12 kB, 0.3 ms). The gunzip of
writer/RugnuxModelFile.cpp moved to compression/JFJochGzip beside the new gzip.

The report is no longer sent to the metadata and preview sockets: like the
result blocks it is only for the writer, and it is now a tagged item.

An END that does not fit MESSAGE_SIZE_FOR_START_END (the pushers' 256 MiB
serialisation buffer) now first goes without the structure blocks (maps,
models, blobs) and keeps the merged data; only if that still does not fit are
the merged data dropped too. Either way with an error in the log and a warning
in the live status, as before.

Tests: a round trip through CBOR and the writer of 2.4e5 atoms with a 3.5 MB
metadata block, 10^6 map coefficients and a 0.7 MB report; an uncompressed
report as an old producer sends it; a 40 MB END over TCP into an 8 MB receive
ring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
StillsGPUAnalysis held the DiffractionExperiment it was given by reference, and
JFJochReceiverRugnuxStills::OnStart gave it a local (the experiment with the goniometer
removed for force_still), which went at the end of OnStart while the pre-pass, analysis
and merge threads and every checkpoint merge went on reading it. Seen as std::bad_alloc
and heap corruption in a second stills run after a failed one. The member is now a copy.

AddToEndMessage of the stills receiver catches a failure of FillEndMessage as the
rotation receiver does: the end message goes out without the analysis results instead
of not at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
The service decides on GetBufferSize() / GetImageBufferLocationSize() before the start; the receiver now
repeats the same expression after the start message, which sets the bit depth and so may enlarge the slot.
The buffer-too-small test now also checks that the Lite receiver took the collection: every image has its
spot finding and the per-image plot has a point per image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
1.0.0-rc.186: CHANGELOG
Build Packages / Build documentation (push) Successful in 46s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 17m53s
Build Packages / Unit tests (push) Successful in 1h50m27s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 32m45s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 34m18s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 22m31s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 24m24s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 28m28s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 10m59s
Build Packages / Validation Battery (push) Successful in 46m32s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 27m19s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 28m23s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 30m31s
Build Packages / Create release (push) Successful in 8s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 30m46s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 35m38s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 31m21s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 33m3s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 26m11s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 31m58s
Build Packages / Generate python client (push) Successful in 32s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m33s
cae022bc42
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
CBOR data interface release 7: gzipped texts in the END message are not readable by an older writer
Build Packages / Build documentation (push) Successful in 46s
Build Packages / Create release (push) Successful in 8s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m20s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 9m44s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 15m39s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 22m24s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 27m0s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 30m12s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 30m48s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 33m45s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 36m16s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 36m41s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 34m35s
Build Packages / Generate python client (push) Successful in 28s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 34m14s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 30m30s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 24m24s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 24m55s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 32m24s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 28m10s
Build Packages / Unit tests (push) Successful in 1h49m31s
Build Packages / Validation Battery (push) Successful in 46m41s
3deb1c9f60
A mismatched jfjoch_writer now refuses the run at its START message instead of failing on
the END message of a live run with a model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
leonarski_f merged commit aecd48b9d3 into main 2026-11-13 20:13:44 +01:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mx/Jungfraujoch#97