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 fsync_on_close: bool,
364 pub total_runtime: f64,
365 pub total_bytes: u64,
366 perf_rows: Vec<[f64; 5]>,
369 perf_prev: Option<std::time::Instant>,
370 perf_first: Option<std::time::Instant>,
371 attr_datasets: Vec<AttributeDataset>,
373 layout_filename: Option<PathBuf>,
375 layout: Option<Hdf5Layout>,
376 pub layout_valid: bool,
377 pub layout_error: String,
378 resolved_dataset_path: String,
383 resolved_ndattr_group: String,
386 resolved_perf_group: String,
388 ndattr_first_values: std::collections::HashMap<String, NDAttrValue>,
394 ndattr_last_values: std::collections::HashMap<String, NDAttrValue>,
395 ndattr_element_names: Vec<String>,
398}
399
400impl Hdf5Writer {
401 pub fn new() -> Self {
402 Self {
403 current_path: None,
404 handle: None,
405 frame_count: 0,
406 dataset_name: "data".to_string(),
407 open_data_type: None,
408 open_frame_dims: None,
409 open_codec: None,
410 compression_type: 0,
411 z_compress_level: 6,
412 szip_num_pixels: 16,
413 nbit_precision: 8,
416 nbit_offset: 0,
417 jpeg_quality: 90,
418 blosc_shuffle_type: 1,
420 blosc_compressor: 0,
421 blosc_compress_level: 5,
422 chunk: ChunkConfig::default(),
423 open_mode: NDFileMode::Single,
424 num_capture: 1,
425 n_extra_dims: 0,
426 extra_dims: Default::default(),
427 fill_value: 0.0,
428 dim_att_datasets: false,
429 swmr_mode: false,
430 flush_nth_frame: 0,
431 swmr_cb_counter: 0,
432 store_attributes: true,
433 store_performance: false,
434 fsync_on_close: Self::env_fsync_on_close(),
435 total_runtime: 0.0,
436 total_bytes: 0,
437 perf_rows: Vec::new(),
438 perf_prev: None,
439 perf_first: None,
440 attr_datasets: Vec::new(),
441 layout_filename: None,
442 layout: Some(Self::default_layout()),
444 layout_valid: false,
445 layout_error: String::new(),
446 resolved_dataset_path: "data".to_string(),
447 resolved_ndattr_group: String::new(),
448 resolved_perf_group: String::new(),
449 ndattr_first_values: std::collections::HashMap::new(),
450 ndattr_last_values: std::collections::HashMap::new(),
451 ndattr_element_names: Vec::new(),
452 }
453 }
454
455 fn env_fsync_on_close() -> bool {
457 Self::parse_fsync_on_close_env(std::env::var("AD_HDF5_FSYNC_ON_CLOSE").ok().as_deref())
458 }
459
460 fn parse_fsync_on_close_env(v: Option<&str>) -> bool {
465 match v {
466 Some(s) => !matches!(
467 s.trim().to_ascii_lowercase().as_str(),
468 "0" | "false" | "no" | "off"
469 ),
470 None => true,
471 }
472 }
473
474 pub fn set_dataset_name(&mut self, name: &str) {
475 self.dataset_name = name.to_string();
476 }
477
478 pub fn set_compression_type(&mut self, v: i32) {
479 self.compression_type = v;
480 }
481
482 pub fn set_z_compress_level(&mut self, v: u32) {
483 self.z_compress_level = v;
484 }
485
486 pub fn set_szip_num_pixels(&mut self, v: u32) {
487 self.szip_num_pixels = v;
488 }
489
490 pub fn set_blosc_shuffle_type(&mut self, v: i32) {
491 self.blosc_shuffle_type = v;
492 }
493
494 pub fn set_blosc_compressor(&mut self, v: i32) {
495 self.blosc_compressor = v;
496 }
497
498 pub fn set_blosc_compress_level(&mut self, v: u32) {
499 self.blosc_compress_level = v;
500 }
501
502 pub fn set_nbit_precision(&mut self, v: u32) {
503 self.nbit_precision = v;
504 }
505
506 pub fn set_nbit_offset(&mut self, v: u32) {
507 self.nbit_offset = v;
508 }
509
510 pub fn set_jpeg_quality(&mut self, v: u32) {
511 self.jpeg_quality = v;
512 }
513
514 pub fn set_store_attributes(&mut self, v: bool) {
515 self.store_attributes = v;
516 }
517
518 pub fn set_store_performance(&mut self, v: bool) {
519 self.store_performance = v;
520 }
521
522 pub fn set_swmr_mode(&mut self, v: bool) {
523 self.swmr_mode = v;
524 }
525
526 pub fn set_flush_nth_frame(&mut self, v: usize) {
527 self.flush_nth_frame = v;
528 }
529
530 pub fn set_chunk_size_auto(&mut self, v: bool) {
531 self.chunk.auto = v;
532 }
533
534 pub fn set_n_row_chunks(&mut self, v: usize) {
535 self.chunk.n_row_chunks = v;
536 }
537
538 pub fn set_n_col_chunks(&mut self, v: usize) {
539 self.chunk.n_col_chunks = v;
540 }
541
542 pub fn set_n_frames_chunks(&mut self, v: usize) {
543 self.chunk.n_frames_chunks = v;
544 }
545
546 pub fn set_ndattr_chunk(&mut self, v: usize) {
547 self.chunk.ndattr_chunk = v;
550 }
551
552 pub fn set_n_extra_dims(&mut self, v: usize) {
553 self.n_extra_dims = v.min(MAX_EXTRA_DIMS - 1);
559 }
560
561 pub fn set_extra_dim_size(&mut self, idx: usize, size: usize) {
562 if idx < MAX_EXTRA_DIMS {
563 self.extra_dims[idx].size = size;
564 }
565 }
566
567 pub fn set_extra_dim_name(&mut self, idx: usize, name: &str) {
568 if idx < MAX_EXTRA_DIMS {
569 self.extra_dims[idx].name = name.to_string();
570 }
571 }
572
573 pub fn set_fill_value(&mut self, v: f64) {
574 self.fill_value = v;
575 }
576
577 pub fn set_dim_att_datasets(&mut self, v: bool) {
578 self.dim_att_datasets = v;
579 }
580
581 fn default_layout() -> Hdf5Layout {
585 Hdf5Layout::parse(DEFAULT_LAYOUT_XML).expect("built-in default layout must parse")
586 }
587
588 pub fn set_layout_filename(&mut self, path: &str) -> bool {
591 if path.trim().is_empty() {
592 self.layout_filename = None;
596 self.layout = Some(Self::default_layout());
597 self.layout_valid = false;
598 self.layout_error.clear();
599 return true;
600 }
601 let p = PathBuf::from(path);
602 match Hdf5Layout::from_file(&p) {
603 Ok(layout) => {
604 self.layout_filename = Some(p);
605 self.layout = Some(layout);
606 self.layout_valid = true;
607 self.layout_error.clear();
608 true
609 }
610 Err(e) => {
611 self.layout_filename = Some(p);
612 self.layout = None;
613 self.layout_valid = false;
614 self.layout_error = e.0;
615 false
616 }
617 }
618 }
619
620 pub fn frame_count(&self) -> usize {
621 self.frame_count
622 }
623
624 pub fn flush_swmr(&mut self) {
626 if let Some(Hdf5Handle::Swmr { ref mut writer, .. }) = self.handle {
627 if writer.flush().is_ok() {
628 self.swmr_cb_counter += 1;
629 }
630 }
631 }
632
633 pub fn is_swmr_active(&self) -> bool {
635 matches!(self.handle, Some(Hdf5Handle::Swmr { .. }))
636 }
637
638 pub fn swmr_compression_dropped(&self) -> bool {
640 matches!(
641 self.handle,
642 Some(Hdf5Handle::Swmr {
643 compression_dropped: true,
644 ..
645 })
646 )
647 }
648
649 fn build_pipeline(&self, element_size: usize) -> Option<FilterPipeline> {
651 match self.compression_type {
652 COMPRESS_NONE => None,
653 COMPRESS_ZLIB => Some(FilterPipeline::deflate(self.z_compress_level)),
654 COMPRESS_SZIP => Some(FilterPipeline {
655 filters: vec![Filter {
656 id: FILTER_SZIP,
657 flags: 0,
658 cd_values: vec![SZIP_NN_OPTION_MASK, self.szip_num_pixels],
663 }],
664 }),
665 COMPRESS_LZ4 => Some(FilterPipeline::lz4()),
666 COMPRESS_BSHUF => Some(FilterPipeline {
667 filters: vec![Filter {
671 id: FILTER_BSHUF,
672 flags: 0,
673 cd_values: vec![0, 0, element_size as u32, 0, 2],
674 }],
675 }),
676 COMPRESS_BLOSC => {
677 let compressor_code = match self.blosc_compressor {
678 BLOSC_LZ => 0,
679 BLOSC_LZ4 => 1,
680 BLOSC_LZ4HC => 2,
681 BLOSC_SNAPPY => 3,
682 BLOSC_ZLIB => 4,
683 BLOSC_ZSTD => 5,
684 _ => 0,
685 };
686 Some(FilterPipeline {
687 filters: vec![Filter {
688 id: FILTER_BLOSC,
689 flags: 0,
690 cd_values: vec![
691 2,
692 2,
693 element_size as u32,
694 0,
695 self.blosc_compress_level,
696 self.blosc_shuffle_type as u32,
697 compressor_code,
698 ],
699 }],
700 })
701 }
702 COMPRESS_NBIT => {
703 None
719 }
720 COMPRESS_JPEG => Some(FilterPipeline {
721 filters: vec![Filter {
722 id: FILTER_JPEG,
723 flags: 0,
724 cd_values: vec![self.jpeg_quality],
725 }],
726 }),
727 _ => None,
728 }
729 }
730
731 fn nbit_packing(
755 &self,
756 data_type: NDDataType,
757 chunk_nelmts: usize,
758 ) -> Option<(DatatypeMessage, FilterPipeline)> {
759 if self.compression_type != COMPRESS_NBIT {
760 return None;
761 }
762 let (size, signed) = match data_type {
763 NDDataType::Int8 => (1u32, true),
764 NDDataType::UInt8 => (1, false),
765 NDDataType::Int16 => (2, true),
766 NDDataType::UInt16 => (2, false),
767 NDDataType::Int32 => (4, true),
768 NDDataType::UInt32 => (4, false),
769 NDDataType::Int64 => (8, true),
770 NDDataType::UInt64 => (8, false),
771 NDDataType::Float32 | NDDataType::Float64 => return None,
772 };
773 if self.nbit_precision == 0 || self.nbit_precision > size * 8 {
774 return None;
775 }
776 let dt = DatatypeMessage::FixedPoint {
777 size,
778 byte_order: ByteOrder::LittleEndian,
779 signed,
780 bit_offset: self.nbit_offset as u16,
781 bit_precision: self.nbit_precision as u16,
782 };
783 let pipeline = FilterPipeline::nbit(&dt, chunk_nelmts);
784 Some((dt, pipeline))
785 }
786
787 fn codec_filter_pipeline(&self, codec: &Codec) -> Option<FilterPipeline> {
800 let element_size = codec.original_data_type.element_size() as u32;
801 match codec.name {
802 CodecName::LZ4 => Some(FilterPipeline::lz4()),
803 CodecName::BSLZ4 => Some(FilterPipeline {
804 filters: vec![Filter {
808 id: FILTER_BSHUF,
809 flags: 0,
810 cd_values: vec![0, 0, element_size, 0, 2],
811 }],
812 }),
813 CodecName::Blosc => {
814 Some(FilterPipeline {
818 filters: vec![Filter {
819 id: FILTER_BLOSC,
820 flags: 0,
821 cd_values: vec![
822 2,
823 2,
824 element_size,
825 0,
826 codec.level.max(0) as u32,
827 codec.shuffle.max(0) as u32,
828 codec.compressor.max(0) as u32,
829 ],
830 }],
831 })
832 }
833 CodecName::JPEG => Some(FilterPipeline {
839 filters: vec![Filter {
840 id: FILTER_JPEG,
841 flags: 0,
842 cd_values: vec![self.jpeg_quality],
843 }],
844 }),
845 CodecName::None | CodecName::Zlib | CodecName::LZ4HDF5 => None,
846 }
847 }
848
849 fn frame_chunk_dims(&self, frame_dims: &[usize]) -> Vec<usize> {
856 if frame_dims.len() == 2 {
857 let y = frame_dims[0].max(1);
858 let x = frame_dims[1].max(1);
859 vec![
860 Self::clamp_chunk(self.chunk.n_row_chunks, y, self.chunk.auto),
861 Self::clamp_chunk(self.chunk.n_col_chunks, x, self.chunk.auto),
862 ]
863 } else {
864 frame_dims.iter().map(|&d| d.max(1)).collect()
865 }
866 }
867
868 fn extra_dim_axes(&self) -> Option<Vec<usize>> {
878 if self.n_extra_dims == 0 {
879 return None;
880 }
881 Some(
882 (0..=self.n_extra_dims)
883 .rev()
884 .map(|i| self.extra_dims[i].size.max(1))
885 .collect(),
886 )
887 }
888
889 fn standard_layout(
905 &self,
906 frame_dims: &[usize],
907 ) -> (Vec<usize>, Vec<usize>, Option<Vec<usize>>) {
908 let frame_chunk = self.frame_chunk_dims(frame_dims);
909 match self.extra_dim_axes() {
910 Some(leading) => {
911 let mut shape = leading.clone();
912 shape.extend_from_slice(frame_dims);
913 let mut chunk = vec![1usize; leading.len()];
916 chunk.extend_from_slice(&frame_chunk);
917 (shape, chunk, Some(leading))
918 }
919 None => {
920 let mut shape = vec![1usize];
921 shape.extend_from_slice(frame_dims);
922 let mut chunk = vec![self.chunk.n_frames_chunks.max(1)];
923 chunk.extend_from_slice(&frame_chunk);
924 (shape, chunk, None)
925 }
926 }
927 }
928
929 fn compressed_layout(
939 &self,
940 frame_dims: &[usize],
941 ) -> (Vec<usize>, Vec<usize>, Option<Vec<usize>>) {
942 let frame_chunk: Vec<usize> = frame_dims.iter().map(|&d| d.max(1)).collect();
943 match self.extra_dim_axes() {
944 Some(leading) => {
945 let mut shape = leading.clone();
946 shape.extend_from_slice(frame_dims);
947 let mut chunk = vec![1usize; leading.len()];
948 chunk.extend_from_slice(&frame_chunk);
949 (shape, chunk, Some(leading))
950 }
951 None => {
952 let mut shape = vec![1usize];
953 shape.extend_from_slice(frame_dims);
954 let mut chunk = vec![1usize];
955 chunk.extend_from_slice(&frame_chunk);
956 (shape, chunk, None)
957 }
958 }
959 }
960
961 fn primary_layout(&self, frame_dims: &[usize]) -> (Vec<usize>, Vec<usize>, Option<usize>) {
972 let extra_extent = if self.n_extra_dims > 0 {
973 Some(
974 (0..self.n_extra_dims)
975 .map(|i| self.extra_dims[i].size.max(1))
976 .product::<usize>(),
977 )
978 } else {
979 None
980 };
981
982 let mut shape: Vec<usize> = vec![extra_extent.unwrap_or(1)];
983 shape.extend_from_slice(frame_dims);
984
985 let mut chunk = vec![if extra_extent.is_some() {
986 1
987 } else {
988 self.chunk.n_frames_chunks.max(1)
989 }];
990 chunk.extend_from_slice(&self.frame_chunk_dims(frame_dims));
991 (shape, chunk, extra_extent)
992 }
993
994 fn unravel(mut idx: usize, dims: &[usize]) -> Vec<usize> {
1000 let mut coords = vec![0usize; dims.len()];
1001 for d in (0..dims.len()).rev() {
1002 let s = dims[d].max(1);
1003 coords[d] = idx % s;
1004 idx /= s;
1005 }
1006 coords
1007 }
1008
1009 fn clamp_chunk(requested: usize, dim: usize, auto: bool) -> usize {
1012 if auto || requested == 0 || requested > dim {
1013 dim
1014 } else {
1015 requested
1016 }
1017 }
1018
1019 fn flush_band(
1035 ds: &rust_hdf5::H5Dataset,
1036 leading_coords: &[usize],
1037 frames: &[Vec<u8>],
1038 frame_dims: &[usize],
1039 chunk: &[usize],
1040 elem_size: usize,
1041 ) -> ADResult<()> {
1042 for (coords, buf) in
1043 Self::band_chunk_writes(leading_coords, frames, frame_dims, chunk, elem_size)
1044 {
1045 ds.write_chunk_at(&coords, &buf).map_err(|e| {
1046 ADError::UnsupportedConversion(format!("HDF5 write_chunk_at error: {}", e))
1047 })?;
1048 }
1049 Ok(())
1050 }
1051
1052 fn band_chunk_writes(
1065 leading_coords: &[usize],
1066 frames: &[Vec<u8>],
1067 frame_dims: &[usize],
1068 chunk: &[usize],
1069 elem_size: usize,
1070 ) -> Vec<(Vec<usize>, Vec<u8>)> {
1071 let lead = leading_coords.len();
1072 let fc = chunk[0];
1073 if frame_dims.len() != 2 {
1076 let frame_len = frame_dims.iter().product::<usize>() * elem_size;
1077 let mut buf = vec![0u8; fc * frame_len];
1078 for (f, fb) in frames.iter().take(fc).enumerate() {
1079 buf[f * frame_len..f * frame_len + frame_len].copy_from_slice(fb);
1080 }
1081 let mut coords = leading_coords.to_vec();
1082 coords.extend(std::iter::repeat_n(0, frame_dims.len()));
1083 return vec![(coords, buf)];
1084 }
1085
1086 let (y, x) = (frame_dims[0], frame_dims[1]);
1087 let (rc, cc) = (chunk[lead], chunk[lead + 1]);
1088 let row_tiles = y.div_ceil(rc);
1089 let col_tiles = x.div_ceil(cc);
1090 let mut writes = Vec::with_capacity(row_tiles * col_tiles);
1091 for ry in 0..row_tiles {
1092 for cx in 0..col_tiles {
1093 let mut tile = vec![0u8; fc * rc * cc * elem_size];
1094 for f in 0..fc {
1095 let Some(fb) = frames.get(f) else {
1096 break; };
1098 for r in 0..rc {
1099 let sy = ry * rc + r;
1100 if sy >= y {
1101 break;
1102 }
1103 for c in 0..cc {
1104 let sx = cx * cc + c;
1105 if sx >= x {
1106 break;
1107 }
1108 let src = (sy * x + sx) * elem_size;
1109 let dst = ((f * rc + r) * cc + c) * elem_size;
1110 tile[dst..dst + elem_size].copy_from_slice(&fb[src..src + elem_size]);
1111 }
1112 }
1113 }
1114 let mut coords = leading_coords.to_vec();
1115 coords.push(ry);
1116 coords.push(cx);
1117 writes.push((coords, tile));
1118 }
1119 }
1120 writes
1121 }
1122
1123 fn finalize_standard_datasets(&mut self) -> ADResult<()> {
1129 let Some(frame_dims) = self.open_frame_dims.clone() else {
1130 return Ok(());
1131 };
1132 let (_, chunk, leading) = self.standard_layout(&frame_dims);
1133 let elem_size = self.open_data_type.map(|t| t.element_size()).unwrap_or(1);
1134 let fc = chunk[0];
1135 let Some(Hdf5Handle::Standard { detectors, .. }) = self.handle.as_mut() else {
1136 return Ok(());
1137 };
1138 for det in detectors.values_mut() {
1139 let total = det.frame_count;
1140 if !det.frame_band.is_empty() {
1143 let last = total.saturating_sub(1);
1144 let leading_coords = match &leading {
1145 Some(lead) => Self::unravel(last, lead),
1146 None => vec![last / fc],
1147 };
1148 Self::flush_band(
1149 &det.ds,
1150 &leading_coords,
1151 &det.frame_band,
1152 &frame_dims,
1153 &chunk,
1154 elem_size,
1155 )?;
1156 det.frame_band.clear();
1157 }
1158 if total > 0 {
1163 let mut dims = leading.clone().unwrap_or_else(|| vec![total]);
1164 dims.extend_from_slice(&frame_dims);
1165 det.ds.set_extent(&dims).map_err(|e| {
1166 ADError::UnsupportedConversion(format!("HDF5 set_extent error: {}", e))
1167 })?;
1168 }
1169 }
1170 Ok(())
1171 }
1172
1173 fn open_swmr(&mut self, path: &Path, array: &NDArray) -> ADResult<()> {
1192 let mut swmr = SwmrFileWriter::create(path)
1193 .map_err(|e| ADError::UnsupportedConversion(format!("SWMR create error: {}", e)))?;
1194 self.build_swmr_layout_groups(&mut swmr)?;
1195
1196 let usize_frame_dims: Vec<usize> = array.dims.iter().rev().map(|d| d.size).collect();
1197 let frame_dims: Vec<u64> = usize_frame_dims.iter().map(|&d| d as u64).collect();
1198
1199 let element_size = array.data.data_type().element_size();
1205 let pipeline = self.build_pipeline(element_size);
1206 let chunk: Vec<u64> = {
1207 let (_, c, _) = self.primary_layout(&usize_frame_dims);
1208 c.iter().map(|&v| v as u64).collect()
1209 };
1210
1211 let (grid_shape_usize, grid_chunk_usize, grid_leading) =
1217 self.standard_layout(&usize_frame_dims);
1218 let use_grid = grid_leading.is_some() && pipeline.is_none();
1219 let grid_shape: Vec<u64> = grid_shape_usize.iter().map(|&v| v as u64).collect();
1220 let grid_chunk: Vec<u64> = grid_chunk_usize.iter().map(|&v| v as u64).collect();
1221
1222 let ds_name = self.resolved_dataset_path.clone();
1229
1230 macro_rules! create_ds {
1231 ($t:ty) => {
1232 if use_grid {
1233 swmr.create_grid_dataset::<$t>(&ds_name, &grid_shape, &grid_chunk)
1234 .map_err(|e| {
1235 ADError::UnsupportedConversion(format!(
1236 "SWMR create grid dataset error: {}",
1237 e
1238 ))
1239 })
1240 } else {
1241 match pipeline.clone() {
1242 Some(pl) => swmr
1243 .create_streaming_dataset_chunked_compressed::<$t>(
1244 &ds_name,
1245 &frame_dims,
1246 &chunk,
1247 pl,
1248 )
1249 .map_err(|e| {
1250 ADError::UnsupportedConversion(format!(
1251 "SWMR create compressed dataset error: {}",
1252 e
1253 ))
1254 }),
1255 None => swmr
1256 .create_streaming_dataset_chunked::<$t>(&ds_name, &frame_dims, &chunk)
1257 .map_err(|e| {
1258 ADError::UnsupportedConversion(format!(
1259 "SWMR create dataset error: {}",
1260 e
1261 ))
1262 }),
1263 }
1264 }
1265 };
1266 }
1267
1268 let ds_index = match array.data.data_type() {
1269 NDDataType::Int8 => create_ds!(i8)?,
1270 NDDataType::UInt8 => create_ds!(u8)?,
1271 NDDataType::Int16 => create_ds!(i16)?,
1272 NDDataType::UInt16 => create_ds!(u16)?,
1273 NDDataType::Int32 => create_ds!(i32)?,
1274 NDDataType::UInt32 => create_ds!(u32)?,
1275 NDDataType::Int64 => create_ds!(i64)?,
1276 NDDataType::UInt64 => create_ds!(u64)?,
1277 NDDataType::Float32 => create_ds!(f32)?,
1278 NDDataType::Float64 => create_ds!(f64)?,
1279 };
1280
1281 self.write_swmr_layout_dataset_attrs(&mut swmr, ds_index)?;
1286 self.write_swmr_ndarray_default_attrs(&mut swmr, ds_index, array)?;
1287 self.build_swmr_layout_hardlinks(&mut swmr)?;
1288 self.write_swmr_ndattr_element_attrs(&mut swmr, ds_index)?;
1292
1293 swmr.start_swmr()
1294 .map_err(|e| ADError::UnsupportedConversion(format!("SWMR start error: {}", e)))?;
1295
1296 let compression_dropped = self.compression_type != COMPRESS_NONE && pipeline.is_none();
1301 if compression_dropped {
1302 eprintln!(
1303 "NDFileHDF5: WARNING — SWMR mode requested compression type {} \
1304 but no filter pipeline could be built for it; the SWMR file \
1305 will be written UNCOMPRESSED.",
1306 self.compression_type
1307 );
1308 }
1309
1310 let grid = if use_grid {
1311 grid_leading.map(|leading| SwmrGridLayout {
1313 leading,
1314 frame_dims: usize_frame_dims.clone(),
1315 chunk: grid_chunk_usize.clone(),
1316 elem_size: element_size,
1317 })
1318 } else {
1319 None
1320 };
1321 self.handle = Some(Hdf5Handle::Swmr {
1322 writer: Box::new(swmr),
1323 ds_index,
1324 compression_dropped,
1325 grid,
1326 });
1327 self.open_data_type = Some(array.data.data_type());
1328 self.open_frame_dims = Some(array.dims.iter().rev().map(|d| d.size).collect::<Vec<_>>());
1329 Ok(())
1330 }
1331
1332 fn build_swmr_layout_groups(&self, swmr: &mut SwmrFileWriter) -> ADResult<()> {
1341 let layout = match self.layout.as_ref() {
1342 Some(l) => l,
1343 None => return Ok(()),
1344 };
1345 fn collect<'a>(
1346 g: &'a crate::hdf5_layout::LayoutGroup,
1347 prefix: &str,
1348 out: &mut Vec<(String, &'a crate::hdf5_layout::LayoutGroup)>,
1349 ) {
1350 let here = if prefix.is_empty() {
1351 g.name.clone()
1352 } else {
1353 format!("{}/{}", prefix, g.name)
1354 };
1355 out.push((here.clone(), g));
1356 for sub in &g.groups {
1357 collect(sub, &here, out);
1358 }
1359 }
1360 let mut nodes = Vec::new();
1361 for g in &layout.groups {
1362 collect(g, "", &mut nodes);
1363 }
1364 nodes.sort_by_key(|(p, _)| p.matches('/').count());
1365 let mut created: std::collections::HashSet<String> = std::collections::HashSet::new();
1366 for (path, node) in &nodes {
1367 if !created.insert(path.clone()) {
1368 continue;
1369 }
1370 let (parent, leaf) = match path.rsplit_once('/') {
1371 Some((p, l)) => (format!("/{}", p), l),
1372 None => ("/".to_string(), path.as_str()),
1373 };
1374 swmr.create_group(&parent, leaf).map_err(|e| {
1375 ADError::UnsupportedConversion(format!("SWMR layout group '{}': {}", path, e))
1376 })?;
1377 write_swmr_group_constant_attrs(swmr, &format!("/{}", path), &node.attributes)?;
1381 }
1382 Ok(())
1383 }
1384
1385 fn build_swmr_layout_hardlinks(&self, swmr: &mut SwmrFileWriter) -> ADResult<()> {
1395 let layout = match self.layout.as_ref() {
1396 Some(l) => l,
1397 None => return Ok(()),
1398 };
1399 fn collect<'a>(
1400 g: &'a crate::hdf5_layout::LayoutGroup,
1401 prefix: &str,
1402 out: &mut Vec<(String, &'a crate::hdf5_layout::LayoutHardlink)>,
1403 ) {
1404 let here = if prefix.is_empty() {
1405 g.name.clone()
1406 } else {
1407 format!("{}/{}", prefix, g.name)
1408 };
1409 for hl in &g.hardlinks {
1410 out.push((here.clone(), hl));
1411 }
1412 for sub in &g.groups {
1413 collect(sub, &here, out);
1414 }
1415 }
1416 let mut links = Vec::new();
1417 for g in &layout.groups {
1418 collect(g, "", &mut links);
1419 }
1420 for (parent_path, hl) in &links {
1421 let parent = format!("/{}", parent_path);
1422 swmr.create_hard_link(&parent, &hl.name, &hl.target)
1423 .map_err(|e| {
1424 ADError::UnsupportedConversion(format!(
1425 "SWMR layout hardlink '{}/{}' -> '{}': {}",
1426 parent_path, hl.name, hl.target, e
1427 ))
1428 })?;
1429 }
1430 Ok(())
1431 }
1432
1433 fn write_swmr_layout_dataset_attrs(
1441 &self,
1442 swmr: &mut SwmrFileWriter,
1443 ds_index: usize,
1444 ) -> ADResult<()> {
1445 use crate::hdf5_layout::{LayoutDataType, LayoutSource};
1446 let layout = match self.layout.as_ref() {
1447 Some(l) => l,
1448 None => return Ok(()),
1449 };
1450 let resolved_ds = self.resolved_dataset_path.as_str();
1451 let mut attrs: Vec<(String, LayoutDataType, String)> = Vec::new();
1452 layout.for_each_dataset(|path, d| {
1453 let full = format!("{}/{}", path, d.name);
1454 if full.trim_start_matches('/') == resolved_ds {
1455 for a in &d.attributes {
1456 if a.source == LayoutSource::Constant {
1457 attrs.push((a.name.clone(), a.data_type, a.value.clone()));
1458 }
1459 }
1460 }
1461 });
1462 for (name, dtype, value) in &attrs {
1463 match dtype {
1464 LayoutDataType::Int => {
1465 let v: i64 = value.trim().parse().unwrap_or(0);
1466 swmr.set_dataset_attr_numeric(ds_index, name, &v)
1467 }
1468 LayoutDataType::Float => {
1469 let v: f64 = value.trim().parse().unwrap_or(0.0);
1470 swmr.set_dataset_attr_numeric(ds_index, name, &v)
1471 }
1472 LayoutDataType::String => swmr.set_dataset_attr_string(ds_index, name, value),
1473 }
1474 .map_err(|e| {
1475 ADError::UnsupportedConversion(format!(
1476 "SWMR layout dataset attribute '{}': {}",
1477 name, e
1478 ))
1479 })?;
1480 }
1481 Ok(())
1482 }
1483
1484 fn resolve_layout_paths(&mut self) {
1496 let strip = |s: String| s.trim_start_matches('/').to_string();
1497 match self.layout.as_ref() {
1498 Some(layout) => {
1499 self.resolved_dataset_path = layout
1500 .detector_dataset_path()
1501 .map(strip)
1502 .unwrap_or_else(|| self.dataset_name.clone());
1503 self.resolved_ndattr_group =
1504 layout.ndattr_default_group().map(strip).unwrap_or_default();
1505 self.resolved_perf_group = layout
1506 .dataset_group_path("timestamp")
1507 .map(strip)
1508 .unwrap_or_default();
1509 }
1510 None => {
1511 self.resolved_dataset_path = self.dataset_name.clone();
1512 self.resolved_ndattr_group.clear();
1513 self.resolved_perf_group.clear();
1514 }
1515 }
1516 }
1517
1518 fn build_layout_groups(&self, file: &H5File) -> ADResult<()> {
1527 let layout = match self.layout.as_ref() {
1528 Some(l) => l,
1529 None => return Ok(()),
1530 };
1531 fn collect<'a>(
1532 g: &'a crate::hdf5_layout::LayoutGroup,
1533 prefix: &str,
1534 out: &mut Vec<(String, &'a crate::hdf5_layout::LayoutGroup)>,
1535 ) {
1536 let here = if prefix.is_empty() {
1537 g.name.clone()
1538 } else {
1539 format!("{}/{}", prefix, g.name)
1540 };
1541 out.push((here.clone(), g));
1542 for sub in &g.groups {
1543 collect(sub, &here, out);
1544 }
1545 }
1546 let mut nodes = Vec::new();
1547 for g in &layout.groups {
1548 collect(g, "", &mut nodes);
1549 }
1550 nodes.sort_by_key(|(p, _)| p.matches('/').count());
1551 let mut created: std::collections::HashSet<String> = std::collections::HashSet::new();
1552 for (path, node) in &nodes {
1553 if !created.insert(path.clone()) {
1554 continue;
1555 }
1556 let (parent, leaf) = match path.rsplit_once('/') {
1557 Some((p, l)) => (p, l),
1558 None => ("", path.as_str()),
1559 };
1560 let parent_group = if parent.is_empty() {
1562 None
1563 } else {
1564 Some(Self::open_write_group(file, parent)?)
1565 };
1566 let group = match parent_group.as_ref() {
1567 Some(g) => g.create_group(leaf),
1568 None => file.create_group(leaf),
1569 }
1570 .map_err(|e| {
1571 ADError::UnsupportedConversion(format!("HDF5 layout group '{}': {}", path, e))
1572 })?;
1573 write_group_constant_attrs(&group, &node.attributes);
1576 }
1577 Ok(())
1578 }
1579
1580 fn build_layout_hardlinks(&self, file: &H5File) -> ADResult<()> {
1598 let layout = match self.layout.as_ref() {
1599 Some(l) => l,
1600 None => return Ok(()),
1601 };
1602 fn collect<'a>(
1604 g: &'a crate::hdf5_layout::LayoutGroup,
1605 prefix: &str,
1606 out: &mut Vec<(String, &'a crate::hdf5_layout::LayoutHardlink)>,
1607 ) {
1608 let here = if prefix.is_empty() {
1609 g.name.clone()
1610 } else {
1611 format!("{}/{}", prefix, g.name)
1612 };
1613 for hl in &g.hardlinks {
1614 out.push((here.clone(), hl));
1615 }
1616 for sub in &g.groups {
1617 collect(sub, &here, out);
1618 }
1619 }
1620 let mut links = Vec::new();
1621 for g in &layout.groups {
1622 collect(g, "", &mut links);
1623 }
1624 for (parent_path, hl) in &links {
1625 let parent = Self::open_write_group(file, parent_path)?;
1628 parent.link(&hl.name, &hl.target).map_err(|e| {
1629 ADError::UnsupportedConversion(format!(
1630 "HDF5 layout hardlink '{}/{}' -> '{}': {}",
1631 parent_path, hl.name, hl.target, e
1632 ))
1633 })?;
1634 }
1635 Ok(())
1636 }
1637
1638 fn open_write_group(file: &H5File, path: &str) -> ADResult<rust_hdf5::H5Group> {
1642 let mut current: Option<rust_hdf5::H5Group> = None;
1643 for seg in path.split('/').filter(|s| !s.is_empty()) {
1644 let next = match current.as_ref() {
1645 Some(g) => g.group(seg),
1646 None => file.root_group().group(seg),
1647 }
1648 .map_err(|e| {
1649 ADError::UnsupportedConversion(format!("HDF5 group reopen '{}': {}", seg, e))
1650 })?;
1651 current = Some(next);
1652 }
1653 current.ok_or_else(|| ADError::UnsupportedConversion("empty group path".into()))
1654 }
1655
1656 fn create_detector_datasets(&mut self, array: &NDArray) -> ADResult<()> {
1664 let frame_dims: Vec<usize> = array.dims.iter().rev().map(|d| d.size).collect();
1665 let (ds_data_type, shape, chunk, leading, pipeline) = match array.codec.as_ref() {
1673 Some(codec) => {
1674 let pl = self.codec_filter_pipeline(codec).ok_or_else(|| {
1675 ADError::UnsupportedConversion(format!(
1676 "HDF5 cannot direct-chunk-write codec '{}' \
1677 (only jpeg/blosc/lz4/bslz4 are supported)",
1678 codec.name.as_str()
1679 ))
1680 })?;
1681 let (shape, chunk, leading) = self.compressed_layout(&frame_dims);
1682 (codec.original_data_type, shape, chunk, leading, Some(pl))
1683 }
1684 None => {
1685 let dt = array.data.data_type();
1686 let (shape, chunk, leading) = self.standard_layout(&frame_dims);
1687 (
1688 dt,
1689 shape,
1690 chunk,
1691 leading,
1692 self.build_pipeline(dt.element_size()),
1693 )
1694 }
1695 };
1696 let (nbit_dt, pipeline): (Option<DatatypeMessage>, Option<FilterPipeline>) =
1702 if array.codec.is_none() {
1703 let chunk_nelmts: usize = chunk.iter().product();
1704 match self.nbit_packing(ds_data_type, chunk_nelmts) {
1705 Some((dt, pl)) => (Some(dt), Some(pl)),
1706 None => (None, pipeline),
1707 }
1708 } else {
1709 (None, pipeline)
1710 };
1711 let max_shape: Vec<Option<usize>> = shape
1719 .iter()
1720 .zip(chunk.iter())
1721 .enumerate()
1722 .map(|(i, (&s, &c))| {
1723 if leading.is_none() && i == 0 {
1724 None
1725 } else {
1726 Some(s.div_ceil(c) * c)
1727 }
1728 })
1729 .collect();
1730
1731 match self.handle {
1735 Some(Hdf5Handle::Standard { ref file, .. }) => self.build_layout_groups(file)?,
1736 _ => return Err(ADError::UnsupportedConversion("no HDF5 file open".into())),
1737 }
1738
1739 let detector_keys: Vec<String> = match self.layout.as_ref() {
1746 Some(l) => {
1747 let mut keys = l.detector_dataset_paths();
1748 if keys.is_empty() {
1749 keys.push(format!("/{}", self.resolved_dataset_path));
1750 }
1751 keys
1752 }
1753 None => vec![format!("/{}", self.resolved_dataset_path)],
1754 };
1755 let detector_specs: Vec<(
1756 String,
1757 Vec<(String, crate::hdf5_layout::LayoutDataType, String)>,
1758 )> = detector_keys
1759 .iter()
1760 .map(|key| {
1761 let stripped = key.trim_start_matches('/').to_string();
1762 let attrs = self
1763 .layout
1764 .as_ref()
1765 .map(|l| {
1766 use crate::hdf5_layout::LayoutSource;
1767 let mut out = Vec::new();
1768 l.for_each_dataset(|path, d| {
1769 let full = format!("{}/{}", path, d.name);
1770 if full.trim_start_matches('/') == stripped {
1771 for a in &d.attributes {
1772 if a.source == LayoutSource::Constant {
1773 out.push((a.name.clone(), a.data_type, a.value.clone()));
1774 }
1775 }
1776 }
1777 });
1778 out
1779 })
1780 .unwrap_or_default();
1781 (key.clone(), attrs)
1782 })
1783 .collect();
1784
1785 let dtype_ordinal = ds_data_type as i32;
1786 let fill = self.fill_value;
1787 let row_chunks = self.chunk.n_row_chunks as i32;
1788 let col_chunks = self.chunk.n_col_chunks as i32;
1789 let frame_chunks = self.chunk.n_frames_chunks as i32;
1790 let n_extra = self.n_extra_dims as i32;
1791 let extra_meta: Vec<(usize, i32, String)> = (0..self.n_extra_dims)
1792 .map(|i| {
1793 (
1794 i,
1795 self.extra_dims[i].size.max(1) as i32,
1796 self.extra_dims[i].name.clone(),
1797 )
1798 })
1799 .collect();
1800
1801 let nd_num_dims = array.dims.len() as i32;
1806 let dim_offsets: Vec<i32> = array.dims.iter().map(|d| d.offset as i32).collect();
1810 let dim_binnings: Vec<i32> = array.dims.iter().map(|d| d.binning as i32).collect();
1811 let dim_reverses: Vec<i32> = array.dims.iter().map(|d| d.reverse as i32).collect();
1812
1813 macro_rules! create_ds {
1814 ($t:ty, $h5file:expr, $ds_group:expr, $ds_name:expr, $constant_attrs:expr) => {{
1815 let mut builder = match $ds_group.as_ref() {
1816 Some(g) => g.new_dataset::<$t>(),
1817 None => $h5file.new_dataset::<$t>(),
1818 }
1819 .shape(&shape[..])
1820 .chunk(&chunk[..])
1821 .max_shape(&max_shape[..])
1822 .fill_value(fill as $t);
1828 if let Some(ref dt) = nbit_dt {
1832 builder = builder.datatype(dt.clone());
1833 }
1834 if let Some(ref pl) = pipeline {
1835 builder = builder.filter_pipeline(pl.clone());
1836 }
1837 let ds = builder.create($ds_name.as_str()).map_err(|e| {
1838 ADError::UnsupportedConversion(format!("HDF5 dataset error: {}", e))
1839 })?;
1840 let _ = ds
1842 .new_attr::<i32>()
1843 .shape(())
1844 .create(DTYPE_ATTR)
1845 .and_then(|a| a.write_numeric(&dtype_ordinal));
1846 let _ = ds
1850 .new_attr::<f64>()
1851 .shape(())
1852 .create("HDF5_fillValue")
1853 .and_then(|a| a.write_numeric(&fill));
1854 for (name, val) in [
1858 ("HDF5_nRowChunks", row_chunks),
1859 ("HDF5_nColChunks", col_chunks),
1860 ("HDF5_nFramesChunks", frame_chunks),
1861 ("HDF5_nExtraDims", n_extra),
1862 ] {
1863 let _ = ds
1864 .new_attr::<i32>()
1865 .shape(())
1866 .create(name)
1867 .and_then(|a| a.write_numeric(&val));
1868 }
1869 for (i, size, name) in &extra_meta {
1872 let _ = ds
1873 .new_attr::<i32>()
1874 .shape(())
1875 .create(&format!("HDF5_extraDimSize{}", i))
1876 .and_then(|a| a.write_numeric(size));
1877 if !name.is_empty() {
1878 let s = rust_hdf5::types::VarLenUnicode(name.clone());
1879 let _ = ds
1880 .new_attr::<rust_hdf5::types::VarLenUnicode>()
1881 .shape(())
1882 .create(&format!("HDF5_extraDimName{}", i))
1883 .and_then(|a| a.write_scalar(&s));
1884 }
1885 }
1886 for (aname, atype, avalue) in $constant_attrs {
1889 use crate::hdf5_layout::LayoutDataType;
1890 match atype {
1891 LayoutDataType::Int => {
1892 let v: i64 = avalue.trim().parse().unwrap_or(0);
1893 let _ = ds
1894 .new_attr::<i64>()
1895 .shape(())
1896 .create(aname)
1897 .and_then(|a| a.write_numeric(&v));
1898 }
1899 LayoutDataType::Float => {
1900 let v: f64 = avalue.trim().parse().unwrap_or(0.0);
1901 let _ = ds
1902 .new_attr::<f64>()
1903 .shape(())
1904 .create(aname)
1905 .and_then(|a| a.write_numeric(&v));
1906 }
1907 LayoutDataType::String => {
1908 let s = rust_hdf5::types::VarLenUnicode(avalue.clone());
1909 let _ = ds
1910 .new_attr::<rust_hdf5::types::VarLenUnicode>()
1911 .shape(())
1912 .create(aname)
1913 .and_then(|a| a.write_scalar(&s));
1914 }
1915 }
1916 }
1917 let _ = ds
1923 .new_attr::<i32>()
1924 .shape(())
1925 .create("NDArrayNumDims")
1926 .and_then(|a| a.write_numeric(&nd_num_dims));
1927 for (name, vals) in [
1928 ("NDArrayDimOffset", &dim_offsets),
1929 ("NDArrayDimBinning", &dim_binnings),
1930 ("NDArrayDimReverse", &dim_reverses),
1931 ] {
1932 let _ = if vals.len() == 1 {
1933 ds.new_attr::<i32>()
1934 .shape(())
1935 .create(name)
1936 .and_then(|a| a.write_numeric(&vals[0]))
1937 } else {
1938 ds.new_attr::<i32>()
1939 .shape([vals.len()])
1940 .create(name)
1941 .and_then(|a| a.write_array(vals))
1942 };
1943 }
1944 ds
1945 }};
1946 }
1947
1948 let h5file = match self.handle {
1949 Some(Hdf5Handle::Standard { ref file, .. }) => file,
1950 _ => return Err(ADError::UnsupportedConversion("no HDF5 file open".into())),
1951 };
1952
1953 let mut detectors: std::collections::HashMap<String, DetectorDataset> =
1954 std::collections::HashMap::with_capacity(detector_specs.len());
1955 for (key, constant_attrs) in &detector_specs {
1956 let stripped = key.trim_start_matches('/');
1959 let (ds_group, ds_name): (Option<rust_hdf5::H5Group>, String) =
1960 match stripped.rsplit_once('/') {
1961 Some((group_path, leaf)) => (
1962 Some(Self::open_write_group(h5file, group_path)?),
1963 leaf.to_string(),
1964 ),
1965 None => (None, stripped.to_string()),
1966 };
1967 let ds = match ds_data_type {
1972 NDDataType::Int8 => create_ds!(i8, h5file, ds_group, ds_name, constant_attrs),
1973 NDDataType::UInt8 => create_ds!(u8, h5file, ds_group, ds_name, constant_attrs),
1974 NDDataType::Int16 => create_ds!(i16, h5file, ds_group, ds_name, constant_attrs),
1975 NDDataType::UInt16 => create_ds!(u16, h5file, ds_group, ds_name, constant_attrs),
1976 NDDataType::Int32 => create_ds!(i32, h5file, ds_group, ds_name, constant_attrs),
1977 NDDataType::UInt32 => create_ds!(u32, h5file, ds_group, ds_name, constant_attrs),
1978 NDDataType::Int64 => create_ds!(i64, h5file, ds_group, ds_name, constant_attrs),
1979 NDDataType::UInt64 => create_ds!(u64, h5file, ds_group, ds_name, constant_attrs),
1980 NDDataType::Float32 => create_ds!(f32, h5file, ds_group, ds_name, constant_attrs),
1981 NDDataType::Float64 => create_ds!(f64, h5file, ds_group, ds_name, constant_attrs),
1982 };
1983 detectors.insert(
1984 key.clone(),
1985 DetectorDataset {
1986 ds,
1987 frame_count: 0,
1988 frame_band: Vec::new(),
1989 },
1990 );
1991 }
1992
1993 if let Some(Hdf5Handle::Standard { detectors: d, .. }) = self.handle.as_mut() {
1994 *d = detectors;
1995 }
1996 self.open_data_type = Some(ds_data_type);
1997 self.open_frame_dims = Some(frame_dims);
1998 self.open_codec = array.codec.clone();
1999 Ok(())
2000 }
2001
2002 fn resolve_destination_key(&self, array: &NDArray) -> ADResult<String> {
2013 let default = format!("/{}", self.resolved_dataset_path);
2014 let attr_name = self
2015 .layout
2016 .as_ref()
2017 .and_then(|l| l.detector_data_destination.as_deref())
2018 .filter(|s| !s.is_empty());
2019 let Some(attr_name) = attr_name else {
2020 return Ok(default);
2021 };
2022 let Some(attr) = array.attributes.get(attr_name) else {
2025 return Ok(default);
2026 };
2027 match attr.value.as_string_typed() {
2028 Some(value) => {
2029 let known = matches!(
2030 &self.handle,
2031 Some(Hdf5Handle::Standard { detectors, .. })
2032 if detectors.contains_key(value)
2033 );
2034 Ok(if known { value.to_string() } else { default })
2035 }
2036 None => Err(ADError::UnsupportedConversion(format!(
2037 "HDF5 detector_data_destination attribute '{}' is not a string",
2038 attr_name
2039 ))),
2040 }
2041 }
2042
2043 fn write_standard(&mut self, array: &NDArray) -> ADResult<()> {
2048 if self.frame_count == 0 {
2049 self.create_detector_datasets(array)?;
2050 self.create_attribute_datasets(array);
2051 }
2052
2053 let frame_dims = self
2054 .open_frame_dims
2055 .clone()
2056 .ok_or_else(|| ADError::UnsupportedConversion("dataset not initialised".into()))?;
2057 let cur_dims: Vec<usize> = array.dims.iter().rev().map(|d| d.size).collect();
2058 if cur_dims != frame_dims {
2059 return Err(ADError::UnsupportedConversion(format!(
2060 "HDF5 frame shape changed mid-stream: {:?} != {:?}",
2061 cur_dims, frame_dims
2062 )));
2063 }
2064
2065 if !codecs_match(self.open_codec.as_ref(), array.codec.as_ref()) {
2070 return Err(ADError::UnsupportedConversion(format!(
2071 "HDF5 codec changed mid-stream: dataset codec {:?} != frame codec {:?}",
2072 self.open_codec.as_ref().map(|c| c.name),
2073 array.codec.as_ref().map(|c| c.name),
2074 )));
2075 }
2076
2077 let (_shape, chunk, leading) = self.standard_layout(&frame_dims);
2078 let fc = chunk[0];
2079
2080 let dest_key = self.resolve_destination_key(array)?;
2086 let chunk_bytes = array.codec.as_ref().map(|codec| {
2087 let total_bytes =
2088 frame_dims.iter().product::<usize>() * codec.original_data_type.element_size();
2089 codec_chunk_bytes(codec, total_bytes, array.data.as_u8_slice())
2090 });
2091 let frame_le = if chunk_bytes.is_none() {
2092 Some(nd_buffer_to_le_bytes(&array.data))
2093 } else {
2094 None
2095 };
2096 let elem_size = array.data.data_type().element_size();
2097
2098 {
2099 let Some(Hdf5Handle::Standard { detectors, .. }) = self.handle.as_mut() else {
2100 return Err(ADError::UnsupportedConversion(
2101 "HDF5 detector datasets not initialised".into(),
2102 ));
2103 };
2104 let det = detectors.get_mut(&dest_key).ok_or_else(|| {
2105 ADError::UnsupportedConversion(format!(
2106 "HDF5 destination dataset '{}' not found",
2107 dest_key
2108 ))
2109 })?;
2110
2111 let frame_idx = det.frame_count;
2114 if let Some(ref lead) = leading {
2115 let total: usize = lead.iter().product();
2116 if frame_idx >= total {
2117 return Err(ADError::UnsupportedConversion(format!(
2118 "HDF5 extra-dimension capacity exceeded: frame {} >= {}",
2119 frame_idx, total
2120 )));
2121 }
2122 }
2123
2124 if let Some(cb) = &chunk_bytes {
2125 det.ds.write_chunk_raw(frame_idx, cb, 0).map_err(|e| {
2131 ADError::UnsupportedConversion(format!("HDF5 direct chunk write error: {}", e))
2132 })?;
2133 } else {
2134 let fle = frame_le.expect("frame_le is set whenever chunk_bytes is None");
2140 det.frame_band.push(fle);
2141 if det.frame_band.len() >= fc {
2142 let leading_coords = match &leading {
2143 Some(lead) => Self::unravel(frame_idx, lead),
2144 None => vec![frame_idx / fc],
2145 };
2146 Self::flush_band(
2147 &det.ds,
2148 &leading_coords,
2149 &det.frame_band,
2150 &frame_dims,
2151 &chunk,
2152 elem_size,
2153 )?;
2154 det.frame_band.clear();
2155 }
2156 }
2157 det.frame_count += 1;
2158 }
2159
2160 if self.store_attributes {
2162 for ad in self.attr_datasets.iter_mut() {
2163 let value = array
2164 .attributes
2165 .get(&ad.name)
2166 .map(|a| a.value.clone())
2167 .unwrap_or(NDAttrValue::Undefined);
2168 ad.push(&value);
2169 }
2170 }
2171 Ok(())
2172 }
2173
2174 fn create_attribute_datasets(&mut self, array: &NDArray) {
2177 self.attr_datasets.clear();
2178 if !self.store_attributes {
2179 return;
2180 }
2181 for attr in array.attributes.iter() {
2182 self.attr_datasets.push(AttributeDataset::new(attr));
2183 }
2184 }
2185
2186 fn attribute_chunking(&self) -> usize {
2192 if self.chunk.ndattr_chunk != 0 {
2193 return self.chunk.ndattr_chunk;
2194 }
2195 if self.open_mode == NDFileMode::Single {
2196 return 1;
2197 }
2198 if self.num_capture > 0 {
2199 self.num_capture
2200 } else {
2201 16 * 1024
2202 }
2203 }
2204
2205 fn seed_ndattr_element_values(&mut self, array: &NDArray) {
2211 self.ndattr_first_values.clear();
2212 self.ndattr_last_values.clear();
2213 self.ndattr_element_names.clear();
2214 let Some(layout) = self.layout.as_ref() else {
2215 return;
2216 };
2217 let mut names: Vec<String> = Vec::new();
2218 for e in layout.ndattribute_element_attrs() {
2219 if !names.contains(&e.ndattribute) {
2220 names.push(e.ndattribute);
2221 }
2222 }
2223 for name in &names {
2224 if let Some(attr) = array.attributes.get(name) {
2225 self.ndattr_first_values
2226 .insert(name.clone(), attr.value.clone());
2227 self.ndattr_last_values
2228 .insert(name.clone(), attr.value.clone());
2229 }
2230 }
2231 self.ndattr_element_names = names;
2232 }
2233
2234 fn update_ndattr_element_values(&mut self, array: &NDArray) {
2240 if self.ndattr_element_names.is_empty() {
2241 return;
2242 }
2243 for name in &self.ndattr_element_names {
2244 if let Some(attr) = array.attributes.get(name) {
2245 self.ndattr_last_values
2246 .insert(name.clone(), attr.value.clone());
2247 }
2248 }
2249 }
2250
2251 fn flush_ndattr_element_attrs(
2268 &self,
2269 file: &H5File,
2270 detectors: &std::collections::HashMap<String, DetectorDataset>,
2271 ) -> ADResult<()> {
2272 use crate::hdf5_layout::LayoutWhen;
2273 let Some(layout) = self.layout.as_ref() else {
2274 return Ok(());
2275 };
2276 for e in layout.ndattribute_element_attrs() {
2277 let value = match e.when {
2278 LayoutWhen::OnFileOpen | LayoutWhen::OnFrame => {
2279 self.ndattr_first_values.get(&e.ndattribute)
2280 }
2281 LayoutWhen::OnFileClose => self.ndattr_last_values.get(&e.ndattribute),
2282 LayoutWhen::OnFileWrite => None,
2283 };
2284 let Some(value) = value else {
2285 continue;
2286 };
2287 let path = e.element_path.trim_start_matches('/');
2288 if e.is_dataset {
2289 if let Some(det) = detectors.get(&e.element_path) {
2296 write_ndattr_dataset_attr(&det.ds, &e.attr_name, value);
2297 } else if let Ok(ds) = file.dataset_writer(path) {
2298 write_ndattr_dataset_attr(&ds, &e.attr_name, value);
2299 }
2300 } else {
2301 let group = Self::open_write_group(file, path)?;
2302 write_ndattr_group_attr(&group, &e.attr_name, value);
2303 }
2304 }
2305 Ok(())
2306 }
2307
2308 fn write_swmr_ndarray_default_attrs(
2317 &self,
2318 swmr: &mut SwmrFileWriter,
2319 ds_index: usize,
2320 array: &NDArray,
2321 ) -> ADResult<()> {
2322 let nd_num_dims = array.dims.len() as i32;
2323 swmr.set_dataset_attr_numeric(ds_index, "NDArrayNumDims", &nd_num_dims)
2324 .map_err(|e| {
2325 ADError::UnsupportedConversion(format!("SWMR NDArrayNumDims attr: {}", e))
2326 })?;
2327 let dim_offsets: Vec<i32> = array.dims.iter().map(|d| d.offset as i32).collect();
2328 let dim_binnings: Vec<i32> = array.dims.iter().map(|d| d.binning as i32).collect();
2329 let dim_reverses: Vec<i32> = array.dims.iter().map(|d| d.reverse as i32).collect();
2330 for (name, vals) in [
2331 ("NDArrayDimOffset", &dim_offsets),
2332 ("NDArrayDimBinning", &dim_binnings),
2333 ("NDArrayDimReverse", &dim_reverses),
2334 ] {
2335 let res = if vals.len() == 1 {
2336 swmr.set_dataset_attr_numeric(ds_index, name, &vals[0])
2337 } else {
2338 swmr.set_dataset_attr_array(ds_index, name, &[vals.len() as u64], vals)
2339 };
2340 res.map_err(|e| ADError::UnsupportedConversion(format!("SWMR {} attr: {}", name, e)))?;
2341 }
2342 Ok(())
2343 }
2344
2345 fn write_swmr_ndattr_element_attrs(
2356 &self,
2357 swmr: &mut SwmrFileWriter,
2358 ds_index: usize,
2359 ) -> ADResult<()> {
2360 use crate::hdf5_layout::LayoutWhen;
2361 let Some(layout) = self.layout.as_ref() else {
2362 return Ok(());
2363 };
2364 for e in layout.ndattribute_element_attrs() {
2365 if !matches!(e.when, LayoutWhen::OnFileOpen | LayoutWhen::OnFrame) {
2366 continue;
2367 }
2368 let Some(value) = self.ndattr_first_values.get(&e.ndattribute) else {
2369 continue;
2370 };
2371 if e.is_dataset {
2372 if e.element_path.trim_start_matches('/') == self.resolved_dataset_path {
2376 write_swmr_ndattr_dataset_attr(swmr, ds_index, &e.attr_name, value)?;
2377 }
2378 } else {
2379 write_swmr_ndattr_group_attr(swmr, &e.element_path, &e.attr_name, value)?;
2380 }
2381 }
2382 Ok(())
2383 }
2384
2385 fn flush_attribute_datasets(&mut self) -> ADResult<()> {
2388 if self.attr_datasets.is_empty() {
2389 return Ok(());
2390 }
2391 let chunk_depth = self.attribute_chunking();
2392 let ndattr_group = self.resolved_ndattr_group.clone();
2393 let targets: Vec<(String, String)> = self
2397 .attr_datasets
2398 .iter()
2399 .map(|ad| {
2400 match self
2401 .layout
2402 .as_ref()
2403 .and_then(|l| l.ndattribute_dataset(&ad.name))
2404 {
2405 Some((g, name)) => (g.trim_start_matches('/').to_string(), name),
2406 None => (ndattr_group.clone(), ad.name.clone()),
2407 }
2408 })
2409 .collect();
2410 let h5file = match self.handle {
2411 Some(Hdf5Handle::Standard { ref file, .. }) => file,
2412 _ => return Ok(()),
2413 };
2414 let mut group_cache: std::collections::HashMap<String, rust_hdf5::H5Group> =
2418 std::collections::HashMap::new();
2419
2420 for (ad, (group_path, ds_name)) in self.attr_datasets.iter().zip(targets.iter()) {
2421 if ad.frames == 0 {
2422 continue;
2423 }
2424 let n = ad.frames;
2425 let chunk = chunk_depth;
2429
2430 if !group_cache.contains_key(group_path) {
2431 let g = if group_path.is_empty() {
2432 h5file.create_group("NDAttributes").map_err(|e| {
2433 ADError::UnsupportedConversion(format!("HDF5 group error: {}", e))
2434 })?
2435 } else {
2436 Self::open_write_group(h5file, group_path)?
2437 };
2438 group_cache.insert(group_path.clone(), g);
2439 }
2440 let group = &group_cache[group_path];
2441
2442 macro_rules! create_attr_ds {
2443 ($t:ty) => {{
2444 let es = std::mem::size_of::<$t>();
2445 let ds = group
2446 .new_dataset::<$t>()
2447 .shape(&[n])
2448 .chunk(&[chunk])
2449 .max_shape(&[None])
2450 .create(ds_name)
2451 .map_err(|e| {
2452 ADError::UnsupportedConversion(format!(
2453 "HDF5 attribute dataset error: {}",
2454 e
2455 ))
2456 })?;
2457 write_chunked_buffer(&ds, &ad.buffer, chunk * es)?;
2461 ds
2462 }};
2463 }
2464
2465 let ds = match ad.data_type {
2466 NDAttrDataType::Int8 => create_attr_ds!(i8),
2467 NDAttrDataType::UInt8 => create_attr_ds!(u8),
2468 NDAttrDataType::Int16 => create_attr_ds!(i16),
2469 NDAttrDataType::UInt16 => create_attr_ds!(u16),
2470 NDAttrDataType::Int32 => create_attr_ds!(i32),
2471 NDAttrDataType::UInt32 => create_attr_ds!(u32),
2472 NDAttrDataType::Int64 => create_attr_ds!(i64),
2473 NDAttrDataType::UInt64 => create_attr_ds!(u64),
2474 NDAttrDataType::Float32 => create_attr_ds!(f32),
2475 NDAttrDataType::Float64 => create_attr_ds!(f64),
2476 NDAttrDataType::String => {
2477 let es = MAX_ATTRIBUTE_STRING_SIZE;
2484 let ds = group
2485 .new_dataset::<FixedStr256>()
2486 .shape([n])
2487 .chunk(&[chunk])
2488 .max_shape(&[None])
2489 .create(&ad.name)
2490 .map_err(|e| {
2491 ADError::UnsupportedConversion(format!(
2492 "HDF5 attribute dataset error: {}",
2493 e
2494 ))
2495 })?;
2496 write_chunked_buffer(&ds, &ad.buffer, chunk * es)?;
2497 ds
2498 }
2499 };
2500
2501 write_ndattr_descriptors(&ds, ad);
2505 }
2506 Ok(())
2507 }
2508
2509 fn flush_performance_dataset(&mut self) -> ADResult<()> {
2512 if !self.store_performance || self.perf_rows.is_empty() {
2513 return Ok(());
2514 }
2515 let n = self.perf_rows.len();
2516 let mut flat: Vec<f64> = Vec::with_capacity(n * 5);
2517 for row in &self.perf_rows {
2518 flat.extend_from_slice(row);
2519 }
2520 let raw: Vec<u8> = flat.iter().flat_map(|v| v.to_le_bytes()).collect();
2523
2524 let chunking = self.attribute_chunking();
2528 let perf_group = self.resolved_perf_group.clone();
2529 let h5file = match self.handle {
2530 Some(Hdf5Handle::Standard { ref file, .. }) => file,
2531 _ => return Ok(()),
2532 };
2533 let group = if perf_group.is_empty() {
2538 h5file
2539 .create_group("performance")
2540 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 group error: {}", e)))?
2541 } else {
2542 Self::open_write_group(h5file, &perf_group)?
2543 };
2544 let ds = group
2545 .new_dataset::<f64>()
2546 .shape([n, 5])
2547 .chunk(&[chunking, 5])
2548 .max_shape(&[None, Some(5)])
2549 .create("timestamp")
2550 .map_err(|e| {
2551 ADError::UnsupportedConversion(format!("HDF5 performance dataset error: {}", e))
2552 })?;
2553 write_chunked_buffer(&ds, &raw, chunking * 5 * 8)?;
2556 Ok(())
2557 }
2558
2559 fn write_swmr(&mut self, array: &NDArray) -> ADResult<()> {
2561 if array.codec.is_some() {
2570 return Err(ADError::UnsupportedConversion(
2571 "HDF5 SWMR mode cannot write a pre-compressed array (no raw-chunk \
2572 append in the SWMR backend); use standard capture mode for \
2573 direct chunk write"
2574 .into(),
2575 ));
2576 }
2577
2578 let frame_idx = self.frame_count;
2581
2582 let (writer, ds_index, grid) = match self.handle {
2583 Some(Hdf5Handle::Swmr {
2584 ref mut writer,
2585 ds_index,
2586 ref grid,
2587 ..
2588 }) => (writer, ds_index, grid),
2589 _ => return Err(ADError::UnsupportedConversion("no SWMR writer open".into())),
2590 };
2591
2592 let frame_bytes = nd_buffer_to_le_bytes(&array.data);
2597 match grid {
2598 Some(g) => {
2602 let leading_coords = Self::unravel(frame_idx, &g.leading);
2603 for (coords, tile) in Self::band_chunk_writes(
2604 &leading_coords,
2605 std::slice::from_ref(&frame_bytes),
2606 &g.frame_dims,
2607 &g.chunk,
2608 g.elem_size,
2609 ) {
2610 let coords_u64: Vec<u64> = coords.iter().map(|&c| c as u64).collect();
2611 writer
2612 .write_chunk_at(ds_index, &coords_u64, &tile)
2613 .map_err(|e| {
2614 ADError::UnsupportedConversion(format!(
2615 "SWMR write_chunk_at error: {}",
2616 e
2617 ))
2618 })?;
2619 }
2620 }
2621 None => {
2622 writer.append_frame(ds_index, &frame_bytes).map_err(|e| {
2623 ADError::UnsupportedConversion(format!("SWMR append error: {}", e))
2624 })?;
2625 }
2626 }
2627
2628 let count = self.frame_count + 1; if self.flush_nth_frame > 0 && count % self.flush_nth_frame == 0 {
2631 writer
2632 .flush()
2633 .map_err(|e| ADError::UnsupportedConversion(format!("SWMR flush error: {}", e)))?;
2634 }
2635 Ok(())
2636 }
2637
2638 fn record_performance(&mut self, write_duration: f64, frame_bytes: usize) {
2640 let now = std::time::Instant::now();
2641 let first = *self.perf_first.get_or_insert(now);
2642 let runtime = now.duration_since(first).as_secs_f64();
2643 let period = match self.perf_prev {
2644 Some(prev) => now.duration_since(prev).as_secs_f64(),
2645 None => write_duration,
2646 };
2647 self.perf_prev = Some(now);
2648 let fb = frame_bytes as f64;
2649 let inst_speed = if period > 0.0 { fb / period } else { 0.0 };
2650 let avg_speed = if runtime > 0.0 {
2651 (self.perf_rows.len() as f64 + 1.0) * fb / runtime
2652 } else {
2653 0.0
2654 };
2655 self.perf_rows
2656 .push([write_duration, period, runtime, inst_speed, avg_speed]);
2657 }
2658}
2659
2660fn codecs_match(a: Option<&Codec>, b: Option<&Codec>) -> bool {
2667 match (a, b) {
2668 (None, None) => true,
2669 (Some(x), Some(y)) => {
2670 x.name == y.name
2671 && x.level == y.level
2672 && x.shuffle == y.shuffle
2673 && x.compressor == y.compressor
2674 }
2675 _ => false,
2676 }
2677}
2678
2679fn codec_chunk_bytes(codec: &Codec, uncompressed_total_bytes: usize, payload: &[u8]) -> Vec<u8> {
2701 match codec.name {
2702 CodecName::LZ4 => {
2703 let mut out = Vec::with_capacity(16 + payload.len());
2704 out.extend_from_slice(&(uncompressed_total_bytes as u64).to_be_bytes());
2705 out.extend_from_slice(&(uncompressed_total_bytes as u32).to_be_bytes());
2706 out.extend_from_slice(&(payload.len() as u32).to_be_bytes());
2707 out.extend_from_slice(payload);
2708 out
2709 }
2710 CodecName::BSLZ4 => {
2711 let elem_size = codec.original_data_type.element_size();
2712 let block_bytes = crate::codec::bshuf_default_block_size(elem_size) * elem_size;
2713 let mut out = Vec::with_capacity(12 + payload.len());
2714 out.extend_from_slice(&(uncompressed_total_bytes as u64).to_be_bytes());
2715 out.extend_from_slice(&(block_bytes as u32).to_be_bytes());
2716 out.extend_from_slice(payload);
2717 out
2718 }
2719 _ => payload.to_vec(),
2720 }
2721}
2722
2723fn nd_buffer_to_le_bytes(buf: &NDDataBuffer) -> Vec<u8> {
2735 match buf {
2736 NDDataBuffer::I8(v) => v.iter().map(|&x| x as u8).collect(),
2737 NDDataBuffer::U8(v) => v.clone(),
2738 NDDataBuffer::I16(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2739 NDDataBuffer::U16(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2740 NDDataBuffer::I32(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2741 NDDataBuffer::U32(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2742 NDDataBuffer::I64(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2743 NDDataBuffer::U64(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2744 NDDataBuffer::F32(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2745 NDDataBuffer::F64(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2746 }
2747}
2748
2749fn write_chunked_buffer(
2753 ds: &rust_hdf5::H5Dataset,
2754 buffer: &[u8],
2755 chunk_bytes: usize,
2756) -> ADResult<()> {
2757 let n_chunks = buffer.len().div_ceil(chunk_bytes.max(1));
2758 for c in 0..n_chunks {
2759 let start = c * chunk_bytes;
2760 let end = ((c + 1) * chunk_bytes).min(buffer.len());
2761 let slice = &buffer[start..end];
2762 if slice.len() == chunk_bytes {
2763 ds.write_chunk(c, slice)
2764 } else {
2765 let mut padded = vec![0u8; chunk_bytes];
2766 padded[..slice.len()].copy_from_slice(slice);
2767 ds.write_chunk(c, &padded)
2768 }
2769 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 chunk write error: {}", e)))?;
2770 }
2771 Ok(())
2772}
2773
2774fn write_ndattr_descriptors(ds: &rust_hdf5::H5Dataset, ad: &AttributeDataset) {
2780 for (aname, avalue) in [
2781 ("NDAttrName", ad.name.as_str()),
2782 ("NDAttrDescription", ad.description.as_str()),
2783 ("NDAttrSourceType", ad.source_type.as_str()),
2784 ("NDAttrSource", ad.source.as_str()),
2785 ] {
2786 if avalue.is_empty() {
2787 continue;
2788 }
2789 let s = rust_hdf5::types::VarLenUnicode(avalue.to_string());
2790 let _ = ds
2791 .new_attr::<rust_hdf5::types::VarLenUnicode>()
2792 .shape(())
2793 .create(aname)
2794 .and_then(|a| a.write_scalar(&s));
2795 }
2796}
2797
2798fn write_ndattr_group_attr(group: &rust_hdf5::H5Group, attr_name: &str, value: &NDAttrValue) {
2804 macro_rules! num {
2805 ($v:expr) => {{
2806 let _ = group.set_attr_numeric(attr_name, &$v);
2807 }};
2808 }
2809 match value {
2810 NDAttrValue::Int8(v) => num!(*v),
2811 NDAttrValue::UInt8(v) => num!(*v),
2812 NDAttrValue::Int16(v) => num!(*v),
2813 NDAttrValue::UInt16(v) => num!(*v),
2814 NDAttrValue::Int32(v) => num!(*v),
2815 NDAttrValue::UInt32(v) => num!(*v),
2816 NDAttrValue::Int64(v) => num!(*v),
2817 NDAttrValue::UInt64(v) => num!(*v),
2818 NDAttrValue::Float32(v) => num!(*v),
2819 NDAttrValue::Float64(v) => num!(*v),
2820 NDAttrValue::String(s) => {
2821 let _ = group.set_attr_string(attr_name, s);
2822 }
2823 NDAttrValue::Undefined => {}
2824 }
2825}
2826
2827fn write_ndattr_dataset_attr(ds: &rust_hdf5::H5Dataset, attr_name: &str, value: &NDAttrValue) {
2834 macro_rules! num {
2835 ($v:expr, $t:ty) => {{
2836 let v: $t = $v;
2837 let _ = ds
2838 .new_attr::<$t>()
2839 .shape(())
2840 .create(attr_name)
2841 .and_then(|a| a.write_numeric(&v));
2842 }};
2843 }
2844 match value {
2845 NDAttrValue::Int8(v) => num!(*v, i8),
2846 NDAttrValue::UInt8(v) => num!(*v, u8),
2847 NDAttrValue::Int16(v) => num!(*v, i16),
2848 NDAttrValue::UInt16(v) => num!(*v, u16),
2849 NDAttrValue::Int32(v) => num!(*v, i32),
2850 NDAttrValue::UInt32(v) => num!(*v, u32),
2851 NDAttrValue::Int64(v) => num!(*v, i64),
2852 NDAttrValue::UInt64(v) => num!(*v, u64),
2853 NDAttrValue::Float32(v) => num!(*v, f32),
2854 NDAttrValue::Float64(v) => num!(*v, f64),
2855 NDAttrValue::String(s) => {
2856 let sv = rust_hdf5::types::VarLenUnicode(s.clone());
2857 let _ = ds
2858 .new_attr::<rust_hdf5::types::VarLenUnicode>()
2859 .shape(())
2860 .create(attr_name)
2861 .and_then(|a| a.write_scalar(&sv));
2862 }
2863 NDAttrValue::Undefined => {}
2864 }
2865}
2866
2867fn write_swmr_ndattr_group_attr(
2872 swmr: &mut SwmrFileWriter,
2873 group_path: &str,
2874 attr_name: &str,
2875 value: &NDAttrValue,
2876) -> ADResult<()> {
2877 let res = match value {
2878 NDAttrValue::Int8(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2879 NDAttrValue::UInt8(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2880 NDAttrValue::Int16(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2881 NDAttrValue::UInt16(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2882 NDAttrValue::Int32(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2883 NDAttrValue::UInt32(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2884 NDAttrValue::Int64(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2885 NDAttrValue::UInt64(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2886 NDAttrValue::Float32(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2887 NDAttrValue::Float64(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2888 NDAttrValue::String(s) => swmr.set_group_attr_string(group_path, attr_name, s),
2889 NDAttrValue::Undefined => return Ok(()),
2890 };
2891 res.map_err(|e| {
2892 ADError::UnsupportedConversion(format!(
2893 "SWMR ndattribute group attribute '{}/{}': {}",
2894 group_path, attr_name, e
2895 ))
2896 })
2897}
2898
2899fn write_swmr_ndattr_dataset_attr(
2906 swmr: &mut SwmrFileWriter,
2907 ds_index: usize,
2908 attr_name: &str,
2909 value: &NDAttrValue,
2910) -> ADResult<()> {
2911 let res = match value {
2912 NDAttrValue::Int8(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2913 NDAttrValue::UInt8(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2914 NDAttrValue::Int16(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2915 NDAttrValue::UInt16(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2916 NDAttrValue::Int32(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2917 NDAttrValue::UInt32(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2918 NDAttrValue::Int64(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2919 NDAttrValue::UInt64(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2920 NDAttrValue::Float32(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2921 NDAttrValue::Float64(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2922 NDAttrValue::String(s) => swmr.set_dataset_attr_string(ds_index, attr_name, s),
2923 NDAttrValue::Undefined => return Ok(()),
2924 };
2925 res.map_err(|e| {
2926 ADError::UnsupportedConversion(format!(
2927 "SWMR ndattribute dataset attribute '{}': {}",
2928 attr_name, e
2929 ))
2930 })
2931}
2932
2933fn write_group_constant_attrs(
2939 group: &rust_hdf5::H5Group,
2940 attrs: &[crate::hdf5_layout::LayoutAttribute],
2941) {
2942 use crate::hdf5_layout::{LayoutDataType, LayoutSource};
2943 for a in attrs {
2944 if a.source != LayoutSource::Constant {
2945 continue;
2946 }
2947 let _ = match a.data_type {
2948 LayoutDataType::Int => {
2949 group.set_attr_numeric(&a.name, &a.value.trim().parse::<i64>().unwrap_or(0))
2950 }
2951 LayoutDataType::Float => {
2952 group.set_attr_numeric(&a.name, &a.value.trim().parse::<f64>().unwrap_or(0.0))
2953 }
2954 LayoutDataType::String => group.set_attr_string(&a.name, &a.value),
2955 };
2956 }
2957}
2958
2959fn write_swmr_group_constant_attrs(
2963 swmr: &mut SwmrFileWriter,
2964 group_path: &str,
2965 attrs: &[crate::hdf5_layout::LayoutAttribute],
2966) -> ADResult<()> {
2967 use crate::hdf5_layout::{LayoutDataType, LayoutSource};
2968 for a in attrs {
2969 if a.source != LayoutSource::Constant {
2970 continue;
2971 }
2972 match a.data_type {
2973 LayoutDataType::Int => swmr.set_group_attr_numeric(
2974 group_path,
2975 &a.name,
2976 &a.value.trim().parse::<i64>().unwrap_or(0),
2977 ),
2978 LayoutDataType::Float => swmr.set_group_attr_numeric(
2979 group_path,
2980 &a.name,
2981 &a.value.trim().parse::<f64>().unwrap_or(0.0),
2982 ),
2983 LayoutDataType::String => swmr.set_group_attr_string(group_path, &a.name, &a.value),
2984 }
2985 .map_err(|e| {
2986 ADError::UnsupportedConversion(format!(
2987 "SWMR layout group attribute '{}/{}': {}",
2988 group_path, a.name, e
2989 ))
2990 })?;
2991 }
2992 Ok(())
2993}
2994
2995impl Default for Hdf5Writer {
2996 fn default() -> Self {
2997 Self::new()
2998 }
2999}
3000
3001impl NDFileWriter for Hdf5Writer {
3002 fn open_file(&mut self, path: &Path, mode: NDFileMode, array: &NDArray) -> ADResult<()> {
3003 self.current_path = Some(path.to_path_buf());
3004 self.open_mode = mode;
3005 self.frame_count = 0;
3006 self.total_runtime = 0.0;
3007 self.total_bytes = 0;
3008 self.swmr_cb_counter = 0;
3009 self.open_data_type = None;
3010 self.open_frame_dims = None;
3011 self.open_codec = None;
3012 self.perf_rows.clear();
3013 self.perf_prev = None;
3014 self.perf_first = None;
3015 self.attr_datasets.clear();
3016 self.resolve_layout_paths();
3019 self.seed_ndattr_element_values(array);
3022
3023 if self.swmr_mode && mode == NDFileMode::Stream {
3024 self.open_swmr(path, array)
3025 } else {
3026 let h5file = H5File::create(path)
3027 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 create error: {}", e)))?;
3028 self.handle = Some(Hdf5Handle::Standard {
3029 file: h5file,
3030 detectors: std::collections::HashMap::new(),
3031 });
3032 Ok(())
3033 }
3034 }
3035
3036 fn set_num_capture(&mut self, n: usize) {
3037 self.num_capture = n;
3038 }
3039
3040 fn write_file(&mut self, array: &NDArray) -> ADResult<()> {
3041 let start = std::time::Instant::now();
3042
3043 let is_swmr = matches!(self.handle, Some(Hdf5Handle::Swmr { .. }));
3044 if is_swmr {
3045 self.write_swmr(array)?;
3046 } else {
3047 self.write_standard(array)?;
3048 }
3049 self.update_ndattr_element_values(array);
3052 self.frame_count += 1;
3053
3054 let elapsed = start.elapsed().as_secs_f64();
3055 let frame_bytes = array.data.as_u8_slice().len();
3056 if self.store_performance {
3057 self.total_runtime += elapsed;
3058 self.total_bytes += frame_bytes as u64;
3059 self.record_performance(elapsed, frame_bytes);
3060 }
3061 Ok(())
3062 }
3063
3064 fn read_file(&mut self) -> ADResult<NDArray> {
3065 self.resolve_layout_paths();
3069 let dataset_path = self.resolved_dataset_path.clone();
3070 let path = self
3071 .current_path
3072 .as_ref()
3073 .ok_or_else(|| ADError::UnsupportedConversion("no file open".into()))?;
3074
3075 let h5file = H5File::open(path)
3076 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 open error: {}", e)))?;
3077
3078 let ds = h5file
3079 .dataset(&dataset_path)
3080 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 dataset error: {}", e)))?;
3081
3082 let shape = ds.shape();
3083 let dims: Vec<NDDimension> = shape.iter().rev().map(|&s| NDDimension::new(s)).collect();
3084 let element_size = ds.element_size();
3085
3086 let recorded: Option<NDDataType> = ds
3088 .attr(DTYPE_ATTR)
3089 .ok()
3090 .and_then(|a| a.read_numeric::<i32>().ok())
3091 .and_then(|v| NDDataType::from_ordinal(v as u8));
3092
3093 let data_type = recorded.unwrap_or(match element_size {
3094 1 => NDDataType::UInt8,
3095 2 => NDDataType::UInt16,
3096 4 => NDDataType::Float32,
3097 8 => NDDataType::Float64,
3098 other => {
3099 return Err(ADError::UnsupportedConversion(format!(
3100 "unsupported HDF5 element size {}",
3101 other
3102 )));
3103 }
3104 });
3105
3106 macro_rules! read_typed {
3107 ($t:ty, $variant:ident) => {{
3108 let data = ds.read_raw::<$t>().map_err(|e| {
3109 ADError::UnsupportedConversion(format!("HDF5 read error: {}", e))
3110 })?;
3111 let mut arr = NDArray::new(dims, data_type);
3112 arr.data = NDDataBuffer::$variant(data);
3113 return Ok(arr);
3114 }};
3115 }
3116
3117 match data_type {
3118 NDDataType::Int8 => read_typed!(i8, I8),
3119 NDDataType::UInt8 => read_typed!(u8, U8),
3120 NDDataType::Int16 => read_typed!(i16, I16),
3121 NDDataType::UInt16 => read_typed!(u16, U16),
3122 NDDataType::Int32 => read_typed!(i32, I32),
3123 NDDataType::UInt32 => read_typed!(u32, U32),
3124 NDDataType::Int64 => read_typed!(i64, I64),
3125 NDDataType::UInt64 => read_typed!(u64, U64),
3126 NDDataType::Float32 => read_typed!(f32, F32),
3127 NDDataType::Float64 => read_typed!(f64, F64),
3128 }
3129 }
3130
3131 fn close_file(&mut self) -> ADResult<()> {
3132 match self.handle {
3133 Some(Hdf5Handle::Standard { .. }) => {
3134 self.finalize_standard_datasets()?;
3138 self.flush_attribute_datasets()?;
3139 self.flush_performance_dataset()?;
3140 match self.handle {
3143 Some(Hdf5Handle::Standard {
3144 ref file,
3145 ref detectors,
3146 }) => {
3147 self.build_layout_hardlinks(file)?;
3148 self.flush_ndattr_element_attrs(file, detectors)?;
3152 }
3153 _ => unreachable!("handle is Standard in this arm"),
3154 }
3155 if let Some(Hdf5Handle::Standard { file, detectors }) = self.handle.take() {
3161 if self.fsync_on_close {
3162 drop(file);
3163 } else {
3164 file.close_no_sync().map_err(|e| {
3165 ADError::UnsupportedConversion(format!(
3166 "HDF5 close_no_sync error: {}",
3167 e
3168 ))
3169 })?;
3170 }
3171 drop(detectors);
3172 }
3173 }
3174 Some(Hdf5Handle::Swmr { .. }) => {
3175 if let Some(Hdf5Handle::Swmr { mut writer, .. }) = self.handle.take() {
3182 writer.flush().map_err(|e| {
3189 ADError::UnsupportedConversion(format!("SWMR flush-on-close error: {}", e))
3190 })?;
3191 writer.close().map_err(|e| {
3192 ADError::UnsupportedConversion(format!("SWMR close error: {}", e))
3193 })?;
3194 }
3195 }
3196 None => {}
3197 }
3198 self.current_path = None;
3199 Ok(())
3200 }
3201
3202 fn supports_multiple_arrays(&self) -> bool {
3203 true
3204 }
3205}
3206
3207#[derive(Default)]
3213struct Hdf5ParamIndices {
3214 compression_type: Option<usize>,
3215 z_compress_level: Option<usize>,
3216 szip_num_pixels: Option<usize>,
3217 nbit_precision: Option<usize>,
3218 nbit_offset: Option<usize>,
3219 jpeg_quality: Option<usize>,
3220 blosc_shuffle_type: Option<usize>,
3221 blosc_compressor: Option<usize>,
3222 blosc_compress_level: Option<usize>,
3223 store_attributes: Option<usize>,
3224 store_performance: Option<usize>,
3225 total_runtime: Option<usize>,
3226 total_io_speed: Option<usize>,
3227 swmr_mode: Option<usize>,
3228 swmr_flush_now: Option<usize>,
3229 swmr_running: Option<usize>,
3230 swmr_cb_counter: Option<usize>,
3231 swmr_supported: Option<usize>,
3232 flush_nth_frame: Option<usize>,
3233 chunk_size_auto: Option<usize>,
3234 n_row_chunks: Option<usize>,
3235 n_col_chunks: Option<usize>,
3236 n_frames_chunks: Option<usize>,
3237 ndattr_chunk: Option<usize>,
3238 n_extra_dims: Option<usize>,
3239 extra_dim_size: [Option<usize>; MAX_EXTRA_DIMS],
3240 extra_dim_name: [Option<usize>; MAX_EXTRA_DIMS],
3241 fill_value: Option<usize>,
3242 dim_att_datasets: Option<usize>,
3243 layout_filename: Option<usize>,
3244 layout_valid: Option<usize>,
3245 layout_error_msg: Option<usize>,
3246}
3247
3248pub struct Hdf5FileProcessor {
3250 ctrl: FilePluginController<Hdf5Writer>,
3251 hdf5_params: Hdf5ParamIndices,
3252}
3253
3254impl Hdf5FileProcessor {
3255 pub fn new() -> Self {
3256 Self {
3257 ctrl: FilePluginController::new(Hdf5Writer::new()),
3258 hdf5_params: Hdf5ParamIndices::default(),
3259 }
3260 }
3261
3262 pub fn set_dataset_name(&mut self, name: &str) {
3263 self.ctrl.writer.set_dataset_name(name);
3264 }
3265}
3266
3267fn register_hdf5_params(
3269 base: &mut asyn_rs::port::PortDriverBase,
3270) -> asyn_rs::error::AsynResult<()> {
3271 use asyn_rs::param::ParamType;
3272 base.create_param("HDF5_SWMRFlushNow", ParamType::Int32)?;
3273 base.create_param("HDF5_chunkSizeAuto", ParamType::Int32)?;
3274 base.create_param("HDF5_nRowChunks", ParamType::Int32)?;
3275 base.create_param("HDF5_nColChunks", ParamType::Int32)?;
3276 base.create_param("HDF5_chunkSize2", ParamType::Int32)?;
3277 base.create_param("HDF5_chunkSize3", ParamType::Int32)?;
3278 base.create_param("HDF5_chunkSize4", ParamType::Int32)?;
3279 base.create_param("HDF5_chunkSize5", ParamType::Int32)?;
3280 base.create_param("HDF5_chunkSize6", ParamType::Int32)?;
3281 base.create_param("HDF5_chunkSize7", ParamType::Int32)?;
3282 base.create_param("HDF5_chunkSize8", ParamType::Int32)?;
3283 base.create_param("HDF5_chunkSize9", ParamType::Int32)?;
3284 base.create_param("HDF5_nFramesChunks", ParamType::Int32)?;
3285 base.create_param("HDF5_NDAttributeChunk", ParamType::Int32)?;
3286 base.create_param("HDF5_chunkBoundaryAlign", ParamType::Int32)?;
3287 base.create_param("HDF5_chunkBoundaryThreshold", ParamType::Int32)?;
3288 base.create_param("HDF5_nExtraDims", ParamType::Int32)?;
3289 base.create_param("HDF5_extraDimSizeN", ParamType::Int32)?;
3290 base.create_param("HDF5_extraDimNameN", ParamType::Octet)?;
3291 base.create_param("HDF5_extraDimSizeX", ParamType::Int32)?;
3292 base.create_param("HDF5_extraDimNameX", ParamType::Octet)?;
3293 base.create_param("HDF5_extraDimSizeY", ParamType::Int32)?;
3294 base.create_param("HDF5_extraDimNameY", ParamType::Octet)?;
3295 base.create_param("HDF5_extraDimSize3", ParamType::Int32)?;
3296 base.create_param("HDF5_extraDimName3", ParamType::Octet)?;
3297 base.create_param("HDF5_extraDimSize4", ParamType::Int32)?;
3298 base.create_param("HDF5_extraDimName4", ParamType::Octet)?;
3299 base.create_param("HDF5_extraDimSize5", ParamType::Int32)?;
3300 base.create_param("HDF5_extraDimName5", ParamType::Octet)?;
3301 base.create_param("HDF5_extraDimSize6", ParamType::Int32)?;
3302 base.create_param("HDF5_extraDimName6", ParamType::Octet)?;
3303 base.create_param("HDF5_extraDimSize7", ParamType::Int32)?;
3304 base.create_param("HDF5_extraDimName7", ParamType::Octet)?;
3305 base.create_param("HDF5_extraDimSize8", ParamType::Int32)?;
3306 base.create_param("HDF5_extraDimName8", ParamType::Octet)?;
3307 base.create_param("HDF5_extraDimSize9", ParamType::Int32)?;
3308 base.create_param("HDF5_extraDimName9", ParamType::Octet)?;
3309 base.create_param("HDF5_storeAttributes", ParamType::Int32)?;
3310 base.create_param("HDF5_storePerformance", ParamType::Int32)?;
3311 base.create_param("HDF5_totalRuntime", ParamType::Float64)?;
3312 base.create_param("HDF5_totalIoSpeed", ParamType::Float64)?;
3313 base.create_param("HDF5_flushNthFrame", ParamType::Int32)?;
3314 base.create_param("HDF5_compressionType", ParamType::Int32)?;
3315 base.create_param("HDF5_nbitsPrecision", ParamType::Int32)?;
3316 base.create_param("HDF5_nbitsOffset", ParamType::Int32)?;
3317 base.create_param("HDF5_szipNumPixels", ParamType::Int32)?;
3318 base.create_param("HDF5_zCompressLevel", ParamType::Int32)?;
3319 base.create_param("HDF5_bloscShuffleType", ParamType::Int32)?;
3320 base.create_param("HDF5_bloscCompressor", ParamType::Int32)?;
3321 base.create_param("HDF5_bloscCompressLevel", ParamType::Int32)?;
3322 base.create_param("HDF5_jpegQuality", ParamType::Int32)?;
3323 base.create_param("HDF5_dimAttDatasets", ParamType::Int32)?;
3324 base.create_param("HDF5_layoutErrorMsg", ParamType::Octet)?;
3325 base.create_param("HDF5_layoutValid", ParamType::Int32)?;
3326 base.create_param("HDF5_layoutFilename", ParamType::Octet)?;
3327 base.create_param("HDF5_SWMRSupported", ParamType::Int32)?;
3328 base.create_param("HDF5_SWMRMode", ParamType::Int32)?;
3329 base.create_param("HDF5_SWMRRunning", ParamType::Int32)?;
3330 base.create_param("HDF5_SWMRCbCounter", ParamType::Int32)?;
3331 base.create_param("HDF5_posRunning", ParamType::Int32)?;
3332 base.create_param("HDF5_posNameDimN", ParamType::Octet)?;
3333 base.create_param("HDF5_posNameDimX", ParamType::Octet)?;
3334 base.create_param("HDF5_posNameDimY", ParamType::Octet)?;
3335 base.create_param("HDF5_posNameDim3", ParamType::Octet)?;
3336 base.create_param("HDF5_posNameDim4", ParamType::Octet)?;
3337 base.create_param("HDF5_posNameDim5", ParamType::Octet)?;
3338 base.create_param("HDF5_posNameDim6", ParamType::Octet)?;
3339 base.create_param("HDF5_posNameDim7", ParamType::Octet)?;
3340 base.create_param("HDF5_posNameDim8", ParamType::Octet)?;
3341 base.create_param("HDF5_posNameDim9", ParamType::Octet)?;
3342 base.create_param("HDF5_posIndexDimN", ParamType::Octet)?;
3343 base.create_param("HDF5_posIndexDimX", ParamType::Octet)?;
3344 base.create_param("HDF5_posIndexDimY", ParamType::Octet)?;
3345 base.create_param("HDF5_posIndexDim3", ParamType::Octet)?;
3346 base.create_param("HDF5_posIndexDim4", ParamType::Octet)?;
3347 base.create_param("HDF5_posIndexDim5", ParamType::Octet)?;
3348 base.create_param("HDF5_posIndexDim6", ParamType::Octet)?;
3349 base.create_param("HDF5_posIndexDim7", ParamType::Octet)?;
3350 base.create_param("HDF5_posIndexDim8", ParamType::Octet)?;
3351 base.create_param("HDF5_posIndexDim9", ParamType::Octet)?;
3352 base.create_param("HDF5_fillValue", ParamType::Float64)?;
3353 base.create_param("HDF5_extraDimChunkX", ParamType::Int32)?;
3354 base.create_param("HDF5_extraDimChunkY", ParamType::Int32)?;
3355 base.create_param("HDF5_extraDimChunk3", ParamType::Int32)?;
3356 base.create_param("HDF5_extraDimChunk4", ParamType::Int32)?;
3357 base.create_param("HDF5_extraDimChunk5", ParamType::Int32)?;
3358 base.create_param("HDF5_extraDimChunk6", ParamType::Int32)?;
3359 base.create_param("HDF5_extraDimChunk7", ParamType::Int32)?;
3360 base.create_param("HDF5_extraDimChunk8", ParamType::Int32)?;
3361 base.create_param("HDF5_extraDimChunk9", ParamType::Int32)?;
3362 Ok(())
3363}
3364
3365impl Default for Hdf5FileProcessor {
3366 fn default() -> Self {
3367 Self::new()
3368 }
3369}
3370
3371const EXTRA_DIM_SIZE_PARAMS: [&str; MAX_EXTRA_DIMS] = [
3373 "HDF5_extraDimSizeN",
3374 "HDF5_extraDimSizeX",
3375 "HDF5_extraDimSizeY",
3376 "HDF5_extraDimSize3",
3377 "HDF5_extraDimSize4",
3378 "HDF5_extraDimSize5",
3379 "HDF5_extraDimSize6",
3380 "HDF5_extraDimSize7",
3381 "HDF5_extraDimSize8",
3382 "HDF5_extraDimSize9",
3383];
3384
3385const EXTRA_DIM_NAME_PARAMS: [&str; MAX_EXTRA_DIMS] = [
3387 "HDF5_extraDimNameN",
3388 "HDF5_extraDimNameX",
3389 "HDF5_extraDimNameY",
3390 "HDF5_extraDimName3",
3391 "HDF5_extraDimName4",
3392 "HDF5_extraDimName5",
3393 "HDF5_extraDimName6",
3394 "HDF5_extraDimName7",
3395 "HDF5_extraDimName8",
3396 "HDF5_extraDimName9",
3397];
3398
3399impl NDPluginProcess for Hdf5FileProcessor {
3400 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
3401 let was_swmr = self.ctrl.writer.is_swmr_active();
3402 let mut result = self.ctrl.process_array(array);
3403 let is_swmr = self.ctrl.writer.is_swmr_active();
3404
3405 if was_swmr != is_swmr {
3407 if let Some(idx) = self.hdf5_params.swmr_running {
3408 result
3409 .param_updates
3410 .push(ParamUpdate::int32(idx, if is_swmr { 1 } else { 0 }));
3411 }
3412 }
3413
3414 if is_swmr {
3416 if let Some(idx) = self.hdf5_params.swmr_cb_counter {
3417 result.param_updates.push(ParamUpdate::int32(
3418 idx,
3419 self.ctrl.writer.swmr_cb_counter as i32,
3420 ));
3421 }
3422 }
3423
3424 if self.ctrl.writer.store_performance {
3426 if let Some(idx) = self.hdf5_params.total_runtime {
3427 result
3428 .param_updates
3429 .push(ParamUpdate::float64(idx, self.ctrl.writer.total_runtime));
3430 }
3431 if let Some(idx) = self.hdf5_params.total_io_speed {
3432 let speed = if self.ctrl.writer.total_runtime > 0.0 {
3433 self.ctrl.writer.total_bytes as f64
3434 / self.ctrl.writer.total_runtime
3435 / 1_000_000.0
3436 } else {
3437 0.0
3438 };
3439 result.param_updates.push(ParamUpdate::float64(idx, speed));
3440 }
3441 }
3442
3443 result
3444 }
3445
3446 fn plugin_type(&self) -> &str {
3447 "NDFileHDF5"
3448 }
3449
3450 fn compression_aware(&self) -> bool {
3456 true
3457 }
3458
3459 fn does_array_callbacks(&self) -> bool {
3462 false
3463 }
3464
3465 fn register_params(
3466 &mut self,
3467 base: &mut asyn_rs::port::PortDriverBase,
3468 ) -> asyn_rs::error::AsynResult<()> {
3469 self.ctrl.register_params(base)?;
3470 register_hdf5_params(base)?;
3471 self.hdf5_params.compression_type = base.find_param("HDF5_compressionType");
3472 self.hdf5_params.z_compress_level = base.find_param("HDF5_zCompressLevel");
3473 self.hdf5_params.szip_num_pixels = base.find_param("HDF5_szipNumPixels");
3474 self.hdf5_params.nbit_precision = base.find_param("HDF5_nbitsPrecision");
3475 self.hdf5_params.nbit_offset = base.find_param("HDF5_nbitsOffset");
3476 self.hdf5_params.jpeg_quality = base.find_param("HDF5_jpegQuality");
3477 self.hdf5_params.blosc_shuffle_type = base.find_param("HDF5_bloscShuffleType");
3478 self.hdf5_params.blosc_compressor = base.find_param("HDF5_bloscCompressor");
3479 self.hdf5_params.blosc_compress_level = base.find_param("HDF5_bloscCompressLevel");
3480 self.hdf5_params.store_attributes = base.find_param("HDF5_storeAttributes");
3481 self.hdf5_params.store_performance = base.find_param("HDF5_storePerformance");
3482 self.hdf5_params.total_runtime = base.find_param("HDF5_totalRuntime");
3483 self.hdf5_params.total_io_speed = base.find_param("HDF5_totalIoSpeed");
3484 self.hdf5_params.swmr_mode = base.find_param("HDF5_SWMRMode");
3485 self.hdf5_params.swmr_flush_now = base.find_param("HDF5_SWMRFlushNow");
3486 self.hdf5_params.swmr_running = base.find_param("HDF5_SWMRRunning");
3487 self.hdf5_params.swmr_cb_counter = base.find_param("HDF5_SWMRCbCounter");
3488 self.hdf5_params.swmr_supported = base.find_param("HDF5_SWMRSupported");
3489 self.hdf5_params.flush_nth_frame = base.find_param("HDF5_flushNthFrame");
3490 self.hdf5_params.chunk_size_auto = base.find_param("HDF5_chunkSizeAuto");
3491 self.hdf5_params.n_row_chunks = base.find_param("HDF5_nRowChunks");
3492 self.hdf5_params.n_col_chunks = base.find_param("HDF5_nColChunks");
3493 self.hdf5_params.n_frames_chunks = base.find_param("HDF5_nFramesChunks");
3494 self.hdf5_params.ndattr_chunk = base.find_param("HDF5_NDAttributeChunk");
3495 self.hdf5_params.n_extra_dims = base.find_param("HDF5_nExtraDims");
3496 for i in 0..MAX_EXTRA_DIMS {
3497 self.hdf5_params.extra_dim_size[i] = base.find_param(EXTRA_DIM_SIZE_PARAMS[i]);
3498 self.hdf5_params.extra_dim_name[i] = base.find_param(EXTRA_DIM_NAME_PARAMS[i]);
3499 }
3500 self.hdf5_params.fill_value = base.find_param("HDF5_fillValue");
3501 self.hdf5_params.dim_att_datasets = base.find_param("HDF5_dimAttDatasets");
3502 self.hdf5_params.layout_filename = base.find_param("HDF5_layoutFilename");
3503 self.hdf5_params.layout_valid = base.find_param("HDF5_layoutValid");
3504 self.hdf5_params.layout_error_msg = base.find_param("HDF5_layoutErrorMsg");
3505
3506 if let Some(idx) = self.hdf5_params.swmr_supported {
3508 base.set_int32_param(idx, 0, 1)?;
3509 }
3510 Ok(())
3511 }
3512
3513 fn on_param_change(
3514 &mut self,
3515 reason: usize,
3516 params: &PluginParamSnapshot,
3517 ) -> ParamChangeResult {
3518 if Some(reason) == self.hdf5_params.compression_type {
3520 self.ctrl.writer.set_compression_type(params.value.as_i32());
3521 return ParamChangeResult::updates(vec![]);
3522 }
3523 if Some(reason) == self.hdf5_params.z_compress_level {
3524 self.ctrl
3525 .writer
3526 .set_z_compress_level(params.value.as_i32() as u32);
3527 return ParamChangeResult::updates(vec![]);
3528 }
3529 if Some(reason) == self.hdf5_params.szip_num_pixels {
3530 self.ctrl
3531 .writer
3532 .set_szip_num_pixels(params.value.as_i32() as u32);
3533 return ParamChangeResult::updates(vec![]);
3534 }
3535 if Some(reason) == self.hdf5_params.blosc_shuffle_type {
3536 self.ctrl
3537 .writer
3538 .set_blosc_shuffle_type(params.value.as_i32());
3539 return ParamChangeResult::updates(vec![]);
3540 }
3541 if Some(reason) == self.hdf5_params.blosc_compressor {
3542 self.ctrl.writer.set_blosc_compressor(params.value.as_i32());
3543 return ParamChangeResult::updates(vec![]);
3544 }
3545 if Some(reason) == self.hdf5_params.blosc_compress_level {
3546 self.ctrl
3547 .writer
3548 .set_blosc_compress_level(params.value.as_i32() as u32);
3549 return ParamChangeResult::updates(vec![]);
3550 }
3551 if Some(reason) == self.hdf5_params.nbit_precision {
3552 self.ctrl
3553 .writer
3554 .set_nbit_precision(params.value.as_i32() as u32);
3555 return ParamChangeResult::updates(vec![]);
3556 }
3557 if Some(reason) == self.hdf5_params.nbit_offset {
3558 self.ctrl
3559 .writer
3560 .set_nbit_offset(params.value.as_i32() as u32);
3561 return ParamChangeResult::updates(vec![]);
3562 }
3563 if Some(reason) == self.hdf5_params.jpeg_quality {
3564 self.ctrl
3565 .writer
3566 .set_jpeg_quality(params.value.as_i32() as u32);
3567 return ParamChangeResult::updates(vec![]);
3568 }
3569 if Some(reason) == self.hdf5_params.store_attributes {
3570 self.ctrl
3571 .writer
3572 .set_store_attributes(params.value.as_i32() != 0);
3573 return ParamChangeResult::updates(vec![]);
3574 }
3575 if Some(reason) == self.hdf5_params.store_performance {
3576 self.ctrl
3577 .writer
3578 .set_store_performance(params.value.as_i32() != 0);
3579 return ParamChangeResult::updates(vec![]);
3580 }
3581 if Some(reason) == self.hdf5_params.chunk_size_auto {
3583 self.ctrl
3584 .writer
3585 .set_chunk_size_auto(params.value.as_i32() != 0);
3586 return ParamChangeResult::updates(vec![]);
3587 }
3588 if Some(reason) == self.hdf5_params.n_row_chunks {
3589 self.ctrl
3590 .writer
3591 .set_n_row_chunks(params.value.as_i32().max(0) as usize);
3592 return ParamChangeResult::updates(vec![]);
3593 }
3594 if Some(reason) == self.hdf5_params.n_col_chunks {
3595 self.ctrl
3596 .writer
3597 .set_n_col_chunks(params.value.as_i32().max(0) as usize);
3598 return ParamChangeResult::updates(vec![]);
3599 }
3600 if Some(reason) == self.hdf5_params.n_frames_chunks {
3601 self.ctrl
3602 .writer
3603 .set_n_frames_chunks(params.value.as_i32().max(0) as usize);
3604 return ParamChangeResult::updates(vec![]);
3605 }
3606 if Some(reason) == self.hdf5_params.ndattr_chunk {
3607 self.ctrl
3609 .writer
3610 .set_ndattr_chunk(params.value.as_i32().max(0) as usize);
3611 return ParamChangeResult::updates(vec![]);
3612 }
3613 if Some(reason) == self.hdf5_params.n_extra_dims {
3615 self.ctrl
3616 .writer
3617 .set_n_extra_dims(params.value.as_i32().max(0) as usize);
3618 return ParamChangeResult::updates(vec![]);
3619 }
3620 for i in 0..MAX_EXTRA_DIMS {
3621 if Some(reason) == self.hdf5_params.extra_dim_size[i] {
3622 self.ctrl
3623 .writer
3624 .set_extra_dim_size(i, params.value.as_i32().max(1) as usize);
3625 return ParamChangeResult::updates(vec![]);
3626 }
3627 if Some(reason) == self.hdf5_params.extra_dim_name[i] {
3628 self.ctrl
3629 .writer
3630 .set_extra_dim_name(i, params.value.as_string().unwrap_or(""));
3631 return ParamChangeResult::updates(vec![]);
3632 }
3633 }
3634 if Some(reason) == self.hdf5_params.fill_value {
3635 self.ctrl.writer.set_fill_value(params.value.as_f64());
3636 return ParamChangeResult::updates(vec![]);
3637 }
3638 if Some(reason) == self.hdf5_params.dim_att_datasets {
3639 self.ctrl
3640 .writer
3641 .set_dim_att_datasets(params.value.as_i32() != 0);
3642 return ParamChangeResult::updates(vec![]);
3643 }
3644 if Some(reason) == self.hdf5_params.layout_filename {
3646 let path = params.value.as_string().unwrap_or("").to_string();
3647 self.ctrl.writer.set_layout_filename(&path);
3648 let mut updates = vec![];
3649 if let Some(idx) = self.hdf5_params.layout_valid {
3650 updates.push(ParamUpdate::int32(
3651 idx,
3652 if self.ctrl.writer.layout_valid { 1 } else { 0 },
3653 ));
3654 }
3655 if let Some(idx) = self.hdf5_params.layout_error_msg {
3656 updates.push(ParamUpdate::Octet {
3657 reason: idx,
3658 addr: 0,
3659 value: self.ctrl.writer.layout_error.clone(),
3660 });
3661 }
3662 return ParamChangeResult::updates(updates);
3663 }
3664 if Some(reason) == self.hdf5_params.swmr_mode {
3666 self.ctrl.writer.set_swmr_mode(params.value.as_i32() != 0);
3667 return ParamChangeResult::updates(vec![]);
3668 }
3669 if Some(reason) == self.hdf5_params.swmr_flush_now {
3670 if params.value.as_i32() != 0 {
3671 self.ctrl.writer.flush_swmr();
3672 let mut updates = vec![];
3673 if let Some(idx) = self.hdf5_params.swmr_cb_counter {
3674 updates.push(ParamUpdate::int32(
3675 idx,
3676 self.ctrl.writer.swmr_cb_counter as i32,
3677 ));
3678 }
3679 return ParamChangeResult::updates(updates);
3680 }
3681 return ParamChangeResult::updates(vec![]);
3682 }
3683 if Some(reason) == self.hdf5_params.flush_nth_frame {
3684 self.ctrl
3685 .writer
3686 .set_flush_nth_frame(params.value.as_i32().max(0) as usize);
3687 return ParamChangeResult::updates(vec![]);
3688 }
3689 self.ctrl.on_param_change(reason, params)
3690 }
3691}
3692
3693#[cfg(test)]
3694mod tests {
3695 use super::*;
3696 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
3697 use rust_hdf5::format::messages::filter::FILTER_LZ4;
3698 use std::sync::atomic::{AtomicU32, Ordering};
3699
3700 static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);
3701
3702 fn temp_path(prefix: &str) -> PathBuf {
3703 let n = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
3704 std::env::temp_dir().join(format!("adcore_test_{}_{}.h5", prefix, n))
3705 }
3706
3707 #[test]
3708 fn test_write_single_frame() {
3709 let path = temp_path("hdf5_single");
3710 let mut writer = Hdf5Writer::new();
3711
3712 let mut arr = NDArray::new(
3713 vec![NDDimension::new(4), NDDimension::new(4)],
3714 NDDataType::UInt8,
3715 );
3716 if let NDDataBuffer::U8(ref mut v) = arr.data {
3717 for i in 0..16 {
3718 v[i] = i as u8;
3719 }
3720 }
3721
3722 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
3723 writer.write_file(&arr).unwrap();
3724 writer.close_file().unwrap();
3725
3726 let h5 = H5File::open(&path).unwrap();
3728 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3729 assert_eq!(ds.shape(), vec![1, 4, 4]);
3730 let data: Vec<u8> = ds.read_raw().unwrap();
3731 assert_eq!(data[0], 0);
3732 assert_eq!(data[15], 15);
3733 drop(h5);
3734
3735 let mut reader = Hdf5Writer::new();
3736 reader.current_path = Some(path.clone());
3737 let read_arr = reader.read_file().unwrap();
3738 assert_eq!(read_arr.dims.len(), 3);
3739 assert_eq!(read_arr.dims[2].size, 1); std::fs::remove_file(&path).ok();
3742 }
3743
3744 #[test]
3745 fn test_parse_fsync_on_close_env() {
3746 assert!(Hdf5Writer::parse_fsync_on_close_env(None));
3748 for v in ["0", "false", "no", "off", "FALSE", "Off", " no "] {
3750 assert!(
3751 !Hdf5Writer::parse_fsync_on_close_env(Some(v)),
3752 "{v:?} should disable fsync-on-close"
3753 );
3754 }
3755 for v in ["", "1", "true", "yes", "on", "durable"] {
3757 assert!(
3758 Hdf5Writer::parse_fsync_on_close_env(Some(v)),
3759 "{v:?} should keep fsync-on-close"
3760 );
3761 }
3762 }
3763
3764 #[test]
3765 fn test_no_fsync_close_writes_valid_file() {
3766 let path = temp_path("hdf5_no_fsync");
3770 let mut writer = Hdf5Writer::new();
3771 writer.fsync_on_close = false;
3772
3773 let mut arr = NDArray::new(
3774 vec![NDDimension::new(4), NDDimension::new(4)],
3775 NDDataType::UInt8,
3776 );
3777 if let NDDataBuffer::U8(ref mut v) = arr.data {
3778 for i in 0..16 {
3779 v[i] = i as u8;
3780 }
3781 }
3782
3783 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
3784 writer.write_file(&arr).unwrap();
3785 writer.close_file().unwrap();
3786
3787 let h5 = H5File::open(&path).unwrap();
3788 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3789 assert_eq!(ds.shape(), vec![1, 4, 4]);
3790 let data: Vec<u8> = ds.read_raw().unwrap();
3791 assert_eq!(data[0], 0);
3792 assert_eq!(data[15], 15);
3793 drop(h5);
3794
3795 std::fs::remove_file(&path).ok();
3796 }
3797
3798 #[test]
3799 fn test_write_multiple_frames() {
3800 let path = temp_path("hdf5_multi");
3801 let mut writer = Hdf5Writer::new();
3802
3803 let mut arr = NDArray::new(
3804 vec![NDDimension::new(4), NDDimension::new(4)],
3805 NDDataType::UInt8,
3806 );
3807 for f in 0..3u8 {
3809 if let NDDataBuffer::U8(ref mut v) = arr.data {
3810 for x in v.iter_mut() {
3811 *x = f;
3812 }
3813 }
3814 if f == 0 {
3815 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3816 }
3817 writer.write_file(&arr).unwrap();
3818 }
3819 writer.close_file().unwrap();
3820
3821 assert!(writer.supports_multiple_arrays());
3822 assert_eq!(writer.frame_count(), 3);
3823
3824 let data = std::fs::read(&path).unwrap();
3825 assert_eq!(&data[0..8], b"\x89HDF\r\n\x1a\n");
3826
3827 let h5 = H5File::open(&path).unwrap();
3829 let names = h5.dataset_names();
3830 assert!(names.contains(&"entry/instrument/detector/data".to_string()));
3831 assert!(
3832 !names.contains(&"data_1".to_string()),
3833 "must not write per-frame datasets"
3834 );
3835 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3836 assert_eq!(
3837 ds.shape(),
3838 vec![3, 4, 4],
3839 "rank/shape must be [nframes,Y,X]"
3840 );
3841 let raw: Vec<u8> = ds.read_raw().unwrap();
3842 assert_eq!(raw.len(), 3 * 4 * 4);
3843 assert_eq!(raw[0], 0);
3845 assert_eq!(raw[16], 1);
3846 assert_eq!(raw[32], 2);
3847
3848 std::fs::remove_file(&path).ok();
3849 }
3850
3851 #[test]
3852 fn test_sub_frame_chunking() {
3853 let path = temp_path("hdf5_subchunk");
3857 let mut writer = Hdf5Writer::new();
3858 writer.set_chunk_size_auto(false); writer.set_n_row_chunks(4); writer.set_n_col_chunks(4); let mut arr = NDArray::new(
3863 vec![NDDimension::new(8), NDDimension::new(8)],
3864 NDDataType::UInt16,
3865 );
3866 for f in 0..3u16 {
3867 if let NDDataBuffer::U16(ref mut v) = arr.data {
3868 for (i, x) in v.iter_mut().enumerate() {
3869 *x = f * 1000 + i as u16;
3870 }
3871 }
3872 if f == 0 {
3873 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3874 }
3875 writer.write_file(&arr).unwrap();
3876 }
3877 writer.close_file().unwrap();
3878
3879 let h5 = H5File::open(&path).unwrap();
3880 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3881 assert_eq!(ds.shape(), vec![3, 8, 8], "shape must not be chunk-padded");
3882 assert_eq!(
3883 ds.chunk_dims(),
3884 Some(vec![1, 4, 4]),
3885 "chunk grid must be the sub-frame tile size"
3886 );
3887 let raw: Vec<u16> = ds.read_raw().unwrap();
3888 assert_eq!(raw.len(), 3 * 64);
3889 for f in 0..3u16 {
3890 for i in 0..64usize {
3891 assert_eq!(
3892 raw[f as usize * 64 + i],
3893 f * 1000 + i as u16,
3894 "frame {} elem {}",
3895 f,
3896 i
3897 );
3898 }
3899 }
3900
3901 std::fs::remove_file(&path).ok();
3902 }
3903
3904 #[test]
3905 fn test_sub_frame_chunking_with_compression() {
3906 let path = temp_path("hdf5_subchunk_zlib");
3909 let mut writer = Hdf5Writer::new();
3910 writer.set_chunk_size_auto(false);
3911 writer.set_n_row_chunks(4);
3912 writer.set_n_col_chunks(4);
3913 writer.set_compression_type(COMPRESS_ZLIB);
3914
3915 let mut arr = NDArray::new(
3916 vec![NDDimension::new(8), NDDimension::new(8)],
3917 NDDataType::UInt16,
3918 );
3919 for f in 0..2u16 {
3920 if let NDDataBuffer::U16(ref mut v) = arr.data {
3921 for (i, x) in v.iter_mut().enumerate() {
3922 *x = f * 100 + i as u16;
3923 }
3924 }
3925 if f == 0 {
3926 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3927 }
3928 writer.write_file(&arr).unwrap();
3929 }
3930 writer.close_file().unwrap();
3931
3932 let h5 = H5File::open(&path).unwrap();
3933 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3934 assert_eq!(ds.shape(), vec![2, 8, 8]);
3935 assert_eq!(ds.chunk_dims(), Some(vec![1, 4, 4]));
3936 let raw: Vec<u16> = ds.read_raw().unwrap();
3937 for f in 0..2u16 {
3938 for i in 0..64usize {
3939 assert_eq!(raw[f as usize * 64 + i], f * 100 + i as u16);
3940 }
3941 }
3942
3943 std::fs::remove_file(&path).ok();
3944 }
3945
3946 #[test]
3947 fn test_non_dividing_chunk_is_honored_and_extent_trimmed() {
3948 let path = temp_path("hdf5_subchunk_nd");
3952 let mut writer = Hdf5Writer::new();
3953 writer.set_chunk_size_auto(false); writer.set_n_row_chunks(3); writer.set_n_col_chunks(4); let mut arr = NDArray::new(
3958 vec![NDDimension::new(8), NDDimension::new(8)],
3959 NDDataType::UInt16,
3960 );
3961 if let NDDataBuffer::U16(ref mut v) = arr.data {
3962 for (i, x) in v.iter_mut().enumerate() {
3963 *x = i as u16;
3964 }
3965 }
3966 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3967 writer.write_file(&arr).unwrap();
3968 writer.write_file(&arr).unwrap();
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![2, 8, 8], "extent trimmed, not padded");
3974 assert_eq!(ds.chunk_dims(), Some(vec![1, 3, 4]));
3975 let raw: Vec<u16> = ds.read_raw().unwrap();
3976 assert_eq!(raw.len(), 2 * 64);
3977 for i in 0..64usize {
3978 assert_eq!(raw[i], i as u16);
3979 assert_eq!(raw[64 + i], i as u16);
3980 }
3981
3982 std::fs::remove_file(&path).ok();
3983 }
3984
3985 #[test]
3986 fn test_n_frames_chunks_band() {
3987 let path = temp_path("hdf5_framechunks");
3990 let mut writer = Hdf5Writer::new();
3991 writer.set_chunk_size_auto(false);
3992 writer.set_n_frames_chunks(2); let mut arr = NDArray::new(
3995 vec![NDDimension::new(4), NDDimension::new(4)],
3996 NDDataType::UInt16,
3997 );
3998 for f in 0..5u16 {
4000 if let NDDataBuffer::U16(ref mut v) = arr.data {
4001 for (i, x) in v.iter_mut().enumerate() {
4002 *x = f * 1000 + i as u16;
4003 }
4004 }
4005 if f == 0 {
4006 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4007 }
4008 writer.write_file(&arr).unwrap();
4009 }
4010 writer.close_file().unwrap();
4011
4012 let h5 = H5File::open(&path).unwrap();
4013 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4014 assert_eq!(ds.shape(), vec![5, 4, 4], "exact frame count, no padding");
4015 assert_eq!(ds.chunk_dims(), Some(vec![2, 4, 4]));
4016 let raw: Vec<u16> = ds.read_raw().unwrap();
4017 for f in 0..5u16 {
4018 for i in 0..16usize {
4019 assert_eq!(raw[f as usize * 16 + i], f * 1000 + i as u16);
4020 }
4021 }
4022
4023 std::fs::remove_file(&path).ok();
4024 }
4025
4026 #[test]
4027 fn test_frames_chunks_with_sub_frame_tiles() {
4028 let path = temp_path("hdf5_full_chunk");
4032 let mut writer = Hdf5Writer::new();
4033 writer.set_chunk_size_auto(false);
4034 writer.set_n_frames_chunks(2); writer.set_n_row_chunks(4); writer.set_n_col_chunks(4); let mut arr = NDArray::new(
4039 vec![NDDimension::new(8), NDDimension::new(8)],
4040 NDDataType::UInt16,
4041 );
4042 for f in 0..3u16 {
4044 if let NDDataBuffer::U16(ref mut v) = arr.data {
4045 for (i, x) in v.iter_mut().enumerate() {
4046 *x = f * 1000 + i as u16;
4047 }
4048 }
4049 if f == 0 {
4050 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4051 }
4052 writer.write_file(&arr).unwrap();
4053 }
4054 writer.close_file().unwrap();
4055
4056 let h5 = H5File::open(&path).unwrap();
4057 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4058 assert_eq!(ds.shape(), vec![3, 8, 8], "exact frame count");
4059 assert_eq!(ds.chunk_dims(), Some(vec![2, 4, 4]));
4060 let raw: Vec<u16> = ds.read_raw().unwrap();
4061 assert_eq!(raw.len(), 3 * 64);
4062 for f in 0..3u16 {
4063 for i in 0..64usize {
4064 assert_eq!(
4065 raw[f as usize * 64 + i],
4066 f * 1000 + i as u16,
4067 "frame {} elem {}",
4068 f,
4069 i
4070 );
4071 }
4072 }
4073
4074 std::fs::remove_file(&path).ok();
4075 }
4076
4077 #[test]
4078 fn test_attribute_datasets() {
4079 let path = temp_path("hdf5_attr_ds");
4080 let mut writer = Hdf5Writer::new();
4081
4082 let mk = |exposure: f64, count: i32| {
4083 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4084 arr.attributes.add(NDAttribute::new_static(
4085 "exposure",
4086 "",
4087 NDAttrSource::Driver,
4088 NDAttrValue::Float64(exposure),
4089 ));
4090 arr.attributes.add(NDAttribute::new_static(
4091 "count",
4092 "",
4093 NDAttrSource::Driver,
4094 NDAttrValue::Int32(count),
4095 ));
4096 arr
4097 };
4098
4099 let a0 = mk(0.5, 10);
4100 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4101 writer.write_file(&a0).unwrap();
4102 writer.write_file(&mk(0.75, 20)).unwrap();
4103 writer.write_file(&mk(1.25, 30)).unwrap();
4104 writer.close_file().unwrap();
4105
4106 let h5 = H5File::open(&path).unwrap();
4107 let exp = h5
4109 .dataset("entry/instrument/NDAttributes/exposure")
4110 .unwrap();
4111 assert_eq!(exp.shape(), vec![3]);
4112 let exp_vals: Vec<f64> = exp.read_raw().unwrap();
4113 assert_eq!(exp_vals, vec![0.5, 0.75, 1.25]);
4114
4115 let cnt = h5.dataset("entry/instrument/NDAttributes/count").unwrap();
4116 assert_eq!(cnt.shape(), vec![3]);
4117 let cnt_vals: Vec<i32> = cnt.read_raw().unwrap();
4119 assert_eq!(cnt_vals, vec![10, 20, 30]);
4120
4121 std::fs::remove_file(&path).ok();
4122 }
4123
4124 #[test]
4125 fn test_attribute_dataset_chunk_matches_capture_target() {
4126 let path = temp_path("hdf5_attr_chunk_cap");
4133 let mut writer = Hdf5Writer::new();
4134 writer.set_num_capture(10);
4135
4136 let mk = |c: i32| {
4137 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4138 arr.attributes.add(NDAttribute::new_static(
4139 "count",
4140 "",
4141 NDAttrSource::Driver,
4142 NDAttrValue::Int32(c),
4143 ));
4144 arr
4145 };
4146
4147 let a0 = mk(1);
4148 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4149 writer.write_file(&a0).unwrap();
4150 writer.write_file(&mk(2)).unwrap();
4151 writer.write_file(&mk(3)).unwrap();
4152 writer.close_file().unwrap();
4153
4154 let h5 = H5File::open(&path).unwrap();
4155 let ds = h5.dataset("entry/instrument/NDAttributes/count").unwrap();
4156 assert_eq!(ds.shape(), vec![3]);
4157 assert_eq!(ds.chunk_dims(), Some(vec![10]));
4158 let vals: Vec<i32> = ds.read_raw().unwrap();
4159 assert_eq!(vals, vec![1, 2, 3]);
4160 std::fs::remove_file(&path).ok();
4161 }
4162
4163 #[test]
4164 fn test_attribute_dataset_chunk_single_mode_is_one() {
4165 let path = temp_path("hdf5_attr_chunk_single");
4169 let mut writer = Hdf5Writer::new();
4170 writer.set_num_capture(64);
4171
4172 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4173 arr.attributes.add(NDAttribute::new_static(
4174 "count",
4175 "",
4176 NDAttrSource::Driver,
4177 NDAttrValue::Int32(7),
4178 ));
4179 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4180 writer.write_file(&arr).unwrap();
4181 writer.close_file().unwrap();
4182
4183 let h5 = H5File::open(&path).unwrap();
4184 let ds = h5.dataset("entry/instrument/NDAttributes/count").unwrap();
4185 assert_eq!(ds.chunk_dims(), Some(vec![1]));
4186 std::fs::remove_file(&path).ok();
4187 }
4188
4189 #[test]
4190 fn test_string_attribute_dataset_is_rank1_fixed_string() {
4191 let path = temp_path("hdf5_attr_str");
4196 let mut writer = Hdf5Writer::new();
4197
4198 let mk = |mode_name: &str| {
4199 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4200 arr.attributes.add(NDAttribute::new_static(
4201 "ColorMode",
4202 "",
4203 NDAttrSource::Driver,
4204 NDAttrValue::String(mode_name.to_string()),
4205 ));
4206 arr
4207 };
4208
4209 let a0 = mk("Mono");
4210 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4211 writer.write_file(&a0).unwrap();
4212 writer.write_file(&mk("RGB1")).unwrap();
4213 writer.close_file().unwrap();
4214
4215 let h5 = H5File::open(&path).unwrap();
4216 let ds = h5
4219 .dataset("entry/instrument/detector/NDAttributes/ColorMode")
4220 .unwrap();
4221 assert_eq!(ds.shape(), vec![2]);
4223 assert_eq!(ds.element_size(), MAX_ATTRIBUTE_STRING_SIZE);
4224
4225 let frames: Vec<FixedStr256> = ds.read_raw().unwrap();
4226 assert_eq!(frames.len(), 2);
4227 let decode = |f: &FixedStr256| -> String {
4228 f.0.iter()
4229 .take_while(|&&b| b != 0)
4230 .map(|&b| b as char)
4231 .collect()
4232 };
4233 assert_eq!(decode(&frames[0]), "Mono");
4234 assert_eq!(decode(&frames[1]), "RGB1");
4235 std::fs::remove_file(&path).ok();
4236 }
4237
4238 #[test]
4239 fn test_ndattribute_dataset_routed_to_declared_group() {
4240 let path = temp_path("hdf5_attr_route");
4246 let mut writer = Hdf5Writer::new();
4247
4248 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4249 arr.attributes.add(NDAttribute::new_static(
4250 "ColorMode",
4251 "",
4252 NDAttrSource::Driver,
4253 NDAttrValue::String("Mono".to_string()),
4254 ));
4255 arr.attributes.add(NDAttribute::new_static(
4256 "count",
4257 "",
4258 NDAttrSource::Driver,
4259 NDAttrValue::Int32(7),
4260 ));
4261
4262 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4263 writer.write_file(&arr).unwrap();
4264 writer.close_file().unwrap();
4265
4266 let h5 = H5File::open(&path).unwrap();
4267 assert!(
4269 h5.dataset("entry/instrument/detector/NDAttributes/ColorMode")
4270 .is_ok()
4271 );
4272 assert!(h5.dataset("entry/instrument/NDAttributes/count").is_ok());
4274 assert!(
4276 h5.dataset("entry/instrument/NDAttributes/ColorMode")
4277 .is_err()
4278 );
4279 std::fs::remove_file(&path).ok();
4280 }
4281
4282 #[test]
4283 fn test_ndattribute_element_attr_open_close_values() {
4284 let dir = std::env::temp_dir();
4290 let layout = dir.join("adcore_layout_elem_attr.xml");
4291 std::fs::write(
4292 &layout,
4293 r#"<hdf5_layout>
4294 <group name="entry">
4295 <group name="instrument">
4296 <group name="detector">
4297 <attribute name="FirstColorMode" source="ndattribute" ndattribute="ColorMode" when="OnFileOpen"/>
4298 <attribute name="LastColorMode" source="ndattribute" ndattribute="ColorMode" when="OnFileClose"/>
4299 <attribute name="DefaultColorMode" source="ndattribute" ndattribute="ColorMode"/>
4300 <dataset name="data" source="detector" det_default="true">
4301 <attribute name="GainAtOpen" source="ndattribute" ndattribute="Gain" when="OnFileOpen"/>
4302 <attribute name="GainAtClose" source="ndattribute" ndattribute="Gain" when="OnFileClose"/>
4303 </dataset>
4304 </group>
4305 <group name="NDAttributes" ndattr_default="true"/>
4306 </group>
4307 </group>
4308 </hdf5_layout>"#,
4309 )
4310 .unwrap();
4311
4312 let path = temp_path("hdf5_elem_attr");
4313 let mut writer = Hdf5Writer::new();
4314 assert!(
4315 writer.set_layout_filename(layout.to_str().unwrap()),
4316 "layout XML must parse: {}",
4317 writer.layout_error
4318 );
4319
4320 let mk = |mode: &str, gain: i32| {
4321 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4322 arr.attributes.add(NDAttribute::new_static(
4323 "ColorMode",
4324 "",
4325 NDAttrSource::Driver,
4326 NDAttrValue::String(mode.to_string()),
4327 ));
4328 arr.attributes.add(NDAttribute::new_static(
4329 "Gain",
4330 "",
4331 NDAttrSource::Driver,
4332 NDAttrValue::Int32(gain),
4333 ));
4334 arr
4335 };
4336
4337 let a0 = mk("Mono", 10);
4338 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4339 writer.write_file(&a0).unwrap();
4340 writer.write_file(&mk("RGB1", 20)).unwrap();
4341 writer.write_file(&mk("Bayer", 30)).unwrap();
4342 writer.close_file().unwrap();
4343
4344 let h5 = H5File::open(&path).unwrap();
4345
4346 let mut grp = h5.root_group();
4348 for seg in ["entry", "instrument", "detector"] {
4349 grp = grp.group(seg).unwrap();
4350 }
4351 assert_eq!(grp.attr_string("FirstColorMode").unwrap(), "Mono");
4353 assert_eq!(grp.attr_string("DefaultColorMode").unwrap(), "Mono");
4354 assert_eq!(grp.attr_string("LastColorMode").unwrap(), "Bayer");
4356
4357 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4359 assert_eq!(
4360 ds.attr("GainAtOpen")
4361 .unwrap()
4362 .read_numeric::<i32>()
4363 .unwrap(),
4364 10
4365 );
4366 assert_eq!(
4367 ds.attr("GainAtClose")
4368 .unwrap()
4369 .read_numeric::<i32>()
4370 .unwrap(),
4371 30
4372 );
4373
4374 std::fs::remove_file(&path).ok();
4375 std::fs::remove_file(&layout).ok();
4376 }
4377
4378 #[test]
4379 fn test_ndattr_element_attr_on_lazily_created_dataset() {
4380 let dir = std::env::temp_dir();
4388 let layout = dir.join("adcore_layout_lazy_ds_attr.xml");
4389 std::fs::write(
4390 &layout,
4391 r#"<hdf5_layout>
4392 <group name="entry">
4393 <group name="instrument">
4394 <group name="detector">
4395 <dataset name="data" source="detector" det_default="true"/>
4396 <dataset name="GainTrace" source="ndattribute" ndattribute="Gain">
4397 <attribute name="ModeAtOpen" source="ndattribute" ndattribute="ColorMode" when="OnFileOpen"/>
4398 <attribute name="ModeAtClose" source="ndattribute" ndattribute="ColorMode" when="OnFileClose"/>
4399 </dataset>
4400 </group>
4401 <group name="NDAttributes" ndattr_default="true"/>
4402 </group>
4403 </group>
4404 </hdf5_layout>"#,
4405 )
4406 .unwrap();
4407
4408 let path = temp_path("hdf5_lazy_ds_attr");
4409 let mut writer = Hdf5Writer::new();
4410 assert!(
4411 writer.set_layout_filename(layout.to_str().unwrap()),
4412 "layout XML must parse: {}",
4413 writer.layout_error
4414 );
4415
4416 let mk = |mode: &str, gain: i32| {
4417 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4418 arr.attributes.add(NDAttribute::new_static(
4419 "ColorMode",
4420 "",
4421 NDAttrSource::Driver,
4422 NDAttrValue::String(mode.to_string()),
4423 ));
4424 arr.attributes.add(NDAttribute::new_static(
4425 "Gain",
4426 "",
4427 NDAttrSource::Driver,
4428 NDAttrValue::Int32(gain),
4429 ));
4430 arr
4431 };
4432
4433 let a0 = mk("Mono", 10);
4434 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4435 writer.write_file(&a0).unwrap();
4436 writer.write_file(&mk("RGB1", 20)).unwrap();
4437 writer.write_file(&mk("Bayer", 30)).unwrap();
4438 writer.close_file().unwrap();
4439
4440 let h5 = H5File::open(&path).unwrap();
4441 let ds = h5.dataset("entry/instrument/detector/GainTrace").unwrap();
4444 assert_eq!(
4445 ds.attr("ModeAtOpen").unwrap().read_string().unwrap(),
4446 "Mono"
4447 );
4448 assert_eq!(
4449 ds.attr("ModeAtClose").unwrap().read_string().unwrap(),
4450 "Bayer"
4451 );
4452 std::fs::remove_file(&path).ok();
4453 std::fs::remove_file(&layout).ok();
4454 }
4455
4456 #[test]
4457 fn test_swmr_ndattr_element_attr_on_streaming_dataset() {
4458 let dir = std::env::temp_dir();
4464 let layout = dir.join("adcore_layout_swmr_ds_attr.xml");
4465 std::fs::write(
4466 &layout,
4467 r#"<hdf5_layout>
4468 <group name="entry">
4469 <group name="instrument">
4470 <group name="detector">
4471 <dataset name="data" source="detector" det_default="true">
4472 <attribute name="ModeAtOpen" source="ndattribute" ndattribute="ColorMode" when="OnFileOpen"/>
4473 </dataset>
4474 </group>
4475 <group name="NDAttributes" ndattr_default="true"/>
4476 </group>
4477 </group>
4478 </hdf5_layout>"#,
4479 )
4480 .unwrap();
4481
4482 let path = temp_path("hdf5_swmr_ds_attr");
4483 let mut writer = Hdf5Writer::new();
4484 writer.set_swmr_mode(true);
4485 assert!(
4486 writer.set_layout_filename(layout.to_str().unwrap()),
4487 "layout XML must parse: {}",
4488 writer.layout_error
4489 );
4490
4491 let mk = |mode: &str| {
4492 let mut arr = NDArray::new(
4493 vec![NDDimension::new(4), NDDimension::new(4)],
4494 NDDataType::UInt16,
4495 );
4496 arr.attributes.add(NDAttribute::new_static(
4497 "ColorMode",
4498 "",
4499 NDAttrSource::Driver,
4500 NDAttrValue::String(mode.to_string()),
4501 ));
4502 arr
4503 };
4504
4505 let a0 = mk("Mono");
4506 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4507 writer.write_file(&a0).unwrap();
4508 writer.write_file(&mk("RGB1")).unwrap();
4509 writer.close_file().unwrap();
4510
4511 let h5 = H5File::open(&path).unwrap();
4512 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4513 assert_eq!(
4514 ds.attr("ModeAtOpen").unwrap().read_string().unwrap(),
4515 "Mono"
4516 );
4517 std::fs::remove_file(&path).ok();
4518 std::fs::remove_file(&layout).ok();
4519 }
4520
4521 #[test]
4522 fn test_swmr_ndarray_default_attrs_on_streaming_dataset() {
4523 let path = temp_path("hdf5_swmr_dimattr_1d");
4533 let mut writer = Hdf5Writer::new();
4534 writer.set_swmr_mode(true);
4535 let mut d = NDDimension::new(8);
4536 d.offset = 3;
4537 d.binning = 2;
4538 d.reverse = true;
4539 let arr = NDArray::new(vec![d], NDDataType::UInt16);
4540 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4541 writer.write_file(&arr).unwrap();
4542 writer.write_file(&arr).unwrap();
4543 writer.close_file().unwrap();
4544 let h5 = H5File::open(&path).unwrap();
4545 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4546 let geti = |n: &str| -> i32 { ds.attr(n).unwrap().read_numeric().unwrap() };
4547 assert_eq!(geti("NDArrayNumDims"), 1);
4548 assert_eq!(geti("NDArrayDimOffset"), 3);
4549 assert_eq!(geti("NDArrayDimBinning"), 2);
4550 assert_eq!(geti("NDArrayDimReverse"), 1);
4551 std::fs::remove_file(&path).ok();
4552
4553 let path = temp_path("hdf5_swmr_dimattr_2d");
4556 let mut writer = Hdf5Writer::new();
4557 writer.set_swmr_mode(true);
4558 let mut d0 = NDDimension::new(4);
4559 d0.offset = 3;
4560 d0.binning = 2;
4561 d0.reverse = true;
4562 let mut d1 = NDDimension::new(8);
4563 d1.offset = 5;
4564 d1.binning = 4;
4565 d1.reverse = false;
4566 let arr = NDArray::new(vec![d0, d1], NDDataType::UInt16);
4567 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4568 writer.write_file(&arr).unwrap();
4569 writer.write_file(&arr).unwrap();
4570 writer.close_file().unwrap();
4571 let h5 = H5File::open(&path).unwrap();
4572 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4573 let numdims: i32 = ds.attr("NDArrayNumDims").unwrap().read_numeric().unwrap();
4574 assert_eq!(numdims, 2);
4575 let read_i32_arr = |n: &str| -> Vec<i32> {
4576 let raw = ds.attr(n).unwrap().read_raw().unwrap();
4577 assert_eq!(raw.len(), 2 * 4, "{n} must be a 2-element int32 array");
4578 raw.chunks_exact(4)
4579 .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
4580 .collect()
4581 };
4582 assert_eq!(read_i32_arr("NDArrayDimOffset"), vec![3, 5]);
4583 assert_eq!(read_i32_arr("NDArrayDimBinning"), vec![2, 4]);
4584 assert_eq!(read_i32_arr("NDArrayDimReverse"), vec![1, 0]);
4585 std::fs::remove_file(&path).ok();
4586 }
4587
4588 #[test]
4589 fn test_ndattr_descriptor_attributes_written() {
4590 let path = temp_path("hdf5_attr_desc");
4594 let mut writer = Hdf5Writer::new();
4595
4596 let mk = |v: f64| {
4597 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4598 arr.attributes.add(NDAttribute::new_static(
4599 "AcquireTime",
4600 "exposure time",
4601 NDAttrSource::EpicsPV("13SIM1:cam1:AcquireTime_RBV".to_string()),
4602 NDAttrValue::Float64(v),
4603 ));
4604 arr
4605 };
4606
4607 let a0 = mk(0.1);
4608 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4609 writer.write_file(&a0).unwrap();
4610 writer.write_file(&mk(0.2)).unwrap();
4611 writer.close_file().unwrap();
4612
4613 let h5 = H5File::open(&path).unwrap();
4614 let ds = h5
4615 .dataset("entry/instrument/NDAttributes/AcquireTime")
4616 .unwrap();
4617 let read = |n: &str| ds.attr(n).unwrap().read_string().unwrap();
4618 assert_eq!(read("NDAttrName"), "AcquireTime");
4619 assert_eq!(read("NDAttrDescription"), "exposure time");
4620 assert_eq!(read("NDAttrSourceType"), "NDAttrSourceEPICSPV");
4621 assert_eq!(read("NDAttrSource"), "13SIM1:cam1:AcquireTime_RBV");
4622 std::fs::remove_file(&path).ok();
4623 }
4624
4625 #[test]
4626 fn test_detector_dataset_ndarray_dim_attributes() {
4627 let path = temp_path("hdf5_dimattr_1d");
4635 let mut writer = Hdf5Writer::new();
4636 let mut d = NDDimension::new(8);
4637 d.offset = 3;
4638 d.binning = 2;
4639 d.reverse = true;
4640 let arr = NDArray::new(vec![d], NDDataType::UInt16);
4641 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4642 writer.write_file(&arr).unwrap();
4643 writer.close_file().unwrap();
4644 let h5 = H5File::open(&path).unwrap();
4645 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4646 let geti = |n: &str| -> i32 { ds.attr(n).unwrap().read_numeric().unwrap() };
4647 assert_eq!(geti("NDArrayNumDims"), 1);
4648 assert_eq!(geti("NDArrayDimOffset"), 3);
4649 assert_eq!(geti("NDArrayDimBinning"), 2);
4650 assert_eq!(geti("NDArrayDimReverse"), 1);
4651 std::fs::remove_file(&path).ok();
4652
4653 let path = temp_path("hdf5_dimattr_2d");
4657 let mut writer = Hdf5Writer::new();
4658 let mut d0 = NDDimension::new(4);
4659 d0.offset = 3;
4660 d0.binning = 2;
4661 d0.reverse = true;
4662 let mut d1 = NDDimension::new(8);
4663 d1.offset = 5;
4664 d1.binning = 4;
4665 d1.reverse = false;
4666 let arr = NDArray::new(vec![d0, d1], NDDataType::UInt16);
4667 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4668 writer.write_file(&arr).unwrap();
4669 writer.close_file().unwrap();
4670 let h5 = H5File::open(&path).unwrap();
4671 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4672 let numdims: i32 = ds.attr("NDArrayNumDims").unwrap().read_numeric().unwrap();
4673 assert_eq!(numdims, 2);
4674 let read_i32_arr = |n: &str| -> Vec<i32> {
4678 let raw = ds.attr(n).unwrap().read_raw().unwrap();
4679 assert_eq!(raw.len(), 2 * 4, "{n} must be a 2-element int32 array");
4680 raw.chunks_exact(4)
4681 .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
4682 .collect()
4683 };
4684 assert_eq!(read_i32_arr("NDArrayDimOffset"), vec![3, 5]);
4685 assert_eq!(read_i32_arr("NDArrayDimBinning"), vec![2, 4]);
4686 assert_eq!(read_i32_arr("NDArrayDimReverse"), vec![1, 0]);
4687 std::fs::remove_file(&path).ok();
4688 }
4689
4690 #[test]
4691 fn test_fill_value_recorded_on_dataset() {
4692 let path = temp_path("hdf5_fill");
4697 let mut writer = Hdf5Writer::new();
4698 writer.set_fill_value(7.5);
4699
4700 let arr = NDArray::new(
4701 vec![NDDimension::new(4), NDDimension::new(4)],
4702 NDDataType::UInt16,
4703 );
4704 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4705 writer.write_file(&arr).unwrap();
4706 writer.close_file().unwrap();
4707
4708 let h5 = H5File::open(&path).unwrap();
4709 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4710 let fv: f64 = ds.attr("HDF5_fillValue").unwrap().read_numeric().unwrap();
4711 assert_eq!(fv, 7.5);
4712 std::fs::remove_file(&path).ok();
4713
4714 let path2 = temp_path("hdf5_fill_dcpl");
4717 {
4718 let f = H5File::create(&path2).unwrap();
4719 let _ = f
4720 .new_dataset::<i32>()
4721 .shape(&[8][..])
4722 .fill_value(42i32)
4723 .create("unwritten")
4724 .unwrap();
4725 }
4726 let h5b = H5File::open(&path2).unwrap();
4727 let vals: Vec<i32> = h5b.dataset("unwritten").unwrap().read_raw().unwrap();
4728 assert_eq!(vals, vec![42i32; 8]);
4729 std::fs::remove_file(&path2).ok();
4730 }
4731
4732 #[test]
4733 fn test_performance_dataset() {
4734 let path = temp_path("hdf5_perf");
4735 let mut writer = Hdf5Writer::new();
4736 writer.set_store_performance(true);
4737
4738 let arr = NDArray::new(
4739 vec![NDDimension::new(8), NDDimension::new(8)],
4740 NDDataType::UInt16,
4741 );
4742 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4743 writer.write_file(&arr).unwrap();
4744 writer.write_file(&arr).unwrap();
4745 writer.close_file().unwrap();
4746
4747 let h5 = H5File::open(&path).unwrap();
4748 let ts = h5
4749 .dataset("entry/instrument/performance/timestamp")
4750 .unwrap();
4751 assert_eq!(ts.shape(), vec![2, 5]);
4752 let vals: Vec<f64> = ts.read_raw().unwrap();
4753 assert_eq!(vals.len(), 10);
4754
4755 std::fs::remove_file(&path).ok();
4756 }
4757
4758 #[test]
4759 fn test_performance_dataset_chunk_matches_capture_target() {
4760 let path = temp_path("hdf5_perf_chunk");
4766 let mut writer = Hdf5Writer::new();
4767 writer.set_store_performance(true);
4768 writer.set_num_capture(8);
4769
4770 let arr = NDArray::new(
4771 vec![NDDimension::new(8), NDDimension::new(8)],
4772 NDDataType::UInt16,
4773 );
4774 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4775 writer.write_file(&arr).unwrap();
4776 writer.write_file(&arr).unwrap();
4777 writer.close_file().unwrap();
4778
4779 let h5 = H5File::open(&path).unwrap();
4780 let ts = h5
4781 .dataset("entry/instrument/performance/timestamp")
4782 .unwrap();
4783 assert_eq!(ts.shape(), vec![2, 5]);
4784 assert_eq!(ts.chunk_dims(), Some(vec![8, 5]));
4785 let vals: Vec<f64> = ts.read_raw().unwrap();
4786 assert_eq!(vals.len(), 10);
4787
4788 std::fs::remove_file(&path).ok();
4789 }
4790
4791 #[test]
4792 fn test_roundtrip_all_types() {
4793 macro_rules! roundtrip {
4794 ($name:expr, $dt:expr, $variant:ident, $ty:ty, $vals:expr) => {{
4795 let path = temp_path($name);
4796 let mut writer = Hdf5Writer::new();
4797 let mut arr = NDArray::new(vec![NDDimension::new(4)], $dt);
4798 if let NDDataBuffer::$variant(ref mut v) = arr.data {
4799 let src: Vec<$ty> = $vals;
4800 v.copy_from_slice(&src);
4801 }
4802 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4803 writer.write_file(&arr).unwrap();
4804 writer.close_file().unwrap();
4805
4806 let mut reader = Hdf5Writer::new();
4807 reader.current_path = Some(path.clone());
4808 let r = reader.read_file().unwrap();
4809 assert_eq!(r.data.data_type(), $dt, "type for {}", $name);
4810 if let NDDataBuffer::$variant(ref v) = r.data {
4811 let src: Vec<$ty> = $vals;
4812 assert_eq!(v, &src, "values for {}", $name);
4813 } else {
4814 panic!("wrong buffer variant for {}", $name);
4815 }
4816 std::fs::remove_file(&path).ok();
4817 }};
4818 }
4819
4820 roundtrip!("rt_i8", NDDataType::Int8, I8, i8, vec![-1, 0, 1, 127]);
4821 roundtrip!("rt_u8", NDDataType::UInt8, U8, u8, vec![0, 1, 200, 255]);
4822 roundtrip!(
4823 "rt_i16",
4824 NDDataType::Int16,
4825 I16,
4826 i16,
4827 vec![-32768, -1, 1, 32767]
4828 );
4829 roundtrip!(
4830 "rt_u16",
4831 NDDataType::UInt16,
4832 U16,
4833 u16,
4834 vec![0, 1, 40000, 65535]
4835 );
4836 roundtrip!(
4837 "rt_i32",
4838 NDDataType::Int32,
4839 I32,
4840 i32,
4841 vec![i32::MIN, -1, 1, i32::MAX]
4842 );
4843 roundtrip!(
4844 "rt_u32",
4845 NDDataType::UInt32,
4846 U32,
4847 u32,
4848 vec![0, 1, 3_000_000_000, u32::MAX]
4849 );
4850 roundtrip!(
4851 "rt_i64",
4852 NDDataType::Int64,
4853 I64,
4854 i64,
4855 vec![i64::MIN, -1, 1, i64::MAX]
4856 );
4857 roundtrip!(
4858 "rt_u64",
4859 NDDataType::UInt64,
4860 U64,
4861 u64,
4862 vec![0, 1, 9_000_000_000, u64::MAX]
4863 );
4864 roundtrip!(
4865 "rt_f32",
4866 NDDataType::Float32,
4867 F32,
4868 f32,
4869 vec![-1.5, 0.0, 2.25, 3.75]
4870 );
4871 roundtrip!(
4872 "rt_f64",
4873 NDDataType::Float64,
4874 F64,
4875 f64,
4876 vec![-1.5, 0.0, 2.25, 3.75]
4877 );
4878 }
4879
4880 #[test]
4881 fn test_deflate_compressed_write() {
4882 let path = temp_path("hdf5_deflate");
4883 let mut writer = Hdf5Writer::new();
4884 writer.set_compression_type(COMPRESS_ZLIB);
4885 writer.set_z_compress_level(6);
4886
4887 let mut arr = NDArray::new(
4888 vec![NDDimension::new(64), NDDimension::new(64)],
4889 NDDataType::UInt16,
4890 );
4891 if let NDDataBuffer::U16(ref mut v) = arr.data {
4892 for i in 0..v.len() {
4893 v[i] = (i % 256) as u16;
4894 }
4895 }
4896
4897 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4898 writer.write_file(&arr).unwrap();
4899 writer.close_file().unwrap();
4900
4901 let raw_path = temp_path("hdf5_deflate_raw");
4911 {
4912 let mut raw_writer = Hdf5Writer::new();
4913 raw_writer.set_compression_type(COMPRESS_NONE);
4914 raw_writer
4915 .open_file(&raw_path, NDFileMode::Single, &arr)
4916 .unwrap();
4917 raw_writer.write_file(&arr).unwrap();
4918 raw_writer.close_file().unwrap();
4919 }
4920 let compressed_size = std::fs::metadata(&path).unwrap().len();
4921 let raw_size = std::fs::metadata(&raw_path).unwrap().len();
4922 assert!(
4923 compressed_size + 4096 < raw_size,
4924 "deflate must shrink the data chunk: compressed={compressed_size} raw={raw_size}"
4925 );
4926
4927 let h5file = H5File::open(&path).unwrap();
4928 let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
4929 let data: Vec<u16> = ds.read_raw().unwrap();
4930 assert_eq!(data.len(), 64 * 64);
4931 assert_eq!(data[0], 0);
4932 assert_eq!(data[255], 255);
4933 assert_eq!(data[256], 0);
4934
4935 std::fs::remove_file(&path).ok();
4936 }
4937
4938 #[test]
4939 fn test_lz4_compressed_write() {
4940 let path = temp_path("hdf5_lz4");
4941 let mut writer = Hdf5Writer::new();
4942 writer.set_compression_type(COMPRESS_LZ4);
4943
4944 let mut arr = NDArray::new(
4945 vec![NDDimension::new(32), NDDimension::new(32)],
4946 NDDataType::UInt8,
4947 );
4948 if let NDDataBuffer::U8(ref mut v) = arr.data {
4949 for i in 0..v.len() {
4950 v[i] = (i % 4) as u8;
4951 }
4952 }
4953
4954 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4955 writer.write_file(&arr).unwrap();
4956 writer.close_file().unwrap();
4957
4958 let h5file = H5File::open(&path).unwrap();
4959 let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
4960 let data: Vec<u8> = ds.read_raw().unwrap();
4961 assert_eq!(data.len(), 32 * 32);
4962 assert_eq!(data[0], 0);
4963 assert_eq!(data[3], 3);
4964
4965 std::fs::remove_file(&path).ok();
4966 }
4967
4968 #[test]
4969 fn test_bitshuffle_compressed_write() {
4970 let path = temp_path("hdf5_bshuf");
4971 let mut writer = Hdf5Writer::new();
4972 writer.set_compression_type(COMPRESS_BSHUF);
4973
4974 let mut arr = NDArray::new(
4975 vec![NDDimension::new(64), NDDimension::new(64)],
4976 NDDataType::UInt16,
4977 );
4978 if let NDDataBuffer::U16(ref mut v) = arr.data {
4979 for i in 0..v.len() {
4980 v[i] = (i % 8) as u16;
4981 }
4982 }
4983
4984 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4985 writer.write_file(&arr).unwrap();
4986 writer.close_file().unwrap();
4987
4988 let h5file = H5File::open(&path).unwrap();
4989 let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
4990 let data: Vec<u16> = ds.read_raw().unwrap();
4991 assert_eq!(data.len(), 64 * 64);
4992 assert_eq!(data[0], 0);
4993 assert_eq!(data[9], 1);
4994
4995 std::fs::remove_file(&path).ok();
4996 }
4997
4998 #[test]
4999 fn test_szip_uses_nearest_neighbor_mask() {
5000 let mut writer = Hdf5Writer::new();
5003 writer.set_compression_type(COMPRESS_SZIP);
5004 let pipeline = writer.build_pipeline(1).expect("szip pipeline");
5005 assert_eq!(pipeline.filters[0].cd_values[0], SZIP_NN_OPTION_MASK);
5006
5007 let path = temp_path("hdf5_szip_nn");
5008 let mut arr = NDArray::new(
5009 vec![NDDimension::new(64), NDDimension::new(64)],
5010 NDDataType::UInt8,
5011 );
5012 if let NDDataBuffer::U8(ref mut v) = arr.data {
5013 for (i, e) in v.iter_mut().enumerate() {
5014 *e = (i % 13) as u8;
5015 }
5016 }
5017 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
5018 writer.write_file(&arr).unwrap();
5019 writer.close_file().unwrap();
5020
5021 let h5file = H5File::open(&path).unwrap();
5022 let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
5023 let data: Vec<u8> = ds.read_raw().unwrap();
5024 assert_eq!(data.len(), 64 * 64);
5025 assert_eq!(data[20], (20 % 13) as u8);
5026 std::fs::remove_file(&path).ok();
5027 }
5028
5029 #[test]
5030 fn test_blosc_default_shuffle_is_byte_shuffle() {
5031 let mut writer = Hdf5Writer::new();
5034 writer.set_compression_type(COMPRESS_BLOSC);
5035 let pipeline = writer.build_pipeline(2).expect("blosc pipeline");
5036 assert_eq!(pipeline.filters[0].cd_values[5], 1);
5037 }
5038
5039 #[test]
5040 fn test_nbit_packs_to_reduced_precision_datatype() {
5041 let mut writer = Hdf5Writer::new();
5051 writer.set_compression_type(COMPRESS_NBIT);
5052 writer.set_nbit_precision(10);
5053 writer.set_nbit_offset(0);
5054 assert!(
5055 writer.build_pipeline(2).is_none(),
5056 "N-bit packing is applied via a datatype override, not build_pipeline"
5057 );
5058
5059 let path = temp_path("hdf5_nbit_packed");
5060 let mut arr = NDArray::new(
5061 vec![NDDimension::new(8), NDDimension::new(8)],
5062 NDDataType::UInt16,
5063 );
5064 if let NDDataBuffer::U16(ref mut v) = arr.data {
5065 for (i, x) in v.iter_mut().enumerate() {
5066 *x = (i as u16 * 7) & 0x3FF; }
5068 v[0] = 0xFFFF; }
5070 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
5071 writer.write_file(&arr).unwrap();
5072 writer.close_file().unwrap();
5073
5074 let h5file = H5File::open(&path).unwrap();
5075 let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
5076
5077 match ds.datatype().unwrap() {
5080 DatatypeMessage::FixedPoint {
5081 size,
5082 bit_precision,
5083 bit_offset,
5084 signed,
5085 ..
5086 } => {
5087 assert_eq!(size, 2, "byte footprint unchanged");
5088 assert_eq!(bit_precision, 10, "precision narrowed to 10 bits");
5089 assert_eq!(bit_offset, 0);
5090 assert!(!signed, "u16 is unsigned");
5091 }
5092 other => panic!("expected reduced-precision FixedPoint, got {other:?}"),
5093 }
5094
5095 let data: Vec<u16> = ds.read_raw().unwrap();
5096 assert_eq!(data.len(), 8 * 8);
5097 assert_eq!(data[0], 0x3FF, "0xFFFF must pack to the low 10 bits");
5099 for i in 1..data.len() {
5100 assert_eq!(
5101 data[i],
5102 (i as u16 * 7) & 0x3FF,
5103 "in-range value {i} round-trips"
5104 );
5105 }
5106 std::fs::remove_file(&path).ok();
5107 }
5108
5109 #[test]
5110 fn test_chunk_geometry_recorded() {
5111 let path = temp_path("hdf5_chunkgeom");
5114 let mut writer = Hdf5Writer::new();
5115 writer.set_chunk_size_auto(false);
5116 writer.set_n_row_chunks(4);
5117 writer.set_n_col_chunks(2);
5118 writer.set_n_frames_chunks(3);
5119
5120 let mut arr = NDArray::new(
5121 vec![NDDimension::new(8), NDDimension::new(8)],
5122 NDDataType::UInt16,
5123 );
5124 if let NDDataBuffer::U16(ref mut v) = arr.data {
5125 for i in 0..v.len() {
5126 v[i] = i as u16;
5127 }
5128 }
5129
5130 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5131 writer.write_file(&arr).unwrap();
5132 writer.write_file(&arr).unwrap();
5133 writer.close_file().unwrap();
5134
5135 let h5 = H5File::open(&path).unwrap();
5136 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
5137 assert_eq!(ds.shape(), vec![2, 8, 8]);
5138 let data: Vec<u16> = ds.read_raw().unwrap();
5140 assert_eq!(data.len(), 2 * 64);
5141 for i in 0..64usize {
5142 assert_eq!(data[i], i as u16, "frame0 element {}", i);
5143 assert_eq!(data[64 + i], i as u16, "frame1 element {}", i);
5144 }
5145 assert_eq!(
5147 ds.attr("HDF5_nRowChunks")
5148 .unwrap()
5149 .read_numeric::<i32>()
5150 .unwrap(),
5151 4
5152 );
5153 assert_eq!(
5154 ds.attr("HDF5_nColChunks")
5155 .unwrap()
5156 .read_numeric::<i32>()
5157 .unwrap(),
5158 2
5159 );
5160 assert_eq!(
5161 ds.attr("HDF5_nFramesChunks")
5162 .unwrap()
5163 .read_numeric::<i32>()
5164 .unwrap(),
5165 3
5166 );
5167
5168 std::fs::remove_file(&path).ok();
5169 }
5170
5171 #[test]
5172 fn test_extra_dimensions_layout() {
5173 let path = temp_path("hdf5_extradims");
5182 let mut writer = Hdf5Writer::new();
5183 writer.set_n_extra_dims(2);
5184 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");
5188 writer.set_extra_dim_name(1, "scanX");
5189 writer.set_extra_dim_name(2, "scanY");
5190
5191 let mut arr = NDArray::new(
5192 vec![NDDimension::new(4), NDDimension::new(4)],
5193 NDDataType::UInt16,
5194 );
5195 for f in 0..24u16 {
5196 if let NDDataBuffer::U16(ref mut v) = arr.data {
5197 for x in v.iter_mut() {
5198 *x = f;
5199 }
5200 }
5201 if f == 0 {
5202 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5203 }
5204 writer.write_file(&arr).unwrap();
5205 }
5206 writer.close_file().unwrap();
5207
5208 let h5 = H5File::open(&path).unwrap();
5209 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
5210 assert_eq!(ds.shape(), vec![4, 3, 2, 4, 4]);
5212 let data: Vec<u16> = ds.read_raw().unwrap();
5213 assert_eq!(data.len(), 24 * 16);
5214 for f in 0..24usize {
5215 for i in 0..16usize {
5216 assert_eq!(data[f * 16 + i], f as u16, "frame {} elem {}", f, i);
5217 }
5218 }
5219 assert_eq!(
5221 ds.attr("HDF5_nExtraDims")
5222 .unwrap()
5223 .read_numeric::<i32>()
5224 .unwrap(),
5225 2
5226 );
5227 assert_eq!(
5228 ds.attr("HDF5_extraDimSize0")
5229 .unwrap()
5230 .read_numeric::<i32>()
5231 .unwrap(),
5232 2
5233 );
5234 assert_eq!(
5235 ds.attr("HDF5_extraDimName0")
5236 .unwrap()
5237 .read_string()
5238 .unwrap(),
5239 "n"
5240 );
5241
5242 std::fs::remove_file(&path).ok();
5243 }
5244
5245 #[test]
5246 fn test_extra_dimensions_one_virtual() {
5247 let path = temp_path("hdf5_extradims_1");
5251 let mut writer = Hdf5Writer::new();
5252 writer.set_n_extra_dims(1);
5253 writer.set_extra_dim_size(0, 2); writer.set_extra_dim_size(1, 3); let mut arr = NDArray::new(
5257 vec![NDDimension::new(4), NDDimension::new(4)],
5258 NDDataType::UInt16,
5259 );
5260 for f in 0..6u16 {
5261 if let NDDataBuffer::U16(ref mut v) = arr.data {
5262 for x in v.iter_mut() {
5263 *x = f;
5264 }
5265 }
5266 if f == 0 {
5267 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5268 }
5269 writer.write_file(&arr).unwrap();
5270 }
5271 writer.close_file().unwrap();
5272
5273 let h5 = H5File::open(&path).unwrap();
5274 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
5275 assert_eq!(ds.shape(), vec![3, 2, 4, 4]);
5276 let data: Vec<u16> = ds.read_raw().unwrap();
5277 assert_eq!(data.len(), 6 * 16);
5278 for f in 0..6usize {
5279 for i in 0..16usize {
5280 assert_eq!(data[f * 16 + i], f as u16, "frame {} elem {}", f, i);
5281 }
5282 }
5283
5284 std::fs::remove_file(&path).ok();
5285 }
5286
5287 #[test]
5288 fn test_swmr_extra_dimensions_grid_layout() {
5289 let path = temp_path("hdf5_swmr_extradims");
5296 let mut writer = Hdf5Writer::new();
5297 writer.set_swmr_mode(true);
5298 writer.set_n_extra_dims(2);
5299 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(
5304 vec![NDDimension::new(4), NDDimension::new(4)],
5305 NDDataType::UInt16,
5306 );
5307 for f in 0..24u16 {
5308 if let NDDataBuffer::U16(ref mut v) = arr.data {
5309 for x in v.iter_mut() {
5310 *x = f;
5311 }
5312 }
5313 if f == 0 {
5314 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5315 }
5316 writer.write_file(&arr).unwrap();
5317 }
5318 writer.close_file().unwrap();
5319
5320 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
5321 assert_eq!(
5322 reader
5323 .dataset_shape("entry/instrument/detector/data")
5324 .unwrap(),
5325 vec![4, 3, 2, 4, 4]
5326 );
5327 let data: Vec<u16> = reader
5328 .read_dataset("entry/instrument/detector/data")
5329 .unwrap();
5330 assert_eq!(data.len(), 24 * 16);
5331 for f in 0..24usize {
5332 for i in 0..16usize {
5333 assert_eq!(data[f * 16 + i], f as u16, "frame {} elem {}", f, i);
5334 }
5335 }
5336 std::fs::remove_file(&path).ok();
5337 }
5338
5339 #[test]
5340 fn test_swmr_extra_dimensions_grid_subtiled() {
5341 let path = temp_path("hdf5_swmr_extradims_tiled");
5347 let mut writer = Hdf5Writer::new();
5348 writer.set_swmr_mode(true);
5349 writer.set_n_extra_dims(1);
5350 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(
5355 vec![NDDimension::new(4), NDDimension::new(4)],
5356 NDDataType::UInt16,
5357 );
5358 for f in 0..6u16 {
5359 if let NDDataBuffer::U16(ref mut v) = arr.data {
5360 for (i, x) in v.iter_mut().enumerate() {
5363 *x = f * 100 + i as u16;
5364 }
5365 }
5366 if f == 0 {
5367 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5368 }
5369 writer.write_file(&arr).unwrap();
5370 }
5371 writer.close_file().unwrap();
5372
5373 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
5374 assert_eq!(
5375 reader
5376 .dataset_shape("entry/instrument/detector/data")
5377 .unwrap(),
5378 vec![3, 2, 4, 4]
5379 );
5380 let data: Vec<u16> = reader
5381 .read_dataset("entry/instrument/detector/data")
5382 .unwrap();
5383 assert_eq!(data.len(), 6 * 16);
5384 for f in 0..6usize {
5385 for i in 0..16usize {
5386 assert_eq!(
5387 data[f * 16 + i],
5388 f as u16 * 100 + i as u16,
5389 "frame {} elem {}",
5390 f,
5391 i
5392 );
5393 }
5394 }
5395 std::fs::remove_file(&path).ok();
5396 }
5397
5398 #[test]
5399 fn test_swmr_extra_dimensions_grid_partial_fill() {
5400 let path = temp_path("hdf5_swmr_extradims_partial");
5404 let mut writer = Hdf5Writer::new();
5405 writer.set_swmr_mode(true);
5406 writer.set_n_extra_dims(1);
5407 writer.set_extra_dim_size(0, 2); writer.set_extra_dim_size(1, 3); let mut arr = NDArray::new(
5411 vec![NDDimension::new(4), NDDimension::new(4)],
5412 NDDataType::UInt16,
5413 );
5414 for f in 0..4u16 {
5415 if let NDDataBuffer::U16(ref mut v) = arr.data {
5416 for x in v.iter_mut() {
5417 *x = f + 1; }
5419 }
5420 if f == 0 {
5421 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5422 }
5423 writer.write_file(&arr).unwrap();
5424 }
5425 writer.close_file().unwrap();
5426
5427 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
5428 assert_eq!(
5429 reader
5430 .dataset_shape("entry/instrument/detector/data")
5431 .unwrap(),
5432 vec![3, 2, 4, 4]
5433 );
5434 let data: Vec<u16> = reader
5435 .read_dataset("entry/instrument/detector/data")
5436 .unwrap();
5437 assert_eq!(data.len(), 6 * 16);
5438 for f in 0..6usize {
5439 let expected = if f < 4 { f as u16 + 1 } else { 0 };
5440 for i in 0..16usize {
5441 assert_eq!(data[f * 16 + i], expected, "frame {} elem {}", f, i);
5442 }
5443 }
5444 std::fs::remove_file(&path).ok();
5445 }
5446
5447 #[test]
5448 fn test_swmr_streaming() {
5449 let path = temp_path("hdf5_swmr");
5450 let mut writer = Hdf5Writer::new();
5451 writer.set_swmr_mode(true);
5452 writer.set_flush_nth_frame(2);
5453
5454 let arr = NDArray::new(
5455 vec![NDDimension::new(8), NDDimension::new(8)],
5456 NDDataType::Float32,
5457 );
5458
5459 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5460 writer.write_file(&arr).unwrap();
5461 writer.write_file(&arr).unwrap(); writer.write_file(&arr).unwrap();
5463 writer.close_file().unwrap();
5464
5465 assert_eq!(writer.frame_count(), 3);
5466
5467 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
5469 let shape = reader
5470 .dataset_shape("entry/instrument/detector/data")
5471 .unwrap();
5472 assert_eq!(shape[0], 3); assert_eq!(shape[1], 8);
5474 assert_eq!(shape[2], 8);
5475
5476 let data: Vec<f32> = reader
5477 .read_dataset("entry/instrument/detector/data")
5478 .unwrap();
5479 assert_eq!(data.len(), 3 * 8 * 8);
5480
5481 std::fs::remove_file(&path).ok();
5482 }
5483
5484 #[test]
5485 fn test_swmr_compression_is_applied() {
5486 let path = temp_path("hdf5_swmr_comp");
5490 let mut writer = Hdf5Writer::new();
5491 writer.set_swmr_mode(true);
5492 writer.set_compression_type(COMPRESS_ZLIB);
5493
5494 let arr = NDArray::new(
5495 vec![NDDimension::new(8), NDDimension::new(8)],
5496 NDDataType::UInt16,
5497 );
5498 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5499 assert!(
5500 !writer.swmr_compression_dropped(),
5501 "SWMR+ZLIB must apply compression, not drop it"
5502 );
5503 writer.write_file(&arr).unwrap();
5504 writer.write_file(&arr).unwrap();
5505 writer.close_file().unwrap();
5506
5507 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
5509 let shape = reader
5510 .dataset_shape("entry/instrument/detector/data")
5511 .unwrap();
5512 assert_eq!(shape, vec![2, 8, 8]);
5513 let data: Vec<u16> = reader
5514 .read_dataset("entry/instrument/detector/data")
5515 .unwrap();
5516 assert_eq!(data.len(), 2 * 8 * 8);
5517
5518 std::fs::remove_file(&path).ok();
5519 }
5520
5521 #[test]
5522 fn test_layout_xml_param() {
5523 let mut writer = Hdf5Writer::new();
5525 let dir = std::env::temp_dir();
5526 let good = dir.join("adcore_layout_good.xml");
5527 std::fs::write(
5528 &good,
5529 r#"<hdf5_layout><group name="entry"><dataset name="data" source="detector" det_default="true"/></group></hdf5_layout>"#,
5530 )
5531 .unwrap();
5532 assert!(writer.set_layout_filename(good.to_str().unwrap()));
5533 assert!(writer.layout_valid);
5534 assert!(writer.layout_error.is_empty());
5535
5536 let bad = dir.join("adcore_layout_bad.xml");
5537 std::fs::write(&bad, r#"<not_a_layout/>"#).unwrap();
5538 assert!(!writer.set_layout_filename(bad.to_str().unwrap()));
5539 assert!(!writer.layout_valid);
5540 assert!(!writer.layout_error.is_empty());
5541
5542 std::fs::remove_file(&good).ok();
5543 std::fs::remove_file(&bad).ok();
5544 }
5545
5546 #[test]
5547 fn test_layout_xml_places_dataset_in_nested_tree() {
5548 let dir = std::env::temp_dir();
5554 let layout = dir.join("adcore_layout_nested.xml");
5555 std::fs::write(
5556 &layout,
5557 r#"<hdf5_layout>
5558 <group name="entry">
5559 <group name="instrument">
5560 <group name="detector">
5561 <dataset name="data" source="detector" det_default="true">
5562 <attribute name="signal" source="constant" value="1" type="int"/>
5563 </dataset>
5564 </group>
5565 <group name="NDAttributes" ndattr_default="true"/>
5566 <group name="performance">
5567 <dataset name="timestamp"/>
5568 </group>
5569 </group>
5570 </group>
5571 </hdf5_layout>"#,
5572 )
5573 .unwrap();
5574
5575 let path = temp_path("hdf5_layout_nested");
5576 let mut writer = Hdf5Writer::new();
5577 writer.set_store_performance(true);
5578 assert!(
5579 writer.set_layout_filename(layout.to_str().unwrap()),
5580 "layout XML must parse: {}",
5581 writer.layout_error
5582 );
5583
5584 let mk = |fill: f64| {
5585 let mut arr = NDArray::new(
5586 vec![NDDimension::new(4), NDDimension::new(4)],
5587 NDDataType::UInt16,
5588 );
5589 arr.attributes.add(NDAttribute::new_static(
5590 "exposure",
5591 "",
5592 NDAttrSource::Driver,
5593 NDAttrValue::Float64(fill),
5594 ));
5595 arr
5596 };
5597
5598 let a0 = mk(0.5);
5599 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
5600 writer.write_file(&a0).unwrap();
5601 writer.write_file(&mk(0.75)).unwrap();
5602 writer.close_file().unwrap();
5603
5604 let h5 = H5File::open(&path).unwrap();
5605 let names = h5.dataset_names();
5606 assert!(
5608 names.contains(&"entry/instrument/detector/data".to_string()),
5609 "image dataset must be at the nested layout path; got {:?}",
5610 names
5611 );
5612 assert!(
5613 !names.contains(&"data".to_string()),
5614 "must not also write a flat-root `data` dataset"
5615 );
5616 let img = h5.dataset("entry/instrument/detector/data").unwrap();
5617 assert_eq!(img.shape(), vec![2, 4, 4]);
5618 assert_eq!(
5620 img.attr("signal").unwrap().read_numeric::<i64>().unwrap(),
5621 1
5622 );
5623 assert!(
5625 names.contains(&"entry/instrument/NDAttributes/exposure".to_string()),
5626 "NDAttribute dataset must be under the layout ndattr group; got {:?}",
5627 names
5628 );
5629 assert!(
5631 names.contains(&"entry/instrument/performance/timestamp".to_string()),
5632 "performance dataset must be under the layout group; got {:?}",
5633 names
5634 );
5635
5636 drop(h5);
5638 let mut reader = Hdf5Writer::new();
5639 assert!(reader.set_layout_filename(layout.to_str().unwrap()));
5640 reader.current_path = Some(path.clone());
5641 let read_arr = reader.read_file().unwrap();
5642 assert_eq!(read_arr.dims.len(), 3);
5643
5644 std::fs::remove_file(&path).ok();
5645 std::fs::remove_file(&layout).ok();
5646 }
5647
5648 #[test]
5656 fn test_detector_data_destination_routes_by_attribute() {
5657 let dir = std::env::temp_dir();
5658 let layout = dir.join("adcore_layout_multidet.xml");
5659 std::fs::write(
5660 &layout,
5661 r#"<hdf5_layout>
5662 <global name="detector_data_destination" ndattribute="dest"/>
5663 <group name="entry">
5664 <dataset name="data1" source="detector" det_default="true"/>
5665 <dataset name="data2" source="detector"/>
5666 </group>
5667 </hdf5_layout>"#,
5668 )
5669 .unwrap();
5670
5671 let path = temp_path("hdf5_multidet_route");
5672 let mut writer = Hdf5Writer::new();
5673 writer.store_attributes = false;
5675 assert!(
5676 writer.set_layout_filename(layout.to_str().unwrap()),
5677 "layout XML must parse: {}",
5678 writer.layout_error
5679 );
5680
5681 let mk = |val: u16, dest: Option<&str>| {
5684 let mut arr = NDArray::new(
5685 vec![NDDimension::new(2), NDDimension::new(2)],
5686 NDDataType::UInt16,
5687 );
5688 if let NDDataBuffer::U16(ref mut v) = arr.data {
5689 for p in v.iter_mut() {
5690 *p = val;
5691 }
5692 }
5693 if let Some(d) = dest {
5694 arr.attributes.add(NDAttribute::new_static(
5695 "dest",
5696 "",
5697 NDAttrSource::Driver,
5698 NDAttrValue::String(d.to_string()),
5699 ));
5700 }
5701 arr
5702 };
5703
5704 let f0 = mk(10, None);
5710 writer.open_file(&path, NDFileMode::Stream, &f0).unwrap();
5711 writer.write_file(&f0).unwrap();
5712 writer.write_file(&mk(11, Some("/entry/data2"))).unwrap();
5713 writer.write_file(&mk(12, Some("/entry/data2"))).unwrap();
5714 writer.write_file(&mk(13, Some("/nonexistent"))).unwrap();
5715 writer.write_file(&mk(14, Some("/entry/data1"))).unwrap();
5716 writer.close_file().unwrap();
5717
5718 let h5 = H5File::open(&path).unwrap();
5719 let names = h5.dataset_names();
5720 assert!(
5721 names.contains(&"entry/data1".to_string())
5722 && names.contains(&"entry/data2".to_string()),
5723 "both detector datasets must exist; got {:?}",
5724 names
5725 );
5726
5727 let d1 = h5.dataset("entry/data1").unwrap();
5729 assert_eq!(d1.shape(), vec![3, 2, 2], "default dataset extent");
5730 let v1: Vec<u16> = d1.read_raw().unwrap();
5731 assert_eq!(
5732 v1,
5733 vec![10, 10, 10, 10, 13, 13, 13, 13, 14, 14, 14, 14],
5734 "default dataset must hold the default-routed frames in order"
5735 );
5736
5737 let d2 = h5.dataset("entry/data2").unwrap();
5738 assert_eq!(d2.shape(), vec![2, 2, 2], "routed dataset extent");
5739 let v2: Vec<u16> = d2.read_raw().unwrap();
5740 assert_eq!(
5741 v2,
5742 vec![11, 11, 11, 11, 12, 12, 12, 12],
5743 "routed dataset must hold only the frames addressed to it"
5744 );
5745
5746 drop(h5);
5747 std::fs::remove_file(&path).ok();
5748 std::fs::remove_file(&layout).ok();
5749 }
5750
5751 #[test]
5755 fn test_detector_data_destination_non_string_attribute_errors() {
5756 let dir = std::env::temp_dir();
5757 let layout = dir.join("adcore_layout_multidet_err.xml");
5758 std::fs::write(
5759 &layout,
5760 r#"<hdf5_layout>
5761 <global name="detector_data_destination" ndattribute="dest"/>
5762 <group name="entry">
5763 <dataset name="data1" source="detector" det_default="true"/>
5764 <dataset name="data2" source="detector"/>
5765 </group>
5766 </hdf5_layout>"#,
5767 )
5768 .unwrap();
5769
5770 let path = temp_path("hdf5_multidet_err");
5771 let mut writer = Hdf5Writer::new();
5772 writer.store_attributes = false;
5773 assert!(writer.set_layout_filename(layout.to_str().unwrap()));
5774
5775 let mut arr = NDArray::new(
5776 vec![NDDimension::new(2), NDDimension::new(2)],
5777 NDDataType::UInt16,
5778 );
5779 arr.attributes.add(NDAttribute::new_static(
5780 "dest",
5781 "",
5782 NDAttrSource::Driver,
5783 NDAttrValue::Float64(2.0),
5784 ));
5785
5786 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5787 assert!(
5788 writer.write_file(&arr).is_err(),
5789 "a non-string destination attribute must abort the write"
5790 );
5791 writer.close_file().ok();
5792
5793 std::fs::remove_file(&path).ok();
5794 std::fs::remove_file(&layout).ok();
5795 }
5796
5797 #[test]
5798 fn test_layout_hardlink_is_materialised() {
5799 let dir = std::env::temp_dir();
5805 let layout = dir.join("adcore_layout_hardlink.xml");
5806 std::fs::write(
5807 &layout,
5808 r#"<hdf5_layout>
5809 <group name="entry">
5810 <group name="data">
5811 <dataset name="data" source="detector" det_default="true"/>
5812 <hardlink name="data_alias" target="/entry/data/data"/>
5813 </group>
5814 </group>
5815 </hdf5_layout>"#,
5816 )
5817 .unwrap();
5818
5819 let path = temp_path("hdf5_layout_hardlink");
5820 let mut writer = Hdf5Writer::new();
5821 assert!(
5822 writer.set_layout_filename(layout.to_str().unwrap()),
5823 "layout XML must parse: {}",
5824 writer.layout_error
5825 );
5826
5827 let arr = NDArray::new(
5828 vec![NDDimension::new(4), NDDimension::new(4)],
5829 NDDataType::UInt16,
5830 );
5831 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5832 writer.write_file(&arr).unwrap();
5833 writer.close_file().unwrap();
5834
5835 let h5 = H5File::open(&path).unwrap();
5836 let names = h5.dataset_names();
5837 assert!(
5839 names.contains(&"entry/data/data".to_string()),
5840 "image dataset must exist at the layout path; got {:?}",
5841 names
5842 );
5843 assert!(
5845 names.contains(&"entry/data/data_alias".to_string()),
5846 "layout <hardlink> must be materialised as a hard link; got {:?}",
5847 names
5848 );
5849 let alias = h5.dataset("entry/data/data_alias").unwrap();
5851 let orig = h5.dataset("entry/data/data").unwrap();
5852 assert_eq!(alias.shape(), orig.shape());
5853
5854 drop(h5);
5855 std::fs::remove_file(&path).ok();
5856 std::fs::remove_file(&layout).ok();
5857 }
5858
5859 #[test]
5860 fn test_swmr_layout_hardlink_is_materialised() {
5861 let dir = std::env::temp_dir();
5873 let layout = dir.join("adcore_swmr_layout_hardlink.xml");
5874 std::fs::write(
5875 &layout,
5876 r#"<hdf5_layout>
5877 <group name="entry">
5878 <group name="data">
5879 <dataset name="data" source="detector" det_default="true"/>
5880 <hardlink name="data_alias" target="/entry/data/data"/>
5881 </group>
5882 </group>
5883 </hdf5_layout>"#,
5884 )
5885 .unwrap();
5886
5887 let path = temp_path("hdf5_swmr_layout_hardlink");
5888 let mut writer = Hdf5Writer::new();
5889 writer.set_swmr_mode(true);
5890 assert!(
5891 writer.set_layout_filename(layout.to_str().unwrap()),
5892 "layout XML must parse: {}",
5893 writer.layout_error
5894 );
5895
5896 let mut arr = NDArray::new(
5897 vec![NDDimension::new(4), NDDimension::new(4)],
5898 NDDataType::UInt16,
5899 );
5900 if let NDDataBuffer::U16(ref mut v) = arr.data {
5901 for (i, x) in v.iter_mut().enumerate() {
5902 *x = i as u16;
5903 }
5904 }
5905 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5906 assert!(
5907 writer.is_swmr_active(),
5908 "writer must be in SWMR mode for this test"
5909 );
5910 writer.write_file(&arr).unwrap();
5911 writer.write_file(&arr).unwrap();
5912 writer.close_file().unwrap();
5913
5914 let h5 = H5File::open(&path).unwrap();
5915 let names = h5.dataset_names();
5916 assert!(
5918 names.contains(&"entry/data/data".to_string()),
5919 "SWMR image dataset must exist at the nested layout path; got {:?}",
5920 names
5921 );
5922 assert!(
5924 names.contains(&"entry/data/data_alias".to_string()),
5925 "SWMR layout <hardlink> must be materialised as a hard link; got {:?}",
5926 names
5927 );
5928 let alias = h5.dataset("entry/data/data_alias").unwrap();
5930 let orig = h5.dataset("entry/data/data").unwrap();
5931 assert_eq!(alias.shape(), orig.shape());
5932 assert_eq!(orig.shape(), vec![2, 4, 4]);
5933
5934 drop(h5);
5935 std::fs::remove_file(&path).ok();
5936 std::fs::remove_file(&layout).ok();
5937 }
5938
5939 #[test]
5940 fn test_swmr_layout_nested_dataset_placement() {
5941 let dir = std::env::temp_dir();
5948 let layout = dir.join("adcore_swmr_layout_nested.xml");
5949 std::fs::write(
5950 &layout,
5951 r#"<hdf5_layout>
5952 <group name="entry">
5953 <group name="instrument">
5954 <group name="detector">
5955 <dataset name="data" source="detector" det_default="true">
5956 <attribute name="signal" source="constant" value="1" type="int"/>
5957 </dataset>
5958 <hardlink name="data_alias" target="/entry/instrument/detector/data"/>
5959 </group>
5960 </group>
5961 <group name="empty_placeholder"/>
5962 </group>
5963 </hdf5_layout>"#,
5964 )
5965 .unwrap();
5966
5967 let path = temp_path("hdf5_swmr_layout_nested");
5968 let mut writer = Hdf5Writer::new();
5969 writer.set_swmr_mode(true);
5970 assert!(
5971 writer.set_layout_filename(layout.to_str().unwrap()),
5972 "layout XML must parse: {}",
5973 writer.layout_error
5974 );
5975
5976 let mut arr = NDArray::new(
5977 vec![NDDimension::new(4), NDDimension::new(4)],
5978 NDDataType::UInt16,
5979 );
5980 if let NDDataBuffer::U16(ref mut v) = arr.data {
5981 for (i, x) in v.iter_mut().enumerate() {
5982 *x = (i * 3) as u16;
5983 }
5984 }
5985 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5986 assert!(
5987 writer.is_swmr_active(),
5988 "writer must be in SWMR mode for this test"
5989 );
5990 writer.write_file(&arr).unwrap();
5991 writer.write_file(&arr).unwrap();
5992 writer.close_file().unwrap();
5993
5994 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
5997 let names = reader.dataset_names();
5998 assert!(
6000 names.contains(&"entry/instrument/detector/data".to_string()),
6001 "SWMR image dataset must live at the nested layout path; got {:?}",
6002 names
6003 );
6004 assert!(
6005 !names.contains(&"data".to_string()),
6006 "SWMR image dataset must NOT remain at the flat root; got {:?}",
6007 names
6008 );
6009 assert!(
6011 reader.has_group("entry/empty_placeholder"),
6012 "empty layout group must be materialised; groups {:?}",
6013 reader.group_paths()
6014 );
6015 assert!(
6017 names.contains(&"entry/instrument/detector/data_alias".to_string()),
6018 "SWMR layout <hardlink> must resolve to the nested dataset; got {:?}",
6019 names
6020 );
6021 let nested = reader
6022 .dataset_shape("entry/instrument/detector/data")
6023 .unwrap();
6024 let alias = reader
6025 .dataset_shape("entry/instrument/detector/data_alias")
6026 .unwrap();
6027 assert_eq!(nested, vec![2, 4, 4]);
6028 assert_eq!(alias, nested, "hardlink alias must share the target shape");
6029 let via_nested: Vec<u16> = reader
6031 .read_dataset("entry/instrument/detector/data")
6032 .unwrap();
6033 let via_alias: Vec<u16> = reader
6034 .read_dataset("entry/instrument/detector/data_alias")
6035 .unwrap();
6036 assert_eq!(via_nested, via_alias);
6037 assert_eq!(via_nested.len(), 2 * 4 * 4);
6038 let attr_names = reader
6042 .dataset_attr_names("entry/instrument/detector/data")
6043 .unwrap();
6044 for expected in [
6045 "signal",
6046 "NDArrayNumDims",
6047 "NDArrayDimOffset",
6048 "NDArrayDimBinning",
6049 "NDArrayDimReverse",
6050 ] {
6051 assert!(
6052 attr_names.iter().any(|n| n == expected),
6053 "streaming dataset must carry the {expected} attribute; got {attr_names:?}",
6054 );
6055 }
6056
6057 drop(reader);
6058 std::fs::remove_file(&path).ok();
6059 std::fs::remove_file(&layout).ok();
6060 }
6061
6062 #[test]
6063 fn test_default_layout_parses_and_resolves_nexus_paths() {
6064 let layout = Hdf5Writer::default_layout();
6068 assert_eq!(
6069 layout.detector_dataset_path().as_deref(),
6070 Some("/entry/instrument/detector/data")
6071 );
6072 assert_eq!(
6073 layout.ndattr_default_group().as_deref(),
6074 Some("/entry/instrument/NDAttributes")
6075 );
6076 assert_eq!(
6077 layout.dataset_group_path("timestamp").as_deref(),
6078 Some("/entry/instrument/performance")
6079 );
6080 }
6081
6082 #[test]
6083 fn test_no_layout_uses_default_nexus_layout() {
6084 let path = temp_path("hdf5_default_layout");
6089 let mut writer = Hdf5Writer::new();
6090 let arr = NDArray::new(
6091 vec![NDDimension::new(4), NDDimension::new(4)],
6092 NDDataType::UInt8,
6093 );
6094 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
6095 writer.write_file(&arr).unwrap();
6096 writer.close_file().unwrap();
6097
6098 let h5 = H5File::open(&path).unwrap();
6099 let det = h5.dataset("entry/instrument/detector/data").unwrap();
6101 assert_eq!(det.shape(), vec![1, 4, 4]);
6102 assert!(
6103 !h5.dataset_names().contains(&"data".to_string()),
6104 "must not write a flat-root `data`; got {:?}",
6105 h5.dataset_names()
6106 );
6107 let linked = h5.dataset("entry/data/data").unwrap();
6109 assert_eq!(linked.shape(), vec![1, 4, 4]);
6110 std::fs::remove_file(&path).ok();
6111 }
6112
6113 #[test]
6114 fn test_default_layout_group_nx_class_attributes() {
6115 let path = temp_path("hdf5_nxclass");
6119 let mut writer = Hdf5Writer::new();
6120 let arr = NDArray::new(
6121 vec![NDDimension::new(4), NDDimension::new(4)],
6122 NDDataType::UInt8,
6123 );
6124 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
6125 writer.write_file(&arr).unwrap();
6126 writer.close_file().unwrap();
6127
6128 let h5 = H5File::open(&path).unwrap();
6129 let nx = |g: &str| {
6130 let mut grp = h5.root_group();
6131 for seg in g.split('/') {
6132 grp = grp.group(seg).unwrap();
6133 }
6134 grp.attr_string("NX_class").unwrap()
6135 };
6136 assert_eq!(nx("entry"), "NXentry");
6137 assert_eq!(nx("entry/instrument"), "NXinstrument");
6138 assert_eq!(nx("entry/instrument/detector"), "NXdetector");
6139 assert_eq!(nx("entry/instrument/NDAttributes"), "NXcollection");
6140 assert_eq!(nx("entry/instrument/detector/NDAttributes"), "NXcollection");
6141 assert_eq!(nx("entry/data"), "NXdata");
6142 std::fs::remove_file(&path).ok();
6143 }
6144
6145 #[test]
6146 fn test_swmr_default_layout_group_nx_class_attributes() {
6147 let path = temp_path("hdf5_swmr_nxclass");
6150 let mut writer = Hdf5Writer::new();
6151 writer.set_swmr_mode(true);
6152 let arr = NDArray::new(
6153 vec![NDDimension::new(4), NDDimension::new(4)],
6154 NDDataType::Float32,
6155 );
6156 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
6157 writer.write_file(&arr).unwrap();
6158 writer.close_file().unwrap();
6159
6160 let h5 = H5File::open(&path).unwrap();
6161 let nx = |g: &str| {
6162 let mut grp = h5.root_group();
6163 for seg in g.split('/') {
6164 grp = grp.group(seg).unwrap();
6165 }
6166 grp.attr_string("NX_class").unwrap()
6167 };
6168 assert_eq!(nx("entry"), "NXentry");
6169 assert_eq!(nx("entry/instrument/detector"), "NXdetector");
6170 assert_eq!(nx("entry/data"), "NXdata");
6171 std::fs::remove_file(&path).ok();
6172 }
6173
6174 fn ramp_u16(y: usize, x: usize) -> NDArray {
6179 let data: Vec<u16> = (0..(y * x) as u16).collect();
6180 NDArray::with_data(
6181 vec![NDDimension::new(y), NDDimension::new(x)],
6182 NDDataBuffer::U16(data),
6183 )
6184 }
6185
6186 fn u16_pixels(arr: &NDArray) -> Vec<u16> {
6187 match &arr.data {
6188 NDDataBuffer::U16(v) => v.clone(),
6189 _ => unreachable!("expected U16 buffer"),
6190 }
6191 }
6192
6193 #[test]
6194 fn test_codec_chunk_bytes_lz4_prepends_hdf5_header() {
6195 let codec = Codec {
6200 name: CodecName::LZ4,
6201 compressed_size: 4,
6202 level: 0,
6203 shuffle: 0,
6204 compressor: 0,
6205 original_data_type: NDDataType::UInt16,
6206 };
6207 let payload = [0xAAu8, 0xBB, 0xCC, 0xDD];
6208 let out = codec_chunk_bytes(&codec, 32, &payload);
6209 let mut expect = Vec::new();
6210 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);
6214 assert_eq!(out, expect);
6215 }
6216
6217 #[test]
6218 fn test_codec_chunk_bytes_verbatim_for_self_describing() {
6219 for name in [CodecName::Blosc, CodecName::JPEG] {
6223 let codec = Codec {
6224 name,
6225 compressed_size: 3,
6226 level: 0,
6227 shuffle: 0,
6228 compressor: 0,
6229 original_data_type: NDDataType::UInt8,
6230 };
6231 let payload = [1u8, 2, 3];
6232 assert_eq!(codec_chunk_bytes(&codec, 99, &payload), payload.to_vec());
6233 }
6234 }
6235
6236 #[test]
6237 fn test_codecs_match() {
6238 let a = Codec {
6239 name: CodecName::LZ4,
6240 compressed_size: 1,
6241 level: 0,
6242 shuffle: 0,
6243 compressor: 0,
6244 original_data_type: NDDataType::UInt8,
6245 };
6246 assert!(codecs_match(None, None));
6247 assert!(!codecs_match(Some(&a), None));
6248 assert!(!codecs_match(None, Some(&a)));
6249 assert!(codecs_match(Some(&a), Some(&a.clone())));
6250 let mut diff = a.clone();
6251 diff.compressor = 1;
6252 assert!(!codecs_match(Some(&a), Some(&diff)));
6253 let mut other_type = a.clone();
6255 other_type.original_data_type = NDDataType::Float64;
6256 assert!(codecs_match(Some(&a), Some(&other_type)));
6257 }
6258
6259 #[test]
6260 fn test_codec_filter_pipeline_ids() {
6261 let w = Hdf5Writer::new();
6262 let mk = |name| Codec {
6263 name,
6264 compressed_size: 0,
6265 level: 5,
6266 shuffle: 1,
6267 compressor: 2,
6268 original_data_type: NDDataType::UInt16,
6269 };
6270 assert_eq!(
6271 w.codec_filter_pipeline(&mk(CodecName::LZ4))
6272 .unwrap()
6273 .filters[0]
6274 .id,
6275 FILTER_LZ4
6276 );
6277 assert_eq!(
6278 w.codec_filter_pipeline(&mk(CodecName::BSLZ4))
6279 .unwrap()
6280 .filters[0]
6281 .id,
6282 FILTER_BSHUF
6283 );
6284 let blosc = w.codec_filter_pipeline(&mk(CodecName::Blosc)).unwrap();
6285 assert_eq!(blosc.filters[0].id, FILTER_BLOSC);
6286 assert_eq!(blosc.filters[0].cd_values[4], 5, "level");
6289 assert_eq!(blosc.filters[0].cd_values[5], 1, "shuffle");
6290 assert_eq!(blosc.filters[0].cd_values[6], 2, "compressor");
6291 assert_eq!(
6292 w.codec_filter_pipeline(&mk(CodecName::JPEG))
6293 .unwrap()
6294 .filters[0]
6295 .id,
6296 FILTER_JPEG
6297 );
6298 assert!(w.codec_filter_pipeline(&mk(CodecName::None)).is_none());
6300 assert!(w.codec_filter_pipeline(&mk(CodecName::Zlib)).is_none());
6301 assert!(w.codec_filter_pipeline(&mk(CodecName::LZ4HDF5)).is_none());
6302 }
6303
6304 #[test]
6305 fn test_compression_aware_true() {
6306 assert!(Hdf5FileProcessor::new().compression_aware());
6308 }
6309
6310 #[test]
6311 fn test_direct_chunk_write_lz4_roundtrips() {
6312 let path = temp_path("hdf5_dcw_lz4");
6313 let mut writer = Hdf5Writer::new();
6314 let orig = ramp_u16(4, 5);
6315 let expect = u16_pixels(&orig);
6316 let comp = crate::codec::compress_lz4(&orig);
6317 assert!(comp.codec.is_some());
6318 writer.open_file(&path, NDFileMode::Single, &comp).unwrap();
6319 writer.write_file(&comp).unwrap();
6320 writer.close_file().unwrap();
6321
6322 let h5 = H5File::open(&path).unwrap();
6323 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
6324 assert_eq!(ds.shape(), vec![1, 5, 4]);
6328 assert!(ds.is_chunked());
6329 let read = ds.read_raw::<u16>().unwrap();
6330 assert_eq!(
6331 read, expect,
6332 "LZ4 direct-chunk-write must reverse to the original pixels"
6333 );
6334 std::fs::remove_file(&path).ok();
6335 }
6336
6337 #[test]
6338 fn test_direct_chunk_write_blosc_roundtrips() {
6339 let path = temp_path("hdf5_dcw_blosc");
6340 let mut writer = Hdf5Writer::new();
6341 let orig = ramp_u16(4, 5);
6342 let expect = u16_pixels(&orig);
6343 let comp = crate::codec::compress_blosc(&orig, &crate::codec::BloscConfig::default());
6344 assert_eq!(comp.codec.as_ref().unwrap().name, CodecName::Blosc);
6345 writer.open_file(&path, NDFileMode::Single, &comp).unwrap();
6346 writer.write_file(&comp).unwrap();
6347 writer.close_file().unwrap();
6348
6349 let h5 = H5File::open(&path).unwrap();
6350 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
6351 assert_eq!(ds.shape(), vec![1, 5, 4]);
6352 let read = ds.read_raw::<u16>().unwrap();
6353 assert_eq!(read, expect, "BLOSC direct-chunk-write must round-trip");
6354 std::fs::remove_file(&path).ok();
6355 }
6356
6357 #[test]
6358 fn test_direct_chunk_write_bslz4_roundtrips() {
6359 let path = temp_path("hdf5_dcw_bslz4");
6368 let mut writer = Hdf5Writer::new();
6369 let orig = ramp_u16(128, 128);
6370 let expect = u16_pixels(&orig);
6371 let comp = crate::codec::compress_bslz4(&orig);
6372 assert_eq!(comp.codec.as_ref().unwrap().name, CodecName::BSLZ4);
6373 writer.open_file(&path, NDFileMode::Single, &comp).unwrap();
6374 writer.write_file(&comp).unwrap();
6375 writer.close_file().unwrap();
6376
6377 let h5 = H5File::open(&path).unwrap();
6378 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
6379 assert_eq!(ds.shape(), vec![1, 128, 128]);
6380 assert!(ds.is_chunked());
6381 assert_eq!(ds.element_size(), 2, "dataset keeps the original u16 type");
6382 let read = ds.read_raw::<u16>().unwrap();
6383 assert_eq!(read, expect, "BSLZ4 direct-chunk-write must round-trip");
6384 std::fs::remove_file(&path).ok();
6385 }
6386
6387 #[test]
6388 fn test_codec_chunk_bytes_bslz4_header() {
6389 let codec = Codec {
6394 name: CodecName::BSLZ4,
6395 compressed_size: 0,
6396 level: 0,
6397 shuffle: 0,
6398 compressor: 0,
6399 original_data_type: NDDataType::UInt16,
6400 };
6401 let payload = [0xDEu8, 0xAD, 0xBE, 0xEF];
6402 let total = 128 * 128 * 2; let out = codec_chunk_bytes(&codec, total, &payload);
6404 assert_eq!(&out[0..8], &(total as u64).to_be_bytes());
6405 assert_eq!(&out[8..12], &8192u32.to_be_bytes());
6407 assert_eq!(&out[12..], &payload, "canonical stream follows verbatim");
6408 }
6409
6410 #[test]
6411 fn test_direct_chunk_write_lz4_multiframe_extends_leading_axis() {
6412 let path = temp_path("hdf5_dcw_lz4_multi");
6413 let mut writer = Hdf5Writer::new();
6414 let frames: Vec<NDArray> = (0..3u16)
6415 .map(|f| {
6416 let data: Vec<u16> = (0..20u16).map(|i| i + f * 100).collect();
6417 NDArray::with_data(
6418 vec![NDDimension::new(4), NDDimension::new(5)],
6419 NDDataBuffer::U16(data),
6420 )
6421 })
6422 .collect();
6423 let comp: Vec<NDArray> = frames.iter().map(crate::codec::compress_lz4).collect();
6424 writer
6425 .open_file(&path, NDFileMode::Stream, &comp[0])
6426 .unwrap();
6427 for c in &comp {
6428 writer.write_file(c).unwrap();
6429 }
6430 writer.close_file().unwrap();
6431
6432 let h5 = H5File::open(&path).unwrap();
6433 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
6434 assert_eq!(ds.shape(), vec![3, 5, 4], "three compressed frames stacked");
6435 let read = ds.read_raw::<u16>().unwrap();
6436 let mut expect = Vec::new();
6437 for f in 0..3u16 {
6438 for i in 0..20u16 {
6439 expect.push(i + f * 100);
6440 }
6441 }
6442 assert_eq!(read, expect, "every frame's pixels recovered in order");
6443 std::fs::remove_file(&path).ok();
6444 }
6445
6446 #[test]
6447 fn test_direct_chunk_write_jpeg_records_chunked_dataset() {
6448 let path = temp_path("hdf5_dcw_jpeg");
6453 let mut writer = Hdf5Writer::new();
6454 let mono: Vec<u8> = (0..64u16).map(|i| (i * 3) as u8).collect();
6455 let src = NDArray::with_data(
6456 vec![NDDimension::new(8), NDDimension::new(8)],
6457 NDDataBuffer::U8(mono),
6458 );
6459 let comp = crate::codec::compress_jpeg(&src, 90).expect("jpeg encode");
6460 assert_eq!(comp.codec.as_ref().unwrap().name, CodecName::JPEG);
6461 writer.open_file(&path, NDFileMode::Single, &comp).unwrap();
6462 writer.write_file(&comp).unwrap();
6463 writer.close_file().unwrap();
6464
6465 let h5 = H5File::open(&path).unwrap();
6466 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
6467 assert_eq!(ds.shape(), vec![1, 8, 8]);
6468 assert!(ds.is_chunked());
6469 std::fs::remove_file(&path).ok();
6470 }
6471
6472 #[test]
6473 fn test_codec_change_mid_stream_errors() {
6474 let path = temp_path("hdf5_codec_change");
6478 let mut writer = Hdf5Writer::new();
6479 let f0 = crate::codec::compress_lz4(&ramp_u16(4, 5));
6480 let f1_plain = ramp_u16(4, 5); writer.open_file(&path, NDFileMode::Stream, &f0).unwrap();
6482 writer.write_file(&f0).unwrap();
6483 assert!(
6484 writer.write_file(&f1_plain).is_err(),
6485 "an uncompressed frame must be rejected for a compressed dataset"
6486 );
6487 writer.close_file().ok();
6488 std::fs::remove_file(&path).ok();
6489 }
6490
6491 #[test]
6492 fn test_swmr_rejects_compressed_array() {
6493 let path = temp_path("hdf5_swmr_compressed");
6496 let mut writer = Hdf5Writer::new();
6497 writer.set_swmr_mode(true);
6498 let comp = crate::codec::compress_lz4(&ramp_u16(4, 5));
6499 writer.open_file(&path, NDFileMode::Stream, &comp).unwrap();
6500 assert!(
6501 writer.write_file(&comp).is_err(),
6502 "SWMR mode cannot direct-chunk-write a pre-compressed array"
6503 );
6504 writer.close_file().ok();
6505 std::fs::remove_file(&path).ok();
6506 }
6507
6508 #[test]
6509 fn test_unsupported_codec_rejected_on_open() {
6510 let path = temp_path("hdf5_dcw_zlib");
6513 let mut writer = Hdf5Writer::new();
6514 let comp = crate::codec::compress_zlib(&ramp_u16(4, 5));
6515 assert_eq!(comp.codec.as_ref().unwrap().name, CodecName::Zlib);
6516 writer.open_file(&path, NDFileMode::Single, &comp).unwrap();
6517 assert!(
6518 writer.write_file(&comp).is_err(),
6519 "an unsupported (zlib) codec must be rejected, not silently mis-written"
6520 );
6521 writer.close_file().ok();
6522 std::fs::remove_file(&path).ok();
6523 }
6524}