A grid scan and a powder calibration each produce an answer that is not a per-image number,
and neither had anywhere to go. ScanResult gains two top-level optionals, grid and
calibration, in the same idiom the three rotation_* members already use: a mode's payload is
present when that mode ran and absent otherwise. images stays required, so every existing
/result/scan client - the python client and the beamline scripts on it - is untouched.
No oneOf or discriminator. The spec contains none today, so polymorphism is unexercised across
all three generators at once, and the only thing it would buy is making "exactly one payload"
structurally true rather than true by convention. powder_calibration_output has been fully
specified and referenced by nothing since it was written; the calibration member reuses it.
The crystal list travels the same three hops a per-image quantity does. In the CBOR end
message grid_crystals is an array of maps keyed by field name - the shape a spot already uses
- so a reader that does not know a field skips it and the record survives a field being added;
the deserializer steps over an unknown key rather than reading its payload as the next one. In
the HDF5 master it is parallel 1-D datasets under /entry/MX/crystals, one per field, because
crystals are numbered rather than named and a group per crystal would put the index in the
path. The reader reads them back so a stored raster re-opens with what it found.
Nothing caps the list at one. A raster can find several crystals and the ordering carries the
answer: sorted by score, best first.
angle_deg is an axis, not a direction. It lies in [0, 180) and wraps, so 179 and 0 are
adjacent and two of these do not average arithmetically - a pair of nearly parallel needles
averaged across the wrap comes out as a right angle. That is stated at every layer it passes
through, since it is the kind of field a consumer will reach for without reading the producer.
The producer side is not wired yet: nothing fills ScanResult::grid or EndMessage::grid_crystals
here. The seam is receiver/JFJochReceiver.cpp, where rotation_lattice is filled in
EndDataCollection and GetFinalStatistics.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N