diff --git a/CLAUDE.md b/CLAUDE.md index f57bc197..473c15e2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -190,3 +190,34 @@ React 19 + TypeScript + MUI + Vite (`frontend/`). Data layer is generated from t (`@hey-api/openapi-ts` → fetch client + TanStack Query hooks + zod schemas). Scripts: `npm start` (dev server), `npm run build` (tsc + vite), `npm run openapi` (regen client), `npm run redocly4broker` (regen `broker/redoc-static.html`). + +## Adding a per-image scalar quantity + +A per-image scalar (e.g. `ice_ring_score`, `bkg_estimate`, `mosaicity`) flows analysis → message → CBOR +→ HDF5 → scan-result/plot → API → viewer/frontend. To add one, mirror an existing float scalar +(`bkg_estimate` is a clean template) at every layer: + +1. **Compute** where the azint profile is finalized: `image_analysis/MXAnalysisWithoutFPGA.cpp` (CPU), + `receiver/JFJochReceiverFPGA.cpp` (FPGA), and the offline azint worker in `process/JFJochProcess.cpp`. +2. **Message** (`common/JFJochMessages.h`): `std::optional` in `DataMessage`, `std::vector` + in `EndMessage`. +3. **CBOR**: encode in `frame_serialize/CBORStream2Serializer.cpp` (DataMessage block *and* END block), + decode in `CBORStream2Deserializer.cpp` (both). Optional fields are back-compatible — no version bump. +4. **HDF5** (`writer/HDF5DataFilePluginMX.{h,cpp}`): an `AutoIncrVector` with reserve / per-image + write / `SaveVector("/entry/MX/")` — per-image arrays go in the data-file plugin, not NXmx master. +5. **Scan result**: `common/ScanResult.h` (`ScanResultElem`) + `common/ScanResultGenerator.cpp` + (copy in `Add`, resize+fill in `FillEndMessage`). +6. **Receiver plot**: `common/Plot.h` (`PlotType`) + `common/JFJochReceiverPlots.{h,cpp}` (`StatusVector` + + Clear / AddElement / GetPlots / GetPlotRaw cases). +7. **API**: add to the `plot_type` enum and the `scan_result` images schema in `broker/jfjoch_api.yaml`, + regenerate the C++ model (`java -jar openapi-generator-cli.jar generate -i broker/jfjoch_api.yaml -o + broker/gen -g cpp-pistache-server`) and the frontend client (`cd frontend && npm run openapi`), then + wire `broker/OpenAPIConvert.cpp` (`ConvertPlotType` string→enum and the `Convert(ScanResult)` setter). +8. **Reader/viewer**: `reader/JFJochReaderDataset.h` + `reader/JFJochHttpReader.cpp` (`GetPlot_i`) and + `viewer/JFJochViewerDatasetInfo.cpp` (combo item + `ExtractMetric`). +9. **Frontend**: `frontend/src/components/DataProcessingPlots.tsx` (`MenuItem`) + `DataProcessingPlot.tsx` + (y-axis label). +10. **Docs**: `docs/CBOR.md`, `docs/HDF5.md`, `docs/CPU_DATA_ANALYSIS.md`. + +Gotcha: an existing `build/` dir needs a `cmake .` reconfigure to pick up a newly-added `broker/gen` +source file (the source list is a configure-time glob). diff --git a/broker/OpenAPIConvert.cpp b/broker/OpenAPIConvert.cpp index 1590c531..f6093731 100644 --- a/broker/OpenAPIConvert.cpp +++ b/broker/OpenAPIConvert.cpp @@ -880,6 +880,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 == "ice_ring_score") return PlotType::IceRingScore; if (input == "azint") return PlotType::AzInt; if (input == "azint_1d") return PlotType::AzInt1D; if (input == "spot_count") return PlotType::SpotCount; @@ -1077,6 +1078,8 @@ org::openapitools::server::model::Scan_result Convert(const ScanResult& input) { tmp.setSpots(i.spot_count.value()); if (i.spot_count_ice.has_value()) tmp.setSpotsIce(i.spot_count_ice.value()); + if (i.ice_ring_score.has_value()) + tmp.setIceRingScore(i.ice_ring_score.value()); if (i.spot_count_low_res.has_value()) tmp.setSpotsLowRes(i.spot_count_low_res.value()); if (i.spot_count_indexed.has_value()) diff --git a/broker/gen/model/Scan_result_images_inner.cpp b/broker/gen/model/Scan_result_images_inner.cpp index 93173a83..bae2136e 100644 --- a/broker/gen/model/Scan_result_images_inner.cpp +++ b/broker/gen/model/Scan_result_images_inner.cpp @@ -39,6 +39,8 @@ Scan_result_images_inner::Scan_result_images_inner() m_Spots_indexedIsSet = false; m_Spots_ice = 0L; m_Spots_iceIsSet = false; + m_Ice_ring_score = 0.0f; + m_Ice_ring_scoreIsSet = false; m_Index = 0L; m_IndexIsSet = false; m_Latt_count = 0L; @@ -82,7 +84,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; } @@ -122,6 +124,9 @@ bool Scan_result_images_inner::operator==(const Scan_result_images_inner& rhs) c ((!spotsIceIsSet() && !rhs.spotsIceIsSet()) || (spotsIceIsSet() && rhs.spotsIceIsSet() && getSpotsIce() == rhs.getSpotsIce())) && + ((!iceRingScoreIsSet() && !rhs.iceRingScoreIsSet()) || (iceRingScoreIsSet() && rhs.iceRingScoreIsSet() && getIceRingScore() == rhs.getIceRingScore())) && + + ((!indexIsSet() && !rhs.indexIsSet()) || (indexIsSet() && rhs.indexIsSet() && getIndex() == rhs.getIndex())) && @@ -183,6 +188,8 @@ void to_json(nlohmann::json& j, const Scan_result_images_inner& o) j["spots_indexed"] = o.m_Spots_indexed; if(o.spotsIceIsSet()) j["spots_ice"] = o.m_Spots_ice; + if(o.iceRingScoreIsSet()) + j["ice_ring_score"] = o.m_Ice_ring_score; if(o.indexIsSet()) j["index"] = o.m_Index; if(o.lattCountIsSet()) @@ -252,6 +259,11 @@ void from_json(const nlohmann::json& j, Scan_result_images_inner& o) j.at("spots_ice").get_to(o.m_Spots_ice); o.m_Spots_iceIsSet = true; } + if(j.find("ice_ring_score") != j.end()) + { + j.at("ice_ring_score").get_to(o.m_Ice_ring_score); + o.m_Ice_ring_scoreIsSet = true; + } if(j.find("index") != j.end()) { j.at("index").get_to(o.m_Index); @@ -462,6 +474,23 @@ void Scan_result_images_inner::unsetSpots_ice() { m_Spots_iceIsSet = false; } +float Scan_result_images_inner::getIceRingScore() const +{ + return m_Ice_ring_score; +} +void Scan_result_images_inner::setIceRingScore(float const value) +{ + m_Ice_ring_score = value; + m_Ice_ring_scoreIsSet = true; +} +bool Scan_result_images_inner::iceRingScoreIsSet() const +{ + return m_Ice_ring_scoreIsSet; +} +void Scan_result_images_inner::unsetIce_ring_score() +{ + m_Ice_ring_scoreIsSet = false; +} int64_t Scan_result_images_inner::getIndex() const { return m_Index; diff --git a/broker/gen/model/Scan_result_images_inner.h b/broker/gen/model/Scan_result_images_inner.h index 18a5fb1c..9677d5e9 100644 --- a/broker/gen/model/Scan_result_images_inner.h +++ b/broker/gen/model/Scan_result_images_inner.h @@ -125,6 +125,13 @@ public: bool spotsIceIsSet() const; void unsetSpots_ice(); /// + /// Strongest hexagonal-ice ring band/shoulder intensity ratio (1 = no ice) + /// + float getIceRingScore() const; + void setIceRingScore(float const value); + bool iceRingScoreIsSet() const; + void unsetIce_ring_score(); + /// /// Indexing solution /// int64_t getIndex() const; @@ -225,6 +232,8 @@ protected: bool m_Spots_indexedIsSet; int64_t m_Spots_ice; bool m_Spots_iceIsSet; + float m_Ice_ring_score; + bool m_Ice_ring_scoreIsSet; int64_t m_Index; bool m_IndexIsSet; int64_t m_Latt_count; diff --git a/broker/jfjoch_api.yaml b/broker/jfjoch_api.yaml index fd78203e..c3f05238 100644 --- a/broker/jfjoch_api.yaml +++ b/broker/jfjoch_api.yaml @@ -121,6 +121,7 @@ components: - image_scale_cc - image_scale_b - compression_ratio + - ice_ring_score roi: in: query name: roi @@ -1578,6 +1579,10 @@ components: type: integer format: int64 description: Spot count within common ice ring resolutions + ice_ring_score: + type: number + format: float + description: Strongest hexagonal-ice ring band/shoulder intensity ratio (1 = no ice) index: type: integer format: int64 diff --git a/common/AzimuthalIntegrationProfile.cpp b/common/AzimuthalIntegrationProfile.cpp index dc31a79d..91530dd6 100644 --- a/common/AzimuthalIntegrationProfile.cpp +++ b/common/AzimuthalIntegrationProfile.cpp @@ -3,6 +3,9 @@ #include "AzimuthalIntegrationProfile.h" #include "JFJochException.h" +#include "Definitions.h" + +#include inline float sum_to_count(float sum, uint64_t count) { if (count == 0) @@ -171,6 +174,29 @@ float AzimuthalIntegrationProfile::GetBkgEstimate(const AzimuthalIntegrationSett return GetMeanValueOfBins(min_bin, max_bin); } +float AzimuthalIntegrationProfile::GetIceRingScore(const AzimuthalIntegrationSettings &settings, + float half_width_q) const { + // For each hexagonal-ice powder ring, the mean profile intensity in the ring band (+/- half_width in + // q = 2*pi/d) over a baseline from the two shoulders just outside it. Report the strongest ring's + // ratio (1 = no ice); rings whose band+shoulders fall off the measured q-range are skipped. + constexpr float two_pi = 6.283185307f; + const float low_q = settings.GetLowQ_recipA(); + const float high_q = settings.GetHighQ_recipA(); + float score = 1.0f; + for (const float ice_d : ICE_RING_RES_A) { + const float q = two_pi / ice_d; + if (q - 2 * half_width_q < low_q || q + 2 * half_width_q > high_q) + continue; + const float ring = GetMeanValueOfBins(settings.QToBin(q - half_width_q), settings.QToBin(q + half_width_q)); + const float lo = GetMeanValueOfBins(settings.QToBin(q - 2 * half_width_q), settings.QToBin(q - half_width_q)); + const float hi = GetMeanValueOfBins(settings.QToBin(q + half_width_q), settings.QToBin(q + 2 * half_width_q)); + const float baseline = 0.5f * (lo + hi); + if (std::isfinite(baseline) && baseline > 0.0f && std::isfinite(ring)) + score = std::max(score, ring / baseline); + } + return score; +} + AzimuthalIntegrationProfile &AzimuthalIntegrationProfile::operator+=(const AzimuthalIntegrationProfile &other) { if ((other.bin_to_q != bin_to_q) || (sum.size() != other.sum.size())) { throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, diff --git a/common/AzimuthalIntegrationProfile.h b/common/AzimuthalIntegrationProfile.h index 0fd41524..31e52df6 100644 --- a/common/AzimuthalIntegrationProfile.h +++ b/common/AzimuthalIntegrationProfile.h @@ -44,6 +44,9 @@ public: float GetMeanValueOfBins(uint16_t min_bin, uint16_t max_bin) const; float GetBkgEstimate(const AzimuthalIntegrationSettings& settings) const; + // Single per-image ice indicator: the strongest hexagonal-ice ring's band/shoulder intensity ratio + // (1 = no ice, >1 = ice above the local background). Max over the rings in range; 1 if none. + float GetIceRingScore(const AzimuthalIntegrationSettings& settings, float half_width_q) const; MultiLinePlot GetPlot(bool force_1d = false, PlotAzintUnit plot_unit = PlotAzintUnit::Q_recipA) const; AzimuthalIntegrationProfile& operator+=(const AzimuthalIntegrationProfile& profile); // Not thread safe }; diff --git a/common/JFJochMessages.h b/common/JFJochMessages.h index 6865cf68..d8ced99d 100644 --- a/common/JFJochMessages.h +++ b/common/JFJochMessages.h @@ -119,6 +119,7 @@ struct DataMessage { std::vector az_int_profile_count; std::optional bkg_estimate; + std::optional ice_ring_score; // strongest ice-ring band/shoulder ratio from the azint profile (1 = none) std::optional indexing_result; std::optional indexing_lattice; @@ -376,6 +377,7 @@ struct EndMessage { std::vector image_scale_cc; std::vector image_scale_b_factor; std::vector image_scale_mosaicity; + std::vector ice_ring_score; }; struct MetadataMessage { diff --git a/common/JFJochReceiverPlots.cpp b/common/JFJochReceiverPlots.cpp index 553b99ce..16a22b50 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); + ice_ring_score.Clear(r); spot_count.Clear(r); spot_count_low_res.Clear(r); spot_count_indexed.Clear(r); @@ -127,6 +128,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); + 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); spot_count_low_res.AddElement(msg.number, msg.spot_count_low_res); @@ -275,6 +277,9 @@ MultiLinePlot JFJochReceiverPlots::GetPlots(const PlotRequest &request) { case PlotType::BkgEstimate: ret = bkg_estimate.GetMeanPlot(nbins, start, incr, request.fill_value); break; + case PlotType::IceRingScore: + ret = ice_ring_score.GetMeanPlot(nbins, start, incr, request.fill_value); + break; case PlotType::ResolutionEstimate: ret = resolution_estimate.GetMeanPlot(nbins, start, incr, request.fill_value); break; @@ -534,6 +539,9 @@ void JFJochReceiverPlots::GetPlotRaw(std::vector &v, PlotType type, const case PlotType::BkgEstimate: v = bkg_estimate.ExportArray(); break; + case PlotType::IceRingScore: + v = ice_ring_score.ExportArray(); + break; case PlotType::ResolutionEstimate: v = resolution_estimate.ExportArray(); break; diff --git a/common/JFJochReceiverPlots.h b/common/JFJochReceiverPlots.h index 70f1cc91..01b7c17c 100644 --- a/common/JFJochReceiverPlots.h +++ b/common/JFJochReceiverPlots.h @@ -42,6 +42,7 @@ class JFJochReceiverPlots { AutoIncrVector xfel_event_code; StatusVector bkg_estimate; + StatusVector ice_ring_score; StatusVector spot_count; StatusVector spot_count_low_res; StatusVector spot_count_indexed; diff --git a/common/Plot.h b/common/Plot.h index 943d0135..4ecbe1c1 100644 --- a/common/Plot.h +++ b/common/Plot.h @@ -15,7 +15,7 @@ enum class PlotType { ROISum, ROIMean, ROIMaxCount, ROIPixels, ROIWeightedX, ROIWeightedY, PacketsReceived, MaxValue, ResolutionEstimate, ProfileRadius, Mosaicity, BFactor, PixelSum, StrongPixels, RefinementBeamX, RefinementBeamY, ImageProcessingTime, IntegratedReflections, - ImageScaleFactor, ImageScaleCC, ImageScaleBFactor, CompressionRatio, IndexingLatticeCount + ImageScaleFactor, ImageScaleCC, ImageScaleBFactor, CompressionRatio, IndexingLatticeCount, IceRingScore }; enum class PlotAzintUnit { diff --git a/common/ScanResult.h b/common/ScanResult.h index dc385806..d339cfb5 100644 --- a/common/ScanResult.h +++ b/common/ScanResult.h @@ -42,6 +42,7 @@ struct ScanResultElem { std::optional integrated_reflections; std::optional image_scale_factor; std::optional image_scale_cc; + std::optional ice_ring_score; }; struct ScanResult { diff --git a/common/ScanResultGenerator.cpp b/common/ScanResultGenerator.cpp index c7f3d295..3cc19bf1 100644 --- a/common/ScanResultGenerator.cpp +++ b/common/ScanResultGenerator.cpp @@ -62,6 +62,7 @@ void ScanResultGenerator::Add(const DataMessage &message) { v[image_number].integrated_reflections = message.integrated_reflections; v[image_number].image_scale_factor = message.image_scale_factor; v[image_number].image_scale_cc = message.image_scale_cc; + v[image_number].ice_ring_score = message.ice_ring_score; if (message.lattice_type) v[image_number].niggli_class = message.lattice_type->niggli_class; } @@ -103,6 +104,7 @@ void ScanResultGenerator::FillEndMessage(EndMessage &message) const { message.error_pixel_count.resize(n); message.image_scale_factor.resize(n); message.image_scale_cc.resize(n); + message.ice_ring_score.resize(n); message.integrated_reflections.resize(n); message.niggli_class.resize(n); message.pixel_sum.resize(n); @@ -133,6 +135,7 @@ void ScanResultGenerator::FillEndMessage(EndMessage &message) const { message.error_pixel_count[number] = static_cast(value_or_zero(e.err_pixels)); message.image_scale_factor[number] = e.image_scale_factor.value_or(NAN); message.image_scale_cc[number] = e.image_scale_cc.value_or(NAN); + message.ice_ring_score[number] = e.ice_ring_score.value_or(NAN); message.integrated_reflections[number] = static_cast(value_or_zero(e.integrated_reflections)); message.niggli_class[number] = static_cast(value_or_zero(e.niggli_class)); message.pixel_sum[number] = value_or_zero(e.pixel_sum); diff --git a/docs/CBOR.md b/docs/CBOR.md index cf25bc8c..79bacf64 100644 --- a/docs/CBOR.md +++ b/docs/CBOR.md @@ -215,6 +215,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 band/shoulder intensity ratio from the azint profile (1 = no ice) | | | | 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 | | image_scale_factor | float | Scaling result: Image scale factor (g) | | X | @@ -305,6 +306,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 | | +| ice_ring_score | Array(float) | Per-image strongest ice-ring band/shoulder intensity ratio (1 = no ice) | | | profile_radius | Array(float) | Per-image profile radius \[Angstrom^-1\] | | | mosaicity | Array(float) | Per-image mosaicity \[degree\] | | | bFactor | Array(float) | Per-image estimated B-factor \[Angstrom^2\] | | diff --git a/docs/CPU_DATA_ANALYSIS.md b/docs/CPU_DATA_ANALYSIS.md index 5dc8bc51..df352a12 100644 --- a/docs/CPU_DATA_ANALYSIS.md +++ b/docs/CPU_DATA_ANALYSIS.md @@ -180,6 +180,8 @@ Special cases: Spot finding can be restricted to a resolution range $[d_\mathrm{high}, d_\mathrm{low}]$ by masking pixels outside the range. Optionally, pixels in identified ice-ring regions can be tagged so that subsequent indexing/refinement may include or exclude them (see §4 and §6). +A single per-image **ice-ring score** is derived from the azimuthally-integrated radial profile: for each hexagonal-ice powder ring (positions $d$ from Moreau *et al.*, Acta Cryst D77, 2021), the mean profile intensity in the ring band ($\pm$ ice-ring half-width in $q$) is divided by a baseline interpolated from the two shoulders just outside it, and the strongest ring's ratio is reported (1 = no ice, $>1$ = ice above background). It is stored per image (`ice_ring_score`, HDF5 `/entry/MX/iceRingScore`) as a monitoring quantity. Note this is distinct from the merge-time ice masking, which is data-driven from the per-ring merged CC1/2 rather than this background ratio. + A further optional safeguard removes isolated high-resolution “spur” spots by detecting large gaps in $1/d$ (or $q$) space and discarding spots beyond the gap. This is intended for macromolecular diffraction where edge-of-detector backgrounds can be extremely low. ### 3.3 Connected-component labeling (CCL) diff --git a/docs/HDF5.md b/docs/HDF5.md index ddfc52b2..c98f4915 100644 --- a/docs/HDF5.md +++ b/docs/HDF5.md @@ -251,6 +251,7 @@ In legacy/VDS mode these live in the data files and are linked/virtual-stacked i | `resolutionEstimate` | Å | diffraction resolution estimate | | `integratedReflections` | | number of integrated reflections | | `bkgEstimate` | photons | mean background in the 3–5 Å resolution band | +| `iceRingScore` | ratio | strongest hexagonal-ice ring band/shoulder intensity ratio (1 = no ice) | | `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 | diff --git a/frame_serialize/CBORStream2Deserializer.cpp b/frame_serialize/CBORStream2Deserializer.cpp index 4d8c3a34..877c71a2 100644 --- a/frame_serialize/CBORStream2Deserializer.cpp +++ b/frame_serialize/CBORStream2Deserializer.cpp @@ -814,6 +814,8 @@ namespace { message.packets_received = GetCBORUInt(value); else if (key == "bkg_estimate") message.bkg_estimate = GetCBORFloat(value); + else if (key == "ice_ring_score") + message.ice_ring_score = GetCBORFloat(value); else if (key == "adu_histogram") GetCBORUInt64Array(value, message.adu_histogram); else if (key == "beam_corr_x") diff --git a/frame_serialize/CBORStream2Serializer.cpp b/frame_serialize/CBORStream2Serializer.cpp index 6e2bbb0e..da5134fe 100644 --- a/frame_serialize/CBORStream2Serializer.cpp +++ b/frame_serialize/CBORStream2Serializer.cpp @@ -863,6 +863,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_score); 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 78899b07..54f29f16 100644 --- a/frontend/src/client/types.gen.ts +++ b/frontend/src/client/types.gen.ts @@ -1010,6 +1010,10 @@ export type scan_result = { * Spot count within common ice ring resolutions */ spots_ice?: number; + /** + * Strongest hexagonal-ice ring band/shoulder intensity ratio (1 = no ice) + */ + ice_ring_score?: number; /** * Indexing solution */ @@ -1696,7 +1700,8 @@ export const plot_type = { IMAGE_SCALE_FACTOR: 'image_scale_factor', IMAGE_SCALE_CC: 'image_scale_cc', IMAGE_SCALE_B: 'image_scale_b', - COMPRESSION_RATIO: 'compression_ratio' + COMPRESSION_RATIO: 'compression_ratio', + ICE_RING_SCORE: 'ice_ring_score' } as const; /** @@ -2948,7 +2953,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' | 'image_scale_b' | 'compression_ratio'; + 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' | 'image_scale_b' | 'compression_ratio' | 'ice_ring_score'; /** * Fill value for elements that were missed during data collection * @@ -2995,7 +3000,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' | 'image_scale_b' | 'compression_ratio'; + 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' | 'image_scale_b' | '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 93456ffe..b2e79080 100644 --- a/frontend/src/client/zod.gen.ts +++ b/frontend/src/client/zod.gen.ts @@ -415,6 +415,7 @@ export const zScanResult = z.object({ 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(), spots_ice: 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(), + ice_ring_score: z.number().optional(), index: 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(), latt_count: 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(), pr: z.number().optional(), @@ -814,7 +815,8 @@ export const zPlotType = z.enum([ 'image_scale_factor', 'image_scale_cc', 'image_scale_b', - 'compression_ratio' + 'compression_ratio', + 'ice_ring_score' ]); /** @@ -1111,7 +1113,8 @@ export const zGetPreviewPlotQuery = z.object({ 'image_scale_factor', 'image_scale_cc', 'image_scale_b', - 'compression_ratio' + 'compression_ratio', + 'ice_ring_score' ]), fill: z.number().optional(), experimental_coord: z.boolean().optional().default(false), @@ -1166,7 +1169,8 @@ export const zGetPreviewPlotBinQuery = z.object({ 'image_scale_factor', 'image_scale_cc', 'image_scale_b', - 'compression_ratio' + 'compression_ratio', + 'ice_ring_score' ]), roi: z.string().min(1).optional() }); diff --git a/frontend/src/components/DataProcessingPlot.tsx b/frontend/src/components/DataProcessingPlot.tsx index b693e797..f09f6223 100644 --- a/frontend/src/components/DataProcessingPlot.tsx +++ b/frontend/src/components/DataProcessingPlot.tsx @@ -50,6 +50,8 @@ 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_SCORE: + return "Ratio"; 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 1e8206c2..7e950811 100644 --- a/frontend/src/components/DataProcessingPlots.tsx +++ b/frontend/src/components/DataProcessingPlots.tsx @@ -50,6 +50,7 @@ function DataProcessingPlots({type: initialType, height}: MyProps) { Spot count low res. Spot count indexed Spot count ice ring + Ice ring score Azimuthal integration profile Azimuthal integration profile (1D) Background estimate diff --git a/image_analysis/MXAnalysisWithoutFPGA.cpp b/image_analysis/MXAnalysisWithoutFPGA.cpp index 2adcf932..c3569609 100644 --- a/image_analysis/MXAnalysisWithoutFPGA.cpp +++ b/image_analysis/MXAnalysisWithoutFPGA.cpp @@ -113,6 +113,8 @@ void MXAnalysisWithoutFPGA::Analyze(DataMessage &output, output.az_int_profile_std = profile.GetStd(); output.bkg_estimate = profile.GetBkgEstimate(integration.Settings()); + output.ice_ring_score = profile.GetIceRingScore(integration.Settings(), + spot_finding_settings.ice_ring_width_Q_recipA); } void MXAnalysisWithoutFPGA::RebuildROI() { diff --git a/process/JFJochProcess.cpp b/process/JFJochProcess.cpp index f2981917..2acd8ee6 100644 --- a/process/JFJochProcess.cpp +++ b/process/JFJochProcess.cpp @@ -409,6 +409,8 @@ ProcessResult JFJochProcess::Run(JFJochProcessObserver *observer) { msg.az_int_profile_count = profile.GetPixelCount(); msg.az_int_profile_std = profile.GetStd(); msg.bkg_estimate = profile.GetBkgEstimate(mapping.Settings()); + msg.ice_ring_score = profile.GetIceRingScore(mapping.Settings(), + config_.spot_finding.ice_ring_width_Q_recipA); msg.run_number = experiment_.GetRunNumber(); msg.run_name = experiment_.GetRunName(); diff --git a/reader/JFJochHttpReader.cpp b/reader/JFJochHttpReader.cpp index fe1a1f6d..b2223e5b 100644 --- a/reader/JFJochHttpReader.cpp +++ b/reader/JFJochHttpReader.cpp @@ -175,6 +175,7 @@ std::shared_ptr JFJochHttpReader::UpdateDataset_i() { dataset->experiment.FluorescenceSpectrum(msg->start_message->fluorescence_spectrum); dataset->bkg_estimate = GetPlot_i("bkg_estimate"); + 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"); dataset->spot_count_low_res = GetPlot_i("spot_count_low_res"); diff --git a/reader/JFJochReaderDataset.h b/reader/JFJochReaderDataset.h index 6fe956f0..b7db8b80 100644 --- a/reader/JFJochReaderDataset.h +++ b/reader/JFJochReaderDataset.h @@ -36,6 +36,7 @@ struct JFJochReaderDataset { std::vector indexing_result; std::vector indexing_lattice_count; std::vector bkg_estimate; + std::vector ice_ring_score; std::vector resolution_estimate; std::vector efficiency; std::vector profile_radius; diff --git a/receiver/JFJochReceiverFPGA.cpp b/receiver/JFJochReceiverFPGA.cpp index 5d8aa6cc..23288259 100644 --- a/receiver/JFJochReceiverFPGA.cpp +++ b/receiver/JFJochReceiverFPGA.cpp @@ -434,6 +434,8 @@ void JFJochReceiverFPGA::FrameTransformationThread(uint32_t threadid) { if (force_cpu_azint) message.az_int_profile_std = az_int_profile_image.GetStd(); message.bkg_estimate = az_int_profile_image.GetBkgEstimate(experiment.GetAzimuthalIntegrationSettings()); + message.ice_ring_score = az_int_profile_image.GetIceRingScore( + experiment.GetAzimuthalIntegrationSettings(), spot_finding_settings.ice_ring_width_Q_recipA); scan_result.Add(message); diff --git a/viewer/JFJochViewerDatasetInfo.cpp b/viewer/JFJochViewerDatasetInfo.cpp index cd426c11..239b2d0a 100644 --- a/viewer/JFJochViewerDatasetInfo.cpp +++ b/viewer/JFJochViewerDatasetInfo.cpp @@ -132,6 +132,7 @@ void JFJochViewerDatasetInfo::UpdateLabels() { if (this->dataset) { combo_box->addItem("Background estimate", 0); + combo_box->addItem("Ice ring score", 14); combo_box->addItem("Resolution estimate", 7); combo_box->addItem("Spot count", 1); combo_box->addItem("Spot count (indexed)", 2); @@ -233,6 +234,7 @@ std::vector JFJochViewerDatasetInfo::ExtractMetric(const JFJochReaderData else if (val == 11) data = ds.image_scale_cc; 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 >= 100) { const int roi_index = (val - 100) / 4; if (val % 4 == 0) { diff --git a/writer/HDF5DataFilePluginMX.cpp b/writer/HDF5DataFilePluginMX.cpp index 259faa3c..130b3eee 100644 --- a/writer/HDF5DataFilePluginMX.cpp +++ b/writer/HDF5DataFilePluginMX.cpp @@ -53,6 +53,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); if (max_spots == 0) return; @@ -98,6 +99,8 @@ void HDF5DataFilePluginMX::OpenFile(HDF5File &data_file, const DataMessage &msg, void HDF5DataFilePluginMX::Write(const DataMessage &msg, uint64_t image_number) { if (msg.bkg_estimate.has_value()) 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 (max_spots == 0) return; @@ -245,6 +248,8 @@ void HDF5DataFilePluginMX::WriteFinal(HDF5File &data_file) { if (!bkg_estimate.empty()) 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 (!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 53a2f6f1..745c7580 100644 --- a/writer/HDF5DataFilePluginMX.h +++ b/writer/HDF5DataFilePluginMX.h @@ -46,6 +46,7 @@ class HDF5DataFilePluginMX : public HDF5DataFilePlugin { // bkg_estimate AutoIncrVector bkg_estimate{NAN}; + AutoIncrVector ice_ring_score{NAN}; // resolution_estimation AutoIncrVector resolution_estimate{NAN};