diff --git a/broker/OpenAPIConvert.cpp b/broker/OpenAPIConvert.cpp index e44b9fca2..8cf6f8e6d 100644 --- a/broker/OpenAPIConvert.cpp +++ b/broker/OpenAPIConvert.cpp @@ -898,6 +898,7 @@ PlotType ConvertPlotType(const std::optional& input) { throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "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::IceRingScore; if (input == "azint") return PlotType::AzInt; if (input == "azint_1d") return PlotType::AzInt1D; @@ -1134,6 +1135,8 @@ org::openapitools::server::model::Scan_result Convert(const ScanResult& input) { if (i.bkg.has_value()) tmp.setBkg(i.bkg.value()); + if (i.spindle_blind.has_value()) + tmp.setSpindleBlind(i.spindle_blind.value()); if (i.angle_deg.has_value()) tmp.setAngle(i.angle_deg.value()); diff --git a/broker/gen/model/Scan_result_images_inner.cpp b/broker/gen/model/Scan_result_images_inner.cpp index 088d233d5..76197259b 100644 --- a/broker/gen/model/Scan_result_images_inner.cpp +++ b/broker/gen/model/Scan_result_images_inner.cpp @@ -31,6 +31,8 @@ Scan_result_images_inner::Scan_result_images_inner() m_AngleIsSet = false; m_Bkg = 0.0f; m_BkgIsSet = false; + m_Spindle_blind = 0.0f; + m_Spindle_blindIsSet = false; m_Spots = 0L; m_SpotsIsSet = false; m_Spots_low_res = 0L; @@ -84,7 +86,7 @@ bool Scan_result_images_inner::validate(std::stringstream& msg, const std::strin bool success = true; const std::string _pathPrefix = pathPrefix.empty() ? "Scan_result_images_inner" : pathPrefix; - + return success; } @@ -112,6 +114,9 @@ bool Scan_result_images_inner::operator==(const Scan_result_images_inner& rhs) c ((!bkgIsSet() && !rhs.bkgIsSet()) || (bkgIsSet() && rhs.bkgIsSet() && getBkg() == rhs.getBkg())) && + ((!spindleBlindIsSet() && !rhs.spindleBlindIsSet()) || (spindleBlindIsSet() && rhs.spindleBlindIsSet() && getSpindleBlind() == rhs.getSpindleBlind())) && + + ((!spotsIsSet() && !rhs.spotsIsSet()) || (spotsIsSet() && rhs.spotsIsSet() && getSpots() == rhs.getSpots())) && @@ -180,6 +185,8 @@ void to_json(nlohmann::json& j, const Scan_result_images_inner& o) j["angle"] = o.m_Angle; if(o.bkgIsSet()) j["bkg"] = o.m_Bkg; + if(o.spindleBlindIsSet()) + j["spindle_blind"] = o.m_Spindle_blind; if(o.spotsIsSet()) j["spots"] = o.m_Spots; if(o.spotsLowResIsSet()) @@ -239,6 +246,11 @@ void from_json(const nlohmann::json& j, Scan_result_images_inner& o) j.at("bkg").get_to(o.m_Bkg); o.m_BkgIsSet = true; } + if(j.find("spindle_blind") != j.end()) + { + j.at("spindle_blind").get_to(o.m_Spindle_blind); + o.m_Spindle_blindIsSet = true; + } if(j.find("spots") != j.end()) { j.at("spots").get_to(o.m_Spots); @@ -406,6 +418,23 @@ void Scan_result_images_inner::unsetBkg() { m_BkgIsSet = false; } +float Scan_result_images_inner::getSpindleBlind() const +{ + return m_Spindle_blind; +} +void Scan_result_images_inner::setSpindleBlind(float const value) +{ + m_Spindle_blind = value; + m_Spindle_blindIsSet = true; +} +bool Scan_result_images_inner::spindleBlindIsSet() const +{ + return m_Spindle_blindIsSet; +} +void Scan_result_images_inner::unsetSpindle_blind() +{ + m_Spindle_blindIsSet = false; +} int64_t Scan_result_images_inner::getSpots() const { return m_Spots; diff --git a/broker/gen/model/Scan_result_images_inner.h b/broker/gen/model/Scan_result_images_inner.h index 5eab27c97..e99639d74 100644 --- a/broker/gen/model/Scan_result_images_inner.h +++ b/broker/gen/model/Scan_result_images_inner.h @@ -97,6 +97,13 @@ public: bool bkgIsSet() const; void unsetBkg(); /// + /// Fraction (0-1) of a rotation sweep's blind double cone that this orientation makes unrecoverable, assuming the nearest plausible lattice row is a lone 2-fold (a worst-case bound, not an estimate). At 0.5 and above a recovery protocol (second sweep, reorientation) should engage. Absent means the frame could not be assessed, which automation must treat the same as above threshold - it is not a zero. + /// + float getSpindleBlind() const; + void setSpindleBlind(float const value); + bool spindleBlindIsSet() const; + void unsetSpindle_blind(); + /// /// Spot count /// int64_t getSpots() const; @@ -224,6 +231,8 @@ protected: bool m_AngleIsSet; float m_Bkg; bool m_BkgIsSet; + float m_Spindle_blind; + bool m_Spindle_blindIsSet; int64_t m_Spots; bool m_SpotsIsSet; int64_t m_Spots_low_res; diff --git a/broker/jfjoch_api.yaml b/broker/jfjoch_api.yaml index 7b3fe7587..cbb5a71bb 100644 --- a/broker/jfjoch_api.yaml +++ b/broker/jfjoch_api.yaml @@ -84,6 +84,7 @@ components: type: string enum: - bkg_estimate + - spindle_blind_fraction - azint - azint_1d - spot_count @@ -1641,6 +1642,16 @@ components: type: number format: float description: Background estimate + spindle_blind: + type: number + format: float + description: > + Fraction (0-1) of a rotation sweep's blind double cone that this orientation + makes unrecoverable, assuming the nearest plausible lattice row is a lone + 2-fold (a worst-case bound, not an estimate). At 0.5 and above a recovery + protocol (second sweep, reorientation) should engage. Absent means the frame + could not be assessed, which automation must treat the same as above + threshold - it is not a zero. spots: type: integer format: int64 diff --git a/common/JFJochMessages.h b/common/JFJochMessages.h index 317d98eb2..c75e9dc45 100644 --- a/common/JFJochMessages.h +++ b/common/JFJochMessages.h @@ -377,6 +377,9 @@ struct EndMessage { std::optional efficiency; std::optional indexing_rate; std::optional bkg_estimate; + // Run mean of the per-image spindle_blind_fraction, over the frames that had a value; absent + // when none did. Written to /entry/MX/spindleBlindFractionMean. + std::optional spindle_blind_fraction; std::optional end_date; @@ -427,6 +430,10 @@ struct EndMessage { std::vector image_indexed; std::vector indexed_lattice_count; std::vector v_bkg_estimate; + // Per-image spindle_blind_fraction; NaN where the frame had no value. NaN is the CANNOT-SAY + // state, which automation must treat as "engage" - it is not a zero (see SpindleTrigger in + // image_analysis/indexing/SpindleBlindFraction.h). + std::vector v_spindle_blind_fraction; std::vector profile_radius; std::vector mosaicity; std::vector bFactor; diff --git a/common/JFJochReceiverPlots.cpp b/common/JFJochReceiverPlots.cpp index 239606614..ebc64a11d 100644 --- a/common/JFJochReceiverPlots.cpp +++ b/common/JFJochReceiverPlots.cpp @@ -61,6 +61,7 @@ void JFJochReceiverPlots::Setup(const DiffractionExperiment &experiment, const A xfel_event_code.reserve(r); } bkg_estimate.Clear(r); + spindle_blind_fraction.Clear(r); ice_ring_score.Clear(r); spot_count.Clear(r); spot_count_low_res.Clear(r); @@ -128,6 +129,7 @@ void JFJochReceiverPlots::Setup(const DiffractionExperiment &experiment, const A void JFJochReceiverPlots::Add(const DataMessage &msg, const AzimuthalIntegrationProfile &profile) { bkg_estimate.AddElement(msg.number, msg.bkg_estimate); + spindle_blind_fraction.AddElement(msg.number, msg.spindle_blind_fraction); ice_ring_score.AddElement(msg.number, msg.ice_ring_score); resolution_estimate.AddElement(msg.number, msg.resolution_estimate); spot_count.AddElement(msg.number, msg.spot_count); @@ -277,6 +279,9 @@ MultiLinePlot JFJochReceiverPlots::GetPlots(const PlotRequest &request) { case PlotType::BkgEstimate: ret = bkg_estimate.GetMeanPlot(nbins, start, incr, request.fill_value); break; + case PlotType::SpindleBlindFraction: + ret = spindle_blind_fraction.GetMeanPlot(nbins, start, incr, request.fill_value); + break; case PlotType::IceRingScore: ret = ice_ring_score.GetMeanPlot(nbins, start, incr, request.fill_value); break; @@ -472,6 +477,14 @@ std::optional JFJochReceiverPlots::GetBkgEstimate() const { return {}; } +std::optional JFJochReceiverPlots::GetSpindleBlindFraction() const { + auto tmp = spindle_blind_fraction.Mean(); + if (std::isfinite(tmp)) + return tmp; + else + return {}; +} + std::optional JFJochReceiverPlots::GetResolutionEstimate() const { std::vector v = resolution_estimate.ExportArray(); std::erase_if(v, [](float x) { return !std::isfinite(x); }); @@ -573,6 +586,9 @@ void JFJochReceiverPlots::GetPlotRaw(std::vector &v, PlotType type, const case PlotType::BkgEstimate: v = bkg_estimate.ExportArray(); break; + case PlotType::SpindleBlindFraction: + v = spindle_blind_fraction.ExportArray(); + break; case PlotType::IceRingScore: v = ice_ring_score.ExportArray(); break; diff --git a/common/JFJochReceiverPlots.h b/common/JFJochReceiverPlots.h index a7ebdddf3..8a1656962 100644 --- a/common/JFJochReceiverPlots.h +++ b/common/JFJochReceiverPlots.h @@ -42,6 +42,7 @@ class JFJochReceiverPlots { AutoIncrVector xfel_event_code; StatusVector bkg_estimate; + StatusVector spindle_blind_fraction; StatusVector ice_ring_score; StatusVector spot_count; StatusVector spot_count_low_res; @@ -122,6 +123,8 @@ public: std::optional GetIndexingRate() const; std::optional GetBkgEstimate() const; + // Run mean of the per-image spindle severity, over the frames that had one; nothing when none did. + std::optional GetSpindleBlindFraction() const; std::optional GetIceRingScore() const; // Pooled over the run: spots on the hexagonal rings over the same q width of ice-free control // flanks. 1 = spots spread evenly, > 1 = they pile up on the rings (textured ice). diff --git a/common/Plot.h b/common/Plot.h index b629c1b44..3ae807f5e 100644 --- a/common/Plot.h +++ b/common/Plot.h @@ -9,7 +9,7 @@ #include enum class PlotType { - BkgEstimate, AzInt, AzInt1D, SpotCount, SpotCountLowRes, SpotCountIndexed, SpotCountIceRing, + BkgEstimate, SpindleBlindFraction, AzInt, AzInt1D, SpotCount, SpotCountLowRes, SpotCountIndexed, SpotCountIceRing, IndexingRate, IndexingUnitCellLength, IndexingUnitCellAngle, ErrorPixels, SaturatedPixels, ImageCollectionEfficiency, ReceiverDelay, ReceiverFreeSendBuf, ROISum, ROIMean, ROIMaxCount, ROIPixels, ROIWeightedX, ROIWeightedY, PacketsReceived, MaxValue, diff --git a/common/ScanResult.h b/common/ScanResult.h index 92a409bdc..a7a96b49a 100644 --- a/common/ScanResult.h +++ b/common/ScanResult.h @@ -27,6 +27,7 @@ struct ScanResultElem { std::optional err_pixels; std::optional sat_pixels; std::optional bkg; + std::optional spindle_blind; std::optional spot_count; std::optional spot_count_low_res; std::optional spot_count_indexed; diff --git a/common/ScanResultGenerator.cpp b/common/ScanResultGenerator.cpp index 1510da683..841312d1c 100644 --- a/common/ScanResultGenerator.cpp +++ b/common/ScanResultGenerator.cpp @@ -43,6 +43,7 @@ void ScanResultGenerator::Add(const DataMessage &message) { v[image_number].pixel_sum = message.pixel_sum; v[image_number].collection_efficiency = message.image_collection_efficiency.value_or(1.0); v[image_number].bkg = message.bkg_estimate; + v[image_number].spindle_blind = message.spindle_blind_fraction; v[image_number].spot_count = message.spot_count; v[image_number].indexing_solution = message.indexing_result; v[image_number].indexed_lattice_count = message.indexing_lattice_count; @@ -99,6 +100,7 @@ void ScanResultGenerator::FillEndMessage(EndMessage &message) const { message.spot_count_indexed.resize(n); message.image_indexed.resize(n); message.v_bkg_estimate.resize(n); + message.v_spindle_blind_fraction.resize(n); message.profile_radius.resize(n); message.mosaicity.resize(n); message.bFactor.resize(n); @@ -131,6 +133,7 @@ void ScanResultGenerator::FillEndMessage(EndMessage &message) const { message.spot_count_indexed[number] = static_cast(value_or_zero(e.spot_count_indexed)); message.image_indexed[number] = static_cast(e.indexing_solution.value_or(0)); message.v_bkg_estimate[number] = e.bkg.value_or(NAN); + message.v_spindle_blind_fraction[number] = e.spindle_blind.value_or(NAN); message.profile_radius[number] = e.profile_radius.value_or(NAN); message.mosaicity[number] = e.mosaicity.value_or(NAN); message.bFactor[number] = e.b_factor.value_or(NAN); diff --git a/docs/CBOR.md b/docs/CBOR.md index 095c256a8..6c4d3e36a 100644 --- a/docs/CBOR.md +++ b/docs/CBOR.md @@ -225,6 +225,7 @@ See [DECTRIS documentation](https://github.com/dectris/documentation/tree/main/s | 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) | | | +| 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 | | | | spot_count_ice_control | float | Spots in the ice-free flanks beside the hexagonal rings, rescaled to the ring bands' own q width (control for spot_count_ice_rings) | | | | beam_corr_x | float | Beam center correction X applied during processing \[pixel\] | | X | | beam_corr_y | float | Beam center correction Y applied during processing \[pixel\] | | X | @@ -301,6 +302,7 @@ See [DECTRIS documentation](https://github.com/dectris/documentation/tree/main/s | adu_histogram_bin_width | uint64 | Width of bins in the above histogram \[ADU\] | | | max_receiver_delay | uint64 | Internal performance of Jungfraujoch | | | bkg_estimate | float | Mean background estimate for the whole run | | +| spindle_blind_fraction | float | Run mean of the per-image spindle_blind_fraction, over the frames that had one | | | indexing_rate | float | Mean indexing rate for the whole run | | | unit_cell | object (optional) | Unit cell of the system, based on the actual experiment: a, b, c \[angstrom\] and alpha, beta, gamma \[degree\] | | | rotation_lattice_type | object | Bravais lattice classification of the total rotation solution over the run, if available; same schema as `lattice_type` | | @@ -316,6 +318,7 @@ See [DECTRIS documentation](https://github.com/dectris/documentation/tree/main/s | spot_count_indexed | Array(int32) | Per-image number of spots fitting indexing solution | | | 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) | | | 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) | | diff --git a/docs/HDF5.md b/docs/HDF5.md index 1bd2c6ebb..3442b2b22 100644 --- a/docs/HDF5.md +++ b/docs/HDF5.md @@ -346,6 +346,7 @@ In legacy/VDS mode these live in the data files and are linked/virtual-stacked i | `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) | +| `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 | | `beam_corr_x`, `beam_corr_y` | pixel | beam-center correction applied during processing | | `imageScaleFactor` | | on-the-fly per-image scale factor *g* | | `imageScaleCC` | | on-the-fly scaling correlation coefficient | @@ -367,6 +368,7 @@ variants. | `rotationLatticeNiggliClass` | | Niggli class of the run lattice | | `imageIndexedMean` | | mean indexing rate over the run | | `bkgEstimateMean` | photons | mean background over the run | +| `spindleBlindFractionMean` | fraction (0-1) | mean `spindleBlindFraction` over the frames that had one | | `iceRingScoreMean` | ratio | mean `iceRingScore` over the run — the single "how icy was this dataset" number (1 = no ice) | | `indexedLatticeCount` | | per-image lattice count summary (master). *Note: data files use `indexingLatticeCount`; readers accept either.* | | `reindexMatrix` | | change of basis from the setting the per-image data are in to the setting of `/entry/sample/unit_cell` (`[9]`, `int32`, flattened 3×3, row major) — see below | diff --git a/frame_serialize/CBORStream2Deserializer.cpp b/frame_serialize/CBORStream2Deserializer.cpp index 658e5a1cb..bb2daba1e 100644 --- a/frame_serialize/CBORStream2Deserializer.cpp +++ b/frame_serialize/CBORStream2Deserializer.cpp @@ -820,6 +820,8 @@ namespace { message.bkg_estimate = GetCBORFloat(value); else if (key == "ice_ring_score") message.ice_ring_score = GetCBORFloat(value); + else if (key == "spindle_blind_fraction") + message.spindle_blind_fraction = GetCBORFloat(value); else if (key == "adu_histogram") GetCBORUInt64Array(value, message.adu_histogram); else if (key == "beam_corr_x") @@ -1475,6 +1477,8 @@ namespace { message.adu_histogram_bin_width = GetCBORUInt(value); else if (key == "bkg_estimate") message.bkg_estimate = GetCBORFloat(value); + else if (key == "spindle_blind_fraction") + message.spindle_blind_fraction = GetCBORFloat(value); else if (key == "indexing_rate") message.indexing_rate = GetCBORFloat(value); else if (key == "indexed_lattice_count") @@ -1493,6 +1497,8 @@ namespace { GetCBORUInt8Array(value, message.image_indexed); else if (key == "v_bkg_estimate") GetCBORFloatArray(value, message.v_bkg_estimate); + else if (key == "v_spindle_blind_fraction") + GetCBORFloatArray(value, message.v_spindle_blind_fraction); else if (key == "ice_ring_score") GetCBORFloatArray(value, message.ice_ring_score); else if (key == "spot_count_ice_control") diff --git a/frame_serialize/CBORStream2Serializer.cpp b/frame_serialize/CBORStream2Serializer.cpp index 7a17da3f9..1ee9bed6b 100644 --- a/frame_serialize/CBORStream2Serializer.cpp +++ b/frame_serialize/CBORStream2Serializer.cpp @@ -801,6 +801,7 @@ void CBORStream2Serializer::SerializeSequenceEnd(const EndMessage& message) { CBOR_ENC(mapEncoder, "max_receiver_delay", message.max_receiver_delay); CBOR_ENC(mapEncoder, "indexing_rate", message.indexing_rate); CBOR_ENC(mapEncoder, "bkg_estimate", message.bkg_estimate); + CBOR_ENC(mapEncoder, "spindle_blind_fraction", message.spindle_blind_fraction); CBOR_ENC(mapEncoder, "rotation_lattice_type", message.rotation_lattice_type); if (message.rotation_lattice.has_value()) @@ -820,6 +821,7 @@ void CBORStream2Serializer::SerializeSequenceEnd(const EndMessage& message) { CBOR_ENC(mapEncoder, "spot_count_indexed", message.spot_count_indexed); CBOR_ENC(mapEncoder, "image_indexed", message.image_indexed); CBOR_ENC(mapEncoder, "v_bkg_estimate", message.v_bkg_estimate); + CBOR_ENC(mapEncoder, "v_spindle_blind_fraction", message.v_spindle_blind_fraction); CBOR_ENC(mapEncoder, "ice_ring_score", message.ice_ring_score); CBOR_ENC(mapEncoder, "ice_ring_score_mean", message.ice_ring_score_mean); CBOR_ENC(mapEncoder, "spot_count_ice_control", message.spot_count_ice_control); @@ -918,6 +920,7 @@ void CBORStream2Serializer::SerializeImageInternal(CborEncoder &mapEncoder, cons 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_score); + CBOR_ENC(mapEncoder, "spindle_blind_fraction", message.spindle_blind_fraction); CBOR_ENC(mapEncoder, "adu_histogram", message.adu_histogram); CBOR_ENC(mapEncoder, "roi_integrals", message.roi); CBOR_ENC(mapEncoder, "beam_corr_x", message.beam_corr_x); diff --git a/frontend/src/client/types.gen.ts b/frontend/src/client/types.gen.ts index 812ee65da..b6e3e74b3 100644 --- a/frontend/src/client/types.gen.ts +++ b/frontend/src/client/types.gen.ts @@ -1080,6 +1080,11 @@ export type scan_result = { * Background estimate */ bkg?: number; + /** + * Fraction (0-1) of a rotation sweep's blind double cone that this orientation makes unrecoverable, assuming the nearest plausible lattice row is a lone 2-fold (a worst-case bound, not an estimate). At 0.5 and above a recovery protocol (second sweep, reorientation) should engage. Absent means the frame could not be assessed, which automation must treat the same as above threshold - it is not a zero. + * + */ + spindle_blind?: number; /** * Spot count */ @@ -1952,6 +1957,7 @@ export type fill_value = number; */ export const plot_type = { BKG_ESTIMATE: 'bkg_estimate', + SPINDLE_BLIND_FRACTION: 'spindle_blind_fraction', AZINT: 'azint', AZINT_1D: 'azint_1d', SPOT_COUNT: 'spot_count', @@ -3293,7 +3299,7 @@ export type getPreviewPlotData = { /** * Type of requested plot */ - type: 'bkg_estimate' | 'azint' | 'azint_1d' | 'spot_count' | 'spot_count_low_res' | 'spot_count_indexed' | 'spot_count_ice' | 'indexing_rate' | 'indexing_lattice_count' | 'indexing_unit_cell_length' | 'indexing_unit_cell_angle' | 'profile_radius' | 'mosaicity' | 'b_factor' | 'error_pixels' | 'saturated_pixels' | 'image_collection_efficiency' | 'receiver_delay' | 'receiver_free_send_buf' | 'strong_pixels' | 'roi_sum' | 'roi_mean' | 'roi_max_count' | 'roi_pixels' | 'roi_weighted_x' | 'roi_weighted_y' | 'packets_received' | 'max_pixel_value' | 'resolution_estimate' | 'pixel_sum' | 'processing_time' | 'beam_center_x' | 'beam_center_y' | 'integrated_reflections' | 'image_scale_factor' | 'image_scale_cc' | 'compression_ratio' | 'ice_ring_score'; + type: 'bkg_estimate' | 'spindle_blind_fraction' | 'azint' | 'azint_1d' | 'spot_count' | 'spot_count_low_res' | 'spot_count_indexed' | 'spot_count_ice' | 'indexing_rate' | 'indexing_lattice_count' | 'indexing_unit_cell_length' | 'indexing_unit_cell_angle' | 'profile_radius' | 'mosaicity' | 'b_factor' | 'error_pixels' | 'saturated_pixels' | 'image_collection_efficiency' | 'receiver_delay' | 'receiver_free_send_buf' | 'strong_pixels' | 'roi_sum' | 'roi_mean' | 'roi_max_count' | 'roi_pixels' | 'roi_weighted_x' | 'roi_weighted_y' | 'packets_received' | 'max_pixel_value' | 'resolution_estimate' | 'pixel_sum' | 'processing_time' | 'beam_center_x' | 'beam_center_y' | 'integrated_reflections' | 'image_scale_factor' | 'image_scale_cc' | 'compression_ratio' | 'ice_ring_score'; /** * Fill value for elements that were missed during data collection * @@ -3340,7 +3346,7 @@ export type getPreviewPlotBinData = { /** * Type of requested plot */ - type: 'bkg_estimate' | 'azint' | 'azint_1d' | 'spot_count' | 'spot_count_low_res' | 'spot_count_indexed' | 'spot_count_ice' | 'indexing_rate' | 'indexing_lattice_count' | 'indexing_unit_cell_length' | 'indexing_unit_cell_angle' | 'profile_radius' | 'mosaicity' | 'b_factor' | 'error_pixels' | 'saturated_pixels' | 'image_collection_efficiency' | 'receiver_delay' | 'receiver_free_send_buf' | 'strong_pixels' | 'roi_sum' | 'roi_mean' | 'roi_max_count' | 'roi_pixels' | 'roi_weighted_x' | 'roi_weighted_y' | 'packets_received' | 'max_pixel_value' | 'resolution_estimate' | 'pixel_sum' | 'processing_time' | 'beam_center_x' | 'beam_center_y' | 'integrated_reflections' | 'image_scale_factor' | 'image_scale_cc' | 'compression_ratio' | 'ice_ring_score'; + type: 'bkg_estimate' | 'spindle_blind_fraction' | 'azint' | 'azint_1d' | 'spot_count' | 'spot_count_low_res' | 'spot_count_indexed' | 'spot_count_ice' | 'indexing_rate' | 'indexing_lattice_count' | 'indexing_unit_cell_length' | 'indexing_unit_cell_angle' | 'profile_radius' | 'mosaicity' | 'b_factor' | 'error_pixels' | 'saturated_pixels' | 'image_collection_efficiency' | 'receiver_delay' | 'receiver_free_send_buf' | 'strong_pixels' | 'roi_sum' | 'roi_mean' | 'roi_max_count' | 'roi_pixels' | 'roi_weighted_x' | 'roi_weighted_y' | 'packets_received' | 'max_pixel_value' | 'resolution_estimate' | 'pixel_sum' | 'processing_time' | 'beam_center_x' | 'beam_center_y' | 'integrated_reflections' | 'image_scale_factor' | 'image_scale_cc' | 'compression_ratio' | 'ice_ring_score'; /** * Name of ROI for which plot is requested */ diff --git a/frontend/src/client/zod.gen.ts b/frontend/src/client/zod.gen.ts index da9254f7f..a34f337b4 100644 --- a/frontend/src/client/zod.gen.ts +++ b/frontend/src/client/zod.gen.ts @@ -437,6 +437,7 @@ export const zScanResult = z.object({ ny: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).optional(), angle: z.number().optional(), bkg: z.number().optional(), + spindle_blind: z.number().optional(), spots: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).optional(), spots_low_res: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).optional(), spots_indexed: z.coerce.bigint().min(BigInt('-9223372036854775808'), { error: 'Invalid value: Expected int64 to be >= -9223372036854775808' }).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }).optional(), @@ -893,6 +894,7 @@ export const zFillValue = z.number(); */ export const zPlotType = z.enum([ 'bkg_estimate', + 'spindle_blind_fraction', 'azint', 'azint_1d', 'spot_count', @@ -1202,6 +1204,7 @@ export const zGetPreviewPlotQuery = z.object({ binning: z.int().optional().default(1), type: z.enum([ 'bkg_estimate', + 'spindle_blind_fraction', 'azint', 'azint_1d', 'spot_count', @@ -1257,6 +1260,7 @@ export const zGetPreviewPlotResponse = zPlots; export const zGetPreviewPlotBinQuery = z.object({ type: z.enum([ 'bkg_estimate', + 'spindle_blind_fraction', 'azint', 'azint_1d', 'spot_count', diff --git a/frontend/src/components/DataProcessingPlot.tsx b/frontend/src/components/DataProcessingPlot.tsx index 8843dc1b2..9790624c7 100644 --- a/frontend/src/components/DataProcessingPlot.tsx +++ b/frontend/src/components/DataProcessingPlot.tsx @@ -54,6 +54,8 @@ function AxisTypeY(plot: plot_type) : string | ReactNode { return "Count"; case plot_type.ICE_RING_SCORE: return "Ratio"; + case plot_type.SPINDLE_BLIND_FRACTION: + return "Fraction"; case plot_type.AZINT: case plot_type.AZINT_1D: case plot_type.BKG_ESTIMATE: diff --git a/frontend/src/components/DataProcessingPlots.tsx b/frontend/src/components/DataProcessingPlots.tsx index 7e950811c..bff4a20bf 100644 --- a/frontend/src/components/DataProcessingPlots.tsx +++ b/frontend/src/components/DataProcessingPlots.tsx @@ -54,6 +54,7 @@ function DataProcessingPlots({type: initialType, height}: MyProps) { Azimuthal integration profile Azimuthal integration profile (1D) Background estimate + Spindle blind fraction Diffraction resolution estimate ROI area sum ROI area mean diff --git a/reader/HDF5MetadataSource.cpp b/reader/HDF5MetadataSource.cpp index 8b12c1fab..fa86cf8d6 100644 --- a/reader/HDF5MetadataSource.cpp +++ b/reader/HDF5MetadataSource.cpp @@ -557,6 +557,7 @@ 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_score = master_file->ReadOptVector("/entry/MX/iceRingScore"); dataset->resolution_estimate = master_file->ReadOptVector("/entry/MX/resolutionEstimate"); dataset->profile_radius = master_file->ReadOptVector("/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) diff --git a/reader/JFJochReaderDataset.h b/reader/JFJochReaderDataset.h index ab6d36e8a..cc50376eb 100644 --- a/reader/JFJochReaderDataset.h +++ b/reader/JFJochReaderDataset.h @@ -56,6 +56,8 @@ struct JFJochReaderDataset { std::vector indexing_result; std::vector indexing_lattice_count; std::vector bkg_estimate; + // NaN entries are frames with no value (CANNOT SAY), not zeros. + std::vector spindle_blind_fraction; std::vector ice_ring_score; std::vector resolution_estimate; std::vector efficiency; diff --git a/reader/JFJochReaderImage.cpp b/reader/JFJochReaderImage.cpp index 2d22b00a3..bb5688c1e 100644 --- a/reader/JFJochReaderImage.cpp +++ b/reader/JFJochReaderImage.cpp @@ -198,6 +198,7 @@ void JFJochReaderImage::AddImage(const JFJochReaderImage &other) { message.indexing_result = false; message.resolution_estimate = {}; message.bkg_estimate = {}; + message.spindle_blind_fraction = {}; message.spots = {}; error_pixel.clear(); diff --git a/receiver/JFJochReceiver.cpp b/receiver/JFJochReceiver.cpp index 7c1a21799..8c4aca8b3 100644 --- a/receiver/JFJochReceiver.cpp +++ b/receiver/JFJochReceiver.cpp @@ -162,6 +162,7 @@ void JFJochReceiver::SendEndMessage() { message.run_name = experiment.GetRunName(); message.bkg_estimate = plots.GetBkgEstimate(); + message.spindle_blind_fraction = plots.GetSpindleBlindFraction(); message.ice_ring_score_mean = plots.GetIceRingScore(); message.indexing_rate = plots.GetIndexingRate(); diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 72a07c51e..469233cb6 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -3532,6 +3532,7 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b end_msg.run_number = experiment_.GetRunNumber(); end_msg.run_name = experiment_.GetRunName(); end_msg.bkg_estimate = plots.GetBkgEstimate(); + end_msg.spindle_blind_fraction = plots.GetSpindleBlindFraction(); result.spot_resolution_estimate_A = plots.GetResolutionEstimate(); end_msg.ice_ring_score = plots.GetIceRingScoreArray(); end_msg.ice_ring_score_mean = plots.GetIceRingScore(); diff --git a/tests/CBORTest.cpp b/tests/CBORTest.cpp index 4c36ea360..638506425 100644 --- a/tests/CBORTest.cpp +++ b/tests/CBORTest.cpp @@ -497,6 +497,7 @@ TEST_CASE("CBORSerialize_End", "[CBOR]") { .images_sent_to_write_count = 40000, .max_receiver_delay = 3456, .efficiency = 0.99, + .spindle_blind_fraction = 0.31f, .end_date = "ccc", .run_name = "bla5", .run_number = 45676782, @@ -505,7 +506,8 @@ TEST_CASE("CBORSerialize_End", "[CBOR]") { .niggli_class = 22, .crystal_system = gemmi::CrystalSystem::Tetragonal, }, - .rotation_lattice = CrystalLattice(40, 50, 60, 90, 90, 90) + .rotation_lattice = CrystalLattice(40, 50, 60, 90, 90, 90), + .v_spindle_blind_fraction = {0.25f, NAN, 1.0f} }; REQUIRE_NOTHROW(serializer.SerializeSequenceEnd(message)); @@ -533,6 +535,14 @@ TEST_CASE("CBORSerialize_End", "[CBOR]") { CHECK(output_message.rotation_lattice_type->niggli_class == 22); REQUIRE(output_message.rotation_lattice.has_value()); CHECK(output_message.rotation_lattice->GetUnitCell().c == Catch::Approx(60.0)); + + REQUIRE(output_message.spindle_blind_fraction == message.spindle_blind_fraction); + // The per-image vector holds NaN where a frame had no value (CANNOT SAY); the hole must + // survive the round trip as a hole, not as a number. + REQUIRE(output_message.v_spindle_blind_fraction.size() == 3); + CHECK(output_message.v_spindle_blind_fraction[0] == Catch::Approx(0.25f)); + CHECK(std::isnan(output_message.v_spindle_blind_fraction[1])); + CHECK(output_message.v_spindle_blind_fraction[2] == Catch::Approx(1.0f)); } TEST_CASE("CBORSerialize_End_SpaceGroup", "[CBOR]") { @@ -663,6 +673,7 @@ TEST_CASE("CBORSerialize_Image", "[CBOR]") { .spots = spots, .spot_count_ice_rings = 157, .bkg_estimate = 12.345f, + .spindle_blind_fraction = 0.62f, .indexing_result = true, .indexing_unit_cell = UnitCell{.a = 123, .b = 145, .c = 67.5, .alpha = 90, .beta = 120, .gamma = 134}, .adu_histogram = {3, 4, 5, 8}, @@ -716,6 +727,7 @@ TEST_CASE("CBORSerialize_Image", "[CBOR]") { REQUIRE(image_array.error_pixel_count == message.error_pixel_count); REQUIRE(image_array.strong_pixel_count == message.strong_pixel_count); REQUIRE(image_array.bkg_estimate == message.bkg_estimate); + REQUIRE(image_array.spindle_blind_fraction == message.spindle_blind_fraction); REQUIRE(image_array.image_collection_efficiency == message.image_collection_efficiency); REQUIRE(image_array.user_data == message.user_data); REQUIRE(image_array.original_number == message.original_number); @@ -1310,6 +1322,7 @@ TEST_CASE("CBORSerialize_Metadata", "[CBOR]") { REQUIRE(deserialized->metadata->images.size() == 2); CHECK(deserialized->metadata->images.at(0).number == 172); CHECK(deserialized->metadata->images.at(0).bkg_estimate == 45); + CHECK(!deserialized->metadata->images.at(0).spindle_blind_fraction.has_value()); CHECK(deserialized->metadata->images.at(0).image.GetWidth() == 0); CHECK(deserialized->metadata->images.at(1).number == 173); diff --git a/tests/JFJochReaderTest.cpp b/tests/JFJochReaderTest.cpp index 1b3e422b5..3dfa26fe0 100644 --- a/tests/JFJochReaderTest.cpp +++ b/tests/JFJochReaderTest.cpp @@ -636,6 +636,10 @@ TEST_CASE("JFJochReader_DataI16", "[HDF5][Full]") { message.spots = spots; message.indexing_result = (i % 2 == 0); message.bkg_estimate = i * 345.6; + // Only one frame has a spindle severity: the others must come back ABSENT (stored as + // NaN), because no value is the CANNOT-SAY trigger state and a zero is not. + if (i == 2) + message.spindle_blind_fraction = 0.75f; message.number = i; message.profile_radius = 123.09; generator.Add(message); @@ -656,6 +660,13 @@ TEST_CASE("JFJochReader_DataI16", "[HDF5][Full]") { REQUIRE(dataset->spot_count.size() == 4); REQUIRE(dataset->bkg_estimate.size() == 4); REQUIRE(dataset->profile_radius.size() == 4); + REQUIRE(dataset->spindle_blind_fraction.size() == 4); + for (int i = 0; i < 4; i++) { + if (i == 2) + CHECK(dataset->spindle_blind_fraction[i] == Catch::Approx(0.75f)); + else + CHECK(std::isnan(dataset->spindle_blind_fraction[i])); + } REQUIRE_THROWS(reader.LoadImage(4)); std::shared_ptr reader_image; diff --git a/viewer/JFJochHttpReader.cpp b/viewer/JFJochHttpReader.cpp index 1c0bf8c81..b4c6e0f98 100644 --- a/viewer/JFJochHttpReader.cpp +++ b/viewer/JFJochHttpReader.cpp @@ -267,6 +267,7 @@ std::shared_ptr JFJochHttpReader::UpdateDataset_i() { dataset->experiment.FluorescenceSpectrum(msg->start_message->fluorescence_spectrum); dataset->bkg_estimate = GetPlot_i("bkg_estimate"); + dataset->spindle_blind_fraction = GetPlot_i("spindle_blind_fraction"); dataset->ice_ring_score = GetPlot_i("ice_ring_score"); dataset->spot_count = GetPlot_i("spot_count"); dataset->spot_count_ice_rings = GetPlot_i("spot_count_ice"); diff --git a/viewer/JFJochProcessController.cpp b/viewer/JFJochProcessController.cpp index dd859518e..752ea0a4b 100644 --- a/viewer/JFJochProcessController.cpp +++ b/viewer/JFJochProcessController.cpp @@ -122,6 +122,7 @@ void JFJochProcessController::OnImageProcessed(const DataMessage &msg) { if (msg.indexing_result) put(d.indexing_result, *msg.indexing_result ? 1.0f : 0.0f); if (msg.indexing_lattice_count) put(d.indexing_lattice_count, *msg.indexing_lattice_count); if (msg.bkg_estimate) put(d.bkg_estimate, *msg.bkg_estimate); + if (msg.spindle_blind_fraction) put(d.spindle_blind_fraction, *msg.spindle_blind_fraction); if (msg.resolution_estimate) put(d.resolution_estimate, *msg.resolution_estimate); if (msg.profile_radius) put(d.profile_radius, *msg.profile_radius); if (msg.mosaicity_deg) put(d.mosaicity_deg, *msg.mosaicity_deg); diff --git a/viewer/JFJochViewerDatasetInfo.cpp b/viewer/JFJochViewerDatasetInfo.cpp index cd2bd1b1f..237836734 100644 --- a/viewer/JFJochViewerDatasetInfo.cpp +++ b/viewer/JFJochViewerDatasetInfo.cpp @@ -138,6 +138,7 @@ void JFJochViewerDatasetInfo::UpdateLabels() { if (this->dataset) { combo_box->addItem("Background estimate", 0); combo_box->addItem("Ice ring score", 14); + combo_box->addItem("Spindle blind fraction", 15); combo_box->addItem("Resolution estimate", 7); combo_box->addItem("Spot count", 1); combo_box->addItem("Spot count (indexed)", 2); @@ -237,6 +238,7 @@ std::vector JFJochViewerDatasetInfo::ExtractMetric(const JFJochReaderData else if (val == 12) data = ds.integrated_reflections; else if (val == 13) data = ds.indexing_lattice_count; else if (val == 14) data = ds.ice_ring_score; + else if (val == 15) data = ds.spindle_blind_fraction; else if (val >= 100) { const int roi_index = (val - 100) / 4; if (val % 4 == 0) { diff --git a/writer/HDF5DataFilePluginMX.cpp b/writer/HDF5DataFilePluginMX.cpp index 808a2006a..ea4c7d81a 100644 --- a/writer/HDF5DataFilePluginMX.cpp +++ b/writer/HDF5DataFilePluginMX.cpp @@ -54,6 +54,7 @@ HDF5DataFilePluginMX::HDF5DataFilePluginMX(const StartMessage &msg) void HDF5DataFilePluginMX::OpenFile(HDF5File &data_file, const DataMessage &msg, size_t images_per_file) { bkg_estimate.reserve(images_per_file); ice_ring_score.reserve(images_per_file); + spindle_blind_fraction.reserve(images_per_file); if (max_spots == 0) return; @@ -101,6 +102,8 @@ void HDF5DataFilePluginMX::Write(const DataMessage &msg, uint64_t image_number) bkg_estimate[image_number] = msg.bkg_estimate.value(); if (msg.ice_ring_score.has_value()) ice_ring_score[image_number] = msg.ice_ring_score.value(); + if (msg.spindle_blind_fraction.has_value()) + spindle_blind_fraction[image_number] = msg.spindle_blind_fraction.value(); if (max_spots == 0) return; @@ -252,6 +255,8 @@ void HDF5DataFilePluginMX::WriteFinal(HDF5File &data_file) { data_file.SaveVector("/entry/MX/bkgEstimate", bkg_estimate.vec()); if (!ice_ring_score.empty()) data_file.SaveVector("/entry/MX/iceRingScore", ice_ring_score.vec()); + if (!spindle_blind_fraction.empty()) + data_file.SaveVector("/entry/MX/spindleBlindFraction", spindle_blind_fraction.vec()); if (!profile_radius.empty()) data_file.SaveVector("/entry/MX/profileRadius", profile_radius.vec())->Units("Angstrom^-1"); if (!mosaicity_deg.empty()) diff --git a/writer/HDF5DataFilePluginMX.h b/writer/HDF5DataFilePluginMX.h index 68a568edb..d369f8b0d 100644 --- a/writer/HDF5DataFilePluginMX.h +++ b/writer/HDF5DataFilePluginMX.h @@ -48,6 +48,8 @@ class HDF5DataFilePluginMX : public HDF5DataFilePlugin { // bkg_estimate AutoIncrVector bkg_estimate{NAN}; AutoIncrVector ice_ring_score{NAN}; + // NaN = the frame had no value (CANNOT SAY), which is not a zero - see SpindleTrigger. + AutoIncrVector spindle_blind_fraction{NAN}; // resolution_estimation AutoIncrVector resolution_estimate{NAN}; diff --git a/writer/HDF5NXmx.cpp b/writer/HDF5NXmx.cpp index d1eecac63..a0b1be62b 100644 --- a/writer/HDF5NXmx.cpp +++ b/writer/HDF5NXmx.cpp @@ -1179,6 +1179,9 @@ void NXmx::Finalize(const EndMessage &end) { if (end.bkg_estimate) { SaveScalar(*hdf5_file, "/entry/MX/bkgEstimateMean", end.bkg_estimate.value()); } + if (end.spindle_blind_fraction) { + SaveScalar(*hdf5_file, "/entry/MX/spindleBlindFractionMean", end.spindle_blind_fraction.value()); + } if (end.ice_ring_score_mean) { SaveScalar(*hdf5_file, "/entry/MX/iceRingScoreMean", end.ice_ring_score_mean.value()); } @@ -1258,6 +1261,7 @@ void NXmx::EndResultVectors(const EndMessage &end) { SaveVectorIfMissing(*hdf5_file, "/entry/MX/imageIndexed", end.image_indexed); SaveVectorIfMissing(*hdf5_file, "/entry/MX/indexedLatticeCount", end.indexed_lattice_count); SaveVectorIfMissing(*hdf5_file, "/entry/MX/bkgEstimate", end.v_bkg_estimate); + SaveVectorIfMissing(*hdf5_file, "/entry/MX/spindleBlindFraction", end.v_spindle_blind_fraction); SaveVectorIfMissing(*hdf5_file, "/entry/MX/iceRingScore", end.ice_ring_score); SaveVectorIfMissing(*hdf5_file, "/entry/MX/profileRadius", end.profile_radius, "Angstrom^-1"); SaveVectorIfMissing(*hdf5_file, "/entry/MX/mosaicity", end.mosaicity, "deg");