spindle: the per-image severity travels the whole data path, and absence travels with it

The score was computed and then thrown away - carried on the per-image message but transported
nowhere - so the automation it exists for could not read it. It now flows like bkg_estimate at
every layer: CBOR (per-image key, END-block run mean and per-image array), HDF5 (per-image
/entry/MX/spindleBlindFraction in the data files, the array and spindleBlindFractionMean in the
master), read-back into a re-opened dataset, the scan result, the receiver plots, the REST plot
and scan_result schemas, and the viewer and frontend plot menus.

Absence is load-bearing and every transport keeps it distinguishable from a measured zero: the
CBOR key is simply missing, the HDF5 array holds NaN, and read-back turns NaN back into an
absent optional rather than a value. A pipeline that read 0 where the truth is "no value" would
take exactly the wrong action - a measured 0 says one sweep loses nothing, absence says nobody
could look, and the second must engage the recovery protocol while the first must not. The
round-trip tests pin all three states through CBOR and through a written-and-reopened file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N
This commit is contained in:
2026-09-04 10:59:05 +02:00
co-authored by Claude Opus 5
parent bc3d693721
commit 26a82ddc72
31 changed files with 168 additions and 5 deletions
+10
View File
@@ -557,6 +557,7 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen
dataset->indexing_result = master_file->ReadOptVector<float>("/entry/MX/imageIndexed");
dataset->bkg_estimate = master_file->ReadOptVector<float>("/entry/MX/bkgEstimate");
dataset->spindle_blind_fraction = master_file->ReadOptVector<float>("/entry/MX/spindleBlindFraction");
dataset->ice_ring_score = master_file->ReadOptVector<float>("/entry/MX/iceRingScore");
dataset->resolution_estimate = master_file->ReadOptVector<float>("/entry/MX/resolutionEstimate");
dataset->profile_radius = master_file->ReadOptVector<float>("/entry/MX/profileRadius");
@@ -700,6 +701,10 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen
data_file, "/entry/MX/bkgEstimate",
number_of_images, fimages);
ReadVector(dataset->spindle_blind_fraction,
data_file, "/entry/MX/spindleBlindFraction",
number_of_images, fimages);
ReadVector(dataset->ice_ring_score,
data_file, "/entry/MX/iceRingScore",
number_of_images, fimages);
@@ -1327,6 +1332,11 @@ void HDF5MetadataSource::FillPerImage(DataMessage &message, int64_t requested_im
message.indexing_lattice_count = dataset->indexing_lattice_count[image_number];
if (dataset->bkg_estimate.size() > image_number)
message.bkg_estimate = dataset->bkg_estimate[image_number];
// NaN is how the file stores a frame with no value; the optional must come back absent, not
// carrying a NaN, because absence is the CANNOT-SAY trigger state and a value is not.
if (dataset->spindle_blind_fraction.size() > image_number
&& std::isfinite(dataset->spindle_blind_fraction[image_number]))
message.spindle_blind_fraction = dataset->spindle_blind_fraction[image_number];
if (dataset->ice_ring_score.size() > image_number)
message.ice_ring_score = dataset->ice_ring_score[image_number];
if (dataset->efficiency.size() > image_number)