v1.0.0-rc.166 #76
Open
leonarski_f
wants to merge 170 commits from
rc166 into main
pull from: rc166
merge into: :main
:main
:rc166
:gitea-pages
:rc165
:rc164
:rc163-unshare-doc-files
:2608-rc-162
:2608-performance
:adaptive-spot-finding
:stills-partiality-postrefine
:2607-res-prediction
:2607-rc-160
:french-wilson-model-maps
:2607-win-viewer
:2607-state-machine
:2606-rc.156-fixes
:viewer-windows
:2606-viewer-processing
:2606-tcp
:2606-pixel-refine
:2606-eiger-calib
:2606-q-spacing
:2606-eiger-module-fix
:2606-force-cpu-azint
:2606-xds-plugin
:2606-azint
:2506-rot-all
:2606-sgsearch
:2605-scaling
:2605-hdf5-vds
:2605-hdf5-enospc
:2605-hdf-fixes
:2605-b-factor
:2605-speed-up-preview-start
:2604-index-and-refine-fix
:2604-cuda-cleanup
:2604-azint-mapping-parallel
:2604-more-fixes
:2604-start-speed
:2604-async-start
:2605-sigma-correction
:2604-hdf5-errors
:2604-rc.136
:2604-rc.135
:2604-processing-options
:2603-single-file
:2604-detector-parallel-logic
:2603-pipeline-upgrades
:2603-multilattice
:2603-rc.131-2
:2603-rc.131
:2603-rc.130
:2603-rc.129
:2603-ep
:2602-scaling-d-limit
:2602-residual
:2601-completness
:2601-1.0.0-rc.124
:2601-1.0.0-rc.123
:2512-scaling-merging
:2512-1.0.0-rc.122
:2512-1.0.0-rc.121
:2511-1.0.0-rc.120
:2511-1.0.0-rc.119b
:2511-1.0.0-rc.119
:2511-1.0.0-rc.118
:2511-1.0.0-rc.117
:2511-1.0.0-rc.116
:2511-1.0.0-rc.115c
:2511-1.0.0-rc.115
:2511-1.0.0-rc.114b
:2511-1.0.0-rc.114
:2511-1.0.0-rc.113
:2511-1.0.0-rc.112
:2511-1.0.0-rc.111
:2511-1.0.0-rc.110
:2511-1.0.0-rc.109
:2511-1.0.0-rc.108
:2511-1.0.0-rc.107
:2511-1.0.0-rc.106
:2511-1.0.0-rc.105
:2511-1.0.0-rc.104
:2511-rc.103
:2511-1.0.0-rc.102
:2511-viewer-enh-2
:2511-viewer-enh
:2511-eiger-mask-3
:2511-eiger-mask
:2510-viewer-improvements
:2510-viewer-3D
:2510-fpga-clamp
:2510-rc.88
:2510-release
:2509-rc.82
:2509-gitea
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
rugnux --mode calibrationwrites<prefix>.jsonbeside the.poni, whosedataset_settingsmember is ajfjoch_brokerdataset_settingsbody as it stands.rugnuxandjfjoch_viewerread PILATUS miniCBF sweeps natively, without conversion.rugnuxmeasures the beam centre on every run, and indexes with it when the file's value indexes nothing.rugnuxwrites the unmerged MTZ by default, and a P1 merge beside it, so a wrong space group can be re-merged without reprocessing.rugnux: the lattice, the point group, the setting and the systematic absences.rugnuxreport gives the resolution the CC1/2 fit reached, beside the range the reflections were written to.rugnuxreport gives the twinning statistics measured before the space group was decided, beside the ones measured after.rugnuxreport gives the strong-direction diffraction limit, and warns when CC1/2 is not monotone with resolution.rugnuxranks screw axes on the evidence their absences carry, rather than on how many control reflections a candidate happens to have.rugnuxreport gives the detector tilt, the measured tilt and the direct beam beside the beam centre, and a post-refined beam centre is judged against the run's own measurement rather than the file's.--no-refine-tiltholds the detector tilt at the value in the file, instead of zeroing it, when the calibration starts from the spots.jfjoch_viewergrid scan view draws the cells in the proportion of the scan steps, so the map has the shape of the scanned area.--max-spots was a fixed 1000, and on a rotation sweep it sets the DENOMINATOR of the per-frame acceptance gate, which admits a frame when at least a fifth of its spots index. Detections are not all reflections: on a strongly diffracting crystal with heavy solvent background there are 2372 a frame, 1416 of them on ice bands, and only 178 index - so a budget that takes essentially the whole list puts the entire frame population on the gate (median indexed fraction 0.271, tenth percentile 0.218) and the run integrates 83% of its images. The same run with a smaller budget gains 211 frames and loses none, and the frames it gains are the ones with the MOST detections. That is how a larger budget integrates fewer images. So measure it: over the first pass's validation frames, with the sweep's lattice known, tally each spot by rank as +1 if it lies on the lattice and take the budget at argmax over N of n_indexed(N) - 0.20 * n_counted(N) which rises exactly while spots at that depth index better than the gate's own floor and falls after. The 0.20 is that floor, not a new constant - it is lifted out of the function-local it already lived in. It means: as deep into the intensity-ordered list as the image is still showing reflections of THIS crystal. The argmax alone would not do. Under the null that spots index at the same rate at every depth the tally is a driftless random walk, whose maximum is positive whatever the data, so a bare argmax shortens every dataset. The budget therefore has to clear the walk's own noise: the quantity it acts on is the fall from the peak to the end of the list, which is that walk read backwards, and the reflection principle gives its null law in closed form - P(fall > z*sqrt(g(1-g)T)) = 2(1-Phi(z)). That already pays for the search over ranks, so nothing further is owed to multiple comparisons. One false cut in a thousand measurements - a twelfth of one over a 39-crystal two-pass corpus - fixes z at 3.29. The level was chosen before the rule was written and was not revisited afterwards. Battery, same build, one changed default: 36/39 space groups in both arms, none lost, and 36 of the 39 crystals BIT-IDENTICAL. Two move materially - the strong crystal by +17.4% observations and +35.9 points of CC1/2 at 1.58 A, its indexing rate 83.4 -> 95.1%, and another by +93.5% observations with completeness 76.6 -> 98.4%. The significance requirement is what makes that list clean: it removed the one crystal the unguarded rule regressed, and with it two of the five gains, whose peaks do not clear 3.29 sigma on 60 frames. The lever for those is the sample and not the threshold - power grows as the root of the frame count while the bar stays where it is. Rotation only, and the first-pass lattice search always sees the full list: a fixed --max-spots 250 would have been much cheaper to write and fails a battery crystal outright, starving the de-novo FFT into a wrong cell that indexes 7 of 60 frames. Stills never reach the code path - verified bit-identical merged reflections on a serial set - and --max-spots N still pins it, as does the library default the broker and the FPGA use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FBumeJVx4oeXxiBRpkrE5HFirmware 1.x writes the same three values under different names. Try the NXmx spelling first and the old one only if it is absent: detector/distance <- detector/detector_distance detector/saturation_value <- detectorSpecific/countrate_correction_count_cutoff sample/transformations <- sample/goniometer This cannot change what a current file reads: every modern Eiger master carries BOTH spellings. Measured on thirteen masters from ten facilities, firmware release-2020.2.1 through release-2024.1.1 - all of them write detector_distance and countrate_correction_count_cutoff beside the NXmx names, and a goniometer group beside the transformations one. The goniometer is the one that matters. A goniometer is only ever set from that one group, so a file whose axes are somewhere else was not an error - it was read as STILLS, silently, and the run completed with the wrong answer. The old layout also tags no axis with transformation_type and gives no vector, both of which ReadAxis required, so absence now means two different things by layout: in a transformations group it still means "not an axis" (that is how AXISNAME_end and the width scalars are skipped), while in the legacy group every leaf IS an axis and the companions are recognised by name instead. A missing direction defaults to the one every DECTRIS master since has written and says so in a warning rather than assuming it silently; a wrong guess there does not index, so it is visible, and the rotation first pass tries the opposite sign anyway. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>A hybrid master carries an NXmx /entry/sample/transformations group holding one EMPTY SUBGROUP per axis - the direction as a vector attribute, no NX_class, no units, no angles - beside a legacy /entry/sample/goniometer group holding all the angles. Opening it failed outright with "Cannot open HDF5 dataset /entry/sample/transformations/omega": the existing legacy fallback keys off Exists("/entry/sample/transformations"), which is true here, so it never fired, and the axis stub was then opened as if it were the angle dataset. Present is not the same as usable. GoniometerGroup now takes transformations only if it holds at least one DATASET, and ReadAxis asks IsDataSet rather than Exists, so a member that is not a dataset can no longer be read as one. HDF5Object gains that predicate, in the style of the neighbouring Exists. The stub is the load-bearing half, not merely the thing that crashed. In the legacy branch, before falling back to the assumed (-1,0,0), the reader now looks for the NXmx stub and takes its stated vector. With it the axis is (0,-1,0) and the run indexes 60/60 validation frames; with the stub deleted the assumption applies and the same file indexes 0/60 on both schemes and both signs, and the run stops with no lattice. So without this half the fix would have turned "cannot open" into "found no lattice" - a differently shaped failure, not a success. The direction stated here is 90 degrees from the assumption, not merely its negation, which the rotation first pass could have recovered on its own. The vector size check moved out of the first branch so it now covers every path that produces one. Verified: the file processes end to end, 100% of frames indexed, cubic cell 105.87 against a deposited 105.88 (0.009%), space group reported as I23 or I213 - correctly refusing to choose, since the reflections that separate them are extinguished by the I-centring and were never measured. A both-layout master reprocesses unchanged (100% indexed, P212121). [HDF5] passes: 2194 assertions in 91 cases. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lc5JG6kJqZoCWaoZ43JGTWThe detector plane was three PONI angles and nothing else, so the two things it cannot express - an image mirrored in Y, and one mounted at a multiple of 90 degrees - had no home at all. They are now the DetectorOrientation carried by the detector setup, composed with the PONI rotation into one orthogonal matrix whose columns ARE the fast axis, the slow axis and the sample->PONI normal: lab = R(rot1, rot2, rot3) * Delta * ( (x-bx)*p , (y-by)*p , distance ) GetFastAxis/GetSlowAxis/GetNormalAxis read those columns and DetectorAxes() sets the plane from them, decomposing back to the angles; PoniRotMatrix and PoniAnglesFromMatrix are the conversion in both directions, exact on the canonical branch (rot2 in [-pi/2, pi/2]) and with a stated convention at gimbal lock. The angles stay stored rather than re-derived, so a geometry given as angles is written back as the same angles, to the bit. Delta is never inferred. In particular an arbitrary rot3 is NOT decomposed into a quarter turn plus a residual: rot3 is a fitted quantity, and a least-squares step must not be able to turn the stored image. It is set only where something states it - the detector setup, --detector-mirror-y / --detector-quarter-turns, or the value a file this system wrote records - and defaults to the identity, which makes the whole change a no-op for every existing detector and every existing file. It is a different setting from DetectorSetup::mirror_y, which flips the MODULE LAYOUT while an image is assembled and so decides what the stored pixels are. Merging the two would apply the mirror twice for every modular detector, or change the pixel content of every file written; both are ruled out. The new one earns its keep exactly where the old one is a no-op: a detector whose image arrives already assembled has no layout to flip. Both generators are signed permutations of the in-plane offset, so they preserve the distance from the PONI. That is why almost nothing downstream changes: everything needing an azimuth already goes through LabCoord, and everything that does not needs only a radius. The two hand-written copies of the rotation - XtalResidual and RingOptimizer - take the discrete part as four constants next to cos_rot3/sin_rot3, since it acts in the detector frame where rot3 acts in the laboratory and cannot be folded into it. RingOptimizer needs it despite being a radial fit: it fits the tilt, and the discrete part changes which way the tilt tips a ring. Carried as two optional CBOR keys and two detectorSpecific datasets, both back-compatible; the NXmx module axis vectors and the translation direction stop being hardcoded and are computed from it, reproducing today's values exactly at the identity. GetPoniRotMatrix is renamed GetDetectorMatrix, because it is no longer only the PONI rotation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lc5JG6kJqZoCWaoZ43JGTWgemmi states centred_to_primitive as an operator on COORDINATES and CrystalLattice::Multiply combines BASIS VECTORS, so the matrix has to be transposed on the way in - as it already is at both of the other places a gemmi Op::Rot reaches Multiply, one of them in this same file. A, B, C, I and F are symmetric matrices, so for them the transpose is a no-op and the omission never showed. R and H are not. Measured on an R-centred hexagonal lattice, ToPrimitive('R') returned 59.5 81.7 43.3 / 145.6 124.5 46.7 where the rhombohedral primitive cell is 49.3 49.3 49.3 / 60.9 60.9 60.9. What hid it is that a determinant is unchanged by transposition, so the VOLUME came out right - and most callers only take the volume. It is not only cosmetic: the result feeds the re-seating path that puts a lattice into a space group the user fixed by hand, so an R-centred lattice was handed the classifier a "primitive" cell that is not that lattice - broken for exactly the centring whose setting most needs re-seating. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N (cherry picked from commit 6ca00e927d664e870515c04164defa81d8a18725)A screw's control class is the complement of its absent class on one axial row, so the two move together: a candidate that predicts more of the row absent leaves fewer reflections to be judged against. The Beta tail AbsenceEvidence computes grows with that control count, so the stricter candidate was charged for the very reflections it correctly called extinct, and a group whose predicted-absent class is a strict superset of another's - with the extra reflections equally dead - could score LOWER than the group that explains only part of the row. Two mechanisms, one dataset each. * The control-count minimum gated the row MEAN, which is the scale the zone's evidence is stated in, on the same count as the row MEDIAN, which is a violation threshold. A candidate could therefore forfeit a whole zone by being right: on a tetragonal 4_1/4_3 wedge, thirteen 00l reflections measured at 0.1% of the two l = 4n beside them scored ZERO because only two control reflections were left, while the nine of them a 4_2 also predicts absent scored 38.7. The mean now stands on two, the median still on three. * The Beta tail is replaced, for a SCREW zone only, by its b -> infinity limit - the same statistic with the control count dropped. A p-value computed against each candidate's own null is not one scale across candidates; what is left is the likelihood ratio of the absent class against Wilson at the row's own mean, which is a sum over reflections and therefore comparable. Asymptotically it is n_absent * (log(1/ubar) - 1), so an equally dead superset can no longer score lower. Measured on a tetragonal 4_1/4_3 crystal: 29 dead 00l against 8 control read 47.7 nats where a subset of 19 of them against 18 control read 55.5. The centring statistic is untouched: its control is the whole present population, not the complement of a claim on one row, so neither mechanism applies to it. Measured. Analytic superset monotonicity, on a 176368-point grid with no calibration and no corpus: 1.29% violations -> 0.00%, exactly monotone wherever both candidates keep a control class. Over 112 stored merges (open, in-house and private arms) exactly ONE decision changes and it is a gain, a 4_2 -> 4_1/4_3 swap on a deposited 4_3; the screw column over the deposited arm goes 33/41 -> 34/41 with zero screw over-calls before and after, and the four centring outcomes are bit-identical. An eight-wedge reproducer whose 00l row a strict candidate eats goes from two mis-calls to none, end to end. The refused side moves the safe way: 103 zones the old statistic scored negative go lower, none turns positive, and the minimum evidence of an adopted screw rises 20.4 -> 23.8 against an unchanged bound of 20. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53NCalibrateFromProfile/CalibrateFromSpots/WritePoniFile sat in rugnux/, so the only way to reach them was to link the Rugnux library - which drags in JFJochWriter and gemmi. Nothing in them needs either: the includes are all common/ and image_analysis/geom_refinement/, next to the RingOptimizer and RingsFromProfile they call. Moving them to image_analysis/geom_refinement/PowderCalibration.{h,cpp} puts the powder fit beside the rest of the geometry refinement and makes it reachable from anything that already links JFJochImageAnalysis - the receiver and so the broker included, which is what an online geometry calibration would need. Pure move: the file contents differ from their previous form only in the include paths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NfuDvf5ipV3Hi8TiCUKD27A calibration run produced one file, and it was a pyFAI PONI - which pyFAI and its neighbours read and nothing in this system does. Carrying the answer back into the instrument meant a person reading numbers off a printed report and retyping them into a dataset_settings body, and the report is where the two points a "beam centre" can mean are easiest to confuse. So write <prefix>.json beside it. Its "dataset_settings" member holds the geometry under the property names broker/jfjoch_api.yaml gives them and holds nothing else, so it is a valid dataset_settings body as it stands: curl -X POST -H 'Content-Type: application/json' \ -d "$(jq -c .dataset_settings det.json)" http://broker:5232/start beam_x_pxl is the PONI, as everywhere here. The three poni_rot*_rad ride along whenever any is non-zero and are left out when all are zero: a body without them does not leave the tilt unstated, it states a FLAT detector, so they travel together or not at all - the same rule the report's JFJOCH_DATASET_SETTINGS block already follows. The "calibration" member holds what the run knows about that geometry: the residual, the fit's own sigmas and the correlation between the tilt and the beam centre, whether the tilt cleared its significance test or was declined and pinned, where the direct beam lands, and where the spots independently put the beam. A calibration that has gone wrong looks exactly like one that has not until those are read, and a machine-readable file that carried only the geometry would be the easiest possible way to feed a bad one into an instrument. Tested against the model generated from the spec rather than against a list of field names written out by hand: the file's dataset_settings member is parsed into org::openapitools::server::model::Dataset_settings and validated, so a field renamed or newly required in jfjoch_api.yaml fails here rather than at someone's POST. The tilted and untilted branches are both covered, and the PONI/direct-beam distinction is asserted rather than assumed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NfuDvf5ipV3Hi8TiCUKD27The guard that judges the post-refined pass against the header-geometry pass reads completeness off each pass's space-group search merge. That merge does not count possible reflections, so the quotient is 0.0 on both sides and every de-novo rotation report has been printing completeness 0.0% vs 0.0%, CC1/2 before corrections 0.994 vs 0.993 as though a comparison had happened. On the 28-dataset in-house battery that is all 28 runs. The number is not just uninformative, it is a test that did not run: "completeness above 100% means the cell is wrong" is the guard's wrong-cell arm, and it cannot fire against a constant zero, so only the CC1/2 arm decides - which is computed on whatever data the pass kept, so a pass that discards a large part of the sweep can post an equal CC1/2 and win. Carry a measured flag beside the number. The arm is skipped and the log and PASS_DECISION say "completeness not measured" where there is nothing to read. No behaviour changes: the arm could not fire before either. The next commit gives the merge the count so it can. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53NThe merged mmCIF carried the R-free flag in `_refln.status_free` as 1/0 and wrote `o` into `_refln.status` on every row. `_refln.status_free` is not in the PDBx/mmCIF dictionary - it is absent from mmcif_pdbx v4.0, v5.0, v5.288 and v5.362, from CCP4's and phenix's shipped copies, and its wwPDB item page is a 404 - and no deposited structure-factor file uses it. `_refln.status` is the item that carries the free set, with `f` for a test reflection and `o` for a working one; on a deposition that also carries `_refln.pdbx_r_free_flag` the two agree exactly. Measured on a real merged file this run wrote: CCP4 cif2mtz refuses the file outright - "Unexpected context type for category REFLN" from its dictionary-validating parser, exit 1, a 12-byte truncated MTZ. Dropping the non-dictionary column is what fixes it: the same file without it converts. gemmi converts, but its cif2mtz spec knows only `status` and `pdbx_r_free_flag`, so FreeR_flag comes out 1 everywhere and the free set is silently lost - R-free would then be computed on the working set. phenix worked, but only by a filename heuristic matching the words "status" and "free". Writing `f` while keeping the extra column is worse than either, because phenix then finds two candidate free-set arrays and refuses the file, so the column goes in the same change. After it, all three read the same 5% test set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53NGetElementPosFast_step took the snake parity from GetElementPosSlow_step, which is the DISPLAY row: it is the acquisition row r = image / n_fast, flipped to (n_slow-1) - r when the slow step is negative. So for a negative slow step the parity it hands back is parity(n_slow-1) XOR parity(r), and with an even n_slow that is inverted on every row - the whole raster comes out mirrored along the fast axis. An odd n_slow leaves it correct, so the same scan collected with 20 or 25 images disagreed about where image 0 sat: n_fast=5, fast +1.5 um, slow -2.5 um, snake on gives images 0..4 at fast index 4,3,2,1,0 with 4 rows and 0,1,2,3,4 with 5 rows. A positive slow step was correct at both counts, and so was every non-snake configuration. Snake means the stage reverses direction on alternate rows in acquisition order, so the parity has to come from the acquisition row. Taking it from image_number / n_fast directly makes the fast index independent of the slow axis and of the row count, and drops the call into the display-row function that caused the coupling. vertical_scan only relabels which axis is fast, so it was wrong in exactly the same way and is fixed by the same line. Affected files: written by an affected build, with snake on, a negative grid slow step (step_y for a horizontal scan, step_x for a vertical one), and an even number of rows. Their /entry/sample/transformations/grid_scan_x or _y is mirrored along the fast axis, as was the grid map in the frontend and the viewer - both mirrored together, which is why neither showed it. Tests: the interaction of snake with the step signs was never asserted, only each in isolation, so add a table over snake x {+,- fast step} x {+,- slow step} x {even, odd row count} x {horizontal, vertical} asserting positions, plus a case running one affected configuration through GetXContainer_m / GetYContainer_m and Rearrange. Every pre-existing assertion is unchanged and still passes; only the four negative-slow, even-row cells of the product move. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53NView command line instructions
Checkout
From your project repository, check out a new branch and test the changes.