Compare commits

..

24 Commits

Author SHA1 Message Date
leonarski_f 4b6a042e4a jfjoch_test: Fix TCPImagePusherTest to ensure it is more stable
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 12m59s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 15m46s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 17m26s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 17m24s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 17m30s
Build Packages / build:rpm (rocky8) (push) Successful in 17m37s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 18m19s
Build Packages / build:rpm (rocky9) (push) Successful in 10m12s
Build Packages / Generate python client (push) Successful in 50s
Build Packages / Build documentation (push) Successful in 51s
Build Packages / Create release (push) Has been skipped
Build Packages / build:rpm (ubuntu2204) (push) Successful in 11m18s
Build Packages / XDS test (neggia plugin) (push) Successful in 10m24s
Build Packages / XDS test (durin plugin) (push) Successful in 11m28s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 11m56s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 11m46s
Build Packages / DIALS test (push) Successful in 14m30s
Build Packages / Unit tests (push) Successful in 59m57s
2026-05-10 16:44:06 +02:00
leonarski_f b9af590ff5 Refactor splitting scale and merge
Build Packages / Unit tests (push) Failing after 9m5s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 15m3s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 15m28s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 15m59s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 17m15s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 17m26s
Build Packages / build:rpm (rocky8) (push) Successful in 18m11s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 18m58s
Build Packages / build:rpm (rocky9) (push) Successful in 11m14s
Build Packages / Generate python client (push) Successful in 1m33s
Build Packages / Create release (push) Has been skipped
Build Packages / Build documentation (push) Successful in 1m57s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 9m38s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 11m38s
Build Packages / XDS test (neggia plugin) (push) Successful in 8m56s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 9m56s
Build Packages / XDS test (durin plugin) (push) Successful in 10m13s
Build Packages / DIALS test (push) Successful in 13m10s
2026-05-10 16:05:45 +02:00
leonarski_f a441e96b50 HKLKey: Fix
Build Packages / Unit tests (push) Failing after 6m44s
Build Packages / build:rpm (rocky8_nocuda) (push) Failing after 7m54s
Build Packages / build:rpm (rocky9_nocuda) (push) Failing after 9m10s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Failing after 8m55s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Failing after 9m25s
Build Packages / build:rpm (rocky9_sls9) (push) Failing after 12m14s
Build Packages / build:rpm (rocky8_sls9) (push) Failing after 13m54s
Build Packages / build:rpm (rocky8) (push) Failing after 13m18s
Build Packages / Generate python client (push) Successful in 47s
Build Packages / DIALS test (push) Failing after 9m3s
Build Packages / Create release (push) Has been skipped
Build Packages / XDS test (durin plugin) (push) Failing after 8m9s
Build Packages / build:rpm (rocky9) (push) Failing after 12m12s
Build Packages / Build documentation (push) Successful in 43s
Build Packages / build:rpm (ubuntu2404) (push) Failing after 11m4s
Build Packages / build:rpm (ubuntu2204) (push) Failing after 12m44s
Build Packages / XDS test (JFJoch plugin) (push) Failing after 5m27s
Build Packages / XDS test (neggia plugin) (push) Failing after 4m56s
2026-05-10 13:44:01 +02:00
leonarski_f a90ec13418 Split scaling and merge into separate files (WIP)
Build Packages / Unit tests (push) Failing after 7m22s
Build Packages / build:rpm (rocky8_nocuda) (push) Failing after 8m49s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Failing after 10m17s
Build Packages / build:rpm (rocky9_nocuda) (push) Failing after 10m25s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Failing after 10m22s
Build Packages / build:rpm (rocky8) (push) Failing after 14m34s
Build Packages / build:rpm (rocky9_sls9) (push) Failing after 14m47s
Build Packages / build:rpm (rocky8_sls9) (push) Failing after 14m49s
Build Packages / Generate python client (push) Successful in 50s
Build Packages / Build documentation (push) Successful in 1m36s
Build Packages / Create release (push) Has been skipped
Build Packages / build:rpm (rocky9) (push) Failing after 10m28s
Build Packages / XDS test (durin plugin) (push) Failing after 7m28s
Build Packages / DIALS test (push) Failing after 7m44s
Build Packages / build:rpm (ubuntu2204) (push) Failing after 9m54s
Build Packages / build:rpm (ubuntu2404) (push) Failing after 10m15s
Build Packages / XDS test (neggia plugin) (push) Failing after 6m28s
Build Packages / XDS test (JFJoch plugin) (push) Failing after 7m16s
2026-05-10 13:39:32 +02:00
leonarski_f 81af116b4d ScaleAndMerge: Minor clean-up 2026-05-10 10:51:01 +02:00
leonarski_f 3e4e6019c4 ScaleAndMerge: Add "just" merge function for now
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 14m45s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 15m12s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 15m16s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 16m17s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 17m13s
Build Packages / build:rpm (rocky8) (push) Successful in 17m17s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 18m17s
Build Packages / build:rpm (rocky9) (push) Successful in 12m41s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 12m20s
Build Packages / Generate python client (push) Successful in 36s
Build Packages / Create release (push) Has been skipped
Build Packages / Build documentation (push) Successful in 54s
Build Packages / XDS test (neggia plugin) (push) Successful in 10m31s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 13m40s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 11m51s
Build Packages / XDS test (durin plugin) (push) Successful in 12m0s
Build Packages / DIALS test (push) Successful in 15m16s
Build Packages / Unit tests (push) Successful in 1h0m18s
2026-05-10 10:42:56 +02:00
leonarski_f e392a3ae1b ScaleAndMerge: Simplify merge and go back to "standard" weights 2026-05-10 10:34:24 +02:00
leonarski_f cc42ae6bf6 jfjoch_process: Fix bounds for VDS mapping
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 15m21s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 17m39s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 17m35s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 18m9s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 18m3s
Build Packages / build:rpm (rocky8) (push) Successful in 18m20s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m3s
Build Packages / build:rpm (rocky9) (push) Successful in 10m20s
Build Packages / Generate python client (push) Successful in 32s
Build Packages / Build documentation (push) Successful in 1m6s
Build Packages / Create release (push) Has been skipped
Build Packages / build:rpm (ubuntu2404) (push) Successful in 9m58s
Build Packages / XDS test (durin plugin) (push) Successful in 11m14s
Build Packages / XDS test (neggia plugin) (push) Successful in 10m31s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 12m1s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 13m24s
Build Packages / DIALS test (push) Successful in 15m4s
Build Packages / Unit tests (push) Successful in 1h0m17s
2026-05-09 20:43:39 +02:00
leonarski_f cf2ca90fb7 jfjoch_test: Improve a bit XtalOptimizer_hexagonal test hoping for more consistent result
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 13m37s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 14m35s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 15m52s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 16m0s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m8s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 16m48s
Build Packages / build:rpm (rocky8) (push) Successful in 9m49s
Build Packages / build:rpm (rocky9) (push) Successful in 12m17s
Build Packages / XDS test (durin plugin) (push) Successful in 10m3s
Build Packages / Generate python client (push) Successful in 34s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 12m7s
Build Packages / Create release (push) Has been skipped
Build Packages / build:rpm (ubuntu2204) (push) Successful in 12m39s
Build Packages / Build documentation (push) Successful in 1m6s
Build Packages / DIALS test (push) Successful in 14m1s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 7m17s
Build Packages / XDS test (neggia plugin) (push) Successful in 5m54s
Build Packages / Unit tests (push) Successful in 58m52s
2026-05-09 18:17:11 +02:00
leonarski_f 38c2826c09 JFJochHDF5Reader: Add ReadReflections function
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 13m46s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 15m19s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 15m49s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m44s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 17m5s
Build Packages / build:rpm (rocky8) (push) Successful in 17m17s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 18m15s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 11m44s
Build Packages / build:rpm (rocky9) (push) Successful in 13m29s
Build Packages / Generate python client (push) Successful in 35s
Build Packages / Create release (push) Has been skipped
Build Packages / Build documentation (push) Successful in 53s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 12m37s
Build Packages / XDS test (neggia plugin) (push) Successful in 10m19s
Build Packages / XDS test (durin plugin) (push) Successful in 11m41s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 11m34s
Build Packages / DIALS test (push) Successful in 14m15s
Build Packages / Unit tests (push) Successful in 55m15s
2026-05-09 08:39:07 +02:00
leonarski_f d718999335 FileWriter: Allow to save master files with custom suffix (like _process.h5) 2026-05-09 08:04:04 +02:00
leonarski_f 93914e1fb9 jfjoch_process: Fix typo
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 10m28s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 9m22s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 10m50s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 11m7s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 11m36s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 10m40s
Build Packages / build:rpm (rocky8) (push) Successful in 14m17s
Build Packages / XDS test (durin plugin) (push) Successful in 11m59s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 13m55s
Build Packages / Generate python client (push) Successful in 19s
Build Packages / Create release (push) Has been skipped
Build Packages / build:rpm (ubuntu2204) (push) Successful in 14m59s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 11m2s
Build Packages / Build documentation (push) Successful in 42s
Build Packages / build:rpm (rocky9) (push) Successful in 16m10s
Build Packages / DIALS test (push) Successful in 15m48s
Build Packages / XDS test (neggia plugin) (push) Successful in 6m2s
Build Packages / Unit tests (push) Successful in 57m47s
2026-05-08 20:09:46 +02:00
leonarski_f 0503ef0271 jfjoch_process: Adapt behavior to write file with processing results and link to original images (to be tested)
Build Packages / Unit tests (push) Has been cancelled
Build Packages / build:rpm (rocky8_nocuda) (push) Has been cancelled
Build Packages / build:rpm (rocky9_nocuda) (push) Has been cancelled
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Has been cancelled
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Has been cancelled
Build Packages / build:rpm (rocky8_sls9) (push) Has been cancelled
Build Packages / build:rpm (rocky9_sls9) (push) Has been cancelled
Build Packages / DIALS test (push) Has been cancelled
Build Packages / XDS test (neggia plugin) (push) Has been cancelled
Build Packages / Generate python client (push) Has been cancelled
Build Packages / Build documentation (push) Has been cancelled
Build Packages / build:rpm (rocky8) (push) Has been cancelled
Build Packages / build:rpm (rocky9) (push) Has been cancelled
Build Packages / build:rpm (ubuntu2204) (push) Has been cancelled
Build Packages / build:rpm (ubuntu2404) (push) Has been cancelled
Build Packages / XDS test (durin plugin) (push) Has been cancelled
Build Packages / XDS test (JFJoch plugin) (push) Has been cancelled
Build Packages / Create release (push) Has been cancelled
2026-05-08 20:06:41 +02:00
leonarski_f 1b8c528ed2 HDF5NXmx: Clean-up 2026-05-08 20:02:46 +02:00
leonarski_f 8b5eb5a208 HDF5NXmx: Don't write nPeaks in HDF5NXmx - this needs to be consistent with spot finding results, so should come from spot finder 2026-05-08 19:59:52 +02:00
leonarski_f da15714080 jfjoch_test: Fix tests
Build Packages / Unit tests (push) Has been cancelled
Build Packages / build:rpm (rocky8_nocuda) (push) Has been cancelled
Build Packages / build:rpm (rocky9_nocuda) (push) Has been cancelled
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Has been cancelled
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Has been cancelled
Build Packages / build:rpm (rocky8_sls9) (push) Has been cancelled
Build Packages / build:rpm (rocky9_sls9) (push) Has been cancelled
Build Packages / build:rpm (rocky8) (push) Has been cancelled
Build Packages / build:rpm (rocky9) (push) Has been cancelled
Build Packages / build:rpm (ubuntu2204) (push) Has been cancelled
Build Packages / build:rpm (ubuntu2404) (push) Has been cancelled
Build Packages / DIALS test (push) Has been cancelled
Build Packages / XDS test (durin plugin) (push) Has been cancelled
Build Packages / XDS test (JFJoch plugin) (push) Has been cancelled
Build Packages / XDS test (neggia plugin) (push) Has been cancelled
Build Packages / Generate python client (push) Has been cancelled
Build Packages / Build documentation (push) Has been cancelled
Build Packages / Create release (push) Has been cancelled
2026-05-08 19:56:24 +02:00
leonarski_f 91c714f1a4 Fix
Build Packages / build:rpm (rocky8_nocuda) (push) Failing after 9m9s
Build Packages / build:rpm (rocky9_nocuda) (push) Failing after 10m59s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Failing after 8m49s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Failing after 9m36s
Build Packages / build:rpm (rocky8_sls9) (push) Failing after 10m11s
Build Packages / build:rpm (rocky9_sls9) (push) Failing after 11m6s
Build Packages / build:rpm (rocky8) (push) Failing after 8m55s
Build Packages / build:rpm (rocky9) (push) Failing after 9m6s
Build Packages / Generate python client (push) Successful in 41s
Build Packages / Build documentation (push) Successful in 1m17s
Build Packages / Create release (push) Has been skipped
Build Packages / XDS test (durin plugin) (push) Successful in 9m9s
Build Packages / build:rpm (ubuntu2204) (push) Failing after 11m51s
Build Packages / build:rpm (ubuntu2404) (push) Failing after 12m46s
Build Packages / XDS test (neggia plugin) (push) Successful in 8m46s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 9m32s
Build Packages / DIALS test (push) Successful in 16m2s
Build Packages / Unit tests (push) Failing after 36m36s
2026-05-08 19:40:12 +02:00
leonarski_f 025c9b3aee FileWriter: Master HDF5 contains scalar per-image processing results, reader is also made to prioritize reading from master file
Build Packages / Unit tests (push) Failing after 8m2s
Build Packages / build:rpm (rocky9_nocuda) (push) Failing after 9m20s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Failing after 9m19s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Failing after 9m51s
Build Packages / build:rpm (rocky8_nocuda) (push) Failing after 10m57s
Build Packages / build:rpm (rocky9_sls9) (push) Failing after 12m46s
Build Packages / build:rpm (rocky8_sls9) (push) Failing after 14m28s
Build Packages / build:rpm (rocky8) (push) Failing after 14m27s
Build Packages / Generate python client (push) Successful in 1m15s
Build Packages / Build documentation (push) Successful in 1m30s
Build Packages / Create release (push) Has been skipped
Build Packages / build:rpm (rocky9) (push) Failing after 10m4s
Build Packages / build:rpm (ubuntu2404) (push) Failing after 10m8s
Build Packages / build:rpm (ubuntu2204) (push) Failing after 11m36s
Build Packages / XDS test (durin plugin) (push) Successful in 10m49s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 9m18s
Build Packages / XDS test (neggia plugin) (push) Successful in 8m52s
Build Packages / DIALS test (push) Successful in 15m45s
2026-05-08 19:37:16 +02:00
leonarski_f 27bcb19328 ImagePusher: Serializer defined inside ImagePusher class, not in subclasses 2026-05-08 17:47:01 +02:00
leonarski_f f5176b56a9 DiffractionExperiment: Max image number is 2 million + 1; it is unsafe to collect more 2026-05-08 17:46:20 +02:00
leonarski_f cd5d97aa55 FileWriter: Fix
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 10m22s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 12m20s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 11m31s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 14m36s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 14m37s
Build Packages / build:rpm (rocky8) (push) Successful in 12m57s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 14m39s
Build Packages / build:rpm (rocky9) (push) Successful in 11m13s
Build Packages / Generate python client (push) Successful in 56s
Build Packages / Build documentation (push) Successful in 54s
Build Packages / Create release (push) Has been skipped
Build Packages / build:rpm (ubuntu2404) (push) Successful in 8m39s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 9m57s
Build Packages / XDS test (neggia plugin) (push) Successful in 7m28s
Build Packages / XDS test (durin plugin) (push) Successful in 9m6s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 9m2s
Build Packages / DIALS test (push) Successful in 12m32s
Build Packages / Unit tests (push) Successful in 1h0m13s
2026-05-08 13:34:12 +02:00
leonarski_f 6c8c953c92 jfjoch_process: Cleanup help + by default save HDF5, but without analysis results
Build Packages / Unit tests (push) Failing after 7m55s
Build Packages / build:rpm (rocky8_nocuda) (push) Failing after 8m59s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Failing after 9m12s
Build Packages / build:rpm (rocky9_nocuda) (push) Failing after 10m20s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Failing after 10m21s
Build Packages / build:rpm (rocky9_sls9) (push) Failing after 11m38s
Build Packages / build:rpm (rocky8_sls9) (push) Failing after 11m40s
Build Packages / build:rpm (rocky8) (push) Failing after 11m47s
Build Packages / Generate python client (push) Successful in 1m35s
Build Packages / Build documentation (push) Successful in 2m4s
Build Packages / Create release (push) Has been skipped
Build Packages / build:rpm (rocky9) (push) Failing after 7m51s
Build Packages / DIALS test (push) Failing after 6m32s
Build Packages / build:rpm (ubuntu2204) (push) Failing after 8m14s
Build Packages / build:rpm (ubuntu2404) (push) Failing after 9m55s
Build Packages / XDS test (durin plugin) (push) Failing after 8m44s
Build Packages / XDS test (neggia plugin) (push) Failing after 8m53s
Build Packages / XDS test (JFJoch plugin) (push) Failing after 9m18s
2026-05-08 13:32:09 +02:00
leonarski_f 173198be40 HDF5DataFile: Include File name explicitly + FileWriter: Handle NXmxIntegrated in a smarter way
Build Packages / Unit tests (push) Failing after 8m37s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Failing after 9m50s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Failing after 9m49s
Build Packages / build:rpm (rocky8_nocuda) (push) Failing after 9m55s
Build Packages / build:rpm (rocky9_nocuda) (push) Failing after 10m20s
Build Packages / build:rpm (rocky8) (push) Failing after 11m27s
Build Packages / build:rpm (rocky9_sls9) (push) Failing after 11m37s
Build Packages / build:rpm (rocky8_sls9) (push) Failing after 11m40s
Build Packages / Generate python client (push) Successful in 1m27s
Build Packages / Build documentation (push) Successful in 2m17s
Build Packages / Create release (push) Has been skipped
Build Packages / build:rpm (rocky9) (push) Failing after 7m46s
Build Packages / build:rpm (ubuntu2204) (push) Failing after 7m46s
Build Packages / DIALS test (push) Failing after 7m53s
Build Packages / XDS test (durin plugin) (push) Failing after 8m6s
Build Packages / XDS test (JFJoch plugin) (push) Failing after 6m51s
Build Packages / XDS test (neggia plugin) (push) Failing after 6m47s
Build Packages / build:rpm (ubuntu2404) (push) Failing after 9m21s
2026-05-08 13:06:44 +02:00
leonarski_f 930cfb0b35 HDF5DataFile: Saving images is a configurable option 2026-05-08 12:31:56 +02:00
62 changed files with 2610 additions and 1497 deletions
+2
View File
@@ -17,6 +17,8 @@ constexpr size_t CONVERTED_MODULE_COLS = 1030;
constexpr size_t CONVERTED_MODULE_SIZE = CONVERTED_MODULE_LINES * CONVERTED_MODULE_COLS;
constexpr size_t JUNGFRAU_PACKET_SIZE_BYTES = 8192;
constexpr int MAX_IMAGE_NUMBER = 2*1024*1024;
constexpr std::chrono::nanoseconds MIN_COUNT_TIME = std::chrono::microseconds(3);
constexpr std::chrono::nanoseconds MIN_STORAGE_CELL_DELAY = std::chrono::nanoseconds(2100);
constexpr std::chrono::nanoseconds MIN_FRAME_TIME_JUNGFRAU_HALF_SPEED = std::chrono::microseconds(1000);
+6
View File
@@ -1060,6 +1060,12 @@ DiffractionExperiment &DiffractionExperiment::ImportDatasetSettings(const Datase
+ std::to_string(MAX_FRAMES));
}
if (GetImageNum() > MAX_IMAGE_NUMBER) {
dataset = tmp;
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Number of images cannot exceed " + std::to_string(MAX_IMAGE_NUMBER));
}
return *this;
}
+34 -2
View File
@@ -24,7 +24,7 @@
#include "XrayFluorescenceSpectrum.h"
#include "../symmetry/gemmi/symmetry.hpp"
constexpr const uint64_t user_data_release = 5;
constexpr const uint64_t user_data_release = 6;
constexpr const uint64_t user_data_magic_number = 0x52320000UL | user_data_release;
enum class CBORImageType {START, END, IMAGE, CALIBRATION, METADATA, NONE};
@@ -168,6 +168,15 @@ struct DataMessage {
std::optional<LatticeMessage> lattice_type;
};
struct HDF5DataSourceMessage {
std::string filename;
std::string dataset = "/entry/data/data";
uint64_t source_first_image = 0;
uint64_t virtual_first_image = 0;
uint64_t image_count = 0;
};
struct StartMessage {
float detector_distance;
float beam_center_x;
@@ -251,6 +260,7 @@ struct StartMessage {
std::optional<float> attenuator_transmission;
std::optional<bool> write_master_file;
std::optional<bool> write_images;
nlohmann::json user_data;
@@ -282,6 +292,9 @@ struct StartMessage {
XrayFluorescenceSpectrum fluorescence_spectrum;
std::optional<bool> detect_ice_rings;
std::vector<HDF5DataSourceMessage> hdf5_source_data;
std::optional<std::string> master_suffix;
};
struct EndMessage {
@@ -305,7 +318,26 @@ struct EndMessage {
std::optional<LatticeMessage> rotation_lattice_type;
std::optional<CrystalLattice> rotation_lattice;
std::vector<float> scale_factor;
// Vectors with end result:
std::vector<float> data_collection_efficiency;
std::vector<int32_t> spot_count;
std::vector<int32_t> spot_count_ice_ring;
std::vector<int32_t> spot_count_low_res;
std::vector<int32_t> spot_count_indexed;
std::vector<uint8_t> image_indexed;
std::vector<float> v_bkg_estimate;
std::vector<float> profile_radius;
std::vector<float> mosaicity;
std::vector<float> bFactor;
std::vector<float> resolution_estimate;
std::vector<int64_t> min_viable_pixel_value;
std::vector<int64_t> max_viable_pixel_value;
std::vector<int32_t> saturated_pixel_count;
std::vector<int32_t> error_pixel_count;
std::vector<float> image_scale_factor;
std::vector<int32_t> integrated_reflections;
std::vector<uint8_t> niggli_class;
std::vector<int64_t> pixel_sum;
};
struct MetadataMessage {
+10
View File
@@ -24,7 +24,17 @@ struct Reflection {
float rlp;
float partiality;
float zeta;
float scaling_correction; // I_true = scaling_correction * I; scaling_correction = rlp / (partiality * image_scale)
bool observed = false;
};
struct MergedReflection {
int32_t h;
int32_t k;
int32_t l;
float I;
float sigma;
float d = 0.0;
};
#endif //JFJOCH_REFLECTION_H
+5
View File
@@ -22,6 +22,7 @@ struct ScanResultElem {
std::optional<float> angle_deg;
std::optional<uint64_t> pixel_sum;
std::optional<int64_t> min_viable_pixel;
std::optional<int64_t> max_viable_pixel;
std::optional<int64_t> err_pixels;
std::optional<int64_t> sat_pixels;
@@ -36,6 +37,10 @@ struct ScanResultElem {
std::optional<float> res;
std::optional<UnitCell> uc;
std::optional<uint64_t> xfel_pulse_id;
std::optional<float> mosaicity;
std::optional<float> image_scale_factor;
std::optional<int64_t> niggli_class;
std::optional<int64_t> integrated_reflections;
};
struct ScanResult {
+78 -1
View File
@@ -1,8 +1,18 @@
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <algorithm>
#include <cmath>
#include "ScanResultGenerator.h"
namespace {
template<class T>
T value_or_zero(const std::optional<T>& v) {
return v.value_or(static_cast<T>(0));
}
}
ScanResultGenerator::ScanResultGenerator(const DiffractionExperiment &experiment) {
grid_scan = experiment.GetGridScan();
goniometer_axis = experiment.GetGoniometer();
@@ -21,7 +31,7 @@ void ScanResultGenerator::Add(const DataMessage &message) {
if (grid_scan)
image_number = grid_scan->Rearrange(image_number);
if (image_number < v.size()) {
if (image_number >= 0 && static_cast<size_t>(image_number) < v.size()) {
if (grid_scan) {
v[image_number].x = grid_scan->GetElementPosX_step(message.number);
v[image_number].y = grid_scan->GetElementPosY_step(message.number);
@@ -36,16 +46,22 @@ void ScanResultGenerator::Add(const DataMessage &message) {
v[image_number].spot_count = message.spot_count;
v[image_number].indexing_solution = message.indexing_result;
v[image_number].profile_radius = message.profile_radius;
v[image_number].mosaicity = message.mosaicity_deg;
v[image_number].b_factor = message.b_factor;
v[image_number].uc = message.indexing_unit_cell;
v[image_number].xfel_pulse_id = message.xfel_pulse_id;
v[image_number].err_pixels = message.error_pixel_count;
v[image_number].min_viable_pixel = message.min_viable_pixel_value;
v[image_number].max_viable_pixel = message.max_viable_pixel_value;
v[image_number].sat_pixels = message.saturated_pixel_count;
v[image_number].spot_count_ice = message.spot_count_ice_rings;
v[image_number].spot_count_low_res = message.spot_count_low_res;
v[image_number].spot_count_indexed = message.spot_count_indexed;
v[image_number].res = message.resolution_estimate;
v[image_number].integrated_reflections = message.integrated_reflections;
if (message.lattice_type)
v[image_number].niggli_class = message.lattice_type->niggli_class;
}
}
@@ -56,3 +72,64 @@ ScanResult ScanResultGenerator::GetResult() const {
ret.images = v;
return ret;
}
void ScanResultGenerator::FillEndMessage(EndMessage &message) const {
std::unique_lock ul(m);
size_t n = 0;
for (const auto &e: v) {
if (e.number >= 0)
n = std::max(n, static_cast<size_t>(e.number) + 1);
}
if (n == 0)
return;
message.data_collection_efficiency.resize(n);
message.spot_count.resize(n);
message.spot_count_ice_ring.resize(n);
message.spot_count_low_res.resize(n);
message.spot_count_indexed.resize(n);
message.image_indexed.resize(n);
message.v_bkg_estimate.resize(n);
message.profile_radius.resize(n);
message.mosaicity.resize(n);
message.bFactor.resize(n);
message.resolution_estimate.resize(n);
message.min_viable_pixel_value.resize(n);
message.max_viable_pixel_value.resize(n);
message.saturated_pixel_count.resize(n);
message.error_pixel_count.resize(n);
message.image_scale_factor.resize(n);
message.integrated_reflections.resize(n);
message.niggli_class.resize(n);
message.pixel_sum.resize(n);
for (const auto &e: v) {
if (e.number < 0)
continue;
const auto number = static_cast<size_t>(e.number);
if (number >= n)
continue;
message.data_collection_efficiency[number] = e.collection_efficiency;
message.spot_count[number] = static_cast<int32_t>(value_or_zero(e.spot_count));
message.spot_count_ice_ring[number] = static_cast<int32_t>(value_or_zero(e.spot_count_ice));
message.spot_count_low_res[number] = static_cast<int32_t>(value_or_zero(e.spot_count_low_res));
message.spot_count_indexed[number] = static_cast<int32_t>(value_or_zero(e.spot_count_indexed));
message.image_indexed[number] = static_cast<uint8_t>(e.indexing_solution.value_or(0));
message.v_bkg_estimate[number] = e.bkg.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);
message.resolution_estimate[number] = e.res.value_or(NAN);
message.min_viable_pixel_value[number] = value_or_zero(e.min_viable_pixel);
message.max_viable_pixel_value[number] = value_or_zero(e.max_viable_pixel);
message.saturated_pixel_count[number] = static_cast<int32_t>(value_or_zero(e.sat_pixels));
message.error_pixel_count[number] = static_cast<int32_t>(value_or_zero(e.err_pixels));
message.image_scale_factor[number] = e.image_scale_factor.value_or(NAN);
message.integrated_reflections[number] = static_cast<int32_t>(value_or_zero(e.integrated_reflections));
message.niggli_class[number] = static_cast<uint8_t>(value_or_zero(e.niggli_class));
message.pixel_sum[number] = static_cast<int64_t>(value_or_zero(e.pixel_sum));
}
}
+2
View File
@@ -25,6 +25,8 @@ public:
explicit ScanResultGenerator(const DiffractionExperiment& experiment);
void Add(const DataMessage& message);
ScanResult GetResult() const;
void FillEndMessage(EndMessage &message) const;
};
+156 -122
View File
@@ -12,105 +12,106 @@ There are minor differences at the moment:
## Start message
| Field name | Type | Description | Present in DECTRIS format |
|----------------------------------|----------------------|-----------------------------------------------------------------------------------------------------------------------------------|:-------------------------:|
| type | String | value "start" | X |
| magic_number | uint64 | Number used to describe version of the Jungfraujoch data interface - to allow to detect inconsistency between sender and receiver | |
| detector_distance | float | Detector distance \[m\] | |
| detector_translation | Array(float) | Detector translation vector \[m\] | X |
| beam_center_x | float | Beam center in X direction \[pixels\] | X |
| beam_center_y | float | Beam center in Y direction \[pixels\] | X |
| countrate_correction_enabled | bool | Countrate correction enabled | X |
| flatfield_enabled | bool | Flatfield enabled | X |
| number_of_images | uint64 | Number of images in the series | X |
| image_size_x | uint64 | Image width \[pixels\] | X |
| image_size_y | uint64 | Image height \[pixels\] | X |
| incident_energy | float | X-ray energy \[eV\] | X |
| incident_wavelength | float | X-ray wavelength \[Angstrom\] | X |
| frame_time | float | Frame time, if multiple frames per trigger \[s\] | X |
| count_time | float | Exposure time \[s\] | X |
| saturation_value | int64 | Maximum valid sample value | X |
| error_value | int64 (optional) | Value used in images to describe pixels that are in error state or missing | |
| pixel_size_x | float | Pixel width \[m\] | X |
| pixel_size_y | float | Pixel height \[m\] | X |
| sensor_thickness | float | Sensor thickness \[m\] | X |
| sensor_material | string | Sensor material | X |
| arm_date | date | Approximate date of arming | X |
| pixel_mask_enabled | bool | Pixel mask applied on images | X |
| detector_description | string | Name of the detector | X |
| detector_serial_number | string | Detector serial number | X |
| series_unique_id | string | Unique text ID of the series (run_name parameter) | X |
| series_id | uint64 | Unique numeric ID of the series (run_number parameter) | X |
| fluorescence | object (optional) | X-ray fluorescence spectrum collected at start | |
| - energy | Array(float) | Energy of measuring point \[eV\] | |
| - data | Array(float) | Fluorescence scan result `data` \[arbitrary units\]; must be strictly the same length as energy | |
| goniometer | Map | Definition of rotation axis (optional) | X |
| - `AXIS` | string | Rotation axis name (e.g. omega) - only one axis is supported in Jungfraujoch | X |
| - - increment | float | Rotation axis increment (per image) in degree \[deg\] | X |
| - - start | float | Rotation axis start angle \[deg\] | X |
| - - axis | Array(float) | Vector for the rotation axis | |
| - - helical_step | Array(float) | Translation for helical scan for 1 image \[m\] | |
| - - screening_wedge | Array(float) | Wedge for screening \[deg\] (increment would correspond to difference between screening points) | |
| grid_scan | object | Grid scan definition (optional and exclusive with rotation axis) | |
| - n_fast | uint64 | Number of elements along fast axis | |
| - n_slow | uint64 | Number of elements along slow axis | |
| - step_x_axis | float | Step along X axis, can be negative \[m\] | |
| - step_y_axis | float | Step along Y axis, can be negative \[m\] | |
| - snake_scan | bool | Snake scan (rows alternate direction) | |
| - vertical_scan | bool | Vertical scan (enabled: fast direction = Y, disabled: fast direction = X) | |
| jungfrau_conversion_enabled | bool (optional) | Applying JUNGFRAU pixel conversion (to photons or keV) | |
| jungfrau_conversion_factor | float (optional) | Factor used for JUNGFRAU conversion \[eV\] | |
| geometry_transformation_enabled | bool (optional) | Transformation from detector module geometry (512x1024) to full detector geometry | |
| pixel_mask | Map(string -> Image) | Pixel mask - multiple in case of storage cells | X |
| channels | Array(string) | List of image channels | X |
| max_spot_count | uint64 | Maximum number of spots identified in spot finding | |
| storage_cell_number | uint64 (optional) | Number of storage cells used by JUNGFRAU | |
| storage_cell_delay | Rational | Delay of storage cells in JUNGFRAU | |
| threshold_energy | float | Threshold energy for EIGER detector \[eV\] | |
| image_dtype | string | Pixel bit type (e.g. uint16) | X |
| unit_cell | object (optional) | Unit cell of the system: a, b, c \[angstrom\] and alpha, beta, gamma \[degree\] | |
| az_int_q_bin_count | uint64 | Number of azimuthal integration bins in the radial direction | |
| az_int_phi_bin_count | uint64 | Number of azimuthal integration bins in the phi angle direction | |
| az_int_bin_to_q | Array(float) | Q value for each azimuthal integration bin \[angstrom^-1\] | |
| az_int_bin_to_two_theta | Array(float) | Two theta angle value for each azimuthal integration bin \[deg\] | |
| az_int_bin_to_phi | Array(float) | Phi value for each azimuthal integration bin \[deg\] | |
| summation | uint64 | Factor of frame summation | |
| user_data | string | JSON serialized to string that can contain the following fields (all fields are optional): | X |
| - file_prefix | string | File prefix | |
| - images_per_file | uint64 | Number of images written per file | |
| - images_per_trigger | uint64 | Number of images collected per trigger | |
| - source_name | string | Facility name | |
| - source_type | string | Type of X-ray source (use NXsource/type values, for example "Synchrotron X-ray Source" or "Free-Electron Laser") | |
| - instrument_name | string | Instrument name | |
| - sample_name | string | Name of the sample | |
| - user | any valid JSON | Value of header_appendix provided at collection start to Jungfraujoch | |
| - attenuator_transmission | float | Attenuator transmission \[\] | |
| - total_flux | float | Total flux \[ph/s\] | |
| - space_group_number | uint64 | Space group number | |
| - summation_mode | string | Summation mode (internal\|fpga\|cpu) | |
| - overwrite | bool | Overwrite existing HDF5 files | |
| - file_format | int | File writer format: 0 = no master file, 1 = soft links, 2 = virtual dataset, 3 = CBF, 4 = TIFF | |
| - roi | Array(object) | ROI configurations; each element is one of: | |
| | | type "box": xmin, xmax, ymin, ymax (numbers) | |
| | | type "circle": r, x, y (numbers) | |
| | | type "azim": qmin, qmax (numbers) | |
| - gain_file_names | Array(string) | Names of JUNGFRAU gain files used for the current detector | |
| - write_master_file | bool | With multiple sockets, it selects which socket will provide master file | |
| - data_reduction_factor_serialmx | uint64 | Data reduction factor for serial MX | |
| - experiment_group | string | ID of instrument user, e.g., p-group (SLS/SwissFEL) or proposal number | |
| - jfjoch_release | string | Jungfraujoch release number | |
| - socket_number | uint64 | Number of ZeroMQ socket (on `jfjoch_broker` side) used for transmission | |
| - bit_depth_readout | uint64 | Bit depth of the detector readout | |
| - writer_notification_zmq_addr | string | ZeroMQ address to inform `jfjoch_broker` about writers that finished operation | |
| - xfel_pulse_id | uint64 | Pulse IDs are recorded for images | |
| - ring_current_mA | float | Ring current at the start of the measurement | |
| - sample_temperature_K | float | Sample temperature \[K\] | |
| - detect_ice_rings | bool | Ice ring detection feature is enabled | |
| - indexing_algorithm | string | Indexing algorithm used on-the-fly; allowed values: ffbidx, fft, fftw, none | |
| - geom_refinement_algorithm | string | Post-indexing detector geometry refinement algorithm; allowed values: none, beam_center, beam_center_tetragonal | |
| - poni_rot1 | float | Tilt of the detector rot1 according to PyFAI PONI convention \[rad\] | |
| - poni_rot2 | float | Tilt of the detector rot2 according to PyFAI PONI convention \[rad\] | |
| - poni_rot3 | float | Tilt of the detector rot3 according to PyFAI PONI convention \[rad\] | |
| Field name | Type | Description | Present in DECTRIS format |
|----------------------------------|----------------------|------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------:|
| type | String | value "start" | X |
| magic_number | uint64 | Number used to describe version of the Jungfraujoch data interface - to allow to detect inconsistency between sender and receiver | |
| detector_distance | float | Detector distance \[m\] | |
| detector_translation | Array(float) | Detector translation vector \[m\] | X |
| beam_center_x | float | Beam center in X direction \[pixels\] | X |
| beam_center_y | float | Beam center in Y direction \[pixels\] | X |
| countrate_correction_enabled | bool | Countrate correction enabled | X |
| flatfield_enabled | bool | Flatfield enabled | X |
| number_of_images | uint64 | Number of images in the series | X |
| image_size_x | uint64 | Image width \[pixels\] | X |
| image_size_y | uint64 | Image height \[pixels\] | X |
| incident_energy | float | X-ray energy \[eV\] | X |
| incident_wavelength | float | X-ray wavelength \[Angstrom\] | X |
| frame_time | float | Frame time, if multiple frames per trigger \[s\] | X |
| count_time | float | Exposure time \[s\] | X |
| saturation_value | int64 | Maximum valid sample value | X |
| error_value | int64 (optional) | Value used in images to describe pixels that are in error state or missing | |
| pixel_size_x | float | Pixel width \[m\] | X |
| pixel_size_y | float | Pixel height \[m\] | X |
| sensor_thickness | float | Sensor thickness \[m\] | X |
| sensor_material | string | Sensor material | X |
| arm_date | date | Approximate date of arming | X |
| pixel_mask_enabled | bool | Pixel mask applied on images | X |
| detector_description | string | Name of the detector | X |
| detector_serial_number | string | Detector serial number | X |
| series_unique_id | string | Unique text ID of the series (run_name parameter) | X |
| series_id | uint64 | Unique numeric ID of the series (run_number parameter) | X |
| fluorescence | object (optional) | X-ray fluorescence spectrum collected at start | |
| - energy | Array(float) | Energy of measuring point \[eV\] | |
| - data | Array(float) | Fluorescence scan result `data` \[arbitrary units\]; must be strictly the same length as energy | |
| goniometer | Map | Definition of rotation axis (optional) | X |
| - `AXIS` | string | Rotation axis name (e.g. omega) - only one axis is supported in Jungfraujoch | X |
| - - increment | float | Rotation axis increment (per image) in degree \[deg\] | X |
| - - start | float | Rotation axis start angle \[deg\] | X |
| - - axis | Array(float) | Vector for the rotation axis | |
| - - helical_step | Array(float) | Translation for helical scan for 1 image \[m\] | |
| - - screening_wedge | Array(float) | Wedge for screening \[deg\] (increment would correspond to difference between screening points) | |
| grid_scan | object | Grid scan definition (optional and exclusive with rotation axis) | |
| - n_fast | uint64 | Number of elements along fast axis | |
| - n_slow | uint64 | Number of elements along slow axis | |
| - step_x_axis | float | Step along X axis, can be negative \[m\] | |
| - step_y_axis | float | Step along Y axis, can be negative \[m\] | |
| - snake_scan | bool | Snake scan (rows alternate direction) | |
| - vertical_scan | bool | Vertical scan (enabled: fast direction = Y, disabled: fast direction = X) | |
| jungfrau_conversion_enabled | bool (optional) | Applying JUNGFRAU pixel conversion (to photons or keV) | |
| jungfrau_conversion_factor | float (optional) | Factor used for JUNGFRAU conversion \[eV\] | |
| geometry_transformation_enabled | bool (optional) | Transformation from detector module geometry (512x1024) to full detector geometry | |
| pixel_mask | Map(string -> Image) | Pixel mask - multiple in case of storage cells | X |
| channels | Array(string) | List of image channels | X |
| max_spot_count | uint64 | Maximum number of spots identified in spot finding | |
| storage_cell_number | uint64 (optional) | Number of storage cells used by JUNGFRAU | |
| storage_cell_delay | Rational | Delay of storage cells in JUNGFRAU | |
| threshold_energy | float | Threshold energy for EIGER detector \[eV\] | |
| image_dtype | string | Pixel bit type (e.g. uint16) | X |
| unit_cell | object (optional) | Unit cell of the system: a, b, c \[angstrom\] and alpha, beta, gamma \[degree\] | |
| az_int_q_bin_count | uint64 | Number of azimuthal integration bins in the radial direction | |
| az_int_phi_bin_count | uint64 | Number of azimuthal integration bins in the phi angle direction | |
| az_int_bin_to_q | Array(float) | Q value for each azimuthal integration bin \[angstrom^-1\] | |
| az_int_bin_to_two_theta | Array(float) | Two theta angle value for each azimuthal integration bin \[deg\] | |
| az_int_bin_to_phi | Array(float) | Phi value for each azimuthal integration bin \[deg\] | |
| summation | uint64 | Factor of frame summation | |
| user_data | string | JSON serialized to string that can contain the following fields (all fields are optional): | X |
| - file_prefix | string | File prefix | |
| - images_per_file | uint64 | Number of images written per file | |
| - images_per_trigger | uint64 | Number of images collected per trigger | |
| - source_name | string | Facility name | |
| - source_type | string | Type of X-ray source (use NXsource/type values, for example "Synchrotron X-ray Source" or "Free-Electron Laser") | |
| - instrument_name | string | Instrument name | |
| - sample_name | string | Name of the sample | |
| - user | any valid JSON | Value of header_appendix provided at collection start to Jungfraujoch | |
| - attenuator_transmission | float | Attenuator transmission \[\] | |
| - total_flux | float | Total flux \[ph/s\] | |
| - space_group_number | uint64 | Space group number | |
| - summation_mode | string | Summation mode (internal\|fpga\|cpu) | |
| - overwrite | bool | Overwrite existing HDF5 files | |
| - file_format | int | File writer format: 0 = only data files, 1 = NXmx legacy soft links, 2 = NXmx VDS, 3 = NXmx integrated, 4 = CBF, 5 = TIFF, 6 = no file written | |
| - roi | Array(object) | ROI configurations; each element is one of: | |
| | | type "box": xmin, xmax, ymin, ymax (numbers) | |
| | | type "circle": r, x, y (numbers) | |
| | | type "azim": qmin, qmax (numbers) | |
| - gain_file_names | Array(string) | Names of JUNGFRAU gain files used for the current detector | |
| - write_master_file | bool | With multiple sockets, it selects which socket will provide master file | |
| - write_images | bool | Write images in the HDF5 file (if false, will only write metadata) | |
| - data_reduction_factor_serialmx | uint64 | Data reduction factor for serial MX | |
| - experiment_group | string | ID of instrument user, e.g., p-group (SLS/SwissFEL) or proposal number | |
| - jfjoch_release | string | Jungfraujoch release number | |
| - socket_number | uint64 | Number of ZeroMQ socket (on `jfjoch_broker` side) used for transmission | |
| - bit_depth_readout | uint64 | Bit depth of the detector readout | |
| - writer_notification_zmq_addr | string | ZeroMQ address to inform `jfjoch_broker` about writers that finished operation | |
| - xfel_pulse_id | uint64 | Pulse IDs are recorded for images | |
| - ring_current_mA | float | Ring current at the start of the measurement | |
| - sample_temperature_K | float | Sample temperature \[K\] | |
| - detect_ice_rings | bool | Ice ring detection feature is enabled | |
| - indexing_algorithm | string | Indexing algorithm used on-the-fly; allowed values: ffbidx, fft, fftw, none | |
| - geom_refinement_algorithm | string | Post-indexing detector geometry refinement algorithm; allowed values: none, beam_center, beam_center_tetragonal | |
| - poni_rot1 | float | Tilt of the detector rot1 according to PyFAI PONI convention \[rad\] | |
| - poni_rot2 | float | Tilt of the detector rot2 according to PyFAI PONI convention \[rad\] | |
| - poni_rot3 | float | Tilt of the detector rot3 according to PyFAI PONI convention \[rad\] | |
See [DECTRIS documentation](https://github.com/dectris/documentation/tree/main/stream_v2) for definition of Image as MultiDimArray with optional compression.
@@ -252,28 +253,49 @@ See [DECTRIS documentation](https://github.com/dectris/documentation/tree/main/s
## End message
| Field name | Type | Description | Present in DECTRIS format |
|----------------------------|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------|:-------------------------:|
| type | String | value "end" | X |
| magic_number | uint64 | Number used to describe version of the Jungfraujoch data interface - to allow to detect inconsistency between sender and receiver | |
| series_unique_id | string | Unique text ID of the series (run_name parameter) | X |
| series_id | uint64 | Unique numeric ID of the series (run_number parameter) | X |
| end_date | date | Approximate end date | |
| max_image_number | uint64 | Number of image with the highest number (this is counted from 1 - to distinguish zero images and one image) | |
| images_collected | uint64 | Number of image collected | |
| images_sent_to_write | uint64 | Number of image sent to writer; if writer queues were full, it is possible this is less than images collected | |
| data_collection_efficiency | float | Network packets collected / Network packets expected \[\] | |
| az_int_result | Map(text->Array(float)) | Azimuthal integration results, use az_int_bin_to_q from start message for legend | |
| adu_histogram | Map(text->Array(uint64)) | ADU values histogram | |
| 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 | |
| indexing_rate | float | Mean indexing rate for the whole run | |
| rotation_lattice_type | object | Bravais lattice classification of the total rotation solution over the run (if available); same schema as `lattice_type` | |
| - centering | string | One-letter centering code: P, A, B, C, I, F, or R | |
| - niggli_class | int64 | Integer identifier for the Niggli-reduced Bravais class | |
| - system | string | Crystal system: triclinic, monoclinic, orthorhombic, tetragonal, trigonal, hexagonal, cubic | |
| rotation_lattice | Array(9 * float) | Real-space lattice basis (flattened 3x3 in row-major), corresponding to the rotation indexing result | |
| Field name | Type | Description | Present in DECTRIS format |
|------------------------------------|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------|:-------------------------:|
| type | String | value "end" | X |
| magic_number | uint64 | Number used to describe version of the Jungfraujoch data interface - to allow to detect inconsistency between sender and receiver | |
| series_unique_id | string | Unique text ID of the series (run_name parameter) | X |
| series_id | uint64 | Unique numeric ID of the series (run_number parameter) | X |
| end_date | date | Approximate end date | |
| max_image_number | uint64 | Number of image with the highest number; counted from 1 to distinguish zero images and one image | |
| images_collected | uint64 | Number of images collected | |
| images_sent_to_write | uint64 | Number of images sent to writer; if writer queues were full, it is possible this is less than images collected | |
| data_collection_efficiency | float | Overall network packets collected / network packets expected | |
| az_int_result | Map(text->Array(float)) | Azimuthal integration results, use az_int_bin_to_q from start message for legend | |
| adu_histogram | Map(text->Array(uint64)) | ADU values histogram | |
| 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 | |
| indexing_rate | float | Mean indexing rate for the whole run | |
| rotation_lattice_type | object | Bravais lattice classification of the total rotation solution over the run, if available; same schema as `lattice_type` | |
| - centering | string | One-letter centering code: P, A, B, C, I, F, or R | |
| - niggli_class | int64 | Integer identifier for the Niggli-reduced Bravais class | |
| - system | string | Crystal system: triclinic, monoclinic, orthorhombic, tetragonal, trigonal, hexagonal, cubic | |
| rotation_lattice | Array(9 * float) | Real-space lattice basis, flattened 3x3 in row-major order | |
| data_collection_efficiency_image | Array(float) | Per-image data collection efficiency. Missing values are encoded as 0 or 1 depending on producer context | |
| spot_count | Array(int32) | Per-image spot count | |
| spot_count_ice_ring | Array(int32) | Per-image number of spots within identified ice-ring resolution ranges | |
| spot_count_low_res | Array(int32) | Per-image number of low-resolution spots | |
| 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 | |
| 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\] | |
| resolution_estimate | Array(float) | Per-image diffraction resolution estimate \[Angstrom\] | |
| min_viable_pixel_value | Array(int64) | Per-image minimum valid pixel value, excluding error/saturated pixels | |
| max_viable_pixel_value | Array(int64) | Per-image maximum valid pixel value, excluding error/saturated pixels | |
| saturated_pixel_count | Array(int32) | Per-image saturated pixel count | |
| error_pixel_count | Array(int32) | Per-image error pixel count | |
| image_scale_factor | Array(float) | Per-image scale factor, if scaling/merging was performed | |
| integrated_reflections | Array(int32) | Per-image count of integrated reflections | |
| niggli_class | Array(uint8) | Per-image Niggli class identifier for indexed images; 0 if unavailable | |
| pixel_sum | Array(int64) | Per-image sum of all valid pixels, excluding error/saturated pixels | |
End-message vector fields are optional. When present, they provide master-file summary data so readers can inspect scan-level and per-image analysis results without opening every linked data file. Missing optional per-image values are encoded by the producer as zero unless otherwise noted.
## Calibration message
@@ -300,4 +322,16 @@ Therefore `user_data` is serialized by Jungfraujoch as CBOR object. There is mem
- Compression:
- Uncompressed: raw CBOR byte string
- Bitshuffle+LZ4: tag with \["bslz4", elem_size, bytes\]
- Bitshuffle+Zstandard: tag with \["bszstd", elem_size, bytes\]
- Bitshuffle+Zstandard: tag with \["bszstd", elem_size, bytes\]
### Notes on typed arrays
Jungfraujoch uses RFC 8746-style typed byte-string tags for compact numeric arrays.
Common tags used in this protocol include:
- float32 little-endian arrays for `Array(float)`
- uint8 arrays for compact boolean/integer flags such as `image_indexed`
- int32 little-endian arrays for per-image counts
- int64 little-endian arrays for large per-image integer values
- uint64 little-endian arrays for histograms
+75 -1
View File
@@ -228,6 +228,42 @@ namespace {
return {ptr, len};
}
void GetCBORUInt8Array(CborValue &value, std::vector<uint8_t> &v) {
if (GetCBORTag(value) != TagUnsignedInt8Bit)
throw JFJochException(JFJochExceptionCategory::CBORError, "Incorrect array type tag");
auto [ptr, len] = GetCBORByteString(value);
v.resize(len);
memcpy(v.data(), ptr, len);
}
void GetCBORInt32Array(CborValue &value, std::vector<int32_t> &v) {
if (GetCBORTag(value) != TagSignedInt32BitLE)
throw JFJochException(JFJochExceptionCategory::CBORError, "Incorrect array type tag");
auto [ptr, len] = GetCBORByteString(value);
if (len % sizeof(int32_t))
throw JFJochException(JFJochExceptionCategory::CBORError, "Size mismatch");
v.resize(len / sizeof(int32_t));
memcpy(v.data(), ptr, len);
}
void GetCBORInt64Array(CborValue &value, std::vector<int64_t> &v) {
if (GetCBORTag(value) != TagSignedInt64BitLE)
throw JFJochException(JFJochExceptionCategory::CBORError, "Incorrect array type tag");
auto [ptr, len] = GetCBORByteString(value);
if (len % sizeof(int64_t))
throw JFJochException(JFJochExceptionCategory::CBORError, "Size mismatch");
v.resize(len / sizeof(int64_t));
memcpy(v.data(), ptr, len);
}
void GetCBORFloatArray(CborValue &value, std::vector<float> &v) {
if (GetCBORTag(value) != TagFloatLE)
throw JFJochException(JFJochExceptionCategory::CBORError, "Incorrect array type tag");
@@ -1012,6 +1048,8 @@ namespace {
ProcessROIConfig(message, j["roi"]);
if (j.contains("gain_file_names"))
message.gain_file_names = j["gain_file_names"];
if (j.contains("write_images"))
message.write_images = j["write_images"];
if (j.contains("write_master_file"))
message.write_master_file = j["write_master_file"];
if (j.contains("data_reduction_factor_serialmx"))
@@ -1282,8 +1320,44 @@ namespace {
message.bkg_estimate = GetCBORFloat(value);
else if (key == "indexing_rate")
message.indexing_rate = GetCBORFloat(value);
else if (key == "data_collection_efficiency_image")
GetCBORFloatArray(value, message.data_collection_efficiency);
else if (key == "spot_count")
GetCBORInt32Array(value, message.spot_count);
else if (key == "spot_count_ice_ring")
GetCBORInt32Array(value, message.spot_count_ice_ring);
else if (key == "spot_count_low_res")
GetCBORInt32Array(value, message.spot_count_low_res);
else if (key == "spot_count_indexed")
GetCBORInt32Array(value, message.spot_count_indexed);
else if (key == "image_indexed")
GetCBORUInt8Array(value, message.image_indexed);
else if (key == "v_bkg_estimate")
GetCBORFloatArray(value, message.v_bkg_estimate);
else if (key == "profile_radius")
GetCBORFloatArray(value, message.profile_radius);
else if (key == "mosaicity")
GetCBORFloatArray(value, message.mosaicity);
else if (key == "bFactor")
GetCBORFloatArray(value, message.bFactor);
else if (key == "resolution_estimate")
GetCBORFloatArray(value, message.resolution_estimate);
else if (key == "min_viable_pixel_value")
GetCBORInt64Array(value, message.min_viable_pixel_value);
else if (key == "max_viable_pixel_value")
GetCBORInt64Array(value, message.max_viable_pixel_value);
else if (key == "saturated_pixel_count")
GetCBORInt32Array(value, message.saturated_pixel_count);
else if (key == "error_pixel_count")
GetCBORInt32Array(value, message.error_pixel_count);
else if (key == "image_scale_factor")
GetCBORFloatArray(value, message.scale_factor);
GetCBORFloatArray(value, message.image_scale_factor);
else if (key == "integrated_reflections")
GetCBORInt32Array(value, message.integrated_reflections);
else if (key == "niggli_class")
GetCBORUInt8Array(value, message.niggli_class);
else if (key == "pixel_sum")
GetCBORInt64Array(value, message.pixel_sum);
else if (key == "rotation_lattice") {
std::vector<float> tmp;
GetCBORFloatArray(value, tmp);
+40 -1
View File
@@ -153,6 +153,23 @@ inline void CBOR_ENC(CborEncoder &encoder, const char* key, const std::vector<fl
cborErr(cbor_encode_byte_string(&encoder, (uint8_t *) v.data(), v.size() * sizeof(float)));
}
inline void CBOR_ENC(CborEncoder &encoder, const char* key, const std::vector<uint8_t>& v) {
cborErr(cbor_encode_text_stringz(&encoder, key));
cborErr(cbor_encode_tag(&encoder, TagUnsignedInt8Bit));
cborErr(cbor_encode_byte_string(&encoder, v.data(), v.size() * sizeof(uint8_t)));
}
inline void CBOR_ENC(CborEncoder &encoder, const char* key, const std::vector<int32_t>& v) {
cborErr(cbor_encode_text_stringz(&encoder, key));
cborErr(cbor_encode_tag(&encoder, TagSignedInt32BitLE));
cborErr(cbor_encode_byte_string(&encoder, reinterpret_cast<const uint8_t *>(v.data()), v.size() * sizeof(int32_t)));
}
inline void CBOR_ENC(CborEncoder &encoder, const char* key, const std::vector<int64_t>& v) {
cborErr(cbor_encode_text_stringz(&encoder, key));
cborErr(cbor_encode_tag(&encoder, TagSignedInt64BitLE));
cborErr(cbor_encode_byte_string(&encoder, reinterpret_cast<const uint8_t *>(v.data()), v.size() * sizeof(int64_t)));
}
inline void CBOR_ENC(CborEncoder &encoder, const char* key, const std::vector<uint64_t>& v) {
cborErr(cbor_encode_text_stringz(&encoder, key));
@@ -493,6 +510,8 @@ inline void CBOR_ENC_START_USER_DATA(CborEncoder& encoder, const char* key,
j["gain_file_names"] = message.gain_file_names;
if (message.write_master_file)
j["write_master_file"] = message.write_master_file.value();
if (message.write_images)
j["write_images"] = message.write_images.value();
if (message.data_reduction_factor_serialmx)
j["data_reduction_factor_serialmx"] = message.data_reduction_factor_serialmx.value();
j["experiment_group"] = message.experiment_group;
@@ -677,7 +696,27 @@ void CBORStream2Serializer::SerializeSequenceEnd(const EndMessage& message) {
CBOR_ENC(mapEncoder, "rotation_lattice_type", message.rotation_lattice_type);
if (message.rotation_lattice.has_value())
CBOR_ENC(mapEncoder, "rotation_lattice", message.rotation_lattice->GetVector());
CBOR_ENC(mapEncoder, "image_scale_factor", message.scale_factor);
CBOR_ENC(mapEncoder, "data_collection_efficiency_image", message.data_collection_efficiency);
CBOR_ENC(mapEncoder, "spot_count", message.spot_count);
CBOR_ENC(mapEncoder, "spot_count_ice_ring", message.spot_count_ice_ring);
CBOR_ENC(mapEncoder, "spot_count_low_res", message.spot_count_low_res);
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, "profile_radius", message.profile_radius);
CBOR_ENC(mapEncoder, "mosaicity", message.mosaicity);
CBOR_ENC(mapEncoder, "bFactor", message.bFactor);
CBOR_ENC(mapEncoder, "resolution_estimate", message.resolution_estimate);
CBOR_ENC(mapEncoder, "min_viable_pixel_value", message.min_viable_pixel_value);
CBOR_ENC(mapEncoder, "max_viable_pixel_value", message.max_viable_pixel_value);
CBOR_ENC(mapEncoder, "saturated_pixel_count", message.saturated_pixel_count);
CBOR_ENC(mapEncoder, "error_pixel_count", message.error_pixel_count);
CBOR_ENC(mapEncoder, "image_scale_factor", message.image_scale_factor);
CBOR_ENC(mapEncoder, "integrated_reflections", message.integrated_reflections);
CBOR_ENC(mapEncoder, "niggli_class", message.niggli_class);
CBOR_ENC(mapEncoder, "pixel_sum", message.pixel_sum);
cborErr(cbor_encoder_close_container(&encoder, &mapEncoder));
curr_size = cbor_encoder_get_buffer_size(&encoder, buffer);
+1
View File
@@ -24,5 +24,6 @@ constexpr const CborTag TagUnsignedInt32BitLE = 0b01000110;
constexpr const CborTag TagSignedInt32BitLE = 0b01001110;
constexpr const CborTag TagUnsignedInt64BitLE = 0b01000111;
constexpr const CborTag TagSignedInt64BitLE = 0b01001111;
#endif //JUNGFRAUJOCH_CBORUTIL_H
+2 -1
View File
@@ -9,6 +9,7 @@
#include "indexing/AnalyzeIndexing.h"
#include "indexing/FFTIndexer.h"
#include "lattice_search/LatticeSearch.h"
#include "scale_merge/ScaleAll.h"
IndexAndRefine::IndexAndRefine(const DiffractionExperiment &x, IndexerThreadPool *indexer)
: index_ice_rings(x.GetIndexingSettings().GetIndexIceRings()),
@@ -275,7 +276,7 @@ std::optional<RotationIndexerResult> IndexAndRefine::Finalize() {
return {};
}
std::optional<ScaleMergeResult> IndexAndRefine::ScaleRotationData(const ScaleMergeOptions &opts) const {
std::optional<ScaleMergeResult> IndexAndRefine::ScaleRotationData(const ScaleMergeOptions &opts) {
size_t nrefl = 0;
for (const auto &i: reflections)
nrefl += i.size();
+2 -2
View File
@@ -12,7 +12,7 @@
#include "bragg_prediction/BraggPrediction.h"
#include "indexing/IndexerThreadPool.h"
#include "lattice_search/LatticeSearch.h"
#include "scale_merge/ScaleAndMerge.h"
#include "scale_merge/Merge.h"
#include "RotationIndexer.h"
#include "RotationParameters.h"
@@ -63,7 +63,7 @@ public:
/// Run scale-and-merge on accumulated reflections to refine per-image
/// mosaicity (and optionally B-factors / scale factors).
/// Returns std::nullopt if there are too few reflections to be meaningful.
std::optional<ScaleMergeResult> ScaleRotationData(const ScaleMergeOptions &opts = {}) const;
std::optional<ScaleMergeResult> ScaleRotationData(const ScaleMergeOptions &opts = {});
std::optional<RotationIndexerResult> Finalize();
};
@@ -167,6 +167,7 @@ std::vector<Reflection> IntegrateInternal(const DiffractionExperiment &experimen
if (r.observed) {
if (experiment.GetPolarizationFactor())
r.rlp /= geom.CalcAzIntPolarizationCorr(r.predicted_x, r.predicted_y, experiment.GetPolarizationFactor().value());
r.scaling_correction = r.rlp / r.partiality;
r.image_number = static_cast<float>(image_number);
ret.emplace_back(r);
}
@@ -131,7 +131,8 @@ int BraggPrediction::Calc(const DiffractionExperiment &experiment, const Crystal
.dist_ewald = dist_ewald_sphere,
.rlp = 1.0,
.partiality = 1.0,
.zeta = 1.0
.zeta = 1.0,
.scaling_correction = 1.0
};
++i;
}
@@ -115,6 +115,7 @@ namespace {
out.rlp = 1.0f;
out.partiality = 1.0f;
out.zeta = 1.0f;
out.scaling_correction = 1.0f;
return true;
}
@@ -141,7 +141,8 @@ int BraggPredictionRot::Calc(const DiffractionExperiment &experiment, const Crys
.dist_ewald = dist_ewald_sphere,
.rlp = lorentz_reciprocal,
.partiality = partiality,
.zeta = zeta_abs
.zeta = zeta_abs,
.scaling_correction = lorentz_reciprocal / partiality,
};
i++;
}
@@ -158,6 +158,7 @@ namespace {
out[count].rlp = lorentz;
out[count].partiality = partiality;
out[count].zeta = zeta_abs;
out[count].scaling_correction = lorentz / partiality;
count++;
}
return count;
+8 -2
View File
@@ -1,4 +1,10 @@
ADD_LIBRARY(JFJochScaleMerge ScaleAndMerge.cpp ScaleAndMerge.h FrenchWilson.cpp FrenchWilson.h
ADD_LIBRARY(JFJochScaleMerge ScaleAll.cpp ScaleAll.h FrenchWilson.cpp FrenchWilson.h
SearchSpaceGroup.cpp
SearchSpaceGroup.h)
SearchSpaceGroup.h
Merge.cpp
Merge.h
ScaleOnTheFly.cpp
ScaleOnTheFly.h
HKLKey.cpp
HKLKey.h)
TARGET_LINK_LIBRARIES(JFJochScaleMerge Ceres::ceres Eigen3::Eigen JFJochCommon)
+1 -1
View File
@@ -112,7 +112,7 @@ FrenchWilson(const std::vector<MergedReflection>& merged,
out[i].k = merged[i].k;
out[i].l = merged[i].l;
out[i].sigmaI = merged[i].sigma;
const double I_pos = std::max(merged[i].I, 0.0);
const double I_pos = std::max(merged[i].I, 0.0f);
out[i].I = I_pos;
out[i].F = std::sqrt(I_pos);
out[i].sigmaF = 0.0;
+1 -1
View File
@@ -6,7 +6,7 @@
#include <vector>
#include <cstdint>
#include "ScaleAndMerge.h"
#include "Merge.h"
/// Result of the French-Wilson procedure for a single reflection
struct FrenchWilsonReflection {
+56
View File
@@ -0,0 +1,56 @@
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <cmath>
#include "HKLKey.h"
#include "gemmi/symmetry.hpp"
HKLKey CanonicalHKL(int32_t h, int32_t k, int32_t l, bool merge_friedel, const std::optional<gemmi::SpaceGroup> &in_sg) {
HKLKey key{h, k, l, true};
if (!in_sg.has_value()) {
if (!merge_friedel) {
const HKLKey neg{-h, -k, -l, true};
if (std::tie(key.h, key.k, key.l) < std::tie(neg.h, neg.k, neg.l)) {
key.h = -key.h;
key.k = -key.k;
key.l = -key.l;
key.plus = false;
}
}
} else {
gemmi::SpaceGroup sg = in_sg.value();
const auto ops = sg.operations();
const gemmi::ReciprocalAsu asu(&sg);
const gemmi::Op::Miller in{h, k, l};
const auto [hkl, sign_plus] = asu.to_asu_sign(in, ops);
key.h = hkl[0];
key.k = hkl[1];
key.l = hkl[2];
key.plus = merge_friedel ? true : sign_plus;
}
return key;
}
HKLKey CanonicalHKL(const Reflection &r, bool merge_friedel, const std::optional<gemmi::SpaceGroup> &sg) {
return CanonicalHKL(r.h, r.k, r.l, merge_friedel, sg);
}
HKLKey CanonicalHKL(const MergedReflection &r, bool merge_friedel, const std::optional<gemmi::SpaceGroup> &sg) {
return CanonicalHKL(r.h, r.k, r.l, merge_friedel, sg);
}
bool AcceptReflection(const Reflection &r, double d_min_limit) {
if (!std::isfinite(r.I))
return false;
if (!std::isfinite(r.d) || r.d <= 0.0f)
return false;
if (d_min_limit > 0.0 && r.d < d_min_limit)
return false;
if (!std::isfinite(r.rlp) || r.rlp == 0.0f)
return false;
return true;
}
+25
View File
@@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <tuple>
#include "../../common/Reflection.h"
#include "gemmi/symmetry.hpp"
struct HKLKey {
int h = 0;
int k = 0;
int l = 0;
bool plus = true;
bool operator<(const HKLKey &o) const {
return std::tie(h, k, l, plus) < std::tie(o.h, o.k, o.l, o.plus);
}
};
HKLKey CanonicalHKL(const Reflection &r, bool merge_friedel, const std::optional<gemmi::SpaceGroup> &sg);
HKLKey CanonicalHKL(const MergedReflection &r, bool merge_friedel, const std::optional<gemmi::SpaceGroup> &sg);
HKLKey CanonicalHKL(int32_t h, int32_t k, int32_t l, bool merge_friedel, const std::optional<gemmi::SpaceGroup> &sg);
bool AcceptReflection(const Reflection &r, double d_min_limit);
+300
View File
@@ -0,0 +1,300 @@
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "Merge.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <map>
#include <stdexcept>
#include <tuple>
#include <unordered_set>
#include "../../common/ResolutionShells.h"
#include "HKLKey.h"
namespace {
struct Obs {
const Reflection *r = nullptr;
int hkl = -1;
double sigma = 1.0;
};
double SafeSigma(double sigma, double min_sigma) {
if (!std::isfinite(sigma) || sigma <= 0.0)
return min_sigma;
return std::max(sigma, min_sigma);
}
double SafeInv(double x, double fallback) {
if (!std::isfinite(x) || x == 0.0)
return fallback;
return 1.0 / x;
}
std::vector<Obs> BuildObservations(const std::vector<std::vector<Reflection>> &observations,
const ScaleMergeOptions &opt,
std::vector<HKLKey> &slot_to_hkl) {
std::map<HKLKey, int> hkl_to_slot;
std::vector<Obs> out;
size_t nrefl = 0;
for (const auto &image: observations)
nrefl += image.size();
out.reserve(nrefl);
for (const auto &image: observations) {
for (const auto &r: image) {
if (r.scaling_correction <= 0.0 || !std::isfinite(r.scaling_correction))
continue;
if (!AcceptReflection(r, opt.d_min_limit_A))
continue;
HKLKey key;
try {
key = CanonicalHKL(r, opt.merge_friedel, opt.space_group);
} catch (...) {
continue;
}
auto it = hkl_to_slot.find(key);
if (it == hkl_to_slot.end()) {
const int slot = static_cast<int>(slot_to_hkl.size());
it = hkl_to_slot.emplace(key, slot).first;
slot_to_hkl.push_back(key);
}
out.push_back({
.r = &r,
.hkl = it->second,
.sigma = SafeSigma(r.sigma, opt.min_sigma)
});
}
}
return out;
}
ScaleMergeResult InitResult(const std::vector<HKLKey> &slot_to_hkl,
const std::vector<Obs> &obs) {
ScaleMergeResult out;
out.merged.resize(slot_to_hkl.size());
for (int i = 0; i < static_cast<int>(slot_to_hkl.size()); ++i) {
out.merged[i].h = slot_to_hkl[i].h;
out.merged[i].k = slot_to_hkl[i].k;
out.merged[i].l = slot_to_hkl[i].l;
out.merged[i].I = 0.0;
out.merged[i].sigma = 0.0;
out.merged[i].d = 0.0;
}
std::vector<std::vector<double>> d_values(slot_to_hkl.size());
for (const auto &o: obs) {
if (std::isfinite(o.r->d) && o.r->d > 0.0f)
d_values[o.hkl].push_back(o.r->d);
}
for (int h = 0; h < static_cast<int>(d_values.size()); ++h) {
auto &v = d_values[h];
if (v.empty())
continue;
std::nth_element(v.begin(), v.begin() + static_cast<long>(v.size() / 2), v.end());
out.merged[h].d = v[v.size() / 2];
}
return out;
}
void Merge(size_t nhkl, ScaleMergeResult &out, const std::vector<Obs> &obs) {
struct Accum {
double sum_wI = 0.0;
double sum_w = 0.0;
double sum_wsigma2 = 0.0;
};
std::vector<Accum> acc(nhkl);
for (const auto &o: obs) {
const double I_corr = static_cast<double>(o.r->I) * o.r->scaling_correction;
const double sigma_corr = o.sigma * o.r->scaling_correction;
if (!std::isfinite(I_corr) || !std::isfinite(sigma_corr) || sigma_corr <= 0.0)
continue;
// Extra factor o.r->scaling_correction down-weights weak images / low partiality observations.
const double w = o.r->scaling_correction / (sigma_corr * sigma_corr);
auto &a = acc[o.hkl];
a.sum_wI += w * I_corr;
a.sum_w += w;
a.sum_wsigma2 += w * w * sigma_corr * sigma_corr;
}
for (int h = 0; h < static_cast<int>(nhkl); ++h) {
const auto &a = acc[h];
if (a.sum_w <= 0.0)
continue;
out.merged[h].I = a.sum_wI / a.sum_w;
out.merged[h].sigma = std::sqrt(a.sum_wsigma2) / a.sum_w;
}
}
void Stats(const ScaleMergeOptions &opt, ScaleMergeResult &out, const std::vector<Obs> &obs) {
constexpr int n_shells = 10;
float d_min = std::numeric_limits<float>::max();
float d_max = 0.0f;
for (const auto &m: out.merged) {
const auto d = static_cast<float>(m.d);
if (!std::isfinite(d) || d <= 0.0f)
continue;
if (opt.d_min_limit_A > 0.0 && d < static_cast<float>(opt.d_min_limit_A))
continue;
d_min = std::min(d_min, d);
d_max = std::max(d_max, d);
}
if (!(d_min < d_max && d_min > 0.0f))
return;
const float d_min_pad = d_min * 0.999f;
const float d_max_pad = d_max * 1.001f;
ResolutionShells shells(d_min_pad, d_max_pad, n_shells);
const auto shell_mean_1_d2 = shells.GetShellMeanOneOverResSq();
const auto shell_min_res = shells.GetShellMinRes();
std::vector<int> hkl_shell(out.merged.size(), -1);
for (int h = 0; h < static_cast<int>(out.merged.size()); ++h) {
auto s = shells.GetShell(out.merged[h].d);
if (s)
hkl_shell[h] = *s;
}
struct PerHKL {
double sum_I = 0.0;
std::vector<double> I;
};
std::vector<PerHKL> per_hkl(out.merged.size());
for (const auto &o: obs) {
if (o.hkl < 0 || o.hkl >= static_cast<int>(per_hkl.size()))
continue;
if (hkl_shell[o.hkl] < 0)
continue;
const double I_corr = static_cast<double>(o.r->I) * o.r->scaling_correction;
if (!std::isfinite(I_corr))
continue;
per_hkl[o.hkl].sum_I += I_corr;
per_hkl[o.hkl].I.push_back(I_corr);
}
struct ShellAccum {
int total_obs = 0;
std::unordered_set<int> unique;
double rmeas_num = 0.0;
double rmeas_den = 0.0;
double sum_i_over_sigma = 0.0;
int n_i_over_sigma = 0;
};
std::vector<ShellAccum> acc(n_shells);
for (int h = 0; h < static_cast<int>(per_hkl.size()); ++h) {
const int s = hkl_shell[h];
if (s < 0 || per_hkl[h].I.empty())
continue;
auto &sa = acc[s];
const auto &ph = per_hkl[h];
const int n = static_cast<int>(ph.I.size());
const double mean_I = ph.sum_I / n;
sa.unique.insert(h);
sa.total_obs += n;
if (n >= 2) {
double sum_abs_dev = 0.0;
for (double I: ph.I)
sum_abs_dev += std::abs(I - mean_I);
sa.rmeas_num += std::sqrt(static_cast<double>(n) / (n - 1.0)) * sum_abs_dev;
}
for (double I: ph.I)
sa.rmeas_den += std::abs(I);
if (out.merged[h].sigma > 0.0) {
sa.sum_i_over_sigma += out.merged[h].I / out.merged[h].sigma;
++sa.n_i_over_sigma;
}
}
out.statistics.shells.resize(n_shells);
for (int s = 0; s < n_shells; ++s) {
const auto &sa = acc[s];
auto &ss = out.statistics.shells[s];
ss.mean_one_over_d2 = shell_mean_1_d2[s];
ss.d_min = shell_min_res[s];
ss.d_max = s == 0 ? d_max_pad : shell_min_res[s - 1];
ss.total_observations = sa.total_obs;
ss.unique_reflections = static_cast<int>(sa.unique.size());
ss.rmeas = sa.rmeas_den > 0.0 ? sa.rmeas_num / sa.rmeas_den : 0.0;
ss.mean_i_over_sigma = sa.n_i_over_sigma > 0
? sa.sum_i_over_sigma / sa.n_i_over_sigma
: 0.0;
ss.completeness = 0.0;
ss.possible_reflections = 0;
}
auto &overall = out.statistics.overall;
overall.d_min = d_min;
overall.d_max = d_max;
std::unordered_set<int> all_unique;
double rmeas_num = 0.0;
double rmeas_den = 0.0;
double sum_i_over_sigma = 0.0;
int n_i_over_sigma = 0;
for (const auto &sa: acc) {
overall.total_observations += sa.total_obs;
all_unique.insert(sa.unique.begin(), sa.unique.end());
rmeas_num += sa.rmeas_num;
rmeas_den += sa.rmeas_den;
sum_i_over_sigma += sa.sum_i_over_sigma;
n_i_over_sigma += sa.n_i_over_sigma;
}
overall.unique_reflections = static_cast<int>(all_unique.size());
overall.rmeas = rmeas_den > 0.0 ? rmeas_num / rmeas_den : 0.0;
overall.mean_i_over_sigma = n_i_over_sigma > 0 ? sum_i_over_sigma / n_i_over_sigma : 0.0;
overall.completeness = 0.0;
overall.possible_reflections = 0;
}
}
ScaleMergeResult MergeReflections(const std::vector<std::vector<Reflection>> &observations,
const ScaleMergeOptions &opt) {
std::vector<HKLKey> slot_to_hkl;
auto obs = BuildObservations(observations, opt, slot_to_hkl);
auto out = InitResult(slot_to_hkl, obs);
Merge(slot_to_hkl.size(), out, obs);
Stats(opt, out, obs);
return out;
}
+70
View File
@@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <cstdint>
#include <optional>
#include <vector>
#include "../../common/Reflection.h"
#include "gemmi/symmetry.hpp"
struct ScaleMergeOptions {
int max_num_iterations = 100;
double max_solver_time_s = 1.0;
double image_number_rounding = 1.0;
double min_sigma = 1e-3;
std::optional<gemmi::SpaceGroup> space_group;
bool merge_friedel = true;
std::optional<double> wedge_deg;
double mosaicity_init_deg = 0.17;
double mosaicity_min_deg = 1e-3;
double mosaicity_max_deg = 2.0;
std::vector<double> mosaicity_init_deg_vec;
bool regularize_scale_to_one = true;
double scale_regularization_sigma = 0.05;
bool smoothen_g = true;
bool smoothen_mos = true;
double d_min_limit_A = 0.0;
int64_t image_cluster = 1;
bool refine_wedge = false;
enum class PartialityModel { Fixed, Rotation, Unity, Still } partiality_model = PartialityModel::Fixed;
};
struct MergeStatisticsShell {
float d_min = 0.0f;
float d_max = 0.0f;
float mean_one_over_d2 = 0;
int total_observations = 0;
int unique_reflections = 0;
double rmeas = 0.0;
double mean_i_over_sigma = 0.0;
double completeness = 0.0;
int possible_reflections = 0;
};
struct MergeStatistics {
std::vector<MergeStatisticsShell> shells;
MergeStatisticsShell overall;
};
struct ScaleMergeResult {
std::vector<MergedReflection> merged;
std::vector<float> image_scale_g;
std::vector<float> mosaicity_deg;
MergeStatistics statistics;
};
ScaleMergeResult MergeReflections(const std::vector<std::vector<Reflection>> &observations,
const ScaleMergeOptions &opt = {});
+461
View File
@@ -0,0 +1,461 @@
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "ScaleAll.h"
#include <ceres/ceres.h>
#include <algorithm>
#include <cmath>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <thread>
#include "HKLKey.h"
namespace {
struct ScaleObs {
const Reflection *r = nullptr;
int image = 0;
int hkl = -1;
double sigma = 1.0;
};
double SafeSigma(double sigma, double min_sigma) {
if (!std::isfinite(sigma) || sigma <= 0.0)
return min_sigma;
return std::max(sigma, min_sigma);
}
double SafeInv(double x, double fallback) {
if (!std::isfinite(x) || x == 0.0)
return fallback;
return 1.0 / x;
}
bool AcceptReflectionForScaling(const Reflection &r, const ScaleMergeOptions &opt) {
if (!AcceptReflection(r, opt.d_min_limit_A))
return false;
switch (opt.partiality_model) {
case ScaleMergeOptions::PartialityModel::Rotation:
return std::isfinite(r.zeta) && r.zeta > 0.0f;
case ScaleMergeOptions::PartialityModel::Still:
return std::isfinite(r.dist_ewald);
case ScaleMergeOptions::PartialityModel::Fixed:
case ScaleMergeOptions::PartialityModel::Unity:
return true;
}
return true;
}
struct IntensityFixedResidual {
IntensityFixedResidual(const Reflection &r, double sigma, double partiality)
: Iobs(r.I),
weight(SafeInv(sigma, 1.0)),
correction(partiality * SafeInv(r.rlp, 1.0)) {
}
template<typename T>
bool operator()(const T *const G, const T *const Itrue, T *residual) const {
residual[0] = (T(correction) * G[0] * Itrue[0] - T(Iobs)) * T(weight);
return true;
}
double Iobs;
double weight;
double correction;
};
struct IntensityRotationResidual {
IntensityRotationResidual(const Reflection &r, double sigma)
: Iobs(r.I),
weight(SafeInv(sigma, 1.0)),
delta_phi_deg(r.delta_phi_deg),
lp(SafeInv(r.rlp, 1.0)),
c1(r.zeta / std::sqrt(2.0)) {
}
template<typename T>
bool operator()(const T *const G,
const T *const mosaicity,
const T *const Itrue,
const T *const wedge,
T *residual) const {
const T half_wedge = wedge[0] / T(2.0);
const T arg_plus = T(delta_phi_deg + half_wedge) * T(c1) / mosaicity[0];
const T arg_minus = T(delta_phi_deg - half_wedge) * T(c1) / mosaicity[0];
const T partiality = (ceres::erf(arg_plus) - ceres::erf(arg_minus)) / T(2.0);
residual[0] = (G[0] * partiality * T(lp) * Itrue[0] - T(Iobs)) * T(weight);
return true;
}
double Iobs;
double weight;
double delta_phi_deg;
double lp;
double c1;
};
struct IntensityStillResidual {
IntensityStillResidual(const Reflection &r, double sigma)
: Iobs(r.I),
weight(SafeInv(sigma, 1.0)),
lp(SafeInv(r.rlp, 1.0)),
dist_ewald_sq(r.dist_ewald * r.dist_ewald) {
}
template<typename T>
bool operator()(const T *const G,
const T *const R_sq,
const T *const Itrue,
T *residual) const {
const T partiality = ceres::exp(-T(dist_ewald_sq) / R_sq[0]);
residual[0] = (G[0] * partiality * T(lp) * Itrue[0] - T(Iobs)) * T(weight);
return true;
}
double Iobs;
double weight;
double lp;
double dist_ewald_sq;
};
struct ScaleRegularizationResidual {
explicit ScaleRegularizationResidual(double sigma)
: inv_sigma(SafeInv(sigma, 1.0)) {
}
template<typename T>
bool operator()(const T *const k, T *residual) const {
residual[0] = (k[0] - T(1.0)) * T(inv_sigma);
return true;
}
double inv_sigma;
};
struct SmoothnessResidual {
explicit SmoothnessResidual(double sigma)
: inv_sigma(SafeInv(sigma, 1.0)) {
}
template<typename T>
bool operator()(const T *const x0,
const T *const x1,
const T *const x2,
T *residual) const {
residual[0] = (ceres::log(x0[0]) + ceres::log(x2[0]) - T(2.0) * ceres::log(x1[0])) * T(inv_sigma);
return true;
}
double inv_sigma;
};
std::vector<ScaleObs> BuildScaleObs(const std::vector<std::vector<Reflection>> &observations,
const ScaleMergeOptions &opt,
std::vector<uint8_t> &image_used,
int &nhkl) {
std::map<HKLKey, int> hkl_to_slot;
std::vector<ScaleObs> obs;
size_t nrefl = 0;
for (const auto &image: observations)
nrefl += image.size();
obs.reserve(nrefl);
for (int image = 0; image < static_cast<int>(observations.size()); ++image) {
const int image_slot = image / static_cast<int>(opt.image_cluster);
for (const auto &r: observations[image]) {
if (!AcceptReflectionForScaling(r, opt))
continue;
HKLKey key;
try {
key = CanonicalHKL(r, opt.merge_friedel, opt.space_group);
} catch (...) {
continue;
}
auto it = hkl_to_slot.find(key);
if (it == hkl_to_slot.end()) {
const int slot = static_cast<int>(hkl_to_slot.size());
it = hkl_to_slot.emplace(key, slot).first;
}
image_used[image_slot] = 1;
obs.push_back({
.r = &r,
.image = image_slot,
.hkl = it->second,
.sigma = SafeSigma(r.sigma, opt.min_sigma)
});
}
}
nhkl = static_cast<int>(hkl_to_slot.size());
return obs;
}
std::vector<double> InitialIntensities(int nhkl,
const ScaleMergeOptions &opt,
const std::vector<ScaleObs> &obs) {
std::vector<std::vector<double>> values(nhkl);
for (const auto &o: obs)
values[o.hkl].push_back(o.r->I);
std::vector<double> Itrue(nhkl, opt.min_sigma);
for (int h = 0; h < nhkl; ++h) {
auto &v = values[h];
if (v.empty())
continue;
std::nth_element(v.begin(), v.begin() + static_cast<long>(v.size() / 2), v.end());
Itrue[h] = v[v.size() / 2];
if (!std::isfinite(Itrue[h]) || Itrue[h] <= opt.min_sigma)
Itrue[h] = opt.min_sigma;
}
return Itrue;
}
void Scale(const ScaleMergeOptions &opt,
const std::vector<ScaleObs> &obs,
const std::vector<uint8_t> &image_used,
int nhkl,
std::vector<double> &G,
std::vector<double> &mosaicity,
std::vector<double> &R_sq) {
ceres::Problem problem;
auto Itrue = InitialIntensities(nhkl, opt, obs);
double wedge = opt.wedge_deg.value_or(0.0);
for (const auto &o: obs) {
switch (opt.partiality_model) {
case ScaleMergeOptions::PartialityModel::Rotation: {
auto *cost = new ceres::AutoDiffCostFunction<IntensityRotationResidual, 1, 1, 1, 1, 1>(
new IntensityRotationResidual(*o.r, o.sigma));
problem.AddResidualBlock(cost, nullptr, &G[o.image], &mosaicity[o.image], &Itrue[o.hkl], &wedge);
break;
}
case ScaleMergeOptions::PartialityModel::Still: {
auto *cost = new ceres::AutoDiffCostFunction<IntensityStillResidual, 1, 1, 1, 1>(
new IntensityStillResidual(*o.r, o.sigma));
problem.AddResidualBlock(cost, nullptr, &G[o.image], &R_sq[o.image], &Itrue[o.hkl]);
break;
}
case ScaleMergeOptions::PartialityModel::Unity: {
auto *cost = new ceres::AutoDiffCostFunction<IntensityFixedResidual, 1, 1, 1>(
new IntensityFixedResidual(*o.r, o.sigma, 1.0));
problem.AddResidualBlock(cost, nullptr, &G[o.image], &Itrue[o.hkl]);
break;
}
case ScaleMergeOptions::PartialityModel::Fixed: {
auto *cost = new ceres::AutoDiffCostFunction<IntensityFixedResidual, 1, 1, 1>(
new IntensityFixedResidual(*o.r, o.sigma, o.r->partiality));
problem.AddResidualBlock(cost, nullptr, &G[o.image], &Itrue[o.hkl]);
break;
}
}
}
for (int i = 0; i < static_cast<int>(G.size()); ++i) {
if (!image_used[i])
continue;
problem.SetParameterLowerBound(&G[i], 0, 1e-12);
if (opt.regularize_scale_to_one) {
auto *cost = new ceres::AutoDiffCostFunction<ScaleRegularizationResidual, 1, 1>(
new ScaleRegularizationResidual(opt.scale_regularization_sigma));
problem.AddResidualBlock(cost, nullptr, &G[i]);
}
}
if (opt.smoothen_g) {
for (int i = 0; i + 2 < static_cast<int>(G.size()); ++i) {
if (!(image_used[i] && image_used[i + 1] && image_used[i + 2]))
continue;
auto *cost = new ceres::AutoDiffCostFunction<SmoothnessResidual, 1, 1, 1, 1>(
new SmoothnessResidual(0.05));
problem.AddResidualBlock(cost, nullptr, &G[i], &G[i + 1], &G[i + 2]);
}
}
if (opt.partiality_model == ScaleMergeOptions::PartialityModel::Rotation) {
for (int i = 0; i < static_cast<int>(mosaicity.size()); ++i) {
if (!image_used[i])
continue;
problem.SetParameterLowerBound(&mosaicity[i], 0, opt.mosaicity_min_deg);
problem.SetParameterUpperBound(&mosaicity[i], 0, opt.mosaicity_max_deg);
}
if (opt.smoothen_mos) {
for (int i = 0; i + 2 < static_cast<int>(mosaicity.size()); ++i) {
if (!(image_used[i] && image_used[i + 1] && image_used[i + 2]))
continue;
auto *cost = new ceres::AutoDiffCostFunction<SmoothnessResidual, 1, 1, 1, 1>(
new SmoothnessResidual(0.05));
problem.AddResidualBlock(cost, nullptr, &mosaicity[i], &mosaicity[i + 1], &mosaicity[i + 2]);
}
}
if (!opt.refine_wedge)
problem.SetParameterBlockConstant(&wedge);
else
problem.SetParameterLowerBound(&wedge, 0, 0.0);
}
if (opt.partiality_model == ScaleMergeOptions::PartialityModel::Still) {
for (int i = 0; i < static_cast<int>(R_sq.size()); ++i) {
if (!image_used[i])
continue;
problem.SetParameterLowerBound(&R_sq[i], 0, 1e-9);
problem.SetParameterUpperBound(&R_sq[i], 0, 1.0);
}
}
unsigned int hw = std::thread::hardware_concurrency();
if (hw == 0)
hw = 1;
ceres::Solver::Options options;
options.linear_solver_type = ceres::SPARSE_NORMAL_CHOLESKY;
options.minimizer_progress_to_stdout = true;
options.max_num_iterations = opt.max_num_iterations;
options.max_solver_time_in_seconds = opt.max_solver_time_s;
options.num_threads = static_cast<int>(hw);
options.function_tolerance = 1e-4;
ceres::Solver::Summary summary;
ceres::Solve(options, &problem, &summary);
std::cout << summary.FullReport() << std::endl;
}
double Partiality(const Reflection &r,
const ScaleMergeOptions &opt,
int image_slot,
const std::vector<double> &mosaicity,
const std::vector<double> &R_sq) {
switch (opt.partiality_model) {
case ScaleMergeOptions::PartialityModel::Fixed:
return r.partiality;
case ScaleMergeOptions::PartialityModel::Unity:
return 1.0;
case ScaleMergeOptions::PartialityModel::Rotation: {
const double half_wedge = opt.wedge_deg.value_or(0.0) / 2.0;
const double c1 = r.zeta / std::sqrt(2.0);
const double arg_plus = (r.delta_phi_deg + half_wedge) * c1 / mosaicity[image_slot];
const double arg_minus = (r.delta_phi_deg - half_wedge) * c1 / mosaicity[image_slot];
return (std::erf(arg_plus) - std::erf(arg_minus)) / 2.0;
}
case ScaleMergeOptions::PartialityModel::Still:
return std::exp(-r.dist_ewald * r.dist_ewald / R_sq[image_slot]);
}
return 1.0;
}
void CalcCorrections(std::vector<std::vector<Reflection>> &observations,
const ScaleMergeOptions &opt,
const std::vector<double> &G,
const std::vector<double> &mosaicity,
const std::vector<double> &R_sq) {
size_t nrefl = 0;
for (const auto &image: observations)
nrefl += image.size();
for (int image = 0; image < static_cast<int>(observations.size()); ++image) {
const int image_slot = image / static_cast<int>(opt.image_cluster);
for (auto &r: observations[image]) {
if (!AcceptReflectionForScaling(r, opt)) {
r.scaling_correction = 0.0;
continue;
}
const double partiality = Partiality(r, opt, image_slot, mosaicity, R_sq);
if (!std::isfinite(partiality) || partiality < 0.01) {
r.partiality = 0.0;
r.scaling_correction = 0.0;
} else {
const double denom = G[image_slot] * partiality;
const double correction = denom > 0.0 ? r.rlp / denom : 0.0;
r.partiality = partiality;
r.scaling_correction = std::isfinite(correction) ? correction : 0.0;
}
}
}
}
}
ScaleMergeResult ScaleAndMergeReflectionsCeres(std::vector<std::vector<Reflection>> &observations,
const ScaleMergeOptions &opt) {
if (opt.image_cluster <= 0)
throw std::invalid_argument("image_cluster must be positive");
const size_t n_image_slots = observations.size() / opt.image_cluster +
(observations.size() % opt.image_cluster > 0 ? 1 : 0);
std::vector<uint8_t> image_used(n_image_slots, 0);
int nhkl = 0;
auto scale_obs = BuildScaleObs(observations, opt, image_used, nhkl);
std::vector<double> G(n_image_slots, 1.0);
std::vector<double> mosaicity(n_image_slots, opt.mosaicity_init_deg);
std::vector<double> R_sq(n_image_slots, 0.001 * 0.001);
for (int i = 0; i < static_cast<int>(n_image_slots); ++i) {
if (!image_used[i]) {
G[i] = NAN;
mosaicity[i] = NAN;
R_sq[i] = NAN;
} else if (opt.mosaicity_init_deg_vec.size() > static_cast<size_t>(i) &&
std::isfinite(opt.mosaicity_init_deg_vec[i])) {
mosaicity[i] = opt.mosaicity_init_deg_vec[i];
}
}
Scale(opt, scale_obs, image_used, nhkl, G, mosaicity, R_sq);
CalcCorrections(observations, opt, G, mosaicity, R_sq);
auto out = MergeReflections(observations, opt);
out.image_scale_g.resize(observations.size(), NAN);
out.mosaicity_deg.resize(observations.size(), NAN);
for (int image = 0; image < static_cast<int>(observations.size()); ++image) {
const int image_slot = image / static_cast<int>(opt.image_cluster);
if (image_slot < static_cast<int>(image_used.size()) && image_used[image_slot]) {
out.image_scale_g[image] = G[image_slot];
out.mosaicity_deg[image] = mosaicity[image_slot];
}
}
return out;
}
+9
View File
@@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include "Merge.h"
ScaleMergeResult ScaleAndMergeReflectionsCeres(std::vector<std::vector<Reflection>>& observations,
const ScaleMergeOptions& opt = {});
@@ -1,789 +0,0 @@
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "ScaleAndMerge.h"
#include <ceres/ceres.h>
#include <thread>
#include <iostream>
#include <algorithm>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <tuple>
#include <unordered_map>
#include <utility>
#include <vector>
#include "../../common/ResolutionShells.h"
namespace {
struct HKLKey {
int64_t h = 0;
int64_t k = 0;
int64_t l = 0;
bool is_positive = true; // only relevant if opt.merge_friedel == false
bool operator==(const HKLKey &o) const noexcept {
return h == o.h && k == o.k && l == o.l && is_positive == o.is_positive;
}
};
struct HKLKeyHash {
size_t operator()(const HKLKey &key) const noexcept {
auto mix = [](uint64_t x) {
x ^= x >> 33;
x *= 0xff51afd7ed558ccdULL;
x ^= x >> 33;
x *= 0xc4ceb9fe1a85ec53ULL;
x ^= x >> 33;
return x;
};
const uint64_t a = static_cast<uint64_t>(key.h);
const uint64_t b = static_cast<uint64_t>(key.k);
const uint64_t c = static_cast<uint64_t>(key.l);
const uint64_t d = static_cast<uint64_t>(key.is_positive ? 1 : 0);
return static_cast<size_t>(mix(a) ^ (mix(b) << 1) ^ (mix(c) << 2) ^ (mix(d) << 3));
}
};
inline int RoundImageId(float image_number, double rounding_step) {
if (!(rounding_step > 0.0))
rounding_step = 1.0;
const double x = static_cast<double>(image_number) / rounding_step;
const double r = std::round(x) * rounding_step;
return static_cast<int>(std::llround(r / rounding_step));
}
inline double SafeSigma(double s, double min_sigma) {
if (!std::isfinite(s) || s <= 0.0)
return min_sigma;
return std::max(s, min_sigma);
}
inline double SafeD(double d) {
if (!std::isfinite(d) || d <= 0.0)
return std::numeric_limits<double>::quiet_NaN();
return d;
}
inline int SafeToInt(int64_t x) {
if (x < std::numeric_limits<int>::min() || x > std::numeric_limits<int>::max())
throw std::out_of_range("HKL index out of int range for Gemmi");
return static_cast<int>(x);
}
inline double SafeInv(double x, double fallback) {
if (!std::isfinite(x) || x == 0.0)
return fallback;
return 1.0 / x;
}
inline HKLKey CanonicalizeHKLKey(const Reflection &r, const ScaleMergeOptions &opt) {
HKLKey key{};
key.h = r.h;
key.k = r.k;
key.l = r.l;
key.is_positive = true;
if (!opt.space_group.has_value()) {
if (!opt.merge_friedel) {
const HKLKey neg{-r.h, -r.k, -r.l, true};
const bool pos = std::tie(key.h, key.k, key.l) >= std::tie(neg.h, neg.k, neg.l);
if (!pos) {
key.h = -key.h;
key.k = -key.k;
key.l = -key.l;
key.is_positive = false;
}
}
return key;
}
const gemmi::SpaceGroup &sg = *opt.space_group;
const gemmi::GroupOps gops = sg.operations();
const gemmi::ReciprocalAsu rasu(&sg);
const gemmi::Op::Miller in{{SafeToInt(r.h), SafeToInt(r.k), SafeToInt(r.l)}};
const auto [asu_hkl, sign_plus] = rasu.to_asu_sign(in, gops);
key.h = asu_hkl[0];
key.k = asu_hkl[1];
key.l = asu_hkl[2];
key.is_positive = opt.merge_friedel ? true : sign_plus;
return key;
}
struct IntensityRotResidual {
IntensityRotResidual(const Reflection &r, double sigma_obs, double wedge_deg)
: Iobs_(static_cast<double>(r.I)),
weight_(SafeInv(sigma_obs, 1.0)),
delta_phi_(r.delta_phi_deg),
lp_(SafeInv(r.rlp, 1.0)),
c1_(r.zeta / std::sqrt(2.0)),
partiality_(r.partiality) {
}
template<typename T>
bool operator()(const T *const G,
const T *const mosaicity,
const T *const Itrue,
const T *const wedge,
T *residual) const {
T partiality;
if (mosaicity[0] >= 0.0) {
const T half_wedge = wedge[0] / T(2.0);
const T arg_plus = T(delta_phi_ + half_wedge) * T(c1_) / mosaicity[0];
const T arg_minus = T(delta_phi_ - half_wedge) * T(c1_) / mosaicity[0];
partiality = (ceres::erf(arg_plus) - ceres::erf(arg_minus)) / T(2.0);
} else
partiality = T(1.0);
const T Ipred = G[0] * partiality * T(lp_) * Itrue[0];
residual[0] = (Ipred - T(Iobs_)) * T(weight_);
return true;
}
double Iobs_;
double weight_;
double delta_phi_;
double lp_;
double c1_;
double partiality_;
};
struct IntensityStillResidual {
IntensityStillResidual(const Reflection &r, double sigma_obs)
: Iobs_(static_cast<double>(r.I)),
weight_(SafeInv(sigma_obs, 1.0)),
lp_(SafeInv(r.rlp, 1.0)),
dist_ewald_sq_(r.dist_ewald * r.dist_ewald) {
}
template<typename T>
bool operator()(const T *const G,
const T *const R,
const T *const Itrue,
T *residual) const {
const T partiality = ceres::exp(-T(dist_ewald_sq_)/R[0]);
const T Ipred = G[0] * partiality * T(lp_) * Itrue[0];
residual[0] = (Ipred - T(Iobs_)) * T(weight_);
return true;
}
double Iobs_;
double weight_;
double lp_;
double dist_ewald_sq_;
};
struct IntensityFixedResidual {
IntensityFixedResidual(const Reflection &r, double sigma_obs, double partiality)
: Iobs_(static_cast<double>(r.I)),
weight_(SafeInv(sigma_obs, 1.0)),
corr_(partiality * SafeInv(r.rlp, 1.0))
{}
template<typename T>
bool operator()(const T *const G,
const T *const Itrue,
T *residual) const {
const T Ipred = T(corr_) * G[0] * Itrue[0];
residual[0] = (Ipred - T(Iobs_)) * T(weight_);
return true;
}
double Iobs_;
double weight_;
double corr_;
};
struct ScaleRegularizationResidual {
explicit ScaleRegularizationResidual(double sigma_k)
: inv_sigma_(SafeInv(sigma_k, 1.0)) {
}
template<typename T>
bool operator()(const T *const k, T *residual) const {
residual[0] = (k[0] - T(1.0)) * T(inv_sigma_);
return true;
}
double inv_sigma_;
};
struct SmoothnessRegularizationResidual {
explicit SmoothnessRegularizationResidual(double sigma)
: inv_sigma_(SafeInv(sigma, 1.0)) {
}
template<typename T>
bool operator()(const T *const k0,
const T *const k1,
const T *const k2,
T *residual) const {
residual[0] = (ceres::log(k0[0]) + ceres::log(k2[0]) - T(2.0) * ceres::log(k1[0])) * T(inv_sigma_);
return true;
}
double inv_sigma_;
};
struct ObsRef {
const Reflection *r = nullptr;
int img_id = 0;
int hkl_slot = -1;
double sigma = 0.0;
};
struct CorrectedObs {
int hkl_slot;
double I_corr;
double sigma_corr;
double weight;
};
void scale(const ScaleMergeOptions &opt,
std::vector<double> &g,
std::vector<double> &mosaicity,
std::vector<double> &R_sq,
const std::vector<uint8_t> &image_slot_used,
bool rotation_crystallography,
size_t nhkl,
const std::vector<ObsRef> &obs) {
ceres::Problem problem;
std::vector<double> Itrue(nhkl, 0.0);
// Initialize Itrue from per-HKL median of observed intensities
{
std::vector<std::vector<double> > per_hkl_I(nhkl);
for (const auto &o: obs) {
per_hkl_I[o.hkl_slot].push_back(static_cast<double>(o.r->I));
}
for (int h = 0; h < nhkl; ++h) {
auto &v = per_hkl_I[h];
if (v.empty()) {
Itrue[h] = std::max(opt.min_sigma, 1e-6);
continue;
}
std::nth_element(v.begin(), v.begin() + static_cast<long>(v.size() / 2), v.end());
double med = v[v.size() / 2];
if (!std::isfinite(med) || med <= opt.min_sigma)
med = opt.min_sigma;
Itrue[h] = med;
}
}
double wedge = opt.wedge_deg.value_or(0.0);
std::vector<bool> is_valid_hkl_slot(nhkl, false);
for (const auto &o: obs) {
switch (opt.partiality_model) {
case ScaleMergeOptions::PartialityModel::Rotation: {
auto *cost = new ceres::AutoDiffCostFunction<IntensityRotResidual, 1, 1, 1, 1, 1>(
new IntensityRotResidual(*o.r, o.sigma, opt.wedge_deg.value_or(0.0)));
problem.AddResidualBlock(cost,
nullptr,
&g[o.img_id],
&mosaicity[o.img_id],
&Itrue[o.hkl_slot],
&wedge);
}
break;
case ScaleMergeOptions::PartialityModel::Still: {
auto *cost = new ceres::AutoDiffCostFunction<IntensityStillResidual, 1, 1, 1, 1>(
new IntensityStillResidual(*o.r, o.sigma));
problem.AddResidualBlock(cost,
nullptr,
&g[o.img_id],
&R_sq[o.img_id],
&Itrue[o.hkl_slot]);
}
break;
case ScaleMergeOptions::PartialityModel::Unity: {
auto *cost = new ceres::AutoDiffCostFunction<IntensityFixedResidual, 1, 1, 1>(
new IntensityFixedResidual(*o.r, o.sigma, 1.0));
problem.AddResidualBlock(cost,
nullptr,
&g[o.img_id],
&Itrue[o.hkl_slot]);
}
break;
case ScaleMergeOptions::PartialityModel::Fixed: {
auto *cost = new ceres::AutoDiffCostFunction<IntensityFixedResidual, 1, 1, 1>(
new IntensityFixedResidual(*o.r, o.sigma, o.r->partiality));
problem.AddResidualBlock(cost,
nullptr,
&g[o.img_id],
&Itrue[o.hkl_slot]);
}
break;
}
is_valid_hkl_slot[o.hkl_slot] = true;
}
for (int i = 0; i < g.size(); ++i) {
if (image_slot_used[i]) {
auto *cost = new ceres::AutoDiffCostFunction<ScaleRegularizationResidual, 1, 1>(
new ScaleRegularizationResidual(0.05));
problem.AddResidualBlock(cost, nullptr, &g[i]);
}
}
if (rotation_crystallography) {
if (opt.smoothen_g) {
for (int i = 0; i < g.size() - 2; ++i) {
if (image_slot_used[i] && image_slot_used[i + 1] && image_slot_used[i + 2]) {
auto *cost = new ceres::AutoDiffCostFunction<SmoothnessRegularizationResidual, 1, 1, 1, 1>(
new SmoothnessRegularizationResidual(0.05));
problem.AddResidualBlock(cost, nullptr, &g[i], &g[i + 1], &g[i + 2]);
}
}
}
if (opt.smoothen_mos && opt.partiality_model == ScaleMergeOptions::PartialityModel::Rotation) {
for (int i = 0; i < mosaicity.size() - 2; ++i) {
if (image_slot_used[i] && image_slot_used[i + 1] && image_slot_used[i + 2]) {
auto *cost = new ceres::AutoDiffCostFunction<SmoothnessRegularizationResidual, 1, 1, 1, 1>(
new SmoothnessRegularizationResidual(0.05));
problem.AddResidualBlock(cost, nullptr, &mosaicity[i], &mosaicity[i + 1], &mosaicity[i + 2]);
}
}
}
}
if (opt.partiality_model == ScaleMergeOptions::PartialityModel::Still) {
for (int i = 0; i < R_sq.size(); ++i) {
if (image_slot_used[i]) {
problem.SetParameterLowerBound(&R_sq[i], 0, 1e-9);
problem.SetParameterUpperBound(&R_sq[i], 0, 1.0);
}
}
}
// Scaling factors must be always positive
for (int i = 0; i < g.size(); i++) {
if (image_slot_used[i])
problem.SetParameterLowerBound(&g[i], 0, 1e-12);
}
// Mosaicity refinement + bounds
if (opt.partiality_model == ScaleMergeOptions::PartialityModel::Rotation) {
for (int i = 0; i < mosaicity.size(); ++i) {
if (image_slot_used[i]) {
problem.SetParameterLowerBound(&mosaicity[i], 0, opt.mosaicity_min_deg);
problem.SetParameterUpperBound(&mosaicity[i], 0, opt.mosaicity_max_deg);
}
}
if (!opt.refine_wedge)
problem.SetParameterBlockConstant(&wedge);
else
problem.SetParameterLowerBound(&wedge, 0, 0.0);
}
// use all available threads
unsigned int hw = std::thread::hardware_concurrency();
if (hw == 0)
hw = 1; // fallback
ceres::Solver::Options options;
options.linear_solver_type = ceres::SPARSE_NORMAL_CHOLESKY;
options.minimizer_progress_to_stdout = true;
options.max_num_iterations = opt.max_num_iterations;
options.max_solver_time_in_seconds = opt.max_solver_time_s;
options.num_threads = static_cast<int>(hw);
options.function_tolerance = 1e-4;
ceres::Solver::Summary summary;
ceres::Solve(options, &problem, &summary);
std::cout << summary.FullReport() << std::endl;
}
void merge(size_t nhkl, ScaleMergeResult &out, const std::vector<CorrectedObs> &corr_obs) {
// ---- Merge (XDS/XSCALE style: inverse-variance weighted mean) ----
struct HKLAccum {
double sum_wI = 0.0;
double sum_w = 0.0;
double sum_wsigma2 = 0.0;
};
std::vector<HKLAccum> accum(nhkl);
for (const auto &co: corr_obs) {
const double w = co.weight / (co.sigma_corr * co.sigma_corr);
auto &a = accum[co.hkl_slot];
a.sum_wI += w * co.I_corr;
a.sum_w += w;
a.sum_wsigma2 += w * w * co.sigma_corr * co.sigma_corr;
}
for (int h = 0; h < nhkl; ++h) {
const auto &a = accum[h];
if (a.sum_w <= 0.0)
continue;
out.merged[h].I = a.sum_wI / a.sum_w;
out.merged[h].sigma = std::sqrt(a.sum_wsigma2) / a.sum_w;
}
}
void stats(const ScaleMergeOptions &opt, size_t nhkl, ScaleMergeResult &out, const std::vector<CorrectedObs> &corr_obs)
// ---- Compute per-shell merging statistics ----
{
constexpr int kStatShells = 10;
float stat_d_min = std::numeric_limits<float>::max();
float stat_d_max = 0.0f;
for (int h = 0; h < nhkl; ++h) {
const auto d = static_cast<float>(out.merged[h].d);
if (std::isfinite(d) && d > 0.0f) {
if (opt.d_min_limit_A > 0.0 && d < static_cast<float>(opt.d_min_limit_A))
continue;
stat_d_min = std::min(stat_d_min, d);
stat_d_max = std::max(stat_d_max, d);
}
}
if (stat_d_min < stat_d_max && stat_d_min > 0.0f) {
const float d_min_pad = stat_d_min * 0.999f;
const float d_max_pad = stat_d_max * 1.001f;
ResolutionShells stat_shells(d_min_pad, d_max_pad, kStatShells);
const auto shell_mean_1_d2 = stat_shells.GetShellMeanOneOverResSq();
const auto shell_min_res = stat_shells.GetShellMinRes();
// Assign each unique reflection to a shell
std::vector<int32_t> hkl_shell(nhkl, -1);
for (int h = 0; h < nhkl; ++h) {
const auto d = static_cast<float>(out.merged[h].d);
if (std::isfinite(d) && d > 0.0f) {
if (opt.d_min_limit_A > 0.0 && d < static_cast<float>(opt.d_min_limit_A))
continue;
auto s = stat_shells.GetShell(d);
if (s.has_value())
hkl_shell[h] = s.value();
}
}
// Per-HKL: collect corrected intensities for Rmeas
struct HKLStats {
double sum_I = 0.0;
int n = 0;
std::vector<double> I_list;
};
std::vector<HKLStats> per_hkl(nhkl);
for (const auto &co: corr_obs) {
if (hkl_shell[co.hkl_slot] < 0)
continue;
auto &hs = per_hkl[co.hkl_slot];
hs.sum_I += co.I_corr;
hs.n += 1;
hs.I_list.push_back(co.I_corr);
}
// Accumulators per shell
struct ShellAccum {
int total_obs = 0;
std::unordered_set<int> unique_hkls;
double rmeas_num = 0.0;
double rmeas_den = 0.0;
double sum_i_over_sig = 0.0;
int n_merged_with_sigma = 0;
};
std::vector<ShellAccum> shell_acc(kStatShells);
for (int h = 0; h < nhkl; ++h) {
if (hkl_shell[h] < 0)
continue;
const int s = hkl_shell[h];
auto &sa = shell_acc[s];
const auto &hs = per_hkl[h];
if (hs.n == 0)
continue;
sa.unique_hkls.insert(h);
sa.total_obs += hs.n;
const double mean_I = hs.sum_I / static_cast<double>(hs.n);
if (hs.n >= 2) {
double sum_abs_dev = 0.0;
for (double Ii: hs.I_list)
sum_abs_dev += std::abs(Ii - mean_I);
sa.rmeas_num += std::sqrt(static_cast<double>(hs.n) / (hs.n - 1.0)) * sum_abs_dev;
}
for (double Ii: hs.I_list)
sa.rmeas_den += std::abs(Ii);
if (out.merged[h].sigma > 0.0) {
sa.sum_i_over_sig += out.merged[h].I / out.merged[h].sigma;
sa.n_merged_with_sigma += 1;
}
}
// Completeness (not yet available without unit cell)
std::vector<int> possible_per_shell(kStatShells, 0);
int total_possible = 0;
bool have_completeness = false;
// Fill output statistics
out.statistics.shells.resize(kStatShells);
for (int s = 0; s < kStatShells; ++s) {
auto &ss = out.statistics.shells[s];
const auto &sa = shell_acc[s];
ss.mean_one_over_d2 = shell_mean_1_d2[s];
ss.d_min = shell_min_res[s];
ss.d_max = (s == 0) ? d_max_pad : shell_min_res[s - 1];
ss.total_observations = sa.total_obs;
ss.unique_reflections = static_cast<int>(sa.unique_hkls.size());
ss.rmeas = (sa.rmeas_den > 0.0) ? (sa.rmeas_num / sa.rmeas_den) : 0.0;
ss.mean_i_over_sigma = (sa.n_merged_with_sigma > 0)
? (sa.sum_i_over_sig / sa.n_merged_with_sigma)
: 0.0;
ss.possible_reflections = possible_per_shell[s];
ss.completeness = (have_completeness && possible_per_shell[s] > 0)
? static_cast<double>(sa.unique_hkls.size()) / possible_per_shell[s]
: 0.0;
}
// Overall statistics
{
auto &ov = out.statistics.overall;
ov.d_min = stat_d_min;
ov.d_max = stat_d_max;
ov.mean_one_over_d2 = 0.0f;
int total_obs_all = 0;
std::unordered_set<int> all_unique;
double rmeas_num_all = 0.0, rmeas_den_all = 0.0;
double sum_isig_all = 0.0;
int n_isig_all = 0;
for (int s = 0; s < kStatShells; ++s) {
const auto &sa = shell_acc[s];
total_obs_all += sa.total_obs;
all_unique.insert(sa.unique_hkls.begin(), sa.unique_hkls.end());
rmeas_num_all += sa.rmeas_num;
rmeas_den_all += sa.rmeas_den;
sum_isig_all += sa.sum_i_over_sig;
n_isig_all += sa.n_merged_with_sigma;
}
ov.total_observations = total_obs_all;
ov.unique_reflections = static_cast<int>(all_unique.size());
ov.rmeas = (rmeas_den_all > 0.0) ? (rmeas_num_all / rmeas_den_all) : 0.0;
ov.mean_i_over_sigma = (n_isig_all > 0) ? (sum_isig_all / n_isig_all) : 0.0;
ov.possible_reflections = total_possible;
ov.completeness = (have_completeness && total_possible > 0)
? static_cast<double>(all_unique.size()) / total_possible
: 0.0;
}
}
}
void calc_obs(const ScaleMergeOptions &opt,
std::vector<double> &g,
std::vector<double> &mosaicity,
std::vector<double> &R_sq,
const std::vector<ObsRef> &obs,
std::vector<CorrectedObs> &corr_obs) {
// ---- Compute corrected observations once (used for both merging and statistics) ----
const double half_wedge = opt.wedge_deg.value_or(0.0) / 2.0;
for (const auto &o: obs) {
const Reflection &r = *o.r;
const double lp = SafeInv(static_cast<double>(r.rlp), 1.0);
const double G_i = g[o.img_id];
// Compute partiality with refined mosaicity
double partiality = 1.0;
switch (opt.partiality_model) {
case ScaleMergeOptions::PartialityModel::Fixed:
partiality = r.partiality;
break;
case ScaleMergeOptions::PartialityModel::Rotation: {
const double c1 = r.zeta / std::sqrt(2.0);
const double arg_plus = (r.delta_phi_deg + half_wedge) * c1 / mosaicity[o.img_id];
const double arg_minus = (r.delta_phi_deg - half_wedge) * c1 / mosaicity[o.img_id];
partiality = (std::erf(arg_plus) - std::erf(arg_minus)) / 2.0;
}
break;
case ScaleMergeOptions::PartialityModel::Still:
partiality = std::exp(-r.dist_ewald * r.dist_ewald / R_sq[o.img_id]);
break;
case ScaleMergeOptions::PartialityModel::Unity:
break;
}
if (partiality <= opt.min_partiality_for_merge)
continue;
const double correction = G_i * partiality * lp;
if (correction <= 0.0)
continue;
corr_obs.push_back({
o.hkl_slot,
static_cast<double>(r.I) / correction,
o.sigma / correction,
1 / correction
});
}
}
void proc_obs(const std::vector<std::vector<Reflection> > &observations,
const ScaleMergeOptions &opt,
std::vector<uint8_t> &image_slot_used,
std::vector<ObsRef> &obs,
std::unordered_map<HKLKey, int, HKLKeyHash> &hklToSlot
) {
for (int i = 0; i < observations.size(); i++) {
for (const auto &r: observations[i]) {
const double d = SafeD(r.d);
if (!std::isfinite(d))
continue;
if (!std::isfinite(r.I))
continue;
if (opt.d_min_limit_A > 0.0 && d < opt.d_min_limit_A)
continue;
if (!std::isfinite(r.zeta) || r.zeta <= 0.0f)
continue;
if (!std::isfinite(r.rlp) || r.rlp == 0.0f)
continue;
const double sigma = SafeSigma(r.sigma, opt.min_sigma);
const int img_id = i / opt.image_cluster;
image_slot_used[img_id] = 1;
int hkl_slot;
try {
const HKLKey key = CanonicalizeHKLKey(r, opt);
auto it = hklToSlot.find(key);
if (it == hklToSlot.end()) {
hkl_slot = static_cast<int>(hklToSlot.size());
hklToSlot.emplace(key, hkl_slot);
} else {
hkl_slot = it->second;
}
} catch (...) {
continue;
}
ObsRef o;
o.r = &r;
o.img_id = img_id;
o.hkl_slot = hkl_slot;
o.sigma = sigma;
obs.push_back(o);
}
}
}
} // namespace
ScaleMergeResult ScaleAndMergeReflectionsCeres(const std::vector<std::vector<Reflection> > &observations,
const ScaleMergeOptions &opt) {
if (opt.image_cluster <= 0)
throw std::invalid_argument("image_cluster must be positive");
const bool rotation_crystallography = opt.wedge_deg.has_value();
size_t nrefl = 0;
for (const auto &i: observations)
nrefl += i.size();
std::vector<ObsRef> obs;
std::vector<CorrectedObs> corr_obs;
obs.reserve(nrefl);
corr_obs.reserve(nrefl);
std::unordered_map<HKLKey, int, HKLKeyHash> hklToSlot;
hklToSlot.reserve(nrefl);
size_t n_image_slots = observations.size() / opt.image_cluster +
(observations.size() % opt.image_cluster > 0 ? 1 : 0);
std::vector<uint8_t> image_slot_used(n_image_slots, 0);
proc_obs(observations, opt, image_slot_used, obs, hklToSlot);
const int nhkl = static_cast<int>(hklToSlot.size());
std::vector<double> g(n_image_slots, 1.0);
std::vector<double> mosaicity(n_image_slots, opt.mosaicity_init_deg);
std::vector<double> R_sq(n_image_slots, 0.001 * 0.001);
for (int i = 0; i < n_image_slots; i++) {
if (!image_slot_used[i]) {
mosaicity[i] = NAN;
g[i] = NAN;
R_sq[i] = NAN;
} else if (opt.mosaicity_init_deg_vec.size() > i && std::isfinite(opt.mosaicity_init_deg_vec[i])) {
mosaicity[i] = opt.mosaicity_init_deg_vec[i];
}
}
scale(opt, g, mosaicity, R_sq, image_slot_used, rotation_crystallography, nhkl, obs);
ScaleMergeResult out;
out.image_scale_g.resize(observations.size(), NAN);
out.mosaicity_deg.resize(observations.size(), NAN);
for (int i = 0; i < observations.size(); i++) {
size_t img_slot = i / opt.image_cluster;
if (image_slot_used[img_slot]) {
out.image_scale_g[i] = g[img_slot];
out.mosaicity_deg[i] = mosaicity[img_slot];
}
}
std::vector<HKLKey> slotToHKL(nhkl);
for (const auto &kv: hklToSlot)
slotToHKL[kv.second] = kv.first;
out.merged.resize(nhkl);
for (int h = 0; h < nhkl; ++h) {
out.merged[h].h = slotToHKL[h].h;
out.merged[h].k = slotToHKL[h].k;
out.merged[h].l = slotToHKL[h].l;
out.merged[h].I = 0.0;
out.merged[h].sigma = 0.0;
out.merged[h].d = 0.0;
}
// Populate d from median of observations per HKL
{
std::vector<std::vector<double> > per_hkl_d(nhkl);
for (const auto &o: obs) {
const double d_val = static_cast<double>(o.r->d);
if (std::isfinite(d_val) && d_val > 0.0)
per_hkl_d[o.hkl_slot].push_back(d_val);
}
for (int h = 0; h < nhkl; ++h) {
auto &v = per_hkl_d[h];
if (!v.empty()) {
std::nth_element(v.begin(), v.begin() + static_cast<long>(v.size() / 2), v.end());
out.merged[h].d = v[v.size() / 2];
}
}
}
calc_obs(opt, g, mosaicity, R_sq, obs, corr_obs);
merge(nhkl, out, corr_obs);
stats(opt, nhkl, out, corr_obs);
return out;
}
@@ -1,94 +0,0 @@
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <vector>
#include <cstdint>
#include <optional>
#include "../../common/Reflection.h"
#include "gemmi/symmetry.hpp"
struct ScaleMergeOptions {
int max_num_iterations = 100;
double max_solver_time_s = 1.0;
double image_number_rounding = 1.0;
double min_sigma = 1e-3;
// Symmetry canonicalization of HKL prior to merging/scaling.
// If not set, the routine uses raw HKL as-is.
std::optional<gemmi::SpaceGroup> space_group;
// If true, treat Friedel mates as equivalent (merge anomalous pairs).
// If false, keep them separate by including a sign flag in the HKL key.
bool merge_friedel = true;
// --- Kabsch(XDS)-style partiality model ---
// Rotation range (wedge) used in partiality calculation.
// Set to 0 to disable partiality correction.
std::optional<double> wedge_deg;
// --- Mosaicity (user input in degrees; internally converted to radians) ---
double mosaicity_init_deg = 0.17;
double mosaicity_min_deg = 1e-3;
double mosaicity_max_deg = 2.0;
std::vector<double> mosaicity_init_deg_vec;
// --- Optional: regularize per-image scale k towards 1 (Kabsch-like) ---
bool regularize_scale_to_one = true;
double scale_regularization_sigma = 0.05;
double min_partiality_for_merge = 0.2;
bool smoothen_g = true;
bool smoothen_mos = true;
double d_min_limit_A = 0.0;
int64_t image_cluster = 1;
bool refine_wedge = false;
enum class PartialityModel {Fixed, Rotation, Unity, Still} partiality_model = PartialityModel::Fixed;
};
struct MergedReflection {
int h;
int k;
int l;
double I;
double sigma;
double d = 0.0;
};
/// Per-resolution-shell merging statistics
struct MergeStatisticsShell {
float d_min = 0.0f; ///< High-resolution limit of this shell (Å)
float d_max = 0.0f; ///< Low-resolution limit of this shell (Å)
float mean_one_over_d2 = 0; ///< Mean 1/d² in shell
int total_observations = 0; ///< Total number of (corrected) observations
int unique_reflections = 0; ///< Number of unique HKLs with observations
double rmeas = 0.0; ///< Redundancy-independent merging R-factor
double mean_i_over_sigma = 0.0; ///< Mean I/σ(I) of the merged reflections
double completeness = 0.0; ///< Fraction of possible reflections observed (0 if unknown)
int possible_reflections = 0;///< Theoretical number of reflections in this shell (0 if unknown)
};
/// Overall + per-shell merging statistics
struct MergeStatistics {
std::vector<MergeStatisticsShell> shells;
MergeStatisticsShell overall; ///< Statistics over all shells combined
};
struct ScaleMergeResult {
std::vector<MergedReflection> merged;
std::vector<float> image_scale_g;
std::vector<float> mosaicity_deg;
/// Per-shell and overall merging statistics (populated after merging)
MergeStatistics statistics;
};
ScaleMergeResult ScaleAndMergeReflectionsCeres(const std::vector<std::vector<Reflection>>& observations,
const ScaleMergeOptions& opt = {});
@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "ScaleOnTheFly.h"
ScaleOnTheFly::ScaleOnTheFly(const std::vector<MergedReflection> &ref,
std::optional<gemmi::SpaceGroup> sg,
bool merge_friedel) : sg(sg), merge_friedel(merge_friedel) {
for (const auto &r : ref) {
const auto key = CanonicalHKL(r, merge_friedel, sg);
reference_data[key] = r.I;
}
}
@@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include "HKLKey.h"
#include "Merge.h"
#include <map>
class ScaleOnTheFly {
std::optional<gemmi::SpaceGroup> sg;
bool merge_friedel;
std::map<HKLKey, double> reference_data;
public:
ScaleOnTheFly(const std::vector<MergedReflection> &ref, std::optional<gemmi::SpaceGroup> sg, bool merge_friedel = true);
void Scale(std::vector<Reflection> &reflections);
};
@@ -244,10 +244,10 @@ namespace {
const int idx = bin_index(r.d);
auto& bin = bins[static_cast<size_t>(idx)];
bin.d_min_A = std::min(bin.d_min_A, r.d);
bin.d_max_A = std::max(bin.d_max_A, r.d);
bin.d_min_A = std::min<double>(bin.d_min_A, r.d);
bin.d_max_A = std::max<double>(bin.d_max_A, r.d);
const double i_over_sigma = std::max(0.0, r.I / r.sigma);
const double i_over_sigma = std::max<double>(0.0, r.I / r.sigma);
const gemmi::Op::Miller hkl{{r.h, r.k, r.l}};
if (gops.is_systematically_absent(hkl)) {
@@ -7,7 +7,7 @@
#include <string>
#include <vector>
#include "ScaleAndMerge.h"
#include "Merge.h"
#include "gemmi/symmetry.hpp"
struct SpaceGroupOperatorScore {
+2 -2
View File
@@ -11,7 +11,7 @@ void CBORFilePusher::StartDataCollection(StartMessage &message) {
dataset_number++;
img_number = 0;
size_t approx_size = 1024*1024;
size_t approx_size = 256*1024*1024;
for (const auto &[key, value] : message.pixel_mask)
approx_size += value.size() * sizeof(uint32_t);
@@ -25,7 +25,7 @@ void CBORFilePusher::StartDataCollection(StartMessage &message) {
bool CBORFilePusher::EndDataCollection(const EndMessage &message) {
std::unique_lock ul(m);
size_t approx_size = 1024*1024;
size_t approx_size = 256*1024*1024;
std::vector<uint8_t> serialization_buffer(approx_size);
CBORStream2Serializer serializer(serialization_buffer.data(), serialization_buffer.size());
-9
View File
@@ -27,12 +27,6 @@ void HDF5FilePusher::StartDataCollection(StartMessage &message) {
StartMessage repub_message = message;
repub_message.writer_notification_zmq_addr = "";
size_t approx_size = 1024 * 1024;
for (const auto &[key, value] : repub_message.pixel_mask)
approx_size += value.size() * sizeof(uint32_t);
std::vector<uint8_t> serialization_buffer(approx_size);
CBORStream2Serializer serializer(serialization_buffer.data(), serialization_buffer.size());
serializer.SerializeSequenceStart(repub_message);
repub_active = repub_socket->Send(serialization_buffer.data(), serializer.GetBufferSize(), true);
@@ -59,9 +53,6 @@ bool HDF5FilePusher::EndDataCollection(const EndMessage &message) {
if (repub_socket) {
try {
size_t approx_size = 1024 * 1024;
std::vector<uint8_t> serialization_buffer(approx_size);
CBORStream2Serializer serializer(serialization_buffer.data(), serialization_buffer.size());
serializer.SerializeSequenceEnd(message);
if (repub_active)
+4
View File
@@ -13,6 +13,10 @@ void PrepareCBORImage(DataMessage& message,
experiment.GetCompressionAlgorithm());
}
ImagePusher::ImagePusher()
: serialization_buffer(MESSAGE_SIZE_FOR_START_END),
serializer(serialization_buffer.data(), serialization_buffer.size()) {}
std::string ImagePusher::Finalize() {
return "";
}
+5
View File
@@ -10,6 +10,7 @@
#include "../common/JFJochMessages.h"
#include "../common/ZeroCopyReturnValue.h"
#include "../common/Logger.h"
#include "../frame_serialize/CBORStream2Serializer.h"
enum class ImagePusherType {HDF5, CBOR, TCP, ZMQ, Test, None};
@@ -33,6 +34,10 @@ void PrepareCBORImage(DataMessage& message,
void *image, size_t image_size);
class ImagePusher {
protected:
std::vector<uint8_t> serialization_buffer;
CBORStream2Serializer serializer;
ImagePusher();
public:
virtual void StartDataCollection(StartMessage& message) = 0;
virtual bool EndDataCollection(const EndMessage& message) = 0; // Non-blocking
+1 -3
View File
@@ -108,9 +108,7 @@ void TCPStreamPusher::CloseFd(std::atomic<int>& fd) {
TCPStreamPusher::TCPStreamPusher(const std::string& addr,
size_t in_max_connections,
std::optional<int32_t> in_send_buffer_size)
: serialization_buffer(256 * 1024 * 1024),
serializer(serialization_buffer.data(), serialization_buffer.size()),
endpoint(addr),
: endpoint(addr),
max_connections(in_max_connections),
send_buffer_size(in_send_buffer_size) {
if (endpoint.empty())
-3
View File
@@ -97,9 +97,6 @@ class TCPStreamPusher : public ImagePusher {
std::chrono::steady_clock::time_point last_keepalive_recv{};
};
std::vector<uint8_t> serialization_buffer;
CBORStream2Serializer serializer;
std::string endpoint;
size_t max_connections;
std::optional<int32_t> send_buffer_size;
-2
View File
@@ -7,8 +7,6 @@
ZMQStream2Pusher::ZMQStream2Pusher(const std::vector<std::string> &addr,
std::optional<int32_t> send_buffer_high_watermark,
std::optional<int32_t> send_buffer_size)
: serialization_buffer(256*1024*1024),
serializer(serialization_buffer.data(), serialization_buffer.size())
{
if (addr.empty())
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "No writer ZMQ address provided");
-4
View File
@@ -9,12 +9,8 @@
#include "../preview/PreviewCounter.h"
#include "ZMQWriterNotificationPuller.h"
#include "ZMQStream2PusherSocket.h"
#include "../frame_serialize/CBORStream2Serializer.h"
class ZMQStream2Pusher : public ImagePusher {
std::vector<uint8_t> serialization_buffer;
CBORStream2Serializer serializer;
std::vector<std::unique_ptr<ZMQStream2PusherSocket>> socket;
std::unique_ptr<ZMQWriterNotificationPuller> writer_notification_socket;
+440 -173
View File
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <cmath>
#include "JFJochHDF5Reader.h"
#include "spdlog/fmt/fmt.h"
#include "../image_analysis/bragg_integration/CalcISigma.h"
@@ -55,7 +56,7 @@ inline std::pair<gemmi::CrystalSystem, char> parse_niggli_class(int64_t val) {
}
}
std::vector<hsize_t> GetDimension(HDF5Object& object, const std::string& path) {
std::vector<hsize_t> GetDimension(HDF5Object &object, const std::string &path) {
const auto dim = object.GetDimension(path);
if (dim.size() != 3)
throw JFJochException(JFJochExceptionCategory::HDF5, "Wrong dimension of /entry/data/data");
@@ -93,7 +94,7 @@ std::string ResolveRelativeToMaster(const std::string &directory,
return (std::filesystem::path(directory) / path).string();
}
template <class T>
template<class T>
void JFJochHDF5Reader::ReadVector(std::vector<T> &v,
HDF5Object &file,
const std::string &dataset_name,
@@ -106,33 +107,123 @@ void JFJochHDF5Reader::ReadVector(std::vector<T> &v,
for (int i = 0; i < tmp.size(); i++)
v[image0 + i] = tmp[i];
}
} catch (JFJochException &e) {}
} catch (JFJochException &e) {
}
}
std::string removeSuffix(const std::string& s, const std::string& suffix)
{
std::string removeSuffix(const std::string &s, const std::string &suffix) {
if (s.ends_with(suffix))
return s.substr(0, s.size() - suffix.size());
return s;
}
std::string dataset_name(const std::string& path) {
std::string dataset_name(const std::string &path) {
std::string file = path;
int pos = file.rfind('/');
if (pos != std::string::npos)
file = file.substr(pos+1);
file = file.substr(pos + 1);
file = removeSuffix(file, "_master.h5");
// If previous suffix was not found, try removing this one
// If previous suffix was not found, try removing this one
file = removeSuffix(file, ".h5");
return file;
}
void JFJochHDF5Reader::ReadFile(const std::string& filename) {
bool ReadReflectionsFromGroup(HDF5Object &file,
const std::string &image_group_name,
std::vector<Reflection> &reflections) {
if (!file.Exists("/entry/reflections") || !file.Exists(image_group_name))
return false;
auto h = file.ReadOptVector<int32_t>(image_group_name + "/h");
auto k = file.ReadOptVector<int32_t>(image_group_name + "/k");
auto l = file.ReadOptVector<int32_t>(image_group_name + "/l");
auto predicted_x = file.ReadOptVector<float>(image_group_name + "/predicted_x");
auto predicted_y = file.ReadOptVector<float>(image_group_name + "/predicted_y");
auto d = file.ReadOptVector<float>(image_group_name + "/d");
auto int_sum = file.ReadOptVector<float>(image_group_name + "/int_sum");
auto int_err = file.ReadOptVector<float>(image_group_name + "/int_err");
auto bkg = file.ReadOptVector<float>(image_group_name + "/background_mean");
auto lp = file.ReadOptVector<float>(image_group_name + "/lp");
auto partiality = file.ReadOptVector<float>(image_group_name + "/partiality");
auto phi = file.ReadOptVector<float>(image_group_name + "/delta_phi");
auto zeta = file.ReadOptVector<float>(image_group_name + "/zeta");
if (h.size() != l.size() || h.size() != k.size() || h.size() != d.size()
|| h.size() != predicted_x.size() || h.size() != predicted_y.size()
|| h.size() != int_sum.size() || h.size() != int_err.size() || h.size() != bkg.size())
throw JFJochException(JFJochExceptionCategory::HDF5, "Wrong size of reflections dataset");
for (size_t i = 0; i < h.size(); i++) {
float lp_val = 0.0;
if (lp.size() > i && lp[i] != 0.0f)
lp_val = 1.0f / lp[i];
float partiality_val = -1.0f;
if (partiality.size() > i && partiality[i] >= 0.0f)
partiality_val = partiality[i];
float delta_phi_val = NAN;
if (phi.size() > i)
delta_phi_val = phi[i];
float zeta_val = NAN;
if (zeta.size() > i)
zeta_val = zeta[i];
Reflection r{
.h = h.at(i),
.k = k.at(i),
.l = l.at(i),
.delta_phi_deg = delta_phi_val,
.predicted_x = predicted_x.at(i),
.predicted_y = predicted_y.at(i),
.d = d.at(i),
.I = int_sum.at(i),
.bkg = bkg.at(i),
.sigma = int_err.at(i),
.rlp = lp_val,
.partiality = partiality_val,
.zeta = zeta_val
};
reflections.emplace_back(r);
}
return true;
}
template<class T>
std::optional<T> ReadElementMasterFirst(HDF5Object &master_file,
HDF5Object &source_file,
const std::string &path,
hsize_t master_image,
hsize_t source_image) {
if (master_file.Exists(path))
return master_file.ReadElement<T>(path, master_image);
if (source_file.Exists(path))
return source_file.ReadElement<T>(path, source_image);
return {};
}
template<class T>
std::vector<T> ReadVectorMasterFirst(HDF5Object &master_file,
HDF5Object &source_file,
const std::string &path,
const std::vector<hsize_t> &master_start,
const std::vector<hsize_t> &source_start,
const std::vector<hsize_t> &size) {
if (master_file.Exists(path))
return master_file.ReadOptVector<T>(path, master_start, size);
if (source_file.Exists(path))
return source_file.ReadOptVector<T>(path, source_start, size);
return {};
}
void JFJochHDF5Reader::ReadFile(const std::string &filename) {
std::unique_lock ul(hdf5_mutex);
try {
auto dataset = std::make_shared<JFJochReaderDataset>();
master_file = std::make_shared<HDF5ReadOnlyFile>(filename);
master_filename = filename;
dataset->experiment = default_experiment;
std::filesystem::path master_path(filename);
@@ -165,7 +256,7 @@ void JFJochHDF5Reader::ReadFile(const std::string& filename) {
vds_data_mappings = ReadVDSImageMappings(*master_file, "/entry/data/data");
if (master_file->Exists("/entry/instrument/detector/detectorSpecific/data_collection_efficiency_image"))
dataset->efficiency = master_file->ReadVector<float>(
dataset->efficiency = master_file->ReadVector<float>(
"/entry/instrument/detector/detectorSpecific/data_collection_efficiency_image");
else
dataset->efficiency = std::vector<float>(number_of_images, 1.0);
@@ -198,6 +289,8 @@ void JFJochHDF5Reader::ReadFile(const std::string& filename) {
dataset->profile_radius = master_file->ReadOptVector<float>("/entry/MX/profileRadius");
dataset->mosaicity_deg = master_file->ReadOptVector<float>("/entry/MX/mosaicity");
dataset->b_factor = master_file->ReadOptVector<float>("/entry/MX/bFactor");
dataset->scale_factor = master_file->ReadOptVector<float>("/entry/MX/imageScaleFactor");
dataset->integrated_reflections = master_file->ReadOptVector<float>("/entry/MX/integratedReflections");
}
if (master_file->Exists("/entry/image"))
dataset->max_value = master_file->ReadOptVector<int64_t>("/entry/image/max_value");
@@ -317,8 +410,8 @@ void JFJochHDF5Reader::ReadFile(const std::string& filename) {
number_of_images, fimages);
ReadVector(dataset->mosaicity_deg,
data_file, "/entry/MX/mosaicity",
number_of_images, fimages);
data_file, "/entry/MX/mosaicity",
number_of_images, fimages);
ReadVector(dataset->b_factor,
data_file, "/entry/MX/bFactor",
@@ -334,7 +427,8 @@ void JFJochHDF5Reader::ReadFile(const std::string& filename) {
data_file, "/entry/image/max_value",
number_of_images, fimages);
}
} catch (JFJochException &e) {}
} catch (JFJochException &e) {
}
if (nfiles == 0)
images_per_file = fimages;
@@ -353,17 +447,19 @@ void JFJochHDF5Reader::ReadFile(const std::string& filename) {
dataset->experiment.IndexingAlgorithm(IndexingAlgorithmEnum::FFT);
else if (indexing == "ffbidx")
dataset->experiment.IndexingAlgorithm(IndexingAlgorithmEnum::FFBIDX);
dataset->scale_factor = master_file->ReadOptVector<float>("/entry/MX/imageScaleFactor");
}
auto ring_current_A = master_file->GetOptFloat("/entry/source/current");
if (ring_current_A) dataset->experiment.RingCurrent_mA(ring_current_A.value() * 1000.0);
dataset->experiment.DetectIceRings(master_file->GetOptBool("/entry/instrument/detector/detectorSpecific/detect_ice_rings").value_or(false));
dataset->experiment.PoniRot1_rad(master_file->GetOptFloat("/entry/instrument/detector/transformations/rot1").value_or(0.0));
dataset->experiment.PoniRot2_rad(master_file->GetOptFloat("/entry/instrument/detector/transformations/rot2").value_or(0.0));
dataset->experiment.PoniRot3_rad(master_file->GetOptFloat("/entry/instrument/detector/transformations/rot3").value_or(0.0));
dataset->experiment.DetectIceRings(
master_file->GetOptBool("/entry/instrument/detector/detectorSpecific/detect_ice_rings").value_or(false));
dataset->experiment.PoniRot1_rad(
master_file->GetOptFloat("/entry/instrument/detector/transformations/rot1").value_or(0.0));
dataset->experiment.PoniRot2_rad(
master_file->GetOptFloat("/entry/instrument/detector/transformations/rot2").value_or(0.0));
dataset->experiment.PoniRot3_rad(
master_file->GetOptFloat("/entry/instrument/detector/transformations/rot3").value_or(0.0));
dataset->experiment.SampleTemperature_K(master_file->GetOptFloat("/entry/sample/temperature"));
dataset->experiment.BeamX_pxl(master_file->GetFloat("/entry/instrument/detector/beam_center_x"));
@@ -374,7 +470,8 @@ void JFJochHDF5Reader::ReadFile(const std::string& filename) {
det_distance = 0.1; // Set to 100 mm, if det distance is less than 1 mm
dataset->experiment.DetectorDistance_mm(det_distance * 1000.0);
dataset->experiment.IncidentEnergy_keV(WVL_1A_IN_KEV / master_file->GetFloat("/entry/instrument/beam/incident_wavelength"));
dataset->experiment.IncidentEnergy_keV(
WVL_1A_IN_KEV / master_file->GetFloat("/entry/instrument/beam/incident_wavelength"));
dataset->error_value = master_file->GetOptInt("/entry/instrument/detector/error_value");
@@ -391,12 +488,12 @@ void JFJochHDF5Reader::ReadFile(const std::string& filename) {
dataset->experiment.Goniometer(omega);
} else if (master_file->Exists("/entry/sample/grid_scan")) {
GridScanSettings grid(
master_file->GetInt("/entry/sample/grid_scan/n_fast"),
master_file->GetFloat("/entry/sample/grid_scan/step_x") * 1e6f,
master_file->GetFloat("/entry/sample/grid_scan/step_y") * 1e6f,
master_file->GetOptBool("/entry/sample/grid_scan/snake_scan").value_or(false),
master_file->GetOptBool("/entry/sample/grid_scan/vertical_scan").value_or(false)
);
master_file->GetInt("/entry/sample/grid_scan/n_fast"),
master_file->GetFloat("/entry/sample/grid_scan/step_x") * 1e6f,
master_file->GetFloat("/entry/sample/grid_scan/step_y") * 1e6f,
master_file->GetOptBool("/entry/sample/grid_scan/snake_scan").value_or(false),
master_file->GetOptBool("/entry/sample/grid_scan/vertical_scan").value_or(false)
);
grid.ImageNum(number_of_images);
dataset->experiment.GridScan(grid);
}
@@ -405,18 +502,20 @@ void JFJochHDF5Reader::ReadFile(const std::string& filename) {
auto tmp = master_file->ReadOptVector<float>("/entry/sample/unit_cell");
if (tmp.size() == 6)
dataset->experiment.SetUnitCell(UnitCell{
.a = tmp[0],
.b = tmp[1],
.c = tmp[2],
.alpha = tmp[3],
.beta = tmp[4],
.gamma = tmp[5]});
.a = tmp[0],
.b = tmp[1],
.c = tmp[2],
.alpha = tmp[3],
.beta = tmp[4],
.gamma = tmp[5]
});
dataset->experiment.SpaceGroupNumber(master_file->GetOptInt("/entry/sample/space_group_number"));
dataset->experiment.SampleName(master_file->GetString("/entry/sample/name"));
if (master_file->Exists("/entry/instrument/attenuator"))
dataset->experiment.AttenuatorTransmission(master_file->GetOptFloat("/entry/instrument/attenuator/attenuator_transmission"));
dataset->experiment.AttenuatorTransmission(
master_file->GetOptFloat("/entry/instrument/attenuator/attenuator_transmission"));
dataset->experiment.TotalFlux(master_file->GetOptFloat("/entry/instrument/beam/total_flux"));
if (master_file->Exists("/entry/azint") && master_file->Exists("/entry/azint/bin_to_q")) {
@@ -432,14 +531,14 @@ void JFJochHDF5Reader::ReadFile(const std::string& filename) {
dataset->azimuthal_bins = dim[0];
dataset->q_bins = dim[1];
dataset->az_int_bin_to_q.resize(dim[0] * dim[1]);
bin_to_q_dataset.ReadVector(dataset->az_int_bin_to_q, {0,0}, dim);
bin_to_q_dataset.ReadVector(dataset->az_int_bin_to_q, {0, 0}, dim);
} else
throw JFJochException(JFJochExceptionCategory::HDF5, "Wrong dimension of /entry/azint/image dataset");
if (master_file->Exists("/entry/azint/bin_to_phi")) {
HDF5DataSet bin_to_phi_dataset(*master_file, "/entry/azint/bin_to_phi");
if (dataset->q_bins > 0) {
dataset->az_int_bin_to_phi.resize(dim[0] * dim[1]);
bin_to_phi_dataset.ReadVector(dataset->az_int_bin_to_phi, {0,0}, dim);
bin_to_phi_dataset.ReadVector(dataset->az_int_bin_to_phi, {0, 0}, dim);
} else {
bin_to_phi_dataset.ReadVector(dataset->az_int_bin_to_phi);
}
@@ -461,7 +560,7 @@ void JFJochHDF5Reader::ReadFile(const std::string& filename) {
detector.SaturationLimit(master_file->GetInt("/entry/instrument/detector/saturation_value"));
detector.MinFrameTime(std::chrono::microseconds(0));
detector.MinCountTime(std::chrono::microseconds(0));
detector.ReadOutTime(std::chrono::nanoseconds (0));
detector.ReadOutTime(std::chrono::nanoseconds(0));
dataset->experiment.Detector(detector);
dataset->experiment.FrameTime(
@@ -480,9 +579,9 @@ void JFJochHDF5Reader::ReadFile(const std::string& filename) {
if (image_size_x * image_size_y > 0) {
auto mask_tmp = master_file->ReadOptVector<uint32_t>(
"/entry/instrument/detector/pixel_mask",
{0, 0},
{image_size_y, image_size_x}
"/entry/instrument/detector/pixel_mask",
{0, 0},
{image_size_y, image_size_x}
);
if (mask_tmp.empty())
mask_tmp = std::vector<uint32_t>(image_size_x * image_size_y);
@@ -490,7 +589,7 @@ void JFJochHDF5Reader::ReadFile(const std::string& filename) {
}
dataset->experiment.ImagesPerTrigger(number_of_images);
SetStartMessage(dataset);
} catch (const std::exception& e) {
} catch (const std::exception &e) {
master_file = {};
number_of_images = 0;
SetStartMessage({});
@@ -531,11 +630,14 @@ CompressedImage JFJochHDF5Reader::LoadImageDataset(std::vector<uint8_t> &tmp, HD
}
if (datatype.IsFloat())
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,"Float datasets not supported at this time");
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Float datasets not supported at this time");
return {tmp, dim[2], dim[1],
CalcImageMode(datatype.GetElemSize(), datatype.IsFloat(), datatype.IsSigned()),
algorithm};
return {
tmp, dim[2], dim[1],
CalcImageMode(datatype.GetElemSize(), datatype.IsFloat(), datatype.IsSigned()),
algorithm
};
}
std::pair<std::shared_ptr<HDF5ReadOnlyFile>, uint32_t> JFJochHDF5Reader::GetImageLocation(int64_t image_number) {
@@ -558,7 +660,7 @@ std::pair<std::shared_ptr<HDF5ReadOnlyFile>, uint32_t> JFJochHDF5Reader::GetImag
image_id = static_cast<uint32_t>(mapping.SourceImage(image));
data_file = std::make_shared<HDF5ReadOnlyFile>(
ResolveRelativeToMaster(master_file_directory, mapping.filename)
ResolveRelativeToMaster(master_file_directory, mapping.filename)
);
return {std::move(data_file), image_id};
}
@@ -601,69 +703,106 @@ bool JFJochHDF5Reader::LoadImage_i(std::shared_ptr<JFJochReaderDataset> &dataset
"Cannot load image if file not loaded");
auto [source_file, image_id] = GetImageLocation(image_number);
message.image = LoadImageDataset(buffer, *source_file, image_id);
message.number = image_number;
auto spot_count_opt = source_file->ReadElement<uint32_t>("/entry/MX/nPeaks", image_id);
const auto master_image = static_cast<hsize_t>(image_number);
const auto source_image = static_cast<hsize_t>(image_id);
auto spot_count_opt = ReadElementMasterFirst<uint32_t>(*master_file,
*source_file,
"/entry/MX/nPeaks",
master_image,
source_image);
if (spot_count_opt.has_value() && spot_count_opt.value() > 0) {
size_t spot_count = spot_count_opt.value();
auto spot_x = source_file->ReadVector<float>(
"/entry/MX/peakXPosRaw",
{(hsize_t) image_id, 0},
{1, spot_count}
auto spot_x = ReadVectorMasterFirst<float>(
*master_file,
*source_file,
"/entry/MX/peakXPosRaw",
{master_image, 0},
{source_image, 0},
{1, spot_count}
);
auto spot_y = source_file->ReadVector<float>(
"/entry/MX/peakYPosRaw",
{(hsize_t) image_id, 0},
{1, spot_count}
auto spot_y = ReadVectorMasterFirst<float>(
*master_file,
*source_file,
"/entry/MX/peakYPosRaw",
{master_image, 0},
{source_image, 0},
{1, spot_count}
);
auto spot_intensity = source_file->ReadVector<float>(
"/entry/MX/peakTotalIntensity",
{(hsize_t) image_id, 0},
{1, spot_count}
);
auto spot_indexed = source_file->ReadOptVector<uint8_t>(
"/entry/MX/peakIndexed",
{(hsize_t) image_id, 0},
{1, spot_count}
auto spot_intensity = ReadVectorMasterFirst<float>(
*master_file,
*source_file,
"/entry/MX/peakTotalIntensity",
{master_image, 0},
{source_image, 0},
{1, spot_count}
);
auto spot_ice = source_file->ReadOptVector<uint8_t>(
"/entry/MX/peakIceRingRes",
{(hsize_t) image_id, 0},
{1, spot_count}
if (spot_x.size() < spot_count || spot_y.size() < spot_count || spot_intensity.size() < spot_count)
throw JFJochException(JFJochExceptionCategory::HDF5, "Wrong size of spot dataset");
auto spot_indexed = ReadVectorMasterFirst<uint8_t>(
*master_file,
*source_file,
"/entry/MX/peakIndexed",
{master_image, 0},
{source_image, 0},
{1, spot_count}
);
auto spot_h = source_file->ReadOptVector<int32_t>(
"/entry/MX/peakH",
{(hsize_t) image_id, 0},
{1, spot_count}
auto spot_ice = ReadVectorMasterFirst<uint8_t>(
*master_file,
*source_file,
"/entry/MX/peakIceRingRes",
{master_image, 0},
{source_image, 0},
{1, spot_count}
);
auto spot_k = source_file->ReadOptVector<int32_t>(
"/entry/MX/peakK",
{(hsize_t) image_id, 0},
{1, spot_count}
auto spot_h = ReadVectorMasterFirst<int32_t>(
*master_file,
*source_file,
"/entry/MX/peakH",
{master_image, 0},
{source_image, 0},
{1, spot_count}
);
auto spot_l = source_file->ReadOptVector<int32_t>(
"/entry/MX/peakL",
{(hsize_t) image_id, 0},
{1, spot_count}
auto spot_k = ReadVectorMasterFirst<int32_t>(
*master_file,
*source_file,
"/entry/MX/peakK",
{master_image, 0},
{source_image, 0},
{1, spot_count}
);
auto spot_dist_ewald_sphere = source_file->ReadOptVector<float>(
"/entry/MX/peakDistEwaldSphere",
{(hsize_t) image_id, 0},
{1, spot_count}
auto spot_l = ReadVectorMasterFirst<int32_t>(
*master_file,
*source_file,
"/entry/MX/peakL",
{master_image, 0},
{source_image, 0},
{1, spot_count}
);
auto spot_dist_ewald_sphere = ReadVectorMasterFirst<float>(
*master_file,
*source_file,
"/entry/MX/peakDistEwaldSphere",
{master_image, 0},
{source_image, 0},
{1, spot_count}
);
auto geom = dataset->experiment.GetDiffractionGeometry();
for (int i = 0; i < spot_count; i++) {
auto x = spot_x.at(i);
auto y = spot_y.at(i);
auto d = geom.PxlToRes(x, y);
@@ -689,40 +828,47 @@ bool JFJochHDF5Reader::LoadImage_i(std::shared_ptr<JFJochReaderDataset> &dataset
s.ice_ring = (spot_ice.at(i) != 0);
message.spots.emplace_back(s);
}
if (source_file->Exists("/entry/MX/peakCountUnfiltered"))
message.spot_count = source_file->ReadElement<uint32_t>("/entry/MX/peakCountUnfiltered", image_id);
if (auto v = ReadElementMasterFirst<uint32_t>(*master_file, *source_file,
"/entry/MX/peakCountUnfiltered",
master_image, source_image); v)
message.spot_count = v;
else
message.spot_count = source_file->ReadElement<uint32_t>("/entry/MX/nPeaks", image_id);
if (source_file->Exists("/entry/MX/peakCountIceRingRes"))
message.spot_count_ice_rings = source_file->ReadElement<uint32_t>("/entry/MX/peakCountIceRingRes", image_id);
if (source_file->Exists("/entry/MX/peakCountLowRes"))
message.spot_count_low_res = source_file->ReadElement<uint32_t>("/entry/MX/peakCountLowRes", image_id);
if (source_file->Exists("/entry/MX/peakCountIndexed"))
message.spot_count_indexed = source_file->ReadElement<uint32_t>("/entry/MX/peakCountIndexed", image_id);
message.spot_count = spot_count_opt;
message.spot_count_ice_rings = ReadElementMasterFirst<uint32_t>(
*master_file, *source_file, "/entry/MX/peakCountIceRingRes", master_image, source_image);
message.spot_count_low_res = ReadElementMasterFirst<uint32_t>(
*master_file, *source_file, "/entry/MX/peakCountLowRes", master_image, source_image);
message.spot_count_indexed = ReadElementMasterFirst<uint32_t>(
*master_file, *source_file, "/entry/MX/peakCountIndexed", master_image, source_image);
GenerateSpotPlot(message, 1.5);
}
if (source_file->Exists("/entry/MX/integratedReflections"))
message.integrated_reflections = source_file->ReadElement<float>("/entry/MX/integratedReflections", image_id);
if (!dataset->az_int_bin_to_q.empty()) {
if (dataset->azimuthal_bins == 0)
message.az_int_profile = source_file->ReadOptVector<float>(
if (dataset->azimuthal_bins == 0) {
message.az_int_profile = ReadVectorMasterFirst<float>(
*master_file,
*source_file,
"/entry/azint/image",
{(hsize_t) image_id, 0},
{master_image, 0},
{source_image, 0},
{1, dataset->az_int_bin_to_q.size()}
);
else {
message.az_int_profile.resize(dataset->azimuthal_bins * dataset->q_bins, 0);
message.az_int_profile = source_file->ReadOptVector<float>(
} else {
message.az_int_profile = ReadVectorMasterFirst<float>(
*master_file,
*source_file,
"/entry/azint/image",
{(hsize_t) image_id, 0, 0},
{master_image, 0, 0},
{source_image, 0, 0},
{1, dataset->azimuthal_bins, dataset->q_bins}
);
}
}
if (dataset->integrated_reflections.size() > image_number)
message.integrated_reflections = static_cast<int64_t>(std::lround(dataset->integrated_reflections.at(image_number)));
if (dataset->resolution_estimate.size() > image_number)
message.resolution_estimate = dataset->resolution_estimate[image_number];
if (dataset->indexing_result.size() > image_number)
@@ -740,17 +886,30 @@ bool JFJochHDF5Reader::LoadImage_i(std::shared_ptr<JFJochReaderDataset> &dataset
if (dataset->indexing_result.size() > image_number
&& dataset->indexing_result[image_number] != 0
&& source_file->Exists("/entry/MX")
&& source_file->Exists("/entry/MX/latticeIndexed")) {
&& (master_file->Exists("/entry/MX/latticeIndexed") ||
source_file->Exists("/entry/MX/latticeIndexed"))) {
std::vector<float> tmp = ReadVectorMasterFirst<float>(
*master_file,
*source_file,
"/entry/MX/latticeIndexed",
{master_image, 0},
{source_image, 0},
{1, 9}
);
std::vector<float> tmp = source_file->ReadVector<float>(
"/entry/MX/latticeIndexed",
{(hsize_t) image_id, 0},
{1, 9}
);
message.indexing_lattice = CrystalLattice(tmp);
if (tmp.size() == 9)
message.indexing_lattice = CrystalLattice(tmp);
std::optional<uint32_t> niggli_opt;
if (master_file->Exists("/entry/MX/niggli_class"))
niggli_opt = master_file->ReadElement<uint32_t>("/entry/MX/niggli_class", image_number);
else if (master_file->Exists("/entry/MX/niggliClass"))
niggli_opt = master_file->ReadElement<uint32_t>("/entry/MX/niggliClass", image_number);
else if (source_file->Exists("/entry/MX/niggli_class"))
niggli_opt = source_file->ReadElement<uint32_t>("/entry/MX/niggli_class", image_id);
else if (source_file->Exists("/entry/MX/niggliClass"))
niggli_opt = source_file->ReadElement<uint32_t>("/entry/MX/niggliClass", image_id);
std::optional<uint32_t> niggli_opt = source_file->ReadElement<uint32_t>("/entry/MX/niggli_class", image_id);
if (niggli_opt) {
auto symm_info = parse_niggli_class(niggli_opt.value());
@@ -762,66 +921,15 @@ bool JFJochHDF5Reader::LoadImage_i(std::shared_ptr<JFJochReaderDataset> &dataset
}
}
std::string image_group_name = fmt::format("/entry/reflections/image_{:06d}", image_id);
if (source_file->Exists("/entry/reflections") && source_file->Exists(image_group_name)) {
auto h = source_file->ReadOptVector<int32_t>(image_group_name + "/h");
auto k = source_file->ReadOptVector<int32_t>(image_group_name + "/k");
auto l = source_file->ReadOptVector<int32_t>(image_group_name + "/l");
auto predicted_x = source_file->ReadOptVector<float>(image_group_name + "/predicted_x");
auto predicted_y = source_file->ReadOptVector<float>(image_group_name + "/predicted_y");
auto d = source_file->ReadOptVector<float>(image_group_name + "/d");
auto int_sum = source_file->ReadOptVector<float>(image_group_name + "/int_sum");
auto int_err = source_file->ReadOptVector<float>(image_group_name + "/int_err");
auto bkg = source_file->ReadOptVector<float>(image_group_name + "/background_mean");
auto lp = source_file->ReadOptVector<float>(image_group_name + "/lp");
auto partiality = source_file->ReadOptVector<float>(image_group_name + "/partiality");
auto phi = source_file->ReadOptVector<float>(image_group_name + "/delta_phi");
auto zeta = source_file->ReadOptVector<float>(image_group_name + "/zeta");
if (h.size() != l.size() || h.size() != k.size() || h.size() != d.size()
|| h.size() != predicted_x.size() || h.size() != predicted_y.size()
|| h.size() != int_sum.size() || h.size() != int_err.size() || h.size() != bkg.size())
throw JFJochException(JFJochExceptionCategory::HDF5, "Wrong size of reflections dataset");
for (size_t i = 0; i < h.size(); i++) {
float lp_val = 0.0;
if (lp.size() > i && lp[i] != 0.0f)
lp_val = 1.0f / lp[i];
float partiality_val = -1.0f;
if (partiality.size() > i && partiality[i] >= 0.0f)
partiality_val = partiality[i];
float delta_phi_val = NAN;
if (phi.size() > i)
delta_phi_val = phi[i];
float zeta_val = NAN;
if (zeta.size() > i)
zeta_val = zeta[i];
Reflection r{
.h = h.at(i),
.k = k.at(i),
.l = l.at(i),
.delta_phi_deg = delta_phi_val,
.predicted_x = predicted_x.at(i),
.predicted_y = predicted_y.at(i),
.d = d.at(i),
.I = int_sum.at(i),
.bkg = bkg.at(i),
.sigma = int_err.at(i),
.rlp = lp_val,
.partiality = partiality_val,
.zeta = zeta_val
};
message.reflections.emplace_back(r);
}
const std::string master_reflection_group_name = fmt::format("/entry/reflections/image_{:06d}", image_number);
const std::string source_reflection_group_name = fmt::format("/entry/reflections/image_{:06d}", image_id);
if (!ReadReflectionsFromGroup(*master_file, master_reflection_group_name, message.reflections))
ReadReflectionsFromGroup(*source_file, source_reflection_group_name, message.reflections);
if (!message.reflections.empty()) {
CalcISigma(message);
CalcWilsonBFactor(message, !message.b_factor.has_value());
}
return true;
}
@@ -832,6 +940,7 @@ void JFJochHDF5Reader::Close() {
legacy_format_files.clear();
vds_data_mappings.clear();
master_file_directory.clear();
master_filename.clear();
SetStartMessage({});
}
@@ -892,7 +1001,165 @@ CompressedImage JFJochHDF5Reader::ReadCalibration(std::vector<uint8_t> &tmp, con
dataset.ReadVectorToU8(tmp, start, {dim[0], dim[1]});
algorithm = CompressionAlgorithm::NO_COMPRESSION;
return {tmp, dim[1], dim[0],
CalcImageMode(datatype.GetElemSize(), datatype.IsFloat(), datatype.IsSigned()),
algorithm};
return {
tmp, dim[1], dim[0],
CalcImageMode(datatype.GetElemSize(), datatype.IsFloat(), datatype.IsSigned()),
algorithm
};
}
void AppendOrExtendSourceMapping(std::vector<HDF5DataSourceMessage> &ret,
const std::string &filename,
const std::string &dataset,
uint64_t source_first_image,
uint64_t virtual_first_image,
uint64_t image_count) {
if (image_count == 0)
return;
if (!ret.empty()) {
auto &last = ret.back();
if (last.filename == filename
&& last.dataset == dataset
&& last.source_first_image + last.image_count == source_first_image
&& last.virtual_first_image + last.image_count == virtual_first_image) {
last.image_count += image_count;
return;
}
}
ret.push_back(HDF5DataSourceMessage{
.filename = filename,
.dataset = dataset,
.source_first_image = source_first_image,
.virtual_first_image = virtual_first_image,
.image_count = image_count
});
};
std::vector<HDF5DataSourceMessage> JFJochHDF5Reader::GetHDF5DataSource(
uint64_t first_image,
std::optional<uint64_t> image_count
) const {
std::unique_lock ul(hdf5_mutex);
if (!master_file)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Cannot generate HDF5 source mapping if file not loaded");
if (first_image > number_of_images)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"First image outside dataset range");
const uint64_t requested_count = image_count.value_or(number_of_images - first_image);
if (first_image + requested_count > number_of_images)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Requested image range outside dataset range");
std::vector<HDF5DataSourceMessage> ret;
if (requested_count == 0)
return ret;
// Integrated / contiguous source: link directly to original master file.
if (format == FileWriterFormat::NXmxVDS && data_layout != HDF5DataSetLayout::VIRTUAL) {
AppendOrExtendSourceMapping(ret,
master_filename,
"/entry/data/data",
first_image,
0,
requested_count);
return ret;
}
// VDS source: expand VDS mappings to original source files, not to the VDS master.
if (format == FileWriterFormat::NXmxVDS && data_layout == HDF5DataSetLayout::VIRTUAL) {
for (uint64_t local_image = 0; local_image < requested_count; ++local_image) {
const hsize_t virtual_image = first_image + local_image;
bool found = false;
for (const auto &mapping: vds_data_mappings) {
if (!mapping.ContainsVirtualImage(virtual_image))
continue;
const uint64_t source_image = mapping.SourceImage(virtual_image);
const std::string filename = ResolveRelativeToMaster(master_file_directory, mapping.filename);
const std::string dataset = mapping.dataset.empty() ? "/entry/data/data" : mapping.dataset;
AppendOrExtendSourceMapping(ret,
filename,
dataset,
source_image,
local_image,
1);
found = true;
break;
}
if (!found)
throw JFJochException(JFJochExceptionCategory::HDF5,
"Image not covered by /entry/data/data VDS mappings");
}
return ret;
}
// Legacy source: link directly to linked data files.
if (format == FileWriterFormat::NXmxLegacy) {
if (images_per_file == 0)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Cannot generate HDF5 source mapping: images_per_file is zero");
for (uint64_t local_image = 0; local_image < requested_count; ++local_image) {
const uint64_t source_global_image = first_image + local_image;
const uint64_t file_id = source_global_image / images_per_file;
const uint64_t source_image = source_global_image % images_per_file;
if (file_id >= legacy_format_files.size())
throw JFJochException(JFJochExceptionCategory::HDF5,
"Legacy image source file missing");
AppendOrExtendSourceMapping(ret,
legacy_format_files.at(file_id),
"/entry/data/data",
source_image,
local_image,
1);
}
return ret;
}
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Unsupported HDF5 file layout for source mapping");
}
std::vector<std::vector<Reflection> > JFJochHDF5Reader::ReadReflections(size_t start_image, std::optional<size_t> end_image) const
{
std::unique_lock ul(hdf5_mutex);
if (start_image >= number_of_images)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"start_image must be less than number_of_images");
if (end_image.has_value() && end_image.value() < start_image)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"end_image must be greater than start_image if provided");
int end_image_val = end_image.value_or(number_of_images);
if (end_image_val > number_of_images)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"end_image_val must be less than or equal to number_of_images");
if (!master_file)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Cannot read all reflections if file not loaded");
std::vector<std::vector<Reflection> > ret(end_image_val - start_image);
for (int i = 0; i < end_image_val - start_image; ++i)
ReadReflectionsFromGroup(*master_file,
fmt::format("/entry/reflections/image_{:06d}", start_image + i),
ret[i]);
return ret;
}
+9
View File
@@ -16,6 +16,7 @@ class JFJochHDF5Reader : public JFJochReader {
std::vector<std::string> legacy_format_files;
std::vector<HDF5VirtualDatasetMapping> vds_data_mappings;
std::string master_file_directory;
std::string master_filename;
size_t images_per_file = 1;
size_t number_of_images = 0;
@@ -43,8 +44,16 @@ public:
void ReadFile(const std::string& filename);
uint64_t GetNumberOfImages() const override;
void Close() override;
std::vector<HDF5DataSourceMessage> GetHDF5DataSource(
uint64_t first_image = 0,
std::optional<uint64_t> image_count = {}
) const;
std::vector<std::vector<Reflection>> ReadReflections(size_t start_image = 0, std::optional<size_t> end_image = {}) const;
CompressedImage ReadCalibration(std::vector<uint8_t> &tmp, const std::string &name) const;
std::shared_ptr<JFJochReaderRawImage> GetRawImage(int64_t image_number) override;
+1
View File
@@ -182,6 +182,7 @@ std::shared_ptr<JFJochReaderDataset> JFJochHttpReader::UpdateDataset_i() {
dataset->b_factor = GetPlot_i("b_factor");
dataset->resolution_estimate = GetPlot_i("resolution_estimate");
dataset->efficiency = GetPlot_i("image_collection_efficiency");
dataset->integrated_reflections = GetPlot_i("integrated_reflections");
if (msg->start_message->goniometer)
dataset->experiment.Goniometer(msg->start_message->goniometer);
+1
View File
@@ -40,6 +40,7 @@ struct JFJochReaderDataset {
std::vector<float> profile_radius;
std::vector<float> mosaicity_deg;
std::vector<float> b_factor;
std::vector<float> integrated_reflections;
std::vector<float> scale_factor;
+2
View File
@@ -175,6 +175,8 @@ void JFJochReceiver::SendEndMessage() {
for (int i = 0; i < adu_histogram_module.size(); i++)
message.adu_histogram["module" + std::to_string(i)] = adu_histogram_module[i]->GetHistogram();
scan_result.FillEndMessage(message);
if (push_images_to_writer) {
if (!image_pusher.EndDataCollection(message))
logger.Error("End message not sent via ZeroMQ (time-out)");
+3 -1
View File
@@ -468,7 +468,9 @@ TEST_CASE("HDF5Writer", "[HDF5][Full]") {
DiffractionExperiment x(DetJF4M());
std::vector<SpotToSave> spots;
x.FilePrefix("test02_1p10").ImagesPerTrigger(5).ImagesPerFile(2).Compression(CompressionAlgorithm::NO_COMPRESSION);
x.FilePrefix("test02_1p10").ImagesPerTrigger(5).ImagesPerFile(2).Compression(CompressionAlgorithm::NO_COMPRESSION)
.OverwriteExistingFiles(true);
StartMessage start_message;
x.FillMessage(start_message);
+447 -82
View File
@@ -4,6 +4,7 @@
#include <catch2/catch_all.hpp>
#include "../common/DiffractionExperiment.h"
#include "../common/ScanResultGenerator.h"
#include "../writer/FileWriter.h"
#include "../reader/JFJochHDF5Reader.h"
#include "../compression/JFJochCompressor.h"
@@ -39,13 +40,13 @@ TEST_CASE("HDF5DataType_ElemType","[HDF5]") {
TEST_CASE("JFJochReader_MasterFile", "[HDF5][Full]") {
DiffractionExperiment x(DetJF(1));
x.FilePrefix("test08").ImagesPerTrigger(950).OverwriteExistingFiles(true);
x.BeamX_pxl(100).BeamY_pxl(200).DetectorDistance_mm(150)
.IncidentEnergy_keV(WVL_1A_IN_KEV)
.FrameTime(std::chrono::microseconds(500), std::chrono::microseconds(10))
.SetUnitCell(UnitCell{.a= 10, .b= 20, .c= 30, .alpha= 90, .beta= 101, .gamma = 90});
RegisterHDF5Filter();
{
StartMessage start_message;
x.FillMessage(start_message);
@@ -210,6 +211,8 @@ TEST_CASE("JFJochReader_PixelMask", "[HDF5][Full]") {
pixel_mask[x.GetPixelsNum() - 1] = 4;
pixel_mask[0] = 256;
ScanResultGenerator generator(x);
std::vector<uint16_t> image(x.GetPixelsNum(), 0);
{
StartMessage start_message;
@@ -223,9 +226,11 @@ TEST_CASE("JFJochReader_PixelMask", "[HDF5][Full]") {
message.number = 0;
REQUIRE_NOTHROW(file_set.WriteHDF5(message));
REQUIRE_NOTHROW(generator.Add(message));
EndMessage end_message;
end_message.max_image_number = 1;
generator.FillEndMessage(end_message);
file_set.WriteHDF5(end_message);
file_set.Finalize();
@@ -263,6 +268,7 @@ TEST_CASE("JFJochReader_Goniometer", "[HDF5][Full]") {
RegisterHDF5Filter();
ScanResultGenerator generator(x);
std::vector<uint16_t> image(x.GetPixelsNum(), 0);
{
StartMessage start_message;
@@ -276,10 +282,12 @@ TEST_CASE("JFJochReader_Goniometer", "[HDF5][Full]") {
message.number = i;
REQUIRE_NOTHROW(file_set.WriteHDF5(message));
REQUIRE_NOTHROW(generator.Add(message));
}
EndMessage end_message;
end_message.max_image_number = 5;
generator.FillEndMessage(end_message);
file_set.WriteHDF5(end_message);
file_set.Finalize();
@@ -320,6 +328,7 @@ TEST_CASE("JFJochReader_GridScan", "[HDF5][Full]") {
x.ImportDatasetSettings(d);
RegisterHDF5Filter();
ScanResultGenerator generator(x);
std::vector<uint16_t> image(x.GetPixelsNum(), 0);
{
@@ -334,10 +343,12 @@ TEST_CASE("JFJochReader_GridScan", "[HDF5][Full]") {
message.number = i;
REQUIRE_NOTHROW(file_set.WriteHDF5(message));
generator.Add(message);
}
EndMessage end_message;
end_message.max_image_number = 5;
generator.FillEndMessage(end_message);
file_set.WriteHDF5(end_message);
file_set.Finalize();
@@ -378,6 +389,8 @@ TEST_CASE("JFJochReader_DataI16", "[HDF5][Full]") {
image[2] = 456;
image[3] = -3456;
ScanResultGenerator generator(x);
RegisterHDF5Filter();
{
StartMessage start_message;
@@ -395,12 +408,13 @@ TEST_CASE("JFJochReader_DataI16", "[HDF5][Full]") {
message.bkg_estimate = i * 345.6;
message.number = i;
message.profile_radius = 123.09;
generator.Add(message);
REQUIRE_NOTHROW(file_set.WriteHDF5(message));
}
EndMessage end_message;
end_message.max_image_number = x.GetImageNum();
generator.FillEndMessage(end_message);
file_set.WriteHDF5(end_message);
file_set.Finalize();
}
@@ -529,6 +543,8 @@ TEST_CASE("JFJochReader_DataU16", "[HDF5][Full]") {
image[1] = INT16_MAX;
image[2] = 456;
ScanResultGenerator generator(x);
RegisterHDF5Filter();
{
StartMessage start_message;
@@ -547,10 +563,11 @@ TEST_CASE("JFJochReader_DataU16", "[HDF5][Full]") {
message.number = i;
REQUIRE_NOTHROW(file_set.WriteHDF5(message));
generator.Add(message);
}
EndMessage end_message;
end_message.max_image_number = x.GetImageNum();
generator.FillEndMessage(end_message);
file_set.WriteHDF5(end_message);
file_set.Finalize();
}
@@ -597,6 +614,8 @@ TEST_CASE("JFJochReader_DataI32", "[HDF5][Full]") {
image[1] = INT32_MIN;
image[2] = 456;
ScanResultGenerator generator(x);
RegisterHDF5Filter();
{
StartMessage start_message;
@@ -613,10 +632,11 @@ TEST_CASE("JFJochReader_DataI32", "[HDF5][Full]") {
message.number = i;
REQUIRE_NOTHROW(file_set.WriteHDF5(message));
generator.Add(message);
}
EndMessage end_message;
end_message.max_image_number = x.GetImageNum();
generator.FillEndMessage(end_message);
file_set.WriteHDF5(end_message);
file_set.Finalize();
}
@@ -663,6 +683,8 @@ TEST_CASE("JFJochReader_DataU32", "[HDF5][Full]") {
image[3] = INT32_MAX;
image[4] = INT32_MAX - 1;
ScanResultGenerator generator(x);
RegisterHDF5Filter();
{
StartMessage start_message;
@@ -678,11 +700,12 @@ TEST_CASE("JFJochReader_DataU32", "[HDF5][Full]") {
message.spots = spots;
message.number = i;
generator.Add(message);
REQUIRE_NOTHROW(file_set.WriteHDF5(message));
}
EndMessage end_message;
end_message.max_image_number = x.GetImageNum();
generator.FillEndMessage(end_message);
file_set.WriteHDF5(end_message);
file_set.Finalize();
}
@@ -731,6 +754,8 @@ TEST_CASE("JFJochReader_Summation", "[HDF5][Full]") {
image_3[0] = INT16_MAX;
image_2[1] = INT16_MIN;
ScanResultGenerator generator(x);
RegisterHDF5Filter();
{
StartMessage start_message;
@@ -745,17 +770,21 @@ TEST_CASE("JFJochReader_Summation", "[HDF5][Full]") {
message.image = CompressedImage(image_1, x.GetXPixelsNum(), x.GetYPixelsNum());
message.number = 0;
REQUIRE_NOTHROW(file_set.WriteHDF5(message));
generator.Add(message);
message.image = CompressedImage(image_2, x.GetXPixelsNum(), x.GetYPixelsNum());
message.number = 1;
REQUIRE_NOTHROW(file_set.WriteHDF5(message));
generator.Add(message);
message.image = CompressedImage(image_3, x.GetXPixelsNum(), x.GetYPixelsNum());
message.number = 2;
REQUIRE_NOTHROW(file_set.WriteHDF5(message));
generator.Add(message);
EndMessage end_message;
end_message.max_image_number = x.GetImageNum();
generator.FillEndMessage(end_message);
file_set.WriteHDF5(end_message);
file_set.Finalize();
}
@@ -800,6 +829,8 @@ TEST_CASE("JFJochReader_Summation_5", "[HDF5][Full]") {
image_3[0] = INT16_MAX;
image_2[1] = INT16_MIN;
ScanResultGenerator generator(x);
RegisterHDF5Filter();
{
StartMessage start_message;
@@ -814,25 +845,31 @@ TEST_CASE("JFJochReader_Summation_5", "[HDF5][Full]") {
message.image = CompressedImage(image_1, x.GetXPixelsNum(), x.GetYPixelsNum());
message.number = 0;
REQUIRE_NOTHROW(file_set.WriteHDF5(message));
generator.Add(message);
message.image = CompressedImage(image_2, x.GetXPixelsNum(), x.GetYPixelsNum());
message.number = 1;
REQUIRE_NOTHROW(file_set.WriteHDF5(message));
generator.Add(message);
message.image = CompressedImage(image_3, x.GetXPixelsNum(), x.GetYPixelsNum());
message.number = 2;
REQUIRE_NOTHROW(file_set.WriteHDF5(message));
generator.Add(message);
message.image = CompressedImage(image_4, x.GetXPixelsNum(), x.GetYPixelsNum());
message.number = 3;
REQUIRE_NOTHROW(file_set.WriteHDF5(message));
generator.Add(message);
message.image = CompressedImage(image_5, x.GetXPixelsNum(), x.GetYPixelsNum());
message.number = 4;
REQUIRE_NOTHROW(file_set.WriteHDF5(message));
generator.Add(message);
EndMessage end_message;
end_message.max_image_number = x.GetImageNum();
generator.FillEndMessage(end_message);
file_set.WriteHDF5(end_message);
file_set.Finalize();
}
@@ -860,82 +897,6 @@ TEST_CASE("JFJochReader_Summation_5", "[HDF5][Full]") {
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
}
TEST_CASE("JFJochReader_ROI", "[HDF5][Full]") {
DiffractionExperiment x(DetJF(1));
x.FilePrefix("test25").ImagesPerTrigger(4).OverwriteExistingFiles(true);
x.BitDepthImage(16).ImagesPerFile(1).SetFileWriterFormat(FileWriterFormat::NXmxVDS).PixelSigned(false);
x.Compression(CompressionAlgorithm::NO_COMPRESSION);
x.ROI().SetROI(ROIDefinition{
.boxes = {ROIBox("beam", 100, 120, 20, 30)},
.circles = {ROICircle("roi1", 500, 800, 10)}
});
std::vector<uint16_t> image(x.GetPixelsNum());
RegisterHDF5Filter();
{
StartMessage start_message;
x.FillMessage(start_message);
FileWriter file_set(start_message);
for (int i = 0; i < x.GetImageNum(); i++) {
std::vector<SpotToSave> spots;
DataMessage message{};
message.image = CompressedImage(image, x.GetXPixelsNum(), x.GetYPixelsNum());
message.spots = spots;
message.number = i;
message.roi["beam"] = ROIMessage{
.sum = 12 + i,
.sum_square = (uint64_t) 123 * i,
.max_count = 123 - i,
.pixels = (uint64_t) 189 + i
};
message.roi["roi1"] = ROIMessage{
.sum = 25 + i,
.sum_square = (uint64_t) 15 * i,
.max_count = 67 - i,
.pixels = (uint64_t) 95 + i
};
REQUIRE_NOTHROW(file_set.WriteHDF5(message));
}
EndMessage end_message;
end_message.max_image_number = x.GetImageNum();
file_set.WriteHDF5(end_message);
file_set.Finalize();
}
{
JFJochHDF5Reader reader;
REQUIRE_NOTHROW(reader.ReadFile("test25_master.h5"));
auto dataset = reader.GetDataset();
CHECK(dataset->experiment.GetImageNum() == 4);
CHECK(dataset->roi.size() == 2);
REQUIRE(dataset->roi_max.size() == 2);
int index = 0;
if (dataset->roi[1] == "beam")
index = 1;
REQUIRE(dataset->roi_max[index].size() == 4);
CHECK(dataset->roi_max[index][2] == 123 - 2);
CHECK(dataset->roi_sum_sq[index][0] == 0);
CHECK(dataset->roi_sum_sq[index][1] == 123);
CHECK(dataset->roi_npixel[index][3] == 189 + 3);
CHECK(dataset->roi_sum[1 - index][3] == 25 + 3);
}
remove("test25_master.h5");
remove("test25_data_000001.h5");
remove("test25_data_000002.h5");
remove("test25_data_000003.h5");
remove("test25_data_000004.h5");
// No leftover HDF5 objects
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
}
TEST_CASE("JFJochReader_Azint", "[HDF5][Full]") {
DiffractionExperiment x(DetJF(1));
@@ -1669,5 +1630,409 @@ TEST_CASE("JFJochReader_GetRawImage_Integrated", "[HDF5][Full]") {
}
remove("test_read_raw_image_master.h5");
// No leftover HDF5 objects
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
}
TEST_CASE("JFJochReader_HDF5DataSource_Integrated", "[HDF5][Full]") {
DiffractionExperiment x(DetJF(1));
x.FilePrefix("source_integrated").ImagesPerTrigger(5).OverwriteExistingFiles(true);
x.BitDepthImage(16).SetFileWriterFormat(FileWriterFormat::NXmxIntegrated).PixelSigned(true);
x.Compression(CompressionAlgorithm::NO_COMPRESSION);
std::vector<int16_t> image(x.GetPixelsNum(), 17);
RegisterHDF5Filter();
{
StartMessage start_message;
x.FillMessage(start_message);
FileWriter writer(start_message);
for (int i = 0; i < x.GetImageNum(); i++) {
image[5678] = static_cast<int16_t>(100 + i);
DataMessage message{};
message.image = CompressedImage(image, x.GetXPixelsNum(), x.GetYPixelsNum());
message.number = i;
REQUIRE_NOTHROW(writer.WriteHDF5(message));
}
EndMessage end_message;
end_message.max_image_number = x.GetImageNum();
writer.WriteHDF5(end_message);
writer.Finalize();
}
{
JFJochHDF5Reader reader;
REQUIRE_NOTHROW(reader.ReadFile("source_integrated_master.h5"));
auto source = reader.GetHDF5DataSource(1, 3);
REQUIRE(source.size() == 1);
CHECK(source[0].filename == "source_integrated_master.h5");
CHECK(source[0].dataset == "/entry/data/data");
CHECK(source[0].source_first_image == 1);
CHECK(source[0].virtual_first_image == 0);
CHECK(source[0].image_count == 3);
}
remove("source_integrated_master.h5");
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
}
TEST_CASE("JFJochReader_HDF5DataSource_VDS", "[HDF5][Full]") {
DiffractionExperiment x(DetJF(1));
x.FilePrefix("source_vds_mapping").ImagesPerTrigger(5).ImagesPerFile(2).OverwriteExistingFiles(true);
x.BitDepthImage(16).SetFileWriterFormat(FileWriterFormat::NXmxVDS).PixelSigned(true);
x.Compression(CompressionAlgorithm::NO_COMPRESSION);
std::vector<int16_t> image(x.GetPixelsNum(), 21);
RegisterHDF5Filter();
{
StartMessage start_message;
x.FillMessage(start_message);
FileWriter writer(start_message);
for (int i = 0; i < x.GetImageNum(); i++) {
image[5678] = static_cast<int16_t>(200 + i);
DataMessage message{};
message.image = CompressedImage(image, x.GetXPixelsNum(), x.GetYPixelsNum());
message.number = i;
REQUIRE_NOTHROW(writer.WriteHDF5(message));
}
EndMessage end_message;
end_message.max_image_number = x.GetImageNum();
writer.WriteHDF5(end_message);
writer.Finalize();
}
{
JFJochHDF5Reader reader;
REQUIRE_NOTHROW(reader.ReadFile("source_vds_mapping_master.h5"));
// Range crosses file boundary:
// global images 1,2,3 map to:
// data_000001 image 1
// data_000002 images 0,1
auto source = reader.GetHDF5DataSource(1, 3);
REQUIRE(source.size() == 2);
CHECK(source[0].filename == "source_vds_mapping_data_000001.h5");
CHECK(source[0].dataset == "/entry/data/data");
CHECK(source[0].source_first_image == 1);
CHECK(source[0].virtual_first_image == 0);
CHECK(source[0].image_count == 1);
CHECK(source[1].filename == "source_vds_mapping_data_000002.h5");
CHECK(source[1].dataset == "/entry/data/data");
CHECK(source[1].source_first_image == 0);
CHECK(source[1].virtual_first_image == 1);
CHECK(source[1].image_count == 2);
}
remove("source_vds_mapping_master.h5");
remove("source_vds_mapping_data_000001.h5");
remove("source_vds_mapping_data_000002.h5");
remove("source_vds_mapping_data_000003.h5");
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
}
TEST_CASE("JFJochReader_HDF5DataSource_Legacy", "[HDF5][Full]") {
DiffractionExperiment x(DetJF(1));
x.FilePrefix("source_legacy_mapping").ImagesPerTrigger(5).ImagesPerFile(2).OverwriteExistingFiles(true);
x.BitDepthImage(16).SetFileWriterFormat(FileWriterFormat::NXmxLegacy).PixelSigned(true);
x.Compression(CompressionAlgorithm::NO_COMPRESSION);
std::vector<int16_t> image(x.GetPixelsNum(), 31);
RegisterHDF5Filter();
{
StartMessage start_message;
x.FillMessage(start_message);
FileWriter writer(start_message);
for (int i = 0; i < x.GetImageNum(); i++) {
image[5678] = static_cast<int16_t>(300 + i);
DataMessage message{};
message.image = CompressedImage(image, x.GetXPixelsNum(), x.GetYPixelsNum());
message.number = i;
REQUIRE_NOTHROW(writer.WriteHDF5(message));
}
EndMessage end_message;
end_message.max_image_number = x.GetImageNum();
writer.WriteHDF5(end_message);
writer.Finalize();
}
{
JFJochHDF5Reader reader;
REQUIRE_NOTHROW(reader.ReadFile("source_legacy_mapping_master.h5"));
auto source = reader.GetHDF5DataSource(1, 3);
REQUIRE(source.size() == 2);
CHECK(source[0].filename == "source_legacy_mapping_data_000001.h5");
CHECK(source[0].dataset == "/entry/data/data");
CHECK(source[0].source_first_image == 1);
CHECK(source[0].virtual_first_image == 0);
CHECK(source[0].image_count == 1);
CHECK(source[1].filename == "source_legacy_mapping_data_000002.h5");
CHECK(source[1].dataset == "/entry/data/data");
CHECK(source[1].source_first_image == 0);
CHECK(source[1].virtual_first_image == 1);
CHECK(source[1].image_count == 2);
}
remove("source_legacy_mapping_master.h5");
remove("source_legacy_mapping_data_000001.h5");
remove("source_legacy_mapping_data_000002.h5");
remove("source_legacy_mapping_data_000003.h5");
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
}
TEST_CASE("JFJochReader_ProcessingHDF5_FromVDS_MapsToDataFiles", "[HDF5][Full]") {
DiffractionExperiment x(DetJF(1));
x.FilePrefix("proc_source_vds").ImagesPerTrigger(5).ImagesPerFile(2).OverwriteExistingFiles(true);
x.BitDepthImage(16).SetFileWriterFormat(FileWriterFormat::NXmxVDS).PixelSigned(true);
x.Compression(CompressionAlgorithm::NO_COMPRESSION);
std::vector<int16_t> image(x.GetPixelsNum(), 51);
RegisterHDF5Filter();
{
StartMessage start_message;
x.FillMessage(start_message);
FileWriter writer(start_message);
for (int i = 0; i < x.GetImageNum(); i++) {
image[5678] = static_cast<int16_t>(500 + i);
DataMessage message{};
message.image = CompressedImage(image, x.GetXPixelsNum(), x.GetYPixelsNum());
message.number = i;
REQUIRE_NOTHROW(writer.WriteHDF5(message));
}
EndMessage end_message;
end_message.max_image_number = x.GetImageNum();
writer.WriteHDF5(end_message);
writer.Finalize();
}
std::vector<HDF5DataSourceMessage> source_data;
{
JFJochHDF5Reader reader;
REQUIRE_NOTHROW(reader.ReadFile("proc_source_vds_master.h5"));
source_data = reader.GetHDF5DataSource(1, 3);
REQUIRE(source_data.size() == 2);
CHECK(source_data[0].filename == "proc_source_vds_data_000001.h5");
CHECK(source_data[1].filename == "proc_source_vds_data_000002.h5");
}
{
DiffractionExperiment proc_x = x;
proc_x.FilePrefix("proc_from_vds")
.ImagesPerTrigger(3)
.SetFileWriterFormat(FileWriterFormat::NXmxIntegrated)
.OverwriteExistingFiles(true);
StartMessage start_message;
proc_x.FillMessage(start_message);
start_message.number_of_images = 3;
start_message.images_per_file = 3;
start_message.write_images = false;
start_message.write_master_file = true;
start_message.hdf5_source_data = source_data;
FileWriter writer(start_message);
for (int i = 0; i < 3; i++) {
DataMessage message{};
message.number = i;
message.original_number = i + 1;
message.image = CompressedImage(image, x.GetXPixelsNum(), x.GetYPixelsNum());
message.spot_count = 200 + i;
REQUIRE_NOTHROW(writer.WriteHDF5(message));
}
EndMessage end_message;
end_message.max_image_number = 3;
writer.WriteHDF5(end_message);
writer.Finalize();
}
{
HDF5ReadOnlyFile file("proc_from_vds_master.h5");
HDF5DataSet data(file, "/entry/data/data");
HDF5Dcpl dcpl(data);
REQUIRE(dcpl.GetLayout() == HDF5DataSetLayout::VIRTUAL);
auto mappings = dcpl.GetVirtualMappings();
REQUIRE(mappings.size() == 2);
CHECK(mappings[0].filename == "proc_source_vds_data_000001.h5");
CHECK(mappings[1].filename == "proc_source_vds_data_000002.h5");
}
{
JFJochHDF5Reader reader;
REQUIRE_NOTHROW(reader.ReadFile("proc_from_vds_master.h5"));
auto img0 = reader.LoadImage(0);
REQUIRE(img0);
CHECK(img0->Image()[5678] == 501);
auto img2 = reader.LoadImage(2);
REQUIRE(img2);
CHECK(img2->Image()[5678] == 503);
}
remove("proc_source_vds_master.h5");
remove("proc_source_vds_data_000001.h5");
remove("proc_source_vds_data_000002.h5");
remove("proc_source_vds_data_000003.h5");
remove("proc_from_vds_master.h5");
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
}
TEST_CASE("JFJochReader_ReadReflections_VDS", "[HDF5][Full]") {
DiffractionExperiment x(DetJF(1));
x.FilePrefix("read_reflections_vds")
.ImagesPerTrigger(4)
.ImagesPerFile(1)
.OverwriteExistingFiles(true)
.BitDepthImage(16)
.PixelSigned(true)
.SetFileWriterFormat(FileWriterFormat::NXmxVDS)
.IndexingAlgorithm(IndexingAlgorithmEnum::FFT)
.Compression(CompressionAlgorithm::NO_COMPRESSION);
std::vector<int16_t> image(x.GetPixelsNum(), 0);
RegisterHDF5Filter();
{
StartMessage start_message;
x.FillMessage(start_message);
FileWriter writer(start_message);
ScanResultGenerator scan_result(x);
for (int i = 0; i < x.GetImageNum(); i++) {
DataMessage message{};
message.image = CompressedImage(image, x.GetXPixelsNum(), x.GetYPixelsNum());
message.number = i;
if (i == 1 || i == 3) {
message.integrated_reflections = 2;
message.reflections = {
Reflection{
.h = static_cast<int32_t>(10 + i),
.k = 20,
.l = 30,
.delta_phi_deg = 0.1f,
.predicted_x = 100.0f + static_cast<float>(i),
.predicted_y = 200.0f + static_cast<float>(i),
.d = 1.5f,
.I = 1000.0f + static_cast<float>(i),
.bkg = 10.0f,
.sigma = 2.0f,
.rlp = 1.0f,
.partiality = 0.9f,
.zeta = 0.01f
},
Reflection{
.h = static_cast<int32_t>(40 + i),
.k = 50,
.l = 60,
.delta_phi_deg = 0.2f,
.predicted_x = 300.0f + static_cast<float>(i),
.predicted_y = 400.0f + static_cast<float>(i),
.d = 2.5f,
.I = 2000.0f + static_cast<float>(i),
.bkg = 20.0f,
.sigma = 3.0f,
.rlp = 1.0f,
.partiality = 0.8f,
.zeta = 0.02f
}
};
}
REQUIRE_NOTHROW(writer.WriteHDF5(message));
scan_result.Add(message);
}
EndMessage end_message;
end_message.max_image_number = x.GetImageNum();
scan_result.FillEndMessage(end_message);
writer.WriteHDF5(end_message);
writer.Finalize();
}
{
JFJochHDF5Reader reader;
REQUIRE_NOTHROW(reader.ReadFile("read_reflections_vds_master.h5"));
auto reflections = reader.ReadReflections();
REQUIRE(reflections.size() == 4);
CHECK(reflections[0].empty());
REQUIRE(reflections[1].size() == 2);
CHECK(reflections[1][0].h == 11);
CHECK(reflections[1][0].k == 20);
CHECK(reflections[1][0].l == 30);
CHECK(reflections[1][0].I == Catch::Approx(1001.0f));
CHECK(reflections[1][0].predicted_x == Catch::Approx(101.0f));
CHECK(reflections[1][0].predicted_y == Catch::Approx(201.0f));
CHECK(reflections[2].empty());
REQUIRE(reflections[3].size() == 2);
CHECK(reflections[3][0].h == 13);
CHECK(reflections[3][0].I == Catch::Approx(1003.0f));
CHECK(reflections[3][1].h == 43);
CHECK(reflections[3][1].I == Catch::Approx(2003.0f));
}
{
JFJochHDF5Reader reader;
REQUIRE_NOTHROW(reader.ReadFile("read_reflections_vds_master.h5"));
auto reflections = reader.ReadReflections(1, 4);
REQUIRE(reflections.size() == 3);
REQUIRE(reflections[0].size() == 2); // original image 1
CHECK(reflections[0][0].h == 11);
CHECK(reflections[1].empty()); // original image 2
REQUIRE(reflections[2].size() == 2); // original image 3
CHECK(reflections[2][0].h == 13);
}
remove("read_reflections_vds_master.h5");
remove("read_reflections_vds_data_000001.h5");
remove("read_reflections_vds_data_000002.h5");
remove("read_reflections_vds_data_000003.h5");
remove("read_reflections_vds_data_000004.h5");
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
}
+6 -2
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only
#include <random>
#include <future>
#include <catch2/catch_all.hpp>
#include "../image_pusher/TCPStreamPusher.h"
@@ -425,7 +426,10 @@ TEST_CASE("TCPImageCommTest_AutoPort_StarBind", "[TCP]") {
TCPStreamPusher pusher("tcp://127.0.0.1:*", 1);
TCPImagePuller puller(pusher.GetAddress()[0], 64 * 1024 * 1024);
std::thread receiver([&] {
std::this_thread::sleep_for(std::chrono::seconds(2));
REQUIRE(pusher.GetConnectedWriters() == 1);
std::future<void> receiver = std::async(std::launch::async, [&] {
bool seen_end = false;
uint64_t processed = 0;
@@ -474,7 +478,7 @@ TEST_CASE("TCPImageCommTest_AutoPort_StarBind", "[TCP]") {
}
REQUIRE(pusher.EndDataCollection(end));
receiver.join();
REQUIRE_NOTHROW(receiver.get());
puller.Disconnect();
}
+3 -3
View File
@@ -300,13 +300,13 @@ TEST_CASE("XtalOptimizer_hexagonal") {
xtal_opt.geom.BeamX_pxl(1007).BeamY_pxl(990).DetectorDistance_mm(200)
.PoniRot1_rad(0.01).PoniRot2_rad(0.02);
xtal_opt.crystal_system = gemmi::CrystalSystem::Hexagonal;
xtal_opt.max_time = 30.0;
xtal_opt.max_time = 60.0;
auto start = std::chrono::high_resolution_clock::now();
REQUIRE(XtalOptimizer(xtal_opt, spots));
bool ret = XtalOptimizer(xtal_opt, spots);
auto end = std::chrono::high_resolution_clock::now();
std::cout << "XtalOptimizer took " << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count()
<< " microseconds" << std::endl;
REQUIRE(ret);
auto uc_i = latt_i.GetUnitCell();
auto uc_o = xtal_opt.latt.GetUnitCell();
+42 -31
View File
@@ -37,21 +37,30 @@ void print_usage(Logger &logger) {
logger.Info(" -s<num> Start image number (default: 0)");
logger.Info(" -e<num> End image number (default: all)");
logger.Info(" -v Verbose output");
logger.Info(" -R[num] Rotation indexing (optional: min angular range deg)");
logger.Info(" -F Use FFT indexing algorithm (shortcut for -XFFT)");
logger.Info(" -X<txt> Indexing algorithm (FFBIDX|FFT|FFTW|Auto|None)");
logger.Info(" -x No least-square beam center refinement");
logger.Info(" -W Include images in the written HDF5 file (otherwise only analysis results are saved)");
logger.Info(" -U Unmerged intensities are written to a text file");
logger.Info("");
logger.Info(" Spot finding");
logger.Info(" -T<num> Noise sigma level for spot finding (default: 3.0)");
logger.Info(" -t<num> Photon count threshold for spot finding (default: 10)");
logger.Info(" -d<num> High resolution limit for spot finding (default: 1.5)");
logger.Info(" -c<num> Max spot count (default: 250)");
logger.Info("");
logger.Info(" Indexing");
logger.Info(" -R[num] Rotation indexing (optional: min angular range deg)");
logger.Info(" -X<txt> Indexing algorithm (FFBIDX|FFT|FFTW|Auto|None)");
logger.Info(" -F Use FFT indexing algorithm (shortcut for -XFFT)");
logger.Info(" -S<num> Space group number - used for both indexing and scaling");
logger.Info(" -C<cell> Fix reference unit cell: -C\"a,b,c,alpha,beta,gamma\" (comma-separated, no spaces; quotes optional)");
logger.Info(" -x No least-square beam center refinement");
logger.Info("");
logger.Info(" Scaling and merging");
logger.Info(" -D<num> High resolution limit for scaling/merging (default: 0.0; no limit)");
logger.Info(" -S<num> Space group number");
logger.Info(" -M Scale and merge (refine mosaicity) and write scaled.hkl + image.dat");
logger.Info(" -P<txt> Partiality refinement fixed|rot|unity (default: fixed)");
logger.Info(" -A Anomalous mode (don't merge Friedel pairs)");
logger.Info(" -C<cell> Fix reference unit cell: -C\"a,b,c,alpha,beta,gamma\" (comma-separated, no spaces; quotes optional)");
logger.Info(" -c<num> Max spot count (default: 250)");
logger.Info(" -W HDF5 file with analysis results is written");
logger.Info(" -T<num> Noise sigma level for spot finding (default: 3.0)");
logger.Info(" -t<num> Photon count threshold for spot finding (default: 10)");
}
void trim_in_place(std::string& t) {
@@ -364,15 +373,9 @@ int main(int argc, char **argv) {
start_message.pixel_mask["default"] = pixel_mask.GetMask(experiment);
start_message.max_spot_count = experiment.GetMaxSpotCount();
std::unique_ptr<FileWriter> writer;
try {
if (write_output)
writer = std::make_unique<FileWriter>(start_message);
} catch (const std::exception &e) {
logger.Error("Failed to initialize file writer: {}", e.what());
exit(EXIT_FAILURE);
}
start_message.write_images = write_output;
start_message.file_format = FileWriterFormat::NXmxIntegrated;
start_message.master_suffix = "process";
// 4. Processing Setup
int total_images_in_file = reader.GetNumberOfImages();
@@ -392,7 +395,22 @@ int main(int argc, char **argv) {
std::atomic<int> processed_count = 0;
std::atomic<uint64_t> total_uncompressed_bytes = 0;
std::atomic<uint64_t> max_image_number_sent = 0;
start_message.file_format = FileWriterFormat::NXmxIntegrated;
start_message.write_master_file = true;
start_message.write_images = false;
start_message.number_of_images = images_to_process;
start_message.images_per_file = images_to_process;
start_message.hdf5_source_data = reader.GetHDF5DataSource(start_image, images_to_process);
std::unique_ptr<FileWriter> writer;
try {
if (!output_prefix.empty())
writer = std::make_unique<FileWriter>(start_message);
} catch (const std::exception &e) {
logger.Error("Failed to initialize file writer: {}", e.what());
exit(EXIT_FAILURE);
}
// Mimic JFJochReceiver lattice handling (IndexAndRefine handles the logic per thread,
// but we need a central accumulator or use the pool's functionality if IndexAndRefine wraps it)
@@ -430,7 +448,8 @@ int main(int argc, char **argv) {
DataMessage msg{};
msg.image = img->image;
msg.number = image_idx;
msg.number = current_idx_offset;
msg.original_number = image_idx;
msg.image_collection_efficiency = dataset->efficiency[image_idx];
total_uncompressed_bytes += msg.image.GetUncompressedSize();
@@ -458,13 +477,6 @@ int main(int argc, char **argv) {
if (writer)
writer->Write(msg);
// Update max sent tracking
uint64_t current_max = max_image_number_sent.load();
while (static_cast<uint64_t>(msg.number) > current_max) {
if (max_image_number_sent.compare_exchange_weak(current_max, static_cast<uint64_t>(msg.number)))
break;
}
finished_count.fetch_add(1);
// Progress log
@@ -510,7 +522,7 @@ int main(int argc, char **argv) {
// 5. Finalize Statistics and Write EndMessage
EndMessage end_msg;
end_msg.max_image_number = max_image_number_sent;
end_msg.max_image_number = images_to_process;
end_msg.images_collected_count = images_to_process;
end_msg.images_sent_to_write_count = images_to_process;
end_msg.end_date = time_UTC(std::chrono::system_clock::now());
@@ -536,7 +548,6 @@ int main(int argc, char **argv) {
logger.Info("Rotation Indexing found lattice");
}
// --- Optional: run scaling (mosaicity refinement) on accumulated reflections ---
// --- Optional: run scaling (mosaicity refinement) on accumulated reflections ---
if (run_scaling) {
logger.Info("Running scaling (mosaicity refinement) ...");
@@ -606,7 +617,7 @@ int main(int argc, char **argv) {
}
if (scale_result) {
end_msg.scale_factor = scale_result->image_scale_g;
end_msg.image_scale_factor = scale_result->image_scale_g;
logger.Info("Scaling completed in {:.2f} s ({} unique reflections)",
scale_time, scale_result->merged.size());
+1 -1
View File
@@ -109,7 +109,7 @@ void JFJochViewerMenu::openSelected() {
this,
"Open File", // Dialog title
"", // Default folder
"HDF5 Master Files (*_master.h5);; HDF5 Files (*.h5);;All Files (*)" // Filter for .h5 files
"HDF5 Master Files (*_master.h5 *_process.h5);; HDF5 Files (*.h5);;All Files (*)" // Filter for .h5 files
);
if (!fileName.isEmpty())
+36 -19
View File
@@ -71,26 +71,34 @@ void FileWriter::WriteHDF5(const DataMessage& msg) {
if (msg.number < 0)
throw JFJochException(JFJochExceptionCategory::ArrayOutOfBounds, "No support for negative images");
const uint64_t file_number = (start_message.images_per_file == 0) ? 0 : msg.number / start_message.images_per_file;
const uint64_t image_number = (start_message.images_per_file == 0) ? msg.number : msg.number % start_message.images_per_file;
if (closed_files.contains(file_number))
return;
if (files.size() <= file_number)
files.resize(file_number + 1);
if (!files[file_number]) {
files[file_number] = std::make_unique<HDF5DataFile>(start_message, file_number);
if (format == FileWriterFormat::NXmxIntegrated && master_file)
files[file_number]->CreateFile(msg, master_file->GetFile());
}
files[file_number]->Write(msg, image_number);
if (files[file_number]->GetNumImages() == start_message.images_per_file) {
CloseFile(file_number);
if (format == FileWriterFormat::NXmxIntegrated && master_file) {
if (files.empty() )
files.resize(1);
if (!files[0]) {
files[0] = std::make_unique<HDF5DataFile>(start_message, 0, HDF5Metadata::MasterFileName(start_message));
files[0]->CreateFile(msg, master_file->GetFile());
}
files[0]->Write(msg, msg.number);
} else {
CloseOldFiles(static_cast<uint64_t>(msg.number));
const uint64_t file_number = (start_message.images_per_file == 0) ? 0 : msg.number / start_message.images_per_file;
const uint64_t image_number = (start_message.images_per_file == 0) ? msg.number : msg.number % start_message.images_per_file;
if (closed_files.contains(file_number))
return;
if (files.size() <= file_number)
files.resize(file_number + 1);
if (!files[file_number])
files[file_number] = std::make_unique<HDF5DataFile>(start_message, file_number,
HDF5Metadata::DataFileName(start_message, file_number));
files[file_number]->Write(msg, image_number);
if (files[file_number]->GetNumImages() == start_message.images_per_file) {
CloseFile(file_number);
} else {
CloseOldFiles(static_cast<uint64_t>(msg.number));
}
}
}
@@ -230,6 +238,15 @@ void FileWriter::WriteHDF5(const CompressedImage &msg) {
void FileWriter::WriteHDF5(const EndMessage &msg) {
if (master_file) {
std::lock_guard<std::mutex> lock(hdf5_mutex);
if (format == FileWriterFormat::NXmxIntegrated) {
try {
CloseFile(0);
} catch (...) {
throw;
}
}
master_file->Finalize(msg);
}
}
+48 -40
View File
@@ -20,9 +20,10 @@
#include "HDF5NXmx.h"
#include "../common/time_utc.h"
HDF5DataFile::HDF5DataFile(const StartMessage &msg, uint64_t in_file_number) {
file_number = in_file_number;
HDF5DataFile::HDF5DataFile(const StartMessage &msg, uint64_t file_number, const std::string &filename) :
filename(filename),
file_number(file_number),
write_images(msg.write_images.value_or(true)) {
if (msg.overwrite.has_value())
overwrite = msg.overwrite.value();
@@ -30,9 +31,14 @@ HDF5DataFile::HDF5DataFile(const StartMessage &msg, uint64_t in_file_number) {
ypixel = 0;
max_image_number = 0;
nimages = 0;
filename = HDF5Metadata::DataFileName(msg, file_number);
image_low = file_number * msg.images_per_file;
images_per_file = msg.images_per_file;
if (msg.file_format == FileWriterFormat::NXmxIntegrated) {
image_low = 0;
images_per_file = msg.number_of_images;
} else {
image_low = file_number * msg.images_per_file;
images_per_file = msg.images_per_file;
}
timestamp.reserve(images_per_file);
exptime.reserve(images_per_file);
@@ -110,7 +116,6 @@ HDF5DataFile::~HDF5DataFile() {
if (data_file) {
try {
data_set.reset();
data_set_image_number.reset();
data_file.reset();
if (manage_file) {
std::error_code ec;
@@ -124,42 +129,44 @@ HDF5DataFile::~HDF5DataFile() {
}
}
void HDF5DataFile::CreateFile(const DataMessage& msg, std::shared_ptr<HDF5File> in_data_file, bool integrated) {
HDF5Dcpl dcpl;
HDF5DataType data_type(msg.image.GetMode());
xpixel = msg.image.GetWidth();
ypixel = msg.image.GetHeight();
dcpl.SetCompression(msg.image.GetCompressionAlgorithm(), JFJochBitShuffleCompressor::DefaultBlockSize);
dcpl.SetChunking( {1, ypixel, xpixel});
H5Pset_fill_time(dcpl.GetID(), H5D_FILL_TIME_NEVER);
H5Pset_alloc_time(dcpl.GetID(), H5D_ALLOC_TIME_INCR);
switch (msg.image.GetMode()) {
case CompressedImageMode::Int8:
dcpl.SetFillValue8(INT8_MIN);
break;
case CompressedImageMode::Int16:
dcpl.SetFillValue16(INT16_MIN);
break;
case CompressedImageMode::Int32:
dcpl.SetFillValue32(INT32_MIN);
break;
default:
break;
}
void HDF5DataFile::CreateFile(const DataMessage& msg, std::shared_ptr<HDF5File> in_data_file) {
data_file = in_data_file;
HDF5Group(*data_file, "/entry").NXClass("NXentry");
HDF5Group(*data_file, "/entry/data").NXClass("NXdata");
if (write_images) {
HDF5Dcpl dcpl;
HDF5DataType data_type(msg.image.GetMode());
xpixel = msg.image.GetWidth();
ypixel = msg.image.GetHeight();
dcpl.SetCompression(msg.image.GetCompressionAlgorithm(), JFJochBitShuffleCompressor::DefaultBlockSize);
dcpl.SetChunking( {1, ypixel, xpixel});
H5Pset_fill_time(dcpl.GetID(), H5D_FILL_TIME_NEVER);
H5Pset_alloc_time(dcpl.GetID(), H5D_ALLOC_TIME_INCR);
switch (msg.image.GetMode()) {
case CompressedImageMode::Int8:
dcpl.SetFillValue8(INT8_MIN);
break;
case CompressedImageMode::Int16:
dcpl.SetFillValue16(INT16_MIN);
break;
case CompressedImageMode::Int32:
dcpl.SetFillValue32(INT32_MIN);
break;
default:
break;
}
HDF5Group(*data_file, "/entry/data").NXClass("NXdata");
HDF5DataSpace data_space({1, ypixel, xpixel}, {H5S_UNLIMITED, ypixel, xpixel});
data_set = std::make_unique<HDF5DataSet>(*data_file, "/entry/data/data", data_type, data_space, dcpl);
data_set->SetExtent({images_per_file, ypixel, xpixel});
}
HDF5DataSpace data_space({1, ypixel, xpixel}, {H5S_UNLIMITED, ypixel, xpixel});
data_set = std::make_unique<HDF5DataSet>(*data_file, "/entry/data/data", data_type, data_space, dcpl);
data_set->SetExtent({images_per_file, ypixel, xpixel});
for (auto &p: plugins)
p->OpenFile(*data_file, msg, images_per_file);
}
@@ -186,7 +193,8 @@ void HDF5DataFile::Write(const DataMessage &msg, uint64_t image_number) {
}
nimages++;
data_set->WriteDirectChunk(msg.image.GetCompressed(), msg.image.GetCompressedSize(), {image_number, 0, 0});
if (data_set)
data_set->WriteDirectChunk(msg.image.GetCompressed(), msg.image.GetCompressedSize(), {image_number, 0, 0});
for (auto &p: plugins)
p->Write(msg, image_number);
+6 -5
View File
@@ -22,12 +22,14 @@ struct HDF5DataFileStatistics {
};
class HDF5DataFile {
std::string filename;
const std::string filename;
const uint64_t file_number;
const bool write_images;
std::string tmp_filename;
std::shared_ptr<HDF5File> data_file = nullptr;
std::unique_ptr<HDF5DataSet> data_set = nullptr;
std::unique_ptr<HDF5DataSet> data_set_image_number = nullptr;
std::vector<std::unique_ptr<HDF5DataFilePlugin>> plugins;
size_t images_per_file;
size_t xpixel;
@@ -46,17 +48,16 @@ class HDF5DataFile {
bool closed = false;
bool overwrite = false;
int64_t file_number;
bool new_file = true;
bool manage_file = false;
public:
HDF5DataFile(const StartMessage &msg, uint64_t file_number);
HDF5DataFile(const StartMessage &msg, uint64_t file_number, const std::string &filename);
~HDF5DataFile();
std::optional<HDF5DataFileStatistics> Close();
void Write(const DataMessage& msg, uint64_t image_number);
size_t GetNumImages() const;
void CreateFile(const DataMessage& msg, std::shared_ptr<HDF5File> data_file, bool integrated = false);
void CreateFile(const DataMessage& msg, std::shared_ptr<HDF5File> data_file);
};
#endif //HDF5DATAFILE_H
-6
View File
@@ -15,8 +15,6 @@ void HDF5DataFilePluginDetector::OpenFile(HDF5File &in_data_file, const DataMess
efficiency.reserve(images_per_file);
packets_received.reserve(images_per_file);
packets_expected.reserve(images_per_file);
pixel_sum.reserve(images_per_file);
processing_time.reserve(images_per_file);
}
void HDF5DataFilePluginDetector::Write(const DataMessage &msg, uint64_t image_number) {
@@ -33,8 +31,6 @@ void HDF5DataFilePluginDetector::Write(const DataMessage &msg, uint64_t image_nu
packets_received[image_number] = msg.packets_received.value();
if (msg.packets_expected.has_value())
packets_expected[image_number] = msg.packets_expected.value();
if (msg.pixel_sum.has_value())
pixel_sum[image_number] = msg.pixel_sum.value();
if (msg.image_collection_efficiency.has_value())
efficiency[image_number] = msg.image_collection_efficiency.value();
@@ -57,7 +53,5 @@ void HDF5DataFilePluginDetector::WriteFinal(HDF5File &data_file) {
data_file.SaveVector(prefix + "/packets_received", packets_received.vec());
if (!packets_expected.empty())
data_file.SaveVector(prefix + "/packets_expected", packets_expected.vec());
if (!pixel_sum.empty())
data_file.SaveVector(prefix + "/pixel_sum", pixel_sum.vec());
data_file.SaveVector(prefix + "/data_collection_efficiency_image", efficiency.vec());
}
-2
View File
@@ -16,8 +16,6 @@ class HDF5DataFilePluginDetector : public HDF5DataFilePlugin {
AutoIncrVector<float> efficiency;
AutoIncrVector<uint64_t> packets_received;
AutoIncrVector<uint64_t> packets_expected;
AutoIncrVector<int64_t> pixel_sum;
AutoIncrVector<float> processing_time{NAN};
public:
HDF5DataFilePluginDetector(const StartMessage& msg);
void OpenFile(HDF5File &data_file, const DataMessage& msg, size_t images_per_file) override;
+5
View File
@@ -8,6 +8,7 @@ void HDF5DataFilePluginImageStats::OpenFile(HDF5File &data_file, const DataMessa
min_value.reserve(images_per_file);
error_pixels.reserve(images_per_file);
saturated_pixels.reserve(images_per_file);
pixel_sum.reserve(images_per_file);
}
void HDF5DataFilePluginImageStats::Write(const DataMessage &msg, uint64_t image_number) {
@@ -19,6 +20,8 @@ void HDF5DataFilePluginImageStats::Write(const DataMessage &msg, uint64_t image_
error_pixels[image_number] = msg.error_pixel_count.value();
if (msg.saturated_pixel_count)
saturated_pixels[image_number] = msg.saturated_pixel_count.value();
if (msg.pixel_sum)
pixel_sum[image_number] = msg.pixel_sum.value();
}
void HDF5DataFilePluginImageStats::WriteFinal(HDF5File &data_file) {
@@ -31,4 +34,6 @@ void HDF5DataFilePluginImageStats::WriteFinal(HDF5File &data_file) {
data_file.SaveVector("/entry/image/error_pixels", error_pixels.vec());
if (!saturated_pixels.empty())
data_file.SaveVector("/entry/image/saturated_pixels", saturated_pixels.vec());
if (!pixel_sum.empty())
data_file.SaveVector("/entry/image/pixel_sum", pixel_sum.vec());
}
+1
View File
@@ -13,6 +13,7 @@ class HDF5DataFilePluginImageStats : public HDF5DataFilePlugin {
AutoIncrVector<int64_t> min_value;
AutoIncrVector<int64_t> error_pixels;
AutoIncrVector<int64_t> saturated_pixels;
AutoIncrVector<int64_t> pixel_sum;
public:
void OpenFile(HDF5File &data_file, const DataMessage& msg, size_t images_per_file) override;
void Write(const DataMessage& msg, uint64_t image_number) override;
+2 -2
View File
@@ -225,9 +225,9 @@ void HDF5DataFilePluginMX::WriteFinal(HDF5File &data_file) {
if (!beam_corr_y.empty())
data_file.SaveVector("/entry/MX/beam_corr_y", beam_corr_y.vec())->Units("pixel");
if (!niggli_class.empty())
data_file.SaveVector("/entry/MX/niggli_class", niggli_class.vec());
data_file.SaveVector("/entry/MX/niggliClass", niggli_class.vec());
if (!bravais_lattice.empty())
data_file.SaveVector("/entry/MX/bravais_lattice", bravais_lattice.vec());
data_file.SaveVector("/entry/MX/bravaisLattice", bravais_lattice.vec());
if (!resolution_estimate.empty())
data_file.SaveVector("/entry/MX/resolutionEstimate", resolution_estimate.vec())->Units("Angstrom");
if (!integrated_reflections.empty())
+151 -80
View File
@@ -11,19 +11,19 @@
#include "../common/time_utc.h"
#include "gemmi/symmetry.hpp"
namespace {
std::string GenFilename(const StartMessage &start) {
return fmt::format("{:s}_master.h5", start.file_prefix);
}
std::string HDF5Metadata::MasterFileName(const StartMessage &start) {
if (start.master_suffix.has_value())
return fmt::format("{:s}_{:s}.h5", start.file_prefix, start.master_suffix.value());
return fmt::format("{:s}_master.h5", start.file_prefix);
}
NXmx::NXmx(const StartMessage &start)
: start_message(start),
filename(GenFilename(start)) {
filename(HDF5Metadata::MasterFileName(start)) {
uint64_t tmp_suffix;
try {
if (!start.arm_date.empty())
tmp_suffix = parse_UTC_to_ms(start.arm_date);
tmp_suffix = parse_UTC_to_ms(start.arm_date);
} catch (...) {
tmp_suffix = std::chrono::system_clock::now().time_since_epoch().count();
}
@@ -35,7 +35,8 @@ NXmx::NXmx(const StartMessage &start)
MakeDirectory(filename);
bool v1_10 = (start.file_format == FileWriterFormat::NXmxVDS);
bool v1_10 = (start.file_format == FileWriterFormat::NXmxVDS)
|| !start.hdf5_source_data.empty();
hdf5_file = std::make_shared<HDF5File>(tmp_filename, v1_10);
hdf5_file->Attr("file_name", filename);
@@ -121,54 +122,12 @@ void NXmx::LinkToData_VDS(const StartMessage &start, const EndMessage &end) {
data_dataset->Attr("image_nr_low", (int32_t) 1)
.Attr("image_nr_high",(int32_t) total_images);
VDS(start,
"/entry/detector/data_collection_efficiency_image",
"/entry/instrument/detector/detectorSpecific/data_collection_efficiency_image",
{total_images},
HDF5DataType(0.0f));
VDS(start,
"/entry/detector/pixel_sum",
"/entry/instrument/detector/detectorSpecific/pixel_sum",
{total_images},
HDF5DataType((int64_t) 0));
HDF5Group(*hdf5_file, "/entry/image").NXClass("NXCollection");
VDS(start,
"/entry/image/max_value",
{total_images},
HDF5DataType((int64_t)0));
VDS(start,
"/entry/image/min_value",
{total_images},
HDF5DataType((int64_t)0));
VDS(start,
"/entry/image/error_pixels",
{total_images},
HDF5DataType((int64_t)0));
VDS(start,
"/entry/image/saturated_pixels",
{total_images},
HDF5DataType((int64_t)0));
if (start.max_spot_count > 0) {
VDS(start, "/entry/MX/peakXPosRaw",{total_images, start.max_spot_count}, HDF5DataType(0.0f));
VDS(start, "/entry/MX/peakYPosRaw",{total_images, start.max_spot_count}, HDF5DataType(0.0f));
VDS(start, "/entry/MX/peakTotalIntensity",{total_images, start.max_spot_count}, HDF5DataType(0.0f));
VDS(start, "/entry/MX/peakIceRingRes", {total_images, start.max_spot_count}, HDF5DataType((uint8_t) 0));
VDS(start, "/entry/MX/nPeaks",{total_images}, HDF5DataType((uint32_t) 0));
VDS(start, "/entry/MX/strongPixels", {total_images}, HDF5DataType((uint32_t) 0));
VDS(start, "/entry/MX/bkgEstimate", {total_images}, HDF5DataType(0.0f));
VDS(start, "/entry/MX/resolutionEstimate",{total_images}, HDF5DataType(0.0f));
VDS(start, "/entry/MX/peakCountUnfiltered",{total_images}, HDF5DataType((uint32_t) 0));
VDS(start, "/entry/MX/peakCountIceRingRes",{total_images}, HDF5DataType((uint32_t) 0));
VDS(start, "/entry/MX/peakCountLowRes",{total_images}, HDF5DataType((uint32_t) 0));
VDS(start, "/entry/MX/peakCountIndexed",{total_images}, HDF5DataType((uint32_t) 0));
VDS(start, "/entry/MX/nPeaks", {total_images}, HDF5DataType((uint32_t) 0));
}
if (start.indexing_algorithm != IndexingAlgorithmEnum::None) {
@@ -177,23 +136,9 @@ void NXmx::LinkToData_VDS(const StartMessage &start, const EndMessage &end) {
VDS(start, "/entry/MX/peakK", {total_images, start.max_spot_count}, HDF5DataType((int32_t) 0));
VDS(start, "/entry/MX/peakL", {total_images, start.max_spot_count}, HDF5DataType((int32_t) 0));
VDS(start, "/entry/MX/peakDistEwaldSphere", {total_images, start.max_spot_count}, HDF5DataType((float) 0));
VDS(start, "/entry/MX/imageIndexed", {total_images}, HDF5DataType((uint8_t) 0));
VDS(start, "/entry/MX/latticeIndexed", {total_images,9}, HDF5DataType((float) 0))->Units("Angstrom");
VDS(start, "/entry/MX/profileRadius", {total_images}, HDF5DataType(0.0f))->Units("Angstrom^-1");
VDS(start, "/entry/MX/bFactor", {total_images}, HDF5DataType(0.0f))->Units("Angstrom^2");
}
if (start.geom_refinement_algorithm != GeomRefinementAlgorithmEnum::None) {
VDS(start, "/entry/detector/beam_center_x",
"/entry/instrument/detector/refined_beam_center_x",
{total_images},
HDF5DataType(0.0f));
VDS(start, "/entry/detector/beam_center_y",
"/entry/instrument/detector/refined_beam_center_y",
{total_images},
HDF5DataType(0.0f));
}
if (!start.az_int_bin_to_q.empty()) {
size_t azimuthal_bins = start.az_int_phi_bin_count.value_or(1);
size_t q_bins = start.az_int_q_bin_count.value_or(1);
@@ -217,20 +162,89 @@ void NXmx::LinkToData_VDS(const StartMessage &start, const EndMessage &end) {
{total_images},
HDF5DataType((uint8_t) 0));
if (!start.rois.empty()) {
HDF5Group(*hdf5_file, "/entry/roi").NXClass("NXcollection");
LinkToReflections_VDS(start, end);
}
}
for (const auto &r: start.rois) {
std::string roi = r.name;
HDF5Group(*hdf5_file, "/entry/roi/" + roi);
VDS(start, "/entry/roi/" + roi + "/max", {total_images}, HDF5DataType((int64_t) 0));
VDS(start, "/entry/roi/" + roi + "/sum", {total_images}, HDF5DataType((int64_t) 0));
VDS(start, "/entry/roi/" + roi + "/sum_sq", {total_images}, HDF5DataType((int64_t) 0));
VDS(start, "/entry/roi/" + roi + "/npixel", {total_images}, HDF5DataType((int64_t) 0));
VDS(start, "/entry/roi/" + roi + "/x", {total_images}, HDF5DataType((float) 0));
VDS(start, "/entry/roi/" + roi + "/y", {total_images}, HDF5DataType((float) 0));
}
}
void NXmx::LinkToData_ProcessingVDS(const StartMessage &start, const EndMessage &end) {
if (start.hdf5_source_data.empty() || end.max_image_number == 0)
return;
const hsize_t total_images = end.max_image_number;
const hsize_t width = start.image_size_x;
const hsize_t height = start.image_size_y;
HDF5Group(*hdf5_file, "/entry/data").NXClass("NXdata");
HDF5DataSpace full_data_space({total_images, height, width});
HDF5Dcpl dcpl;
dcpl.SetChunking({1, height, width});
for (const auto &mapping: start.hdf5_source_data) {
if (mapping.image_count == 0)
continue;
if (mapping.virtual_first_image + mapping.image_count > total_images)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Processing VDS mapping exceeds output image count");
const std::string source_dataset = mapping.dataset.empty()
? "/entry/data/data"
: mapping.dataset;
HDF5DataSpace virtual_data_space({total_images, height, width});
virtual_data_space.SelectHyperslab(
{static_cast<hsize_t>(mapping.virtual_first_image), 0, 0},
{static_cast<hsize_t>(mapping.image_count), height, width}
);
const hsize_t source_extent_images = mapping.source_first_image + mapping.image_count;
HDF5DataSpace source_data_space({source_extent_images, height, width});
source_data_space.SelectHyperslab(
{static_cast<hsize_t>(mapping.source_first_image), 0, 0},
{static_cast<hsize_t>(mapping.image_count), height, width}
);
dcpl.SetVirtual(mapping.filename,
source_dataset,
source_data_space,
virtual_data_space);
}
auto data_dataset = std::make_unique<HDF5DataSet>(
*hdf5_file,
"/entry/data/data",
HDF5DataType(start.bit_depth_image / 8, start.pixel_signed),
full_data_space,
dcpl
);
data_dataset->Attr("image_nr_low", static_cast<int32_t>(1))
.Attr("image_nr_high", static_cast<int32_t>(total_images));
}
void NXmx::LinkToReflections_VDS(const StartMessage &start, const EndMessage &end) {
if (end.integrated_reflections.empty())
return;
HDF5Group(*hdf5_file, "/entry/reflections").NXClass("NXcollection");
for (size_t image = 0; image < end.integrated_reflections.size(); ++image) {
if (end.integrated_reflections[image] <= 0)
continue;
if (start.images_per_file <= 0)
continue;
const uint64_t file_id = image / static_cast<uint64_t>(start.images_per_file);
const uint64_t image_in_file = image % static_cast<uint64_t>(start.images_per_file);
const std::string local_name = fmt::format("/entry/reflections/image_{:06d}", image);
const std::string source_name = fmt::format("/entry/reflections/image_{:06d}", image_in_file);
hdf5_file->ExternalLink(HDF5Metadata::DataFileName(start, file_id),
source_name,
local_name);
}
}
@@ -698,6 +712,19 @@ void NXmx::ADUHistogram(const EndMessage &end) {
}
}
template<class T>
void SaveVectorIfMissing(HDF5Object &object,
const std::string &path,
const std::vector<T> &values,
const std::string &units = "") {
if (values.empty() || object.Exists(path))
return;
auto dataset = object.SaveVector(path, values);
if (!units.empty())
dataset->Units(units);
}
void NXmx::Finalize(const EndMessage &end) {
try {
if (!hdf5_file)
@@ -717,6 +744,7 @@ void NXmx::Finalize(const EndMessage &end) {
Sample(start_message, end);
AzimuthalIntegration(start_message, end);
ADUHistogram(end);
EndResultVectors(end);
switch (start_message.file_format.value_or(FileWriterFormat::NXmxLegacy)) {
case FileWriterFormat::NXmxLegacy:
@@ -726,6 +754,9 @@ void NXmx::Finalize(const EndMessage &end) {
LinkToData_VDS(start_message, end);
break;
case FileWriterFormat::NXmxIntegrated:
if (!start_message.hdf5_source_data.empty())
LinkToData_ProcessingVDS(start_message, end);
break;
default:
break;
}
@@ -744,9 +775,6 @@ void NXmx::Finalize(const EndMessage &end) {
SaveScalar(*hdf5_file, "/entry/MX/bkgEstimateMean", end.bkg_estimate.value());
}
if (!end.scale_factor.empty())
SaveVector(*hdf5_file, "/entry/MX/imageScaleFactor", end.scale_factor);
hdf5_file->Close();
hdf5_file.reset();
} catch (const JFJochException &e) {
@@ -786,3 +814,46 @@ void NXmx::UserData(const StartMessage &start) {
std::shared_ptr<HDF5File> NXmx::GetFile() {
return hdf5_file;
}
void NXmx::EndResultVectors(const EndMessage &end) {
if (!end.data_collection_efficiency.empty()) {
HDF5Group det_specific(*hdf5_file, "/entry/instrument/detector/detectorSpecific");
det_specific.NXClass("NXcollection");
SaveVectorIfMissing(*hdf5_file,
"/entry/instrument/detector/detectorSpecific/data_collection_efficiency_image",
end.data_collection_efficiency);
}
if (!end.max_viable_pixel_value.empty() ||
!end.min_viable_pixel_value.empty() ||
!end.error_pixel_count.empty() ||
!end.saturated_pixel_count.empty() ||
!end.pixel_sum.empty()) {
HDF5Group image_group(*hdf5_file, "/entry/image");
image_group.NXClass("NXcollection");
SaveVectorIfMissing(*hdf5_file, "/entry/image/max_value", end.max_viable_pixel_value);
SaveVectorIfMissing(*hdf5_file, "/entry/image/min_value", end.min_viable_pixel_value);
SaveVectorIfMissing(*hdf5_file, "/entry/image/error_pixels", end.error_pixel_count);
SaveVectorIfMissing(*hdf5_file, "/entry/image/saturated_pixels", end.saturated_pixel_count);
SaveVectorIfMissing(*hdf5_file, "/entry/image/pixel_sum", end.pixel_sum);
}
HDF5Group mx_group(*hdf5_file, "/entry/MX");
mx_group.NXClass("NXcollection");
SaveVectorIfMissing(*hdf5_file, "/entry/MX/peakCountIceRingRes", end.spot_count_ice_ring);
SaveVectorIfMissing(*hdf5_file, "/entry/MX/peakCountLowRes", end.spot_count_low_res);
SaveVectorIfMissing(*hdf5_file, "/entry/MX/peakCountIndexed", end.spot_count_indexed);
SaveVectorIfMissing(*hdf5_file, "/entry/MX/imageIndexed", end.image_indexed);
SaveVectorIfMissing(*hdf5_file, "/entry/MX/bkgEstimate", end.v_bkg_estimate);
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");
SaveVectorIfMissing(*hdf5_file, "/entry/MX/resolutionEstimate", end.resolution_estimate, "Angstrom");
SaveVectorIfMissing(*hdf5_file, "/entry/MX/imageScaleFactor", end.image_scale_factor);
SaveVectorIfMissing(*hdf5_file, "/entry/MX/integratedReflections", end.integrated_reflections);
if (!end.niggli_class.empty())
SaveVectorIfMissing(*hdf5_file, "/entry/MX/niggliClass", end.niggli_class);
}
+7 -4
View File
@@ -1,14 +1,14 @@
// SPDX-FileCopyrightText: 2024 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#ifndef JUNGFRAUJOCH_HDF5NXMX_H
#define JUNGFRAUJOCH_HDF5NXMX_H
#pragma once
#include "../common/JFJochMessages.h"
#include "HDF5Objects.h"
namespace HDF5Metadata {
std::string MasterFileName(const StartMessage &msg);
std::string DataFileName(const StartMessage &msg, int64_t file_number);
}
@@ -22,6 +22,9 @@ class NXmx {
void LinkToData(const StartMessage &start, const EndMessage &end);
void LinkToData_VDS(const StartMessage &start, const EndMessage &end);
void LinkToData_ProcessingVDS(const StartMessage &start, const EndMessage &end);
void LinkToReflections_VDS(const StartMessage &start, const EndMessage &end);
std::unique_ptr<HDF5DataSet> VDS(const StartMessage &start,
const std::string& name,
const std::vector<hsize_t> &dim,
@@ -48,9 +51,11 @@ class NXmx {
void SaveCBORImage(const std::string& hdf5_path, const CompressedImage &image);
void AzimuthalIntegration(const StartMessage &start, const EndMessage &end);
void ADUHistogram(const EndMessage &end);
void EndResultVectors(const EndMessage &end);
void UserData(const StartMessage &start);
void MX(const StartMessage &start);
void Fluorescence(const StartMessage &start);
public:
NXmx(const StartMessage& start);
~NXmx();
@@ -61,5 +66,3 @@ public:
std::shared_ptr<HDF5File> GetFile();
};
#endif //JUNGFRAUJOCH_HDF5NXMX_H