1use std::path::{Path, PathBuf};
2
3use ad_core_rs::attributes::{NDAttrDataType, NDAttrSource, NDAttrValue, NDAttribute};
4use ad_core_rs::codec::{Codec, CodecName};
5use ad_core_rs::error::{ADError, ADResult};
6use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType, NDDimension};
7use ad_core_rs::ndarray_pool::NDArrayPool;
8use ad_core_rs::plugin::file_base::{NDFileMode, NDFileWriter};
9use ad_core_rs::plugin::file_controller::FilePluginController;
10use ad_core_rs::plugin::runtime::{
11 NDPluginProcess, ParamChangeResult, ParamUpdate, PluginParamSnapshot, ProcessResult,
12};
13
14use rust_hdf5::H5File;
15use rust_hdf5::format::messages::datatype::{ByteOrder, DatatypeMessage};
16use rust_hdf5::format::messages::filter::{
17 FILTER_BLOSC, FILTER_BSHUF, FILTER_JPEG, FILTER_SZIP, Filter, FilterPipeline,
18};
19use rust_hdf5::swmr::SwmrFileWriter;
20
21use crate::hdf5_layout::Hdf5Layout;
22
23const COMPRESS_NONE: i32 = 0;
25const COMPRESS_NBIT: i32 = 1;
26const COMPRESS_SZIP: i32 = 2;
27const COMPRESS_ZLIB: i32 = 3;
28const COMPRESS_BLOSC: i32 = 4;
29const COMPRESS_BSHUF: i32 = 5;
30const COMPRESS_LZ4: i32 = 6;
31const COMPRESS_JPEG: i32 = 7;
32
33const SZIP_NN_OPTION_MASK: u32 = 32;
36
37const BLOSC_LZ: i32 = 0;
39const BLOSC_LZ4: i32 = 1;
40const BLOSC_LZ4HC: i32 = 2;
41const BLOSC_SNAPPY: i32 = 3;
42const BLOSC_ZLIB: i32 = 4;
43const BLOSC_ZSTD: i32 = 5;
44
45const MAX_EXTRA_DIMS: usize = 10;
47
48const DTYPE_ATTR: &str = "NDArrayDataType";
51
52const DEFAULT_LAYOUT_XML: &str = r#"
59<hdf5_layout>
60<group name="entry">
61 <attribute name="NX_class" source="constant" value="NXentry" type="string"></attribute>
62 <group name="instrument">
63 <attribute name="NX_class" source="constant" value="NXinstrument" type="string"></attribute>
64 <group name="detector">
65 <attribute name="NX_class" source="constant" value="NXdetector" type="string"></attribute>
66 <dataset name="data" source="detector" det_default="true">
67 <attribute name="signal" source="constant" value="1" type="int"></attribute>
68 </dataset>
69 <group name="NDAttributes">
70 <attribute name="NX_class" source="constant" value="NXcollection" type="string"></attribute>
71 <dataset name="ColorMode" source="ndattribute" ndattribute="ColorMode"></dataset>
72 </group>
73 </group>
74 <group name="NDAttributes" ndattr_default="true">
75 <attribute name="NX_class" source="constant" value="NXcollection" type="string"></attribute>
76 </group>
77 <group name="performance">
78 <dataset name="timestamp"></dataset>
79 </group>
80 </group>
81 <group name="data">
82 <attribute name="NX_class" source="constant" value="NXdata" type="string"></attribute>
83 <hardlink name="data" target="/entry/instrument/detector/data"></hardlink>
84 </group>
85</group>
86</hdf5_layout>
87"#;
88
89#[derive(Clone)]
91struct ChunkConfig {
92 auto: bool,
95 n_row_chunks: usize,
96 n_col_chunks: usize,
97 n_frames_chunks: usize,
98 ndattr_chunk: usize,
101}
102
103impl Default for ChunkConfig {
104 fn default() -> Self {
105 Self {
106 auto: true,
107 n_row_chunks: 0,
108 n_col_chunks: 0,
109 n_frames_chunks: 1,
110 ndattr_chunk: 0,
111 }
112 }
113}
114
115#[derive(Clone, Default)]
117struct ExtraDim {
118 size: usize,
119 name: String,
120}
121
122struct AttributeDataset {
126 name: String,
127 description: String,
132 source: String,
133 source_type: String,
134 data_type: NDAttrDataType,
135 buffer: Vec<u8>,
137 frames: usize,
138}
139
140fn nd_attr_source_type_string(source: &NDAttrSource) -> &'static str {
143 match source {
144 NDAttrSource::Driver => "NDAttrSourceDriver",
145 NDAttrSource::Param { .. } => "NDAttrSourceParam",
146 NDAttrSource::EpicsPV(_) => "NDAttrSourceEPICSPV",
147 NDAttrSource::Function(_) => "NDAttrSourceFunct",
148 NDAttrSource::Constant(_) => "NDAttrSourceConst",
149 NDAttrSource::Undefined => "Undefined",
150 }
151}
152
153impl AttributeDataset {
154 fn new(attr: &NDAttribute) -> Self {
155 Self {
156 name: attr.name.clone(),
157 description: attr.description.clone(),
158 source: attr.source.source_string().to_string(),
159 source_type: nd_attr_source_type_string(&attr.source).to_string(),
160 data_type: attr.value.data_type(),
161 buffer: Vec::new(),
162 frames: 0,
163 }
164 }
165
166 fn element_size(&self) -> usize {
169 match self.data_type {
170 NDAttrDataType::Int8 | NDAttrDataType::UInt8 => 1,
171 NDAttrDataType::Int16 | NDAttrDataType::UInt16 => 2,
172 NDAttrDataType::Int32 | NDAttrDataType::UInt32 | NDAttrDataType::Float32 => 4,
173 NDAttrDataType::Int64 | NDAttrDataType::UInt64 | NDAttrDataType::Float64 => 8,
174 NDAttrDataType::String => MAX_ATTRIBUTE_STRING_SIZE,
175 }
176 }
177
178 fn push(&mut self, value: &NDAttrValue) {
180 let es = self.element_size();
181 let mut bytes = vec![0u8; es];
182 match self.data_type {
183 NDAttrDataType::Int8 => bytes[0] = value.as_i64().unwrap_or(0) as i8 as u8,
184 NDAttrDataType::UInt8 => bytes[0] = value.as_i64().unwrap_or(0) as u8,
185 NDAttrDataType::Int16 => {
186 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0) as i16).to_le_bytes())
187 }
188 NDAttrDataType::UInt16 => {
189 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0) as u16).to_le_bytes())
190 }
191 NDAttrDataType::Int32 => {
192 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0) as i32).to_le_bytes())
193 }
194 NDAttrDataType::UInt32 => {
195 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0) as u32).to_le_bytes())
196 }
197 NDAttrDataType::Int64 => {
198 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0)).to_le_bytes())
199 }
200 NDAttrDataType::UInt64 => {
201 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0) as u64).to_le_bytes())
202 }
203 NDAttrDataType::Float32 => {
204 bytes.copy_from_slice(&(value.as_f64().unwrap_or(0.0) as f32).to_le_bytes())
205 }
206 NDAttrDataType::Float64 => {
207 bytes.copy_from_slice(&(value.as_f64().unwrap_or(0.0)).to_le_bytes())
208 }
209 NDAttrDataType::String => {
210 let s = value.as_string();
211 let src = s.as_bytes();
212 let n = src.len().min(es - 1);
213 bytes[..n].copy_from_slice(&src[..n]);
214 }
215 }
216 self.buffer.extend_from_slice(&bytes);
217 self.frames += 1;
218 }
219}
220
221const MAX_ATTRIBUTE_STRING_SIZE: usize = 256;
224
225#[derive(Clone, Copy)]
236#[repr(transparent)]
237struct FixedStr256([u8; MAX_ATTRIBUTE_STRING_SIZE]);
238
239impl rust_hdf5::types::H5Type for FixedStr256 {
240 fn hdf5_type() -> rust_hdf5::format::messages::datatype::DatatypeMessage {
241 rust_hdf5::format::messages::datatype::DatatypeMessage::fixed_string(
242 MAX_ATTRIBUTE_STRING_SIZE as u32,
243 )
244 }
245
246 fn element_size() -> usize {
247 MAX_ATTRIBUTE_STRING_SIZE
248 }
249}
250
251struct DetectorDataset {
258 ds: rust_hdf5::H5Dataset,
261 frame_count: usize,
263 frame_band: Vec<Vec<u8>>,
266}
267
268enum Hdf5Handle {
270 Standard {
271 file: H5File,
272 detectors: std::collections::HashMap<String, DetectorDataset>,
276 },
277 Swmr {
278 writer: Box<SwmrFileWriter>,
280 ds_index: usize,
281 compression_dropped: bool,
284 grid: Option<SwmrGridLayout>,
289 },
290}
291
292struct SwmrGridLayout {
299 leading: Vec<usize>,
301 frame_dims: Vec<usize>,
303 chunk: Vec<usize>,
305 elem_size: usize,
307}
308
309pub struct Hdf5Writer {
311 current_path: Option<PathBuf>,
312 handle: Option<Hdf5Handle>,
313 frame_count: usize,
318 dataset_name: String,
319 open_data_type: Option<NDDataType>,
321 open_frame_dims: Option<Vec<usize>>,
323 open_codec: Option<Codec>,
328 compression_type: i32,
330 z_compress_level: u32,
331 szip_num_pixels: u32,
332 nbit_precision: u32,
333 nbit_offset: u32,
334 jpeg_quality: u32,
335 blosc_shuffle_type: i32,
336 blosc_compressor: i32,
337 blosc_compress_level: u32,
338 chunk: ChunkConfig,
340 open_mode: NDFileMode,
345 num_capture: usize,
346 n_extra_dims: usize,
347 extra_dims: [ExtraDim; MAX_EXTRA_DIMS],
348 fill_value: f64,
349 dim_att_datasets: bool,
350 swmr_mode: bool,
352 flush_nth_frame: usize,
353 pub swmr_cb_counter: u32,
354 pub store_attributes: bool,
356 pub store_performance: bool,
357 pub total_runtime: f64,
358 pub total_bytes: u64,
359 perf_rows: Vec<[f64; 5]>,
362 perf_prev: Option<std::time::Instant>,
363 perf_first: Option<std::time::Instant>,
364 attr_datasets: Vec<AttributeDataset>,
366 layout_filename: Option<PathBuf>,
368 layout: Option<Hdf5Layout>,
369 pub layout_valid: bool,
370 pub layout_error: String,
371 resolved_dataset_path: String,
376 resolved_ndattr_group: String,
379 resolved_perf_group: String,
381 ndattr_first_values: std::collections::HashMap<String, NDAttrValue>,
387 ndattr_last_values: std::collections::HashMap<String, NDAttrValue>,
388 ndattr_element_names: Vec<String>,
391}
392
393impl Hdf5Writer {
394 pub fn new() -> Self {
395 Self {
396 current_path: None,
397 handle: None,
398 frame_count: 0,
399 dataset_name: "data".to_string(),
400 open_data_type: None,
401 open_frame_dims: None,
402 open_codec: None,
403 compression_type: 0,
404 z_compress_level: 6,
405 szip_num_pixels: 16,
406 nbit_precision: 8,
409 nbit_offset: 0,
410 jpeg_quality: 90,
411 blosc_shuffle_type: 1,
413 blosc_compressor: 0,
414 blosc_compress_level: 5,
415 chunk: ChunkConfig::default(),
416 open_mode: NDFileMode::Single,
417 num_capture: 1,
418 n_extra_dims: 0,
419 extra_dims: Default::default(),
420 fill_value: 0.0,
421 dim_att_datasets: false,
422 swmr_mode: false,
423 flush_nth_frame: 0,
424 swmr_cb_counter: 0,
425 store_attributes: true,
426 store_performance: false,
427 total_runtime: 0.0,
428 total_bytes: 0,
429 perf_rows: Vec::new(),
430 perf_prev: None,
431 perf_first: None,
432 attr_datasets: Vec::new(),
433 layout_filename: None,
434 layout: Some(Self::default_layout()),
436 layout_valid: false,
437 layout_error: String::new(),
438 resolved_dataset_path: "data".to_string(),
439 resolved_ndattr_group: String::new(),
440 resolved_perf_group: String::new(),
441 ndattr_first_values: std::collections::HashMap::new(),
442 ndattr_last_values: std::collections::HashMap::new(),
443 ndattr_element_names: Vec::new(),
444 }
445 }
446
447 pub fn set_dataset_name(&mut self, name: &str) {
448 self.dataset_name = name.to_string();
449 }
450
451 pub fn set_compression_type(&mut self, v: i32) {
452 self.compression_type = v;
453 }
454
455 pub fn set_z_compress_level(&mut self, v: u32) {
456 self.z_compress_level = v;
457 }
458
459 pub fn set_szip_num_pixels(&mut self, v: u32) {
460 self.szip_num_pixels = v;
461 }
462
463 pub fn set_blosc_shuffle_type(&mut self, v: i32) {
464 self.blosc_shuffle_type = v;
465 }
466
467 pub fn set_blosc_compressor(&mut self, v: i32) {
468 self.blosc_compressor = v;
469 }
470
471 pub fn set_blosc_compress_level(&mut self, v: u32) {
472 self.blosc_compress_level = v;
473 }
474
475 pub fn set_nbit_precision(&mut self, v: u32) {
476 self.nbit_precision = v;
477 }
478
479 pub fn set_nbit_offset(&mut self, v: u32) {
480 self.nbit_offset = v;
481 }
482
483 pub fn set_jpeg_quality(&mut self, v: u32) {
484 self.jpeg_quality = v;
485 }
486
487 pub fn set_store_attributes(&mut self, v: bool) {
488 self.store_attributes = v;
489 }
490
491 pub fn set_store_performance(&mut self, v: bool) {
492 self.store_performance = v;
493 }
494
495 pub fn set_swmr_mode(&mut self, v: bool) {
496 self.swmr_mode = v;
497 }
498
499 pub fn set_flush_nth_frame(&mut self, v: usize) {
500 self.flush_nth_frame = v;
501 }
502
503 pub fn set_chunk_size_auto(&mut self, v: bool) {
504 self.chunk.auto = v;
505 }
506
507 pub fn set_n_row_chunks(&mut self, v: usize) {
508 self.chunk.n_row_chunks = v;
509 }
510
511 pub fn set_n_col_chunks(&mut self, v: usize) {
512 self.chunk.n_col_chunks = v;
513 }
514
515 pub fn set_n_frames_chunks(&mut self, v: usize) {
516 self.chunk.n_frames_chunks = v;
517 }
518
519 pub fn set_ndattr_chunk(&mut self, v: usize) {
520 self.chunk.ndattr_chunk = v;
523 }
524
525 pub fn set_n_extra_dims(&mut self, v: usize) {
526 self.n_extra_dims = v.min(MAX_EXTRA_DIMS - 1);
532 }
533
534 pub fn set_extra_dim_size(&mut self, idx: usize, size: usize) {
535 if idx < MAX_EXTRA_DIMS {
536 self.extra_dims[idx].size = size;
537 }
538 }
539
540 pub fn set_extra_dim_name(&mut self, idx: usize, name: &str) {
541 if idx < MAX_EXTRA_DIMS {
542 self.extra_dims[idx].name = name.to_string();
543 }
544 }
545
546 pub fn set_fill_value(&mut self, v: f64) {
547 self.fill_value = v;
548 }
549
550 pub fn set_dim_att_datasets(&mut self, v: bool) {
551 self.dim_att_datasets = v;
552 }
553
554 fn default_layout() -> Hdf5Layout {
558 Hdf5Layout::parse(DEFAULT_LAYOUT_XML).expect("built-in default layout must parse")
559 }
560
561 pub fn set_layout_filename(&mut self, path: &str) -> bool {
564 if path.trim().is_empty() {
565 self.layout_filename = None;
569 self.layout = Some(Self::default_layout());
570 self.layout_valid = false;
571 self.layout_error.clear();
572 return true;
573 }
574 let p = PathBuf::from(path);
575 match Hdf5Layout::from_file(&p) {
576 Ok(layout) => {
577 self.layout_filename = Some(p);
578 self.layout = Some(layout);
579 self.layout_valid = true;
580 self.layout_error.clear();
581 true
582 }
583 Err(e) => {
584 self.layout_filename = Some(p);
585 self.layout = None;
586 self.layout_valid = false;
587 self.layout_error = e.0;
588 false
589 }
590 }
591 }
592
593 pub fn frame_count(&self) -> usize {
594 self.frame_count
595 }
596
597 pub fn flush_swmr(&mut self) {
599 if let Some(Hdf5Handle::Swmr { ref mut writer, .. }) = self.handle {
600 if writer.flush().is_ok() {
601 self.swmr_cb_counter += 1;
602 }
603 }
604 }
605
606 pub fn is_swmr_active(&self) -> bool {
608 matches!(self.handle, Some(Hdf5Handle::Swmr { .. }))
609 }
610
611 pub fn swmr_compression_dropped(&self) -> bool {
613 matches!(
614 self.handle,
615 Some(Hdf5Handle::Swmr {
616 compression_dropped: true,
617 ..
618 })
619 )
620 }
621
622 fn build_pipeline(&self, element_size: usize) -> Option<FilterPipeline> {
624 match self.compression_type {
625 COMPRESS_NONE => None,
626 COMPRESS_ZLIB => Some(FilterPipeline::deflate(self.z_compress_level)),
627 COMPRESS_SZIP => Some(FilterPipeline {
628 filters: vec![Filter {
629 id: FILTER_SZIP,
630 flags: 0,
631 cd_values: vec![SZIP_NN_OPTION_MASK, self.szip_num_pixels],
636 }],
637 }),
638 COMPRESS_LZ4 => Some(FilterPipeline::lz4()),
639 COMPRESS_BSHUF => Some(FilterPipeline {
640 filters: vec![Filter {
644 id: FILTER_BSHUF,
645 flags: 0,
646 cd_values: vec![0, 0, element_size as u32, 0, 2],
647 }],
648 }),
649 COMPRESS_BLOSC => {
650 let compressor_code = match self.blosc_compressor {
651 BLOSC_LZ => 0,
652 BLOSC_LZ4 => 1,
653 BLOSC_LZ4HC => 2,
654 BLOSC_SNAPPY => 3,
655 BLOSC_ZLIB => 4,
656 BLOSC_ZSTD => 5,
657 _ => 0,
658 };
659 Some(FilterPipeline {
660 filters: vec![Filter {
661 id: FILTER_BLOSC,
662 flags: 0,
663 cd_values: vec![
664 2,
665 2,
666 element_size as u32,
667 0,
668 self.blosc_compress_level,
669 self.blosc_shuffle_type as u32,
670 compressor_code,
671 ],
672 }],
673 })
674 }
675 COMPRESS_NBIT => {
676 None
692 }
693 COMPRESS_JPEG => Some(FilterPipeline {
694 filters: vec![Filter {
695 id: FILTER_JPEG,
696 flags: 0,
697 cd_values: vec![self.jpeg_quality],
698 }],
699 }),
700 _ => None,
701 }
702 }
703
704 fn nbit_packing(
728 &self,
729 data_type: NDDataType,
730 chunk_nelmts: usize,
731 ) -> Option<(DatatypeMessage, FilterPipeline)> {
732 if self.compression_type != COMPRESS_NBIT {
733 return None;
734 }
735 let (size, signed) = match data_type {
736 NDDataType::Int8 => (1u32, true),
737 NDDataType::UInt8 => (1, false),
738 NDDataType::Int16 => (2, true),
739 NDDataType::UInt16 => (2, false),
740 NDDataType::Int32 => (4, true),
741 NDDataType::UInt32 => (4, false),
742 NDDataType::Int64 => (8, true),
743 NDDataType::UInt64 => (8, false),
744 NDDataType::Float32 | NDDataType::Float64 => return None,
745 };
746 if self.nbit_precision == 0 || self.nbit_precision > size * 8 {
747 return None;
748 }
749 let dt = DatatypeMessage::FixedPoint {
750 size,
751 byte_order: ByteOrder::LittleEndian,
752 signed,
753 bit_offset: self.nbit_offset as u16,
754 bit_precision: self.nbit_precision as u16,
755 };
756 let pipeline = FilterPipeline::nbit(&dt, chunk_nelmts);
757 Some((dt, pipeline))
758 }
759
760 fn codec_filter_pipeline(&self, codec: &Codec) -> Option<FilterPipeline> {
773 let element_size = codec.original_data_type.element_size() as u32;
774 match codec.name {
775 CodecName::LZ4 => Some(FilterPipeline::lz4()),
776 CodecName::BSLZ4 => Some(FilterPipeline {
777 filters: vec![Filter {
781 id: FILTER_BSHUF,
782 flags: 0,
783 cd_values: vec![0, 0, element_size, 0, 2],
784 }],
785 }),
786 CodecName::Blosc => {
787 Some(FilterPipeline {
791 filters: vec![Filter {
792 id: FILTER_BLOSC,
793 flags: 0,
794 cd_values: vec![
795 2,
796 2,
797 element_size,
798 0,
799 codec.level.max(0) as u32,
800 codec.shuffle.max(0) as u32,
801 codec.compressor.max(0) as u32,
802 ],
803 }],
804 })
805 }
806 CodecName::JPEG => Some(FilterPipeline {
812 filters: vec![Filter {
813 id: FILTER_JPEG,
814 flags: 0,
815 cd_values: vec![self.jpeg_quality],
816 }],
817 }),
818 CodecName::None | CodecName::Zlib | CodecName::LZ4HDF5 => None,
819 }
820 }
821
822 fn frame_chunk_dims(&self, frame_dims: &[usize]) -> Vec<usize> {
829 if frame_dims.len() == 2 {
830 let y = frame_dims[0].max(1);
831 let x = frame_dims[1].max(1);
832 vec![
833 Self::clamp_chunk(self.chunk.n_row_chunks, y, self.chunk.auto),
834 Self::clamp_chunk(self.chunk.n_col_chunks, x, self.chunk.auto),
835 ]
836 } else {
837 frame_dims.iter().map(|&d| d.max(1)).collect()
838 }
839 }
840
841 fn extra_dim_axes(&self) -> Option<Vec<usize>> {
851 if self.n_extra_dims == 0 {
852 return None;
853 }
854 Some(
855 (0..=self.n_extra_dims)
856 .rev()
857 .map(|i| self.extra_dims[i].size.max(1))
858 .collect(),
859 )
860 }
861
862 fn standard_layout(
878 &self,
879 frame_dims: &[usize],
880 ) -> (Vec<usize>, Vec<usize>, Option<Vec<usize>>) {
881 let frame_chunk = self.frame_chunk_dims(frame_dims);
882 match self.extra_dim_axes() {
883 Some(leading) => {
884 let mut shape = leading.clone();
885 shape.extend_from_slice(frame_dims);
886 let mut chunk = vec![1usize; leading.len()];
889 chunk.extend_from_slice(&frame_chunk);
890 (shape, chunk, Some(leading))
891 }
892 None => {
893 let mut shape = vec![1usize];
894 shape.extend_from_slice(frame_dims);
895 let mut chunk = vec![self.chunk.n_frames_chunks.max(1)];
896 chunk.extend_from_slice(&frame_chunk);
897 (shape, chunk, None)
898 }
899 }
900 }
901
902 fn compressed_layout(
912 &self,
913 frame_dims: &[usize],
914 ) -> (Vec<usize>, Vec<usize>, Option<Vec<usize>>) {
915 let frame_chunk: Vec<usize> = frame_dims.iter().map(|&d| d.max(1)).collect();
916 match self.extra_dim_axes() {
917 Some(leading) => {
918 let mut shape = leading.clone();
919 shape.extend_from_slice(frame_dims);
920 let mut chunk = vec![1usize; leading.len()];
921 chunk.extend_from_slice(&frame_chunk);
922 (shape, chunk, Some(leading))
923 }
924 None => {
925 let mut shape = vec![1usize];
926 shape.extend_from_slice(frame_dims);
927 let mut chunk = vec![1usize];
928 chunk.extend_from_slice(&frame_chunk);
929 (shape, chunk, None)
930 }
931 }
932 }
933
934 fn primary_layout(&self, frame_dims: &[usize]) -> (Vec<usize>, Vec<usize>, Option<usize>) {
945 let extra_extent = if self.n_extra_dims > 0 {
946 Some(
947 (0..self.n_extra_dims)
948 .map(|i| self.extra_dims[i].size.max(1))
949 .product::<usize>(),
950 )
951 } else {
952 None
953 };
954
955 let mut shape: Vec<usize> = vec![extra_extent.unwrap_or(1)];
956 shape.extend_from_slice(frame_dims);
957
958 let mut chunk = vec![if extra_extent.is_some() {
959 1
960 } else {
961 self.chunk.n_frames_chunks.max(1)
962 }];
963 chunk.extend_from_slice(&self.frame_chunk_dims(frame_dims));
964 (shape, chunk, extra_extent)
965 }
966
967 fn unravel(mut idx: usize, dims: &[usize]) -> Vec<usize> {
973 let mut coords = vec![0usize; dims.len()];
974 for d in (0..dims.len()).rev() {
975 let s = dims[d].max(1);
976 coords[d] = idx % s;
977 idx /= s;
978 }
979 coords
980 }
981
982 fn clamp_chunk(requested: usize, dim: usize, auto: bool) -> usize {
985 if auto || requested == 0 || requested > dim {
986 dim
987 } else {
988 requested
989 }
990 }
991
992 fn flush_band(
1008 ds: &rust_hdf5::H5Dataset,
1009 leading_coords: &[usize],
1010 frames: &[Vec<u8>],
1011 frame_dims: &[usize],
1012 chunk: &[usize],
1013 elem_size: usize,
1014 ) -> ADResult<()> {
1015 for (coords, buf) in
1016 Self::band_chunk_writes(leading_coords, frames, frame_dims, chunk, elem_size)
1017 {
1018 ds.write_chunk_at(&coords, &buf).map_err(|e| {
1019 ADError::UnsupportedConversion(format!("HDF5 write_chunk_at error: {}", e))
1020 })?;
1021 }
1022 Ok(())
1023 }
1024
1025 fn band_chunk_writes(
1038 leading_coords: &[usize],
1039 frames: &[Vec<u8>],
1040 frame_dims: &[usize],
1041 chunk: &[usize],
1042 elem_size: usize,
1043 ) -> Vec<(Vec<usize>, Vec<u8>)> {
1044 let lead = leading_coords.len();
1045 let fc = chunk[0];
1046 if frame_dims.len() != 2 {
1049 let frame_len = frame_dims.iter().product::<usize>() * elem_size;
1050 let mut buf = vec![0u8; fc * frame_len];
1051 for (f, fb) in frames.iter().take(fc).enumerate() {
1052 buf[f * frame_len..f * frame_len + frame_len].copy_from_slice(fb);
1053 }
1054 let mut coords = leading_coords.to_vec();
1055 coords.extend(std::iter::repeat_n(0, frame_dims.len()));
1056 return vec![(coords, buf)];
1057 }
1058
1059 let (y, x) = (frame_dims[0], frame_dims[1]);
1060 let (rc, cc) = (chunk[lead], chunk[lead + 1]);
1061 let row_tiles = y.div_ceil(rc);
1062 let col_tiles = x.div_ceil(cc);
1063 let mut writes = Vec::with_capacity(row_tiles * col_tiles);
1064 for ry in 0..row_tiles {
1065 for cx in 0..col_tiles {
1066 let mut tile = vec![0u8; fc * rc * cc * elem_size];
1067 for f in 0..fc {
1068 let Some(fb) = frames.get(f) else {
1069 break; };
1071 for r in 0..rc {
1072 let sy = ry * rc + r;
1073 if sy >= y {
1074 break;
1075 }
1076 for c in 0..cc {
1077 let sx = cx * cc + c;
1078 if sx >= x {
1079 break;
1080 }
1081 let src = (sy * x + sx) * elem_size;
1082 let dst = ((f * rc + r) * cc + c) * elem_size;
1083 tile[dst..dst + elem_size].copy_from_slice(&fb[src..src + elem_size]);
1084 }
1085 }
1086 }
1087 let mut coords = leading_coords.to_vec();
1088 coords.push(ry);
1089 coords.push(cx);
1090 writes.push((coords, tile));
1091 }
1092 }
1093 writes
1094 }
1095
1096 fn finalize_standard_datasets(&mut self) -> ADResult<()> {
1102 let Some(frame_dims) = self.open_frame_dims.clone() else {
1103 return Ok(());
1104 };
1105 let (_, chunk, leading) = self.standard_layout(&frame_dims);
1106 let elem_size = self.open_data_type.map(|t| t.element_size()).unwrap_or(1);
1107 let fc = chunk[0];
1108 let Some(Hdf5Handle::Standard { detectors, .. }) = self.handle.as_mut() else {
1109 return Ok(());
1110 };
1111 for det in detectors.values_mut() {
1112 let total = det.frame_count;
1113 if !det.frame_band.is_empty() {
1116 let last = total.saturating_sub(1);
1117 let leading_coords = match &leading {
1118 Some(lead) => Self::unravel(last, lead),
1119 None => vec![last / fc],
1120 };
1121 Self::flush_band(
1122 &det.ds,
1123 &leading_coords,
1124 &det.frame_band,
1125 &frame_dims,
1126 &chunk,
1127 elem_size,
1128 )?;
1129 det.frame_band.clear();
1130 }
1131 if total > 0 {
1136 let mut dims = leading.clone().unwrap_or_else(|| vec![total]);
1137 dims.extend_from_slice(&frame_dims);
1138 det.ds.set_extent(&dims).map_err(|e| {
1139 ADError::UnsupportedConversion(format!("HDF5 set_extent error: {}", e))
1140 })?;
1141 }
1142 }
1143 Ok(())
1144 }
1145
1146 fn open_swmr(&mut self, path: &Path, array: &NDArray) -> ADResult<()> {
1159 let mut swmr = SwmrFileWriter::create(path)
1160 .map_err(|e| ADError::UnsupportedConversion(format!("SWMR create error: {}", e)))?;
1161
1162 let usize_frame_dims: Vec<usize> = array.dims.iter().rev().map(|d| d.size).collect();
1163 let frame_dims: Vec<u64> = usize_frame_dims.iter().map(|&d| d as u64).collect();
1164
1165 let element_size = array.data.data_type().element_size();
1171 let pipeline = self.build_pipeline(element_size);
1172 let chunk: Vec<u64> = {
1173 let (_, c, _) = self.primary_layout(&usize_frame_dims);
1174 c.iter().map(|&v| v as u64).collect()
1175 };
1176
1177 let (grid_shape_usize, grid_chunk_usize, grid_leading) =
1183 self.standard_layout(&usize_frame_dims);
1184 let use_grid = grid_leading.is_some() && pipeline.is_none();
1185 let grid_shape: Vec<u64> = grid_shape_usize.iter().map(|&v| v as u64).collect();
1186 let grid_chunk: Vec<u64> = grid_chunk_usize.iter().map(|&v| v as u64).collect();
1187
1188 let ds_group_path: Option<String> = self
1196 .resolved_dataset_path
1197 .rsplit_once('/')
1198 .map(|(group_path, _leaf)| group_path.to_string());
1199 let ds_name = self.resolved_dataset_path.clone();
1200
1201 macro_rules! create_ds {
1202 ($t:ty) => {
1203 if use_grid {
1204 swmr.create_grid_dataset::<$t>(&ds_name, &grid_shape, &grid_chunk)
1205 .map_err(|e| {
1206 ADError::UnsupportedConversion(format!(
1207 "SWMR create grid dataset error: {}",
1208 e
1209 ))
1210 })
1211 } else {
1212 match pipeline.clone() {
1213 Some(pl) => swmr
1214 .create_streaming_dataset_chunked_compressed::<$t>(
1215 &ds_name,
1216 &frame_dims,
1217 &chunk,
1218 pl,
1219 )
1220 .map_err(|e| {
1221 ADError::UnsupportedConversion(format!(
1222 "SWMR create compressed dataset error: {}",
1223 e
1224 ))
1225 }),
1226 None => swmr
1227 .create_streaming_dataset_chunked::<$t>(&ds_name, &frame_dims, &chunk)
1228 .map_err(|e| {
1229 ADError::UnsupportedConversion(format!(
1230 "SWMR create dataset error: {}",
1231 e
1232 ))
1233 }),
1234 }
1235 }
1236 };
1237 }
1238
1239 let ds_index = match array.data.data_type() {
1240 NDDataType::Int8 => create_ds!(i8)?,
1241 NDDataType::UInt8 => create_ds!(u8)?,
1242 NDDataType::Int16 => create_ds!(i16)?,
1243 NDDataType::UInt16 => create_ds!(u16)?,
1244 NDDataType::Int32 => create_ds!(i32)?,
1245 NDDataType::UInt32 => create_ds!(u32)?,
1246 NDDataType::Int64 => create_ds!(i64)?,
1247 NDDataType::UInt64 => create_ds!(u64)?,
1248 NDDataType::Float32 => create_ds!(f32)?,
1249 NDDataType::Float64 => create_ds!(f64)?,
1250 };
1251
1252 self.build_swmr_layout_groups(&mut swmr)?;
1259 if let Some(ref group_path) = ds_group_path {
1260 let abs_group = format!("/{}", group_path);
1263 swmr.assign_dataset_to_group(&abs_group, ds_index)
1264 .map_err(|e| {
1265 ADError::UnsupportedConversion(format!(
1266 "SWMR assign dataset to group '{}': {}",
1267 abs_group, e
1268 ))
1269 })?;
1270 }
1271 self.write_swmr_layout_dataset_attrs(&mut swmr, ds_index)?;
1272 self.write_swmr_ndarray_default_attrs(&mut swmr, ds_index, array)?;
1273 self.build_swmr_layout_hardlinks(&mut swmr)?;
1274 self.write_swmr_ndattr_element_attrs(&mut swmr, ds_index)?;
1278
1279 swmr.start_swmr()
1280 .map_err(|e| ADError::UnsupportedConversion(format!("SWMR start error: {}", e)))?;
1281
1282 let compression_dropped = self.compression_type != COMPRESS_NONE && pipeline.is_none();
1287 if compression_dropped {
1288 eprintln!(
1289 "NDFileHDF5: WARNING — SWMR mode requested compression type {} \
1290 but no filter pipeline could be built for it; the SWMR file \
1291 will be written UNCOMPRESSED.",
1292 self.compression_type
1293 );
1294 }
1295
1296 let grid = if use_grid {
1297 grid_leading.map(|leading| SwmrGridLayout {
1299 leading,
1300 frame_dims: usize_frame_dims.clone(),
1301 chunk: grid_chunk_usize.clone(),
1302 elem_size: element_size,
1303 })
1304 } else {
1305 None
1306 };
1307 self.handle = Some(Hdf5Handle::Swmr {
1308 writer: Box::new(swmr),
1309 ds_index,
1310 compression_dropped,
1311 grid,
1312 });
1313 self.open_data_type = Some(array.data.data_type());
1314 self.open_frame_dims = Some(array.dims.iter().rev().map(|d| d.size).collect::<Vec<_>>());
1315 Ok(())
1316 }
1317
1318 fn build_swmr_layout_groups(&self, swmr: &mut SwmrFileWriter) -> ADResult<()> {
1327 let layout = match self.layout.as_ref() {
1328 Some(l) => l,
1329 None => return Ok(()),
1330 };
1331 fn collect<'a>(
1332 g: &'a crate::hdf5_layout::LayoutGroup,
1333 prefix: &str,
1334 out: &mut Vec<(String, &'a crate::hdf5_layout::LayoutGroup)>,
1335 ) {
1336 let here = if prefix.is_empty() {
1337 g.name.clone()
1338 } else {
1339 format!("{}/{}", prefix, g.name)
1340 };
1341 out.push((here.clone(), g));
1342 for sub in &g.groups {
1343 collect(sub, &here, out);
1344 }
1345 }
1346 let mut nodes = Vec::new();
1347 for g in &layout.groups {
1348 collect(g, "", &mut nodes);
1349 }
1350 nodes.sort_by_key(|(p, _)| p.matches('/').count());
1351 let mut created: std::collections::HashSet<String> = std::collections::HashSet::new();
1352 for (path, node) in &nodes {
1353 if !created.insert(path.clone()) {
1354 continue;
1355 }
1356 let (parent, leaf) = match path.rsplit_once('/') {
1357 Some((p, l)) => (format!("/{}", p), l),
1358 None => ("/".to_string(), path.as_str()),
1359 };
1360 swmr.create_group(&parent, leaf).map_err(|e| {
1361 ADError::UnsupportedConversion(format!("SWMR layout group '{}': {}", path, e))
1362 })?;
1363 write_swmr_group_constant_attrs(swmr, &format!("/{}", path), &node.attributes)?;
1367 }
1368 Ok(())
1369 }
1370
1371 fn build_swmr_layout_hardlinks(&self, swmr: &mut SwmrFileWriter) -> ADResult<()> {
1381 let layout = match self.layout.as_ref() {
1382 Some(l) => l,
1383 None => return Ok(()),
1384 };
1385 fn collect<'a>(
1386 g: &'a crate::hdf5_layout::LayoutGroup,
1387 prefix: &str,
1388 out: &mut Vec<(String, &'a crate::hdf5_layout::LayoutHardlink)>,
1389 ) {
1390 let here = if prefix.is_empty() {
1391 g.name.clone()
1392 } else {
1393 format!("{}/{}", prefix, g.name)
1394 };
1395 for hl in &g.hardlinks {
1396 out.push((here.clone(), hl));
1397 }
1398 for sub in &g.groups {
1399 collect(sub, &here, out);
1400 }
1401 }
1402 let mut links = Vec::new();
1403 for g in &layout.groups {
1404 collect(g, "", &mut links);
1405 }
1406 for (parent_path, hl) in &links {
1407 let parent = format!("/{}", parent_path);
1408 swmr.create_hard_link(&parent, &hl.name, &hl.target)
1409 .map_err(|e| {
1410 ADError::UnsupportedConversion(format!(
1411 "SWMR layout hardlink '{}/{}' -> '{}': {}",
1412 parent_path, hl.name, hl.target, e
1413 ))
1414 })?;
1415 }
1416 Ok(())
1417 }
1418
1419 fn write_swmr_layout_dataset_attrs(
1427 &self,
1428 swmr: &mut SwmrFileWriter,
1429 ds_index: usize,
1430 ) -> ADResult<()> {
1431 use crate::hdf5_layout::{LayoutDataType, LayoutSource};
1432 let layout = match self.layout.as_ref() {
1433 Some(l) => l,
1434 None => return Ok(()),
1435 };
1436 let resolved_ds = self.resolved_dataset_path.as_str();
1437 let mut attrs: Vec<(String, LayoutDataType, String)> = Vec::new();
1438 layout.for_each_dataset(|path, d| {
1439 let full = format!("{}/{}", path, d.name);
1440 if full.trim_start_matches('/') == resolved_ds {
1441 for a in &d.attributes {
1442 if a.source == LayoutSource::Constant {
1443 attrs.push((a.name.clone(), a.data_type, a.value.clone()));
1444 }
1445 }
1446 }
1447 });
1448 for (name, dtype, value) in &attrs {
1449 match dtype {
1450 LayoutDataType::Int => {
1451 let v: i64 = value.trim().parse().unwrap_or(0);
1452 swmr.set_dataset_attr_numeric(ds_index, name, &v)
1453 }
1454 LayoutDataType::Float => {
1455 let v: f64 = value.trim().parse().unwrap_or(0.0);
1456 swmr.set_dataset_attr_numeric(ds_index, name, &v)
1457 }
1458 LayoutDataType::String => swmr.set_dataset_attr_string(ds_index, name, value),
1459 }
1460 .map_err(|e| {
1461 ADError::UnsupportedConversion(format!(
1462 "SWMR layout dataset attribute '{}': {}",
1463 name, e
1464 ))
1465 })?;
1466 }
1467 Ok(())
1468 }
1469
1470 fn resolve_layout_paths(&mut self) {
1482 let strip = |s: String| s.trim_start_matches('/').to_string();
1483 match self.layout.as_ref() {
1484 Some(layout) => {
1485 self.resolved_dataset_path = layout
1486 .detector_dataset_path()
1487 .map(strip)
1488 .unwrap_or_else(|| self.dataset_name.clone());
1489 self.resolved_ndattr_group =
1490 layout.ndattr_default_group().map(strip).unwrap_or_default();
1491 self.resolved_perf_group = layout
1492 .dataset_group_path("timestamp")
1493 .map(strip)
1494 .unwrap_or_default();
1495 }
1496 None => {
1497 self.resolved_dataset_path = self.dataset_name.clone();
1498 self.resolved_ndattr_group.clear();
1499 self.resolved_perf_group.clear();
1500 }
1501 }
1502 }
1503
1504 fn build_layout_groups(&self, file: &H5File) -> ADResult<()> {
1513 let layout = match self.layout.as_ref() {
1514 Some(l) => l,
1515 None => return Ok(()),
1516 };
1517 fn collect<'a>(
1518 g: &'a crate::hdf5_layout::LayoutGroup,
1519 prefix: &str,
1520 out: &mut Vec<(String, &'a crate::hdf5_layout::LayoutGroup)>,
1521 ) {
1522 let here = if prefix.is_empty() {
1523 g.name.clone()
1524 } else {
1525 format!("{}/{}", prefix, g.name)
1526 };
1527 out.push((here.clone(), g));
1528 for sub in &g.groups {
1529 collect(sub, &here, out);
1530 }
1531 }
1532 let mut nodes = Vec::new();
1533 for g in &layout.groups {
1534 collect(g, "", &mut nodes);
1535 }
1536 nodes.sort_by_key(|(p, _)| p.matches('/').count());
1537 let mut created: std::collections::HashSet<String> = std::collections::HashSet::new();
1538 for (path, node) in &nodes {
1539 if !created.insert(path.clone()) {
1540 continue;
1541 }
1542 let (parent, leaf) = match path.rsplit_once('/') {
1543 Some((p, l)) => (p, l),
1544 None => ("", path.as_str()),
1545 };
1546 let parent_group = if parent.is_empty() {
1548 None
1549 } else {
1550 Some(Self::open_write_group(file, parent)?)
1551 };
1552 let group = match parent_group.as_ref() {
1553 Some(g) => g.create_group(leaf),
1554 None => file.create_group(leaf),
1555 }
1556 .map_err(|e| {
1557 ADError::UnsupportedConversion(format!("HDF5 layout group '{}': {}", path, e))
1558 })?;
1559 write_group_constant_attrs(&group, &node.attributes);
1562 }
1563 Ok(())
1564 }
1565
1566 fn build_layout_hardlinks(&self, file: &H5File) -> ADResult<()> {
1584 let layout = match self.layout.as_ref() {
1585 Some(l) => l,
1586 None => return Ok(()),
1587 };
1588 fn collect<'a>(
1590 g: &'a crate::hdf5_layout::LayoutGroup,
1591 prefix: &str,
1592 out: &mut Vec<(String, &'a crate::hdf5_layout::LayoutHardlink)>,
1593 ) {
1594 let here = if prefix.is_empty() {
1595 g.name.clone()
1596 } else {
1597 format!("{}/{}", prefix, g.name)
1598 };
1599 for hl in &g.hardlinks {
1600 out.push((here.clone(), hl));
1601 }
1602 for sub in &g.groups {
1603 collect(sub, &here, out);
1604 }
1605 }
1606 let mut links = Vec::new();
1607 for g in &layout.groups {
1608 collect(g, "", &mut links);
1609 }
1610 for (parent_path, hl) in &links {
1611 let parent = Self::open_write_group(file, parent_path)?;
1614 parent.link(&hl.name, &hl.target).map_err(|e| {
1615 ADError::UnsupportedConversion(format!(
1616 "HDF5 layout hardlink '{}/{}' -> '{}': {}",
1617 parent_path, hl.name, hl.target, e
1618 ))
1619 })?;
1620 }
1621 Ok(())
1622 }
1623
1624 fn open_write_group(file: &H5File, path: &str) -> ADResult<rust_hdf5::H5Group> {
1628 let mut current: Option<rust_hdf5::H5Group> = None;
1629 for seg in path.split('/').filter(|s| !s.is_empty()) {
1630 let next = match current.as_ref() {
1631 Some(g) => g.group(seg),
1632 None => file.root_group().group(seg),
1633 }
1634 .map_err(|e| {
1635 ADError::UnsupportedConversion(format!("HDF5 group reopen '{}': {}", seg, e))
1636 })?;
1637 current = Some(next);
1638 }
1639 current.ok_or_else(|| ADError::UnsupportedConversion("empty group path".into()))
1640 }
1641
1642 fn create_detector_datasets(&mut self, array: &NDArray) -> ADResult<()> {
1650 let frame_dims: Vec<usize> = array.dims.iter().rev().map(|d| d.size).collect();
1651 let (ds_data_type, shape, chunk, leading, pipeline) = match array.codec.as_ref() {
1659 Some(codec) => {
1660 let pl = self.codec_filter_pipeline(codec).ok_or_else(|| {
1661 ADError::UnsupportedConversion(format!(
1662 "HDF5 cannot direct-chunk-write codec '{}' \
1663 (only jpeg/blosc/lz4/bslz4 are supported)",
1664 codec.name.as_str()
1665 ))
1666 })?;
1667 let (shape, chunk, leading) = self.compressed_layout(&frame_dims);
1668 (codec.original_data_type, shape, chunk, leading, Some(pl))
1669 }
1670 None => {
1671 let dt = array.data.data_type();
1672 let (shape, chunk, leading) = self.standard_layout(&frame_dims);
1673 (
1674 dt,
1675 shape,
1676 chunk,
1677 leading,
1678 self.build_pipeline(dt.element_size()),
1679 )
1680 }
1681 };
1682 let (nbit_dt, pipeline): (Option<DatatypeMessage>, Option<FilterPipeline>) =
1688 if array.codec.is_none() {
1689 let chunk_nelmts: usize = chunk.iter().product();
1690 match self.nbit_packing(ds_data_type, chunk_nelmts) {
1691 Some((dt, pl)) => (Some(dt), Some(pl)),
1692 None => (None, pipeline),
1693 }
1694 } else {
1695 (None, pipeline)
1696 };
1697 let max_shape: Vec<Option<usize>> = shape
1705 .iter()
1706 .zip(chunk.iter())
1707 .enumerate()
1708 .map(|(i, (&s, &c))| {
1709 if leading.is_none() && i == 0 {
1710 None
1711 } else {
1712 Some(s.div_ceil(c) * c)
1713 }
1714 })
1715 .collect();
1716
1717 match self.handle {
1721 Some(Hdf5Handle::Standard { ref file, .. }) => self.build_layout_groups(file)?,
1722 _ => return Err(ADError::UnsupportedConversion("no HDF5 file open".into())),
1723 }
1724
1725 let detector_keys: Vec<String> = match self.layout.as_ref() {
1732 Some(l) => {
1733 let mut keys = l.detector_dataset_paths();
1734 if keys.is_empty() {
1735 keys.push(format!("/{}", self.resolved_dataset_path));
1736 }
1737 keys
1738 }
1739 None => vec![format!("/{}", self.resolved_dataset_path)],
1740 };
1741 let detector_specs: Vec<(
1742 String,
1743 Vec<(String, crate::hdf5_layout::LayoutDataType, String)>,
1744 )> = detector_keys
1745 .iter()
1746 .map(|key| {
1747 let stripped = key.trim_start_matches('/').to_string();
1748 let attrs = self
1749 .layout
1750 .as_ref()
1751 .map(|l| {
1752 use crate::hdf5_layout::LayoutSource;
1753 let mut out = Vec::new();
1754 l.for_each_dataset(|path, d| {
1755 let full = format!("{}/{}", path, d.name);
1756 if full.trim_start_matches('/') == stripped {
1757 for a in &d.attributes {
1758 if a.source == LayoutSource::Constant {
1759 out.push((a.name.clone(), a.data_type, a.value.clone()));
1760 }
1761 }
1762 }
1763 });
1764 out
1765 })
1766 .unwrap_or_default();
1767 (key.clone(), attrs)
1768 })
1769 .collect();
1770
1771 let dtype_ordinal = ds_data_type as i32;
1772 let fill = self.fill_value;
1773 let row_chunks = self.chunk.n_row_chunks as i32;
1774 let col_chunks = self.chunk.n_col_chunks as i32;
1775 let frame_chunks = self.chunk.n_frames_chunks as i32;
1776 let n_extra = self.n_extra_dims as i32;
1777 let extra_meta: Vec<(usize, i32, String)> = (0..self.n_extra_dims)
1778 .map(|i| {
1779 (
1780 i,
1781 self.extra_dims[i].size.max(1) as i32,
1782 self.extra_dims[i].name.clone(),
1783 )
1784 })
1785 .collect();
1786
1787 let nd_num_dims = array.dims.len() as i32;
1792 let dim_offsets: Vec<i32> = array.dims.iter().map(|d| d.offset as i32).collect();
1796 let dim_binnings: Vec<i32> = array.dims.iter().map(|d| d.binning as i32).collect();
1797 let dim_reverses: Vec<i32> = array.dims.iter().map(|d| d.reverse as i32).collect();
1798
1799 macro_rules! create_ds {
1800 ($t:ty, $h5file:expr, $ds_group:expr, $ds_name:expr, $constant_attrs:expr) => {{
1801 let mut builder = match $ds_group.as_ref() {
1802 Some(g) => g.new_dataset::<$t>(),
1803 None => $h5file.new_dataset::<$t>(),
1804 }
1805 .shape(&shape[..])
1806 .chunk(&chunk[..])
1807 .max_shape(&max_shape[..])
1808 .fill_value(fill as $t);
1814 if let Some(ref dt) = nbit_dt {
1818 builder = builder.datatype(dt.clone());
1819 }
1820 if let Some(ref pl) = pipeline {
1821 builder = builder.filter_pipeline(pl.clone());
1822 }
1823 let ds = builder.create($ds_name.as_str()).map_err(|e| {
1824 ADError::UnsupportedConversion(format!("HDF5 dataset error: {}", e))
1825 })?;
1826 let _ = ds
1828 .new_attr::<i32>()
1829 .shape(())
1830 .create(DTYPE_ATTR)
1831 .and_then(|a| a.write_numeric(&dtype_ordinal));
1832 let _ = ds
1836 .new_attr::<f64>()
1837 .shape(())
1838 .create("HDF5_fillValue")
1839 .and_then(|a| a.write_numeric(&fill));
1840 for (name, val) in [
1844 ("HDF5_nRowChunks", row_chunks),
1845 ("HDF5_nColChunks", col_chunks),
1846 ("HDF5_nFramesChunks", frame_chunks),
1847 ("HDF5_nExtraDims", n_extra),
1848 ] {
1849 let _ = ds
1850 .new_attr::<i32>()
1851 .shape(())
1852 .create(name)
1853 .and_then(|a| a.write_numeric(&val));
1854 }
1855 for (i, size, name) in &extra_meta {
1858 let _ = ds
1859 .new_attr::<i32>()
1860 .shape(())
1861 .create(&format!("HDF5_extraDimSize{}", i))
1862 .and_then(|a| a.write_numeric(size));
1863 if !name.is_empty() {
1864 let s = rust_hdf5::types::VarLenUnicode(name.clone());
1865 let _ = ds
1866 .new_attr::<rust_hdf5::types::VarLenUnicode>()
1867 .shape(())
1868 .create(&format!("HDF5_extraDimName{}", i))
1869 .and_then(|a| a.write_scalar(&s));
1870 }
1871 }
1872 for (aname, atype, avalue) in $constant_attrs {
1875 use crate::hdf5_layout::LayoutDataType;
1876 match atype {
1877 LayoutDataType::Int => {
1878 let v: i64 = avalue.trim().parse().unwrap_or(0);
1879 let _ = ds
1880 .new_attr::<i64>()
1881 .shape(())
1882 .create(aname)
1883 .and_then(|a| a.write_numeric(&v));
1884 }
1885 LayoutDataType::Float => {
1886 let v: f64 = avalue.trim().parse().unwrap_or(0.0);
1887 let _ = ds
1888 .new_attr::<f64>()
1889 .shape(())
1890 .create(aname)
1891 .and_then(|a| a.write_numeric(&v));
1892 }
1893 LayoutDataType::String => {
1894 let s = rust_hdf5::types::VarLenUnicode(avalue.clone());
1895 let _ = ds
1896 .new_attr::<rust_hdf5::types::VarLenUnicode>()
1897 .shape(())
1898 .create(aname)
1899 .and_then(|a| a.write_scalar(&s));
1900 }
1901 }
1902 }
1903 let _ = ds
1909 .new_attr::<i32>()
1910 .shape(())
1911 .create("NDArrayNumDims")
1912 .and_then(|a| a.write_numeric(&nd_num_dims));
1913 for (name, vals) in [
1914 ("NDArrayDimOffset", &dim_offsets),
1915 ("NDArrayDimBinning", &dim_binnings),
1916 ("NDArrayDimReverse", &dim_reverses),
1917 ] {
1918 let _ = if vals.len() == 1 {
1919 ds.new_attr::<i32>()
1920 .shape(())
1921 .create(name)
1922 .and_then(|a| a.write_numeric(&vals[0]))
1923 } else {
1924 ds.new_attr::<i32>()
1925 .shape([vals.len()])
1926 .create(name)
1927 .and_then(|a| a.write_array(vals))
1928 };
1929 }
1930 ds
1931 }};
1932 }
1933
1934 let h5file = match self.handle {
1935 Some(Hdf5Handle::Standard { ref file, .. }) => file,
1936 _ => return Err(ADError::UnsupportedConversion("no HDF5 file open".into())),
1937 };
1938
1939 let mut detectors: std::collections::HashMap<String, DetectorDataset> =
1940 std::collections::HashMap::with_capacity(detector_specs.len());
1941 for (key, constant_attrs) in &detector_specs {
1942 let stripped = key.trim_start_matches('/');
1945 let (ds_group, ds_name): (Option<rust_hdf5::H5Group>, String) =
1946 match stripped.rsplit_once('/') {
1947 Some((group_path, leaf)) => (
1948 Some(Self::open_write_group(h5file, group_path)?),
1949 leaf.to_string(),
1950 ),
1951 None => (None, stripped.to_string()),
1952 };
1953 let ds = match ds_data_type {
1958 NDDataType::Int8 => create_ds!(i8, h5file, ds_group, ds_name, constant_attrs),
1959 NDDataType::UInt8 => create_ds!(u8, h5file, ds_group, ds_name, constant_attrs),
1960 NDDataType::Int16 => create_ds!(i16, h5file, ds_group, ds_name, constant_attrs),
1961 NDDataType::UInt16 => create_ds!(u16, h5file, ds_group, ds_name, constant_attrs),
1962 NDDataType::Int32 => create_ds!(i32, h5file, ds_group, ds_name, constant_attrs),
1963 NDDataType::UInt32 => create_ds!(u32, h5file, ds_group, ds_name, constant_attrs),
1964 NDDataType::Int64 => create_ds!(i64, h5file, ds_group, ds_name, constant_attrs),
1965 NDDataType::UInt64 => create_ds!(u64, h5file, ds_group, ds_name, constant_attrs),
1966 NDDataType::Float32 => create_ds!(f32, h5file, ds_group, ds_name, constant_attrs),
1967 NDDataType::Float64 => create_ds!(f64, h5file, ds_group, ds_name, constant_attrs),
1968 };
1969 detectors.insert(
1970 key.clone(),
1971 DetectorDataset {
1972 ds,
1973 frame_count: 0,
1974 frame_band: Vec::new(),
1975 },
1976 );
1977 }
1978
1979 if let Some(Hdf5Handle::Standard { detectors: d, .. }) = self.handle.as_mut() {
1980 *d = detectors;
1981 }
1982 self.open_data_type = Some(ds_data_type);
1983 self.open_frame_dims = Some(frame_dims);
1984 self.open_codec = array.codec.clone();
1985 Ok(())
1986 }
1987
1988 fn resolve_destination_key(&self, array: &NDArray) -> ADResult<String> {
1999 let default = format!("/{}", self.resolved_dataset_path);
2000 let attr_name = self
2001 .layout
2002 .as_ref()
2003 .and_then(|l| l.detector_data_destination.as_deref())
2004 .filter(|s| !s.is_empty());
2005 let Some(attr_name) = attr_name else {
2006 return Ok(default);
2007 };
2008 let Some(attr) = array.attributes.get(attr_name) else {
2011 return Ok(default);
2012 };
2013 match attr.value.as_string_typed() {
2014 Some(value) => {
2015 let known = matches!(
2016 &self.handle,
2017 Some(Hdf5Handle::Standard { detectors, .. })
2018 if detectors.contains_key(value)
2019 );
2020 Ok(if known { value.to_string() } else { default })
2021 }
2022 None => Err(ADError::UnsupportedConversion(format!(
2023 "HDF5 detector_data_destination attribute '{}' is not a string",
2024 attr_name
2025 ))),
2026 }
2027 }
2028
2029 fn write_standard(&mut self, array: &NDArray) -> ADResult<()> {
2034 if self.frame_count == 0 {
2035 self.create_detector_datasets(array)?;
2036 self.create_attribute_datasets(array);
2037 }
2038
2039 let frame_dims = self
2040 .open_frame_dims
2041 .clone()
2042 .ok_or_else(|| ADError::UnsupportedConversion("dataset not initialised".into()))?;
2043 let cur_dims: Vec<usize> = array.dims.iter().rev().map(|d| d.size).collect();
2044 if cur_dims != frame_dims {
2045 return Err(ADError::UnsupportedConversion(format!(
2046 "HDF5 frame shape changed mid-stream: {:?} != {:?}",
2047 cur_dims, frame_dims
2048 )));
2049 }
2050
2051 if !codecs_match(self.open_codec.as_ref(), array.codec.as_ref()) {
2056 return Err(ADError::UnsupportedConversion(format!(
2057 "HDF5 codec changed mid-stream: dataset codec {:?} != frame codec {:?}",
2058 self.open_codec.as_ref().map(|c| c.name),
2059 array.codec.as_ref().map(|c| c.name),
2060 )));
2061 }
2062
2063 let (_shape, chunk, leading) = self.standard_layout(&frame_dims);
2064 let fc = chunk[0];
2065
2066 let dest_key = self.resolve_destination_key(array)?;
2072 let chunk_bytes = array.codec.as_ref().map(|codec| {
2073 let total_bytes =
2074 frame_dims.iter().product::<usize>() * codec.original_data_type.element_size();
2075 codec_chunk_bytes(codec, total_bytes, array.data.as_u8_slice())
2076 });
2077 let frame_le = if chunk_bytes.is_none() {
2078 Some(nd_buffer_to_le_bytes(&array.data))
2079 } else {
2080 None
2081 };
2082 let elem_size = array.data.data_type().element_size();
2083
2084 {
2085 let Some(Hdf5Handle::Standard { detectors, .. }) = self.handle.as_mut() else {
2086 return Err(ADError::UnsupportedConversion(
2087 "HDF5 detector datasets not initialised".into(),
2088 ));
2089 };
2090 let det = detectors.get_mut(&dest_key).ok_or_else(|| {
2091 ADError::UnsupportedConversion(format!(
2092 "HDF5 destination dataset '{}' not found",
2093 dest_key
2094 ))
2095 })?;
2096
2097 let frame_idx = det.frame_count;
2100 if let Some(ref lead) = leading {
2101 let total: usize = lead.iter().product();
2102 if frame_idx >= total {
2103 return Err(ADError::UnsupportedConversion(format!(
2104 "HDF5 extra-dimension capacity exceeded: frame {} >= {}",
2105 frame_idx, total
2106 )));
2107 }
2108 }
2109
2110 if let Some(cb) = &chunk_bytes {
2111 det.ds.write_chunk_raw(frame_idx, cb, 0).map_err(|e| {
2117 ADError::UnsupportedConversion(format!("HDF5 direct chunk write error: {}", e))
2118 })?;
2119 } else {
2120 let fle = frame_le.expect("frame_le is set whenever chunk_bytes is None");
2126 det.frame_band.push(fle);
2127 if det.frame_band.len() >= fc {
2128 let leading_coords = match &leading {
2129 Some(lead) => Self::unravel(frame_idx, lead),
2130 None => vec![frame_idx / fc],
2131 };
2132 Self::flush_band(
2133 &det.ds,
2134 &leading_coords,
2135 &det.frame_band,
2136 &frame_dims,
2137 &chunk,
2138 elem_size,
2139 )?;
2140 det.frame_band.clear();
2141 }
2142 }
2143 det.frame_count += 1;
2144 }
2145
2146 if self.store_attributes {
2148 for ad in self.attr_datasets.iter_mut() {
2149 let value = array
2150 .attributes
2151 .get(&ad.name)
2152 .map(|a| a.value.clone())
2153 .unwrap_or(NDAttrValue::Undefined);
2154 ad.push(&value);
2155 }
2156 }
2157 Ok(())
2158 }
2159
2160 fn create_attribute_datasets(&mut self, array: &NDArray) {
2163 self.attr_datasets.clear();
2164 if !self.store_attributes {
2165 return;
2166 }
2167 for attr in array.attributes.iter() {
2168 self.attr_datasets.push(AttributeDataset::new(attr));
2169 }
2170 }
2171
2172 fn attribute_chunking(&self) -> usize {
2178 if self.chunk.ndattr_chunk != 0 {
2179 return self.chunk.ndattr_chunk;
2180 }
2181 if self.open_mode == NDFileMode::Single {
2182 return 1;
2183 }
2184 if self.num_capture > 0 {
2185 self.num_capture
2186 } else {
2187 16 * 1024
2188 }
2189 }
2190
2191 fn seed_ndattr_element_values(&mut self, array: &NDArray) {
2197 self.ndattr_first_values.clear();
2198 self.ndattr_last_values.clear();
2199 self.ndattr_element_names.clear();
2200 let Some(layout) = self.layout.as_ref() else {
2201 return;
2202 };
2203 let mut names: Vec<String> = Vec::new();
2204 for e in layout.ndattribute_element_attrs() {
2205 if !names.contains(&e.ndattribute) {
2206 names.push(e.ndattribute);
2207 }
2208 }
2209 for name in &names {
2210 if let Some(attr) = array.attributes.get(name) {
2211 self.ndattr_first_values
2212 .insert(name.clone(), attr.value.clone());
2213 self.ndattr_last_values
2214 .insert(name.clone(), attr.value.clone());
2215 }
2216 }
2217 self.ndattr_element_names = names;
2218 }
2219
2220 fn update_ndattr_element_values(&mut self, array: &NDArray) {
2226 if self.ndattr_element_names.is_empty() {
2227 return;
2228 }
2229 for name in &self.ndattr_element_names {
2230 if let Some(attr) = array.attributes.get(name) {
2231 self.ndattr_last_values
2232 .insert(name.clone(), attr.value.clone());
2233 }
2234 }
2235 }
2236
2237 fn flush_ndattr_element_attrs(
2254 &self,
2255 file: &H5File,
2256 detectors: &std::collections::HashMap<String, DetectorDataset>,
2257 ) -> ADResult<()> {
2258 use crate::hdf5_layout::LayoutWhen;
2259 let Some(layout) = self.layout.as_ref() else {
2260 return Ok(());
2261 };
2262 for e in layout.ndattribute_element_attrs() {
2263 let value = match e.when {
2264 LayoutWhen::OnFileOpen | LayoutWhen::OnFrame => {
2265 self.ndattr_first_values.get(&e.ndattribute)
2266 }
2267 LayoutWhen::OnFileClose => self.ndattr_last_values.get(&e.ndattribute),
2268 LayoutWhen::OnFileWrite => None,
2269 };
2270 let Some(value) = value else {
2271 continue;
2272 };
2273 let path = e.element_path.trim_start_matches('/');
2274 if e.is_dataset {
2275 if let Some(det) = detectors.get(&e.element_path) {
2282 write_ndattr_dataset_attr(&det.ds, &e.attr_name, value);
2283 } else if let Ok(ds) = file.dataset_writer(path) {
2284 write_ndattr_dataset_attr(&ds, &e.attr_name, value);
2285 }
2286 } else {
2287 let group = Self::open_write_group(file, path)?;
2288 write_ndattr_group_attr(&group, &e.attr_name, value);
2289 }
2290 }
2291 Ok(())
2292 }
2293
2294 fn write_swmr_ndarray_default_attrs(
2303 &self,
2304 swmr: &mut SwmrFileWriter,
2305 ds_index: usize,
2306 array: &NDArray,
2307 ) -> ADResult<()> {
2308 let nd_num_dims = array.dims.len() as i32;
2309 swmr.set_dataset_attr_numeric(ds_index, "NDArrayNumDims", &nd_num_dims)
2310 .map_err(|e| {
2311 ADError::UnsupportedConversion(format!("SWMR NDArrayNumDims attr: {}", e))
2312 })?;
2313 let dim_offsets: Vec<i32> = array.dims.iter().map(|d| d.offset as i32).collect();
2314 let dim_binnings: Vec<i32> = array.dims.iter().map(|d| d.binning as i32).collect();
2315 let dim_reverses: Vec<i32> = array.dims.iter().map(|d| d.reverse as i32).collect();
2316 for (name, vals) in [
2317 ("NDArrayDimOffset", &dim_offsets),
2318 ("NDArrayDimBinning", &dim_binnings),
2319 ("NDArrayDimReverse", &dim_reverses),
2320 ] {
2321 let res = if vals.len() == 1 {
2322 swmr.set_dataset_attr_numeric(ds_index, name, &vals[0])
2323 } else {
2324 swmr.set_dataset_attr_array(ds_index, name, &[vals.len() as u64], vals)
2325 };
2326 res.map_err(|e| ADError::UnsupportedConversion(format!("SWMR {} attr: {}", name, e)))?;
2327 }
2328 Ok(())
2329 }
2330
2331 fn write_swmr_ndattr_element_attrs(
2342 &self,
2343 swmr: &mut SwmrFileWriter,
2344 ds_index: usize,
2345 ) -> ADResult<()> {
2346 use crate::hdf5_layout::LayoutWhen;
2347 let Some(layout) = self.layout.as_ref() else {
2348 return Ok(());
2349 };
2350 for e in layout.ndattribute_element_attrs() {
2351 if !matches!(e.when, LayoutWhen::OnFileOpen | LayoutWhen::OnFrame) {
2352 continue;
2353 }
2354 let Some(value) = self.ndattr_first_values.get(&e.ndattribute) else {
2355 continue;
2356 };
2357 if e.is_dataset {
2358 if e.element_path.trim_start_matches('/') == self.resolved_dataset_path {
2362 write_swmr_ndattr_dataset_attr(swmr, ds_index, &e.attr_name, value)?;
2363 }
2364 } else {
2365 write_swmr_ndattr_group_attr(swmr, &e.element_path, &e.attr_name, value)?;
2366 }
2367 }
2368 Ok(())
2369 }
2370
2371 fn flush_attribute_datasets(&mut self) -> ADResult<()> {
2374 if self.attr_datasets.is_empty() {
2375 return Ok(());
2376 }
2377 let chunk_depth = self.attribute_chunking();
2378 let ndattr_group = self.resolved_ndattr_group.clone();
2379 let targets: Vec<(String, String)> = self
2383 .attr_datasets
2384 .iter()
2385 .map(|ad| {
2386 match self
2387 .layout
2388 .as_ref()
2389 .and_then(|l| l.ndattribute_dataset(&ad.name))
2390 {
2391 Some((g, name)) => (g.trim_start_matches('/').to_string(), name),
2392 None => (ndattr_group.clone(), ad.name.clone()),
2393 }
2394 })
2395 .collect();
2396 let h5file = match self.handle {
2397 Some(Hdf5Handle::Standard { ref file, .. }) => file,
2398 _ => return Ok(()),
2399 };
2400 let mut group_cache: std::collections::HashMap<String, rust_hdf5::H5Group> =
2404 std::collections::HashMap::new();
2405
2406 for (ad, (group_path, ds_name)) in self.attr_datasets.iter().zip(targets.iter()) {
2407 if ad.frames == 0 {
2408 continue;
2409 }
2410 let n = ad.frames;
2411 let chunk = chunk_depth;
2415
2416 if !group_cache.contains_key(group_path) {
2417 let g = if group_path.is_empty() {
2418 h5file.create_group("NDAttributes").map_err(|e| {
2419 ADError::UnsupportedConversion(format!("HDF5 group error: {}", e))
2420 })?
2421 } else {
2422 Self::open_write_group(h5file, group_path)?
2423 };
2424 group_cache.insert(group_path.clone(), g);
2425 }
2426 let group = &group_cache[group_path];
2427
2428 macro_rules! create_attr_ds {
2429 ($t:ty) => {{
2430 let es = std::mem::size_of::<$t>();
2431 let ds = group
2432 .new_dataset::<$t>()
2433 .shape(&[n])
2434 .chunk(&[chunk])
2435 .max_shape(&[None])
2436 .create(ds_name)
2437 .map_err(|e| {
2438 ADError::UnsupportedConversion(format!(
2439 "HDF5 attribute dataset error: {}",
2440 e
2441 ))
2442 })?;
2443 write_chunked_buffer(&ds, &ad.buffer, chunk * es)?;
2447 ds
2448 }};
2449 }
2450
2451 let ds = match ad.data_type {
2452 NDAttrDataType::Int8 => create_attr_ds!(i8),
2453 NDAttrDataType::UInt8 => create_attr_ds!(u8),
2454 NDAttrDataType::Int16 => create_attr_ds!(i16),
2455 NDAttrDataType::UInt16 => create_attr_ds!(u16),
2456 NDAttrDataType::Int32 => create_attr_ds!(i32),
2457 NDAttrDataType::UInt32 => create_attr_ds!(u32),
2458 NDAttrDataType::Int64 => create_attr_ds!(i64),
2459 NDAttrDataType::UInt64 => create_attr_ds!(u64),
2460 NDAttrDataType::Float32 => create_attr_ds!(f32),
2461 NDAttrDataType::Float64 => create_attr_ds!(f64),
2462 NDAttrDataType::String => {
2463 let es = MAX_ATTRIBUTE_STRING_SIZE;
2470 let ds = group
2471 .new_dataset::<FixedStr256>()
2472 .shape([n])
2473 .chunk(&[chunk])
2474 .max_shape(&[None])
2475 .create(&ad.name)
2476 .map_err(|e| {
2477 ADError::UnsupportedConversion(format!(
2478 "HDF5 attribute dataset error: {}",
2479 e
2480 ))
2481 })?;
2482 write_chunked_buffer(&ds, &ad.buffer, chunk * es)?;
2483 ds
2484 }
2485 };
2486
2487 write_ndattr_descriptors(&ds, ad);
2491 }
2492 Ok(())
2493 }
2494
2495 fn flush_performance_dataset(&mut self) -> ADResult<()> {
2498 if !self.store_performance || self.perf_rows.is_empty() {
2499 return Ok(());
2500 }
2501 let n = self.perf_rows.len();
2502 let mut flat: Vec<f64> = Vec::with_capacity(n * 5);
2503 for row in &self.perf_rows {
2504 flat.extend_from_slice(row);
2505 }
2506 let raw: Vec<u8> = flat.iter().flat_map(|v| v.to_le_bytes()).collect();
2509
2510 let chunking = self.attribute_chunking();
2514 let perf_group = self.resolved_perf_group.clone();
2515 let h5file = match self.handle {
2516 Some(Hdf5Handle::Standard { ref file, .. }) => file,
2517 _ => return Ok(()),
2518 };
2519 let group = if perf_group.is_empty() {
2524 h5file
2525 .create_group("performance")
2526 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 group error: {}", e)))?
2527 } else {
2528 Self::open_write_group(h5file, &perf_group)?
2529 };
2530 let ds = group
2531 .new_dataset::<f64>()
2532 .shape([n, 5])
2533 .chunk(&[chunking, 5])
2534 .max_shape(&[None, Some(5)])
2535 .create("timestamp")
2536 .map_err(|e| {
2537 ADError::UnsupportedConversion(format!("HDF5 performance dataset error: {}", e))
2538 })?;
2539 write_chunked_buffer(&ds, &raw, chunking * 5 * 8)?;
2542 Ok(())
2543 }
2544
2545 fn write_swmr(&mut self, array: &NDArray) -> ADResult<()> {
2547 if array.codec.is_some() {
2556 return Err(ADError::UnsupportedConversion(
2557 "HDF5 SWMR mode cannot write a pre-compressed array (no raw-chunk \
2558 append in the SWMR backend); use standard capture mode for \
2559 direct chunk write"
2560 .into(),
2561 ));
2562 }
2563
2564 let frame_idx = self.frame_count;
2567
2568 let (writer, ds_index, grid) = match self.handle {
2569 Some(Hdf5Handle::Swmr {
2570 ref mut writer,
2571 ds_index,
2572 ref grid,
2573 ..
2574 }) => (writer, ds_index, grid),
2575 _ => return Err(ADError::UnsupportedConversion("no SWMR writer open".into())),
2576 };
2577
2578 let frame_bytes = nd_buffer_to_le_bytes(&array.data);
2583 match grid {
2584 Some(g) => {
2588 let leading_coords = Self::unravel(frame_idx, &g.leading);
2589 for (coords, tile) in Self::band_chunk_writes(
2590 &leading_coords,
2591 std::slice::from_ref(&frame_bytes),
2592 &g.frame_dims,
2593 &g.chunk,
2594 g.elem_size,
2595 ) {
2596 let coords_u64: Vec<u64> = coords.iter().map(|&c| c as u64).collect();
2597 writer
2598 .write_chunk_at(ds_index, &coords_u64, &tile)
2599 .map_err(|e| {
2600 ADError::UnsupportedConversion(format!(
2601 "SWMR write_chunk_at error: {}",
2602 e
2603 ))
2604 })?;
2605 }
2606 }
2607 None => {
2608 writer.append_frame(ds_index, &frame_bytes).map_err(|e| {
2609 ADError::UnsupportedConversion(format!("SWMR append error: {}", e))
2610 })?;
2611 }
2612 }
2613
2614 let count = self.frame_count + 1; if self.flush_nth_frame > 0 && count % self.flush_nth_frame == 0 {
2617 writer
2618 .flush()
2619 .map_err(|e| ADError::UnsupportedConversion(format!("SWMR flush error: {}", e)))?;
2620 }
2621 Ok(())
2622 }
2623
2624 fn record_performance(&mut self, write_duration: f64, frame_bytes: usize) {
2626 let now = std::time::Instant::now();
2627 let first = *self.perf_first.get_or_insert(now);
2628 let runtime = now.duration_since(first).as_secs_f64();
2629 let period = match self.perf_prev {
2630 Some(prev) => now.duration_since(prev).as_secs_f64(),
2631 None => write_duration,
2632 };
2633 self.perf_prev = Some(now);
2634 let fb = frame_bytes as f64;
2635 let inst_speed = if period > 0.0 { fb / period } else { 0.0 };
2636 let avg_speed = if runtime > 0.0 {
2637 (self.perf_rows.len() as f64 + 1.0) * fb / runtime
2638 } else {
2639 0.0
2640 };
2641 self.perf_rows
2642 .push([write_duration, period, runtime, inst_speed, avg_speed]);
2643 }
2644}
2645
2646fn codecs_match(a: Option<&Codec>, b: Option<&Codec>) -> bool {
2653 match (a, b) {
2654 (None, None) => true,
2655 (Some(x), Some(y)) => {
2656 x.name == y.name
2657 && x.level == y.level
2658 && x.shuffle == y.shuffle
2659 && x.compressor == y.compressor
2660 }
2661 _ => false,
2662 }
2663}
2664
2665fn codec_chunk_bytes(codec: &Codec, uncompressed_total_bytes: usize, payload: &[u8]) -> Vec<u8> {
2687 match codec.name {
2688 CodecName::LZ4 => {
2689 let mut out = Vec::with_capacity(16 + payload.len());
2690 out.extend_from_slice(&(uncompressed_total_bytes as u64).to_be_bytes());
2691 out.extend_from_slice(&(uncompressed_total_bytes as u32).to_be_bytes());
2692 out.extend_from_slice(&(payload.len() as u32).to_be_bytes());
2693 out.extend_from_slice(payload);
2694 out
2695 }
2696 CodecName::BSLZ4 => {
2697 let elem_size = codec.original_data_type.element_size();
2698 let block_bytes = crate::codec::bshuf_default_block_size(elem_size) * elem_size;
2699 let mut out = Vec::with_capacity(12 + payload.len());
2700 out.extend_from_slice(&(uncompressed_total_bytes as u64).to_be_bytes());
2701 out.extend_from_slice(&(block_bytes as u32).to_be_bytes());
2702 out.extend_from_slice(payload);
2703 out
2704 }
2705 _ => payload.to_vec(),
2706 }
2707}
2708
2709fn nd_buffer_to_le_bytes(buf: &NDDataBuffer) -> Vec<u8> {
2721 match buf {
2722 NDDataBuffer::I8(v) => v.iter().map(|&x| x as u8).collect(),
2723 NDDataBuffer::U8(v) => v.clone(),
2724 NDDataBuffer::I16(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2725 NDDataBuffer::U16(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2726 NDDataBuffer::I32(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2727 NDDataBuffer::U32(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2728 NDDataBuffer::I64(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2729 NDDataBuffer::U64(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2730 NDDataBuffer::F32(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2731 NDDataBuffer::F64(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2732 }
2733}
2734
2735fn write_chunked_buffer(
2739 ds: &rust_hdf5::H5Dataset,
2740 buffer: &[u8],
2741 chunk_bytes: usize,
2742) -> ADResult<()> {
2743 let n_chunks = buffer.len().div_ceil(chunk_bytes.max(1));
2744 for c in 0..n_chunks {
2745 let start = c * chunk_bytes;
2746 let end = ((c + 1) * chunk_bytes).min(buffer.len());
2747 let slice = &buffer[start..end];
2748 if slice.len() == chunk_bytes {
2749 ds.write_chunk(c, slice)
2750 } else {
2751 let mut padded = vec![0u8; chunk_bytes];
2752 padded[..slice.len()].copy_from_slice(slice);
2753 ds.write_chunk(c, &padded)
2754 }
2755 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 chunk write error: {}", e)))?;
2756 }
2757 Ok(())
2758}
2759
2760fn write_ndattr_descriptors(ds: &rust_hdf5::H5Dataset, ad: &AttributeDataset) {
2766 for (aname, avalue) in [
2767 ("NDAttrName", ad.name.as_str()),
2768 ("NDAttrDescription", ad.description.as_str()),
2769 ("NDAttrSourceType", ad.source_type.as_str()),
2770 ("NDAttrSource", ad.source.as_str()),
2771 ] {
2772 if avalue.is_empty() {
2773 continue;
2774 }
2775 let s = rust_hdf5::types::VarLenUnicode(avalue.to_string());
2776 let _ = ds
2777 .new_attr::<rust_hdf5::types::VarLenUnicode>()
2778 .shape(())
2779 .create(aname)
2780 .and_then(|a| a.write_scalar(&s));
2781 }
2782}
2783
2784fn write_ndattr_group_attr(group: &rust_hdf5::H5Group, attr_name: &str, value: &NDAttrValue) {
2790 macro_rules! num {
2791 ($v:expr) => {{
2792 let _ = group.set_attr_numeric(attr_name, &$v);
2793 }};
2794 }
2795 match value {
2796 NDAttrValue::Int8(v) => num!(*v),
2797 NDAttrValue::UInt8(v) => num!(*v),
2798 NDAttrValue::Int16(v) => num!(*v),
2799 NDAttrValue::UInt16(v) => num!(*v),
2800 NDAttrValue::Int32(v) => num!(*v),
2801 NDAttrValue::UInt32(v) => num!(*v),
2802 NDAttrValue::Int64(v) => num!(*v),
2803 NDAttrValue::UInt64(v) => num!(*v),
2804 NDAttrValue::Float32(v) => num!(*v),
2805 NDAttrValue::Float64(v) => num!(*v),
2806 NDAttrValue::String(s) => {
2807 let _ = group.set_attr_string(attr_name, s);
2808 }
2809 NDAttrValue::Undefined => {}
2810 }
2811}
2812
2813fn write_ndattr_dataset_attr(ds: &rust_hdf5::H5Dataset, attr_name: &str, value: &NDAttrValue) {
2820 macro_rules! num {
2821 ($v:expr, $t:ty) => {{
2822 let v: $t = $v;
2823 let _ = ds
2824 .new_attr::<$t>()
2825 .shape(())
2826 .create(attr_name)
2827 .and_then(|a| a.write_numeric(&v));
2828 }};
2829 }
2830 match value {
2831 NDAttrValue::Int8(v) => num!(*v, i8),
2832 NDAttrValue::UInt8(v) => num!(*v, u8),
2833 NDAttrValue::Int16(v) => num!(*v, i16),
2834 NDAttrValue::UInt16(v) => num!(*v, u16),
2835 NDAttrValue::Int32(v) => num!(*v, i32),
2836 NDAttrValue::UInt32(v) => num!(*v, u32),
2837 NDAttrValue::Int64(v) => num!(*v, i64),
2838 NDAttrValue::UInt64(v) => num!(*v, u64),
2839 NDAttrValue::Float32(v) => num!(*v, f32),
2840 NDAttrValue::Float64(v) => num!(*v, f64),
2841 NDAttrValue::String(s) => {
2842 let sv = rust_hdf5::types::VarLenUnicode(s.clone());
2843 let _ = ds
2844 .new_attr::<rust_hdf5::types::VarLenUnicode>()
2845 .shape(())
2846 .create(attr_name)
2847 .and_then(|a| a.write_scalar(&sv));
2848 }
2849 NDAttrValue::Undefined => {}
2850 }
2851}
2852
2853fn write_swmr_ndattr_group_attr(
2858 swmr: &mut SwmrFileWriter,
2859 group_path: &str,
2860 attr_name: &str,
2861 value: &NDAttrValue,
2862) -> ADResult<()> {
2863 let res = match value {
2864 NDAttrValue::Int8(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2865 NDAttrValue::UInt8(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2866 NDAttrValue::Int16(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2867 NDAttrValue::UInt16(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2868 NDAttrValue::Int32(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2869 NDAttrValue::UInt32(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2870 NDAttrValue::Int64(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2871 NDAttrValue::UInt64(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2872 NDAttrValue::Float32(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2873 NDAttrValue::Float64(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2874 NDAttrValue::String(s) => swmr.set_group_attr_string(group_path, attr_name, s),
2875 NDAttrValue::Undefined => return Ok(()),
2876 };
2877 res.map_err(|e| {
2878 ADError::UnsupportedConversion(format!(
2879 "SWMR ndattribute group attribute '{}/{}': {}",
2880 group_path, attr_name, e
2881 ))
2882 })
2883}
2884
2885fn write_swmr_ndattr_dataset_attr(
2892 swmr: &mut SwmrFileWriter,
2893 ds_index: usize,
2894 attr_name: &str,
2895 value: &NDAttrValue,
2896) -> ADResult<()> {
2897 let res = match value {
2898 NDAttrValue::Int8(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2899 NDAttrValue::UInt8(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2900 NDAttrValue::Int16(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2901 NDAttrValue::UInt16(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2902 NDAttrValue::Int32(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2903 NDAttrValue::UInt32(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2904 NDAttrValue::Int64(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2905 NDAttrValue::UInt64(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2906 NDAttrValue::Float32(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2907 NDAttrValue::Float64(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2908 NDAttrValue::String(s) => swmr.set_dataset_attr_string(ds_index, attr_name, s),
2909 NDAttrValue::Undefined => return Ok(()),
2910 };
2911 res.map_err(|e| {
2912 ADError::UnsupportedConversion(format!(
2913 "SWMR ndattribute dataset attribute '{}': {}",
2914 attr_name, e
2915 ))
2916 })
2917}
2918
2919fn write_group_constant_attrs(
2925 group: &rust_hdf5::H5Group,
2926 attrs: &[crate::hdf5_layout::LayoutAttribute],
2927) {
2928 use crate::hdf5_layout::{LayoutDataType, LayoutSource};
2929 for a in attrs {
2930 if a.source != LayoutSource::Constant {
2931 continue;
2932 }
2933 let _ = match a.data_type {
2934 LayoutDataType::Int => {
2935 group.set_attr_numeric(&a.name, &a.value.trim().parse::<i64>().unwrap_or(0))
2936 }
2937 LayoutDataType::Float => {
2938 group.set_attr_numeric(&a.name, &a.value.trim().parse::<f64>().unwrap_or(0.0))
2939 }
2940 LayoutDataType::String => group.set_attr_string(&a.name, &a.value),
2941 };
2942 }
2943}
2944
2945fn write_swmr_group_constant_attrs(
2949 swmr: &mut SwmrFileWriter,
2950 group_path: &str,
2951 attrs: &[crate::hdf5_layout::LayoutAttribute],
2952) -> ADResult<()> {
2953 use crate::hdf5_layout::{LayoutDataType, LayoutSource};
2954 for a in attrs {
2955 if a.source != LayoutSource::Constant {
2956 continue;
2957 }
2958 match a.data_type {
2959 LayoutDataType::Int => swmr.set_group_attr_numeric(
2960 group_path,
2961 &a.name,
2962 &a.value.trim().parse::<i64>().unwrap_or(0),
2963 ),
2964 LayoutDataType::Float => swmr.set_group_attr_numeric(
2965 group_path,
2966 &a.name,
2967 &a.value.trim().parse::<f64>().unwrap_or(0.0),
2968 ),
2969 LayoutDataType::String => swmr.set_group_attr_string(group_path, &a.name, &a.value),
2970 }
2971 .map_err(|e| {
2972 ADError::UnsupportedConversion(format!(
2973 "SWMR layout group attribute '{}/{}': {}",
2974 group_path, a.name, e
2975 ))
2976 })?;
2977 }
2978 Ok(())
2979}
2980
2981impl Default for Hdf5Writer {
2982 fn default() -> Self {
2983 Self::new()
2984 }
2985}
2986
2987impl NDFileWriter for Hdf5Writer {
2988 fn open_file(&mut self, path: &Path, mode: NDFileMode, array: &NDArray) -> ADResult<()> {
2989 self.current_path = Some(path.to_path_buf());
2990 self.open_mode = mode;
2991 self.frame_count = 0;
2992 self.total_runtime = 0.0;
2993 self.total_bytes = 0;
2994 self.swmr_cb_counter = 0;
2995 self.open_data_type = None;
2996 self.open_frame_dims = None;
2997 self.open_codec = None;
2998 self.perf_rows.clear();
2999 self.perf_prev = None;
3000 self.perf_first = None;
3001 self.attr_datasets.clear();
3002 self.resolve_layout_paths();
3005 self.seed_ndattr_element_values(array);
3008
3009 if self.swmr_mode && mode == NDFileMode::Stream {
3010 self.open_swmr(path, array)
3011 } else {
3012 let h5file = H5File::create(path)
3013 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 create error: {}", e)))?;
3014 self.handle = Some(Hdf5Handle::Standard {
3015 file: h5file,
3016 detectors: std::collections::HashMap::new(),
3017 });
3018 Ok(())
3019 }
3020 }
3021
3022 fn set_num_capture(&mut self, n: usize) {
3023 self.num_capture = n;
3024 }
3025
3026 fn write_file(&mut self, array: &NDArray) -> ADResult<()> {
3027 let start = std::time::Instant::now();
3028
3029 let is_swmr = matches!(self.handle, Some(Hdf5Handle::Swmr { .. }));
3030 if is_swmr {
3031 self.write_swmr(array)?;
3032 } else {
3033 self.write_standard(array)?;
3034 }
3035 self.update_ndattr_element_values(array);
3038 self.frame_count += 1;
3039
3040 let elapsed = start.elapsed().as_secs_f64();
3041 let frame_bytes = array.data.as_u8_slice().len();
3042 if self.store_performance {
3043 self.total_runtime += elapsed;
3044 self.total_bytes += frame_bytes as u64;
3045 self.record_performance(elapsed, frame_bytes);
3046 }
3047 Ok(())
3048 }
3049
3050 fn read_file(&mut self) -> ADResult<NDArray> {
3051 self.resolve_layout_paths();
3055 let dataset_path = self.resolved_dataset_path.clone();
3056 let path = self
3057 .current_path
3058 .as_ref()
3059 .ok_or_else(|| ADError::UnsupportedConversion("no file open".into()))?;
3060
3061 let h5file = H5File::open(path)
3062 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 open error: {}", e)))?;
3063
3064 let ds = h5file
3065 .dataset(&dataset_path)
3066 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 dataset error: {}", e)))?;
3067
3068 let shape = ds.shape();
3069 let dims: Vec<NDDimension> = shape.iter().rev().map(|&s| NDDimension::new(s)).collect();
3070 let element_size = ds.element_size();
3071
3072 let recorded: Option<NDDataType> = ds
3074 .attr(DTYPE_ATTR)
3075 .ok()
3076 .and_then(|a| a.read_numeric::<i32>().ok())
3077 .and_then(|v| NDDataType::from_ordinal(v as u8));
3078
3079 let data_type = recorded.unwrap_or(match element_size {
3080 1 => NDDataType::UInt8,
3081 2 => NDDataType::UInt16,
3082 4 => NDDataType::Float32,
3083 8 => NDDataType::Float64,
3084 other => {
3085 return Err(ADError::UnsupportedConversion(format!(
3086 "unsupported HDF5 element size {}",
3087 other
3088 )));
3089 }
3090 });
3091
3092 macro_rules! read_typed {
3093 ($t:ty, $variant:ident) => {{
3094 let data = ds.read_raw::<$t>().map_err(|e| {
3095 ADError::UnsupportedConversion(format!("HDF5 read error: {}", e))
3096 })?;
3097 let mut arr = NDArray::new(dims, data_type);
3098 arr.data = NDDataBuffer::$variant(data);
3099 return Ok(arr);
3100 }};
3101 }
3102
3103 match data_type {
3104 NDDataType::Int8 => read_typed!(i8, I8),
3105 NDDataType::UInt8 => read_typed!(u8, U8),
3106 NDDataType::Int16 => read_typed!(i16, I16),
3107 NDDataType::UInt16 => read_typed!(u16, U16),
3108 NDDataType::Int32 => read_typed!(i32, I32),
3109 NDDataType::UInt32 => read_typed!(u32, U32),
3110 NDDataType::Int64 => read_typed!(i64, I64),
3111 NDDataType::UInt64 => read_typed!(u64, U64),
3112 NDDataType::Float32 => read_typed!(f32, F32),
3113 NDDataType::Float64 => read_typed!(f64, F64),
3114 }
3115 }
3116
3117 fn close_file(&mut self) -> ADResult<()> {
3118 match self.handle {
3119 Some(Hdf5Handle::Standard { .. }) => {
3120 self.finalize_standard_datasets()?;
3124 self.flush_attribute_datasets()?;
3125 self.flush_performance_dataset()?;
3126 match self.handle {
3129 Some(Hdf5Handle::Standard {
3130 ref file,
3131 ref detectors,
3132 }) => {
3133 self.build_layout_hardlinks(file)?;
3134 self.flush_ndattr_element_attrs(file, detectors)?;
3138 }
3139 _ => unreachable!("handle is Standard in this arm"),
3140 }
3141 self.handle = None;
3142 }
3143 Some(Hdf5Handle::Swmr { .. }) => {
3144 if let Some(Hdf5Handle::Swmr { mut writer, .. }) = self.handle.take() {
3151 writer.flush().map_err(|e| {
3158 ADError::UnsupportedConversion(format!("SWMR flush-on-close error: {}", e))
3159 })?;
3160 writer.close().map_err(|e| {
3161 ADError::UnsupportedConversion(format!("SWMR close error: {}", e))
3162 })?;
3163 }
3164 }
3165 None => {}
3166 }
3167 self.current_path = None;
3168 Ok(())
3169 }
3170
3171 fn supports_multiple_arrays(&self) -> bool {
3172 true
3173 }
3174}
3175
3176#[derive(Default)]
3182struct Hdf5ParamIndices {
3183 compression_type: Option<usize>,
3184 z_compress_level: Option<usize>,
3185 szip_num_pixels: Option<usize>,
3186 nbit_precision: Option<usize>,
3187 nbit_offset: Option<usize>,
3188 jpeg_quality: Option<usize>,
3189 blosc_shuffle_type: Option<usize>,
3190 blosc_compressor: Option<usize>,
3191 blosc_compress_level: Option<usize>,
3192 store_attributes: Option<usize>,
3193 store_performance: Option<usize>,
3194 total_runtime: Option<usize>,
3195 total_io_speed: Option<usize>,
3196 swmr_mode: Option<usize>,
3197 swmr_flush_now: Option<usize>,
3198 swmr_running: Option<usize>,
3199 swmr_cb_counter: Option<usize>,
3200 swmr_supported: Option<usize>,
3201 flush_nth_frame: Option<usize>,
3202 chunk_size_auto: Option<usize>,
3203 n_row_chunks: Option<usize>,
3204 n_col_chunks: Option<usize>,
3205 n_frames_chunks: Option<usize>,
3206 ndattr_chunk: Option<usize>,
3207 n_extra_dims: Option<usize>,
3208 extra_dim_size: [Option<usize>; MAX_EXTRA_DIMS],
3209 extra_dim_name: [Option<usize>; MAX_EXTRA_DIMS],
3210 fill_value: Option<usize>,
3211 dim_att_datasets: Option<usize>,
3212 layout_filename: Option<usize>,
3213 layout_valid: Option<usize>,
3214 layout_error_msg: Option<usize>,
3215}
3216
3217pub struct Hdf5FileProcessor {
3219 ctrl: FilePluginController<Hdf5Writer>,
3220 hdf5_params: Hdf5ParamIndices,
3221}
3222
3223impl Hdf5FileProcessor {
3224 pub fn new() -> Self {
3225 Self {
3226 ctrl: FilePluginController::new(Hdf5Writer::new()),
3227 hdf5_params: Hdf5ParamIndices::default(),
3228 }
3229 }
3230
3231 pub fn set_dataset_name(&mut self, name: &str) {
3232 self.ctrl.writer.set_dataset_name(name);
3233 }
3234}
3235
3236fn register_hdf5_params(
3238 base: &mut asyn_rs::port::PortDriverBase,
3239) -> asyn_rs::error::AsynResult<()> {
3240 use asyn_rs::param::ParamType;
3241 base.create_param("HDF5_SWMRFlushNow", ParamType::Int32)?;
3242 base.create_param("HDF5_chunkSizeAuto", ParamType::Int32)?;
3243 base.create_param("HDF5_nRowChunks", ParamType::Int32)?;
3244 base.create_param("HDF5_nColChunks", ParamType::Int32)?;
3245 base.create_param("HDF5_chunkSize2", ParamType::Int32)?;
3246 base.create_param("HDF5_chunkSize3", ParamType::Int32)?;
3247 base.create_param("HDF5_chunkSize4", ParamType::Int32)?;
3248 base.create_param("HDF5_chunkSize5", ParamType::Int32)?;
3249 base.create_param("HDF5_chunkSize6", ParamType::Int32)?;
3250 base.create_param("HDF5_chunkSize7", ParamType::Int32)?;
3251 base.create_param("HDF5_chunkSize8", ParamType::Int32)?;
3252 base.create_param("HDF5_chunkSize9", ParamType::Int32)?;
3253 base.create_param("HDF5_nFramesChunks", ParamType::Int32)?;
3254 base.create_param("HDF5_NDAttributeChunk", ParamType::Int32)?;
3255 base.create_param("HDF5_chunkBoundaryAlign", ParamType::Int32)?;
3256 base.create_param("HDF5_chunkBoundaryThreshold", ParamType::Int32)?;
3257 base.create_param("HDF5_nExtraDims", ParamType::Int32)?;
3258 base.create_param("HDF5_extraDimSizeN", ParamType::Int32)?;
3259 base.create_param("HDF5_extraDimNameN", ParamType::Octet)?;
3260 base.create_param("HDF5_extraDimSizeX", ParamType::Int32)?;
3261 base.create_param("HDF5_extraDimNameX", ParamType::Octet)?;
3262 base.create_param("HDF5_extraDimSizeY", ParamType::Int32)?;
3263 base.create_param("HDF5_extraDimNameY", ParamType::Octet)?;
3264 base.create_param("HDF5_extraDimSize3", ParamType::Int32)?;
3265 base.create_param("HDF5_extraDimName3", ParamType::Octet)?;
3266 base.create_param("HDF5_extraDimSize4", ParamType::Int32)?;
3267 base.create_param("HDF5_extraDimName4", ParamType::Octet)?;
3268 base.create_param("HDF5_extraDimSize5", ParamType::Int32)?;
3269 base.create_param("HDF5_extraDimName5", ParamType::Octet)?;
3270 base.create_param("HDF5_extraDimSize6", ParamType::Int32)?;
3271 base.create_param("HDF5_extraDimName6", ParamType::Octet)?;
3272 base.create_param("HDF5_extraDimSize7", ParamType::Int32)?;
3273 base.create_param("HDF5_extraDimName7", ParamType::Octet)?;
3274 base.create_param("HDF5_extraDimSize8", ParamType::Int32)?;
3275 base.create_param("HDF5_extraDimName8", ParamType::Octet)?;
3276 base.create_param("HDF5_extraDimSize9", ParamType::Int32)?;
3277 base.create_param("HDF5_extraDimName9", ParamType::Octet)?;
3278 base.create_param("HDF5_storeAttributes", ParamType::Int32)?;
3279 base.create_param("HDF5_storePerformance", ParamType::Int32)?;
3280 base.create_param("HDF5_totalRuntime", ParamType::Float64)?;
3281 base.create_param("HDF5_totalIoSpeed", ParamType::Float64)?;
3282 base.create_param("HDF5_flushNthFrame", ParamType::Int32)?;
3283 base.create_param("HDF5_compressionType", ParamType::Int32)?;
3284 base.create_param("HDF5_nbitsPrecision", ParamType::Int32)?;
3285 base.create_param("HDF5_nbitsOffset", ParamType::Int32)?;
3286 base.create_param("HDF5_szipNumPixels", ParamType::Int32)?;
3287 base.create_param("HDF5_zCompressLevel", ParamType::Int32)?;
3288 base.create_param("HDF5_bloscShuffleType", ParamType::Int32)?;
3289 base.create_param("HDF5_bloscCompressor", ParamType::Int32)?;
3290 base.create_param("HDF5_bloscCompressLevel", ParamType::Int32)?;
3291 base.create_param("HDF5_jpegQuality", ParamType::Int32)?;
3292 base.create_param("HDF5_dimAttDatasets", ParamType::Int32)?;
3293 base.create_param("HDF5_layoutErrorMsg", ParamType::Octet)?;
3294 base.create_param("HDF5_layoutValid", ParamType::Int32)?;
3295 base.create_param("HDF5_layoutFilename", ParamType::Octet)?;
3296 base.create_param("HDF5_SWMRSupported", ParamType::Int32)?;
3297 base.create_param("HDF5_SWMRMode", ParamType::Int32)?;
3298 base.create_param("HDF5_SWMRRunning", ParamType::Int32)?;
3299 base.create_param("HDF5_SWMRCbCounter", ParamType::Int32)?;
3300 base.create_param("HDF5_posRunning", ParamType::Int32)?;
3301 base.create_param("HDF5_posNameDimN", ParamType::Octet)?;
3302 base.create_param("HDF5_posNameDimX", ParamType::Octet)?;
3303 base.create_param("HDF5_posNameDimY", ParamType::Octet)?;
3304 base.create_param("HDF5_posNameDim3", ParamType::Octet)?;
3305 base.create_param("HDF5_posNameDim4", ParamType::Octet)?;
3306 base.create_param("HDF5_posNameDim5", ParamType::Octet)?;
3307 base.create_param("HDF5_posNameDim6", ParamType::Octet)?;
3308 base.create_param("HDF5_posNameDim7", ParamType::Octet)?;
3309 base.create_param("HDF5_posNameDim8", ParamType::Octet)?;
3310 base.create_param("HDF5_posNameDim9", ParamType::Octet)?;
3311 base.create_param("HDF5_posIndexDimN", ParamType::Octet)?;
3312 base.create_param("HDF5_posIndexDimX", ParamType::Octet)?;
3313 base.create_param("HDF5_posIndexDimY", ParamType::Octet)?;
3314 base.create_param("HDF5_posIndexDim3", ParamType::Octet)?;
3315 base.create_param("HDF5_posIndexDim4", ParamType::Octet)?;
3316 base.create_param("HDF5_posIndexDim5", ParamType::Octet)?;
3317 base.create_param("HDF5_posIndexDim6", ParamType::Octet)?;
3318 base.create_param("HDF5_posIndexDim7", ParamType::Octet)?;
3319 base.create_param("HDF5_posIndexDim8", ParamType::Octet)?;
3320 base.create_param("HDF5_posIndexDim9", ParamType::Octet)?;
3321 base.create_param("HDF5_fillValue", ParamType::Float64)?;
3322 base.create_param("HDF5_extraDimChunkX", ParamType::Int32)?;
3323 base.create_param("HDF5_extraDimChunkY", ParamType::Int32)?;
3324 base.create_param("HDF5_extraDimChunk3", ParamType::Int32)?;
3325 base.create_param("HDF5_extraDimChunk4", ParamType::Int32)?;
3326 base.create_param("HDF5_extraDimChunk5", ParamType::Int32)?;
3327 base.create_param("HDF5_extraDimChunk6", ParamType::Int32)?;
3328 base.create_param("HDF5_extraDimChunk7", ParamType::Int32)?;
3329 base.create_param("HDF5_extraDimChunk8", ParamType::Int32)?;
3330 base.create_param("HDF5_extraDimChunk9", ParamType::Int32)?;
3331 Ok(())
3332}
3333
3334impl Default for Hdf5FileProcessor {
3335 fn default() -> Self {
3336 Self::new()
3337 }
3338}
3339
3340const EXTRA_DIM_SIZE_PARAMS: [&str; MAX_EXTRA_DIMS] = [
3342 "HDF5_extraDimSizeN",
3343 "HDF5_extraDimSizeX",
3344 "HDF5_extraDimSizeY",
3345 "HDF5_extraDimSize3",
3346 "HDF5_extraDimSize4",
3347 "HDF5_extraDimSize5",
3348 "HDF5_extraDimSize6",
3349 "HDF5_extraDimSize7",
3350 "HDF5_extraDimSize8",
3351 "HDF5_extraDimSize9",
3352];
3353
3354const EXTRA_DIM_NAME_PARAMS: [&str; MAX_EXTRA_DIMS] = [
3356 "HDF5_extraDimNameN",
3357 "HDF5_extraDimNameX",
3358 "HDF5_extraDimNameY",
3359 "HDF5_extraDimName3",
3360 "HDF5_extraDimName4",
3361 "HDF5_extraDimName5",
3362 "HDF5_extraDimName6",
3363 "HDF5_extraDimName7",
3364 "HDF5_extraDimName8",
3365 "HDF5_extraDimName9",
3366];
3367
3368impl NDPluginProcess for Hdf5FileProcessor {
3369 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
3370 let was_swmr = self.ctrl.writer.is_swmr_active();
3371 let mut result = self.ctrl.process_array(array);
3372 let is_swmr = self.ctrl.writer.is_swmr_active();
3373
3374 if was_swmr != is_swmr {
3376 if let Some(idx) = self.hdf5_params.swmr_running {
3377 result
3378 .param_updates
3379 .push(ParamUpdate::int32(idx, if is_swmr { 1 } else { 0 }));
3380 }
3381 }
3382
3383 if is_swmr {
3385 if let Some(idx) = self.hdf5_params.swmr_cb_counter {
3386 result.param_updates.push(ParamUpdate::int32(
3387 idx,
3388 self.ctrl.writer.swmr_cb_counter as i32,
3389 ));
3390 }
3391 }
3392
3393 if self.ctrl.writer.store_performance {
3395 if let Some(idx) = self.hdf5_params.total_runtime {
3396 result
3397 .param_updates
3398 .push(ParamUpdate::float64(idx, self.ctrl.writer.total_runtime));
3399 }
3400 if let Some(idx) = self.hdf5_params.total_io_speed {
3401 let speed = if self.ctrl.writer.total_runtime > 0.0 {
3402 self.ctrl.writer.total_bytes as f64
3403 / self.ctrl.writer.total_runtime
3404 / 1_000_000.0
3405 } else {
3406 0.0
3407 };
3408 result.param_updates.push(ParamUpdate::float64(idx, speed));
3409 }
3410 }
3411
3412 result
3413 }
3414
3415 fn plugin_type(&self) -> &str {
3416 "NDFileHDF5"
3417 }
3418
3419 fn compression_aware(&self) -> bool {
3425 true
3426 }
3427
3428 fn does_array_callbacks(&self) -> bool {
3431 false
3432 }
3433
3434 fn register_params(
3435 &mut self,
3436 base: &mut asyn_rs::port::PortDriverBase,
3437 ) -> asyn_rs::error::AsynResult<()> {
3438 self.ctrl.register_params(base)?;
3439 register_hdf5_params(base)?;
3440 self.hdf5_params.compression_type = base.find_param("HDF5_compressionType");
3441 self.hdf5_params.z_compress_level = base.find_param("HDF5_zCompressLevel");
3442 self.hdf5_params.szip_num_pixels = base.find_param("HDF5_szipNumPixels");
3443 self.hdf5_params.nbit_precision = base.find_param("HDF5_nbitsPrecision");
3444 self.hdf5_params.nbit_offset = base.find_param("HDF5_nbitsOffset");
3445 self.hdf5_params.jpeg_quality = base.find_param("HDF5_jpegQuality");
3446 self.hdf5_params.blosc_shuffle_type = base.find_param("HDF5_bloscShuffleType");
3447 self.hdf5_params.blosc_compressor = base.find_param("HDF5_bloscCompressor");
3448 self.hdf5_params.blosc_compress_level = base.find_param("HDF5_bloscCompressLevel");
3449 self.hdf5_params.store_attributes = base.find_param("HDF5_storeAttributes");
3450 self.hdf5_params.store_performance = base.find_param("HDF5_storePerformance");
3451 self.hdf5_params.total_runtime = base.find_param("HDF5_totalRuntime");
3452 self.hdf5_params.total_io_speed = base.find_param("HDF5_totalIoSpeed");
3453 self.hdf5_params.swmr_mode = base.find_param("HDF5_SWMRMode");
3454 self.hdf5_params.swmr_flush_now = base.find_param("HDF5_SWMRFlushNow");
3455 self.hdf5_params.swmr_running = base.find_param("HDF5_SWMRRunning");
3456 self.hdf5_params.swmr_cb_counter = base.find_param("HDF5_SWMRCbCounter");
3457 self.hdf5_params.swmr_supported = base.find_param("HDF5_SWMRSupported");
3458 self.hdf5_params.flush_nth_frame = base.find_param("HDF5_flushNthFrame");
3459 self.hdf5_params.chunk_size_auto = base.find_param("HDF5_chunkSizeAuto");
3460 self.hdf5_params.n_row_chunks = base.find_param("HDF5_nRowChunks");
3461 self.hdf5_params.n_col_chunks = base.find_param("HDF5_nColChunks");
3462 self.hdf5_params.n_frames_chunks = base.find_param("HDF5_nFramesChunks");
3463 self.hdf5_params.ndattr_chunk = base.find_param("HDF5_NDAttributeChunk");
3464 self.hdf5_params.n_extra_dims = base.find_param("HDF5_nExtraDims");
3465 for i in 0..MAX_EXTRA_DIMS {
3466 self.hdf5_params.extra_dim_size[i] = base.find_param(EXTRA_DIM_SIZE_PARAMS[i]);
3467 self.hdf5_params.extra_dim_name[i] = base.find_param(EXTRA_DIM_NAME_PARAMS[i]);
3468 }
3469 self.hdf5_params.fill_value = base.find_param("HDF5_fillValue");
3470 self.hdf5_params.dim_att_datasets = base.find_param("HDF5_dimAttDatasets");
3471 self.hdf5_params.layout_filename = base.find_param("HDF5_layoutFilename");
3472 self.hdf5_params.layout_valid = base.find_param("HDF5_layoutValid");
3473 self.hdf5_params.layout_error_msg = base.find_param("HDF5_layoutErrorMsg");
3474
3475 if let Some(idx) = self.hdf5_params.swmr_supported {
3477 base.set_int32_param(idx, 0, 1)?;
3478 }
3479 Ok(())
3480 }
3481
3482 fn on_param_change(
3483 &mut self,
3484 reason: usize,
3485 params: &PluginParamSnapshot,
3486 ) -> ParamChangeResult {
3487 if Some(reason) == self.hdf5_params.compression_type {
3489 self.ctrl.writer.set_compression_type(params.value.as_i32());
3490 return ParamChangeResult::updates(vec![]);
3491 }
3492 if Some(reason) == self.hdf5_params.z_compress_level {
3493 self.ctrl
3494 .writer
3495 .set_z_compress_level(params.value.as_i32() as u32);
3496 return ParamChangeResult::updates(vec![]);
3497 }
3498 if Some(reason) == self.hdf5_params.szip_num_pixels {
3499 self.ctrl
3500 .writer
3501 .set_szip_num_pixels(params.value.as_i32() as u32);
3502 return ParamChangeResult::updates(vec![]);
3503 }
3504 if Some(reason) == self.hdf5_params.blosc_shuffle_type {
3505 self.ctrl
3506 .writer
3507 .set_blosc_shuffle_type(params.value.as_i32());
3508 return ParamChangeResult::updates(vec![]);
3509 }
3510 if Some(reason) == self.hdf5_params.blosc_compressor {
3511 self.ctrl.writer.set_blosc_compressor(params.value.as_i32());
3512 return ParamChangeResult::updates(vec![]);
3513 }
3514 if Some(reason) == self.hdf5_params.blosc_compress_level {
3515 self.ctrl
3516 .writer
3517 .set_blosc_compress_level(params.value.as_i32() as u32);
3518 return ParamChangeResult::updates(vec![]);
3519 }
3520 if Some(reason) == self.hdf5_params.nbit_precision {
3521 self.ctrl
3522 .writer
3523 .set_nbit_precision(params.value.as_i32() as u32);
3524 return ParamChangeResult::updates(vec![]);
3525 }
3526 if Some(reason) == self.hdf5_params.nbit_offset {
3527 self.ctrl
3528 .writer
3529 .set_nbit_offset(params.value.as_i32() as u32);
3530 return ParamChangeResult::updates(vec![]);
3531 }
3532 if Some(reason) == self.hdf5_params.jpeg_quality {
3533 self.ctrl
3534 .writer
3535 .set_jpeg_quality(params.value.as_i32() as u32);
3536 return ParamChangeResult::updates(vec![]);
3537 }
3538 if Some(reason) == self.hdf5_params.store_attributes {
3539 self.ctrl
3540 .writer
3541 .set_store_attributes(params.value.as_i32() != 0);
3542 return ParamChangeResult::updates(vec![]);
3543 }
3544 if Some(reason) == self.hdf5_params.store_performance {
3545 self.ctrl
3546 .writer
3547 .set_store_performance(params.value.as_i32() != 0);
3548 return ParamChangeResult::updates(vec![]);
3549 }
3550 if Some(reason) == self.hdf5_params.chunk_size_auto {
3552 self.ctrl
3553 .writer
3554 .set_chunk_size_auto(params.value.as_i32() != 0);
3555 return ParamChangeResult::updates(vec![]);
3556 }
3557 if Some(reason) == self.hdf5_params.n_row_chunks {
3558 self.ctrl
3559 .writer
3560 .set_n_row_chunks(params.value.as_i32().max(0) as usize);
3561 return ParamChangeResult::updates(vec![]);
3562 }
3563 if Some(reason) == self.hdf5_params.n_col_chunks {
3564 self.ctrl
3565 .writer
3566 .set_n_col_chunks(params.value.as_i32().max(0) as usize);
3567 return ParamChangeResult::updates(vec![]);
3568 }
3569 if Some(reason) == self.hdf5_params.n_frames_chunks {
3570 self.ctrl
3571 .writer
3572 .set_n_frames_chunks(params.value.as_i32().max(0) as usize);
3573 return ParamChangeResult::updates(vec![]);
3574 }
3575 if Some(reason) == self.hdf5_params.ndattr_chunk {
3576 self.ctrl
3578 .writer
3579 .set_ndattr_chunk(params.value.as_i32().max(0) as usize);
3580 return ParamChangeResult::updates(vec![]);
3581 }
3582 if Some(reason) == self.hdf5_params.n_extra_dims {
3584 self.ctrl
3585 .writer
3586 .set_n_extra_dims(params.value.as_i32().max(0) as usize);
3587 return ParamChangeResult::updates(vec![]);
3588 }
3589 for i in 0..MAX_EXTRA_DIMS {
3590 if Some(reason) == self.hdf5_params.extra_dim_size[i] {
3591 self.ctrl
3592 .writer
3593 .set_extra_dim_size(i, params.value.as_i32().max(1) as usize);
3594 return ParamChangeResult::updates(vec![]);
3595 }
3596 if Some(reason) == self.hdf5_params.extra_dim_name[i] {
3597 self.ctrl
3598 .writer
3599 .set_extra_dim_name(i, params.value.as_string().unwrap_or(""));
3600 return ParamChangeResult::updates(vec![]);
3601 }
3602 }
3603 if Some(reason) == self.hdf5_params.fill_value {
3604 self.ctrl.writer.set_fill_value(params.value.as_f64());
3605 return ParamChangeResult::updates(vec![]);
3606 }
3607 if Some(reason) == self.hdf5_params.dim_att_datasets {
3608 self.ctrl
3609 .writer
3610 .set_dim_att_datasets(params.value.as_i32() != 0);
3611 return ParamChangeResult::updates(vec![]);
3612 }
3613 if Some(reason) == self.hdf5_params.layout_filename {
3615 let path = params.value.as_string().unwrap_or("").to_string();
3616 self.ctrl.writer.set_layout_filename(&path);
3617 let mut updates = vec![];
3618 if let Some(idx) = self.hdf5_params.layout_valid {
3619 updates.push(ParamUpdate::int32(
3620 idx,
3621 if self.ctrl.writer.layout_valid { 1 } else { 0 },
3622 ));
3623 }
3624 if let Some(idx) = self.hdf5_params.layout_error_msg {
3625 updates.push(ParamUpdate::Octet {
3626 reason: idx,
3627 addr: 0,
3628 value: self.ctrl.writer.layout_error.clone(),
3629 });
3630 }
3631 return ParamChangeResult::updates(updates);
3632 }
3633 if Some(reason) == self.hdf5_params.swmr_mode {
3635 self.ctrl.writer.set_swmr_mode(params.value.as_i32() != 0);
3636 return ParamChangeResult::updates(vec![]);
3637 }
3638 if Some(reason) == self.hdf5_params.swmr_flush_now {
3639 if params.value.as_i32() != 0 {
3640 self.ctrl.writer.flush_swmr();
3641 let mut updates = vec![];
3642 if let Some(idx) = self.hdf5_params.swmr_cb_counter {
3643 updates.push(ParamUpdate::int32(
3644 idx,
3645 self.ctrl.writer.swmr_cb_counter as i32,
3646 ));
3647 }
3648 return ParamChangeResult::updates(updates);
3649 }
3650 return ParamChangeResult::updates(vec![]);
3651 }
3652 if Some(reason) == self.hdf5_params.flush_nth_frame {
3653 self.ctrl
3654 .writer
3655 .set_flush_nth_frame(params.value.as_i32().max(0) as usize);
3656 return ParamChangeResult::updates(vec![]);
3657 }
3658 self.ctrl.on_param_change(reason, params)
3659 }
3660}
3661
3662#[cfg(test)]
3663mod tests {
3664 use super::*;
3665 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
3666 use rust_hdf5::format::messages::filter::FILTER_LZ4;
3667 use std::sync::atomic::{AtomicU32, Ordering};
3668
3669 static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);
3670
3671 fn temp_path(prefix: &str) -> PathBuf {
3672 let n = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
3673 std::env::temp_dir().join(format!("adcore_test_{}_{}.h5", prefix, n))
3674 }
3675
3676 #[test]
3677 fn test_write_single_frame() {
3678 let path = temp_path("hdf5_single");
3679 let mut writer = Hdf5Writer::new();
3680
3681 let mut arr = NDArray::new(
3682 vec![NDDimension::new(4), NDDimension::new(4)],
3683 NDDataType::UInt8,
3684 );
3685 if let NDDataBuffer::U8(ref mut v) = arr.data {
3686 for i in 0..16 {
3687 v[i] = i as u8;
3688 }
3689 }
3690
3691 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
3692 writer.write_file(&arr).unwrap();
3693 writer.close_file().unwrap();
3694
3695 let h5 = H5File::open(&path).unwrap();
3697 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3698 assert_eq!(ds.shape(), vec![1, 4, 4]);
3699 let data: Vec<u8> = ds.read_raw().unwrap();
3700 assert_eq!(data[0], 0);
3701 assert_eq!(data[15], 15);
3702 drop(h5);
3703
3704 let mut reader = Hdf5Writer::new();
3705 reader.current_path = Some(path.clone());
3706 let read_arr = reader.read_file().unwrap();
3707 assert_eq!(read_arr.dims.len(), 3);
3708 assert_eq!(read_arr.dims[2].size, 1); std::fs::remove_file(&path).ok();
3711 }
3712
3713 #[test]
3714 fn test_write_multiple_frames() {
3715 let path = temp_path("hdf5_multi");
3716 let mut writer = Hdf5Writer::new();
3717
3718 let mut arr = NDArray::new(
3719 vec![NDDimension::new(4), NDDimension::new(4)],
3720 NDDataType::UInt8,
3721 );
3722 for f in 0..3u8 {
3724 if let NDDataBuffer::U8(ref mut v) = arr.data {
3725 for x in v.iter_mut() {
3726 *x = f;
3727 }
3728 }
3729 if f == 0 {
3730 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3731 }
3732 writer.write_file(&arr).unwrap();
3733 }
3734 writer.close_file().unwrap();
3735
3736 assert!(writer.supports_multiple_arrays());
3737 assert_eq!(writer.frame_count(), 3);
3738
3739 let data = std::fs::read(&path).unwrap();
3740 assert_eq!(&data[0..8], b"\x89HDF\r\n\x1a\n");
3741
3742 let h5 = H5File::open(&path).unwrap();
3744 let names = h5.dataset_names();
3745 assert!(names.contains(&"entry/instrument/detector/data".to_string()));
3746 assert!(
3747 !names.contains(&"data_1".to_string()),
3748 "must not write per-frame datasets"
3749 );
3750 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3751 assert_eq!(
3752 ds.shape(),
3753 vec![3, 4, 4],
3754 "rank/shape must be [nframes,Y,X]"
3755 );
3756 let raw: Vec<u8> = ds.read_raw().unwrap();
3757 assert_eq!(raw.len(), 3 * 4 * 4);
3758 assert_eq!(raw[0], 0);
3760 assert_eq!(raw[16], 1);
3761 assert_eq!(raw[32], 2);
3762
3763 std::fs::remove_file(&path).ok();
3764 }
3765
3766 #[test]
3767 fn test_sub_frame_chunking() {
3768 let path = temp_path("hdf5_subchunk");
3772 let mut writer = Hdf5Writer::new();
3773 writer.set_chunk_size_auto(false); writer.set_n_row_chunks(4); writer.set_n_col_chunks(4); let mut arr = NDArray::new(
3778 vec![NDDimension::new(8), NDDimension::new(8)],
3779 NDDataType::UInt16,
3780 );
3781 for f in 0..3u16 {
3782 if let NDDataBuffer::U16(ref mut v) = arr.data {
3783 for (i, x) in v.iter_mut().enumerate() {
3784 *x = f * 1000 + i as u16;
3785 }
3786 }
3787 if f == 0 {
3788 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3789 }
3790 writer.write_file(&arr).unwrap();
3791 }
3792 writer.close_file().unwrap();
3793
3794 let h5 = H5File::open(&path).unwrap();
3795 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3796 assert_eq!(ds.shape(), vec![3, 8, 8], "shape must not be chunk-padded");
3797 assert_eq!(
3798 ds.chunk_dims(),
3799 Some(vec![1, 4, 4]),
3800 "chunk grid must be the sub-frame tile size"
3801 );
3802 let raw: Vec<u16> = ds.read_raw().unwrap();
3803 assert_eq!(raw.len(), 3 * 64);
3804 for f in 0..3u16 {
3805 for i in 0..64usize {
3806 assert_eq!(
3807 raw[f as usize * 64 + i],
3808 f * 1000 + i as u16,
3809 "frame {} elem {}",
3810 f,
3811 i
3812 );
3813 }
3814 }
3815
3816 std::fs::remove_file(&path).ok();
3817 }
3818
3819 #[test]
3820 fn test_sub_frame_chunking_with_compression() {
3821 let path = temp_path("hdf5_subchunk_zlib");
3824 let mut writer = Hdf5Writer::new();
3825 writer.set_chunk_size_auto(false);
3826 writer.set_n_row_chunks(4);
3827 writer.set_n_col_chunks(4);
3828 writer.set_compression_type(COMPRESS_ZLIB);
3829
3830 let mut arr = NDArray::new(
3831 vec![NDDimension::new(8), NDDimension::new(8)],
3832 NDDataType::UInt16,
3833 );
3834 for f in 0..2u16 {
3835 if let NDDataBuffer::U16(ref mut v) = arr.data {
3836 for (i, x) in v.iter_mut().enumerate() {
3837 *x = f * 100 + i as u16;
3838 }
3839 }
3840 if f == 0 {
3841 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3842 }
3843 writer.write_file(&arr).unwrap();
3844 }
3845 writer.close_file().unwrap();
3846
3847 let h5 = H5File::open(&path).unwrap();
3848 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3849 assert_eq!(ds.shape(), vec![2, 8, 8]);
3850 assert_eq!(ds.chunk_dims(), Some(vec![1, 4, 4]));
3851 let raw: Vec<u16> = ds.read_raw().unwrap();
3852 for f in 0..2u16 {
3853 for i in 0..64usize {
3854 assert_eq!(raw[f as usize * 64 + i], f * 100 + i as u16);
3855 }
3856 }
3857
3858 std::fs::remove_file(&path).ok();
3859 }
3860
3861 #[test]
3862 fn test_non_dividing_chunk_is_honored_and_extent_trimmed() {
3863 let path = temp_path("hdf5_subchunk_nd");
3867 let mut writer = Hdf5Writer::new();
3868 writer.set_chunk_size_auto(false); writer.set_n_row_chunks(3); writer.set_n_col_chunks(4); let mut arr = NDArray::new(
3873 vec![NDDimension::new(8), NDDimension::new(8)],
3874 NDDataType::UInt16,
3875 );
3876 if let NDDataBuffer::U16(ref mut v) = arr.data {
3877 for (i, x) in v.iter_mut().enumerate() {
3878 *x = i as u16;
3879 }
3880 }
3881 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3882 writer.write_file(&arr).unwrap();
3883 writer.write_file(&arr).unwrap();
3884 writer.close_file().unwrap();
3885
3886 let h5 = H5File::open(&path).unwrap();
3887 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3888 assert_eq!(ds.shape(), vec![2, 8, 8], "extent trimmed, not padded");
3889 assert_eq!(ds.chunk_dims(), Some(vec![1, 3, 4]));
3890 let raw: Vec<u16> = ds.read_raw().unwrap();
3891 assert_eq!(raw.len(), 2 * 64);
3892 for i in 0..64usize {
3893 assert_eq!(raw[i], i as u16);
3894 assert_eq!(raw[64 + i], i as u16);
3895 }
3896
3897 std::fs::remove_file(&path).ok();
3898 }
3899
3900 #[test]
3901 fn test_n_frames_chunks_band() {
3902 let path = temp_path("hdf5_framechunks");
3905 let mut writer = Hdf5Writer::new();
3906 writer.set_chunk_size_auto(false);
3907 writer.set_n_frames_chunks(2); let mut arr = NDArray::new(
3910 vec![NDDimension::new(4), NDDimension::new(4)],
3911 NDDataType::UInt16,
3912 );
3913 for f in 0..5u16 {
3915 if let NDDataBuffer::U16(ref mut v) = arr.data {
3916 for (i, x) in v.iter_mut().enumerate() {
3917 *x = f * 1000 + i as u16;
3918 }
3919 }
3920 if f == 0 {
3921 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3922 }
3923 writer.write_file(&arr).unwrap();
3924 }
3925 writer.close_file().unwrap();
3926
3927 let h5 = H5File::open(&path).unwrap();
3928 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3929 assert_eq!(ds.shape(), vec![5, 4, 4], "exact frame count, no padding");
3930 assert_eq!(ds.chunk_dims(), Some(vec![2, 4, 4]));
3931 let raw: Vec<u16> = ds.read_raw().unwrap();
3932 for f in 0..5u16 {
3933 for i in 0..16usize {
3934 assert_eq!(raw[f as usize * 16 + i], f * 1000 + i as u16);
3935 }
3936 }
3937
3938 std::fs::remove_file(&path).ok();
3939 }
3940
3941 #[test]
3942 fn test_frames_chunks_with_sub_frame_tiles() {
3943 let path = temp_path("hdf5_full_chunk");
3947 let mut writer = Hdf5Writer::new();
3948 writer.set_chunk_size_auto(false);
3949 writer.set_n_frames_chunks(2); writer.set_n_row_chunks(4); writer.set_n_col_chunks(4); let mut arr = NDArray::new(
3954 vec![NDDimension::new(8), NDDimension::new(8)],
3955 NDDataType::UInt16,
3956 );
3957 for f in 0..3u16 {
3959 if let NDDataBuffer::U16(ref mut v) = arr.data {
3960 for (i, x) in v.iter_mut().enumerate() {
3961 *x = f * 1000 + i as u16;
3962 }
3963 }
3964 if f == 0 {
3965 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3966 }
3967 writer.write_file(&arr).unwrap();
3968 }
3969 writer.close_file().unwrap();
3970
3971 let h5 = H5File::open(&path).unwrap();
3972 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3973 assert_eq!(ds.shape(), vec![3, 8, 8], "exact frame count");
3974 assert_eq!(ds.chunk_dims(), Some(vec![2, 4, 4]));
3975 let raw: Vec<u16> = ds.read_raw().unwrap();
3976 assert_eq!(raw.len(), 3 * 64);
3977 for f in 0..3u16 {
3978 for i in 0..64usize {
3979 assert_eq!(
3980 raw[f as usize * 64 + i],
3981 f * 1000 + i as u16,
3982 "frame {} elem {}",
3983 f,
3984 i
3985 );
3986 }
3987 }
3988
3989 std::fs::remove_file(&path).ok();
3990 }
3991
3992 #[test]
3993 fn test_attribute_datasets() {
3994 let path = temp_path("hdf5_attr_ds");
3995 let mut writer = Hdf5Writer::new();
3996
3997 let mk = |exposure: f64, count: i32| {
3998 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
3999 arr.attributes.add(NDAttribute::new_static(
4000 "exposure",
4001 "",
4002 NDAttrSource::Driver,
4003 NDAttrValue::Float64(exposure),
4004 ));
4005 arr.attributes.add(NDAttribute::new_static(
4006 "count",
4007 "",
4008 NDAttrSource::Driver,
4009 NDAttrValue::Int32(count),
4010 ));
4011 arr
4012 };
4013
4014 let a0 = mk(0.5, 10);
4015 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4016 writer.write_file(&a0).unwrap();
4017 writer.write_file(&mk(0.75, 20)).unwrap();
4018 writer.write_file(&mk(1.25, 30)).unwrap();
4019 writer.close_file().unwrap();
4020
4021 let h5 = H5File::open(&path).unwrap();
4022 let exp = h5
4024 .dataset("entry/instrument/NDAttributes/exposure")
4025 .unwrap();
4026 assert_eq!(exp.shape(), vec![3]);
4027 let exp_vals: Vec<f64> = exp.read_raw().unwrap();
4028 assert_eq!(exp_vals, vec![0.5, 0.75, 1.25]);
4029
4030 let cnt = h5.dataset("entry/instrument/NDAttributes/count").unwrap();
4031 assert_eq!(cnt.shape(), vec![3]);
4032 let cnt_vals: Vec<i32> = cnt.read_raw().unwrap();
4034 assert_eq!(cnt_vals, vec![10, 20, 30]);
4035
4036 std::fs::remove_file(&path).ok();
4037 }
4038
4039 #[test]
4040 fn test_attribute_dataset_chunk_matches_capture_target() {
4041 let path = temp_path("hdf5_attr_chunk_cap");
4048 let mut writer = Hdf5Writer::new();
4049 writer.set_num_capture(10);
4050
4051 let mk = |c: i32| {
4052 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4053 arr.attributes.add(NDAttribute::new_static(
4054 "count",
4055 "",
4056 NDAttrSource::Driver,
4057 NDAttrValue::Int32(c),
4058 ));
4059 arr
4060 };
4061
4062 let a0 = mk(1);
4063 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4064 writer.write_file(&a0).unwrap();
4065 writer.write_file(&mk(2)).unwrap();
4066 writer.write_file(&mk(3)).unwrap();
4067 writer.close_file().unwrap();
4068
4069 let h5 = H5File::open(&path).unwrap();
4070 let ds = h5.dataset("entry/instrument/NDAttributes/count").unwrap();
4071 assert_eq!(ds.shape(), vec![3]);
4072 assert_eq!(ds.chunk_dims(), Some(vec![10]));
4073 let vals: Vec<i32> = ds.read_raw().unwrap();
4074 assert_eq!(vals, vec![1, 2, 3]);
4075 std::fs::remove_file(&path).ok();
4076 }
4077
4078 #[test]
4079 fn test_attribute_dataset_chunk_single_mode_is_one() {
4080 let path = temp_path("hdf5_attr_chunk_single");
4084 let mut writer = Hdf5Writer::new();
4085 writer.set_num_capture(64);
4086
4087 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4088 arr.attributes.add(NDAttribute::new_static(
4089 "count",
4090 "",
4091 NDAttrSource::Driver,
4092 NDAttrValue::Int32(7),
4093 ));
4094 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4095 writer.write_file(&arr).unwrap();
4096 writer.close_file().unwrap();
4097
4098 let h5 = H5File::open(&path).unwrap();
4099 let ds = h5.dataset("entry/instrument/NDAttributes/count").unwrap();
4100 assert_eq!(ds.chunk_dims(), Some(vec![1]));
4101 std::fs::remove_file(&path).ok();
4102 }
4103
4104 #[test]
4105 fn test_string_attribute_dataset_is_rank1_fixed_string() {
4106 let path = temp_path("hdf5_attr_str");
4111 let mut writer = Hdf5Writer::new();
4112
4113 let mk = |mode_name: &str| {
4114 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4115 arr.attributes.add(NDAttribute::new_static(
4116 "ColorMode",
4117 "",
4118 NDAttrSource::Driver,
4119 NDAttrValue::String(mode_name.to_string()),
4120 ));
4121 arr
4122 };
4123
4124 let a0 = mk("Mono");
4125 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4126 writer.write_file(&a0).unwrap();
4127 writer.write_file(&mk("RGB1")).unwrap();
4128 writer.close_file().unwrap();
4129
4130 let h5 = H5File::open(&path).unwrap();
4131 let ds = h5
4134 .dataset("entry/instrument/detector/NDAttributes/ColorMode")
4135 .unwrap();
4136 assert_eq!(ds.shape(), vec![2]);
4138 assert_eq!(ds.element_size(), MAX_ATTRIBUTE_STRING_SIZE);
4139
4140 let frames: Vec<FixedStr256> = ds.read_raw().unwrap();
4141 assert_eq!(frames.len(), 2);
4142 let decode = |f: &FixedStr256| -> String {
4143 f.0.iter()
4144 .take_while(|&&b| b != 0)
4145 .map(|&b| b as char)
4146 .collect()
4147 };
4148 assert_eq!(decode(&frames[0]), "Mono");
4149 assert_eq!(decode(&frames[1]), "RGB1");
4150 std::fs::remove_file(&path).ok();
4151 }
4152
4153 #[test]
4154 fn test_ndattribute_dataset_routed_to_declared_group() {
4155 let path = temp_path("hdf5_attr_route");
4161 let mut writer = Hdf5Writer::new();
4162
4163 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4164 arr.attributes.add(NDAttribute::new_static(
4165 "ColorMode",
4166 "",
4167 NDAttrSource::Driver,
4168 NDAttrValue::String("Mono".to_string()),
4169 ));
4170 arr.attributes.add(NDAttribute::new_static(
4171 "count",
4172 "",
4173 NDAttrSource::Driver,
4174 NDAttrValue::Int32(7),
4175 ));
4176
4177 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4178 writer.write_file(&arr).unwrap();
4179 writer.close_file().unwrap();
4180
4181 let h5 = H5File::open(&path).unwrap();
4182 assert!(
4184 h5.dataset("entry/instrument/detector/NDAttributes/ColorMode")
4185 .is_ok()
4186 );
4187 assert!(h5.dataset("entry/instrument/NDAttributes/count").is_ok());
4189 assert!(
4191 h5.dataset("entry/instrument/NDAttributes/ColorMode")
4192 .is_err()
4193 );
4194 std::fs::remove_file(&path).ok();
4195 }
4196
4197 #[test]
4198 fn test_ndattribute_element_attr_open_close_values() {
4199 let dir = std::env::temp_dir();
4205 let layout = dir.join("adcore_layout_elem_attr.xml");
4206 std::fs::write(
4207 &layout,
4208 r#"<hdf5_layout>
4209 <group name="entry">
4210 <group name="instrument">
4211 <group name="detector">
4212 <attribute name="FirstColorMode" source="ndattribute" ndattribute="ColorMode" when="OnFileOpen"/>
4213 <attribute name="LastColorMode" source="ndattribute" ndattribute="ColorMode" when="OnFileClose"/>
4214 <attribute name="DefaultColorMode" source="ndattribute" ndattribute="ColorMode"/>
4215 <dataset name="data" source="detector" det_default="true">
4216 <attribute name="GainAtOpen" source="ndattribute" ndattribute="Gain" when="OnFileOpen"/>
4217 <attribute name="GainAtClose" source="ndattribute" ndattribute="Gain" when="OnFileClose"/>
4218 </dataset>
4219 </group>
4220 <group name="NDAttributes" ndattr_default="true"/>
4221 </group>
4222 </group>
4223 </hdf5_layout>"#,
4224 )
4225 .unwrap();
4226
4227 let path = temp_path("hdf5_elem_attr");
4228 let mut writer = Hdf5Writer::new();
4229 assert!(
4230 writer.set_layout_filename(layout.to_str().unwrap()),
4231 "layout XML must parse: {}",
4232 writer.layout_error
4233 );
4234
4235 let mk = |mode: &str, gain: i32| {
4236 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4237 arr.attributes.add(NDAttribute::new_static(
4238 "ColorMode",
4239 "",
4240 NDAttrSource::Driver,
4241 NDAttrValue::String(mode.to_string()),
4242 ));
4243 arr.attributes.add(NDAttribute::new_static(
4244 "Gain",
4245 "",
4246 NDAttrSource::Driver,
4247 NDAttrValue::Int32(gain),
4248 ));
4249 arr
4250 };
4251
4252 let a0 = mk("Mono", 10);
4253 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4254 writer.write_file(&a0).unwrap();
4255 writer.write_file(&mk("RGB1", 20)).unwrap();
4256 writer.write_file(&mk("Bayer", 30)).unwrap();
4257 writer.close_file().unwrap();
4258
4259 let h5 = H5File::open(&path).unwrap();
4260
4261 let mut grp = h5.root_group();
4263 for seg in ["entry", "instrument", "detector"] {
4264 grp = grp.group(seg).unwrap();
4265 }
4266 assert_eq!(grp.attr_string("FirstColorMode").unwrap(), "Mono");
4268 assert_eq!(grp.attr_string("DefaultColorMode").unwrap(), "Mono");
4269 assert_eq!(grp.attr_string("LastColorMode").unwrap(), "Bayer");
4271
4272 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4274 assert_eq!(
4275 ds.attr("GainAtOpen")
4276 .unwrap()
4277 .read_numeric::<i32>()
4278 .unwrap(),
4279 10
4280 );
4281 assert_eq!(
4282 ds.attr("GainAtClose")
4283 .unwrap()
4284 .read_numeric::<i32>()
4285 .unwrap(),
4286 30
4287 );
4288
4289 std::fs::remove_file(&path).ok();
4290 std::fs::remove_file(&layout).ok();
4291 }
4292
4293 #[test]
4294 fn test_ndattr_element_attr_on_lazily_created_dataset() {
4295 let dir = std::env::temp_dir();
4303 let layout = dir.join("adcore_layout_lazy_ds_attr.xml");
4304 std::fs::write(
4305 &layout,
4306 r#"<hdf5_layout>
4307 <group name="entry">
4308 <group name="instrument">
4309 <group name="detector">
4310 <dataset name="data" source="detector" det_default="true"/>
4311 <dataset name="GainTrace" source="ndattribute" ndattribute="Gain">
4312 <attribute name="ModeAtOpen" source="ndattribute" ndattribute="ColorMode" when="OnFileOpen"/>
4313 <attribute name="ModeAtClose" source="ndattribute" ndattribute="ColorMode" when="OnFileClose"/>
4314 </dataset>
4315 </group>
4316 <group name="NDAttributes" ndattr_default="true"/>
4317 </group>
4318 </group>
4319 </hdf5_layout>"#,
4320 )
4321 .unwrap();
4322
4323 let path = temp_path("hdf5_lazy_ds_attr");
4324 let mut writer = Hdf5Writer::new();
4325 assert!(
4326 writer.set_layout_filename(layout.to_str().unwrap()),
4327 "layout XML must parse: {}",
4328 writer.layout_error
4329 );
4330
4331 let mk = |mode: &str, gain: i32| {
4332 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4333 arr.attributes.add(NDAttribute::new_static(
4334 "ColorMode",
4335 "",
4336 NDAttrSource::Driver,
4337 NDAttrValue::String(mode.to_string()),
4338 ));
4339 arr.attributes.add(NDAttribute::new_static(
4340 "Gain",
4341 "",
4342 NDAttrSource::Driver,
4343 NDAttrValue::Int32(gain),
4344 ));
4345 arr
4346 };
4347
4348 let a0 = mk("Mono", 10);
4349 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4350 writer.write_file(&a0).unwrap();
4351 writer.write_file(&mk("RGB1", 20)).unwrap();
4352 writer.write_file(&mk("Bayer", 30)).unwrap();
4353 writer.close_file().unwrap();
4354
4355 let h5 = H5File::open(&path).unwrap();
4356 let ds = h5.dataset("entry/instrument/detector/GainTrace").unwrap();
4359 assert_eq!(
4360 ds.attr("ModeAtOpen").unwrap().read_string().unwrap(),
4361 "Mono"
4362 );
4363 assert_eq!(
4364 ds.attr("ModeAtClose").unwrap().read_string().unwrap(),
4365 "Bayer"
4366 );
4367 std::fs::remove_file(&path).ok();
4368 std::fs::remove_file(&layout).ok();
4369 }
4370
4371 #[test]
4372 fn test_swmr_ndattr_element_attr_on_streaming_dataset() {
4373 let dir = std::env::temp_dir();
4379 let layout = dir.join("adcore_layout_swmr_ds_attr.xml");
4380 std::fs::write(
4381 &layout,
4382 r#"<hdf5_layout>
4383 <group name="entry">
4384 <group name="instrument">
4385 <group name="detector">
4386 <dataset name="data" source="detector" det_default="true">
4387 <attribute name="ModeAtOpen" source="ndattribute" ndattribute="ColorMode" when="OnFileOpen"/>
4388 </dataset>
4389 </group>
4390 <group name="NDAttributes" ndattr_default="true"/>
4391 </group>
4392 </group>
4393 </hdf5_layout>"#,
4394 )
4395 .unwrap();
4396
4397 let path = temp_path("hdf5_swmr_ds_attr");
4398 let mut writer = Hdf5Writer::new();
4399 writer.set_swmr_mode(true);
4400 assert!(
4401 writer.set_layout_filename(layout.to_str().unwrap()),
4402 "layout XML must parse: {}",
4403 writer.layout_error
4404 );
4405
4406 let mk = |mode: &str| {
4407 let mut arr = NDArray::new(
4408 vec![NDDimension::new(4), NDDimension::new(4)],
4409 NDDataType::UInt16,
4410 );
4411 arr.attributes.add(NDAttribute::new_static(
4412 "ColorMode",
4413 "",
4414 NDAttrSource::Driver,
4415 NDAttrValue::String(mode.to_string()),
4416 ));
4417 arr
4418 };
4419
4420 let a0 = mk("Mono");
4421 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4422 writer.write_file(&a0).unwrap();
4423 writer.write_file(&mk("RGB1")).unwrap();
4424 writer.close_file().unwrap();
4425
4426 let h5 = H5File::open(&path).unwrap();
4427 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4428 assert_eq!(
4429 ds.attr("ModeAtOpen").unwrap().read_string().unwrap(),
4430 "Mono"
4431 );
4432 std::fs::remove_file(&path).ok();
4433 std::fs::remove_file(&layout).ok();
4434 }
4435
4436 #[test]
4437 fn test_swmr_ndarray_default_attrs_on_streaming_dataset() {
4438 let path = temp_path("hdf5_swmr_dimattr_1d");
4448 let mut writer = Hdf5Writer::new();
4449 writer.set_swmr_mode(true);
4450 let mut d = NDDimension::new(8);
4451 d.offset = 3;
4452 d.binning = 2;
4453 d.reverse = true;
4454 let arr = NDArray::new(vec![d], NDDataType::UInt16);
4455 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4456 writer.write_file(&arr).unwrap();
4457 writer.write_file(&arr).unwrap();
4458 writer.close_file().unwrap();
4459 let h5 = H5File::open(&path).unwrap();
4460 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4461 let geti = |n: &str| -> i32 { ds.attr(n).unwrap().read_numeric().unwrap() };
4462 assert_eq!(geti("NDArrayNumDims"), 1);
4463 assert_eq!(geti("NDArrayDimOffset"), 3);
4464 assert_eq!(geti("NDArrayDimBinning"), 2);
4465 assert_eq!(geti("NDArrayDimReverse"), 1);
4466 std::fs::remove_file(&path).ok();
4467
4468 let path = temp_path("hdf5_swmr_dimattr_2d");
4471 let mut writer = Hdf5Writer::new();
4472 writer.set_swmr_mode(true);
4473 let mut d0 = NDDimension::new(4);
4474 d0.offset = 3;
4475 d0.binning = 2;
4476 d0.reverse = true;
4477 let mut d1 = NDDimension::new(8);
4478 d1.offset = 5;
4479 d1.binning = 4;
4480 d1.reverse = false;
4481 let arr = NDArray::new(vec![d0, d1], NDDataType::UInt16);
4482 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4483 writer.write_file(&arr).unwrap();
4484 writer.write_file(&arr).unwrap();
4485 writer.close_file().unwrap();
4486 let h5 = H5File::open(&path).unwrap();
4487 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4488 let numdims: i32 = ds.attr("NDArrayNumDims").unwrap().read_numeric().unwrap();
4489 assert_eq!(numdims, 2);
4490 let read_i32_arr = |n: &str| -> Vec<i32> {
4491 let raw = ds.attr(n).unwrap().read_raw().unwrap();
4492 assert_eq!(raw.len(), 2 * 4, "{n} must be a 2-element int32 array");
4493 raw.chunks_exact(4)
4494 .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
4495 .collect()
4496 };
4497 assert_eq!(read_i32_arr("NDArrayDimOffset"), vec![3, 5]);
4498 assert_eq!(read_i32_arr("NDArrayDimBinning"), vec![2, 4]);
4499 assert_eq!(read_i32_arr("NDArrayDimReverse"), vec![1, 0]);
4500 std::fs::remove_file(&path).ok();
4501 }
4502
4503 #[test]
4504 fn test_ndattr_descriptor_attributes_written() {
4505 let path = temp_path("hdf5_attr_desc");
4509 let mut writer = Hdf5Writer::new();
4510
4511 let mk = |v: f64| {
4512 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4513 arr.attributes.add(NDAttribute::new_static(
4514 "AcquireTime",
4515 "exposure time",
4516 NDAttrSource::EpicsPV("13SIM1:cam1:AcquireTime_RBV".to_string()),
4517 NDAttrValue::Float64(v),
4518 ));
4519 arr
4520 };
4521
4522 let a0 = mk(0.1);
4523 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4524 writer.write_file(&a0).unwrap();
4525 writer.write_file(&mk(0.2)).unwrap();
4526 writer.close_file().unwrap();
4527
4528 let h5 = H5File::open(&path).unwrap();
4529 let ds = h5
4530 .dataset("entry/instrument/NDAttributes/AcquireTime")
4531 .unwrap();
4532 let read = |n: &str| ds.attr(n).unwrap().read_string().unwrap();
4533 assert_eq!(read("NDAttrName"), "AcquireTime");
4534 assert_eq!(read("NDAttrDescription"), "exposure time");
4535 assert_eq!(read("NDAttrSourceType"), "NDAttrSourceEPICSPV");
4536 assert_eq!(read("NDAttrSource"), "13SIM1:cam1:AcquireTime_RBV");
4537 std::fs::remove_file(&path).ok();
4538 }
4539
4540 #[test]
4541 fn test_detector_dataset_ndarray_dim_attributes() {
4542 let path = temp_path("hdf5_dimattr_1d");
4550 let mut writer = Hdf5Writer::new();
4551 let mut d = NDDimension::new(8);
4552 d.offset = 3;
4553 d.binning = 2;
4554 d.reverse = true;
4555 let arr = NDArray::new(vec![d], NDDataType::UInt16);
4556 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4557 writer.write_file(&arr).unwrap();
4558 writer.close_file().unwrap();
4559 let h5 = H5File::open(&path).unwrap();
4560 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4561 let geti = |n: &str| -> i32 { ds.attr(n).unwrap().read_numeric().unwrap() };
4562 assert_eq!(geti("NDArrayNumDims"), 1);
4563 assert_eq!(geti("NDArrayDimOffset"), 3);
4564 assert_eq!(geti("NDArrayDimBinning"), 2);
4565 assert_eq!(geti("NDArrayDimReverse"), 1);
4566 std::fs::remove_file(&path).ok();
4567
4568 let path = temp_path("hdf5_dimattr_2d");
4572 let mut writer = Hdf5Writer::new();
4573 let mut d0 = NDDimension::new(4);
4574 d0.offset = 3;
4575 d0.binning = 2;
4576 d0.reverse = true;
4577 let mut d1 = NDDimension::new(8);
4578 d1.offset = 5;
4579 d1.binning = 4;
4580 d1.reverse = false;
4581 let arr = NDArray::new(vec![d0, d1], NDDataType::UInt16);
4582 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4583 writer.write_file(&arr).unwrap();
4584 writer.close_file().unwrap();
4585 let h5 = H5File::open(&path).unwrap();
4586 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4587 let numdims: i32 = ds.attr("NDArrayNumDims").unwrap().read_numeric().unwrap();
4588 assert_eq!(numdims, 2);
4589 let read_i32_arr = |n: &str| -> Vec<i32> {
4593 let raw = ds.attr(n).unwrap().read_raw().unwrap();
4594 assert_eq!(raw.len(), 2 * 4, "{n} must be a 2-element int32 array");
4595 raw.chunks_exact(4)
4596 .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
4597 .collect()
4598 };
4599 assert_eq!(read_i32_arr("NDArrayDimOffset"), vec![3, 5]);
4600 assert_eq!(read_i32_arr("NDArrayDimBinning"), vec![2, 4]);
4601 assert_eq!(read_i32_arr("NDArrayDimReverse"), vec![1, 0]);
4602 std::fs::remove_file(&path).ok();
4603 }
4604
4605 #[test]
4606 fn test_fill_value_recorded_on_dataset() {
4607 let path = temp_path("hdf5_fill");
4612 let mut writer = Hdf5Writer::new();
4613 writer.set_fill_value(7.5);
4614
4615 let arr = NDArray::new(
4616 vec![NDDimension::new(4), NDDimension::new(4)],
4617 NDDataType::UInt16,
4618 );
4619 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4620 writer.write_file(&arr).unwrap();
4621 writer.close_file().unwrap();
4622
4623 let h5 = H5File::open(&path).unwrap();
4624 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4625 let fv: f64 = ds.attr("HDF5_fillValue").unwrap().read_numeric().unwrap();
4626 assert_eq!(fv, 7.5);
4627 std::fs::remove_file(&path).ok();
4628
4629 let path2 = temp_path("hdf5_fill_dcpl");
4632 {
4633 let f = H5File::create(&path2).unwrap();
4634 let _ = f
4635 .new_dataset::<i32>()
4636 .shape(&[8][..])
4637 .fill_value(42i32)
4638 .create("unwritten")
4639 .unwrap();
4640 }
4641 let h5b = H5File::open(&path2).unwrap();
4642 let vals: Vec<i32> = h5b.dataset("unwritten").unwrap().read_raw().unwrap();
4643 assert_eq!(vals, vec![42i32; 8]);
4644 std::fs::remove_file(&path2).ok();
4645 }
4646
4647 #[test]
4648 fn test_performance_dataset() {
4649 let path = temp_path("hdf5_perf");
4650 let mut writer = Hdf5Writer::new();
4651 writer.set_store_performance(true);
4652
4653 let arr = NDArray::new(
4654 vec![NDDimension::new(8), NDDimension::new(8)],
4655 NDDataType::UInt16,
4656 );
4657 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4658 writer.write_file(&arr).unwrap();
4659 writer.write_file(&arr).unwrap();
4660 writer.close_file().unwrap();
4661
4662 let h5 = H5File::open(&path).unwrap();
4663 let ts = h5
4664 .dataset("entry/instrument/performance/timestamp")
4665 .unwrap();
4666 assert_eq!(ts.shape(), vec![2, 5]);
4667 let vals: Vec<f64> = ts.read_raw().unwrap();
4668 assert_eq!(vals.len(), 10);
4669
4670 std::fs::remove_file(&path).ok();
4671 }
4672
4673 #[test]
4674 fn test_performance_dataset_chunk_matches_capture_target() {
4675 let path = temp_path("hdf5_perf_chunk");
4681 let mut writer = Hdf5Writer::new();
4682 writer.set_store_performance(true);
4683 writer.set_num_capture(8);
4684
4685 let arr = NDArray::new(
4686 vec![NDDimension::new(8), NDDimension::new(8)],
4687 NDDataType::UInt16,
4688 );
4689 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4690 writer.write_file(&arr).unwrap();
4691 writer.write_file(&arr).unwrap();
4692 writer.close_file().unwrap();
4693
4694 let h5 = H5File::open(&path).unwrap();
4695 let ts = h5
4696 .dataset("entry/instrument/performance/timestamp")
4697 .unwrap();
4698 assert_eq!(ts.shape(), vec![2, 5]);
4699 assert_eq!(ts.chunk_dims(), Some(vec![8, 5]));
4700 let vals: Vec<f64> = ts.read_raw().unwrap();
4701 assert_eq!(vals.len(), 10);
4702
4703 std::fs::remove_file(&path).ok();
4704 }
4705
4706 #[test]
4707 fn test_roundtrip_all_types() {
4708 macro_rules! roundtrip {
4709 ($name:expr, $dt:expr, $variant:ident, $ty:ty, $vals:expr) => {{
4710 let path = temp_path($name);
4711 let mut writer = Hdf5Writer::new();
4712 let mut arr = NDArray::new(vec![NDDimension::new(4)], $dt);
4713 if let NDDataBuffer::$variant(ref mut v) = arr.data {
4714 let src: Vec<$ty> = $vals;
4715 v.copy_from_slice(&src);
4716 }
4717 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4718 writer.write_file(&arr).unwrap();
4719 writer.close_file().unwrap();
4720
4721 let mut reader = Hdf5Writer::new();
4722 reader.current_path = Some(path.clone());
4723 let r = reader.read_file().unwrap();
4724 assert_eq!(r.data.data_type(), $dt, "type for {}", $name);
4725 if let NDDataBuffer::$variant(ref v) = r.data {
4726 let src: Vec<$ty> = $vals;
4727 assert_eq!(v, &src, "values for {}", $name);
4728 } else {
4729 panic!("wrong buffer variant for {}", $name);
4730 }
4731 std::fs::remove_file(&path).ok();
4732 }};
4733 }
4734
4735 roundtrip!("rt_i8", NDDataType::Int8, I8, i8, vec![-1, 0, 1, 127]);
4736 roundtrip!("rt_u8", NDDataType::UInt8, U8, u8, vec![0, 1, 200, 255]);
4737 roundtrip!(
4738 "rt_i16",
4739 NDDataType::Int16,
4740 I16,
4741 i16,
4742 vec![-32768, -1, 1, 32767]
4743 );
4744 roundtrip!(
4745 "rt_u16",
4746 NDDataType::UInt16,
4747 U16,
4748 u16,
4749 vec![0, 1, 40000, 65535]
4750 );
4751 roundtrip!(
4752 "rt_i32",
4753 NDDataType::Int32,
4754 I32,
4755 i32,
4756 vec![i32::MIN, -1, 1, i32::MAX]
4757 );
4758 roundtrip!(
4759 "rt_u32",
4760 NDDataType::UInt32,
4761 U32,
4762 u32,
4763 vec![0, 1, 3_000_000_000, u32::MAX]
4764 );
4765 roundtrip!(
4766 "rt_i64",
4767 NDDataType::Int64,
4768 I64,
4769 i64,
4770 vec![i64::MIN, -1, 1, i64::MAX]
4771 );
4772 roundtrip!(
4773 "rt_u64",
4774 NDDataType::UInt64,
4775 U64,
4776 u64,
4777 vec![0, 1, 9_000_000_000, u64::MAX]
4778 );
4779 roundtrip!(
4780 "rt_f32",
4781 NDDataType::Float32,
4782 F32,
4783 f32,
4784 vec![-1.5, 0.0, 2.25, 3.75]
4785 );
4786 roundtrip!(
4787 "rt_f64",
4788 NDDataType::Float64,
4789 F64,
4790 f64,
4791 vec![-1.5, 0.0, 2.25, 3.75]
4792 );
4793 }
4794
4795 #[test]
4796 fn test_deflate_compressed_write() {
4797 let path = temp_path("hdf5_deflate");
4798 let mut writer = Hdf5Writer::new();
4799 writer.set_compression_type(COMPRESS_ZLIB);
4800 writer.set_z_compress_level(6);
4801
4802 let mut arr = NDArray::new(
4803 vec![NDDimension::new(64), NDDimension::new(64)],
4804 NDDataType::UInt16,
4805 );
4806 if let NDDataBuffer::U16(ref mut v) = arr.data {
4807 for i in 0..v.len() {
4808 v[i] = (i % 256) as u16;
4809 }
4810 }
4811
4812 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4813 writer.write_file(&arr).unwrap();
4814 writer.close_file().unwrap();
4815
4816 let file_size = std::fs::metadata(&path).unwrap().len();
4817 assert!(
4818 file_size < 8192,
4819 "compressed file should be smaller than raw data"
4820 );
4821
4822 let h5file = H5File::open(&path).unwrap();
4823 let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
4824 let data: Vec<u16> = ds.read_raw().unwrap();
4825 assert_eq!(data.len(), 64 * 64);
4826 assert_eq!(data[0], 0);
4827 assert_eq!(data[255], 255);
4828 assert_eq!(data[256], 0);
4829
4830 std::fs::remove_file(&path).ok();
4831 }
4832
4833 #[test]
4834 fn test_lz4_compressed_write() {
4835 let path = temp_path("hdf5_lz4");
4836 let mut writer = Hdf5Writer::new();
4837 writer.set_compression_type(COMPRESS_LZ4);
4838
4839 let mut arr = NDArray::new(
4840 vec![NDDimension::new(32), NDDimension::new(32)],
4841 NDDataType::UInt8,
4842 );
4843 if let NDDataBuffer::U8(ref mut v) = arr.data {
4844 for i in 0..v.len() {
4845 v[i] = (i % 4) as u8;
4846 }
4847 }
4848
4849 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4850 writer.write_file(&arr).unwrap();
4851 writer.close_file().unwrap();
4852
4853 let h5file = H5File::open(&path).unwrap();
4854 let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
4855 let data: Vec<u8> = ds.read_raw().unwrap();
4856 assert_eq!(data.len(), 32 * 32);
4857 assert_eq!(data[0], 0);
4858 assert_eq!(data[3], 3);
4859
4860 std::fs::remove_file(&path).ok();
4861 }
4862
4863 #[test]
4864 fn test_bitshuffle_compressed_write() {
4865 let path = temp_path("hdf5_bshuf");
4866 let mut writer = Hdf5Writer::new();
4867 writer.set_compression_type(COMPRESS_BSHUF);
4868
4869 let mut arr = NDArray::new(
4870 vec![NDDimension::new(64), NDDimension::new(64)],
4871 NDDataType::UInt16,
4872 );
4873 if let NDDataBuffer::U16(ref mut v) = arr.data {
4874 for i in 0..v.len() {
4875 v[i] = (i % 8) as u16;
4876 }
4877 }
4878
4879 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4880 writer.write_file(&arr).unwrap();
4881 writer.close_file().unwrap();
4882
4883 let h5file = H5File::open(&path).unwrap();
4884 let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
4885 let data: Vec<u16> = ds.read_raw().unwrap();
4886 assert_eq!(data.len(), 64 * 64);
4887 assert_eq!(data[0], 0);
4888 assert_eq!(data[9], 1);
4889
4890 std::fs::remove_file(&path).ok();
4891 }
4892
4893 #[test]
4894 fn test_szip_uses_nearest_neighbor_mask() {
4895 let mut writer = Hdf5Writer::new();
4898 writer.set_compression_type(COMPRESS_SZIP);
4899 let pipeline = writer.build_pipeline(1).expect("szip pipeline");
4900 assert_eq!(pipeline.filters[0].cd_values[0], SZIP_NN_OPTION_MASK);
4901
4902 let path = temp_path("hdf5_szip_nn");
4903 let mut arr = NDArray::new(
4904 vec![NDDimension::new(64), NDDimension::new(64)],
4905 NDDataType::UInt8,
4906 );
4907 if let NDDataBuffer::U8(ref mut v) = arr.data {
4908 for (i, e) in v.iter_mut().enumerate() {
4909 *e = (i % 13) as u8;
4910 }
4911 }
4912 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4913 writer.write_file(&arr).unwrap();
4914 writer.close_file().unwrap();
4915
4916 let h5file = H5File::open(&path).unwrap();
4917 let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
4918 let data: Vec<u8> = ds.read_raw().unwrap();
4919 assert_eq!(data.len(), 64 * 64);
4920 assert_eq!(data[20], (20 % 13) as u8);
4921 std::fs::remove_file(&path).ok();
4922 }
4923
4924 #[test]
4925 fn test_blosc_default_shuffle_is_byte_shuffle() {
4926 let mut writer = Hdf5Writer::new();
4929 writer.set_compression_type(COMPRESS_BLOSC);
4930 let pipeline = writer.build_pipeline(2).expect("blosc pipeline");
4931 assert_eq!(pipeline.filters[0].cd_values[5], 1);
4932 }
4933
4934 #[test]
4935 fn test_nbit_packs_to_reduced_precision_datatype() {
4936 let mut writer = Hdf5Writer::new();
4946 writer.set_compression_type(COMPRESS_NBIT);
4947 writer.set_nbit_precision(10);
4948 writer.set_nbit_offset(0);
4949 assert!(
4950 writer.build_pipeline(2).is_none(),
4951 "N-bit packing is applied via a datatype override, not build_pipeline"
4952 );
4953
4954 let path = temp_path("hdf5_nbit_packed");
4955 let mut arr = NDArray::new(
4956 vec![NDDimension::new(8), NDDimension::new(8)],
4957 NDDataType::UInt16,
4958 );
4959 if let NDDataBuffer::U16(ref mut v) = arr.data {
4960 for (i, x) in v.iter_mut().enumerate() {
4961 *x = (i as u16 * 7) & 0x3FF; }
4963 v[0] = 0xFFFF; }
4965 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4966 writer.write_file(&arr).unwrap();
4967 writer.close_file().unwrap();
4968
4969 let h5file = H5File::open(&path).unwrap();
4970 let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
4971
4972 match ds.datatype().unwrap() {
4975 DatatypeMessage::FixedPoint {
4976 size,
4977 bit_precision,
4978 bit_offset,
4979 signed,
4980 ..
4981 } => {
4982 assert_eq!(size, 2, "byte footprint unchanged");
4983 assert_eq!(bit_precision, 10, "precision narrowed to 10 bits");
4984 assert_eq!(bit_offset, 0);
4985 assert!(!signed, "u16 is unsigned");
4986 }
4987 other => panic!("expected reduced-precision FixedPoint, got {other:?}"),
4988 }
4989
4990 let data: Vec<u16> = ds.read_raw().unwrap();
4991 assert_eq!(data.len(), 8 * 8);
4992 assert_eq!(data[0], 0x3FF, "0xFFFF must pack to the low 10 bits");
4994 for i in 1..data.len() {
4995 assert_eq!(
4996 data[i],
4997 (i as u16 * 7) & 0x3FF,
4998 "in-range value {i} round-trips"
4999 );
5000 }
5001 std::fs::remove_file(&path).ok();
5002 }
5003
5004 #[test]
5005 fn test_chunk_geometry_recorded() {
5006 let path = temp_path("hdf5_chunkgeom");
5009 let mut writer = Hdf5Writer::new();
5010 writer.set_chunk_size_auto(false);
5011 writer.set_n_row_chunks(4);
5012 writer.set_n_col_chunks(2);
5013 writer.set_n_frames_chunks(3);
5014
5015 let mut arr = NDArray::new(
5016 vec![NDDimension::new(8), NDDimension::new(8)],
5017 NDDataType::UInt16,
5018 );
5019 if let NDDataBuffer::U16(ref mut v) = arr.data {
5020 for i in 0..v.len() {
5021 v[i] = i as u16;
5022 }
5023 }
5024
5025 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5026 writer.write_file(&arr).unwrap();
5027 writer.write_file(&arr).unwrap();
5028 writer.close_file().unwrap();
5029
5030 let h5 = H5File::open(&path).unwrap();
5031 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
5032 assert_eq!(ds.shape(), vec![2, 8, 8]);
5033 let data: Vec<u16> = ds.read_raw().unwrap();
5035 assert_eq!(data.len(), 2 * 64);
5036 for i in 0..64usize {
5037 assert_eq!(data[i], i as u16, "frame0 element {}", i);
5038 assert_eq!(data[64 + i], i as u16, "frame1 element {}", i);
5039 }
5040 assert_eq!(
5042 ds.attr("HDF5_nRowChunks")
5043 .unwrap()
5044 .read_numeric::<i32>()
5045 .unwrap(),
5046 4
5047 );
5048 assert_eq!(
5049 ds.attr("HDF5_nColChunks")
5050 .unwrap()
5051 .read_numeric::<i32>()
5052 .unwrap(),
5053 2
5054 );
5055 assert_eq!(
5056 ds.attr("HDF5_nFramesChunks")
5057 .unwrap()
5058 .read_numeric::<i32>()
5059 .unwrap(),
5060 3
5061 );
5062
5063 std::fs::remove_file(&path).ok();
5064 }
5065
5066 #[test]
5067 fn test_extra_dimensions_layout() {
5068 let path = temp_path("hdf5_extradims");
5077 let mut writer = Hdf5Writer::new();
5078 writer.set_n_extra_dims(2);
5079 writer.set_extra_dim_size(0, 2); writer.set_extra_dim_size(1, 3); writer.set_extra_dim_size(2, 4); writer.set_extra_dim_name(0, "n");
5083 writer.set_extra_dim_name(1, "scanX");
5084 writer.set_extra_dim_name(2, "scanY");
5085
5086 let mut arr = NDArray::new(
5087 vec![NDDimension::new(4), NDDimension::new(4)],
5088 NDDataType::UInt16,
5089 );
5090 for f in 0..24u16 {
5091 if let NDDataBuffer::U16(ref mut v) = arr.data {
5092 for x in v.iter_mut() {
5093 *x = f;
5094 }
5095 }
5096 if f == 0 {
5097 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5098 }
5099 writer.write_file(&arr).unwrap();
5100 }
5101 writer.close_file().unwrap();
5102
5103 let h5 = H5File::open(&path).unwrap();
5104 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
5105 assert_eq!(ds.shape(), vec![4, 3, 2, 4, 4]);
5107 let data: Vec<u16> = ds.read_raw().unwrap();
5108 assert_eq!(data.len(), 24 * 16);
5109 for f in 0..24usize {
5110 for i in 0..16usize {
5111 assert_eq!(data[f * 16 + i], f as u16, "frame {} elem {}", f, i);
5112 }
5113 }
5114 assert_eq!(
5116 ds.attr("HDF5_nExtraDims")
5117 .unwrap()
5118 .read_numeric::<i32>()
5119 .unwrap(),
5120 2
5121 );
5122 assert_eq!(
5123 ds.attr("HDF5_extraDimSize0")
5124 .unwrap()
5125 .read_numeric::<i32>()
5126 .unwrap(),
5127 2
5128 );
5129 assert_eq!(
5130 ds.attr("HDF5_extraDimName0")
5131 .unwrap()
5132 .read_string()
5133 .unwrap(),
5134 "n"
5135 );
5136
5137 std::fs::remove_file(&path).ok();
5138 }
5139
5140 #[test]
5141 fn test_extra_dimensions_one_virtual() {
5142 let path = temp_path("hdf5_extradims_1");
5146 let mut writer = Hdf5Writer::new();
5147 writer.set_n_extra_dims(1);
5148 writer.set_extra_dim_size(0, 2); writer.set_extra_dim_size(1, 3); let mut arr = NDArray::new(
5152 vec![NDDimension::new(4), NDDimension::new(4)],
5153 NDDataType::UInt16,
5154 );
5155 for f in 0..6u16 {
5156 if let NDDataBuffer::U16(ref mut v) = arr.data {
5157 for x in v.iter_mut() {
5158 *x = f;
5159 }
5160 }
5161 if f == 0 {
5162 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5163 }
5164 writer.write_file(&arr).unwrap();
5165 }
5166 writer.close_file().unwrap();
5167
5168 let h5 = H5File::open(&path).unwrap();
5169 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
5170 assert_eq!(ds.shape(), vec![3, 2, 4, 4]);
5171 let data: Vec<u16> = ds.read_raw().unwrap();
5172 assert_eq!(data.len(), 6 * 16);
5173 for f in 0..6usize {
5174 for i in 0..16usize {
5175 assert_eq!(data[f * 16 + i], f as u16, "frame {} elem {}", f, i);
5176 }
5177 }
5178
5179 std::fs::remove_file(&path).ok();
5180 }
5181
5182 #[test]
5183 fn test_swmr_extra_dimensions_grid_layout() {
5184 let path = temp_path("hdf5_swmr_extradims");
5191 let mut writer = Hdf5Writer::new();
5192 writer.set_swmr_mode(true);
5193 writer.set_n_extra_dims(2);
5194 writer.set_extra_dim_size(0, 2); writer.set_extra_dim_size(1, 3); writer.set_extra_dim_size(2, 4); let mut arr = NDArray::new(
5199 vec![NDDimension::new(4), NDDimension::new(4)],
5200 NDDataType::UInt16,
5201 );
5202 for f in 0..24u16 {
5203 if let NDDataBuffer::U16(ref mut v) = arr.data {
5204 for x in v.iter_mut() {
5205 *x = f;
5206 }
5207 }
5208 if f == 0 {
5209 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5210 }
5211 writer.write_file(&arr).unwrap();
5212 }
5213 writer.close_file().unwrap();
5214
5215 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
5216 assert_eq!(
5217 reader
5218 .dataset_shape("entry/instrument/detector/data")
5219 .unwrap(),
5220 vec![4, 3, 2, 4, 4]
5221 );
5222 let data: Vec<u16> = reader
5223 .read_dataset("entry/instrument/detector/data")
5224 .unwrap();
5225 assert_eq!(data.len(), 24 * 16);
5226 for f in 0..24usize {
5227 for i in 0..16usize {
5228 assert_eq!(data[f * 16 + i], f as u16, "frame {} elem {}", f, i);
5229 }
5230 }
5231 std::fs::remove_file(&path).ok();
5232 }
5233
5234 #[test]
5235 fn test_swmr_extra_dimensions_grid_subtiled() {
5236 let path = temp_path("hdf5_swmr_extradims_tiled");
5242 let mut writer = Hdf5Writer::new();
5243 writer.set_swmr_mode(true);
5244 writer.set_n_extra_dims(1);
5245 writer.set_extra_dim_size(0, 2); writer.set_extra_dim_size(1, 3); writer.set_n_row_chunks(2); let mut arr = NDArray::new(
5250 vec![NDDimension::new(4), NDDimension::new(4)],
5251 NDDataType::UInt16,
5252 );
5253 for f in 0..6u16 {
5254 if let NDDataBuffer::U16(ref mut v) = arr.data {
5255 for (i, x) in v.iter_mut().enumerate() {
5258 *x = f * 100 + i as u16;
5259 }
5260 }
5261 if f == 0 {
5262 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5263 }
5264 writer.write_file(&arr).unwrap();
5265 }
5266 writer.close_file().unwrap();
5267
5268 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
5269 assert_eq!(
5270 reader
5271 .dataset_shape("entry/instrument/detector/data")
5272 .unwrap(),
5273 vec![3, 2, 4, 4]
5274 );
5275 let data: Vec<u16> = reader
5276 .read_dataset("entry/instrument/detector/data")
5277 .unwrap();
5278 assert_eq!(data.len(), 6 * 16);
5279 for f in 0..6usize {
5280 for i in 0..16usize {
5281 assert_eq!(
5282 data[f * 16 + i],
5283 f as u16 * 100 + i as u16,
5284 "frame {} elem {}",
5285 f,
5286 i
5287 );
5288 }
5289 }
5290 std::fs::remove_file(&path).ok();
5291 }
5292
5293 #[test]
5294 fn test_swmr_extra_dimensions_grid_partial_fill() {
5295 let path = temp_path("hdf5_swmr_extradims_partial");
5299 let mut writer = Hdf5Writer::new();
5300 writer.set_swmr_mode(true);
5301 writer.set_n_extra_dims(1);
5302 writer.set_extra_dim_size(0, 2); writer.set_extra_dim_size(1, 3); let mut arr = NDArray::new(
5306 vec![NDDimension::new(4), NDDimension::new(4)],
5307 NDDataType::UInt16,
5308 );
5309 for f in 0..4u16 {
5310 if let NDDataBuffer::U16(ref mut v) = arr.data {
5311 for x in v.iter_mut() {
5312 *x = f + 1; }
5314 }
5315 if f == 0 {
5316 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5317 }
5318 writer.write_file(&arr).unwrap();
5319 }
5320 writer.close_file().unwrap();
5321
5322 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
5323 assert_eq!(
5324 reader
5325 .dataset_shape("entry/instrument/detector/data")
5326 .unwrap(),
5327 vec![3, 2, 4, 4]
5328 );
5329 let data: Vec<u16> = reader
5330 .read_dataset("entry/instrument/detector/data")
5331 .unwrap();
5332 assert_eq!(data.len(), 6 * 16);
5333 for f in 0..6usize {
5334 let expected = if f < 4 { f as u16 + 1 } else { 0 };
5335 for i in 0..16usize {
5336 assert_eq!(data[f * 16 + i], expected, "frame {} elem {}", f, i);
5337 }
5338 }
5339 std::fs::remove_file(&path).ok();
5340 }
5341
5342 #[test]
5343 fn test_swmr_streaming() {
5344 let path = temp_path("hdf5_swmr");
5345 let mut writer = Hdf5Writer::new();
5346 writer.set_swmr_mode(true);
5347 writer.set_flush_nth_frame(2);
5348
5349 let arr = NDArray::new(
5350 vec![NDDimension::new(8), NDDimension::new(8)],
5351 NDDataType::Float32,
5352 );
5353
5354 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5355 writer.write_file(&arr).unwrap();
5356 writer.write_file(&arr).unwrap(); writer.write_file(&arr).unwrap();
5358 writer.close_file().unwrap();
5359
5360 assert_eq!(writer.frame_count(), 3);
5361
5362 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
5364 let shape = reader
5365 .dataset_shape("entry/instrument/detector/data")
5366 .unwrap();
5367 assert_eq!(shape[0], 3); assert_eq!(shape[1], 8);
5369 assert_eq!(shape[2], 8);
5370
5371 let data: Vec<f32> = reader
5372 .read_dataset("entry/instrument/detector/data")
5373 .unwrap();
5374 assert_eq!(data.len(), 3 * 8 * 8);
5375
5376 std::fs::remove_file(&path).ok();
5377 }
5378
5379 #[test]
5380 fn test_swmr_compression_is_applied() {
5381 let path = temp_path("hdf5_swmr_comp");
5385 let mut writer = Hdf5Writer::new();
5386 writer.set_swmr_mode(true);
5387 writer.set_compression_type(COMPRESS_ZLIB);
5388
5389 let arr = NDArray::new(
5390 vec![NDDimension::new(8), NDDimension::new(8)],
5391 NDDataType::UInt16,
5392 );
5393 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5394 assert!(
5395 !writer.swmr_compression_dropped(),
5396 "SWMR+ZLIB must apply compression, not drop it"
5397 );
5398 writer.write_file(&arr).unwrap();
5399 writer.write_file(&arr).unwrap();
5400 writer.close_file().unwrap();
5401
5402 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
5404 let shape = reader
5405 .dataset_shape("entry/instrument/detector/data")
5406 .unwrap();
5407 assert_eq!(shape, vec![2, 8, 8]);
5408 let data: Vec<u16> = reader
5409 .read_dataset("entry/instrument/detector/data")
5410 .unwrap();
5411 assert_eq!(data.len(), 2 * 8 * 8);
5412
5413 std::fs::remove_file(&path).ok();
5414 }
5415
5416 #[test]
5417 fn test_layout_xml_param() {
5418 let mut writer = Hdf5Writer::new();
5420 let dir = std::env::temp_dir();
5421 let good = dir.join("adcore_layout_good.xml");
5422 std::fs::write(
5423 &good,
5424 r#"<hdf5_layout><group name="entry"><dataset name="data" source="detector" det_default="true"/></group></hdf5_layout>"#,
5425 )
5426 .unwrap();
5427 assert!(writer.set_layout_filename(good.to_str().unwrap()));
5428 assert!(writer.layout_valid);
5429 assert!(writer.layout_error.is_empty());
5430
5431 let bad = dir.join("adcore_layout_bad.xml");
5432 std::fs::write(&bad, r#"<not_a_layout/>"#).unwrap();
5433 assert!(!writer.set_layout_filename(bad.to_str().unwrap()));
5434 assert!(!writer.layout_valid);
5435 assert!(!writer.layout_error.is_empty());
5436
5437 std::fs::remove_file(&good).ok();
5438 std::fs::remove_file(&bad).ok();
5439 }
5440
5441 #[test]
5442 fn test_layout_xml_places_dataset_in_nested_tree() {
5443 let dir = std::env::temp_dir();
5449 let layout = dir.join("adcore_layout_nested.xml");
5450 std::fs::write(
5451 &layout,
5452 r#"<hdf5_layout>
5453 <group name="entry">
5454 <group name="instrument">
5455 <group name="detector">
5456 <dataset name="data" source="detector" det_default="true">
5457 <attribute name="signal" source="constant" value="1" type="int"/>
5458 </dataset>
5459 </group>
5460 <group name="NDAttributes" ndattr_default="true"/>
5461 <group name="performance">
5462 <dataset name="timestamp"/>
5463 </group>
5464 </group>
5465 </group>
5466 </hdf5_layout>"#,
5467 )
5468 .unwrap();
5469
5470 let path = temp_path("hdf5_layout_nested");
5471 let mut writer = Hdf5Writer::new();
5472 writer.set_store_performance(true);
5473 assert!(
5474 writer.set_layout_filename(layout.to_str().unwrap()),
5475 "layout XML must parse: {}",
5476 writer.layout_error
5477 );
5478
5479 let mk = |fill: f64| {
5480 let mut arr = NDArray::new(
5481 vec![NDDimension::new(4), NDDimension::new(4)],
5482 NDDataType::UInt16,
5483 );
5484 arr.attributes.add(NDAttribute::new_static(
5485 "exposure",
5486 "",
5487 NDAttrSource::Driver,
5488 NDAttrValue::Float64(fill),
5489 ));
5490 arr
5491 };
5492
5493 let a0 = mk(0.5);
5494 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
5495 writer.write_file(&a0).unwrap();
5496 writer.write_file(&mk(0.75)).unwrap();
5497 writer.close_file().unwrap();
5498
5499 let h5 = H5File::open(&path).unwrap();
5500 let names = h5.dataset_names();
5501 assert!(
5503 names.contains(&"entry/instrument/detector/data".to_string()),
5504 "image dataset must be at the nested layout path; got {:?}",
5505 names
5506 );
5507 assert!(
5508 !names.contains(&"data".to_string()),
5509 "must not also write a flat-root `data` dataset"
5510 );
5511 let img = h5.dataset("entry/instrument/detector/data").unwrap();
5512 assert_eq!(img.shape(), vec![2, 4, 4]);
5513 assert_eq!(
5515 img.attr("signal").unwrap().read_numeric::<i64>().unwrap(),
5516 1
5517 );
5518 assert!(
5520 names.contains(&"entry/instrument/NDAttributes/exposure".to_string()),
5521 "NDAttribute dataset must be under the layout ndattr group; got {:?}",
5522 names
5523 );
5524 assert!(
5526 names.contains(&"entry/instrument/performance/timestamp".to_string()),
5527 "performance dataset must be under the layout group; got {:?}",
5528 names
5529 );
5530
5531 drop(h5);
5533 let mut reader = Hdf5Writer::new();
5534 assert!(reader.set_layout_filename(layout.to_str().unwrap()));
5535 reader.current_path = Some(path.clone());
5536 let read_arr = reader.read_file().unwrap();
5537 assert_eq!(read_arr.dims.len(), 3);
5538
5539 std::fs::remove_file(&path).ok();
5540 std::fs::remove_file(&layout).ok();
5541 }
5542
5543 #[test]
5551 fn test_detector_data_destination_routes_by_attribute() {
5552 let dir = std::env::temp_dir();
5553 let layout = dir.join("adcore_layout_multidet.xml");
5554 std::fs::write(
5555 &layout,
5556 r#"<hdf5_layout>
5557 <global name="detector_data_destination" ndattribute="dest"/>
5558 <group name="entry">
5559 <dataset name="data1" source="detector" det_default="true"/>
5560 <dataset name="data2" source="detector"/>
5561 </group>
5562 </hdf5_layout>"#,
5563 )
5564 .unwrap();
5565
5566 let path = temp_path("hdf5_multidet_route");
5567 let mut writer = Hdf5Writer::new();
5568 writer.store_attributes = false;
5570 assert!(
5571 writer.set_layout_filename(layout.to_str().unwrap()),
5572 "layout XML must parse: {}",
5573 writer.layout_error
5574 );
5575
5576 let mk = |val: u16, dest: Option<&str>| {
5579 let mut arr = NDArray::new(
5580 vec![NDDimension::new(2), NDDimension::new(2)],
5581 NDDataType::UInt16,
5582 );
5583 if let NDDataBuffer::U16(ref mut v) = arr.data {
5584 for p in v.iter_mut() {
5585 *p = val;
5586 }
5587 }
5588 if let Some(d) = dest {
5589 arr.attributes.add(NDAttribute::new_static(
5590 "dest",
5591 "",
5592 NDAttrSource::Driver,
5593 NDAttrValue::String(d.to_string()),
5594 ));
5595 }
5596 arr
5597 };
5598
5599 let f0 = mk(10, None);
5605 writer.open_file(&path, NDFileMode::Stream, &f0).unwrap();
5606 writer.write_file(&f0).unwrap();
5607 writer.write_file(&mk(11, Some("/entry/data2"))).unwrap();
5608 writer.write_file(&mk(12, Some("/entry/data2"))).unwrap();
5609 writer.write_file(&mk(13, Some("/nonexistent"))).unwrap();
5610 writer.write_file(&mk(14, Some("/entry/data1"))).unwrap();
5611 writer.close_file().unwrap();
5612
5613 let h5 = H5File::open(&path).unwrap();
5614 let names = h5.dataset_names();
5615 assert!(
5616 names.contains(&"entry/data1".to_string())
5617 && names.contains(&"entry/data2".to_string()),
5618 "both detector datasets must exist; got {:?}",
5619 names
5620 );
5621
5622 let d1 = h5.dataset("entry/data1").unwrap();
5624 assert_eq!(d1.shape(), vec![3, 2, 2], "default dataset extent");
5625 let v1: Vec<u16> = d1.read_raw().unwrap();
5626 assert_eq!(
5627 v1,
5628 vec![10, 10, 10, 10, 13, 13, 13, 13, 14, 14, 14, 14],
5629 "default dataset must hold the default-routed frames in order"
5630 );
5631
5632 let d2 = h5.dataset("entry/data2").unwrap();
5633 assert_eq!(d2.shape(), vec![2, 2, 2], "routed dataset extent");
5634 let v2: Vec<u16> = d2.read_raw().unwrap();
5635 assert_eq!(
5636 v2,
5637 vec![11, 11, 11, 11, 12, 12, 12, 12],
5638 "routed dataset must hold only the frames addressed to it"
5639 );
5640
5641 drop(h5);
5642 std::fs::remove_file(&path).ok();
5643 std::fs::remove_file(&layout).ok();
5644 }
5645
5646 #[test]
5650 fn test_detector_data_destination_non_string_attribute_errors() {
5651 let dir = std::env::temp_dir();
5652 let layout = dir.join("adcore_layout_multidet_err.xml");
5653 std::fs::write(
5654 &layout,
5655 r#"<hdf5_layout>
5656 <global name="detector_data_destination" ndattribute="dest"/>
5657 <group name="entry">
5658 <dataset name="data1" source="detector" det_default="true"/>
5659 <dataset name="data2" source="detector"/>
5660 </group>
5661 </hdf5_layout>"#,
5662 )
5663 .unwrap();
5664
5665 let path = temp_path("hdf5_multidet_err");
5666 let mut writer = Hdf5Writer::new();
5667 writer.store_attributes = false;
5668 assert!(writer.set_layout_filename(layout.to_str().unwrap()));
5669
5670 let mut arr = NDArray::new(
5671 vec![NDDimension::new(2), NDDimension::new(2)],
5672 NDDataType::UInt16,
5673 );
5674 arr.attributes.add(NDAttribute::new_static(
5675 "dest",
5676 "",
5677 NDAttrSource::Driver,
5678 NDAttrValue::Float64(2.0),
5679 ));
5680
5681 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5682 assert!(
5683 writer.write_file(&arr).is_err(),
5684 "a non-string destination attribute must abort the write"
5685 );
5686 writer.close_file().ok();
5687
5688 std::fs::remove_file(&path).ok();
5689 std::fs::remove_file(&layout).ok();
5690 }
5691
5692 #[test]
5693 fn test_layout_hardlink_is_materialised() {
5694 let dir = std::env::temp_dir();
5700 let layout = dir.join("adcore_layout_hardlink.xml");
5701 std::fs::write(
5702 &layout,
5703 r#"<hdf5_layout>
5704 <group name="entry">
5705 <group name="data">
5706 <dataset name="data" source="detector" det_default="true"/>
5707 <hardlink name="data_alias" target="/entry/data/data"/>
5708 </group>
5709 </group>
5710 </hdf5_layout>"#,
5711 )
5712 .unwrap();
5713
5714 let path = temp_path("hdf5_layout_hardlink");
5715 let mut writer = Hdf5Writer::new();
5716 assert!(
5717 writer.set_layout_filename(layout.to_str().unwrap()),
5718 "layout XML must parse: {}",
5719 writer.layout_error
5720 );
5721
5722 let arr = NDArray::new(
5723 vec![NDDimension::new(4), NDDimension::new(4)],
5724 NDDataType::UInt16,
5725 );
5726 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5727 writer.write_file(&arr).unwrap();
5728 writer.close_file().unwrap();
5729
5730 let h5 = H5File::open(&path).unwrap();
5731 let names = h5.dataset_names();
5732 assert!(
5734 names.contains(&"entry/data/data".to_string()),
5735 "image dataset must exist at the layout path; got {:?}",
5736 names
5737 );
5738 assert!(
5740 names.contains(&"entry/data/data_alias".to_string()),
5741 "layout <hardlink> must be materialised as a hard link; got {:?}",
5742 names
5743 );
5744 let alias = h5.dataset("entry/data/data_alias").unwrap();
5746 let orig = h5.dataset("entry/data/data").unwrap();
5747 assert_eq!(alias.shape(), orig.shape());
5748
5749 drop(h5);
5750 std::fs::remove_file(&path).ok();
5751 std::fs::remove_file(&layout).ok();
5752 }
5753
5754 #[test]
5755 fn test_swmr_layout_hardlink_is_materialised() {
5756 let dir = std::env::temp_dir();
5768 let layout = dir.join("adcore_swmr_layout_hardlink.xml");
5769 std::fs::write(
5770 &layout,
5771 r#"<hdf5_layout>
5772 <group name="entry">
5773 <group name="data">
5774 <dataset name="data" source="detector" det_default="true"/>
5775 <hardlink name="data_alias" target="/entry/data/data"/>
5776 </group>
5777 </group>
5778 </hdf5_layout>"#,
5779 )
5780 .unwrap();
5781
5782 let path = temp_path("hdf5_swmr_layout_hardlink");
5783 let mut writer = Hdf5Writer::new();
5784 writer.set_swmr_mode(true);
5785 assert!(
5786 writer.set_layout_filename(layout.to_str().unwrap()),
5787 "layout XML must parse: {}",
5788 writer.layout_error
5789 );
5790
5791 let mut arr = NDArray::new(
5792 vec![NDDimension::new(4), NDDimension::new(4)],
5793 NDDataType::UInt16,
5794 );
5795 if let NDDataBuffer::U16(ref mut v) = arr.data {
5796 for (i, x) in v.iter_mut().enumerate() {
5797 *x = i as u16;
5798 }
5799 }
5800 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5801 assert!(
5802 writer.is_swmr_active(),
5803 "writer must be in SWMR mode for this test"
5804 );
5805 writer.write_file(&arr).unwrap();
5806 writer.write_file(&arr).unwrap();
5807 writer.close_file().unwrap();
5808
5809 let h5 = H5File::open(&path).unwrap();
5810 let names = h5.dataset_names();
5811 assert!(
5813 names.contains(&"entry/data/data".to_string()),
5814 "SWMR image dataset must exist at the nested layout path; got {:?}",
5815 names
5816 );
5817 assert!(
5819 names.contains(&"entry/data/data_alias".to_string()),
5820 "SWMR layout <hardlink> must be materialised as a hard link; got {:?}",
5821 names
5822 );
5823 let alias = h5.dataset("entry/data/data_alias").unwrap();
5825 let orig = h5.dataset("entry/data/data").unwrap();
5826 assert_eq!(alias.shape(), orig.shape());
5827 assert_eq!(orig.shape(), vec![2, 4, 4]);
5828
5829 drop(h5);
5830 std::fs::remove_file(&path).ok();
5831 std::fs::remove_file(&layout).ok();
5832 }
5833
5834 #[test]
5835 fn test_swmr_layout_nested_dataset_placement() {
5836 let dir = std::env::temp_dir();
5843 let layout = dir.join("adcore_swmr_layout_nested.xml");
5844 std::fs::write(
5845 &layout,
5846 r#"<hdf5_layout>
5847 <group name="entry">
5848 <group name="instrument">
5849 <group name="detector">
5850 <dataset name="data" source="detector" det_default="true">
5851 <attribute name="signal" source="constant" value="1" type="int"/>
5852 </dataset>
5853 <hardlink name="data_alias" target="/entry/instrument/detector/data"/>
5854 </group>
5855 </group>
5856 <group name="empty_placeholder"/>
5857 </group>
5858 </hdf5_layout>"#,
5859 )
5860 .unwrap();
5861
5862 let path = temp_path("hdf5_swmr_layout_nested");
5863 let mut writer = Hdf5Writer::new();
5864 writer.set_swmr_mode(true);
5865 assert!(
5866 writer.set_layout_filename(layout.to_str().unwrap()),
5867 "layout XML must parse: {}",
5868 writer.layout_error
5869 );
5870
5871 let mut arr = NDArray::new(
5872 vec![NDDimension::new(4), NDDimension::new(4)],
5873 NDDataType::UInt16,
5874 );
5875 if let NDDataBuffer::U16(ref mut v) = arr.data {
5876 for (i, x) in v.iter_mut().enumerate() {
5877 *x = (i * 3) as u16;
5878 }
5879 }
5880 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5881 assert!(
5882 writer.is_swmr_active(),
5883 "writer must be in SWMR mode for this test"
5884 );
5885 writer.write_file(&arr).unwrap();
5886 writer.write_file(&arr).unwrap();
5887 writer.close_file().unwrap();
5888
5889 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
5892 let names = reader.dataset_names();
5893 assert!(
5895 names.contains(&"entry/instrument/detector/data".to_string()),
5896 "SWMR image dataset must live at the nested layout path; got {:?}",
5897 names
5898 );
5899 assert!(
5900 !names.contains(&"data".to_string()),
5901 "SWMR image dataset must NOT remain at the flat root; got {:?}",
5902 names
5903 );
5904 assert!(
5906 reader.has_group("entry/empty_placeholder"),
5907 "empty layout group must be materialised; groups {:?}",
5908 reader.group_paths()
5909 );
5910 assert!(
5912 names.contains(&"entry/instrument/detector/data_alias".to_string()),
5913 "SWMR layout <hardlink> must resolve to the nested dataset; got {:?}",
5914 names
5915 );
5916 let nested = reader
5917 .dataset_shape("entry/instrument/detector/data")
5918 .unwrap();
5919 let alias = reader
5920 .dataset_shape("entry/instrument/detector/data_alias")
5921 .unwrap();
5922 assert_eq!(nested, vec![2, 4, 4]);
5923 assert_eq!(alias, nested, "hardlink alias must share the target shape");
5924 let via_nested: Vec<u16> = reader
5926 .read_dataset("entry/instrument/detector/data")
5927 .unwrap();
5928 let via_alias: Vec<u16> = reader
5929 .read_dataset("entry/instrument/detector/data_alias")
5930 .unwrap();
5931 assert_eq!(via_nested, via_alias);
5932 assert_eq!(via_nested.len(), 2 * 4 * 4);
5933 let attr_names = reader
5937 .dataset_attr_names("entry/instrument/detector/data")
5938 .unwrap();
5939 for expected in [
5940 "signal",
5941 "NDArrayNumDims",
5942 "NDArrayDimOffset",
5943 "NDArrayDimBinning",
5944 "NDArrayDimReverse",
5945 ] {
5946 assert!(
5947 attr_names.iter().any(|n| n == expected),
5948 "streaming dataset must carry the {expected} attribute; got {attr_names:?}",
5949 );
5950 }
5951
5952 drop(reader);
5953 std::fs::remove_file(&path).ok();
5954 std::fs::remove_file(&layout).ok();
5955 }
5956
5957 #[test]
5958 fn test_default_layout_parses_and_resolves_nexus_paths() {
5959 let layout = Hdf5Writer::default_layout();
5963 assert_eq!(
5964 layout.detector_dataset_path().as_deref(),
5965 Some("/entry/instrument/detector/data")
5966 );
5967 assert_eq!(
5968 layout.ndattr_default_group().as_deref(),
5969 Some("/entry/instrument/NDAttributes")
5970 );
5971 assert_eq!(
5972 layout.dataset_group_path("timestamp").as_deref(),
5973 Some("/entry/instrument/performance")
5974 );
5975 }
5976
5977 #[test]
5978 fn test_no_layout_uses_default_nexus_layout() {
5979 let path = temp_path("hdf5_default_layout");
5984 let mut writer = Hdf5Writer::new();
5985 let arr = NDArray::new(
5986 vec![NDDimension::new(4), NDDimension::new(4)],
5987 NDDataType::UInt8,
5988 );
5989 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
5990 writer.write_file(&arr).unwrap();
5991 writer.close_file().unwrap();
5992
5993 let h5 = H5File::open(&path).unwrap();
5994 let det = h5.dataset("entry/instrument/detector/data").unwrap();
5996 assert_eq!(det.shape(), vec![1, 4, 4]);
5997 assert!(
5998 !h5.dataset_names().contains(&"data".to_string()),
5999 "must not write a flat-root `data`; got {:?}",
6000 h5.dataset_names()
6001 );
6002 let linked = h5.dataset("entry/data/data").unwrap();
6004 assert_eq!(linked.shape(), vec![1, 4, 4]);
6005 std::fs::remove_file(&path).ok();
6006 }
6007
6008 #[test]
6009 fn test_default_layout_group_nx_class_attributes() {
6010 let path = temp_path("hdf5_nxclass");
6014 let mut writer = Hdf5Writer::new();
6015 let arr = NDArray::new(
6016 vec![NDDimension::new(4), NDDimension::new(4)],
6017 NDDataType::UInt8,
6018 );
6019 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
6020 writer.write_file(&arr).unwrap();
6021 writer.close_file().unwrap();
6022
6023 let h5 = H5File::open(&path).unwrap();
6024 let nx = |g: &str| {
6025 let mut grp = h5.root_group();
6026 for seg in g.split('/') {
6027 grp = grp.group(seg).unwrap();
6028 }
6029 grp.attr_string("NX_class").unwrap()
6030 };
6031 assert_eq!(nx("entry"), "NXentry");
6032 assert_eq!(nx("entry/instrument"), "NXinstrument");
6033 assert_eq!(nx("entry/instrument/detector"), "NXdetector");
6034 assert_eq!(nx("entry/instrument/NDAttributes"), "NXcollection");
6035 assert_eq!(nx("entry/instrument/detector/NDAttributes"), "NXcollection");
6036 assert_eq!(nx("entry/data"), "NXdata");
6037 std::fs::remove_file(&path).ok();
6038 }
6039
6040 #[test]
6041 fn test_swmr_default_layout_group_nx_class_attributes() {
6042 let path = temp_path("hdf5_swmr_nxclass");
6045 let mut writer = Hdf5Writer::new();
6046 writer.set_swmr_mode(true);
6047 let arr = NDArray::new(
6048 vec![NDDimension::new(4), NDDimension::new(4)],
6049 NDDataType::Float32,
6050 );
6051 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
6052 writer.write_file(&arr).unwrap();
6053 writer.close_file().unwrap();
6054
6055 let h5 = H5File::open(&path).unwrap();
6056 let nx = |g: &str| {
6057 let mut grp = h5.root_group();
6058 for seg in g.split('/') {
6059 grp = grp.group(seg).unwrap();
6060 }
6061 grp.attr_string("NX_class").unwrap()
6062 };
6063 assert_eq!(nx("entry"), "NXentry");
6064 assert_eq!(nx("entry/instrument/detector"), "NXdetector");
6065 assert_eq!(nx("entry/data"), "NXdata");
6066 std::fs::remove_file(&path).ok();
6067 }
6068
6069 fn ramp_u16(y: usize, x: usize) -> NDArray {
6074 let data: Vec<u16> = (0..(y * x) as u16).collect();
6075 NDArray::with_data(
6076 vec![NDDimension::new(y), NDDimension::new(x)],
6077 NDDataBuffer::U16(data),
6078 )
6079 }
6080
6081 fn u16_pixels(arr: &NDArray) -> Vec<u16> {
6082 match &arr.data {
6083 NDDataBuffer::U16(v) => v.clone(),
6084 _ => unreachable!("expected U16 buffer"),
6085 }
6086 }
6087
6088 #[test]
6089 fn test_codec_chunk_bytes_lz4_prepends_hdf5_header() {
6090 let codec = Codec {
6095 name: CodecName::LZ4,
6096 compressed_size: 4,
6097 level: 0,
6098 shuffle: 0,
6099 compressor: 0,
6100 original_data_type: NDDataType::UInt16,
6101 };
6102 let payload = [0xAAu8, 0xBB, 0xCC, 0xDD];
6103 let out = codec_chunk_bytes(&codec, 32, &payload);
6104 let mut expect = Vec::new();
6105 expect.extend_from_slice(&32u64.to_be_bytes()); expect.extend_from_slice(&32u32.to_be_bytes()); expect.extend_from_slice(&4u32.to_be_bytes()); expect.extend_from_slice(&payload);
6109 assert_eq!(out, expect);
6110 }
6111
6112 #[test]
6113 fn test_codec_chunk_bytes_verbatim_for_self_describing() {
6114 for name in [CodecName::Blosc, CodecName::JPEG] {
6118 let codec = Codec {
6119 name,
6120 compressed_size: 3,
6121 level: 0,
6122 shuffle: 0,
6123 compressor: 0,
6124 original_data_type: NDDataType::UInt8,
6125 };
6126 let payload = [1u8, 2, 3];
6127 assert_eq!(codec_chunk_bytes(&codec, 99, &payload), payload.to_vec());
6128 }
6129 }
6130
6131 #[test]
6132 fn test_codecs_match() {
6133 let a = Codec {
6134 name: CodecName::LZ4,
6135 compressed_size: 1,
6136 level: 0,
6137 shuffle: 0,
6138 compressor: 0,
6139 original_data_type: NDDataType::UInt8,
6140 };
6141 assert!(codecs_match(None, None));
6142 assert!(!codecs_match(Some(&a), None));
6143 assert!(!codecs_match(None, Some(&a)));
6144 assert!(codecs_match(Some(&a), Some(&a.clone())));
6145 let mut diff = a.clone();
6146 diff.compressor = 1;
6147 assert!(!codecs_match(Some(&a), Some(&diff)));
6148 let mut other_type = a.clone();
6150 other_type.original_data_type = NDDataType::Float64;
6151 assert!(codecs_match(Some(&a), Some(&other_type)));
6152 }
6153
6154 #[test]
6155 fn test_codec_filter_pipeline_ids() {
6156 let w = Hdf5Writer::new();
6157 let mk = |name| Codec {
6158 name,
6159 compressed_size: 0,
6160 level: 5,
6161 shuffle: 1,
6162 compressor: 2,
6163 original_data_type: NDDataType::UInt16,
6164 };
6165 assert_eq!(
6166 w.codec_filter_pipeline(&mk(CodecName::LZ4))
6167 .unwrap()
6168 .filters[0]
6169 .id,
6170 FILTER_LZ4
6171 );
6172 assert_eq!(
6173 w.codec_filter_pipeline(&mk(CodecName::BSLZ4))
6174 .unwrap()
6175 .filters[0]
6176 .id,
6177 FILTER_BSHUF
6178 );
6179 let blosc = w.codec_filter_pipeline(&mk(CodecName::Blosc)).unwrap();
6180 assert_eq!(blosc.filters[0].id, FILTER_BLOSC);
6181 assert_eq!(blosc.filters[0].cd_values[4], 5, "level");
6184 assert_eq!(blosc.filters[0].cd_values[5], 1, "shuffle");
6185 assert_eq!(blosc.filters[0].cd_values[6], 2, "compressor");
6186 assert_eq!(
6187 w.codec_filter_pipeline(&mk(CodecName::JPEG))
6188 .unwrap()
6189 .filters[0]
6190 .id,
6191 FILTER_JPEG
6192 );
6193 assert!(w.codec_filter_pipeline(&mk(CodecName::None)).is_none());
6195 assert!(w.codec_filter_pipeline(&mk(CodecName::Zlib)).is_none());
6196 assert!(w.codec_filter_pipeline(&mk(CodecName::LZ4HDF5)).is_none());
6197 }
6198
6199 #[test]
6200 fn test_compression_aware_true() {
6201 assert!(Hdf5FileProcessor::new().compression_aware());
6203 }
6204
6205 #[test]
6206 fn test_direct_chunk_write_lz4_roundtrips() {
6207 let path = temp_path("hdf5_dcw_lz4");
6208 let mut writer = Hdf5Writer::new();
6209 let orig = ramp_u16(4, 5);
6210 let expect = u16_pixels(&orig);
6211 let comp = crate::codec::compress_lz4(&orig);
6212 assert!(comp.codec.is_some());
6213 writer.open_file(&path, NDFileMode::Single, &comp).unwrap();
6214 writer.write_file(&comp).unwrap();
6215 writer.close_file().unwrap();
6216
6217 let h5 = H5File::open(&path).unwrap();
6218 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
6219 assert_eq!(ds.shape(), vec![1, 5, 4]);
6223 assert!(ds.is_chunked());
6224 let read = ds.read_raw::<u16>().unwrap();
6225 assert_eq!(
6226 read, expect,
6227 "LZ4 direct-chunk-write must reverse to the original pixels"
6228 );
6229 std::fs::remove_file(&path).ok();
6230 }
6231
6232 #[test]
6233 fn test_direct_chunk_write_blosc_roundtrips() {
6234 let path = temp_path("hdf5_dcw_blosc");
6235 let mut writer = Hdf5Writer::new();
6236 let orig = ramp_u16(4, 5);
6237 let expect = u16_pixels(&orig);
6238 let comp = crate::codec::compress_blosc(&orig, &crate::codec::BloscConfig::default());
6239 assert_eq!(comp.codec.as_ref().unwrap().name, CodecName::Blosc);
6240 writer.open_file(&path, NDFileMode::Single, &comp).unwrap();
6241 writer.write_file(&comp).unwrap();
6242 writer.close_file().unwrap();
6243
6244 let h5 = H5File::open(&path).unwrap();
6245 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
6246 assert_eq!(ds.shape(), vec![1, 5, 4]);
6247 let read = ds.read_raw::<u16>().unwrap();
6248 assert_eq!(read, expect, "BLOSC direct-chunk-write must round-trip");
6249 std::fs::remove_file(&path).ok();
6250 }
6251
6252 #[test]
6253 fn test_direct_chunk_write_bslz4_roundtrips() {
6254 let path = temp_path("hdf5_dcw_bslz4");
6263 let mut writer = Hdf5Writer::new();
6264 let orig = ramp_u16(128, 128);
6265 let expect = u16_pixels(&orig);
6266 let comp = crate::codec::compress_bslz4(&orig);
6267 assert_eq!(comp.codec.as_ref().unwrap().name, CodecName::BSLZ4);
6268 writer.open_file(&path, NDFileMode::Single, &comp).unwrap();
6269 writer.write_file(&comp).unwrap();
6270 writer.close_file().unwrap();
6271
6272 let h5 = H5File::open(&path).unwrap();
6273 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
6274 assert_eq!(ds.shape(), vec![1, 128, 128]);
6275 assert!(ds.is_chunked());
6276 assert_eq!(ds.element_size(), 2, "dataset keeps the original u16 type");
6277 let read = ds.read_raw::<u16>().unwrap();
6278 assert_eq!(read, expect, "BSLZ4 direct-chunk-write must round-trip");
6279 std::fs::remove_file(&path).ok();
6280 }
6281
6282 #[test]
6283 fn test_codec_chunk_bytes_bslz4_header() {
6284 let codec = Codec {
6289 name: CodecName::BSLZ4,
6290 compressed_size: 0,
6291 level: 0,
6292 shuffle: 0,
6293 compressor: 0,
6294 original_data_type: NDDataType::UInt16,
6295 };
6296 let payload = [0xDEu8, 0xAD, 0xBE, 0xEF];
6297 let total = 128 * 128 * 2; let out = codec_chunk_bytes(&codec, total, &payload);
6299 assert_eq!(&out[0..8], &(total as u64).to_be_bytes());
6300 assert_eq!(&out[8..12], &8192u32.to_be_bytes());
6302 assert_eq!(&out[12..], &payload, "canonical stream follows verbatim");
6303 }
6304
6305 #[test]
6306 fn test_direct_chunk_write_lz4_multiframe_extends_leading_axis() {
6307 let path = temp_path("hdf5_dcw_lz4_multi");
6308 let mut writer = Hdf5Writer::new();
6309 let frames: Vec<NDArray> = (0..3u16)
6310 .map(|f| {
6311 let data: Vec<u16> = (0..20u16).map(|i| i + f * 100).collect();
6312 NDArray::with_data(
6313 vec![NDDimension::new(4), NDDimension::new(5)],
6314 NDDataBuffer::U16(data),
6315 )
6316 })
6317 .collect();
6318 let comp: Vec<NDArray> = frames.iter().map(crate::codec::compress_lz4).collect();
6319 writer
6320 .open_file(&path, NDFileMode::Stream, &comp[0])
6321 .unwrap();
6322 for c in &comp {
6323 writer.write_file(c).unwrap();
6324 }
6325 writer.close_file().unwrap();
6326
6327 let h5 = H5File::open(&path).unwrap();
6328 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
6329 assert_eq!(ds.shape(), vec![3, 5, 4], "three compressed frames stacked");
6330 let read = ds.read_raw::<u16>().unwrap();
6331 let mut expect = Vec::new();
6332 for f in 0..3u16 {
6333 for i in 0..20u16 {
6334 expect.push(i + f * 100);
6335 }
6336 }
6337 assert_eq!(read, expect, "every frame's pixels recovered in order");
6338 std::fs::remove_file(&path).ok();
6339 }
6340
6341 #[test]
6342 fn test_direct_chunk_write_jpeg_records_chunked_dataset() {
6343 let path = temp_path("hdf5_dcw_jpeg");
6348 let mut writer = Hdf5Writer::new();
6349 let mono: Vec<u8> = (0..64u16).map(|i| (i * 3) as u8).collect();
6350 let src = NDArray::with_data(
6351 vec![NDDimension::new(8), NDDimension::new(8)],
6352 NDDataBuffer::U8(mono),
6353 );
6354 let comp = crate::codec::compress_jpeg(&src, 90).expect("jpeg encode");
6355 assert_eq!(comp.codec.as_ref().unwrap().name, CodecName::JPEG);
6356 writer.open_file(&path, NDFileMode::Single, &comp).unwrap();
6357 writer.write_file(&comp).unwrap();
6358 writer.close_file().unwrap();
6359
6360 let h5 = H5File::open(&path).unwrap();
6361 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
6362 assert_eq!(ds.shape(), vec![1, 8, 8]);
6363 assert!(ds.is_chunked());
6364 std::fs::remove_file(&path).ok();
6365 }
6366
6367 #[test]
6368 fn test_codec_change_mid_stream_errors() {
6369 let path = temp_path("hdf5_codec_change");
6373 let mut writer = Hdf5Writer::new();
6374 let f0 = crate::codec::compress_lz4(&ramp_u16(4, 5));
6375 let f1_plain = ramp_u16(4, 5); writer.open_file(&path, NDFileMode::Stream, &f0).unwrap();
6377 writer.write_file(&f0).unwrap();
6378 assert!(
6379 writer.write_file(&f1_plain).is_err(),
6380 "an uncompressed frame must be rejected for a compressed dataset"
6381 );
6382 writer.close_file().ok();
6383 std::fs::remove_file(&path).ok();
6384 }
6385
6386 #[test]
6387 fn test_swmr_rejects_compressed_array() {
6388 let path = temp_path("hdf5_swmr_compressed");
6391 let mut writer = Hdf5Writer::new();
6392 writer.set_swmr_mode(true);
6393 let comp = crate::codec::compress_lz4(&ramp_u16(4, 5));
6394 writer.open_file(&path, NDFileMode::Stream, &comp).unwrap();
6395 assert!(
6396 writer.write_file(&comp).is_err(),
6397 "SWMR mode cannot direct-chunk-write a pre-compressed array"
6398 );
6399 writer.close_file().ok();
6400 std::fs::remove_file(&path).ok();
6401 }
6402
6403 #[test]
6404 fn test_unsupported_codec_rejected_on_open() {
6405 let path = temp_path("hdf5_dcw_zlib");
6408 let mut writer = Hdf5Writer::new();
6409 let comp = crate::codec::compress_zlib(&ramp_u16(4, 5));
6410 assert_eq!(comp.codec.as_ref().unwrap().name, CodecName::Zlib);
6411 writer.open_file(&path, NDFileMode::Single, &comp).unwrap();
6412 assert!(
6413 writer.write_file(&comp).is_err(),
6414 "an unsupported (zlib) codec must be rejected, not silently mis-written"
6415 );
6416 writer.close_file().ok();
6417 std::fs::remove_file(&path).ok();
6418 }
6419}