From 748d14d2cdcc31a453a02dda28a3ba2879fbc516 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Tue, 8 Sep 2026 07:42:23 +0200 Subject: [PATCH] file format: the ice ring ratio is stored and sent under its own name, and the retired one is still read Completes the rename the C++ identifiers already carried. Producers now emit ice_ring_ratio / ice_ring_ratio_mean over CBOR and write /entry/MX/iceRingRatio and iceRingRatioMean; both readers accept the retired ice_ring_score / iceRingScore spellings as well, so nothing that exists on disk or on the wire stops being readable. The REST plot_type gains ice_ring_ratio and KEEPS ice_ring_score, both mapping to the same plot, so no client breaks. --ice-min-score is deliberately unchanged: it is a threshold on the ratio, it is the one surface a user types, and this program has 91 long options and no aliases. The fallback is not a formality. rugnux --mode scale reads the stored per-image value to reproduce the ice gate the writing run applied, and an unread dataset does not fail loudly - ice_n == 0 sends the gate to its fail-safe branch, "ice present". Demonstrated on a clean crystal whose stored file uses the old names: with the fallback the gate reads 1.06 against a 1.50 threshold and skips ice handling; with the fallback removed the same file has 272056 of 1032493 reflections (26%) excluded from the scale fit, and ISa, R_meas, I/sigma, SIGANO and both error-model terms all move. That is a silent change to merged intensities on files already written, which is why the two new CBOR test cases were each checked to FAIL when their fallback is taken out rather than merely to pass. Verified: same binary on an old-name and a new-name copy of one file gives byte-identical .hkl, .mtz and unmerged .mtz and an identical report; a file written after the rename scales to the same bytes again; the master and the data-file read paths were each exercised with both spellings; and the live API serves the same plot under either name. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N --- broker/OpenAPIConvert.cpp | 4 +- broker/jfjoch_api.yaml | 1 + common/JFJochMessages.h | 5 +- common/ScalingSettings.h | 2 +- docs/CBOR.md | 6 +- docs/CPU_DATA_ANALYSIS.md | 16 +++-- docs/CPU_DATA_ANALYSIS_IMAGE.md | 4 +- docs/HDF5.md | 4 +- frame_serialize/CBORStream2Deserializer.cpp | 9 ++- frame_serialize/CBORStream2Serializer.cpp | 6 +- .../src/components/DataProcessingPlot.tsx | 1 + .../src/components/DataProcessingPlots.tsx | 2 +- reader/HDF5MetadataSource.cpp | 12 +++- tests/CBORTest.cpp | 70 +++++++++++++++++++ writer/HDF5DataFilePluginMX.cpp | 2 +- writer/HDF5NXmx.cpp | 4 +- 16 files changed, 121 insertions(+), 27 deletions(-) diff --git a/broker/OpenAPIConvert.cpp b/broker/OpenAPIConvert.cpp index cf40188ca..978feaa42 100644 --- a/broker/OpenAPIConvert.cpp +++ b/broker/OpenAPIConvert.cpp @@ -899,7 +899,9 @@ PlotType ConvertPlotType(const std::optional& input) { "Plot type is compulsory paramater"); if (input == "bkg_estimate") return PlotType::BkgEstimate; if (input == "spindle_blind_fraction") return PlotType::SpindleBlindFraction; - if (input == "ice_ring_score") return PlotType::IceRingRatio; + // ice_ring_score is the retired spelling of ice_ring_ratio, kept so existing clients keep + // working; both name the same plot. + if (input == "ice_ring_ratio" || input == "ice_ring_score") return PlotType::IceRingRatio; if (input == "protein_score") return PlotType::ProteinScore; if (input == "ice_score") return PlotType::IceScore; if (input == "azint") return PlotType::AzInt; diff --git a/broker/jfjoch_api.yaml b/broker/jfjoch_api.yaml index bac2f7cd2..da6a44297 100644 --- a/broker/jfjoch_api.yaml +++ b/broker/jfjoch_api.yaml @@ -121,6 +121,7 @@ components: - image_scale_factor - image_scale_cc - compression_ratio + - ice_ring_ratio - ice_ring_score - protein_score - ice_score diff --git a/common/JFJochMessages.h b/common/JFJochMessages.h index 514d2863d..26ec7521d 100644 --- a/common/JFJochMessages.h +++ b/common/JFJochMessages.h @@ -129,7 +129,8 @@ struct DataMessage { std::optional bkg_estimate; // Strongest ice ring over the smooth radial background. A RATIO, so 1 means no ice and it is // unbounded above - the opposite convention to the two *_score members below, which is why the - // name says ratio. Still travels the wire and the file as "ice_ring_score"/iceRingScore. + // name says ratio. Wire key "ice_ring_ratio", dataset /entry/MX/iceRingRatio; both readers also + // accept the retired "ice_ring_score"/iceRingScore spellings. std::optional ice_ring_ratio; // How much of a single sweep's blind cone this orientation makes unrecoverable: 0 = one sweep // about the spindle reaches everything the point group can give, 1 = a short lattice row lies on @@ -477,7 +478,7 @@ struct EndMessage { // v_bkg_estimate above - it was shipped that way and the CBOR key is part of the stream format. std::vector ice_ring_ratio; // Run mean of the above, the single "how icy was this dataset" number (1 = no ice). The - // bkg_estimate scalar's counterpart; written to /entry/MX/iceRingScoreMean. + // bkg_estimate scalar's counterpart; written to /entry/MX/iceRingRatioMean. std::optional ice_ring_ratio_mean; // Per-image sweep-quality code: 0 = the image falls in no flagged range, otherwise 1 + the diff --git a/common/ScalingSettings.h b/common/ScalingSettings.h index ac25888c9..364701d4d 100644 --- a/common/ScalingSettings.h +++ b/common/ScalingSettings.h @@ -70,7 +70,7 @@ class ScalingSettings { // better on weak. --no-expected-variance-merge restores the old observed-sigma weighting. bool expected_variance_merge = true; - // Minimum measured ice strength (iceRingScore, 1 = no ice) before any ice-ring handling is applied + // Minimum measured ice strength (iceRingRatio, 1 = no ice) before any ice-ring handling is applied // at all. The eleven fixed hexagonal bands cover 16-26% of the unique reflections at typical // resolutions REGARDLESS of whether the crystal has ice, so flagging unconditionally taxes clean // data for nothing. 0 disables the gate (always handle ice, the previous behaviour). diff --git a/docs/CBOR.md b/docs/CBOR.md index 4837df0b4..f3cf80a99 100644 --- a/docs/CBOR.md +++ b/docs/CBOR.md @@ -226,7 +226,7 @@ See [DECTRIS documentation](https://github.com/dectris/documentation/tree/main/s | packets_expected | uint64 | Number of packets expected per image (in units of 2 kB) | | | | packets_received | uint64 | Number of packets received per image (in units of 2 kB) | | | | bkg_estimate | float | Mean value for pixels in resolution range from 3.0 to 5.0 A \[photons\] | | | -| ice_ring_score | float | Strongest hexagonal-ice ring intensity over the smooth radial background (1 = no ice) | | | +| ice_ring_ratio | float | Strongest hexagonal-ice ring intensity over the smooth radial background (1 = no ice). Was `ice_ring_score`; readers accept both, producers emit this one | | | | protein_score | float | Protein diffraction detection score, 0 to 1, saturating (0 = none, 1 = certain) | | | | ice_score | float | Crystalline ice detection score, 0 to 1, saturating (0 = none, 1 = certain) | | | | spindle_blind_fraction | float | Fraction (0-1) of a rotation sweep's blind cone this orientation makes unrecoverable, as a lone-2-fold worst-case bound; >= 0.5 should engage a recovery protocol, and ABSENT means the frame could not be assessed, which automation must treat the same way | | | @@ -324,11 +324,11 @@ See [DECTRIS documentation](https://github.com/dectris/documentation/tree/main/s | image_indexed | Array(uint8) | Per-image indexing result; 0 = not indexed, nonzero = indexed | | | v_bkg_estimate | Array(float) | Per-image background estimate | | | v_spindle_blind_fraction | Array(float) | Per-image spindle_blind_fraction; NaN where the frame had no value (which is "cannot say", not zero) | | -| ice_ring_score | Array(float) | Per-image strongest ice-ring intensity over the smooth radial background (1 = no ice) | | +| ice_ring_ratio | Array(float) | Per-image strongest ice-ring intensity over the smooth radial background (1 = no ice). Was `ice_ring_score`; readers accept both | | | v_protein_score | Array(float) | Per-image protein diffraction detection score, 0 to 1 | | | v_ice_score | Array(float) | Per-image crystalline ice detection score, 0 to 1 | | | spot_count_ice_control | Array(float) | Per-image spot count in the ice-free flanks beside the hexagonal rings, rescaled to the ring bands' q width | | -| ice_ring_score_mean | float | Mean ice-ring score for the whole run (1 = no ice) | | +| ice_ring_ratio_mean | float | Mean ice-ring ratio for the whole run (1 = no ice). Was `ice_ring_score_mean`; readers accept both | | | protein_score | float | Mean protein detection score for the whole run | | | ice_score | float | Mean ice detection score for the whole run | | | profile_radius | Array(float) | Per-image profile radius \[Angstrom^-1\] | | diff --git a/docs/CPU_DATA_ANALYSIS.md b/docs/CPU_DATA_ANALYSIS.md index 062beb5be..3c9bca136 100644 --- a/docs/CPU_DATA_ANALYSIS.md +++ b/docs/CPU_DATA_ANALYSIS.md @@ -48,10 +48,18 @@ the same physics on opposite conventions and the name is the only thing that say So a score is bounded and 1 is certainty; a ratio is unbounded and 1 is nothing. `ice_score` and `ice_ring_ratio` both describe ice and their extremes are opposite ends of the scale. -The C++ identifiers follow this. The **stored and wire names do not yet**: the ratio is still -written as `/entry/MX/iceRingScore`, sent as the CBOR key `ice_ring_score`, served as the -`ice_ring_score` plot type, and gated by `--ice-min-score`. Renaming those changes stored files, the -stream format, the REST API and a CLI flag, so it is a separate decision. +The identifiers, the stored names and the wire names all follow it. The ice ring ratio was called +`ice_ring_score` before; nothing that reads an old file or an old stream lost anything in the rename: + +| Surface | Now | Retired spelling | +|---|---|---| +| HDF5 | `/entry/MX/iceRingRatio`, `iceRingRatioMean` | `iceRingScore`, `iceRingScoreMean` — **still read**, so a file written before the rename still opens | +| CBOR | `ice_ring_ratio`, `ice_ring_ratio_mean` | `ice_ring_score`, `ice_ring_score_mean` — **still decoded**, so an older producer's stream still reads | +| REST `plot_type` | `ice_ring_ratio` | `ice_ring_score` — **still accepted**, and not scheduled for removal; both name the same plot | +| CLI | `--ice-min-score` | *unchanged*, deliberately. It is a threshold on the ratio, and it is the one surface a user types | + +Writers and producers emit the new spelling only. The old one is read but never written, which is +what makes the rename a one-way migration rather than a flag day. ## References diff --git a/docs/CPU_DATA_ANALYSIS_IMAGE.md b/docs/CPU_DATA_ANALYSIS_IMAGE.md index 9e08c3799..f54a207fa 100644 --- a/docs/CPU_DATA_ANALYSIS_IMAGE.md +++ b/docs/CPU_DATA_ANALYSIS_IMAGE.md @@ -427,7 +427,7 @@ Both constants carry a mechanism. A quantile from the middle of the distribution ### 3.7 Detection scores: is there protein here, is there ice here -`iceRingScore` above is a *magnitude* — a ratio, unbounded, answering "how strong is the worst ring". +`iceRingRatio` above is a *magnitude* — a ratio, unbounded, answering "how strong is the worst ring". Two further per-image scalars answer a different question, the one a grid scan actually asks: `proteinScore` and `iceScore`, both in $[0,1]$ and both **saturating**, so a superb crystal and a barely-diffracting one score the same. They are **detection** scores, not quality measures — the @@ -450,7 +450,7 @@ the pooled false-positive rate over the remaining ones below $5\times10^{-4}$. **Ice** reaches the frame two ways, and they need different evidence, so two channels are computed and the stronger one wins. The *radial* channel reads the plain azimuthal profile — not the -peak-excluded background the `iceRingScore` uses, because it needs the profile's own standard +peak-excluded background the `iceRingRatio` uses, because it needs the profile's own standard deviation, which that background does not carry. Each band is read as an excess over a running median (half-window 6 bins, which rejects a 3–5 bin powder ring but follows the ~40-bin vitreous halo, so the halo cannot score), in units of the bin mean's own error $\sigma/\sqrt{n}$ smoothed diff --git a/docs/HDF5.md b/docs/HDF5.md index 771ff4e65..1267fab6c 100644 --- a/docs/HDF5.md +++ b/docs/HDF5.md @@ -345,7 +345,7 @@ In legacy/VDS mode these live in the data files and are linked/virtual-stacked i | `resolutionEstimate` | Å | resolution the merged data are predicted to reach, from this image's spots alone | | `integratedReflections` | | number of integrated reflections | | `bkgEstimate` | photons | mean background in the 3–5 Å resolution band | -| `iceRingScore` | ratio | strongest hexagonal-ice ring intensity over the smooth radial background (1 = no ice) | +| `iceRingRatio` | ratio | strongest hexagonal-ice ring intensity over the smooth radial background (1 = no ice). Was `iceRingScore`; the reader accepts either, so a file written before the rename still opens | | `proteinScore` | | protein diffraction detection score, 0 to 1, saturating (0 = none, 1 = certain) | | `iceScore` | | crystalline ice detection score, 0 to 1, saturating (0 = none, 1 = certain) | | `spindleBlindFraction` | fraction (0-1) | how much of a rotation sweep's blind cone this orientation makes unrecoverable, as a lone-2-fold worst-case bound; NaN = the frame could not be assessed, which automation must treat like a value at or above the 0.5 trigger, never as 0 | @@ -372,7 +372,7 @@ variants. | `bkgEstimateMean` | photons | mean background over the run | | `spindleBlindFractionMean` | fraction (0-1) | mean `spindleBlindFraction` over the frames that had one | | `spindleLostUniqueFraction` | fraction (0-1) | unique reflections (to the run's resolution limit) the mounting made unmeasurable, exact under the measured point group and indexed orientation; offline (rugnux) only | -| `iceRingScoreMean` | ratio | mean `iceRingScore` over the run — the single "how icy was this dataset" number (1 = no ice) | +| `iceRingRatioMean` | ratio | mean `iceRingRatio` over the run — the single "how icy was this dataset" number (1 = no ice). Was `iceRingScoreMean` | | `proteinScoreMean` | | mean `proteinScore` over the run | | `iceScoreMean` | | mean `iceScore` over the run | | `scoreBeamCenterX`, `scoreBeamCenterY` | pixel | the beam centre `proteinScore` and `iceScore` were computed with. Both read *d* out of the geometry, and `/entry/instrument/detector/beam_center_x`/`_y` carries the **refined** centre where refinement ran, so without this pair a rescoring could not tell an algorithm disagreement from a geometry one | diff --git a/frame_serialize/CBORStream2Deserializer.cpp b/frame_serialize/CBORStream2Deserializer.cpp index b37f54281..ec4d6577a 100644 --- a/frame_serialize/CBORStream2Deserializer.cpp +++ b/frame_serialize/CBORStream2Deserializer.cpp @@ -822,7 +822,9 @@ namespace { message.packets_received = GetCBORUInt(value); else if (key == "bkg_estimate") message.bkg_estimate = GetCBORFloat(value); - else if (key == "ice_ring_score") + // Renamed from ice_ring_score; both spellings are accepted so a stream from an older + // producer still decodes. Optional keys are back-compatible either way, so no version bump. + else if (key == "ice_ring_ratio" || key == "ice_ring_score") message.ice_ring_ratio = GetCBORFloat(value); else if (key == "protein_score") message.protein_score = GetCBORFloat(value); @@ -1517,11 +1519,12 @@ namespace { message.ice_score = GetCBORFloat(value); else if (key == "v_spindle_blind_fraction") GetCBORFloatArray(value, message.v_spindle_blind_fraction); - else if (key == "ice_ring_score") + // Both spellings, as in the image block above. + else if (key == "ice_ring_ratio" || key == "ice_ring_score") GetCBORFloatArray(value, message.ice_ring_ratio); else if (key == "spot_count_ice_control") GetCBORFloatArray(value, message.spot_count_ice_control); - else if (key == "ice_ring_score_mean") + else if (key == "ice_ring_ratio_mean" || key == "ice_ring_score_mean") message.ice_ring_ratio_mean = GetCBORFloat(value); else if (key == "profile_radius") GetCBORFloatArray(value, message.profile_radius); diff --git a/frame_serialize/CBORStream2Serializer.cpp b/frame_serialize/CBORStream2Serializer.cpp index fb7c5f2ff..9a6a8f8e4 100644 --- a/frame_serialize/CBORStream2Serializer.cpp +++ b/frame_serialize/CBORStream2Serializer.cpp @@ -829,8 +829,8 @@ void CBORStream2Serializer::SerializeSequenceEnd(const EndMessage& message) { CBOR_ENC(mapEncoder, "protein_score", message.protein_score); CBOR_ENC(mapEncoder, "ice_score", message.ice_score); CBOR_ENC(mapEncoder, "v_spindle_blind_fraction", message.v_spindle_blind_fraction); - CBOR_ENC(mapEncoder, "ice_ring_score", message.ice_ring_ratio); - CBOR_ENC(mapEncoder, "ice_ring_score_mean", message.ice_ring_ratio_mean); + CBOR_ENC(mapEncoder, "ice_ring_ratio", message.ice_ring_ratio); + CBOR_ENC(mapEncoder, "ice_ring_ratio_mean", message.ice_ring_ratio_mean); CBOR_ENC(mapEncoder, "spot_count_ice_control", message.spot_count_ice_control); CBOR_ENC(mapEncoder, "profile_radius", message.profile_radius); CBOR_ENC(mapEncoder, "mosaicity", message.mosaicity); @@ -926,7 +926,7 @@ void CBORStream2Serializer::SerializeImageInternal(CborEncoder &mapEncoder, cons CBOR_ENC(mapEncoder, "packets_expected", message.packets_expected); CBOR_ENC(mapEncoder, "packets_received", message.packets_received); CBOR_ENC(mapEncoder, "bkg_estimate", message.bkg_estimate); - CBOR_ENC(mapEncoder, "ice_ring_score", message.ice_ring_ratio); + CBOR_ENC(mapEncoder, "ice_ring_ratio", message.ice_ring_ratio); CBOR_ENC(mapEncoder, "protein_score", message.protein_score); CBOR_ENC(mapEncoder, "ice_score", message.ice_score); CBOR_ENC(mapEncoder, "spindle_blind_fraction", message.spindle_blind_fraction); diff --git a/frontend/src/components/DataProcessingPlot.tsx b/frontend/src/components/DataProcessingPlot.tsx index aa3745f8f..ce7e2c7c0 100644 --- a/frontend/src/components/DataProcessingPlot.tsx +++ b/frontend/src/components/DataProcessingPlot.tsx @@ -52,6 +52,7 @@ function AxisTypeY(plot: plot_type) : string | ReactNode { case plot_type.SPOT_COUNT_INDEXED: case plot_type.SPOT_COUNT_ICE: return "Count"; + case plot_type.ICE_RING_RATIO: case plot_type.ICE_RING_SCORE: return "Ratio"; case plot_type.PROTEIN_SCORE: diff --git a/frontend/src/components/DataProcessingPlots.tsx b/frontend/src/components/DataProcessingPlots.tsx index f9a1ab602..a5dba85f4 100644 --- a/frontend/src/components/DataProcessingPlots.tsx +++ b/frontend/src/components/DataProcessingPlots.tsx @@ -50,7 +50,7 @@ function DataProcessingPlots({type: initialType, height}: MyProps) { Spot count low res. Spot count indexed Spot count ice ring - Ice ring score + Ice ring ratio Protein detection score Ice detection score Azimuthal integration profile diff --git a/reader/HDF5MetadataSource.cpp b/reader/HDF5MetadataSource.cpp index 30653eeed..71fee6092 100644 --- a/reader/HDF5MetadataSource.cpp +++ b/reader/HDF5MetadataSource.cpp @@ -575,7 +575,12 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen dataset->indexing_result = master_file->ReadOptVector("/entry/MX/imageIndexed"); dataset->bkg_estimate = master_file->ReadOptVector("/entry/MX/bkgEstimate"); dataset->spindle_blind_fraction = master_file->ReadOptVector("/entry/MX/spindleBlindFraction"); - dataset->ice_ring_ratio = master_file->ReadOptVector("/entry/MX/iceRingScore"); + // Renamed from iceRingScore. Accept either for backward compatibility - a file + // written before the rename must still read, and --mode scale reads this to + // reproduce the ice gate the writing run applied. + dataset->ice_ring_ratio = master_file->ReadOptVector("/entry/MX/iceRingRatio"); + if (dataset->ice_ring_ratio.empty()) + dataset->ice_ring_ratio = master_file->ReadOptVector("/entry/MX/iceRingScore"); dataset->protein_score = master_file->ReadOptVector("/entry/MX/proteinScore"); dataset->ice_score = master_file->ReadOptVector("/entry/MX/iceScore"); dataset->score_beam_center_x = master_file->GetOptFloat("/entry/MX/scoreBeamCenterX"); @@ -726,8 +731,11 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen data_file, "/entry/MX/spindleBlindFraction", number_of_images, fimages); + // Either spelling, as in the master above. ReadVector(dataset->ice_ring_ratio, - data_file, "/entry/MX/iceRingScore", + data_file, + data_file.Exists("/entry/MX/iceRingRatio") + ? "/entry/MX/iceRingRatio" : "/entry/MX/iceRingScore", number_of_images, fimages); ReadVector(dataset->protein_score, diff --git a/tests/CBORTest.cpp b/tests/CBORTest.cpp index 0a49a535a..df38b145b 100644 --- a/tests/CBORTest.cpp +++ b/tests/CBORTest.cpp @@ -1483,3 +1483,73 @@ TEST_CASE("CBORSerialize_End_Transformations", "[CBOR]") { CHECK(!chain[1].IsConstant()); CHECK(chain[2].IsConstant()); } + +namespace { + // "ice_ring_ratio" replaced its predecessor "ice_ring_score", and the two spellings are the same + // length - so an old-format stream is made from a new one by substituting the key text in place, + // leaving the CBOR text-string length prefix correct. That is what lets one test drive the + // deserializer with a stream this serializer can no longer produce. + void RetireIceRingKeySpelling(std::vector &buffer, size_t size) { + const std::string now = "ice_ring_ratio"; + const std::string before = "ice_ring_score"; + REQUIRE(now.size() == before.size()); + for (size_t i = 0; i + now.size() <= size; i++) + if (std::equal(now.begin(), now.end(), buffer.begin() + i)) + std::copy(before.begin(), before.end(), buffer.begin() + i); + } + + // Serialize an image and an end message carrying the ice ring ratio, optionally rewriting the key + // to the retired spelling, and return what the deserializer makes of each. + std::pair IceRingRatioRoundTrip(bool old_spelling) { + std::vector pixels(512, 7); + const CompressedImage image(pixels, 256, 2); + + std::vector buffer(1024 * 1024); + CBORStream2Serializer image_serializer(buffer.data(), buffer.size()); + DataMessage msg{.number = 11, .image = image}; + msg.ice_ring_ratio = 2.75f; + REQUIRE_NOTHROW(image_serializer.SerializeImage(msg)); + if (old_spelling) + RetireIceRingKeySpelling(buffer, image_serializer.GetBufferSize()); + auto image_out = CBORStream2Deserialize(buffer.data(), image_serializer.GetBufferSize()); + REQUIRE(image_out); + REQUIRE(image_out->data_message); + + std::vector end_buffer(1024 * 1024); + CBORStream2Serializer end_serializer(end_buffer.data(), end_buffer.size()); + EndMessage end{.max_image_number = 3}; + end.ice_ring_ratio = {1.0f, 2.5f, 4.25f}; + end.ice_ring_ratio_mean = 2.5f; + REQUIRE_NOTHROW(end_serializer.SerializeSequenceEnd(end)); + if (old_spelling) + RetireIceRingKeySpelling(end_buffer, end_serializer.GetBufferSize()); + auto end_out = CBORStream2Deserialize(end_buffer.data(), end_serializer.GetBufferSize()); + REQUIRE(end_out); + REQUIRE(end_out->end_message); + + return {*image_out->data_message, *end_out->end_message}; + } +} + +// The spelling this serializer emits. +TEST_CASE("CBORDeserialize_IceRingRatio_NewSpelling", "[CBOR]") { + const auto [image, end] = IceRingRatioRoundTrip(false); + REQUIRE(image.ice_ring_ratio.has_value()); + CHECK(image.ice_ring_ratio.value() == Catch::Approx(2.75f)); + CHECK(end.ice_ring_ratio == std::vector{1.0f, 2.5f, 4.25f}); + REQUIRE(end.ice_ring_ratio_mean.has_value()); + CHECK(end.ice_ring_ratio_mean.value() == Catch::Approx(2.5f)); +} + +// A stream from a producer that predates the rename. It must still decode, and into the same members: +// the writer turns these into the per-image datasets, and rugnux --mode scale reads them back to +// reproduce the ice gate the writing run applied. Silently losing them would not fail here - it would +// change which reflections leave the scale fit, months later. +TEST_CASE("CBORDeserialize_IceRingRatio_RetiredSpelling", "[CBOR]") { + const auto [image, end] = IceRingRatioRoundTrip(true); + REQUIRE(image.ice_ring_ratio.has_value()); + CHECK(image.ice_ring_ratio.value() == Catch::Approx(2.75f)); + CHECK(end.ice_ring_ratio == std::vector{1.0f, 2.5f, 4.25f}); + REQUIRE(end.ice_ring_ratio_mean.has_value()); + CHECK(end.ice_ring_ratio_mean.value() == Catch::Approx(2.5f)); +} diff --git a/writer/HDF5DataFilePluginMX.cpp b/writer/HDF5DataFilePluginMX.cpp index 71696afe8..e8e34dfec 100644 --- a/writer/HDF5DataFilePluginMX.cpp +++ b/writer/HDF5DataFilePluginMX.cpp @@ -260,7 +260,7 @@ void HDF5DataFilePluginMX::WriteFinal(HDF5File &data_file) { if (!bkg_estimate.empty()) data_file.SaveVector("/entry/MX/bkgEstimate", bkg_estimate.vec()); if (!ice_ring_ratio.empty()) - data_file.SaveVector("/entry/MX/iceRingScore", ice_ring_ratio.vec()); + data_file.SaveVector("/entry/MX/iceRingRatio", ice_ring_ratio.vec()); if (!protein_score.empty()) data_file.SaveVector("/entry/MX/proteinScore", protein_score.vec()); if (!ice_score.empty()) diff --git a/writer/HDF5NXmx.cpp b/writer/HDF5NXmx.cpp index 6fa941a91..85765e5a4 100644 --- a/writer/HDF5NXmx.cpp +++ b/writer/HDF5NXmx.cpp @@ -1187,7 +1187,7 @@ void NXmx::Finalize(const EndMessage &end) { end.spindle_lost_unique_fraction.value()); } if (end.ice_ring_ratio_mean) { - SaveScalar(*hdf5_file, "/entry/MX/iceRingScoreMean", end.ice_ring_ratio_mean.value()); + SaveScalar(*hdf5_file, "/entry/MX/iceRingRatioMean", end.ice_ring_ratio_mean.value()); } if (end.protein_score) { SaveScalar(*hdf5_file, "/entry/MX/proteinScoreMean", end.protein_score.value()); @@ -1282,7 +1282,7 @@ void NXmx::EndResultVectors(const EndMessage &end) { SaveVectorIfMissing(*hdf5_file, "/entry/MX/proteinScore", end.v_protein_score); SaveVectorIfMissing(*hdf5_file, "/entry/MX/iceScore", end.v_ice_score); SaveVectorIfMissing(*hdf5_file, "/entry/MX/spindleBlindFraction", end.v_spindle_blind_fraction); - SaveVectorIfMissing(*hdf5_file, "/entry/MX/iceRingScore", end.ice_ring_ratio); + SaveVectorIfMissing(*hdf5_file, "/entry/MX/iceRingRatio", end.ice_ring_ratio); SaveVectorIfMissing(*hdf5_file, "/entry/MX/profileRadius", end.profile_radius, "Angstrom^-1"); SaveVectorIfMissing(*hdf5_file, "/entry/MX/mosaicity", end.mosaicity, "deg"); SaveVectorIfMissing(*hdf5_file, "/entry/MX/bFactor", end.bFactor, "Angstrom^2");