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::finalize::Finalize;
7use ad_core_rs::ndarray::{NDArray, NDDataBuffer, NDDataType, NDDimension};
8use ad_core_rs::ndarray_pool::NDArrayPool;
9use ad_core_rs::plugin::file_base::{FileAttributes, NDFileMode, NDFileWriter};
10use ad_core_rs::plugin::file_controller::FilePluginController;
11use ad_core_rs::plugin::runtime::{
12 NDPluginProcess, ParamChangeResult, ParamUpdate, PluginParamSnapshot, ProcessResult,
13};
14use parking_lot::Mutex;
15
16use rust_hdf5::H5File;
17use rust_hdf5::format::messages::datatype::{ByteOrder, DatatypeMessage};
18use rust_hdf5::format::messages::filter::{
19 FILTER_BLOSC, FILTER_BSHUF, FILTER_JPEG, FILTER_SZIP, Filter, FilterPipeline,
20};
21use rust_hdf5::swmr::SwmrFileWriter;
22
23use crate::hdf5_layout::Hdf5Layout;
24
25const COMPRESS_NONE: i32 = 0;
27const COMPRESS_NBIT: i32 = 1;
28const COMPRESS_SZIP: i32 = 2;
29const COMPRESS_ZLIB: i32 = 3;
30const COMPRESS_BLOSC: i32 = 4;
31const COMPRESS_BSHUF: i32 = 5;
32const COMPRESS_LZ4: i32 = 6;
33const COMPRESS_JPEG: i32 = 7;
34
35const SZIP_NN_OPTION_MASK: u32 = 32;
38
39const BLOSC_LZ: i32 = 0;
41const BLOSC_LZ4: i32 = 1;
42const BLOSC_LZ4HC: i32 = 2;
43const BLOSC_SNAPPY: i32 = 3;
44const BLOSC_ZLIB: i32 = 4;
45const BLOSC_ZSTD: i32 = 5;
46
47const MAX_EXTRA_DIMS: usize = 10;
49
50const DTYPE_ATTR: &str = "NDArrayDataType";
53
54const DEFAULT_LAYOUT_XML: &str = r#"
61<hdf5_layout>
62<group name="entry">
63 <attribute name="NX_class" source="constant" value="NXentry" type="string"></attribute>
64 <group name="instrument">
65 <attribute name="NX_class" source="constant" value="NXinstrument" type="string"></attribute>
66 <group name="detector">
67 <attribute name="NX_class" source="constant" value="NXdetector" type="string"></attribute>
68 <dataset name="data" source="detector" det_default="true">
69 <attribute name="signal" source="constant" value="1" type="int"></attribute>
70 </dataset>
71 <group name="NDAttributes">
72 <attribute name="NX_class" source="constant" value="NXcollection" type="string"></attribute>
73 <dataset name="ColorMode" source="ndattribute" ndattribute="ColorMode"></dataset>
74 </group>
75 </group>
76 <group name="NDAttributes" ndattr_default="true">
77 <attribute name="NX_class" source="constant" value="NXcollection" type="string"></attribute>
78 </group>
79 <group name="performance">
80 <dataset name="timestamp"></dataset>
81 </group>
82 </group>
83 <group name="data">
84 <attribute name="NX_class" source="constant" value="NXdata" type="string"></attribute>
85 <hardlink name="data" target="/entry/instrument/detector/data"></hardlink>
86 </group>
87</group>
88</hdf5_layout>
89"#;
90
91#[derive(Clone)]
93struct ChunkConfig {
94 auto: bool,
97 n_row_chunks: usize,
98 n_col_chunks: usize,
99 n_frames_chunks: usize,
100 ndattr_chunk: usize,
103}
104
105impl Default for ChunkConfig {
106 fn default() -> Self {
107 Self {
108 auto: true,
109 n_row_chunks: 0,
110 n_col_chunks: 0,
111 n_frames_chunks: 1,
112 ndattr_chunk: 0,
113 }
114 }
115}
116
117#[derive(Clone, Default)]
119struct ExtraDim {
120 size: usize,
121 name: String,
122}
123
124struct AttributeDataset {
128 name: String,
129 description: String,
134 source: String,
135 source_type: String,
136 data_type: NDAttrDataType,
137 undefined: bool,
144 buffer: Vec<u8>,
146 frames: usize,
147}
148
149fn nd_attr_source_type_string(source: &NDAttrSource) -> &'static str {
152 match source {
153 NDAttrSource::Driver => "NDAttrSourceDriver",
154 NDAttrSource::Param { .. } => "NDAttrSourceParam",
155 NDAttrSource::EpicsPV(_) => "NDAttrSourceEPICSPV",
156 NDAttrSource::Function(_) => "NDAttrSourceFunct",
157 NDAttrSource::Constant(_) => "NDAttrSourceConst",
158 NDAttrSource::Undefined => "Undefined",
159 }
160}
161
162impl AttributeDataset {
163 fn new(attr: &NDAttribute) -> Self {
164 let undefined = matches!(attr.value, NDAttrValue::Undefined);
174 Self {
175 name: attr.name.clone(),
176 description: attr.description.clone(),
177 source: attr.source.source_string().to_string(),
178 source_type: nd_attr_source_type_string(&attr.source).to_string(),
179 data_type: if undefined {
180 NDAttrDataType::Float32
181 } else {
182 attr.value.data_type()
183 },
184 undefined,
185 buffer: Vec::new(),
186 frames: 0,
187 }
188 }
189
190 fn element_size(&self) -> usize {
193 match self.data_type {
194 NDAttrDataType::Int8 | NDAttrDataType::UInt8 => 1,
195 NDAttrDataType::Int16 | NDAttrDataType::UInt16 => 2,
196 NDAttrDataType::Int32 | NDAttrDataType::UInt32 | NDAttrDataType::Float32 => 4,
197 NDAttrDataType::Int64 | NDAttrDataType::UInt64 | NDAttrDataType::Float64 => 8,
198 NDAttrDataType::String => MAX_ATTRIBUTE_STRING_SIZE,
199 }
200 }
201
202 fn push(&mut self, value: &NDAttrValue) {
204 let es = self.element_size();
205 let mut bytes = vec![0u8; es];
206 if self.undefined {
207 bytes.copy_from_slice(&f32::NAN.to_le_bytes());
214 self.buffer.extend_from_slice(&bytes);
215 self.frames += 1;
216 return;
217 }
218 match self.data_type {
219 NDAttrDataType::Int8 => bytes[0] = value.as_i64().unwrap_or(0) as i8 as u8,
220 NDAttrDataType::UInt8 => bytes[0] = value.as_i64().unwrap_or(0) as u8,
221 NDAttrDataType::Int16 => {
222 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0) as i16).to_le_bytes())
223 }
224 NDAttrDataType::UInt16 => {
225 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0) as u16).to_le_bytes())
226 }
227 NDAttrDataType::Int32 => {
228 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0) as i32).to_le_bytes())
229 }
230 NDAttrDataType::UInt32 => {
231 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0) as u32).to_le_bytes())
232 }
233 NDAttrDataType::Int64 => {
234 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0)).to_le_bytes())
235 }
236 NDAttrDataType::UInt64 => {
237 bytes.copy_from_slice(&(value.as_i64().unwrap_or(0) as u64).to_le_bytes())
238 }
239 NDAttrDataType::Float32 => {
240 bytes.copy_from_slice(&(value.as_f64().unwrap_or(0.0) as f32).to_le_bytes())
241 }
242 NDAttrDataType::Float64 => {
243 bytes.copy_from_slice(&(value.as_f64().unwrap_or(0.0)).to_le_bytes())
244 }
245 NDAttrDataType::String => {
246 let s = value.as_string();
247 let src = s.as_bytes();
248 let n = src.len().min(es - 1);
249 bytes[..n].copy_from_slice(&src[..n]);
250 }
251 }
252 self.buffer.extend_from_slice(&bytes);
253 self.frames += 1;
254 }
255}
256
257const MAX_ATTRIBUTE_STRING_SIZE: usize = 256;
260
261#[derive(Clone, Copy)]
272#[repr(transparent)]
273struct FixedStr256([u8; MAX_ATTRIBUTE_STRING_SIZE]);
274
275impl rust_hdf5::types::H5Type for FixedStr256 {
276 fn hdf5_type() -> rust_hdf5::format::messages::datatype::DatatypeMessage {
277 rust_hdf5::format::messages::datatype::DatatypeMessage::fixed_string(
278 MAX_ATTRIBUTE_STRING_SIZE as u32,
279 )
280 }
281
282 fn element_size() -> usize {
283 MAX_ATTRIBUTE_STRING_SIZE
284 }
285}
286
287struct DetectorDataset {
294 ds: rust_hdf5::H5Dataset,
297 frame_count: usize,
299 frame_band: Vec<Vec<u8>>,
302}
303
304enum Hdf5Handle {
306 Standard {
307 file: H5File,
308 detectors: std::collections::HashMap<String, DetectorDataset>,
312 },
313 Swmr {
314 writer: Box<SwmrFileWriter>,
316 ds_index: usize,
317 compression_dropped: bool,
320 grid: Option<SwmrGridLayout>,
325 },
326}
327
328struct SwmrGridLayout {
335 leading: Vec<usize>,
337 frame_dims: Vec<usize>,
339 chunk: Vec<usize>,
341 elem_size: usize,
343}
344
345pub struct Hdf5Writer {
347 current_path: Option<PathBuf>,
348 handle: Option<Hdf5Handle>,
349 frame_count: usize,
354 dataset_name: String,
355 open_data_type: Option<NDDataType>,
357 open_frame_dims: Option<Vec<usize>>,
359 open_codec: Option<Codec>,
364 compression_type: i32,
366 z_compress_level: u32,
367 szip_num_pixels: u32,
368 nbit_precision: u32,
369 nbit_offset: u32,
370 jpeg_quality: u32,
371 blosc_shuffle_type: i32,
372 blosc_compressor: i32,
373 blosc_compress_level: u32,
374 chunk: ChunkConfig,
376 open_mode: NDFileMode,
381 num_capture: usize,
382 n_extra_dims: usize,
383 extra_dims: [ExtraDim; MAX_EXTRA_DIMS],
384 fill_value: f64,
385 dim_att_datasets: bool,
386 swmr_mode: bool,
388 flush_nth_frame: usize,
389 pub swmr_cb_counter: u32,
390 pub store_attributes: bool,
392 pub store_performance: bool,
393 fsync_on_close: bool,
400 pub total_runtime: f64,
401 pub total_bytes: u64,
402 perf_rows: Vec<[f64; 5]>,
405 perf_prev: Option<std::time::Instant>,
406 perf_first: Option<std::time::Instant>,
407 attr_datasets: Vec<AttributeDataset>,
409 file_attributes: FileAttributes,
412 layout_filename: Option<PathBuf>,
414 layout: Option<Hdf5Layout>,
415 pub layout_valid: bool,
416 pub layout_error: String,
417 resolved_dataset_path: String,
422 resolved_ndattr_group: String,
425 resolved_perf_group: String,
427 ndattr_first_values: std::collections::HashMap<String, NDAttrValue>,
433 ndattr_last_values: std::collections::HashMap<String, NDAttrValue>,
434 ndattr_element_names: Vec<String>,
437}
438
439impl Hdf5Writer {
440 pub fn new() -> Self {
441 Self {
442 current_path: None,
443 handle: None,
444 frame_count: 0,
445 dataset_name: "data".to_string(),
446 open_data_type: None,
447 open_frame_dims: None,
448 open_codec: None,
449 compression_type: 0,
450 z_compress_level: 6,
451 szip_num_pixels: 16,
452 nbit_precision: 8,
455 nbit_offset: 0,
456 jpeg_quality: 90,
457 blosc_shuffle_type: 1,
459 blosc_compressor: 0,
460 blosc_compress_level: 5,
461 chunk: ChunkConfig::default(),
462 open_mode: NDFileMode::Single,
463 num_capture: 1,
464 n_extra_dims: 0,
465 extra_dims: Default::default(),
466 fill_value: 0.0,
467 dim_att_datasets: false,
468 swmr_mode: false,
469 flush_nth_frame: 0,
470 swmr_cb_counter: 0,
471 store_attributes: true,
472 store_performance: false,
473 fsync_on_close: Self::env_fsync_on_close(),
474 total_runtime: 0.0,
475 total_bytes: 0,
476 perf_rows: Vec::new(),
477 perf_prev: None,
478 perf_first: None,
479 attr_datasets: Vec::new(),
480 file_attributes: FileAttributes::default(),
481 layout_filename: None,
482 layout: Some(Self::default_layout()),
484 layout_valid: false,
485 layout_error: String::new(),
486 resolved_dataset_path: "data".to_string(),
487 resolved_ndattr_group: String::new(),
488 resolved_perf_group: String::new(),
489 ndattr_first_values: std::collections::HashMap::new(),
490 ndattr_last_values: std::collections::HashMap::new(),
491 ndattr_element_names: Vec::new(),
492 }
493 }
494
495 fn env_fsync_on_close() -> bool {
497 Self::parse_fsync_on_close_env(std::env::var("AD_HDF5_FSYNC_ON_CLOSE").ok().as_deref())
498 }
499
500 fn parse_fsync_on_close_env(v: Option<&str>) -> bool {
505 match v {
506 Some(s) => !matches!(
507 s.trim().to_ascii_lowercase().as_str(),
508 "0" | "false" | "no" | "off"
509 ),
510 None => true,
511 }
512 }
513
514 pub fn set_dataset_name(&mut self, name: &str) {
515 self.dataset_name = name.to_string();
516 }
517
518 pub fn set_compression_type(&mut self, v: i32) {
519 self.compression_type = v;
520 }
521
522 pub fn set_z_compress_level(&mut self, v: u32) {
523 self.z_compress_level = v;
524 }
525
526 pub fn set_szip_num_pixels(&mut self, v: u32) {
527 self.szip_num_pixels = v;
528 }
529
530 pub fn set_blosc_shuffle_type(&mut self, v: i32) {
531 self.blosc_shuffle_type = v;
532 }
533
534 pub fn set_blosc_compressor(&mut self, v: i32) {
535 self.blosc_compressor = v;
536 }
537
538 pub fn set_blosc_compress_level(&mut self, v: u32) {
539 self.blosc_compress_level = v;
540 }
541
542 pub fn set_nbit_precision(&mut self, v: u32) {
543 self.nbit_precision = v;
544 }
545
546 pub fn set_nbit_offset(&mut self, v: u32) {
547 self.nbit_offset = v;
548 }
549
550 pub fn set_jpeg_quality(&mut self, v: u32) {
551 self.jpeg_quality = v;
552 }
553
554 pub fn set_store_attributes(&mut self, v: bool) {
555 self.store_attributes = v;
556 }
557
558 pub fn set_store_performance(&mut self, v: bool) {
559 self.store_performance = v;
560 }
561
562 pub fn set_swmr_mode(&mut self, v: bool) {
563 self.swmr_mode = v;
564 }
565
566 pub fn set_flush_nth_frame(&mut self, v: usize) {
567 self.flush_nth_frame = v;
568 }
569
570 pub fn set_chunk_size_auto(&mut self, v: bool) {
571 self.chunk.auto = v;
572 }
573
574 pub fn set_n_row_chunks(&mut self, v: usize) {
575 self.chunk.n_row_chunks = v;
576 }
577
578 pub fn set_n_col_chunks(&mut self, v: usize) {
579 self.chunk.n_col_chunks = v;
580 }
581
582 pub fn set_n_frames_chunks(&mut self, v: usize) {
583 self.chunk.n_frames_chunks = v;
584 }
585
586 pub fn set_ndattr_chunk(&mut self, v: usize) {
587 self.chunk.ndattr_chunk = v;
590 }
591
592 pub fn set_n_extra_dims(&mut self, v: usize) {
593 self.n_extra_dims = v.min(MAX_EXTRA_DIMS - 1);
599 }
600
601 pub fn set_extra_dim_size(&mut self, idx: usize, size: usize) {
602 if idx < MAX_EXTRA_DIMS {
603 self.extra_dims[idx].size = size;
604 }
605 }
606
607 pub fn set_extra_dim_name(&mut self, idx: usize, name: &str) {
608 if idx < MAX_EXTRA_DIMS {
609 self.extra_dims[idx].name = name.to_string();
610 }
611 }
612
613 pub fn set_fill_value(&mut self, v: f64) {
614 self.fill_value = v;
615 }
616
617 pub fn set_dim_att_datasets(&mut self, v: bool) {
618 self.dim_att_datasets = v;
619 }
620
621 fn default_layout() -> Hdf5Layout {
625 Hdf5Layout::parse(DEFAULT_LAYOUT_XML).expect("built-in default layout must parse")
626 }
627
628 pub fn set_layout_filename(&mut self, path: &str) -> bool {
631 if path.trim().is_empty() {
632 self.layout_filename = None;
636 self.layout = Some(Self::default_layout());
637 self.layout_valid = false;
638 self.layout_error.clear();
639 return true;
640 }
641 let p = PathBuf::from(path);
642 match Hdf5Layout::from_file(&p) {
643 Ok(layout) => {
644 self.layout_filename = Some(p);
645 self.layout = Some(layout);
646 self.layout_valid = true;
647 self.layout_error.clear();
648 true
649 }
650 Err(e) => {
651 self.layout_filename = Some(p);
652 self.layout = None;
653 self.layout_valid = false;
654 self.layout_error = e.0;
655 false
656 }
657 }
658 }
659
660 pub fn frame_count(&self) -> usize {
661 self.frame_count
662 }
663
664 pub fn flush_swmr(&mut self) {
666 if let Some(Hdf5Handle::Swmr { ref mut writer, .. }) = self.handle {
667 if writer.flush().is_ok() {
668 self.swmr_cb_counter += 1;
669 }
670 }
671 }
672
673 pub fn is_swmr_active(&self) -> bool {
675 matches!(self.handle, Some(Hdf5Handle::Swmr { .. }))
676 }
677
678 pub fn swmr_compression_dropped(&self) -> bool {
680 matches!(
681 self.handle,
682 Some(Hdf5Handle::Swmr {
683 compression_dropped: true,
684 ..
685 })
686 )
687 }
688
689 fn build_pipeline(&self, element_size: usize) -> Option<FilterPipeline> {
691 match self.compression_type {
692 COMPRESS_NONE => None,
693 COMPRESS_ZLIB => Some(FilterPipeline::deflate(self.z_compress_level)),
694 COMPRESS_SZIP => Some(FilterPipeline {
695 filters: vec![Filter {
696 id: FILTER_SZIP,
697 flags: 0,
698 cd_values: vec![SZIP_NN_OPTION_MASK, self.szip_num_pixels],
703 }],
704 }),
705 COMPRESS_LZ4 => Some(FilterPipeline::lz4()),
706 COMPRESS_BSHUF => Some(FilterPipeline {
707 filters: vec![Filter {
711 id: FILTER_BSHUF,
712 flags: 0,
713 cd_values: vec![0, 0, element_size as u32, 0, 2],
714 }],
715 }),
716 COMPRESS_BLOSC => {
717 let compressor_code = match self.blosc_compressor {
718 BLOSC_LZ => 0,
719 BLOSC_LZ4 => 1,
720 BLOSC_LZ4HC => 2,
721 BLOSC_SNAPPY => 3,
722 BLOSC_ZLIB => 4,
723 BLOSC_ZSTD => 5,
724 _ => 0,
725 };
726 Some(FilterPipeline {
727 filters: vec![Filter {
728 id: FILTER_BLOSC,
729 flags: 0,
730 cd_values: vec![
731 2,
732 2,
733 element_size as u32,
734 0,
735 self.blosc_compress_level,
736 self.blosc_shuffle_type as u32,
737 compressor_code,
738 ],
739 }],
740 })
741 }
742 COMPRESS_NBIT => {
743 None
759 }
760 COMPRESS_JPEG => Some(FilterPipeline {
761 filters: vec![Filter {
762 id: FILTER_JPEG,
763 flags: 0,
764 cd_values: vec![self.jpeg_quality],
765 }],
766 }),
767 _ => None,
768 }
769 }
770
771 fn nbit_packing(
795 &self,
796 data_type: NDDataType,
797 chunk_nelmts: usize,
798 ) -> Option<(DatatypeMessage, FilterPipeline)> {
799 if self.compression_type != COMPRESS_NBIT {
800 return None;
801 }
802 let (size, signed) = match data_type {
803 NDDataType::Int8 => (1u32, true),
804 NDDataType::UInt8 => (1, false),
805 NDDataType::Int16 => (2, true),
806 NDDataType::UInt16 => (2, false),
807 NDDataType::Int32 => (4, true),
808 NDDataType::UInt32 => (4, false),
809 NDDataType::Int64 => (8, true),
810 NDDataType::UInt64 => (8, false),
811 NDDataType::Float32 | NDDataType::Float64 => return None,
812 };
813 if self.nbit_precision == 0 || self.nbit_precision > size * 8 {
814 return None;
815 }
816 let dt = DatatypeMessage::FixedPoint {
817 size,
818 byte_order: ByteOrder::LittleEndian,
819 signed,
820 bit_offset: self.nbit_offset as u16,
821 bit_precision: self.nbit_precision as u16,
822 };
823 let pipeline = FilterPipeline::nbit(&dt, chunk_nelmts);
824 Some((dt, pipeline))
825 }
826
827 fn codec_filter_pipeline(&self, codec: &Codec) -> Option<FilterPipeline> {
840 let element_size = codec.original_data_type.element_size() as u32;
841 match codec.name {
842 CodecName::LZ4 => Some(FilterPipeline::lz4()),
843 CodecName::BSLZ4 => Some(FilterPipeline {
844 filters: vec![Filter {
848 id: FILTER_BSHUF,
849 flags: 0,
850 cd_values: vec![0, 0, element_size, 0, 2],
851 }],
852 }),
853 CodecName::Blosc => {
854 Some(FilterPipeline {
858 filters: vec![Filter {
859 id: FILTER_BLOSC,
860 flags: 0,
861 cd_values: vec![
862 2,
863 2,
864 element_size,
865 0,
866 codec.level.max(0) as u32,
867 codec.shuffle.max(0) as u32,
868 codec.compressor.max(0) as u32,
869 ],
870 }],
871 })
872 }
873 CodecName::JPEG => Some(FilterPipeline {
879 filters: vec![Filter {
880 id: FILTER_JPEG,
881 flags: 0,
882 cd_values: vec![self.jpeg_quality],
883 }],
884 }),
885 CodecName::None | CodecName::Zlib | CodecName::LZ4HDF5 => None,
886 }
887 }
888
889 fn frame_chunk_dims(&self, frame_dims: &[usize]) -> Vec<usize> {
896 if frame_dims.len() == 2 {
897 let y = frame_dims[0].max(1);
898 let x = frame_dims[1].max(1);
899 vec![
900 Self::clamp_chunk(self.chunk.n_row_chunks, y, self.chunk.auto),
901 Self::clamp_chunk(self.chunk.n_col_chunks, x, self.chunk.auto),
902 ]
903 } else {
904 frame_dims.iter().map(|&d| d.max(1)).collect()
905 }
906 }
907
908 fn extra_dim_axes(&self) -> Option<Vec<usize>> {
918 if self.n_extra_dims == 0 {
919 return None;
920 }
921 Some(
922 (0..=self.n_extra_dims)
923 .rev()
924 .map(|i| self.extra_dims[i].size.max(1))
925 .collect(),
926 )
927 }
928
929 fn standard_layout(
945 &self,
946 frame_dims: &[usize],
947 ) -> (Vec<usize>, Vec<usize>, Option<Vec<usize>>) {
948 let frame_chunk = self.frame_chunk_dims(frame_dims);
949 match self.extra_dim_axes() {
950 Some(leading) => {
951 let mut shape = leading.clone();
952 shape.extend_from_slice(frame_dims);
953 let mut chunk = vec![1usize; leading.len()];
956 chunk.extend_from_slice(&frame_chunk);
957 (shape, chunk, Some(leading))
958 }
959 None => {
960 let mut shape = vec![1usize];
961 shape.extend_from_slice(frame_dims);
962 let mut chunk = vec![self.chunk.n_frames_chunks.max(1)];
963 chunk.extend_from_slice(&frame_chunk);
964 (shape, chunk, None)
965 }
966 }
967 }
968
969 fn compressed_layout(
979 &self,
980 frame_dims: &[usize],
981 ) -> (Vec<usize>, Vec<usize>, Option<Vec<usize>>) {
982 let frame_chunk: Vec<usize> = frame_dims.iter().map(|&d| d.max(1)).collect();
983 match self.extra_dim_axes() {
984 Some(leading) => {
985 let mut shape = leading.clone();
986 shape.extend_from_slice(frame_dims);
987 let mut chunk = vec![1usize; leading.len()];
988 chunk.extend_from_slice(&frame_chunk);
989 (shape, chunk, Some(leading))
990 }
991 None => {
992 let mut shape = vec![1usize];
993 shape.extend_from_slice(frame_dims);
994 let mut chunk = vec![1usize];
995 chunk.extend_from_slice(&frame_chunk);
996 (shape, chunk, None)
997 }
998 }
999 }
1000
1001 fn primary_layout(&self, frame_dims: &[usize]) -> (Vec<usize>, Vec<usize>, Option<usize>) {
1012 let extra_extent = if self.n_extra_dims > 0 {
1013 Some(
1014 (0..self.n_extra_dims)
1015 .map(|i| self.extra_dims[i].size.max(1))
1016 .product::<usize>(),
1017 )
1018 } else {
1019 None
1020 };
1021
1022 let mut shape: Vec<usize> = vec![extra_extent.unwrap_or(1)];
1023 shape.extend_from_slice(frame_dims);
1024
1025 let mut chunk = vec![if extra_extent.is_some() {
1026 1
1027 } else {
1028 self.chunk.n_frames_chunks.max(1)
1029 }];
1030 chunk.extend_from_slice(&self.frame_chunk_dims(frame_dims));
1031 (shape, chunk, extra_extent)
1032 }
1033
1034 fn unravel(mut idx: usize, dims: &[usize]) -> Vec<usize> {
1040 let mut coords = vec![0usize; dims.len()];
1041 for d in (0..dims.len()).rev() {
1042 let s = dims[d].max(1);
1043 coords[d] = idx % s;
1044 idx /= s;
1045 }
1046 coords
1047 }
1048
1049 fn clamp_chunk(requested: usize, dim: usize, auto: bool) -> usize {
1052 if auto || requested == 0 || requested > dim {
1053 dim
1054 } else {
1055 requested
1056 }
1057 }
1058
1059 fn flush_band(
1075 ds: &rust_hdf5::H5Dataset,
1076 leading_coords: &[usize],
1077 frames: &[Vec<u8>],
1078 frame_dims: &[usize],
1079 chunk: &[usize],
1080 elem_size: usize,
1081 ) -> ADResult<()> {
1082 for (coords, buf) in
1083 Self::band_chunk_writes(leading_coords, frames, frame_dims, chunk, elem_size)
1084 {
1085 ds.write_chunk_at(&coords, &buf).map_err(|e| {
1086 ADError::UnsupportedConversion(format!("HDF5 write_chunk_at error: {}", e))
1087 })?;
1088 }
1089 Ok(())
1090 }
1091
1092 fn band_chunk_writes(
1105 leading_coords: &[usize],
1106 frames: &[Vec<u8>],
1107 frame_dims: &[usize],
1108 chunk: &[usize],
1109 elem_size: usize,
1110 ) -> Vec<(Vec<usize>, Vec<u8>)> {
1111 let lead = leading_coords.len();
1112 let fc = chunk[0];
1113 if frame_dims.len() != 2 {
1116 let frame_len = frame_dims.iter().product::<usize>() * elem_size;
1117 let mut buf = vec![0u8; fc * frame_len];
1118 for (f, fb) in frames.iter().take(fc).enumerate() {
1119 buf[f * frame_len..f * frame_len + frame_len].copy_from_slice(fb);
1120 }
1121 let mut coords = leading_coords.to_vec();
1122 coords.extend(std::iter::repeat_n(0, frame_dims.len()));
1123 return vec![(coords, buf)];
1124 }
1125
1126 let (y, x) = (frame_dims[0], frame_dims[1]);
1127 let (rc, cc) = (chunk[lead], chunk[lead + 1]);
1128 let row_tiles = y.div_ceil(rc);
1129 let col_tiles = x.div_ceil(cc);
1130 let mut writes = Vec::with_capacity(row_tiles * col_tiles);
1131 for ry in 0..row_tiles {
1132 for cx in 0..col_tiles {
1133 let mut tile = vec![0u8; fc * rc * cc * elem_size];
1134 for f in 0..fc {
1135 let Some(fb) = frames.get(f) else {
1136 break; };
1138 for r in 0..rc {
1139 let sy = ry * rc + r;
1140 if sy >= y {
1141 break;
1142 }
1143 for c in 0..cc {
1144 let sx = cx * cc + c;
1145 if sx >= x {
1146 break;
1147 }
1148 let src = (sy * x + sx) * elem_size;
1149 let dst = ((f * rc + r) * cc + c) * elem_size;
1150 tile[dst..dst + elem_size].copy_from_slice(&fb[src..src + elem_size]);
1151 }
1152 }
1153 }
1154 let mut coords = leading_coords.to_vec();
1155 coords.push(ry);
1156 coords.push(cx);
1157 writes.push((coords, tile));
1158 }
1159 }
1160 writes
1161 }
1162
1163 fn finalize_standard_datasets(&mut self) -> ADResult<()> {
1169 let Some(frame_dims) = self.open_frame_dims.clone() else {
1170 return Ok(());
1171 };
1172 let (_, chunk, leading) = self.standard_layout(&frame_dims);
1173 let elem_size = self.open_data_type.map(|t| t.element_size()).unwrap_or(1);
1174 let fc = chunk[0];
1175 let Some(Hdf5Handle::Standard { detectors, .. }) = self.handle.as_mut() else {
1176 return Ok(());
1177 };
1178 for det in detectors.values_mut() {
1179 let total = det.frame_count;
1180 if !det.frame_band.is_empty() {
1183 let last = total.saturating_sub(1);
1184 let leading_coords = match &leading {
1185 Some(lead) => Self::unravel(last, lead),
1186 None => vec![last / fc],
1187 };
1188 Self::flush_band(
1189 &det.ds,
1190 &leading_coords,
1191 &det.frame_band,
1192 &frame_dims,
1193 &chunk,
1194 elem_size,
1195 )?;
1196 det.frame_band.clear();
1197 }
1198 if total > 0 {
1203 let mut dims = leading.clone().unwrap_or_else(|| vec![total]);
1204 dims.extend_from_slice(&frame_dims);
1205 det.ds.set_extent(&dims).map_err(|e| {
1206 ADError::UnsupportedConversion(format!("HDF5 set_extent error: {}", e))
1207 })?;
1208 }
1209 }
1210 Ok(())
1211 }
1212
1213 fn open_swmr(&mut self, path: &Path, array: &NDArray) -> ADResult<()> {
1232 let mut swmr = SwmrFileWriter::create(path)
1233 .map_err(|e| ADError::UnsupportedConversion(format!("SWMR create error: {}", e)))?;
1234 self.build_swmr_layout_groups(&mut swmr)?;
1235
1236 let usize_frame_dims: Vec<usize> = array.dims.iter().rev().map(|d| d.size).collect();
1237 let frame_dims: Vec<u64> = usize_frame_dims.iter().map(|&d| d as u64).collect();
1238
1239 let element_size = array.data.data_type().element_size();
1245 let pipeline = self.build_pipeline(element_size);
1246 let chunk: Vec<u64> = {
1247 let (_, c, _) = self.primary_layout(&usize_frame_dims);
1248 c.iter().map(|&v| v as u64).collect()
1249 };
1250
1251 let (grid_shape_usize, grid_chunk_usize, grid_leading) =
1257 self.standard_layout(&usize_frame_dims);
1258 let use_grid = grid_leading.is_some() && pipeline.is_none();
1259 let grid_shape: Vec<u64> = grid_shape_usize.iter().map(|&v| v as u64).collect();
1260 let grid_chunk: Vec<u64> = grid_chunk_usize.iter().map(|&v| v as u64).collect();
1261
1262 let ds_name = self.resolved_dataset_path.clone();
1269
1270 macro_rules! create_ds {
1271 ($t:ty) => {
1272 if use_grid {
1273 swmr.create_grid_dataset::<$t>(&ds_name, &grid_shape, &grid_chunk)
1274 .map_err(|e| {
1275 ADError::UnsupportedConversion(format!(
1276 "SWMR create grid dataset error: {}",
1277 e
1278 ))
1279 })
1280 } else {
1281 match pipeline.clone() {
1282 Some(pl) => swmr
1283 .create_streaming_dataset_chunked_compressed::<$t>(
1284 &ds_name,
1285 &frame_dims,
1286 &chunk,
1287 pl,
1288 )
1289 .map_err(|e| {
1290 ADError::UnsupportedConversion(format!(
1291 "SWMR create compressed dataset error: {}",
1292 e
1293 ))
1294 }),
1295 None => swmr
1296 .create_streaming_dataset_chunked::<$t>(&ds_name, &frame_dims, &chunk)
1297 .map_err(|e| {
1298 ADError::UnsupportedConversion(format!(
1299 "SWMR create dataset error: {}",
1300 e
1301 ))
1302 }),
1303 }
1304 }
1305 };
1306 }
1307
1308 let ds_index = match array.data.data_type() {
1309 NDDataType::Int8 => create_ds!(i8)?,
1310 NDDataType::UInt8 => create_ds!(u8)?,
1311 NDDataType::Int16 => create_ds!(i16)?,
1312 NDDataType::UInt16 => create_ds!(u16)?,
1313 NDDataType::Int32 => create_ds!(i32)?,
1314 NDDataType::UInt32 => create_ds!(u32)?,
1315 NDDataType::Int64 => create_ds!(i64)?,
1316 NDDataType::UInt64 => create_ds!(u64)?,
1317 NDDataType::Float32 => create_ds!(f32)?,
1318 NDDataType::Float64 => create_ds!(f64)?,
1319 };
1320
1321 self.write_swmr_layout_dataset_attrs(&mut swmr, ds_index)?;
1326 self.write_swmr_ndarray_default_attrs(&mut swmr, ds_index, array)?;
1327 self.build_swmr_layout_hardlinks(&mut swmr)?;
1328 self.write_swmr_ndattr_element_attrs(&mut swmr, ds_index)?;
1332
1333 swmr.start_swmr()
1334 .map_err(|e| ADError::UnsupportedConversion(format!("SWMR start error: {}", e)))?;
1335
1336 let compression_dropped = self.compression_type != COMPRESS_NONE && pipeline.is_none();
1341 if compression_dropped {
1342 eprintln!(
1343 "NDFileHDF5: WARNING — SWMR mode requested compression type {} \
1344 but no filter pipeline could be built for it; the SWMR file \
1345 will be written UNCOMPRESSED.",
1346 self.compression_type
1347 );
1348 }
1349
1350 let grid = if use_grid {
1351 grid_leading.map(|leading| SwmrGridLayout {
1353 leading,
1354 frame_dims: usize_frame_dims.clone(),
1355 chunk: grid_chunk_usize.clone(),
1356 elem_size: element_size,
1357 })
1358 } else {
1359 None
1360 };
1361 self.handle = Some(Hdf5Handle::Swmr {
1362 writer: Box::new(swmr),
1363 ds_index,
1364 compression_dropped,
1365 grid,
1366 });
1367 self.open_data_type = Some(array.data.data_type());
1368 self.open_frame_dims = Some(array.dims.iter().rev().map(|d| d.size).collect::<Vec<_>>());
1369 Ok(())
1370 }
1371
1372 fn build_swmr_layout_groups(&self, swmr: &mut SwmrFileWriter) -> ADResult<()> {
1381 let layout = match self.layout.as_ref() {
1382 Some(l) => l,
1383 None => return Ok(()),
1384 };
1385 fn collect<'a>(
1386 g: &'a crate::hdf5_layout::LayoutGroup,
1387 prefix: &str,
1388 out: &mut Vec<(String, &'a crate::hdf5_layout::LayoutGroup)>,
1389 ) {
1390 let here = if prefix.is_empty() {
1391 g.name.clone()
1392 } else {
1393 format!("{}/{}", prefix, g.name)
1394 };
1395 out.push((here.clone(), g));
1396 for sub in &g.groups {
1397 collect(sub, &here, out);
1398 }
1399 }
1400 let mut nodes = Vec::new();
1401 for g in &layout.groups {
1402 collect(g, "", &mut nodes);
1403 }
1404 nodes.sort_by_key(|(p, _)| p.matches('/').count());
1405 let mut created: std::collections::HashSet<String> = std::collections::HashSet::new();
1406 for (path, node) in &nodes {
1407 if !created.insert(path.clone()) {
1408 continue;
1409 }
1410 let (parent, leaf) = match path.rsplit_once('/') {
1411 Some((p, l)) => (format!("/{}", p), l),
1412 None => ("/".to_string(), path.as_str()),
1413 };
1414 swmr.create_group(&parent, leaf).map_err(|e| {
1415 ADError::UnsupportedConversion(format!("SWMR layout group '{}': {}", path, e))
1416 })?;
1417 write_swmr_group_constant_attrs(swmr, &format!("/{}", path), &node.attributes)?;
1421 }
1422 Ok(())
1423 }
1424
1425 fn build_swmr_layout_hardlinks(&self, swmr: &mut SwmrFileWriter) -> ADResult<()> {
1435 let layout = match self.layout.as_ref() {
1436 Some(l) => l,
1437 None => return Ok(()),
1438 };
1439 fn collect<'a>(
1440 g: &'a crate::hdf5_layout::LayoutGroup,
1441 prefix: &str,
1442 out: &mut Vec<(String, &'a crate::hdf5_layout::LayoutHardlink)>,
1443 ) {
1444 let here = if prefix.is_empty() {
1445 g.name.clone()
1446 } else {
1447 format!("{}/{}", prefix, g.name)
1448 };
1449 for hl in &g.hardlinks {
1450 out.push((here.clone(), hl));
1451 }
1452 for sub in &g.groups {
1453 collect(sub, &here, out);
1454 }
1455 }
1456 let mut links = Vec::new();
1457 for g in &layout.groups {
1458 collect(g, "", &mut links);
1459 }
1460 for (parent_path, hl) in &links {
1461 let parent = format!("/{}", parent_path);
1462 swmr.create_hard_link(&parent, &hl.name, &hl.target)
1463 .map_err(|e| {
1464 ADError::UnsupportedConversion(format!(
1465 "SWMR layout hardlink '{}/{}' -> '{}': {}",
1466 parent_path, hl.name, hl.target, e
1467 ))
1468 })?;
1469 }
1470 Ok(())
1471 }
1472
1473 fn write_swmr_layout_dataset_attrs(
1481 &self,
1482 swmr: &mut SwmrFileWriter,
1483 ds_index: usize,
1484 ) -> ADResult<()> {
1485 use crate::hdf5_layout::{LayoutDataType, LayoutSource};
1486 let layout = match self.layout.as_ref() {
1487 Some(l) => l,
1488 None => return Ok(()),
1489 };
1490 let resolved_ds = self.resolved_dataset_path.as_str();
1491 let mut attrs: Vec<(String, LayoutDataType, String)> = Vec::new();
1492 layout.for_each_dataset(|path, d| {
1493 let full = format!("{}/{}", path, d.name);
1494 if full.trim_start_matches('/') == resolved_ds {
1495 for a in &d.attributes {
1496 if a.source == LayoutSource::Constant {
1497 attrs.push((a.name.clone(), a.data_type, a.value.clone()));
1498 }
1499 }
1500 }
1501 });
1502 for (name, dtype, value) in &attrs {
1503 match dtype {
1504 LayoutDataType::Int => {
1505 let v: i64 = value.trim().parse().unwrap_or(0);
1506 swmr.set_dataset_attr_numeric(ds_index, name, &v)
1507 }
1508 LayoutDataType::Float => {
1509 let v: f64 = value.trim().parse().unwrap_or(0.0);
1510 swmr.set_dataset_attr_numeric(ds_index, name, &v)
1511 }
1512 LayoutDataType::String => swmr.set_dataset_attr_string(ds_index, name, value),
1513 }
1514 .map_err(|e| {
1515 ADError::UnsupportedConversion(format!(
1516 "SWMR layout dataset attribute '{}': {}",
1517 name, e
1518 ))
1519 })?;
1520 }
1521 Ok(())
1522 }
1523
1524 fn resolve_layout_paths(&mut self) {
1536 let strip = |s: String| s.trim_start_matches('/').to_string();
1537 match self.layout.as_ref() {
1538 Some(layout) => {
1539 self.resolved_dataset_path = layout
1540 .detector_dataset_path()
1541 .map(strip)
1542 .unwrap_or_else(|| self.dataset_name.clone());
1543 self.resolved_ndattr_group =
1544 layout.ndattr_default_group().map(strip).unwrap_or_default();
1545 self.resolved_perf_group = layout
1546 .dataset_group_path("timestamp")
1547 .map(strip)
1548 .unwrap_or_default();
1549 }
1550 None => {
1551 self.resolved_dataset_path = self.dataset_name.clone();
1552 self.resolved_ndattr_group.clear();
1553 self.resolved_perf_group.clear();
1554 }
1555 }
1556 }
1557
1558 fn build_layout_groups(&self, file: &H5File) -> ADResult<()> {
1567 let layout = match self.layout.as_ref() {
1568 Some(l) => l,
1569 None => return Ok(()),
1570 };
1571 fn collect<'a>(
1572 g: &'a crate::hdf5_layout::LayoutGroup,
1573 prefix: &str,
1574 out: &mut Vec<(String, &'a crate::hdf5_layout::LayoutGroup)>,
1575 ) {
1576 let here = if prefix.is_empty() {
1577 g.name.clone()
1578 } else {
1579 format!("{}/{}", prefix, g.name)
1580 };
1581 out.push((here.clone(), g));
1582 for sub in &g.groups {
1583 collect(sub, &here, out);
1584 }
1585 }
1586 let mut nodes = Vec::new();
1587 for g in &layout.groups {
1588 collect(g, "", &mut nodes);
1589 }
1590 nodes.sort_by_key(|(p, _)| p.matches('/').count());
1591 let mut created: std::collections::HashSet<String> = std::collections::HashSet::new();
1592 for (path, node) in &nodes {
1593 if !created.insert(path.clone()) {
1594 continue;
1595 }
1596 let (parent, leaf) = match path.rsplit_once('/') {
1597 Some((p, l)) => (p, l),
1598 None => ("", path.as_str()),
1599 };
1600 let parent_group = if parent.is_empty() {
1602 None
1603 } else {
1604 Some(Self::open_write_group(file, parent)?)
1605 };
1606 let group = match parent_group.as_ref() {
1607 Some(g) => g.create_group(leaf),
1608 None => file.create_group(leaf),
1609 }
1610 .map_err(|e| {
1611 ADError::UnsupportedConversion(format!("HDF5 layout group '{}': {}", path, e))
1612 })?;
1613 write_group_constant_attrs(&group, &node.attributes);
1616 }
1617 Ok(())
1618 }
1619
1620 fn build_layout_hardlinks(&self, file: &H5File) -> ADResult<()> {
1638 let layout = match self.layout.as_ref() {
1639 Some(l) => l,
1640 None => return Ok(()),
1641 };
1642 fn collect<'a>(
1644 g: &'a crate::hdf5_layout::LayoutGroup,
1645 prefix: &str,
1646 out: &mut Vec<(String, &'a crate::hdf5_layout::LayoutHardlink)>,
1647 ) {
1648 let here = if prefix.is_empty() {
1649 g.name.clone()
1650 } else {
1651 format!("{}/{}", prefix, g.name)
1652 };
1653 for hl in &g.hardlinks {
1654 out.push((here.clone(), hl));
1655 }
1656 for sub in &g.groups {
1657 collect(sub, &here, out);
1658 }
1659 }
1660 let mut links = Vec::new();
1661 for g in &layout.groups {
1662 collect(g, "", &mut links);
1663 }
1664 for (parent_path, hl) in &links {
1665 let parent = Self::open_write_group(file, parent_path)?;
1668 parent.link(&hl.name, &hl.target).map_err(|e| {
1669 ADError::UnsupportedConversion(format!(
1670 "HDF5 layout hardlink '{}/{}' -> '{}': {}",
1671 parent_path, hl.name, hl.target, e
1672 ))
1673 })?;
1674 }
1675 Ok(())
1676 }
1677
1678 fn open_write_group(file: &H5File, path: &str) -> ADResult<rust_hdf5::H5Group> {
1682 let mut current: Option<rust_hdf5::H5Group> = None;
1683 for seg in path.split('/').filter(|s| !s.is_empty()) {
1684 let next = match current.as_ref() {
1685 Some(g) => g.group(seg),
1686 None => file.root_group().group(seg),
1687 }
1688 .map_err(|e| {
1689 ADError::UnsupportedConversion(format!("HDF5 group reopen '{}': {}", seg, e))
1690 })?;
1691 current = Some(next);
1692 }
1693 current.ok_or_else(|| ADError::UnsupportedConversion("empty group path".into()))
1694 }
1695
1696 fn create_detector_datasets(&mut self, array: &NDArray) -> ADResult<()> {
1704 let frame_dims: Vec<usize> = array.dims.iter().rev().map(|d| d.size).collect();
1705 let (ds_data_type, shape, chunk, leading, pipeline) = match array.codec.as_ref() {
1713 Some(codec) => {
1714 let pl = self.codec_filter_pipeline(codec).ok_or_else(|| {
1715 ADError::UnsupportedConversion(format!(
1716 "HDF5 cannot direct-chunk-write codec '{}' \
1717 (only jpeg/blosc/lz4/bslz4 are supported)",
1718 codec.name.as_str()
1719 ))
1720 })?;
1721 let (shape, chunk, leading) = self.compressed_layout(&frame_dims);
1722 (codec.original_data_type, shape, chunk, leading, Some(pl))
1723 }
1724 None => {
1725 let dt = array.data.data_type();
1726 let (shape, chunk, leading) = self.standard_layout(&frame_dims);
1727 (
1728 dt,
1729 shape,
1730 chunk,
1731 leading,
1732 self.build_pipeline(dt.element_size()),
1733 )
1734 }
1735 };
1736 let (nbit_dt, pipeline): (Option<DatatypeMessage>, Option<FilterPipeline>) =
1742 if array.codec.is_none() {
1743 let chunk_nelmts: usize = chunk.iter().product();
1744 match self.nbit_packing(ds_data_type, chunk_nelmts) {
1745 Some((dt, pl)) => (Some(dt), Some(pl)),
1746 None => (None, pipeline),
1747 }
1748 } else {
1749 (None, pipeline)
1750 };
1751 let max_shape: Vec<Option<usize>> = shape
1759 .iter()
1760 .zip(chunk.iter())
1761 .enumerate()
1762 .map(|(i, (&s, &c))| {
1763 if leading.is_none() && i == 0 {
1764 None
1765 } else {
1766 Some(s.div_ceil(c) * c)
1767 }
1768 })
1769 .collect();
1770
1771 match self.handle {
1775 Some(Hdf5Handle::Standard { ref file, .. }) => self.build_layout_groups(file)?,
1776 _ => return Err(ADError::UnsupportedConversion("no HDF5 file open".into())),
1777 }
1778
1779 let detector_keys: Vec<String> = match self.layout.as_ref() {
1786 Some(l) => {
1787 let mut keys = l.detector_dataset_paths();
1788 if keys.is_empty() {
1789 keys.push(format!("/{}", self.resolved_dataset_path));
1790 }
1791 keys
1792 }
1793 None => vec![format!("/{}", self.resolved_dataset_path)],
1794 };
1795 let detector_specs: Vec<(
1796 String,
1797 Vec<(String, crate::hdf5_layout::LayoutDataType, String)>,
1798 )> = detector_keys
1799 .iter()
1800 .map(|key| {
1801 let stripped = key.trim_start_matches('/').to_string();
1802 let attrs = self
1803 .layout
1804 .as_ref()
1805 .map(|l| {
1806 use crate::hdf5_layout::LayoutSource;
1807 let mut out = Vec::new();
1808 l.for_each_dataset(|path, d| {
1809 let full = format!("{}/{}", path, d.name);
1810 if full.trim_start_matches('/') == stripped {
1811 for a in &d.attributes {
1812 if a.source == LayoutSource::Constant {
1813 out.push((a.name.clone(), a.data_type, a.value.clone()));
1814 }
1815 }
1816 }
1817 });
1818 out
1819 })
1820 .unwrap_or_default();
1821 (key.clone(), attrs)
1822 })
1823 .collect();
1824
1825 let dtype_ordinal = ds_data_type as i32;
1826 let fill = self.fill_value;
1827 let row_chunks = self.chunk.n_row_chunks as i32;
1828 let col_chunks = self.chunk.n_col_chunks as i32;
1829 let frame_chunks = self.chunk.n_frames_chunks as i32;
1830 let n_extra = self.n_extra_dims as i32;
1831 let extra_meta: Vec<(usize, i32, String)> = (0..self.n_extra_dims)
1832 .map(|i| {
1833 (
1834 i,
1835 self.extra_dims[i].size.max(1) as i32,
1836 self.extra_dims[i].name.clone(),
1837 )
1838 })
1839 .collect();
1840
1841 let nd_num_dims = array.dims.len() as i32;
1846 let dim_offsets: Vec<i32> = array.dims.iter().map(|d| d.offset as i32).collect();
1850 let dim_binnings: Vec<i32> = array.dims.iter().map(|d| d.binning as i32).collect();
1851 let dim_reverses: Vec<i32> = array.dims.iter().map(|d| d.reverse as i32).collect();
1852
1853 macro_rules! create_ds {
1854 ($t:ty, $h5file:expr, $ds_group:expr, $ds_name:expr, $constant_attrs:expr) => {{
1855 let mut builder = match $ds_group.as_ref() {
1856 Some(g) => g.new_dataset::<$t>(),
1857 None => $h5file.new_dataset::<$t>(),
1858 }
1859 .shape(&shape[..])
1860 .chunk(&chunk[..])
1861 .max_shape(&max_shape[..])
1862 .fill_value(fill as $t);
1868 if let Some(ref dt) = nbit_dt {
1872 builder = builder.datatype(dt.clone());
1873 }
1874 if let Some(ref pl) = pipeline {
1875 builder = builder.filter_pipeline(pl.clone());
1876 }
1877 let ds = builder.create($ds_name.as_str()).map_err(|e| {
1878 ADError::UnsupportedConversion(format!("HDF5 dataset error: {}", e))
1879 })?;
1880 let _ = ds
1882 .new_attr::<i32>()
1883 .shape(())
1884 .create(DTYPE_ATTR)
1885 .and_then(|a| a.write_numeric(&dtype_ordinal));
1886 let _ = ds
1890 .new_attr::<f64>()
1891 .shape(())
1892 .create("HDF5_fillValue")
1893 .and_then(|a| a.write_numeric(&fill));
1894 for (name, val) in [
1898 ("HDF5_nRowChunks", row_chunks),
1899 ("HDF5_nColChunks", col_chunks),
1900 ("HDF5_nFramesChunks", frame_chunks),
1901 ("HDF5_nExtraDims", n_extra),
1902 ] {
1903 let _ = ds
1904 .new_attr::<i32>()
1905 .shape(())
1906 .create(name)
1907 .and_then(|a| a.write_numeric(&val));
1908 }
1909 for (i, size, name) in &extra_meta {
1912 let _ = ds
1913 .new_attr::<i32>()
1914 .shape(())
1915 .create(&format!("HDF5_extraDimSize{}", i))
1916 .and_then(|a| a.write_numeric(size));
1917 if !name.is_empty() {
1918 let s = rust_hdf5::types::VarLenUnicode(name.clone());
1919 let _ = ds
1920 .new_attr::<rust_hdf5::types::VarLenUnicode>()
1921 .shape(())
1922 .create(&format!("HDF5_extraDimName{}", i))
1923 .and_then(|a| a.write_scalar(&s));
1924 }
1925 }
1926 for (aname, atype, avalue) in $constant_attrs {
1929 use crate::hdf5_layout::LayoutDataType;
1930 match atype {
1931 LayoutDataType::Int => {
1932 let v: i64 = avalue.trim().parse().unwrap_or(0);
1933 let _ = ds
1934 .new_attr::<i64>()
1935 .shape(())
1936 .create(aname)
1937 .and_then(|a| a.write_numeric(&v));
1938 }
1939 LayoutDataType::Float => {
1940 let v: f64 = avalue.trim().parse().unwrap_or(0.0);
1941 let _ = ds
1942 .new_attr::<f64>()
1943 .shape(())
1944 .create(aname)
1945 .and_then(|a| a.write_numeric(&v));
1946 }
1947 LayoutDataType::String => {
1948 let s = rust_hdf5::types::VarLenUnicode(avalue.clone());
1949 let _ = ds
1950 .new_attr::<rust_hdf5::types::VarLenUnicode>()
1951 .shape(())
1952 .create(aname)
1953 .and_then(|a| a.write_scalar(&s));
1954 }
1955 }
1956 }
1957 let _ = ds
1963 .new_attr::<i32>()
1964 .shape(())
1965 .create("NDArrayNumDims")
1966 .and_then(|a| a.write_numeric(&nd_num_dims));
1967 for (name, vals) in [
1968 ("NDArrayDimOffset", &dim_offsets),
1969 ("NDArrayDimBinning", &dim_binnings),
1970 ("NDArrayDimReverse", &dim_reverses),
1971 ] {
1972 let _ = if vals.len() == 1 {
1973 ds.new_attr::<i32>()
1974 .shape(())
1975 .create(name)
1976 .and_then(|a| a.write_numeric(&vals[0]))
1977 } else {
1978 ds.new_attr::<i32>()
1979 .shape([vals.len()])
1980 .create(name)
1981 .and_then(|a| a.write_array(vals))
1982 };
1983 }
1984 ds
1985 }};
1986 }
1987
1988 let h5file = match self.handle {
1989 Some(Hdf5Handle::Standard { ref file, .. }) => file,
1990 _ => return Err(ADError::UnsupportedConversion("no HDF5 file open".into())),
1991 };
1992
1993 let mut detectors: std::collections::HashMap<String, DetectorDataset> =
1994 std::collections::HashMap::with_capacity(detector_specs.len());
1995 for (key, constant_attrs) in &detector_specs {
1996 let stripped = key.trim_start_matches('/');
1999 let (ds_group, ds_name): (Option<rust_hdf5::H5Group>, String) =
2000 match stripped.rsplit_once('/') {
2001 Some((group_path, leaf)) => (
2002 Some(Self::open_write_group(h5file, group_path)?),
2003 leaf.to_string(),
2004 ),
2005 None => (None, stripped.to_string()),
2006 };
2007 let ds = match ds_data_type {
2012 NDDataType::Int8 => create_ds!(i8, h5file, ds_group, ds_name, constant_attrs),
2013 NDDataType::UInt8 => create_ds!(u8, h5file, ds_group, ds_name, constant_attrs),
2014 NDDataType::Int16 => create_ds!(i16, h5file, ds_group, ds_name, constant_attrs),
2015 NDDataType::UInt16 => create_ds!(u16, h5file, ds_group, ds_name, constant_attrs),
2016 NDDataType::Int32 => create_ds!(i32, h5file, ds_group, ds_name, constant_attrs),
2017 NDDataType::UInt32 => create_ds!(u32, h5file, ds_group, ds_name, constant_attrs),
2018 NDDataType::Int64 => create_ds!(i64, h5file, ds_group, ds_name, constant_attrs),
2019 NDDataType::UInt64 => create_ds!(u64, h5file, ds_group, ds_name, constant_attrs),
2020 NDDataType::Float32 => create_ds!(f32, h5file, ds_group, ds_name, constant_attrs),
2021 NDDataType::Float64 => create_ds!(f64, h5file, ds_group, ds_name, constant_attrs),
2022 };
2023 detectors.insert(
2024 key.clone(),
2025 DetectorDataset {
2026 ds,
2027 frame_count: 0,
2028 frame_band: Vec::new(),
2029 },
2030 );
2031 }
2032
2033 if let Some(Hdf5Handle::Standard { detectors: d, .. }) = self.handle.as_mut() {
2034 *d = detectors;
2035 }
2036 self.open_data_type = Some(ds_data_type);
2037 self.open_frame_dims = Some(frame_dims);
2038 self.open_codec = array.codec.clone();
2039 Ok(())
2040 }
2041
2042 fn resolve_destination_key(&self, array: &NDArray) -> ADResult<String> {
2053 let default = format!("/{}", self.resolved_dataset_path);
2054 let attr_name = self
2055 .layout
2056 .as_ref()
2057 .and_then(|l| l.detector_data_destination.as_deref())
2058 .filter(|s| !s.is_empty());
2059 let Some(attr_name) = attr_name else {
2060 return Ok(default);
2061 };
2062 let Some(attr) = array.attributes.get(attr_name) else {
2065 return Ok(default);
2066 };
2067 match attr.value.as_string_typed() {
2068 Some(value) => {
2069 let known = matches!(
2070 &self.handle,
2071 Some(Hdf5Handle::Standard { detectors, .. })
2072 if detectors.contains_key(value)
2073 );
2074 Ok(if known { value.to_string() } else { default })
2075 }
2076 None => Err(ADError::UnsupportedConversion(format!(
2077 "HDF5 detector_data_destination attribute '{}' is not a string",
2078 attr_name
2079 ))),
2080 }
2081 }
2082
2083 fn write_standard(&mut self, array: &NDArray) -> ADResult<()> {
2088 if self.frame_count == 0 {
2089 self.create_detector_datasets(array)?;
2090 self.create_attribute_datasets();
2091 }
2092
2093 let frame_dims = self
2094 .open_frame_dims
2095 .clone()
2096 .ok_or_else(|| ADError::UnsupportedConversion("dataset not initialised".into()))?;
2097 let cur_dims: Vec<usize> = array.dims.iter().rev().map(|d| d.size).collect();
2098 if cur_dims != frame_dims {
2099 return Err(ADError::UnsupportedConversion(format!(
2100 "HDF5 frame shape changed mid-stream: {:?} != {:?}",
2101 cur_dims, frame_dims
2102 )));
2103 }
2104
2105 if !codecs_match(self.open_codec.as_ref(), array.codec.as_ref()) {
2110 return Err(ADError::UnsupportedConversion(format!(
2111 "HDF5 codec changed mid-stream: dataset codec {:?} != frame codec {:?}",
2112 self.open_codec.as_ref().map(|c| c.name),
2113 array.codec.as_ref().map(|c| c.name),
2114 )));
2115 }
2116
2117 let (_shape, chunk, leading) = self.standard_layout(&frame_dims);
2118 let fc = chunk[0];
2119
2120 let dest_key = self.resolve_destination_key(array)?;
2126 let chunk_bytes = array.codec.as_ref().map(|codec| {
2127 let total_bytes =
2128 frame_dims.iter().product::<usize>() * codec.original_data_type.element_size();
2129 codec_chunk_bytes(codec, total_bytes, array.data.as_u8_slice())
2130 });
2131 let frame_le = if chunk_bytes.is_none() {
2132 Some(nd_buffer_to_le_bytes(&array.data))
2133 } else {
2134 None
2135 };
2136 let elem_size = array.data.data_type().element_size();
2137
2138 {
2139 let Some(Hdf5Handle::Standard { detectors, .. }) = self.handle.as_mut() else {
2140 return Err(ADError::UnsupportedConversion(
2141 "HDF5 detector datasets not initialised".into(),
2142 ));
2143 };
2144 let det = detectors.get_mut(&dest_key).ok_or_else(|| {
2145 ADError::UnsupportedConversion(format!(
2146 "HDF5 destination dataset '{}' not found",
2147 dest_key
2148 ))
2149 })?;
2150
2151 let frame_idx = det.frame_count;
2154 if let Some(ref lead) = leading {
2155 let total: usize = lead.iter().product();
2156 if frame_idx >= total {
2157 return Err(ADError::UnsupportedConversion(format!(
2158 "HDF5 extra-dimension capacity exceeded: frame {} >= {}",
2159 frame_idx, total
2160 )));
2161 }
2162 }
2163
2164 if let Some(cb) = &chunk_bytes {
2165 det.ds.write_chunk_raw(frame_idx, cb, 0).map_err(|e| {
2171 ADError::UnsupportedConversion(format!("HDF5 direct chunk write error: {}", e))
2172 })?;
2173 } else {
2174 let fle = frame_le.expect("frame_le is set whenever chunk_bytes is None");
2180 det.frame_band.push(fle);
2181 if det.frame_band.len() >= fc {
2182 let leading_coords = match &leading {
2183 Some(lead) => Self::unravel(frame_idx, lead),
2184 None => vec![frame_idx / fc],
2185 };
2186 Self::flush_band(
2187 &det.ds,
2188 &leading_coords,
2189 &det.frame_band,
2190 &frame_dims,
2191 &chunk,
2192 elem_size,
2193 )?;
2194 det.frame_band.clear();
2195 }
2196 }
2197 det.frame_count += 1;
2198 }
2199
2200 if self.store_attributes {
2206 let Self {
2207 attr_datasets,
2208 file_attributes,
2209 ..
2210 } = self;
2211 for ad in attr_datasets.iter_mut() {
2212 if let Some(attr) = file_attributes.get(&ad.name) {
2213 ad.push(&attr.value);
2214 }
2215 }
2216 }
2217 Ok(())
2218 }
2219
2220 fn create_attribute_datasets(&mut self) {
2223 self.attr_datasets.clear();
2224 if !self.store_attributes {
2225 return;
2226 }
2227 self.attr_datasets = self
2228 .file_attributes
2229 .iter()
2230 .map(AttributeDataset::new)
2231 .collect();
2232 }
2233
2234 fn attribute_chunking(&self) -> usize {
2240 if self.chunk.ndattr_chunk != 0 {
2241 return self.chunk.ndattr_chunk;
2242 }
2243 if self.open_mode == NDFileMode::Single {
2244 return 1;
2245 }
2246 if self.num_capture > 0 {
2247 self.num_capture
2248 } else {
2249 16 * 1024
2250 }
2251 }
2252
2253 fn seed_ndattr_element_values(&mut self) {
2259 self.ndattr_first_values.clear();
2260 self.ndattr_last_values.clear();
2261 self.ndattr_element_names.clear();
2262 let Some(layout) = self.layout.as_ref() else {
2263 return;
2264 };
2265 let mut names: Vec<String> = Vec::new();
2266 for e in layout.ndattribute_element_attrs() {
2267 if !names.contains(&e.ndattribute) {
2268 names.push(e.ndattribute);
2269 }
2270 }
2271 for name in &names {
2272 if let Some(attr) = self.file_attributes.get(name) {
2273 self.ndattr_first_values
2274 .insert(name.clone(), attr.value.clone());
2275 self.ndattr_last_values
2276 .insert(name.clone(), attr.value.clone());
2277 }
2278 }
2279 self.ndattr_element_names = names;
2280 }
2281
2282 fn update_ndattr_element_values(&mut self) {
2288 if self.ndattr_element_names.is_empty() {
2289 return;
2290 }
2291 let Self {
2292 ndattr_element_names,
2293 ndattr_last_values,
2294 file_attributes,
2295 ..
2296 } = self;
2297 for name in ndattr_element_names.iter() {
2298 if let Some(attr) = file_attributes.get(name) {
2299 ndattr_last_values.insert(name.clone(), attr.value.clone());
2300 }
2301 }
2302 }
2303
2304 fn flush_ndattr_element_attrs(
2321 &self,
2322 file: &H5File,
2323 detectors: &std::collections::HashMap<String, DetectorDataset>,
2324 ) -> ADResult<()> {
2325 use crate::hdf5_layout::LayoutWhen;
2326 let Some(layout) = self.layout.as_ref() else {
2327 return Ok(());
2328 };
2329 for e in layout.ndattribute_element_attrs() {
2330 let value = match e.when {
2331 LayoutWhen::OnFileOpen | LayoutWhen::OnFrame => {
2332 self.ndattr_first_values.get(&e.ndattribute)
2333 }
2334 LayoutWhen::OnFileClose => self.ndattr_last_values.get(&e.ndattribute),
2335 LayoutWhen::OnFileWrite => None,
2336 };
2337 let Some(value) = value else {
2338 continue;
2339 };
2340 let path = e.element_path.trim_start_matches('/');
2341 if e.is_dataset {
2342 if let Some(det) = detectors.get(&e.element_path) {
2349 write_ndattr_dataset_attr(&det.ds, &e.attr_name, value);
2350 } else if let Ok(ds) = file.dataset_writer(path) {
2351 write_ndattr_dataset_attr(&ds, &e.attr_name, value);
2352 }
2353 } else {
2354 let group = Self::open_write_group(file, path)?;
2355 write_ndattr_group_attr(&group, &e.attr_name, value);
2356 }
2357 }
2358 Ok(())
2359 }
2360
2361 fn write_swmr_ndarray_default_attrs(
2370 &self,
2371 swmr: &mut SwmrFileWriter,
2372 ds_index: usize,
2373 array: &NDArray,
2374 ) -> ADResult<()> {
2375 let nd_num_dims = array.dims.len() as i32;
2376 swmr.set_dataset_attr_numeric(ds_index, "NDArrayNumDims", &nd_num_dims)
2377 .map_err(|e| {
2378 ADError::UnsupportedConversion(format!("SWMR NDArrayNumDims attr: {}", e))
2379 })?;
2380 let dim_offsets: Vec<i32> = array.dims.iter().map(|d| d.offset as i32).collect();
2381 let dim_binnings: Vec<i32> = array.dims.iter().map(|d| d.binning as i32).collect();
2382 let dim_reverses: Vec<i32> = array.dims.iter().map(|d| d.reverse as i32).collect();
2383 for (name, vals) in [
2384 ("NDArrayDimOffset", &dim_offsets),
2385 ("NDArrayDimBinning", &dim_binnings),
2386 ("NDArrayDimReverse", &dim_reverses),
2387 ] {
2388 let res = if vals.len() == 1 {
2389 swmr.set_dataset_attr_numeric(ds_index, name, &vals[0])
2390 } else {
2391 swmr.set_dataset_attr_array(ds_index, name, &[vals.len() as u64], vals)
2392 };
2393 res.map_err(|e| ADError::UnsupportedConversion(format!("SWMR {} attr: {}", name, e)))?;
2394 }
2395 Ok(())
2396 }
2397
2398 fn write_swmr_ndattr_element_attrs(
2409 &self,
2410 swmr: &mut SwmrFileWriter,
2411 ds_index: usize,
2412 ) -> ADResult<()> {
2413 use crate::hdf5_layout::LayoutWhen;
2414 let Some(layout) = self.layout.as_ref() else {
2415 return Ok(());
2416 };
2417 for e in layout.ndattribute_element_attrs() {
2418 if !matches!(e.when, LayoutWhen::OnFileOpen | LayoutWhen::OnFrame) {
2419 continue;
2420 }
2421 let Some(value) = self.ndattr_first_values.get(&e.ndattribute) else {
2422 continue;
2423 };
2424 if e.is_dataset {
2425 if e.element_path.trim_start_matches('/') == self.resolved_dataset_path {
2429 write_swmr_ndattr_dataset_attr(swmr, ds_index, &e.attr_name, value)?;
2430 }
2431 } else {
2432 write_swmr_ndattr_group_attr(swmr, &e.element_path, &e.attr_name, value)?;
2433 }
2434 }
2435 Ok(())
2436 }
2437
2438 fn flush_attribute_datasets(&mut self) -> ADResult<()> {
2441 if self.attr_datasets.is_empty() {
2442 return Ok(());
2443 }
2444 let chunk_depth = self.attribute_chunking();
2445 let ndattr_group = self.resolved_ndattr_group.clone();
2446 let targets: Vec<(String, String)> = self
2450 .attr_datasets
2451 .iter()
2452 .map(|ad| {
2453 match self
2454 .layout
2455 .as_ref()
2456 .and_then(|l| l.ndattribute_dataset(&ad.name))
2457 {
2458 Some((g, name)) => (g.trim_start_matches('/').to_string(), name),
2459 None => (ndattr_group.clone(), ad.name.clone()),
2460 }
2461 })
2462 .collect();
2463 let h5file = match self.handle {
2464 Some(Hdf5Handle::Standard { ref file, .. }) => file,
2465 _ => return Ok(()),
2466 };
2467 let mut group_cache: std::collections::HashMap<String, rust_hdf5::H5Group> =
2471 std::collections::HashMap::new();
2472
2473 for (ad, (group_path, ds_name)) in self.attr_datasets.iter().zip(targets.iter()) {
2474 if ad.frames == 0 {
2475 continue;
2476 }
2477 let n = ad.frames;
2478 let chunk = chunk_depth;
2482
2483 if !group_cache.contains_key(group_path) {
2484 let g = if group_path.is_empty() {
2485 h5file.create_group("NDAttributes").map_err(|e| {
2486 ADError::UnsupportedConversion(format!("HDF5 group error: {}", e))
2487 })?
2488 } else {
2489 Self::open_write_group(h5file, group_path)?
2490 };
2491 group_cache.insert(group_path.clone(), g);
2492 }
2493 let group = &group_cache[group_path];
2494
2495 macro_rules! create_attr_ds {
2496 ($t:ty) => {{
2497 let es = std::mem::size_of::<$t>();
2498 let ds = group
2499 .new_dataset::<$t>()
2500 .shape(&[n])
2501 .chunk(&[chunk])
2502 .max_shape(&[None])
2503 .create(ds_name)
2504 .map_err(|e| {
2505 ADError::UnsupportedConversion(format!(
2506 "HDF5 attribute dataset error: {}",
2507 e
2508 ))
2509 })?;
2510 write_chunked_buffer(&ds, &ad.buffer, chunk * es)?;
2514 ds
2515 }};
2516 }
2517
2518 let ds = match ad.data_type {
2519 NDAttrDataType::Int8 => create_attr_ds!(i8),
2520 NDAttrDataType::UInt8 => create_attr_ds!(u8),
2521 NDAttrDataType::Int16 => create_attr_ds!(i16),
2522 NDAttrDataType::UInt16 => create_attr_ds!(u16),
2523 NDAttrDataType::Int32 => create_attr_ds!(i32),
2524 NDAttrDataType::UInt32 => create_attr_ds!(u32),
2525 NDAttrDataType::Int64 => create_attr_ds!(i64),
2526 NDAttrDataType::UInt64 => create_attr_ds!(u64),
2527 NDAttrDataType::Float32 => create_attr_ds!(f32),
2528 NDAttrDataType::Float64 => create_attr_ds!(f64),
2529 NDAttrDataType::String => {
2530 let es = MAX_ATTRIBUTE_STRING_SIZE;
2537 let ds = group
2538 .new_dataset::<FixedStr256>()
2539 .shape([n])
2540 .chunk(&[chunk])
2541 .max_shape(&[None])
2542 .create(&ad.name)
2543 .map_err(|e| {
2544 ADError::UnsupportedConversion(format!(
2545 "HDF5 attribute dataset error: {}",
2546 e
2547 ))
2548 })?;
2549 write_chunked_buffer(&ds, &ad.buffer, chunk * es)?;
2550 ds
2551 }
2552 };
2553
2554 write_ndattr_descriptors(&ds, ad);
2558 }
2559 Ok(())
2560 }
2561
2562 fn flush_performance_dataset(&mut self) -> ADResult<()> {
2565 if !self.store_performance || self.perf_rows.is_empty() {
2566 return Ok(());
2567 }
2568 let n = self.perf_rows.len();
2569 let mut flat: Vec<f64> = Vec::with_capacity(n * 5);
2570 for row in &self.perf_rows {
2571 flat.extend_from_slice(row);
2572 }
2573 let raw: Vec<u8> = flat.iter().flat_map(|v| v.to_le_bytes()).collect();
2576
2577 let chunking = self.attribute_chunking();
2581 let perf_group = self.resolved_perf_group.clone();
2582 let h5file = match self.handle {
2583 Some(Hdf5Handle::Standard { ref file, .. }) => file,
2584 _ => return Ok(()),
2585 };
2586 let group = if perf_group.is_empty() {
2591 h5file
2592 .create_group("performance")
2593 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 group error: {}", e)))?
2594 } else {
2595 Self::open_write_group(h5file, &perf_group)?
2596 };
2597 let ds = group
2598 .new_dataset::<f64>()
2599 .shape([n, 5])
2600 .chunk(&[chunking, 5])
2601 .max_shape(&[None, Some(5)])
2602 .create("timestamp")
2603 .map_err(|e| {
2604 ADError::UnsupportedConversion(format!("HDF5 performance dataset error: {}", e))
2605 })?;
2606 write_chunked_buffer(&ds, &raw, chunking * 5 * 8)?;
2609 Ok(())
2610 }
2611
2612 fn write_swmr(&mut self, array: &NDArray) -> ADResult<()> {
2614 if array.codec.is_some() {
2623 return Err(ADError::UnsupportedConversion(
2624 "HDF5 SWMR mode cannot write a pre-compressed array (no raw-chunk \
2625 append in the SWMR backend); use standard capture mode for \
2626 direct chunk write"
2627 .into(),
2628 ));
2629 }
2630
2631 let frame_idx = self.frame_count;
2634
2635 let (writer, ds_index, grid) = match self.handle {
2636 Some(Hdf5Handle::Swmr {
2637 ref mut writer,
2638 ds_index,
2639 ref grid,
2640 ..
2641 }) => (writer, ds_index, grid),
2642 _ => return Err(ADError::UnsupportedConversion("no SWMR writer open".into())),
2643 };
2644
2645 let frame_bytes = nd_buffer_to_le_bytes(&array.data);
2650 match grid {
2651 Some(g) => {
2655 let leading_coords = Self::unravel(frame_idx, &g.leading);
2656 for (coords, tile) in Self::band_chunk_writes(
2657 &leading_coords,
2658 std::slice::from_ref(&frame_bytes),
2659 &g.frame_dims,
2660 &g.chunk,
2661 g.elem_size,
2662 ) {
2663 let coords_u64: Vec<u64> = coords.iter().map(|&c| c as u64).collect();
2664 writer
2665 .write_chunk_at(ds_index, &coords_u64, &tile)
2666 .map_err(|e| {
2667 ADError::UnsupportedConversion(format!(
2668 "SWMR write_chunk_at error: {}",
2669 e
2670 ))
2671 })?;
2672 }
2673 }
2674 None => {
2675 writer.append_frame(ds_index, &frame_bytes).map_err(|e| {
2676 ADError::UnsupportedConversion(format!("SWMR append error: {}", e))
2677 })?;
2678 }
2679 }
2680
2681 let count = self.frame_count + 1; if self.flush_nth_frame > 0 && count % self.flush_nth_frame == 0 {
2684 writer
2685 .flush()
2686 .map_err(|e| ADError::UnsupportedConversion(format!("SWMR flush error: {}", e)))?;
2687 }
2688 Ok(())
2689 }
2690
2691 fn record_performance(&mut self, write_duration: f64, frame_bytes: usize) {
2693 let now = std::time::Instant::now();
2694 let first = *self.perf_first.get_or_insert(now);
2695 let runtime = now.duration_since(first).as_secs_f64();
2696 let period = match self.perf_prev {
2697 Some(prev) => now.duration_since(prev).as_secs_f64(),
2698 None => write_duration,
2699 };
2700 self.perf_prev = Some(now);
2701 let fb = frame_bytes as f64;
2702 let inst_speed = if period > 0.0 { fb / period } else { 0.0 };
2703 let avg_speed = if runtime > 0.0 {
2704 (self.perf_rows.len() as f64 + 1.0) * fb / runtime
2705 } else {
2706 0.0
2707 };
2708 self.perf_rows
2709 .push([write_duration, period, runtime, inst_speed, avg_speed]);
2710 }
2711}
2712
2713fn codecs_match(a: Option<&Codec>, b: Option<&Codec>) -> bool {
2720 match (a, b) {
2721 (None, None) => true,
2722 (Some(x), Some(y)) => {
2723 x.name == y.name
2724 && x.level == y.level
2725 && x.shuffle == y.shuffle
2726 && x.compressor == y.compressor
2727 }
2728 _ => false,
2729 }
2730}
2731
2732fn codec_chunk_bytes(codec: &Codec, uncompressed_total_bytes: usize, payload: &[u8]) -> Vec<u8> {
2754 match codec.name {
2755 CodecName::LZ4 => {
2756 let mut out = Vec::with_capacity(16 + payload.len());
2757 out.extend_from_slice(&(uncompressed_total_bytes as u64).to_be_bytes());
2758 out.extend_from_slice(&(uncompressed_total_bytes as u32).to_be_bytes());
2759 out.extend_from_slice(&(payload.len() as u32).to_be_bytes());
2760 out.extend_from_slice(payload);
2761 out
2762 }
2763 CodecName::BSLZ4 => {
2764 let elem_size = codec.original_data_type.element_size();
2765 let block_bytes = crate::codec::bshuf_default_block_size(elem_size) * elem_size;
2766 let mut out = Vec::with_capacity(12 + payload.len());
2767 out.extend_from_slice(&(uncompressed_total_bytes as u64).to_be_bytes());
2768 out.extend_from_slice(&(block_bytes as u32).to_be_bytes());
2769 out.extend_from_slice(payload);
2770 out
2771 }
2772 _ => payload.to_vec(),
2773 }
2774}
2775
2776fn nd_buffer_to_le_bytes(buf: &NDDataBuffer) -> Vec<u8> {
2788 match buf {
2789 NDDataBuffer::I8(v) => v.iter().map(|&x| x as u8).collect(),
2790 NDDataBuffer::U8(v) => v.clone(),
2791 NDDataBuffer::I16(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2792 NDDataBuffer::U16(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2793 NDDataBuffer::I32(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2794 NDDataBuffer::U32(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2795 NDDataBuffer::I64(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2796 NDDataBuffer::U64(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2797 NDDataBuffer::F32(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2798 NDDataBuffer::F64(v) => v.iter().flat_map(|&x| x.to_le_bytes()).collect(),
2799 }
2800}
2801
2802fn write_chunked_buffer(
2806 ds: &rust_hdf5::H5Dataset,
2807 buffer: &[u8],
2808 chunk_bytes: usize,
2809) -> ADResult<()> {
2810 let n_chunks = buffer.len().div_ceil(chunk_bytes.max(1));
2811 for c in 0..n_chunks {
2812 let start = c * chunk_bytes;
2813 let end = ((c + 1) * chunk_bytes).min(buffer.len());
2814 let slice = &buffer[start..end];
2815 if slice.len() == chunk_bytes {
2816 ds.write_chunk(c, slice)
2817 } else {
2818 let mut padded = vec![0u8; chunk_bytes];
2819 padded[..slice.len()].copy_from_slice(slice);
2820 ds.write_chunk(c, &padded)
2821 }
2822 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 chunk write error: {}", e)))?;
2823 }
2824 Ok(())
2825}
2826
2827fn write_ndattr_descriptors(ds: &rust_hdf5::H5Dataset, ad: &AttributeDataset) {
2833 for (aname, avalue) in [
2834 ("NDAttrName", ad.name.as_str()),
2835 ("NDAttrDescription", ad.description.as_str()),
2836 ("NDAttrSourceType", ad.source_type.as_str()),
2837 ("NDAttrSource", ad.source.as_str()),
2838 ] {
2839 if avalue.is_empty() {
2840 continue;
2841 }
2842 let s = rust_hdf5::types::VarLenUnicode(avalue.to_string());
2843 let _ = ds
2844 .new_attr::<rust_hdf5::types::VarLenUnicode>()
2845 .shape(())
2846 .create(aname)
2847 .and_then(|a| a.write_scalar(&s));
2848 }
2849}
2850
2851fn write_ndattr_group_attr(group: &rust_hdf5::H5Group, attr_name: &str, value: &NDAttrValue) {
2857 macro_rules! num {
2858 ($v:expr) => {{
2859 let _ = group.set_attr_numeric(attr_name, &$v);
2860 }};
2861 }
2862 match value {
2863 NDAttrValue::Int8(v) => num!(*v),
2864 NDAttrValue::UInt8(v) => num!(*v),
2865 NDAttrValue::Int16(v) => num!(*v),
2866 NDAttrValue::UInt16(v) => num!(*v),
2867 NDAttrValue::Int32(v) => num!(*v),
2868 NDAttrValue::UInt32(v) => num!(*v),
2869 NDAttrValue::Int64(v) => num!(*v),
2870 NDAttrValue::UInt64(v) => num!(*v),
2871 NDAttrValue::Float32(v) => num!(*v),
2872 NDAttrValue::Float64(v) => num!(*v),
2873 NDAttrValue::String(s) => {
2874 let _ = group.set_attr_string(attr_name, s);
2875 }
2876 NDAttrValue::Undefined => {}
2877 }
2878}
2879
2880fn write_ndattr_dataset_attr(ds: &rust_hdf5::H5Dataset, attr_name: &str, value: &NDAttrValue) {
2887 macro_rules! num {
2888 ($v:expr, $t:ty) => {{
2889 let v: $t = $v;
2890 let _ = ds
2891 .new_attr::<$t>()
2892 .shape(())
2893 .create(attr_name)
2894 .and_then(|a| a.write_numeric(&v));
2895 }};
2896 }
2897 match value {
2898 NDAttrValue::Int8(v) => num!(*v, i8),
2899 NDAttrValue::UInt8(v) => num!(*v, u8),
2900 NDAttrValue::Int16(v) => num!(*v, i16),
2901 NDAttrValue::UInt16(v) => num!(*v, u16),
2902 NDAttrValue::Int32(v) => num!(*v, i32),
2903 NDAttrValue::UInt32(v) => num!(*v, u32),
2904 NDAttrValue::Int64(v) => num!(*v, i64),
2905 NDAttrValue::UInt64(v) => num!(*v, u64),
2906 NDAttrValue::Float32(v) => num!(*v, f32),
2907 NDAttrValue::Float64(v) => num!(*v, f64),
2908 NDAttrValue::String(s) => {
2909 let sv = rust_hdf5::types::VarLenUnicode(s.clone());
2910 let _ = ds
2911 .new_attr::<rust_hdf5::types::VarLenUnicode>()
2912 .shape(())
2913 .create(attr_name)
2914 .and_then(|a| a.write_scalar(&sv));
2915 }
2916 NDAttrValue::Undefined => {}
2917 }
2918}
2919
2920fn write_swmr_ndattr_group_attr(
2925 swmr: &mut SwmrFileWriter,
2926 group_path: &str,
2927 attr_name: &str,
2928 value: &NDAttrValue,
2929) -> ADResult<()> {
2930 let res = match value {
2931 NDAttrValue::Int8(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2932 NDAttrValue::UInt8(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2933 NDAttrValue::Int16(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2934 NDAttrValue::UInt16(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2935 NDAttrValue::Int32(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2936 NDAttrValue::UInt32(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2937 NDAttrValue::Int64(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2938 NDAttrValue::UInt64(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2939 NDAttrValue::Float32(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2940 NDAttrValue::Float64(v) => swmr.set_group_attr_numeric(group_path, attr_name, v),
2941 NDAttrValue::String(s) => swmr.set_group_attr_string(group_path, attr_name, s),
2942 NDAttrValue::Undefined => return Ok(()),
2943 };
2944 res.map_err(|e| {
2945 ADError::UnsupportedConversion(format!(
2946 "SWMR ndattribute group attribute '{}/{}': {}",
2947 group_path, attr_name, e
2948 ))
2949 })
2950}
2951
2952fn write_swmr_ndattr_dataset_attr(
2959 swmr: &mut SwmrFileWriter,
2960 ds_index: usize,
2961 attr_name: &str,
2962 value: &NDAttrValue,
2963) -> ADResult<()> {
2964 let res = match value {
2965 NDAttrValue::Int8(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2966 NDAttrValue::UInt8(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2967 NDAttrValue::Int16(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2968 NDAttrValue::UInt16(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2969 NDAttrValue::Int32(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2970 NDAttrValue::UInt32(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2971 NDAttrValue::Int64(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2972 NDAttrValue::UInt64(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2973 NDAttrValue::Float32(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2974 NDAttrValue::Float64(v) => swmr.set_dataset_attr_numeric(ds_index, attr_name, v),
2975 NDAttrValue::String(s) => swmr.set_dataset_attr_string(ds_index, attr_name, s),
2976 NDAttrValue::Undefined => return Ok(()),
2977 };
2978 res.map_err(|e| {
2979 ADError::UnsupportedConversion(format!(
2980 "SWMR ndattribute dataset attribute '{}': {}",
2981 attr_name, e
2982 ))
2983 })
2984}
2985
2986fn write_group_constant_attrs(
2992 group: &rust_hdf5::H5Group,
2993 attrs: &[crate::hdf5_layout::LayoutAttribute],
2994) {
2995 use crate::hdf5_layout::{LayoutDataType, LayoutSource};
2996 for a in attrs {
2997 if a.source != LayoutSource::Constant {
2998 continue;
2999 }
3000 let _ = match a.data_type {
3001 LayoutDataType::Int => {
3002 group.set_attr_numeric(&a.name, &a.value.trim().parse::<i64>().unwrap_or(0))
3003 }
3004 LayoutDataType::Float => {
3005 group.set_attr_numeric(&a.name, &a.value.trim().parse::<f64>().unwrap_or(0.0))
3006 }
3007 LayoutDataType::String => group.set_attr_string(&a.name, &a.value),
3008 };
3009 }
3010}
3011
3012fn write_swmr_group_constant_attrs(
3016 swmr: &mut SwmrFileWriter,
3017 group_path: &str,
3018 attrs: &[crate::hdf5_layout::LayoutAttribute],
3019) -> ADResult<()> {
3020 use crate::hdf5_layout::{LayoutDataType, LayoutSource};
3021 for a in attrs {
3022 if a.source != LayoutSource::Constant {
3023 continue;
3024 }
3025 match a.data_type {
3026 LayoutDataType::Int => swmr.set_group_attr_numeric(
3027 group_path,
3028 &a.name,
3029 &a.value.trim().parse::<i64>().unwrap_or(0),
3030 ),
3031 LayoutDataType::Float => swmr.set_group_attr_numeric(
3032 group_path,
3033 &a.name,
3034 &a.value.trim().parse::<f64>().unwrap_or(0.0),
3035 ),
3036 LayoutDataType::String => swmr.set_group_attr_string(group_path, &a.name, &a.value),
3037 }
3038 .map_err(|e| {
3039 ADError::UnsupportedConversion(format!(
3040 "SWMR layout group attribute '{}/{}': {}",
3041 group_path, a.name, e
3042 ))
3043 })?;
3044 }
3045 Ok(())
3046}
3047
3048impl Default for Hdf5Writer {
3049 fn default() -> Self {
3050 Self::new()
3051 }
3052}
3053
3054impl NDFileWriter for Hdf5Writer {
3055 fn open_file(&mut self, path: &Path, mode: NDFileMode, array: &NDArray) -> ADResult<()> {
3056 self.current_path = Some(path.to_path_buf());
3057 self.open_mode = mode;
3058 self.frame_count = 0;
3059 self.total_runtime = 0.0;
3060 self.total_bytes = 0;
3061 self.swmr_cb_counter = 0;
3062 self.open_data_type = None;
3063 self.open_frame_dims = None;
3064 self.open_codec = None;
3065 self.perf_rows.clear();
3066 self.perf_prev = None;
3067 self.perf_first = None;
3068 self.attr_datasets.clear();
3069 self.file_attributes.open(array);
3073 self.resolve_layout_paths();
3076 self.seed_ndattr_element_values();
3079
3080 if self.swmr_mode && mode == NDFileMode::Stream {
3081 self.open_swmr(path, array)
3082 } else {
3083 let h5file = H5File::create(path)
3084 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 create error: {}", e)))?;
3085 self.handle = Some(Hdf5Handle::Standard {
3086 file: h5file,
3087 detectors: std::collections::HashMap::new(),
3088 });
3089 Ok(())
3090 }
3091 }
3092
3093 fn set_num_capture(&mut self, n: usize) {
3094 self.num_capture = n;
3095 }
3096
3097 fn write_file(&mut self, array: &NDArray) -> ADResult<()> {
3098 let start = std::time::Instant::now();
3099
3100 self.file_attributes.frame(array);
3103
3104 let is_swmr = matches!(self.handle, Some(Hdf5Handle::Swmr { .. }));
3105 if is_swmr {
3106 self.write_swmr(array)?;
3107 } else {
3108 self.write_standard(array)?;
3109 }
3110 self.update_ndattr_element_values();
3113 self.frame_count += 1;
3114
3115 let elapsed = start.elapsed().as_secs_f64();
3116 let frame_bytes = array.data.as_u8_slice().len();
3117 if self.store_performance {
3118 self.total_runtime += elapsed;
3119 self.total_bytes += frame_bytes as u64;
3120 self.record_performance(elapsed, frame_bytes);
3121 }
3122 Ok(())
3123 }
3124
3125 fn read_file(&mut self) -> ADResult<NDArray> {
3126 self.resolve_layout_paths();
3130 let dataset_path = self.resolved_dataset_path.clone();
3131 let path = self
3132 .current_path
3133 .as_ref()
3134 .ok_or_else(|| ADError::UnsupportedConversion("no file open".into()))?;
3135
3136 let h5file = H5File::open(path)
3137 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 open error: {}", e)))?;
3138
3139 let ds = h5file
3140 .dataset(&dataset_path)
3141 .map_err(|e| ADError::UnsupportedConversion(format!("HDF5 dataset error: {}", e)))?;
3142
3143 let shape = ds.shape();
3144 let dims: Vec<NDDimension> = shape.iter().rev().map(|&s| NDDimension::new(s)).collect();
3145 let element_size = ds.element_size();
3146
3147 let recorded: Option<NDDataType> = ds
3149 .attr(DTYPE_ATTR)
3150 .ok()
3151 .and_then(|a| a.read_numeric::<i32>().ok())
3152 .and_then(|v| NDDataType::from_ordinal(v as u8));
3153
3154 let data_type = recorded.unwrap_or(match element_size {
3155 1 => NDDataType::UInt8,
3156 2 => NDDataType::UInt16,
3157 4 => NDDataType::Float32,
3158 8 => NDDataType::Float64,
3159 other => {
3160 return Err(ADError::UnsupportedConversion(format!(
3161 "unsupported HDF5 element size {}",
3162 other
3163 )));
3164 }
3165 });
3166
3167 macro_rules! read_typed {
3168 ($t:ty, $variant:ident) => {{
3169 let data = ds.read_raw::<$t>().map_err(|e| {
3170 ADError::UnsupportedConversion(format!("HDF5 read error: {}", e))
3171 })?;
3172 let mut arr = NDArray::new(dims, data_type);
3173 arr.data = NDDataBuffer::$variant(data);
3174 return Ok(arr);
3175 }};
3176 }
3177
3178 match data_type {
3179 NDDataType::Int8 => read_typed!(i8, I8),
3180 NDDataType::UInt8 => read_typed!(u8, U8),
3181 NDDataType::Int16 => read_typed!(i16, I16),
3182 NDDataType::UInt16 => read_typed!(u16, U16),
3183 NDDataType::Int32 => read_typed!(i32, I32),
3184 NDDataType::UInt32 => read_typed!(u32, U32),
3185 NDDataType::Int64 => read_typed!(i64, I64),
3186 NDDataType::UInt64 => read_typed!(u64, U64),
3187 NDDataType::Float32 => read_typed!(f32, F32),
3188 NDDataType::Float64 => read_typed!(f64, F64),
3189 }
3190 }
3191
3192 fn close_file(&mut self) -> ADResult<()> {
3201 let mut closing = Finalize::new(self, |w: &mut Self| {
3202 w.handle = None;
3203 w.current_path = None;
3204 });
3205 closing.run(|w| {
3206 match w.handle {
3207 Some(Hdf5Handle::Standard { .. }) => {
3208 w.finalize_standard_datasets()?;
3212 w.flush_attribute_datasets()?;
3213 w.flush_performance_dataset()?;
3214 match w.handle {
3217 Some(Hdf5Handle::Standard {
3218 ref file,
3219 ref detectors,
3220 }) => {
3221 w.build_layout_hardlinks(file)?;
3222 w.flush_ndattr_element_attrs(file, detectors)?;
3226 }
3227 _ => unreachable!("handle is Standard in this arm"),
3228 }
3229 if let Some(Hdf5Handle::Standard { file, detectors }) = w.handle.take() {
3235 if w.fsync_on_close {
3236 drop(file);
3237 } else {
3238 file.close_no_sync().map_err(|e| {
3239 ADError::UnsupportedConversion(format!(
3240 "HDF5 close_no_sync error: {}",
3241 e
3242 ))
3243 })?;
3244 }
3245 drop(detectors);
3246 }
3247 }
3248 Some(Hdf5Handle::Swmr { .. }) => {
3249 if let Some(Hdf5Handle::Swmr { mut writer, .. }) = w.handle.take() {
3256 writer.flush().map_err(|e| {
3263 ADError::UnsupportedConversion(format!(
3264 "SWMR flush-on-close error: {}",
3265 e
3266 ))
3267 })?;
3268 writer.close().map_err(|e| {
3269 ADError::UnsupportedConversion(format!("SWMR close error: {}", e))
3270 })?;
3271 }
3272 }
3273 None => {}
3274 }
3275 Ok(())
3276 })
3277 }
3278
3279 fn supports_multiple_arrays(&self) -> bool {
3280 true
3281 }
3282}
3283
3284#[derive(Default)]
3290struct Hdf5ParamIndices {
3291 compression_type: Option<usize>,
3292 z_compress_level: Option<usize>,
3293 szip_num_pixels: Option<usize>,
3294 nbit_precision: Option<usize>,
3295 nbit_offset: Option<usize>,
3296 jpeg_quality: Option<usize>,
3297 blosc_shuffle_type: Option<usize>,
3298 blosc_compressor: Option<usize>,
3299 blosc_compress_level: Option<usize>,
3300 store_attributes: Option<usize>,
3301 store_performance: Option<usize>,
3302 total_runtime: Option<usize>,
3303 total_io_speed: Option<usize>,
3304 swmr_mode: Option<usize>,
3305 swmr_flush_now: Option<usize>,
3306 swmr_running: Option<usize>,
3307 swmr_cb_counter: Option<usize>,
3308 swmr_supported: Option<usize>,
3309 flush_nth_frame: Option<usize>,
3310 chunk_size_auto: Option<usize>,
3311 n_row_chunks: Option<usize>,
3312 n_col_chunks: Option<usize>,
3313 n_frames_chunks: Option<usize>,
3314 ndattr_chunk: Option<usize>,
3315 n_extra_dims: Option<usize>,
3316 extra_dim_size: [Option<usize>; MAX_EXTRA_DIMS],
3317 extra_dim_name: [Option<usize>; MAX_EXTRA_DIMS],
3318 fill_value: Option<usize>,
3319 dim_att_datasets: Option<usize>,
3320 layout_filename: Option<usize>,
3321 layout_valid: Option<usize>,
3322 layout_error_msg: Option<usize>,
3323}
3324
3325pub struct Hdf5FileProcessor {
3327 ctrl: Mutex<FilePluginController<Hdf5Writer>>,
3328 hdf5_params: Hdf5ParamIndices,
3329}
3330
3331impl Hdf5FileProcessor {
3332 pub fn new() -> Self {
3333 Self {
3334 ctrl: Mutex::new(FilePluginController::new(Hdf5Writer::new())),
3335 hdf5_params: Hdf5ParamIndices::default(),
3336 }
3337 }
3338
3339 pub fn set_dataset_name(&mut self, name: &str) {
3340 self.ctrl.lock().writer.set_dataset_name(name);
3341 }
3342}
3343
3344fn register_hdf5_params(
3346 base: &mut asyn_rs::port::PortDriverBase,
3347) -> asyn_rs::error::AsynResult<()> {
3348 use asyn_rs::param::ParamType;
3349 base.create_param("HDF5_SWMRFlushNow", ParamType::Int32)?;
3350 base.create_param("HDF5_chunkSizeAuto", ParamType::Int32)?;
3351 base.create_param("HDF5_nRowChunks", ParamType::Int32)?;
3352 base.create_param("HDF5_nColChunks", ParamType::Int32)?;
3353 base.create_param("HDF5_chunkSize2", ParamType::Int32)?;
3354 base.create_param("HDF5_chunkSize3", ParamType::Int32)?;
3355 base.create_param("HDF5_chunkSize4", ParamType::Int32)?;
3356 base.create_param("HDF5_chunkSize5", ParamType::Int32)?;
3357 base.create_param("HDF5_chunkSize6", ParamType::Int32)?;
3358 base.create_param("HDF5_chunkSize7", ParamType::Int32)?;
3359 base.create_param("HDF5_chunkSize8", ParamType::Int32)?;
3360 base.create_param("HDF5_chunkSize9", ParamType::Int32)?;
3361 base.create_param("HDF5_nFramesChunks", ParamType::Int32)?;
3362 base.create_param("HDF5_NDAttributeChunk", ParamType::Int32)?;
3363 base.create_param("HDF5_chunkBoundaryAlign", ParamType::Int32)?;
3364 base.create_param("HDF5_chunkBoundaryThreshold", ParamType::Int32)?;
3365 base.create_param("HDF5_nExtraDims", ParamType::Int32)?;
3366 base.create_param("HDF5_extraDimSizeN", ParamType::Int32)?;
3367 base.create_param("HDF5_extraDimNameN", ParamType::Octet)?;
3368 base.create_param("HDF5_extraDimSizeX", ParamType::Int32)?;
3369 base.create_param("HDF5_extraDimNameX", ParamType::Octet)?;
3370 base.create_param("HDF5_extraDimSizeY", ParamType::Int32)?;
3371 base.create_param("HDF5_extraDimNameY", ParamType::Octet)?;
3372 base.create_param("HDF5_extraDimSize3", ParamType::Int32)?;
3373 base.create_param("HDF5_extraDimName3", ParamType::Octet)?;
3374 base.create_param("HDF5_extraDimSize4", ParamType::Int32)?;
3375 base.create_param("HDF5_extraDimName4", ParamType::Octet)?;
3376 base.create_param("HDF5_extraDimSize5", ParamType::Int32)?;
3377 base.create_param("HDF5_extraDimName5", ParamType::Octet)?;
3378 base.create_param("HDF5_extraDimSize6", ParamType::Int32)?;
3379 base.create_param("HDF5_extraDimName6", ParamType::Octet)?;
3380 base.create_param("HDF5_extraDimSize7", ParamType::Int32)?;
3381 base.create_param("HDF5_extraDimName7", ParamType::Octet)?;
3382 base.create_param("HDF5_extraDimSize8", ParamType::Int32)?;
3383 base.create_param("HDF5_extraDimName8", ParamType::Octet)?;
3384 base.create_param("HDF5_extraDimSize9", ParamType::Int32)?;
3385 base.create_param("HDF5_extraDimName9", ParamType::Octet)?;
3386 base.create_param("HDF5_storeAttributes", ParamType::Int32)?;
3387 base.create_param("HDF5_storePerformance", ParamType::Int32)?;
3388 base.create_param("HDF5_totalRuntime", ParamType::Float64)?;
3389 base.create_param("HDF5_totalIoSpeed", ParamType::Float64)?;
3390 base.create_param("HDF5_flushNthFrame", ParamType::Int32)?;
3391 base.create_param("HDF5_compressionType", ParamType::Int32)?;
3392 base.create_param("HDF5_nbitsPrecision", ParamType::Int32)?;
3393 base.create_param("HDF5_nbitsOffset", ParamType::Int32)?;
3394 base.create_param("HDF5_szipNumPixels", ParamType::Int32)?;
3395 base.create_param("HDF5_zCompressLevel", ParamType::Int32)?;
3396 base.create_param("HDF5_bloscShuffleType", ParamType::Int32)?;
3397 base.create_param("HDF5_bloscCompressor", ParamType::Int32)?;
3398 base.create_param("HDF5_bloscCompressLevel", ParamType::Int32)?;
3399 base.create_param("HDF5_jpegQuality", ParamType::Int32)?;
3400 base.create_param("HDF5_dimAttDatasets", ParamType::Int32)?;
3401 base.create_param("HDF5_layoutErrorMsg", ParamType::Octet)?;
3402 base.create_param("HDF5_layoutValid", ParamType::Int32)?;
3403 base.create_param("HDF5_layoutFilename", ParamType::Octet)?;
3404 base.create_param("HDF5_SWMRSupported", ParamType::Int32)?;
3405 base.create_param("HDF5_SWMRMode", ParamType::Int32)?;
3406 base.create_param("HDF5_SWMRRunning", ParamType::Int32)?;
3407 base.create_param("HDF5_SWMRCbCounter", ParamType::Int32)?;
3408 base.create_param("HDF5_posRunning", ParamType::Int32)?;
3409 base.create_param("HDF5_posNameDimN", ParamType::Octet)?;
3410 base.create_param("HDF5_posNameDimX", ParamType::Octet)?;
3411 base.create_param("HDF5_posNameDimY", ParamType::Octet)?;
3412 base.create_param("HDF5_posNameDim3", ParamType::Octet)?;
3413 base.create_param("HDF5_posNameDim4", ParamType::Octet)?;
3414 base.create_param("HDF5_posNameDim5", ParamType::Octet)?;
3415 base.create_param("HDF5_posNameDim6", ParamType::Octet)?;
3416 base.create_param("HDF5_posNameDim7", ParamType::Octet)?;
3417 base.create_param("HDF5_posNameDim8", ParamType::Octet)?;
3418 base.create_param("HDF5_posNameDim9", ParamType::Octet)?;
3419 base.create_param("HDF5_posIndexDimN", ParamType::Octet)?;
3420 base.create_param("HDF5_posIndexDimX", ParamType::Octet)?;
3421 base.create_param("HDF5_posIndexDimY", ParamType::Octet)?;
3422 base.create_param("HDF5_posIndexDim3", ParamType::Octet)?;
3423 base.create_param("HDF5_posIndexDim4", ParamType::Octet)?;
3424 base.create_param("HDF5_posIndexDim5", ParamType::Octet)?;
3425 base.create_param("HDF5_posIndexDim6", ParamType::Octet)?;
3426 base.create_param("HDF5_posIndexDim7", ParamType::Octet)?;
3427 base.create_param("HDF5_posIndexDim8", ParamType::Octet)?;
3428 base.create_param("HDF5_posIndexDim9", ParamType::Octet)?;
3429 base.create_param("HDF5_fillValue", ParamType::Float64)?;
3430 base.create_param("HDF5_extraDimChunkX", ParamType::Int32)?;
3431 base.create_param("HDF5_extraDimChunkY", ParamType::Int32)?;
3432 base.create_param("HDF5_extraDimChunk3", ParamType::Int32)?;
3433 base.create_param("HDF5_extraDimChunk4", ParamType::Int32)?;
3434 base.create_param("HDF5_extraDimChunk5", ParamType::Int32)?;
3435 base.create_param("HDF5_extraDimChunk6", ParamType::Int32)?;
3436 base.create_param("HDF5_extraDimChunk7", ParamType::Int32)?;
3437 base.create_param("HDF5_extraDimChunk8", ParamType::Int32)?;
3438 base.create_param("HDF5_extraDimChunk9", ParamType::Int32)?;
3439 Ok(())
3440}
3441
3442impl Default for Hdf5FileProcessor {
3443 fn default() -> Self {
3444 Self::new()
3445 }
3446}
3447
3448const EXTRA_DIM_SIZE_PARAMS: [&str; MAX_EXTRA_DIMS] = [
3450 "HDF5_extraDimSizeN",
3451 "HDF5_extraDimSizeX",
3452 "HDF5_extraDimSizeY",
3453 "HDF5_extraDimSize3",
3454 "HDF5_extraDimSize4",
3455 "HDF5_extraDimSize5",
3456 "HDF5_extraDimSize6",
3457 "HDF5_extraDimSize7",
3458 "HDF5_extraDimSize8",
3459 "HDF5_extraDimSize9",
3460];
3461
3462const EXTRA_DIM_NAME_PARAMS: [&str; MAX_EXTRA_DIMS] = [
3464 "HDF5_extraDimNameN",
3465 "HDF5_extraDimNameX",
3466 "HDF5_extraDimNameY",
3467 "HDF5_extraDimName3",
3468 "HDF5_extraDimName4",
3469 "HDF5_extraDimName5",
3470 "HDF5_extraDimName6",
3471 "HDF5_extraDimName7",
3472 "HDF5_extraDimName8",
3473 "HDF5_extraDimName9",
3474];
3475
3476impl NDPluginProcess for Hdf5FileProcessor {
3477 fn process_array(&self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
3478 let mut ctrl = self.ctrl.lock();
3479 let was_swmr = ctrl.writer.is_swmr_active();
3480 let mut result = ctrl.process_array(array);
3481 let is_swmr = ctrl.writer.is_swmr_active();
3482
3483 if was_swmr != is_swmr {
3485 if let Some(idx) = self.hdf5_params.swmr_running {
3486 result
3487 .param_updates
3488 .push(ParamUpdate::int32(idx, if is_swmr { 1 } else { 0 }));
3489 }
3490 }
3491
3492 if is_swmr {
3494 if let Some(idx) = self.hdf5_params.swmr_cb_counter {
3495 result
3496 .param_updates
3497 .push(ParamUpdate::int32(idx, ctrl.writer.swmr_cb_counter as i32));
3498 }
3499 }
3500
3501 if ctrl.writer.store_performance {
3503 if let Some(idx) = self.hdf5_params.total_runtime {
3504 result
3505 .param_updates
3506 .push(ParamUpdate::float64(idx, ctrl.writer.total_runtime));
3507 }
3508 if let Some(idx) = self.hdf5_params.total_io_speed {
3509 let speed = if ctrl.writer.total_runtime > 0.0 {
3510 ctrl.writer.total_bytes as f64 / ctrl.writer.total_runtime / 1_000_000.0
3511 } else {
3512 0.0
3513 };
3514 result.param_updates.push(ParamUpdate::float64(idx, speed));
3515 }
3516 }
3517
3518 result
3519 }
3520
3521 fn plugin_type(&self) -> &str {
3522 "NDFileHDF5"
3523 }
3524
3525 fn compression_aware(&self) -> bool {
3531 true
3532 }
3533
3534 fn does_array_callbacks(&self) -> bool {
3537 false
3538 }
3539
3540 fn register_params(
3541 &mut self,
3542 base: &mut asyn_rs::port::PortDriverBase,
3543 ) -> asyn_rs::error::AsynResult<()> {
3544 self.ctrl.lock().register_params(base)?;
3545 register_hdf5_params(base)?;
3546 self.hdf5_params.compression_type = base.find_param("HDF5_compressionType");
3547 self.hdf5_params.z_compress_level = base.find_param("HDF5_zCompressLevel");
3548 self.hdf5_params.szip_num_pixels = base.find_param("HDF5_szipNumPixels");
3549 self.hdf5_params.nbit_precision = base.find_param("HDF5_nbitsPrecision");
3550 self.hdf5_params.nbit_offset = base.find_param("HDF5_nbitsOffset");
3551 self.hdf5_params.jpeg_quality = base.find_param("HDF5_jpegQuality");
3552 self.hdf5_params.blosc_shuffle_type = base.find_param("HDF5_bloscShuffleType");
3553 self.hdf5_params.blosc_compressor = base.find_param("HDF5_bloscCompressor");
3554 self.hdf5_params.blosc_compress_level = base.find_param("HDF5_bloscCompressLevel");
3555 self.hdf5_params.store_attributes = base.find_param("HDF5_storeAttributes");
3556 self.hdf5_params.store_performance = base.find_param("HDF5_storePerformance");
3557 self.hdf5_params.total_runtime = base.find_param("HDF5_totalRuntime");
3558 self.hdf5_params.total_io_speed = base.find_param("HDF5_totalIoSpeed");
3559 self.hdf5_params.swmr_mode = base.find_param("HDF5_SWMRMode");
3560 self.hdf5_params.swmr_flush_now = base.find_param("HDF5_SWMRFlushNow");
3561 self.hdf5_params.swmr_running = base.find_param("HDF5_SWMRRunning");
3562 self.hdf5_params.swmr_cb_counter = base.find_param("HDF5_SWMRCbCounter");
3563 self.hdf5_params.swmr_supported = base.find_param("HDF5_SWMRSupported");
3564 self.hdf5_params.flush_nth_frame = base.find_param("HDF5_flushNthFrame");
3565 self.hdf5_params.chunk_size_auto = base.find_param("HDF5_chunkSizeAuto");
3566 self.hdf5_params.n_row_chunks = base.find_param("HDF5_nRowChunks");
3567 self.hdf5_params.n_col_chunks = base.find_param("HDF5_nColChunks");
3568 self.hdf5_params.n_frames_chunks = base.find_param("HDF5_nFramesChunks");
3569 self.hdf5_params.ndattr_chunk = base.find_param("HDF5_NDAttributeChunk");
3570 self.hdf5_params.n_extra_dims = base.find_param("HDF5_nExtraDims");
3571 for i in 0..MAX_EXTRA_DIMS {
3572 self.hdf5_params.extra_dim_size[i] = base.find_param(EXTRA_DIM_SIZE_PARAMS[i]);
3573 self.hdf5_params.extra_dim_name[i] = base.find_param(EXTRA_DIM_NAME_PARAMS[i]);
3574 }
3575 self.hdf5_params.fill_value = base.find_param("HDF5_fillValue");
3576 self.hdf5_params.dim_att_datasets = base.find_param("HDF5_dimAttDatasets");
3577 self.hdf5_params.layout_filename = base.find_param("HDF5_layoutFilename");
3578 self.hdf5_params.layout_valid = base.find_param("HDF5_layoutValid");
3579 self.hdf5_params.layout_error_msg = base.find_param("HDF5_layoutErrorMsg");
3580
3581 if let Some(idx) = self.hdf5_params.swmr_supported {
3583 base.set_int32_param(idx, 0, 1)?;
3584 }
3585 Ok(())
3586 }
3587
3588 fn on_param_change(&self, reason: usize, params: &PluginParamSnapshot) -> ParamChangeResult {
3589 let mut ctrl = self.ctrl.lock();
3590 if Some(reason) == self.hdf5_params.compression_type {
3592 ctrl.writer.set_compression_type(params.value.as_i32());
3593 return ParamChangeResult::updates(vec![]);
3594 }
3595 if Some(reason) == self.hdf5_params.z_compress_level {
3596 ctrl.writer
3597 .set_z_compress_level(params.value.as_i32() as u32);
3598 return ParamChangeResult::updates(vec![]);
3599 }
3600 if Some(reason) == self.hdf5_params.szip_num_pixels {
3601 ctrl.writer
3602 .set_szip_num_pixels(params.value.as_i32() as u32);
3603 return ParamChangeResult::updates(vec![]);
3604 }
3605 if Some(reason) == self.hdf5_params.blosc_shuffle_type {
3606 ctrl.writer.set_blosc_shuffle_type(params.value.as_i32());
3607 return ParamChangeResult::updates(vec![]);
3608 }
3609 if Some(reason) == self.hdf5_params.blosc_compressor {
3610 ctrl.writer.set_blosc_compressor(params.value.as_i32());
3611 return ParamChangeResult::updates(vec![]);
3612 }
3613 if Some(reason) == self.hdf5_params.blosc_compress_level {
3614 ctrl.writer
3615 .set_blosc_compress_level(params.value.as_i32() as u32);
3616 return ParamChangeResult::updates(vec![]);
3617 }
3618 if Some(reason) == self.hdf5_params.nbit_precision {
3619 ctrl.writer.set_nbit_precision(params.value.as_i32() as u32);
3620 return ParamChangeResult::updates(vec![]);
3621 }
3622 if Some(reason) == self.hdf5_params.nbit_offset {
3623 ctrl.writer.set_nbit_offset(params.value.as_i32() as u32);
3624 return ParamChangeResult::updates(vec![]);
3625 }
3626 if Some(reason) == self.hdf5_params.jpeg_quality {
3627 ctrl.writer.set_jpeg_quality(params.value.as_i32() as u32);
3628 return ParamChangeResult::updates(vec![]);
3629 }
3630 if Some(reason) == self.hdf5_params.store_attributes {
3631 ctrl.writer.set_store_attributes(params.value.as_i32() != 0);
3632 return ParamChangeResult::updates(vec![]);
3633 }
3634 if Some(reason) == self.hdf5_params.store_performance {
3635 ctrl.writer
3636 .set_store_performance(params.value.as_i32() != 0);
3637 return ParamChangeResult::updates(vec![]);
3638 }
3639 if Some(reason) == self.hdf5_params.chunk_size_auto {
3641 ctrl.writer.set_chunk_size_auto(params.value.as_i32() != 0);
3642 return ParamChangeResult::updates(vec![]);
3643 }
3644 if Some(reason) == self.hdf5_params.n_row_chunks {
3645 ctrl.writer
3646 .set_n_row_chunks(params.value.as_i32().max(0) as usize);
3647 return ParamChangeResult::updates(vec![]);
3648 }
3649 if Some(reason) == self.hdf5_params.n_col_chunks {
3650 ctrl.writer
3651 .set_n_col_chunks(params.value.as_i32().max(0) as usize);
3652 return ParamChangeResult::updates(vec![]);
3653 }
3654 if Some(reason) == self.hdf5_params.n_frames_chunks {
3655 ctrl.writer
3656 .set_n_frames_chunks(params.value.as_i32().max(0) as usize);
3657 return ParamChangeResult::updates(vec![]);
3658 }
3659 if Some(reason) == self.hdf5_params.ndattr_chunk {
3660 ctrl.writer
3662 .set_ndattr_chunk(params.value.as_i32().max(0) as usize);
3663 return ParamChangeResult::updates(vec![]);
3664 }
3665 if Some(reason) == self.hdf5_params.n_extra_dims {
3667 ctrl.writer
3668 .set_n_extra_dims(params.value.as_i32().max(0) as usize);
3669 return ParamChangeResult::updates(vec![]);
3670 }
3671 for i in 0..MAX_EXTRA_DIMS {
3672 if Some(reason) == self.hdf5_params.extra_dim_size[i] {
3673 ctrl.writer
3674 .set_extra_dim_size(i, params.value.as_i32().max(1) as usize);
3675 return ParamChangeResult::updates(vec![]);
3676 }
3677 if Some(reason) == self.hdf5_params.extra_dim_name[i] {
3678 ctrl.writer
3679 .set_extra_dim_name(i, params.value.as_string().unwrap_or(""));
3680 return ParamChangeResult::updates(vec![]);
3681 }
3682 }
3683 if Some(reason) == self.hdf5_params.fill_value {
3684 ctrl.writer.set_fill_value(params.value.as_f64());
3685 return ParamChangeResult::updates(vec![]);
3686 }
3687 if Some(reason) == self.hdf5_params.dim_att_datasets {
3688 ctrl.writer.set_dim_att_datasets(params.value.as_i32() != 0);
3689 return ParamChangeResult::updates(vec![]);
3690 }
3691 if Some(reason) == self.hdf5_params.layout_filename {
3693 let path = params.value.as_string().unwrap_or("").to_string();
3694 ctrl.writer.set_layout_filename(&path);
3695 let mut updates = vec![];
3696 if let Some(idx) = self.hdf5_params.layout_valid {
3697 updates.push(ParamUpdate::int32(
3698 idx,
3699 if ctrl.writer.layout_valid { 1 } else { 0 },
3700 ));
3701 }
3702 if let Some(idx) = self.hdf5_params.layout_error_msg {
3703 updates.push(ParamUpdate::Octet {
3704 reason: idx,
3705 addr: 0,
3706 value: ctrl.writer.layout_error.clone(),
3707 });
3708 }
3709 return ParamChangeResult::updates(updates);
3710 }
3711 if Some(reason) == self.hdf5_params.swmr_mode {
3713 ctrl.writer.set_swmr_mode(params.value.as_i32() != 0);
3714 return ParamChangeResult::updates(vec![]);
3715 }
3716 if Some(reason) == self.hdf5_params.swmr_flush_now {
3717 if params.value.as_i32() != 0 {
3718 ctrl.writer.flush_swmr();
3719 let mut updates = vec![];
3720 if let Some(idx) = self.hdf5_params.swmr_cb_counter {
3721 updates.push(ParamUpdate::int32(idx, ctrl.writer.swmr_cb_counter as i32));
3722 }
3723 return ParamChangeResult::updates(updates);
3724 }
3725 return ParamChangeResult::updates(vec![]);
3726 }
3727 if Some(reason) == self.hdf5_params.flush_nth_frame {
3728 ctrl.writer
3729 .set_flush_nth_frame(params.value.as_i32().max(0) as usize);
3730 return ParamChangeResult::updates(vec![]);
3731 }
3732 ctrl.on_param_change(reason, params)
3733 }
3734}
3735
3736#[cfg(test)]
3737mod tests {
3738 use super::*;
3739 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
3740 use rust_hdf5::format::messages::filter::FILTER_LZ4;
3741 use std::sync::atomic::{AtomicU32, Ordering};
3742
3743 static TEST_COUNTER: AtomicU32 = AtomicU32::new(0);
3744
3745 fn temp_path(prefix: &str) -> PathBuf {
3746 let n = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
3747 std::env::temp_dir().join(format!(
3748 "adcore_test_{}_{}_{}.h5",
3749 std::process::id(),
3750 prefix,
3751 n
3752 ))
3753 }
3754
3755 #[test]
3756 fn test_write_single_frame() {
3757 let path = temp_path("hdf5_single");
3758 let mut writer = Hdf5Writer::new();
3759
3760 let mut arr = NDArray::new(
3761 vec![NDDimension::new(4), NDDimension::new(4)],
3762 NDDataType::UInt8,
3763 );
3764 if let NDDataBuffer::U8(ref mut v) = arr.data {
3765 for i in 0..16 {
3766 v[i] = i as u8;
3767 }
3768 }
3769
3770 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
3771 writer.write_file(&arr).unwrap();
3772 writer.close_file().unwrap();
3773
3774 let h5 = H5File::open(&path).unwrap();
3776 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3777 assert_eq!(ds.shape(), vec![1, 4, 4]);
3778 let data: Vec<u8> = ds.read_raw().unwrap();
3779 assert_eq!(data[0], 0);
3780 assert_eq!(data[15], 15);
3781 drop(h5);
3782
3783 let mut reader = Hdf5Writer::new();
3784 reader.current_path = Some(path.clone());
3785 let read_arr = reader.read_file().unwrap();
3786 assert_eq!(read_arr.dims.len(), 3);
3787 assert_eq!(read_arr.dims[2].size, 1); std::fs::remove_file(&path).ok();
3790 }
3791
3792 #[test]
3793 fn test_parse_fsync_on_close_env() {
3794 assert!(Hdf5Writer::parse_fsync_on_close_env(None));
3796 for v in ["0", "false", "no", "off", "FALSE", "Off", " no "] {
3798 assert!(
3799 !Hdf5Writer::parse_fsync_on_close_env(Some(v)),
3800 "{v:?} should disable fsync-on-close"
3801 );
3802 }
3803 for v in ["", "1", "true", "yes", "on", "durable"] {
3805 assert!(
3806 Hdf5Writer::parse_fsync_on_close_env(Some(v)),
3807 "{v:?} should keep fsync-on-close"
3808 );
3809 }
3810 }
3811
3812 #[test]
3813 fn test_no_fsync_close_writes_valid_file() {
3814 let path = temp_path("hdf5_no_fsync");
3818 let mut writer = Hdf5Writer::new();
3819 writer.fsync_on_close = false;
3820
3821 let mut arr = NDArray::new(
3822 vec![NDDimension::new(4), NDDimension::new(4)],
3823 NDDataType::UInt8,
3824 );
3825 if let NDDataBuffer::U8(ref mut v) = arr.data {
3826 for i in 0..16 {
3827 v[i] = i as u8;
3828 }
3829 }
3830
3831 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
3832 writer.write_file(&arr).unwrap();
3833 writer.close_file().unwrap();
3834
3835 let h5 = H5File::open(&path).unwrap();
3836 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3837 assert_eq!(ds.shape(), vec![1, 4, 4]);
3838 let data: Vec<u8> = ds.read_raw().unwrap();
3839 assert_eq!(data[0], 0);
3840 assert_eq!(data[15], 15);
3841 drop(h5);
3842
3843 std::fs::remove_file(&path).ok();
3844 }
3845
3846 #[test]
3847 fn test_write_multiple_frames() {
3848 let path = temp_path("hdf5_multi");
3849 let mut writer = Hdf5Writer::new();
3850
3851 let mut arr = NDArray::new(
3852 vec![NDDimension::new(4), NDDimension::new(4)],
3853 NDDataType::UInt8,
3854 );
3855 for f in 0..3u8 {
3857 if let NDDataBuffer::U8(ref mut v) = arr.data {
3858 for x in v.iter_mut() {
3859 *x = f;
3860 }
3861 }
3862 if f == 0 {
3863 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3864 }
3865 writer.write_file(&arr).unwrap();
3866 }
3867 writer.close_file().unwrap();
3868
3869 assert!(writer.supports_multiple_arrays());
3870 assert_eq!(writer.frame_count(), 3);
3871
3872 let data = std::fs::read(&path).unwrap();
3873 assert_eq!(&data[0..8], b"\x89HDF\r\n\x1a\n");
3874
3875 let h5 = H5File::open(&path).unwrap();
3877 let names = h5.dataset_names();
3878 assert!(names.contains(&"entry/instrument/detector/data".to_string()));
3879 assert!(
3880 !names.contains(&"data_1".to_string()),
3881 "must not write per-frame datasets"
3882 );
3883 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3884 assert_eq!(
3885 ds.shape(),
3886 vec![3, 4, 4],
3887 "rank/shape must be [nframes,Y,X]"
3888 );
3889 let raw: Vec<u8> = ds.read_raw().unwrap();
3890 assert_eq!(raw.len(), 3 * 4 * 4);
3891 assert_eq!(raw[0], 0);
3893 assert_eq!(raw[16], 1);
3894 assert_eq!(raw[32], 2);
3895
3896 std::fs::remove_file(&path).ok();
3897 }
3898
3899 #[test]
3900 fn test_sub_frame_chunking() {
3901 let path = temp_path("hdf5_subchunk");
3905 let mut writer = Hdf5Writer::new();
3906 writer.set_chunk_size_auto(false); writer.set_n_row_chunks(4); writer.set_n_col_chunks(4); let mut arr = NDArray::new(
3911 vec![NDDimension::new(8), NDDimension::new(8)],
3912 NDDataType::UInt16,
3913 );
3914 for f in 0..3u16 {
3915 if let NDDataBuffer::U16(ref mut v) = arr.data {
3916 for (i, x) in v.iter_mut().enumerate() {
3917 *x = f * 1000 + i as u16;
3918 }
3919 }
3920 if f == 0 {
3921 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3922 }
3923 writer.write_file(&arr).unwrap();
3924 }
3925 writer.close_file().unwrap();
3926
3927 let h5 = H5File::open(&path).unwrap();
3928 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3929 assert_eq!(ds.shape(), vec![3, 8, 8], "shape must not be chunk-padded");
3930 assert_eq!(
3931 ds.chunk_dims(),
3932 Some(vec![1, 4, 4]),
3933 "chunk grid must be the sub-frame tile size"
3934 );
3935 let raw: Vec<u16> = ds.read_raw().unwrap();
3936 assert_eq!(raw.len(), 3 * 64);
3937 for f in 0..3u16 {
3938 for i in 0..64usize {
3939 assert_eq!(
3940 raw[f as usize * 64 + i],
3941 f * 1000 + i as u16,
3942 "frame {} elem {}",
3943 f,
3944 i
3945 );
3946 }
3947 }
3948
3949 std::fs::remove_file(&path).ok();
3950 }
3951
3952 #[test]
3953 fn test_sub_frame_chunking_with_compression() {
3954 let path = temp_path("hdf5_subchunk_zlib");
3957 let mut writer = Hdf5Writer::new();
3958 writer.set_chunk_size_auto(false);
3959 writer.set_n_row_chunks(4);
3960 writer.set_n_col_chunks(4);
3961 writer.set_compression_type(COMPRESS_ZLIB);
3962
3963 let mut arr = NDArray::new(
3964 vec![NDDimension::new(8), NDDimension::new(8)],
3965 NDDataType::UInt16,
3966 );
3967 for f in 0..2u16 {
3968 if let NDDataBuffer::U16(ref mut v) = arr.data {
3969 for (i, x) in v.iter_mut().enumerate() {
3970 *x = f * 100 + i as u16;
3971 }
3972 }
3973 if f == 0 {
3974 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
3975 }
3976 writer.write_file(&arr).unwrap();
3977 }
3978 writer.close_file().unwrap();
3979
3980 let h5 = H5File::open(&path).unwrap();
3981 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
3982 assert_eq!(ds.shape(), vec![2, 8, 8]);
3983 assert_eq!(ds.chunk_dims(), Some(vec![1, 4, 4]));
3984 let raw: Vec<u16> = ds.read_raw().unwrap();
3985 for f in 0..2u16 {
3986 for i in 0..64usize {
3987 assert_eq!(raw[f as usize * 64 + i], f * 100 + i as u16);
3988 }
3989 }
3990
3991 std::fs::remove_file(&path).ok();
3992 }
3993
3994 #[test]
3995 fn test_non_dividing_chunk_is_honored_and_extent_trimmed() {
3996 let path = temp_path("hdf5_subchunk_nd");
4000 let mut writer = Hdf5Writer::new();
4001 writer.set_chunk_size_auto(false); writer.set_n_row_chunks(3); writer.set_n_col_chunks(4); let mut arr = NDArray::new(
4006 vec![NDDimension::new(8), NDDimension::new(8)],
4007 NDDataType::UInt16,
4008 );
4009 if let NDDataBuffer::U16(ref mut v) = arr.data {
4010 for (i, x) in v.iter_mut().enumerate() {
4011 *x = i as u16;
4012 }
4013 }
4014 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4015 writer.write_file(&arr).unwrap();
4016 writer.write_file(&arr).unwrap();
4017 writer.close_file().unwrap();
4018
4019 let h5 = H5File::open(&path).unwrap();
4020 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4021 assert_eq!(ds.shape(), vec![2, 8, 8], "extent trimmed, not padded");
4022 assert_eq!(ds.chunk_dims(), Some(vec![1, 3, 4]));
4023 let raw: Vec<u16> = ds.read_raw().unwrap();
4024 assert_eq!(raw.len(), 2 * 64);
4025 for i in 0..64usize {
4026 assert_eq!(raw[i], i as u16);
4027 assert_eq!(raw[64 + i], i as u16);
4028 }
4029
4030 std::fs::remove_file(&path).ok();
4031 }
4032
4033 #[test]
4034 fn test_n_frames_chunks_band() {
4035 let path = temp_path("hdf5_framechunks");
4038 let mut writer = Hdf5Writer::new();
4039 writer.set_chunk_size_auto(false);
4040 writer.set_n_frames_chunks(2); let mut arr = NDArray::new(
4043 vec![NDDimension::new(4), NDDimension::new(4)],
4044 NDDataType::UInt16,
4045 );
4046 for f in 0..5u16 {
4048 if let NDDataBuffer::U16(ref mut v) = arr.data {
4049 for (i, x) in v.iter_mut().enumerate() {
4050 *x = f * 1000 + i as u16;
4051 }
4052 }
4053 if f == 0 {
4054 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4055 }
4056 writer.write_file(&arr).unwrap();
4057 }
4058 writer.close_file().unwrap();
4059
4060 let h5 = H5File::open(&path).unwrap();
4061 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4062 assert_eq!(ds.shape(), vec![5, 4, 4], "exact frame count, no padding");
4063 assert_eq!(ds.chunk_dims(), Some(vec![2, 4, 4]));
4064 let raw: Vec<u16> = ds.read_raw().unwrap();
4065 for f in 0..5u16 {
4066 for i in 0..16usize {
4067 assert_eq!(raw[f as usize * 16 + i], f * 1000 + i as u16);
4068 }
4069 }
4070
4071 std::fs::remove_file(&path).ok();
4072 }
4073
4074 #[test]
4075 fn test_frames_chunks_with_sub_frame_tiles() {
4076 let path = temp_path("hdf5_full_chunk");
4080 let mut writer = Hdf5Writer::new();
4081 writer.set_chunk_size_auto(false);
4082 writer.set_n_frames_chunks(2); writer.set_n_row_chunks(4); writer.set_n_col_chunks(4); let mut arr = NDArray::new(
4087 vec![NDDimension::new(8), NDDimension::new(8)],
4088 NDDataType::UInt16,
4089 );
4090 for f in 0..3u16 {
4092 if let NDDataBuffer::U16(ref mut v) = arr.data {
4093 for (i, x) in v.iter_mut().enumerate() {
4094 *x = f * 1000 + i as u16;
4095 }
4096 }
4097 if f == 0 {
4098 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4099 }
4100 writer.write_file(&arr).unwrap();
4101 }
4102 writer.close_file().unwrap();
4103
4104 let h5 = H5File::open(&path).unwrap();
4105 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4106 assert_eq!(ds.shape(), vec![3, 8, 8], "exact frame count");
4107 assert_eq!(ds.chunk_dims(), Some(vec![2, 4, 4]));
4108 let raw: Vec<u16> = ds.read_raw().unwrap();
4109 assert_eq!(raw.len(), 3 * 64);
4110 for f in 0..3u16 {
4111 for i in 0..64usize {
4112 assert_eq!(
4113 raw[f as usize * 64 + i],
4114 f * 1000 + i as u16,
4115 "frame {} elem {}",
4116 f,
4117 i
4118 );
4119 }
4120 }
4121
4122 std::fs::remove_file(&path).ok();
4123 }
4124
4125 #[test]
4126 fn test_attribute_datasets() {
4127 let path = temp_path("hdf5_attr_ds");
4128 let mut writer = Hdf5Writer::new();
4129
4130 let mk = |exposure: f64, count: i32| {
4131 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4132 arr.attributes.add(NDAttribute::new_static(
4133 "exposure",
4134 "",
4135 NDAttrSource::Driver,
4136 NDAttrValue::Float64(exposure),
4137 ));
4138 arr.attributes.add(NDAttribute::new_static(
4139 "count",
4140 "",
4141 NDAttrSource::Driver,
4142 NDAttrValue::Int32(count),
4143 ));
4144 arr
4145 };
4146
4147 let a0 = mk(0.5, 10);
4148 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4149 writer.write_file(&a0).unwrap();
4150 writer.write_file(&mk(0.75, 20)).unwrap();
4151 writer.write_file(&mk(1.25, 30)).unwrap();
4152 writer.close_file().unwrap();
4153
4154 let h5 = H5File::open(&path).unwrap();
4155 let exp = h5
4157 .dataset("entry/instrument/NDAttributes/exposure")
4158 .unwrap();
4159 assert_eq!(exp.shape(), vec![3]);
4160 let exp_vals: Vec<f64> = exp.read_raw().unwrap();
4161 assert_eq!(exp_vals, vec![0.5, 0.75, 1.25]);
4162
4163 let cnt = h5.dataset("entry/instrument/NDAttributes/count").unwrap();
4164 assert_eq!(cnt.shape(), vec![3]);
4165 let cnt_vals: Vec<i32> = cnt.read_raw().unwrap();
4167 assert_eq!(cnt_vals, vec![10, 20, 30]);
4168
4169 std::fs::remove_file(&path).ok();
4170 }
4171
4172 #[test]
4180 fn attribute_that_drops_out_keeps_its_last_value() {
4181 let path = temp_path("hdf5_attr_sticky");
4182 let mut writer = Hdf5Writer::new();
4183
4184 let mk = |exposure: Option<f64>| {
4185 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4186 arr.attributes.add(NDAttribute::new_static(
4187 "count",
4188 "",
4189 NDAttrSource::Driver,
4190 NDAttrValue::Int32(7),
4191 ));
4192 if let Some(v) = exposure {
4193 arr.attributes.add(NDAttribute::new_static(
4194 "exposure",
4195 "",
4196 NDAttrSource::Driver,
4197 NDAttrValue::Float64(v),
4198 ));
4199 }
4200 arr
4201 };
4202
4203 let a0 = mk(Some(0.5));
4204 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4205 writer.write_file(&a0).unwrap();
4206 writer.write_file(&mk(Some(0.75))).unwrap();
4208 writer.write_file(&mk(None)).unwrap();
4210 writer.close_file().unwrap();
4211
4212 let h5 = H5File::open(&path).unwrap();
4213 let exp = h5
4214 .dataset("entry/instrument/NDAttributes/exposure")
4215 .unwrap();
4216 assert_eq!(exp.shape(), vec![3]);
4217 let exp_vals: Vec<f64> = exp.read_raw().unwrap();
4218 assert_eq!(exp_vals, vec![0.5, 0.75, 0.75]);
4219
4220 std::fs::remove_file(&path).ok();
4221 }
4222
4223 #[test]
4228 fn attribute_appearing_after_open_creates_no_dataset() {
4229 let path = temp_path("hdf5_attr_late");
4230 let mut writer = Hdf5Writer::new();
4231
4232 let mut a0 = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4233 a0.attributes.add(NDAttribute::new_static(
4234 "count",
4235 "",
4236 NDAttrSource::Driver,
4237 NDAttrValue::Int32(7),
4238 ));
4239 let mut a1 = a0.clone();
4240 a1.attributes.add(NDAttribute::new_static(
4241 "late",
4242 "",
4243 NDAttrSource::Driver,
4244 NDAttrValue::Int32(1),
4245 ));
4246
4247 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4248 writer.write_file(&a0).unwrap();
4249 writer.write_file(&a1).unwrap();
4250 writer.close_file().unwrap();
4251
4252 let h5 = H5File::open(&path).unwrap();
4253 assert_eq!(
4254 h5.dataset("entry/instrument/NDAttributes/count")
4255 .unwrap()
4256 .shape(),
4257 vec![2]
4258 );
4259 assert!(
4260 h5.dataset("entry/instrument/NDAttributes/late").is_err(),
4261 "an attribute absent at open gets no dataset"
4262 );
4263
4264 std::fs::remove_file(&path).ok();
4265 }
4266
4267 #[test]
4268 fn test_adp78_undefined_attribute_dataset_is_float32_nan() {
4269 let path = temp_path("hdf5_attr_undef");
4279 let mut writer = Hdf5Writer::new();
4280
4281 let mk = |undef_value: NDAttrValue| {
4282 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4283 arr.attributes.add(NDAttribute::new_static(
4284 "novalue",
4285 "",
4286 NDAttrSource::Driver,
4287 undef_value,
4288 ));
4289 arr
4290 };
4291
4292 let a0 = mk(NDAttrValue::Undefined);
4294 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4295 writer.write_file(&a0).unwrap();
4296 writer.write_file(&mk(NDAttrValue::Int32(7))).unwrap();
4299 writer.write_file(&mk(NDAttrValue::Int32(9))).unwrap();
4300 writer.close_file().unwrap();
4301
4302 let h5 = H5File::open(&path).unwrap();
4303 let ds = h5.dataset("entry/instrument/NDAttributes/novalue").unwrap();
4304 assert_eq!(ds.shape(), vec![3]);
4305 let vals: Vec<f32> = ds.read_raw().unwrap();
4307 assert_eq!(vals.len(), 3);
4308 for (i, v) in vals.iter().enumerate() {
4309 assert!(
4310 v.is_nan(),
4311 "element {} is {} — a reader cannot tell it from a measured value; C leaves epicsNAN",
4312 i,
4313 v
4314 );
4315 }
4316
4317 std::fs::remove_file(&path).ok();
4318 }
4319
4320 #[test]
4321 fn test_attribute_dataset_chunk_matches_capture_target() {
4322 let path = temp_path("hdf5_attr_chunk_cap");
4329 let mut writer = Hdf5Writer::new();
4330 writer.set_num_capture(10);
4331
4332 let mk = |c: i32| {
4333 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4334 arr.attributes.add(NDAttribute::new_static(
4335 "count",
4336 "",
4337 NDAttrSource::Driver,
4338 NDAttrValue::Int32(c),
4339 ));
4340 arr
4341 };
4342
4343 let a0 = mk(1);
4344 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4345 writer.write_file(&a0).unwrap();
4346 writer.write_file(&mk(2)).unwrap();
4347 writer.write_file(&mk(3)).unwrap();
4348 writer.close_file().unwrap();
4349
4350 let h5 = H5File::open(&path).unwrap();
4351 let ds = h5.dataset("entry/instrument/NDAttributes/count").unwrap();
4352 assert_eq!(ds.shape(), vec![3]);
4353 assert_eq!(ds.chunk_dims(), Some(vec![10]));
4354 let vals: Vec<i32> = ds.read_raw().unwrap();
4355 assert_eq!(vals, vec![1, 2, 3]);
4356 std::fs::remove_file(&path).ok();
4357 }
4358
4359 #[test]
4360 fn test_attribute_dataset_chunk_single_mode_is_one() {
4361 let path = temp_path("hdf5_attr_chunk_single");
4365 let mut writer = Hdf5Writer::new();
4366 writer.set_num_capture(64);
4367
4368 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4369 arr.attributes.add(NDAttribute::new_static(
4370 "count",
4371 "",
4372 NDAttrSource::Driver,
4373 NDAttrValue::Int32(7),
4374 ));
4375 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4376 writer.write_file(&arr).unwrap();
4377 writer.close_file().unwrap();
4378
4379 let h5 = H5File::open(&path).unwrap();
4380 let ds = h5.dataset("entry/instrument/NDAttributes/count").unwrap();
4381 assert_eq!(ds.chunk_dims(), Some(vec![1]));
4382 std::fs::remove_file(&path).ok();
4383 }
4384
4385 #[test]
4386 fn test_string_attribute_dataset_is_rank1_fixed_string() {
4387 let path = temp_path("hdf5_attr_str");
4392 let mut writer = Hdf5Writer::new();
4393
4394 let mk = |mode_name: &str| {
4395 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4396 arr.attributes.add(NDAttribute::new_static(
4397 "ColorMode",
4398 "",
4399 NDAttrSource::Driver,
4400 NDAttrValue::String(mode_name.to_string()),
4401 ));
4402 arr
4403 };
4404
4405 let a0 = mk("Mono");
4406 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4407 writer.write_file(&a0).unwrap();
4408 writer.write_file(&mk("RGB1")).unwrap();
4409 writer.close_file().unwrap();
4410
4411 let h5 = H5File::open(&path).unwrap();
4412 let ds = h5
4415 .dataset("entry/instrument/detector/NDAttributes/ColorMode")
4416 .unwrap();
4417 assert_eq!(ds.shape(), vec![2]);
4419 assert_eq!(ds.element_size(), MAX_ATTRIBUTE_STRING_SIZE);
4420
4421 let frames: Vec<FixedStr256> = ds.read_raw().unwrap();
4422 assert_eq!(frames.len(), 2);
4423 let decode = |f: &FixedStr256| -> String {
4424 f.0.iter()
4425 .take_while(|&&b| b != 0)
4426 .map(|&b| b as char)
4427 .collect()
4428 };
4429 assert_eq!(decode(&frames[0]), "Mono");
4430 assert_eq!(decode(&frames[1]), "RGB1");
4431 std::fs::remove_file(&path).ok();
4432 }
4433
4434 #[test]
4435 fn test_ndattribute_dataset_routed_to_declared_group() {
4436 let path = temp_path("hdf5_attr_route");
4442 let mut writer = Hdf5Writer::new();
4443
4444 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4445 arr.attributes.add(NDAttribute::new_static(
4446 "ColorMode",
4447 "",
4448 NDAttrSource::Driver,
4449 NDAttrValue::String("Mono".to_string()),
4450 ));
4451 arr.attributes.add(NDAttribute::new_static(
4452 "count",
4453 "",
4454 NDAttrSource::Driver,
4455 NDAttrValue::Int32(7),
4456 ));
4457
4458 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4459 writer.write_file(&arr).unwrap();
4460 writer.close_file().unwrap();
4461
4462 let h5 = H5File::open(&path).unwrap();
4463 assert!(
4465 h5.dataset("entry/instrument/detector/NDAttributes/ColorMode")
4466 .is_ok()
4467 );
4468 assert!(h5.dataset("entry/instrument/NDAttributes/count").is_ok());
4470 assert!(
4472 h5.dataset("entry/instrument/NDAttributes/ColorMode")
4473 .is_err()
4474 );
4475 std::fs::remove_file(&path).ok();
4476 }
4477
4478 #[test]
4479 fn test_ndattribute_element_attr_open_close_values() {
4480 let dir = std::env::temp_dir();
4486 let layout = dir.join(format!(
4487 "adcore_layout_elem_attr_{}.xml",
4488 std::process::id()
4489 ));
4490 std::fs::write(
4491 &layout,
4492 r#"<hdf5_layout>
4493 <group name="entry">
4494 <group name="instrument">
4495 <group name="detector">
4496 <attribute name="FirstColorMode" source="ndattribute" ndattribute="ColorMode" when="OnFileOpen"/>
4497 <attribute name="LastColorMode" source="ndattribute" ndattribute="ColorMode" when="OnFileClose"/>
4498 <attribute name="DefaultColorMode" source="ndattribute" ndattribute="ColorMode"/>
4499 <dataset name="data" source="detector" det_default="true">
4500 <attribute name="GainAtOpen" source="ndattribute" ndattribute="Gain" when="OnFileOpen"/>
4501 <attribute name="GainAtClose" source="ndattribute" ndattribute="Gain" when="OnFileClose"/>
4502 </dataset>
4503 </group>
4504 <group name="NDAttributes" ndattr_default="true"/>
4505 </group>
4506 </group>
4507 </hdf5_layout>"#,
4508 )
4509 .unwrap();
4510
4511 let path = temp_path("hdf5_elem_attr");
4512 let mut writer = Hdf5Writer::new();
4513 assert!(
4514 writer.set_layout_filename(layout.to_str().unwrap()),
4515 "layout XML must parse: {}",
4516 writer.layout_error
4517 );
4518
4519 let mk = |mode: &str, gain: i32| {
4520 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4521 arr.attributes.add(NDAttribute::new_static(
4522 "ColorMode",
4523 "",
4524 NDAttrSource::Driver,
4525 NDAttrValue::String(mode.to_string()),
4526 ));
4527 arr.attributes.add(NDAttribute::new_static(
4528 "Gain",
4529 "",
4530 NDAttrSource::Driver,
4531 NDAttrValue::Int32(gain),
4532 ));
4533 arr
4534 };
4535
4536 let a0 = mk("Mono", 10);
4537 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4538 writer.write_file(&a0).unwrap();
4539 writer.write_file(&mk("RGB1", 20)).unwrap();
4540 writer.write_file(&mk("Bayer", 30)).unwrap();
4541 writer.close_file().unwrap();
4542
4543 let h5 = H5File::open(&path).unwrap();
4544
4545 let mut grp = h5.root_group();
4547 for seg in ["entry", "instrument", "detector"] {
4548 grp = grp.group(seg).unwrap();
4549 }
4550 assert_eq!(grp.attr_string("FirstColorMode").unwrap(), "Mono");
4552 assert_eq!(grp.attr_string("DefaultColorMode").unwrap(), "Mono");
4553 assert_eq!(grp.attr_string("LastColorMode").unwrap(), "Bayer");
4555
4556 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4558 assert_eq!(
4559 ds.attr("GainAtOpen")
4560 .unwrap()
4561 .read_numeric::<i32>()
4562 .unwrap(),
4563 10
4564 );
4565 assert_eq!(
4566 ds.attr("GainAtClose")
4567 .unwrap()
4568 .read_numeric::<i32>()
4569 .unwrap(),
4570 30
4571 );
4572
4573 std::fs::remove_file(&path).ok();
4574 std::fs::remove_file(&layout).ok();
4575 }
4576
4577 #[test]
4578 fn test_ndattr_element_attr_on_lazily_created_dataset() {
4579 let dir = std::env::temp_dir();
4587 let layout = dir.join(format!(
4588 "adcore_layout_lazy_ds_attr_{}.xml",
4589 std::process::id()
4590 ));
4591 std::fs::write(
4592 &layout,
4593 r#"<hdf5_layout>
4594 <group name="entry">
4595 <group name="instrument">
4596 <group name="detector">
4597 <dataset name="data" source="detector" det_default="true"/>
4598 <dataset name="GainTrace" source="ndattribute" ndattribute="Gain">
4599 <attribute name="ModeAtOpen" source="ndattribute" ndattribute="ColorMode" when="OnFileOpen"/>
4600 <attribute name="ModeAtClose" source="ndattribute" ndattribute="ColorMode" when="OnFileClose"/>
4601 </dataset>
4602 </group>
4603 <group name="NDAttributes" ndattr_default="true"/>
4604 </group>
4605 </group>
4606 </hdf5_layout>"#,
4607 )
4608 .unwrap();
4609
4610 let path = temp_path("hdf5_lazy_ds_attr");
4611 let mut writer = Hdf5Writer::new();
4612 assert!(
4613 writer.set_layout_filename(layout.to_str().unwrap()),
4614 "layout XML must parse: {}",
4615 writer.layout_error
4616 );
4617
4618 let mk = |mode: &str, gain: i32| {
4619 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4620 arr.attributes.add(NDAttribute::new_static(
4621 "ColorMode",
4622 "",
4623 NDAttrSource::Driver,
4624 NDAttrValue::String(mode.to_string()),
4625 ));
4626 arr.attributes.add(NDAttribute::new_static(
4627 "Gain",
4628 "",
4629 NDAttrSource::Driver,
4630 NDAttrValue::Int32(gain),
4631 ));
4632 arr
4633 };
4634
4635 let a0 = mk("Mono", 10);
4636 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4637 writer.write_file(&a0).unwrap();
4638 writer.write_file(&mk("RGB1", 20)).unwrap();
4639 writer.write_file(&mk("Bayer", 30)).unwrap();
4640 writer.close_file().unwrap();
4641
4642 let h5 = H5File::open(&path).unwrap();
4643 let ds = h5.dataset("entry/instrument/detector/GainTrace").unwrap();
4646 assert_eq!(
4647 ds.attr("ModeAtOpen").unwrap().read_string().unwrap(),
4648 "Mono"
4649 );
4650 assert_eq!(
4651 ds.attr("ModeAtClose").unwrap().read_string().unwrap(),
4652 "Bayer"
4653 );
4654 std::fs::remove_file(&path).ok();
4655 std::fs::remove_file(&layout).ok();
4656 }
4657
4658 #[test]
4659 fn test_swmr_ndattr_element_attr_on_streaming_dataset() {
4660 let dir = std::env::temp_dir();
4666 let layout = dir.join(format!(
4667 "adcore_layout_swmr_ds_attr_{}.xml",
4668 std::process::id()
4669 ));
4670 std::fs::write(
4671 &layout,
4672 r#"<hdf5_layout>
4673 <group name="entry">
4674 <group name="instrument">
4675 <group name="detector">
4676 <dataset name="data" source="detector" det_default="true">
4677 <attribute name="ModeAtOpen" source="ndattribute" ndattribute="ColorMode" when="OnFileOpen"/>
4678 </dataset>
4679 </group>
4680 <group name="NDAttributes" ndattr_default="true"/>
4681 </group>
4682 </group>
4683 </hdf5_layout>"#,
4684 )
4685 .unwrap();
4686
4687 let path = temp_path("hdf5_swmr_ds_attr");
4688 let mut writer = Hdf5Writer::new();
4689 writer.set_swmr_mode(true);
4690 assert!(
4691 writer.set_layout_filename(layout.to_str().unwrap()),
4692 "layout XML must parse: {}",
4693 writer.layout_error
4694 );
4695
4696 let mk = |mode: &str| {
4697 let mut arr = NDArray::new(
4698 vec![NDDimension::new(4), NDDimension::new(4)],
4699 NDDataType::UInt16,
4700 );
4701 arr.attributes.add(NDAttribute::new_static(
4702 "ColorMode",
4703 "",
4704 NDAttrSource::Driver,
4705 NDAttrValue::String(mode.to_string()),
4706 ));
4707 arr
4708 };
4709
4710 let a0 = mk("Mono");
4711 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4712 writer.write_file(&a0).unwrap();
4713 writer.write_file(&mk("RGB1")).unwrap();
4714 writer.close_file().unwrap();
4715
4716 let h5 = H5File::open(&path).unwrap();
4717 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4718 assert_eq!(
4719 ds.attr("ModeAtOpen").unwrap().read_string().unwrap(),
4720 "Mono"
4721 );
4722 std::fs::remove_file(&path).ok();
4723 std::fs::remove_file(&layout).ok();
4724 }
4725
4726 #[test]
4727 fn test_swmr_ndarray_default_attrs_on_streaming_dataset() {
4728 let path = temp_path("hdf5_swmr_dimattr_1d");
4738 let mut writer = Hdf5Writer::new();
4739 writer.set_swmr_mode(true);
4740 let mut d = NDDimension::new(8);
4741 d.offset = 3;
4742 d.binning = 2;
4743 d.reverse = true;
4744 let arr = NDArray::new(vec![d], NDDataType::UInt16);
4745 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4746 writer.write_file(&arr).unwrap();
4747 writer.write_file(&arr).unwrap();
4748 writer.close_file().unwrap();
4749 let h5 = H5File::open(&path).unwrap();
4750 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4751 let geti = |n: &str| -> i32 { ds.attr(n).unwrap().read_numeric().unwrap() };
4752 assert_eq!(geti("NDArrayNumDims"), 1);
4753 assert_eq!(geti("NDArrayDimOffset"), 3);
4754 assert_eq!(geti("NDArrayDimBinning"), 2);
4755 assert_eq!(geti("NDArrayDimReverse"), 1);
4756 std::fs::remove_file(&path).ok();
4757
4758 let path = temp_path("hdf5_swmr_dimattr_2d");
4761 let mut writer = Hdf5Writer::new();
4762 writer.set_swmr_mode(true);
4763 let mut d0 = NDDimension::new(4);
4764 d0.offset = 3;
4765 d0.binning = 2;
4766 d0.reverse = true;
4767 let mut d1 = NDDimension::new(8);
4768 d1.offset = 5;
4769 d1.binning = 4;
4770 d1.reverse = false;
4771 let arr = NDArray::new(vec![d0, d1], NDDataType::UInt16);
4772 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4773 writer.write_file(&arr).unwrap();
4774 writer.write_file(&arr).unwrap();
4775 writer.close_file().unwrap();
4776 let h5 = H5File::open(&path).unwrap();
4777 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4778 let numdims: i32 = ds.attr("NDArrayNumDims").unwrap().read_numeric().unwrap();
4779 assert_eq!(numdims, 2);
4780 let read_i32_arr = |n: &str| -> Vec<i32> {
4781 let raw = ds.attr(n).unwrap().read_raw().unwrap();
4782 assert_eq!(raw.len(), 2 * 4, "{n} must be a 2-element int32 array");
4783 raw.chunks_exact(4)
4784 .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
4785 .collect()
4786 };
4787 assert_eq!(read_i32_arr("NDArrayDimOffset"), vec![3, 5]);
4788 assert_eq!(read_i32_arr("NDArrayDimBinning"), vec![2, 4]);
4789 assert_eq!(read_i32_arr("NDArrayDimReverse"), vec![1, 0]);
4790 std::fs::remove_file(&path).ok();
4791 }
4792
4793 #[test]
4794 fn test_ndattr_descriptor_attributes_written() {
4795 let path = temp_path("hdf5_attr_desc");
4799 let mut writer = Hdf5Writer::new();
4800
4801 let mk = |v: f64| {
4802 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
4803 arr.attributes.add(NDAttribute::new_static(
4804 "AcquireTime",
4805 "exposure time",
4806 NDAttrSource::EpicsPV("13SIM1:cam1:AcquireTime_RBV".to_string()),
4807 NDAttrValue::Float64(v),
4808 ));
4809 arr
4810 };
4811
4812 let a0 = mk(0.1);
4813 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
4814 writer.write_file(&a0).unwrap();
4815 writer.write_file(&mk(0.2)).unwrap();
4816 writer.close_file().unwrap();
4817
4818 let h5 = H5File::open(&path).unwrap();
4819 let ds = h5
4820 .dataset("entry/instrument/NDAttributes/AcquireTime")
4821 .unwrap();
4822 let read = |n: &str| ds.attr(n).unwrap().read_string().unwrap();
4823 assert_eq!(read("NDAttrName"), "AcquireTime");
4824 assert_eq!(read("NDAttrDescription"), "exposure time");
4825 assert_eq!(read("NDAttrSourceType"), "NDAttrSourceEPICSPV");
4826 assert_eq!(read("NDAttrSource"), "13SIM1:cam1:AcquireTime_RBV");
4827 std::fs::remove_file(&path).ok();
4828 }
4829
4830 #[test]
4831 fn test_detector_dataset_ndarray_dim_attributes() {
4832 let path = temp_path("hdf5_dimattr_1d");
4840 let mut writer = Hdf5Writer::new();
4841 let mut d = NDDimension::new(8);
4842 d.offset = 3;
4843 d.binning = 2;
4844 d.reverse = true;
4845 let arr = NDArray::new(vec![d], NDDataType::UInt16);
4846 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4847 writer.write_file(&arr).unwrap();
4848 writer.close_file().unwrap();
4849 let h5 = H5File::open(&path).unwrap();
4850 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4851 let geti = |n: &str| -> i32 { ds.attr(n).unwrap().read_numeric().unwrap() };
4852 assert_eq!(geti("NDArrayNumDims"), 1);
4853 assert_eq!(geti("NDArrayDimOffset"), 3);
4854 assert_eq!(geti("NDArrayDimBinning"), 2);
4855 assert_eq!(geti("NDArrayDimReverse"), 1);
4856 std::fs::remove_file(&path).ok();
4857
4858 let path = temp_path("hdf5_dimattr_2d");
4862 let mut writer = Hdf5Writer::new();
4863 let mut d0 = NDDimension::new(4);
4864 d0.offset = 3;
4865 d0.binning = 2;
4866 d0.reverse = true;
4867 let mut d1 = NDDimension::new(8);
4868 d1.offset = 5;
4869 d1.binning = 4;
4870 d1.reverse = false;
4871 let arr = NDArray::new(vec![d0, d1], NDDataType::UInt16);
4872 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4873 writer.write_file(&arr).unwrap();
4874 writer.close_file().unwrap();
4875 let h5 = H5File::open(&path).unwrap();
4876 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4877 let numdims: i32 = ds.attr("NDArrayNumDims").unwrap().read_numeric().unwrap();
4878 assert_eq!(numdims, 2);
4879 let read_i32_arr = |n: &str| -> Vec<i32> {
4883 let raw = ds.attr(n).unwrap().read_raw().unwrap();
4884 assert_eq!(raw.len(), 2 * 4, "{n} must be a 2-element int32 array");
4885 raw.chunks_exact(4)
4886 .map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
4887 .collect()
4888 };
4889 assert_eq!(read_i32_arr("NDArrayDimOffset"), vec![3, 5]);
4890 assert_eq!(read_i32_arr("NDArrayDimBinning"), vec![2, 4]);
4891 assert_eq!(read_i32_arr("NDArrayDimReverse"), vec![1, 0]);
4892 std::fs::remove_file(&path).ok();
4893 }
4894
4895 #[test]
4896 fn test_fill_value_recorded_on_dataset() {
4897 let path = temp_path("hdf5_fill");
4902 let mut writer = Hdf5Writer::new();
4903 writer.set_fill_value(7.5);
4904
4905 let arr = NDArray::new(
4906 vec![NDDimension::new(4), NDDimension::new(4)],
4907 NDDataType::UInt16,
4908 );
4909 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
4910 writer.write_file(&arr).unwrap();
4911 writer.close_file().unwrap();
4912
4913 let h5 = H5File::open(&path).unwrap();
4914 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
4915 let fv: f64 = ds.attr("HDF5_fillValue").unwrap().read_numeric().unwrap();
4916 assert_eq!(fv, 7.5);
4917 std::fs::remove_file(&path).ok();
4918
4919 let path2 = temp_path("hdf5_fill_dcpl");
4922 {
4923 let f = H5File::create(&path2).unwrap();
4924 let _ = f
4925 .new_dataset::<i32>()
4926 .shape(&[8][..])
4927 .fill_value(42i32)
4928 .create("unwritten")
4929 .unwrap();
4930 }
4931 let h5b = H5File::open(&path2).unwrap();
4932 let vals: Vec<i32> = h5b.dataset("unwritten").unwrap().read_raw().unwrap();
4933 assert_eq!(vals, vec![42i32; 8]);
4934 std::fs::remove_file(&path2).ok();
4935 }
4936
4937 #[test]
4938 fn test_performance_dataset() {
4939 let path = temp_path("hdf5_perf");
4940 let mut writer = Hdf5Writer::new();
4941 writer.set_store_performance(true);
4942
4943 let arr = NDArray::new(
4944 vec![NDDimension::new(8), NDDimension::new(8)],
4945 NDDataType::UInt16,
4946 );
4947 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4948 writer.write_file(&arr).unwrap();
4949 writer.write_file(&arr).unwrap();
4950 writer.close_file().unwrap();
4951
4952 let h5 = H5File::open(&path).unwrap();
4953 let ts = h5
4954 .dataset("entry/instrument/performance/timestamp")
4955 .unwrap();
4956 assert_eq!(ts.shape(), vec![2, 5]);
4957 let vals: Vec<f64> = ts.read_raw().unwrap();
4958 assert_eq!(vals.len(), 10);
4959
4960 std::fs::remove_file(&path).ok();
4961 }
4962
4963 #[test]
4964 fn test_performance_dataset_chunk_matches_capture_target() {
4965 let path = temp_path("hdf5_perf_chunk");
4971 let mut writer = Hdf5Writer::new();
4972 writer.set_store_performance(true);
4973 writer.set_num_capture(8);
4974
4975 let arr = NDArray::new(
4976 vec![NDDimension::new(8), NDDimension::new(8)],
4977 NDDataType::UInt16,
4978 );
4979 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
4980 writer.write_file(&arr).unwrap();
4981 writer.write_file(&arr).unwrap();
4982 writer.close_file().unwrap();
4983
4984 let h5 = H5File::open(&path).unwrap();
4985 let ts = h5
4986 .dataset("entry/instrument/performance/timestamp")
4987 .unwrap();
4988 assert_eq!(ts.shape(), vec![2, 5]);
4989 assert_eq!(ts.chunk_dims(), Some(vec![8, 5]));
4990 let vals: Vec<f64> = ts.read_raw().unwrap();
4991 assert_eq!(vals.len(), 10);
4992
4993 std::fs::remove_file(&path).ok();
4994 }
4995
4996 #[test]
4997 fn test_roundtrip_all_types() {
4998 macro_rules! roundtrip {
4999 ($name:expr, $dt:expr, $variant:ident, $ty:ty, $vals:expr) => {{
5000 let path = temp_path($name);
5001 let mut writer = Hdf5Writer::new();
5002 let mut arr = NDArray::new(vec![NDDimension::new(4)], $dt);
5003 if let NDDataBuffer::$variant(ref mut v) = arr.data {
5004 let src: Vec<$ty> = $vals;
5005 v.copy_from_slice(&src);
5006 }
5007 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
5008 writer.write_file(&arr).unwrap();
5009 writer.close_file().unwrap();
5010
5011 let mut reader = Hdf5Writer::new();
5012 reader.current_path = Some(path.clone());
5013 let r = reader.read_file().unwrap();
5014 assert_eq!(r.data.data_type(), $dt, "type for {}", $name);
5015 if let NDDataBuffer::$variant(ref v) = r.data {
5016 let src: Vec<$ty> = $vals;
5017 assert_eq!(v, &src, "values for {}", $name);
5018 } else {
5019 panic!("wrong buffer variant for {}", $name);
5020 }
5021 std::fs::remove_file(&path).ok();
5022 }};
5023 }
5024
5025 roundtrip!("rt_i8", NDDataType::Int8, I8, i8, vec![-1, 0, 1, 127]);
5026 roundtrip!("rt_u8", NDDataType::UInt8, U8, u8, vec![0, 1, 200, 255]);
5027 roundtrip!(
5028 "rt_i16",
5029 NDDataType::Int16,
5030 I16,
5031 i16,
5032 vec![-32768, -1, 1, 32767]
5033 );
5034 roundtrip!(
5035 "rt_u16",
5036 NDDataType::UInt16,
5037 U16,
5038 u16,
5039 vec![0, 1, 40000, 65535]
5040 );
5041 roundtrip!(
5042 "rt_i32",
5043 NDDataType::Int32,
5044 I32,
5045 i32,
5046 vec![i32::MIN, -1, 1, i32::MAX]
5047 );
5048 roundtrip!(
5049 "rt_u32",
5050 NDDataType::UInt32,
5051 U32,
5052 u32,
5053 vec![0, 1, 3_000_000_000, u32::MAX]
5054 );
5055 roundtrip!(
5056 "rt_i64",
5057 NDDataType::Int64,
5058 I64,
5059 i64,
5060 vec![i64::MIN, -1, 1, i64::MAX]
5061 );
5062 roundtrip!(
5063 "rt_u64",
5064 NDDataType::UInt64,
5065 U64,
5066 u64,
5067 vec![0, 1, 9_000_000_000, u64::MAX]
5068 );
5069 roundtrip!(
5070 "rt_f32",
5071 NDDataType::Float32,
5072 F32,
5073 f32,
5074 vec![-1.5, 0.0, 2.25, 3.75]
5075 );
5076 roundtrip!(
5077 "rt_f64",
5078 NDDataType::Float64,
5079 F64,
5080 f64,
5081 vec![-1.5, 0.0, 2.25, 3.75]
5082 );
5083 }
5084
5085 #[test]
5086 fn test_deflate_compressed_write() {
5087 let path = temp_path("hdf5_deflate");
5088 let mut writer = Hdf5Writer::new();
5089 writer.set_compression_type(COMPRESS_ZLIB);
5090 writer.set_z_compress_level(6);
5091
5092 let mut arr = NDArray::new(
5093 vec![NDDimension::new(64), NDDimension::new(64)],
5094 NDDataType::UInt16,
5095 );
5096 if let NDDataBuffer::U16(ref mut v) = arr.data {
5097 for i in 0..v.len() {
5098 v[i] = (i % 256) as u16;
5099 }
5100 }
5101
5102 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
5103 writer.write_file(&arr).unwrap();
5104 writer.close_file().unwrap();
5105
5106 let raw_path = temp_path("hdf5_deflate_raw");
5116 {
5117 let mut raw_writer = Hdf5Writer::new();
5118 raw_writer.set_compression_type(COMPRESS_NONE);
5119 raw_writer
5120 .open_file(&raw_path, NDFileMode::Single, &arr)
5121 .unwrap();
5122 raw_writer.write_file(&arr).unwrap();
5123 raw_writer.close_file().unwrap();
5124 }
5125 let compressed_size = std::fs::metadata(&path).unwrap().len();
5126 let raw_size = std::fs::metadata(&raw_path).unwrap().len();
5127 assert!(
5128 compressed_size + 4096 < raw_size,
5129 "deflate must shrink the data chunk: compressed={compressed_size} raw={raw_size}"
5130 );
5131
5132 let h5file = H5File::open(&path).unwrap();
5133 let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
5134 let data: Vec<u16> = ds.read_raw().unwrap();
5135 assert_eq!(data.len(), 64 * 64);
5136 assert_eq!(data[0], 0);
5137 assert_eq!(data[255], 255);
5138 assert_eq!(data[256], 0);
5139
5140 std::fs::remove_file(&path).ok();
5141 }
5142
5143 #[test]
5144 fn test_lz4_compressed_write() {
5145 let path = temp_path("hdf5_lz4");
5146 let mut writer = Hdf5Writer::new();
5147 writer.set_compression_type(COMPRESS_LZ4);
5148
5149 let mut arr = NDArray::new(
5150 vec![NDDimension::new(32), NDDimension::new(32)],
5151 NDDataType::UInt8,
5152 );
5153 if let NDDataBuffer::U8(ref mut v) = arr.data {
5154 for i in 0..v.len() {
5155 v[i] = (i % 4) as u8;
5156 }
5157 }
5158
5159 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
5160 writer.write_file(&arr).unwrap();
5161 writer.close_file().unwrap();
5162
5163 let h5file = H5File::open(&path).unwrap();
5164 let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
5165 let data: Vec<u8> = ds.read_raw().unwrap();
5166 assert_eq!(data.len(), 32 * 32);
5167 assert_eq!(data[0], 0);
5168 assert_eq!(data[3], 3);
5169
5170 std::fs::remove_file(&path).ok();
5171 }
5172
5173 #[test]
5174 fn test_bitshuffle_compressed_write() {
5175 let path = temp_path("hdf5_bshuf");
5176 let mut writer = Hdf5Writer::new();
5177 writer.set_compression_type(COMPRESS_BSHUF);
5178
5179 let mut arr = NDArray::new(
5180 vec![NDDimension::new(64), NDDimension::new(64)],
5181 NDDataType::UInt16,
5182 );
5183 if let NDDataBuffer::U16(ref mut v) = arr.data {
5184 for i in 0..v.len() {
5185 v[i] = (i % 8) as u16;
5186 }
5187 }
5188
5189 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
5190 writer.write_file(&arr).unwrap();
5191 writer.close_file().unwrap();
5192
5193 let h5file = H5File::open(&path).unwrap();
5194 let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
5195 let data: Vec<u16> = ds.read_raw().unwrap();
5196 assert_eq!(data.len(), 64 * 64);
5197 assert_eq!(data[0], 0);
5198 assert_eq!(data[9], 1);
5199
5200 std::fs::remove_file(&path).ok();
5201 }
5202
5203 #[test]
5204 fn test_szip_uses_nearest_neighbor_mask() {
5205 let mut writer = Hdf5Writer::new();
5208 writer.set_compression_type(COMPRESS_SZIP);
5209 let pipeline = writer.build_pipeline(1).expect("szip pipeline");
5210 assert_eq!(pipeline.filters[0].cd_values[0], SZIP_NN_OPTION_MASK);
5211
5212 let path = temp_path("hdf5_szip_nn");
5213 let mut arr = NDArray::new(
5214 vec![NDDimension::new(64), NDDimension::new(64)],
5215 NDDataType::UInt8,
5216 );
5217 if let NDDataBuffer::U8(ref mut v) = arr.data {
5218 for (i, e) in v.iter_mut().enumerate() {
5219 *e = (i % 13) as u8;
5220 }
5221 }
5222 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
5223 writer.write_file(&arr).unwrap();
5224 writer.close_file().unwrap();
5225
5226 let h5file = H5File::open(&path).unwrap();
5227 let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
5228 let data: Vec<u8> = ds.read_raw().unwrap();
5229 assert_eq!(data.len(), 64 * 64);
5230 assert_eq!(data[20], (20 % 13) as u8);
5231 std::fs::remove_file(&path).ok();
5232 }
5233
5234 #[test]
5235 fn test_blosc_default_shuffle_is_byte_shuffle() {
5236 let mut writer = Hdf5Writer::new();
5239 writer.set_compression_type(COMPRESS_BLOSC);
5240 let pipeline = writer.build_pipeline(2).expect("blosc pipeline");
5241 assert_eq!(pipeline.filters[0].cd_values[5], 1);
5242 }
5243
5244 #[test]
5245 fn test_nbit_packs_to_reduced_precision_datatype() {
5246 let mut writer = Hdf5Writer::new();
5256 writer.set_compression_type(COMPRESS_NBIT);
5257 writer.set_nbit_precision(10);
5258 writer.set_nbit_offset(0);
5259 assert!(
5260 writer.build_pipeline(2).is_none(),
5261 "N-bit packing is applied via a datatype override, not build_pipeline"
5262 );
5263
5264 let path = temp_path("hdf5_nbit_packed");
5265 let mut arr = NDArray::new(
5266 vec![NDDimension::new(8), NDDimension::new(8)],
5267 NDDataType::UInt16,
5268 );
5269 if let NDDataBuffer::U16(ref mut v) = arr.data {
5270 for (i, x) in v.iter_mut().enumerate() {
5271 *x = (i as u16 * 7) & 0x3FF; }
5273 v[0] = 0xFFFF; }
5275 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
5276 writer.write_file(&arr).unwrap();
5277 writer.close_file().unwrap();
5278
5279 let h5file = H5File::open(&path).unwrap();
5280 let ds = h5file.dataset("entry/instrument/detector/data").unwrap();
5281
5282 match ds.datatype().unwrap() {
5285 DatatypeMessage::FixedPoint {
5286 size,
5287 bit_precision,
5288 bit_offset,
5289 signed,
5290 ..
5291 } => {
5292 assert_eq!(size, 2, "byte footprint unchanged");
5293 assert_eq!(bit_precision, 10, "precision narrowed to 10 bits");
5294 assert_eq!(bit_offset, 0);
5295 assert!(!signed, "u16 is unsigned");
5296 }
5297 other => panic!("expected reduced-precision FixedPoint, got {other:?}"),
5298 }
5299
5300 let data: Vec<u16> = ds.read_raw().unwrap();
5301 assert_eq!(data.len(), 8 * 8);
5302 assert_eq!(data[0], 0x3FF, "0xFFFF must pack to the low 10 bits");
5304 for i in 1..data.len() {
5305 assert_eq!(
5306 data[i],
5307 (i as u16 * 7) & 0x3FF,
5308 "in-range value {i} round-trips"
5309 );
5310 }
5311 std::fs::remove_file(&path).ok();
5312 }
5313
5314 #[test]
5315 fn test_chunk_geometry_recorded() {
5316 let path = temp_path("hdf5_chunkgeom");
5319 let mut writer = Hdf5Writer::new();
5320 writer.set_chunk_size_auto(false);
5321 writer.set_n_row_chunks(4);
5322 writer.set_n_col_chunks(2);
5323 writer.set_n_frames_chunks(3);
5324
5325 let mut arr = NDArray::new(
5326 vec![NDDimension::new(8), NDDimension::new(8)],
5327 NDDataType::UInt16,
5328 );
5329 if let NDDataBuffer::U16(ref mut v) = arr.data {
5330 for i in 0..v.len() {
5331 v[i] = i as u16;
5332 }
5333 }
5334
5335 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5336 writer.write_file(&arr).unwrap();
5337 writer.write_file(&arr).unwrap();
5338 writer.close_file().unwrap();
5339
5340 let h5 = H5File::open(&path).unwrap();
5341 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
5342 assert_eq!(ds.shape(), vec![2, 8, 8]);
5343 let data: Vec<u16> = ds.read_raw().unwrap();
5345 assert_eq!(data.len(), 2 * 64);
5346 for i in 0..64usize {
5347 assert_eq!(data[i], i as u16, "frame0 element {}", i);
5348 assert_eq!(data[64 + i], i as u16, "frame1 element {}", i);
5349 }
5350 assert_eq!(
5352 ds.attr("HDF5_nRowChunks")
5353 .unwrap()
5354 .read_numeric::<i32>()
5355 .unwrap(),
5356 4
5357 );
5358 assert_eq!(
5359 ds.attr("HDF5_nColChunks")
5360 .unwrap()
5361 .read_numeric::<i32>()
5362 .unwrap(),
5363 2
5364 );
5365 assert_eq!(
5366 ds.attr("HDF5_nFramesChunks")
5367 .unwrap()
5368 .read_numeric::<i32>()
5369 .unwrap(),
5370 3
5371 );
5372
5373 std::fs::remove_file(&path).ok();
5374 }
5375
5376 #[test]
5377 fn test_extra_dimensions_layout() {
5378 let path = temp_path("hdf5_extradims");
5387 let mut writer = Hdf5Writer::new();
5388 writer.set_n_extra_dims(2);
5389 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");
5393 writer.set_extra_dim_name(1, "scanX");
5394 writer.set_extra_dim_name(2, "scanY");
5395
5396 let mut arr = NDArray::new(
5397 vec![NDDimension::new(4), NDDimension::new(4)],
5398 NDDataType::UInt16,
5399 );
5400 for f in 0..24u16 {
5401 if let NDDataBuffer::U16(ref mut v) = arr.data {
5402 for x in v.iter_mut() {
5403 *x = f;
5404 }
5405 }
5406 if f == 0 {
5407 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5408 }
5409 writer.write_file(&arr).unwrap();
5410 }
5411 writer.close_file().unwrap();
5412
5413 let h5 = H5File::open(&path).unwrap();
5414 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
5415 assert_eq!(ds.shape(), vec![4, 3, 2, 4, 4]);
5417 let data: Vec<u16> = ds.read_raw().unwrap();
5418 assert_eq!(data.len(), 24 * 16);
5419 for f in 0..24usize {
5420 for i in 0..16usize {
5421 assert_eq!(data[f * 16 + i], f as u16, "frame {} elem {}", f, i);
5422 }
5423 }
5424 assert_eq!(
5426 ds.attr("HDF5_nExtraDims")
5427 .unwrap()
5428 .read_numeric::<i32>()
5429 .unwrap(),
5430 2
5431 );
5432 assert_eq!(
5433 ds.attr("HDF5_extraDimSize0")
5434 .unwrap()
5435 .read_numeric::<i32>()
5436 .unwrap(),
5437 2
5438 );
5439 assert_eq!(
5440 ds.attr("HDF5_extraDimName0")
5441 .unwrap()
5442 .read_string()
5443 .unwrap(),
5444 "n"
5445 );
5446
5447 std::fs::remove_file(&path).ok();
5448 }
5449
5450 #[test]
5451 fn test_extra_dimensions_one_virtual() {
5452 let path = temp_path("hdf5_extradims_1");
5456 let mut writer = Hdf5Writer::new();
5457 writer.set_n_extra_dims(1);
5458 writer.set_extra_dim_size(0, 2); writer.set_extra_dim_size(1, 3); let mut arr = NDArray::new(
5462 vec![NDDimension::new(4), NDDimension::new(4)],
5463 NDDataType::UInt16,
5464 );
5465 for f in 0..6u16 {
5466 if let NDDataBuffer::U16(ref mut v) = arr.data {
5467 for x in v.iter_mut() {
5468 *x = f;
5469 }
5470 }
5471 if f == 0 {
5472 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5473 }
5474 writer.write_file(&arr).unwrap();
5475 }
5476 writer.close_file().unwrap();
5477
5478 let h5 = H5File::open(&path).unwrap();
5479 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
5480 assert_eq!(ds.shape(), vec![3, 2, 4, 4]);
5481 let data: Vec<u16> = ds.read_raw().unwrap();
5482 assert_eq!(data.len(), 6 * 16);
5483 for f in 0..6usize {
5484 for i in 0..16usize {
5485 assert_eq!(data[f * 16 + i], f as u16, "frame {} elem {}", f, i);
5486 }
5487 }
5488
5489 std::fs::remove_file(&path).ok();
5490 }
5491
5492 #[test]
5493 fn test_swmr_extra_dimensions_grid_layout() {
5494 let path = temp_path("hdf5_swmr_extradims");
5501 let mut writer = Hdf5Writer::new();
5502 writer.set_swmr_mode(true);
5503 writer.set_n_extra_dims(2);
5504 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(
5509 vec![NDDimension::new(4), NDDimension::new(4)],
5510 NDDataType::UInt16,
5511 );
5512 for f in 0..24u16 {
5513 if let NDDataBuffer::U16(ref mut v) = arr.data {
5514 for x in v.iter_mut() {
5515 *x = f;
5516 }
5517 }
5518 if f == 0 {
5519 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5520 }
5521 writer.write_file(&arr).unwrap();
5522 }
5523 writer.close_file().unwrap();
5524
5525 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
5526 assert_eq!(
5527 reader
5528 .dataset_shape("entry/instrument/detector/data")
5529 .unwrap(),
5530 vec![4, 3, 2, 4, 4]
5531 );
5532 let data: Vec<u16> = reader
5533 .read_dataset("entry/instrument/detector/data")
5534 .unwrap();
5535 assert_eq!(data.len(), 24 * 16);
5536 for f in 0..24usize {
5537 for i in 0..16usize {
5538 assert_eq!(data[f * 16 + i], f as u16, "frame {} elem {}", f, i);
5539 }
5540 }
5541 std::fs::remove_file(&path).ok();
5542 }
5543
5544 #[test]
5545 fn test_swmr_extra_dimensions_grid_subtiled() {
5546 let path = temp_path("hdf5_swmr_extradims_tiled");
5552 let mut writer = Hdf5Writer::new();
5553 writer.set_swmr_mode(true);
5554 writer.set_n_extra_dims(1);
5555 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(
5560 vec![NDDimension::new(4), NDDimension::new(4)],
5561 NDDataType::UInt16,
5562 );
5563 for f in 0..6u16 {
5564 if let NDDataBuffer::U16(ref mut v) = arr.data {
5565 for (i, x) in v.iter_mut().enumerate() {
5568 *x = f * 100 + i as u16;
5569 }
5570 }
5571 if f == 0 {
5572 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5573 }
5574 writer.write_file(&arr).unwrap();
5575 }
5576 writer.close_file().unwrap();
5577
5578 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
5579 assert_eq!(
5580 reader
5581 .dataset_shape("entry/instrument/detector/data")
5582 .unwrap(),
5583 vec![3, 2, 4, 4]
5584 );
5585 let data: Vec<u16> = reader
5586 .read_dataset("entry/instrument/detector/data")
5587 .unwrap();
5588 assert_eq!(data.len(), 6 * 16);
5589 for f in 0..6usize {
5590 for i in 0..16usize {
5591 assert_eq!(
5592 data[f * 16 + i],
5593 f as u16 * 100 + i as u16,
5594 "frame {} elem {}",
5595 f,
5596 i
5597 );
5598 }
5599 }
5600 std::fs::remove_file(&path).ok();
5601 }
5602
5603 #[test]
5604 fn test_swmr_extra_dimensions_grid_partial_fill() {
5605 let path = temp_path("hdf5_swmr_extradims_partial");
5609 let mut writer = Hdf5Writer::new();
5610 writer.set_swmr_mode(true);
5611 writer.set_n_extra_dims(1);
5612 writer.set_extra_dim_size(0, 2); writer.set_extra_dim_size(1, 3); let mut arr = NDArray::new(
5616 vec![NDDimension::new(4), NDDimension::new(4)],
5617 NDDataType::UInt16,
5618 );
5619 for f in 0..4u16 {
5620 if let NDDataBuffer::U16(ref mut v) = arr.data {
5621 for x in v.iter_mut() {
5622 *x = f + 1; }
5624 }
5625 if f == 0 {
5626 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5627 }
5628 writer.write_file(&arr).unwrap();
5629 }
5630 writer.close_file().unwrap();
5631
5632 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
5633 assert_eq!(
5634 reader
5635 .dataset_shape("entry/instrument/detector/data")
5636 .unwrap(),
5637 vec![3, 2, 4, 4]
5638 );
5639 let data: Vec<u16> = reader
5640 .read_dataset("entry/instrument/detector/data")
5641 .unwrap();
5642 assert_eq!(data.len(), 6 * 16);
5643 for f in 0..6usize {
5644 let expected = if f < 4 { f as u16 + 1 } else { 0 };
5645 for i in 0..16usize {
5646 assert_eq!(data[f * 16 + i], expected, "frame {} elem {}", f, i);
5647 }
5648 }
5649 std::fs::remove_file(&path).ok();
5650 }
5651
5652 #[test]
5653 fn test_swmr_streaming() {
5654 let path = temp_path("hdf5_swmr");
5655 let mut writer = Hdf5Writer::new();
5656 writer.set_swmr_mode(true);
5657 writer.set_flush_nth_frame(2);
5658
5659 let arr = NDArray::new(
5660 vec![NDDimension::new(8), NDDimension::new(8)],
5661 NDDataType::Float32,
5662 );
5663
5664 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5665 writer.write_file(&arr).unwrap();
5666 writer.write_file(&arr).unwrap(); writer.write_file(&arr).unwrap();
5668 writer.close_file().unwrap();
5669
5670 assert_eq!(writer.frame_count(), 3);
5671
5672 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
5674 let shape = reader
5675 .dataset_shape("entry/instrument/detector/data")
5676 .unwrap();
5677 assert_eq!(shape[0], 3); assert_eq!(shape[1], 8);
5679 assert_eq!(shape[2], 8);
5680
5681 let data: Vec<f32> = reader
5682 .read_dataset("entry/instrument/detector/data")
5683 .unwrap();
5684 assert_eq!(data.len(), 3 * 8 * 8);
5685
5686 std::fs::remove_file(&path).ok();
5687 }
5688
5689 #[test]
5690 fn test_swmr_compression_is_applied() {
5691 let path = temp_path("hdf5_swmr_comp");
5695 let mut writer = Hdf5Writer::new();
5696 writer.set_swmr_mode(true);
5697 writer.set_compression_type(COMPRESS_ZLIB);
5698
5699 let arr = NDArray::new(
5700 vec![NDDimension::new(8), NDDimension::new(8)],
5701 NDDataType::UInt16,
5702 );
5703 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5704 assert!(
5705 !writer.swmr_compression_dropped(),
5706 "SWMR+ZLIB must apply compression, not drop it"
5707 );
5708 writer.write_file(&arr).unwrap();
5709 writer.write_file(&arr).unwrap();
5710 writer.close_file().unwrap();
5711
5712 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
5714 let shape = reader
5715 .dataset_shape("entry/instrument/detector/data")
5716 .unwrap();
5717 assert_eq!(shape, vec![2, 8, 8]);
5718 let data: Vec<u16> = reader
5719 .read_dataset("entry/instrument/detector/data")
5720 .unwrap();
5721 assert_eq!(data.len(), 2 * 8 * 8);
5722
5723 std::fs::remove_file(&path).ok();
5724 }
5725
5726 #[test]
5727 fn test_layout_xml_param() {
5728 let mut writer = Hdf5Writer::new();
5730 let dir = std::env::temp_dir();
5731 let good = dir.join(format!("adcore_layout_good_{}.xml", std::process::id()));
5732 std::fs::write(
5733 &good,
5734 r#"<hdf5_layout><group name="entry"><dataset name="data" source="detector" det_default="true"/></group></hdf5_layout>"#,
5735 )
5736 .unwrap();
5737 assert!(writer.set_layout_filename(good.to_str().unwrap()));
5738 assert!(writer.layout_valid);
5739 assert!(writer.layout_error.is_empty());
5740
5741 let bad = dir.join(format!("adcore_layout_bad_{}.xml", std::process::id()));
5742 std::fs::write(&bad, r#"<not_a_layout/>"#).unwrap();
5743 assert!(!writer.set_layout_filename(bad.to_str().unwrap()));
5744 assert!(!writer.layout_valid);
5745 assert!(!writer.layout_error.is_empty());
5746
5747 std::fs::remove_file(&good).ok();
5748 std::fs::remove_file(&bad).ok();
5749 }
5750
5751 #[test]
5752 fn test_layout_xml_places_dataset_in_nested_tree() {
5753 let dir = std::env::temp_dir();
5759 let layout = dir.join(format!("adcore_layout_nested_{}.xml", std::process::id()));
5760 std::fs::write(
5761 &layout,
5762 r#"<hdf5_layout>
5763 <group name="entry">
5764 <group name="instrument">
5765 <group name="detector">
5766 <dataset name="data" source="detector" det_default="true">
5767 <attribute name="signal" source="constant" value="1" type="int"/>
5768 </dataset>
5769 </group>
5770 <group name="NDAttributes" ndattr_default="true"/>
5771 <group name="performance">
5772 <dataset name="timestamp"/>
5773 </group>
5774 </group>
5775 </group>
5776 </hdf5_layout>"#,
5777 )
5778 .unwrap();
5779
5780 let path = temp_path("hdf5_layout_nested");
5781 let mut writer = Hdf5Writer::new();
5782 writer.set_store_performance(true);
5783 assert!(
5784 writer.set_layout_filename(layout.to_str().unwrap()),
5785 "layout XML must parse: {}",
5786 writer.layout_error
5787 );
5788
5789 let mk = |fill: f64| {
5790 let mut arr = NDArray::new(
5791 vec![NDDimension::new(4), NDDimension::new(4)],
5792 NDDataType::UInt16,
5793 );
5794 arr.attributes.add(NDAttribute::new_static(
5795 "exposure",
5796 "",
5797 NDAttrSource::Driver,
5798 NDAttrValue::Float64(fill),
5799 ));
5800 arr
5801 };
5802
5803 let a0 = mk(0.5);
5804 writer.open_file(&path, NDFileMode::Stream, &a0).unwrap();
5805 writer.write_file(&a0).unwrap();
5806 writer.write_file(&mk(0.75)).unwrap();
5807 writer.close_file().unwrap();
5808
5809 let h5 = H5File::open(&path).unwrap();
5810 let names = h5.dataset_names();
5811 assert!(
5813 names.contains(&"entry/instrument/detector/data".to_string()),
5814 "image dataset must be at the nested layout path; got {:?}",
5815 names
5816 );
5817 assert!(
5818 !names.contains(&"data".to_string()),
5819 "must not also write a flat-root `data` dataset"
5820 );
5821 let img = h5.dataset("entry/instrument/detector/data").unwrap();
5822 assert_eq!(img.shape(), vec![2, 4, 4]);
5823 assert_eq!(
5825 img.attr("signal").unwrap().read_numeric::<i64>().unwrap(),
5826 1
5827 );
5828 assert!(
5830 names.contains(&"entry/instrument/NDAttributes/exposure".to_string()),
5831 "NDAttribute dataset must be under the layout ndattr group; got {:?}",
5832 names
5833 );
5834 assert!(
5836 names.contains(&"entry/instrument/performance/timestamp".to_string()),
5837 "performance dataset must be under the layout group; got {:?}",
5838 names
5839 );
5840
5841 drop(h5);
5843 let mut reader = Hdf5Writer::new();
5844 assert!(reader.set_layout_filename(layout.to_str().unwrap()));
5845 reader.current_path = Some(path.clone());
5846 let read_arr = reader.read_file().unwrap();
5847 assert_eq!(read_arr.dims.len(), 3);
5848
5849 std::fs::remove_file(&path).ok();
5850 std::fs::remove_file(&layout).ok();
5851 }
5852
5853 #[test]
5861 fn test_detector_data_destination_routes_by_attribute() {
5862 let dir = std::env::temp_dir();
5863 let layout = dir.join(format!("adcore_layout_multidet_{}.xml", std::process::id()));
5864 std::fs::write(
5865 &layout,
5866 r#"<hdf5_layout>
5867 <global name="detector_data_destination" ndattribute="dest"/>
5868 <group name="entry">
5869 <dataset name="data1" source="detector" det_default="true"/>
5870 <dataset name="data2" source="detector"/>
5871 </group>
5872 </hdf5_layout>"#,
5873 )
5874 .unwrap();
5875
5876 let path = temp_path("hdf5_multidet_route");
5877 let mut writer = Hdf5Writer::new();
5878 writer.store_attributes = false;
5880 assert!(
5881 writer.set_layout_filename(layout.to_str().unwrap()),
5882 "layout XML must parse: {}",
5883 writer.layout_error
5884 );
5885
5886 let mk = |val: u16, dest: Option<&str>| {
5889 let mut arr = NDArray::new(
5890 vec![NDDimension::new(2), NDDimension::new(2)],
5891 NDDataType::UInt16,
5892 );
5893 if let NDDataBuffer::U16(ref mut v) = arr.data {
5894 for p in v.iter_mut() {
5895 *p = val;
5896 }
5897 }
5898 if let Some(d) = dest {
5899 arr.attributes.add(NDAttribute::new_static(
5900 "dest",
5901 "",
5902 NDAttrSource::Driver,
5903 NDAttrValue::String(d.to_string()),
5904 ));
5905 }
5906 arr
5907 };
5908
5909 let f0 = mk(10, None);
5915 writer.open_file(&path, NDFileMode::Stream, &f0).unwrap();
5916 writer.write_file(&f0).unwrap();
5917 writer.write_file(&mk(11, Some("/entry/data2"))).unwrap();
5918 writer.write_file(&mk(12, Some("/entry/data2"))).unwrap();
5919 writer.write_file(&mk(13, Some("/nonexistent"))).unwrap();
5920 writer.write_file(&mk(14, Some("/entry/data1"))).unwrap();
5921 writer.close_file().unwrap();
5922
5923 let h5 = H5File::open(&path).unwrap();
5924 let names = h5.dataset_names();
5925 assert!(
5926 names.contains(&"entry/data1".to_string())
5927 && names.contains(&"entry/data2".to_string()),
5928 "both detector datasets must exist; got {:?}",
5929 names
5930 );
5931
5932 let d1 = h5.dataset("entry/data1").unwrap();
5934 assert_eq!(d1.shape(), vec![3, 2, 2], "default dataset extent");
5935 let v1: Vec<u16> = d1.read_raw().unwrap();
5936 assert_eq!(
5937 v1,
5938 vec![10, 10, 10, 10, 13, 13, 13, 13, 14, 14, 14, 14],
5939 "default dataset must hold the default-routed frames in order"
5940 );
5941
5942 let d2 = h5.dataset("entry/data2").unwrap();
5943 assert_eq!(d2.shape(), vec![2, 2, 2], "routed dataset extent");
5944 let v2: Vec<u16> = d2.read_raw().unwrap();
5945 assert_eq!(
5946 v2,
5947 vec![11, 11, 11, 11, 12, 12, 12, 12],
5948 "routed dataset must hold only the frames addressed to it"
5949 );
5950
5951 drop(h5);
5952 std::fs::remove_file(&path).ok();
5953 std::fs::remove_file(&layout).ok();
5954 }
5955
5956 #[test]
5960 fn test_detector_data_destination_non_string_attribute_errors() {
5961 let dir = std::env::temp_dir();
5962 let layout = dir.join(format!(
5963 "adcore_layout_multidet_err_{}.xml",
5964 std::process::id()
5965 ));
5966 std::fs::write(
5967 &layout,
5968 r#"<hdf5_layout>
5969 <global name="detector_data_destination" ndattribute="dest"/>
5970 <group name="entry">
5971 <dataset name="data1" source="detector" det_default="true"/>
5972 <dataset name="data2" source="detector"/>
5973 </group>
5974 </hdf5_layout>"#,
5975 )
5976 .unwrap();
5977
5978 let path = temp_path("hdf5_multidet_err");
5979 let mut writer = Hdf5Writer::new();
5980 writer.store_attributes = false;
5981 assert!(writer.set_layout_filename(layout.to_str().unwrap()));
5982
5983 let mut arr = NDArray::new(
5984 vec![NDDimension::new(2), NDDimension::new(2)],
5985 NDDataType::UInt16,
5986 );
5987 arr.attributes.add(NDAttribute::new_static(
5988 "dest",
5989 "",
5990 NDAttrSource::Driver,
5991 NDAttrValue::Float64(2.0),
5992 ));
5993
5994 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
5995 assert!(
5996 writer.write_file(&arr).is_err(),
5997 "a non-string destination attribute must abort the write"
5998 );
5999 writer.close_file().ok();
6000
6001 std::fs::remove_file(&path).ok();
6002 std::fs::remove_file(&layout).ok();
6003 }
6004
6005 #[test]
6006 fn test_layout_hardlink_is_materialised() {
6007 let dir = std::env::temp_dir();
6013 let layout = dir.join(format!("adcore_layout_hardlink_{}.xml", std::process::id()));
6014 std::fs::write(
6015 &layout,
6016 r#"<hdf5_layout>
6017 <group name="entry">
6018 <group name="data">
6019 <dataset name="data" source="detector" det_default="true"/>
6020 <hardlink name="data_alias" target="/entry/data/data"/>
6021 </group>
6022 </group>
6023 </hdf5_layout>"#,
6024 )
6025 .unwrap();
6026
6027 let path = temp_path("hdf5_layout_hardlink");
6028 let mut writer = Hdf5Writer::new();
6029 assert!(
6030 writer.set_layout_filename(layout.to_str().unwrap()),
6031 "layout XML must parse: {}",
6032 writer.layout_error
6033 );
6034
6035 let arr = NDArray::new(
6036 vec![NDDimension::new(4), NDDimension::new(4)],
6037 NDDataType::UInt16,
6038 );
6039 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
6040 writer.write_file(&arr).unwrap();
6041 writer.close_file().unwrap();
6042
6043 let h5 = H5File::open(&path).unwrap();
6044 let names = h5.dataset_names();
6045 assert!(
6047 names.contains(&"entry/data/data".to_string()),
6048 "image dataset must exist at the layout path; got {:?}",
6049 names
6050 );
6051 assert!(
6053 names.contains(&"entry/data/data_alias".to_string()),
6054 "layout <hardlink> must be materialised as a hard link; got {:?}",
6055 names
6056 );
6057 let alias = h5.dataset("entry/data/data_alias").unwrap();
6059 let orig = h5.dataset("entry/data/data").unwrap();
6060 assert_eq!(alias.shape(), orig.shape());
6061
6062 drop(h5);
6063 std::fs::remove_file(&path).ok();
6064 std::fs::remove_file(&layout).ok();
6065 }
6066
6067 #[test]
6072 fn close_file_releases_the_handle_when_finalisation_fails() {
6073 let dir = std::env::temp_dir();
6074 let layout = dir.join(format!(
6075 "adcore_layout_broken_hardlink_{}.xml",
6076 std::process::id()
6077 ));
6078 std::fs::write(
6079 &layout,
6080 r#"<hdf5_layout>
6081 <group name="entry">
6082 <group name="data">
6083 <dataset name="data" source="detector" det_default="true"/>
6084 <hardlink name="broken" target="/no_such_object"/>
6085 </group>
6086 </group>
6087 </hdf5_layout>"#,
6088 )
6089 .unwrap();
6090
6091 let path = temp_path("hdf5_close_fail_releases_handle");
6092 let mut writer = Hdf5Writer::new();
6093 assert!(
6094 writer.set_layout_filename(layout.to_str().unwrap()),
6095 "layout XML must parse: {}",
6096 writer.layout_error
6097 );
6098
6099 let arr = NDArray::new(
6100 vec![NDDimension::new(4), NDDimension::new(4)],
6101 NDDataType::UInt16,
6102 );
6103 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
6104 writer.write_file(&arr).unwrap();
6105
6106 assert!(
6107 writer.close_file().is_err(),
6108 "a hardlink to a missing object must fail the close"
6109 );
6110 assert!(
6111 writer.handle.is_none(),
6112 "a failed close must still release the H5 file id and its dataset ids"
6113 );
6114 assert!(writer.current_path.is_none());
6115 writer.close_file().unwrap();
6118
6119 std::fs::remove_file(&path).ok();
6120 std::fs::remove_file(&layout).ok();
6121 }
6122
6123 #[test]
6124 fn test_swmr_layout_hardlink_is_materialised() {
6125 let dir = std::env::temp_dir();
6137 let layout = dir.join(format!(
6138 "adcore_swmr_layout_hardlink_{}.xml",
6139 std::process::id()
6140 ));
6141 std::fs::write(
6142 &layout,
6143 r#"<hdf5_layout>
6144 <group name="entry">
6145 <group name="data">
6146 <dataset name="data" source="detector" det_default="true"/>
6147 <hardlink name="data_alias" target="/entry/data/data"/>
6148 </group>
6149 </group>
6150 </hdf5_layout>"#,
6151 )
6152 .unwrap();
6153
6154 let path = temp_path("hdf5_swmr_layout_hardlink");
6155 let mut writer = Hdf5Writer::new();
6156 writer.set_swmr_mode(true);
6157 assert!(
6158 writer.set_layout_filename(layout.to_str().unwrap()),
6159 "layout XML must parse: {}",
6160 writer.layout_error
6161 );
6162
6163 let mut arr = NDArray::new(
6164 vec![NDDimension::new(4), NDDimension::new(4)],
6165 NDDataType::UInt16,
6166 );
6167 if let NDDataBuffer::U16(ref mut v) = arr.data {
6168 for (i, x) in v.iter_mut().enumerate() {
6169 *x = i as u16;
6170 }
6171 }
6172 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
6173 assert!(
6174 writer.is_swmr_active(),
6175 "writer must be in SWMR mode for this test"
6176 );
6177 writer.write_file(&arr).unwrap();
6178 writer.write_file(&arr).unwrap();
6179 writer.close_file().unwrap();
6180
6181 let h5 = H5File::open(&path).unwrap();
6182 let names = h5.dataset_names();
6183 assert!(
6185 names.contains(&"entry/data/data".to_string()),
6186 "SWMR image dataset must exist at the nested layout path; got {:?}",
6187 names
6188 );
6189 assert!(
6191 names.contains(&"entry/data/data_alias".to_string()),
6192 "SWMR layout <hardlink> must be materialised as a hard link; got {:?}",
6193 names
6194 );
6195 let alias = h5.dataset("entry/data/data_alias").unwrap();
6197 let orig = h5.dataset("entry/data/data").unwrap();
6198 assert_eq!(alias.shape(), orig.shape());
6199 assert_eq!(orig.shape(), vec![2, 4, 4]);
6200
6201 drop(h5);
6202 std::fs::remove_file(&path).ok();
6203 std::fs::remove_file(&layout).ok();
6204 }
6205
6206 #[test]
6207 fn test_swmr_layout_nested_dataset_placement() {
6208 let dir = std::env::temp_dir();
6215 let layout = dir.join(format!(
6216 "adcore_swmr_layout_nested_{}.xml",
6217 std::process::id()
6218 ));
6219 std::fs::write(
6220 &layout,
6221 r#"<hdf5_layout>
6222 <group name="entry">
6223 <group name="instrument">
6224 <group name="detector">
6225 <dataset name="data" source="detector" det_default="true">
6226 <attribute name="signal" source="constant" value="1" type="int"/>
6227 </dataset>
6228 <hardlink name="data_alias" target="/entry/instrument/detector/data"/>
6229 </group>
6230 </group>
6231 <group name="empty_placeholder"/>
6232 </group>
6233 </hdf5_layout>"#,
6234 )
6235 .unwrap();
6236
6237 let path = temp_path("hdf5_swmr_layout_nested");
6238 let mut writer = Hdf5Writer::new();
6239 writer.set_swmr_mode(true);
6240 assert!(
6241 writer.set_layout_filename(layout.to_str().unwrap()),
6242 "layout XML must parse: {}",
6243 writer.layout_error
6244 );
6245
6246 let mut arr = NDArray::new(
6247 vec![NDDimension::new(4), NDDimension::new(4)],
6248 NDDataType::UInt16,
6249 );
6250 if let NDDataBuffer::U16(ref mut v) = arr.data {
6251 for (i, x) in v.iter_mut().enumerate() {
6252 *x = (i * 3) as u16;
6253 }
6254 }
6255 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
6256 assert!(
6257 writer.is_swmr_active(),
6258 "writer must be in SWMR mode for this test"
6259 );
6260 writer.write_file(&arr).unwrap();
6261 writer.write_file(&arr).unwrap();
6262 writer.close_file().unwrap();
6263
6264 let mut reader = rust_hdf5::swmr::SwmrFileReader::open(&path).unwrap();
6267 let names = reader.dataset_names();
6268 assert!(
6270 names.contains(&"entry/instrument/detector/data".to_string()),
6271 "SWMR image dataset must live at the nested layout path; got {:?}",
6272 names
6273 );
6274 assert!(
6275 !names.contains(&"data".to_string()),
6276 "SWMR image dataset must NOT remain at the flat root; got {:?}",
6277 names
6278 );
6279 assert!(
6281 reader.has_group("entry/empty_placeholder"),
6282 "empty layout group must be materialised; groups {:?}",
6283 reader.group_paths()
6284 );
6285 assert!(
6287 names.contains(&"entry/instrument/detector/data_alias".to_string()),
6288 "SWMR layout <hardlink> must resolve to the nested dataset; got {:?}",
6289 names
6290 );
6291 let nested = reader
6292 .dataset_shape("entry/instrument/detector/data")
6293 .unwrap();
6294 let alias = reader
6295 .dataset_shape("entry/instrument/detector/data_alias")
6296 .unwrap();
6297 assert_eq!(nested, vec![2, 4, 4]);
6298 assert_eq!(alias, nested, "hardlink alias must share the target shape");
6299 let via_nested: Vec<u16> = reader
6301 .read_dataset("entry/instrument/detector/data")
6302 .unwrap();
6303 let via_alias: Vec<u16> = reader
6304 .read_dataset("entry/instrument/detector/data_alias")
6305 .unwrap();
6306 assert_eq!(via_nested, via_alias);
6307 assert_eq!(via_nested.len(), 2 * 4 * 4);
6308 let attr_names = reader
6312 .dataset_attr_names("entry/instrument/detector/data")
6313 .unwrap();
6314 for expected in [
6315 "signal",
6316 "NDArrayNumDims",
6317 "NDArrayDimOffset",
6318 "NDArrayDimBinning",
6319 "NDArrayDimReverse",
6320 ] {
6321 assert!(
6322 attr_names.iter().any(|n| n == expected),
6323 "streaming dataset must carry the {expected} attribute; got {attr_names:?}",
6324 );
6325 }
6326
6327 drop(reader);
6328 std::fs::remove_file(&path).ok();
6329 std::fs::remove_file(&layout).ok();
6330 }
6331
6332 #[test]
6333 fn test_default_layout_parses_and_resolves_nexus_paths() {
6334 let layout = Hdf5Writer::default_layout();
6338 assert_eq!(
6339 layout.detector_dataset_path().as_deref(),
6340 Some("/entry/instrument/detector/data")
6341 );
6342 assert_eq!(
6343 layout.ndattr_default_group().as_deref(),
6344 Some("/entry/instrument/NDAttributes")
6345 );
6346 assert_eq!(
6347 layout.dataset_group_path("timestamp").as_deref(),
6348 Some("/entry/instrument/performance")
6349 );
6350 }
6351
6352 #[test]
6353 fn test_no_layout_uses_default_nexus_layout() {
6354 let path = temp_path("hdf5_default_layout");
6359 let mut writer = Hdf5Writer::new();
6360 let arr = NDArray::new(
6361 vec![NDDimension::new(4), NDDimension::new(4)],
6362 NDDataType::UInt8,
6363 );
6364 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
6365 writer.write_file(&arr).unwrap();
6366 writer.close_file().unwrap();
6367
6368 let h5 = H5File::open(&path).unwrap();
6369 let det = h5.dataset("entry/instrument/detector/data").unwrap();
6371 assert_eq!(det.shape(), vec![1, 4, 4]);
6372 assert!(
6373 !h5.dataset_names().contains(&"data".to_string()),
6374 "must not write a flat-root `data`; got {:?}",
6375 h5.dataset_names()
6376 );
6377 let linked = h5.dataset("entry/data/data").unwrap();
6379 assert_eq!(linked.shape(), vec![1, 4, 4]);
6380 std::fs::remove_file(&path).ok();
6381 }
6382
6383 #[test]
6384 fn test_default_layout_group_nx_class_attributes() {
6385 let path = temp_path("hdf5_nxclass");
6389 let mut writer = Hdf5Writer::new();
6390 let arr = NDArray::new(
6391 vec![NDDimension::new(4), NDDimension::new(4)],
6392 NDDataType::UInt8,
6393 );
6394 writer.open_file(&path, NDFileMode::Single, &arr).unwrap();
6395 writer.write_file(&arr).unwrap();
6396 writer.close_file().unwrap();
6397
6398 let h5 = H5File::open(&path).unwrap();
6399 let nx = |g: &str| {
6400 let mut grp = h5.root_group();
6401 for seg in g.split('/') {
6402 grp = grp.group(seg).unwrap();
6403 }
6404 grp.attr_string("NX_class").unwrap()
6405 };
6406 assert_eq!(nx("entry"), "NXentry");
6407 assert_eq!(nx("entry/instrument"), "NXinstrument");
6408 assert_eq!(nx("entry/instrument/detector"), "NXdetector");
6409 assert_eq!(nx("entry/instrument/NDAttributes"), "NXcollection");
6410 assert_eq!(nx("entry/instrument/detector/NDAttributes"), "NXcollection");
6411 assert_eq!(nx("entry/data"), "NXdata");
6412 std::fs::remove_file(&path).ok();
6413 }
6414
6415 #[test]
6416 fn test_swmr_default_layout_group_nx_class_attributes() {
6417 let path = temp_path("hdf5_swmr_nxclass");
6420 let mut writer = Hdf5Writer::new();
6421 writer.set_swmr_mode(true);
6422 let arr = NDArray::new(
6423 vec![NDDimension::new(4), NDDimension::new(4)],
6424 NDDataType::Float32,
6425 );
6426 writer.open_file(&path, NDFileMode::Stream, &arr).unwrap();
6427 writer.write_file(&arr).unwrap();
6428 writer.close_file().unwrap();
6429
6430 let h5 = H5File::open(&path).unwrap();
6431 let nx = |g: &str| {
6432 let mut grp = h5.root_group();
6433 for seg in g.split('/') {
6434 grp = grp.group(seg).unwrap();
6435 }
6436 grp.attr_string("NX_class").unwrap()
6437 };
6438 assert_eq!(nx("entry"), "NXentry");
6439 assert_eq!(nx("entry/instrument/detector"), "NXdetector");
6440 assert_eq!(nx("entry/data"), "NXdata");
6441 std::fs::remove_file(&path).ok();
6442 }
6443
6444 fn ramp_u16(y: usize, x: usize) -> NDArray {
6449 let data: Vec<u16> = (0..(y * x) as u16).collect();
6450 NDArray::with_data(
6451 vec![NDDimension::new(y), NDDimension::new(x)],
6452 NDDataBuffer::U16(data),
6453 )
6454 }
6455
6456 fn u16_pixels(arr: &NDArray) -> Vec<u16> {
6457 match &arr.data {
6458 NDDataBuffer::U16(v) => v.clone(),
6459 _ => unreachable!("expected U16 buffer"),
6460 }
6461 }
6462
6463 #[test]
6464 fn test_codec_chunk_bytes_lz4_prepends_hdf5_header() {
6465 let codec = Codec {
6470 name: CodecName::LZ4,
6471 compressed_size: 4,
6472 level: 0,
6473 shuffle: 0,
6474 compressor: 0,
6475 original_data_type: NDDataType::UInt16,
6476 };
6477 let payload = [0xAAu8, 0xBB, 0xCC, 0xDD];
6478 let out = codec_chunk_bytes(&codec, 32, &payload);
6479 let mut expect = Vec::new();
6480 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);
6484 assert_eq!(out, expect);
6485 }
6486
6487 #[test]
6488 fn test_codec_chunk_bytes_verbatim_for_self_describing() {
6489 for name in [CodecName::Blosc, CodecName::JPEG] {
6493 let codec = Codec {
6494 name,
6495 compressed_size: 3,
6496 level: 0,
6497 shuffle: 0,
6498 compressor: 0,
6499 original_data_type: NDDataType::UInt8,
6500 };
6501 let payload = [1u8, 2, 3];
6502 assert_eq!(codec_chunk_bytes(&codec, 99, &payload), payload.to_vec());
6503 }
6504 }
6505
6506 #[test]
6507 fn test_codecs_match() {
6508 let a = Codec {
6509 name: CodecName::LZ4,
6510 compressed_size: 1,
6511 level: 0,
6512 shuffle: 0,
6513 compressor: 0,
6514 original_data_type: NDDataType::UInt8,
6515 };
6516 assert!(codecs_match(None, None));
6517 assert!(!codecs_match(Some(&a), None));
6518 assert!(!codecs_match(None, Some(&a)));
6519 assert!(codecs_match(Some(&a), Some(&a.clone())));
6520 let mut diff = a.clone();
6521 diff.compressor = 1;
6522 assert!(!codecs_match(Some(&a), Some(&diff)));
6523 let mut other_type = a.clone();
6525 other_type.original_data_type = NDDataType::Float64;
6526 assert!(codecs_match(Some(&a), Some(&other_type)));
6527 }
6528
6529 #[test]
6530 fn test_codec_filter_pipeline_ids() {
6531 let w = Hdf5Writer::new();
6532 let mk = |name| Codec {
6533 name,
6534 compressed_size: 0,
6535 level: 5,
6536 shuffle: 1,
6537 compressor: 2,
6538 original_data_type: NDDataType::UInt16,
6539 };
6540 assert_eq!(
6541 w.codec_filter_pipeline(&mk(CodecName::LZ4))
6542 .unwrap()
6543 .filters[0]
6544 .id,
6545 FILTER_LZ4
6546 );
6547 assert_eq!(
6548 w.codec_filter_pipeline(&mk(CodecName::BSLZ4))
6549 .unwrap()
6550 .filters[0]
6551 .id,
6552 FILTER_BSHUF
6553 );
6554 let blosc = w.codec_filter_pipeline(&mk(CodecName::Blosc)).unwrap();
6555 assert_eq!(blosc.filters[0].id, FILTER_BLOSC);
6556 assert_eq!(blosc.filters[0].cd_values[4], 5, "level");
6559 assert_eq!(blosc.filters[0].cd_values[5], 1, "shuffle");
6560 assert_eq!(blosc.filters[0].cd_values[6], 2, "compressor");
6561 assert_eq!(
6562 w.codec_filter_pipeline(&mk(CodecName::JPEG))
6563 .unwrap()
6564 .filters[0]
6565 .id,
6566 FILTER_JPEG
6567 );
6568 assert!(w.codec_filter_pipeline(&mk(CodecName::None)).is_none());
6570 assert!(w.codec_filter_pipeline(&mk(CodecName::Zlib)).is_none());
6571 assert!(w.codec_filter_pipeline(&mk(CodecName::LZ4HDF5)).is_none());
6572 }
6573
6574 #[test]
6575 fn test_compression_aware_true() {
6576 assert!(Hdf5FileProcessor::new().compression_aware());
6578 }
6579
6580 #[test]
6581 fn test_direct_chunk_write_lz4_roundtrips() {
6582 let path = temp_path("hdf5_dcw_lz4");
6583 let mut writer = Hdf5Writer::new();
6584 let orig = ramp_u16(4, 5);
6585 let expect = u16_pixels(&orig);
6586 let comp = crate::codec::compress_lz4(&orig);
6587 assert!(comp.codec.is_some());
6588 writer.open_file(&path, NDFileMode::Single, &comp).unwrap();
6589 writer.write_file(&comp).unwrap();
6590 writer.close_file().unwrap();
6591
6592 let h5 = H5File::open(&path).unwrap();
6593 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
6594 assert_eq!(ds.shape(), vec![1, 5, 4]);
6598 assert!(ds.is_chunked());
6599 let read = ds.read_raw::<u16>().unwrap();
6600 assert_eq!(
6601 read, expect,
6602 "LZ4 direct-chunk-write must reverse to the original pixels"
6603 );
6604 std::fs::remove_file(&path).ok();
6605 }
6606
6607 #[test]
6608 fn test_direct_chunk_write_blosc_roundtrips() {
6609 let path = temp_path("hdf5_dcw_blosc");
6610 let mut writer = Hdf5Writer::new();
6611 let orig = ramp_u16(4, 5);
6612 let expect = u16_pixels(&orig);
6613 let comp = crate::codec::compress_blosc(&orig, &crate::codec::BloscConfig::default());
6614 assert_eq!(comp.codec.as_ref().unwrap().name, CodecName::Blosc);
6615 writer.open_file(&path, NDFileMode::Single, &comp).unwrap();
6616 writer.write_file(&comp).unwrap();
6617 writer.close_file().unwrap();
6618
6619 let h5 = H5File::open(&path).unwrap();
6620 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
6621 assert_eq!(ds.shape(), vec![1, 5, 4]);
6622 let read = ds.read_raw::<u16>().unwrap();
6623 assert_eq!(read, expect, "BLOSC direct-chunk-write must round-trip");
6624 std::fs::remove_file(&path).ok();
6625 }
6626
6627 #[test]
6628 fn test_direct_chunk_write_bslz4_roundtrips() {
6629 let path = temp_path("hdf5_dcw_bslz4");
6638 let mut writer = Hdf5Writer::new();
6639 let orig = ramp_u16(128, 128);
6640 let expect = u16_pixels(&orig);
6641 let comp = crate::codec::compress_bslz4(&orig);
6642 assert_eq!(comp.codec.as_ref().unwrap().name, CodecName::BSLZ4);
6643 writer.open_file(&path, NDFileMode::Single, &comp).unwrap();
6644 writer.write_file(&comp).unwrap();
6645 writer.close_file().unwrap();
6646
6647 let h5 = H5File::open(&path).unwrap();
6648 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
6649 assert_eq!(ds.shape(), vec![1, 128, 128]);
6650 assert!(ds.is_chunked());
6651 assert_eq!(ds.element_size(), 2, "dataset keeps the original u16 type");
6652 let read = ds.read_raw::<u16>().unwrap();
6653 assert_eq!(read, expect, "BSLZ4 direct-chunk-write must round-trip");
6654 std::fs::remove_file(&path).ok();
6655 }
6656
6657 #[test]
6658 fn test_codec_chunk_bytes_bslz4_header() {
6659 let codec = Codec {
6664 name: CodecName::BSLZ4,
6665 compressed_size: 0,
6666 level: 0,
6667 shuffle: 0,
6668 compressor: 0,
6669 original_data_type: NDDataType::UInt16,
6670 };
6671 let payload = [0xDEu8, 0xAD, 0xBE, 0xEF];
6672 let total = 128 * 128 * 2; let out = codec_chunk_bytes(&codec, total, &payload);
6674 assert_eq!(&out[0..8], &(total as u64).to_be_bytes());
6675 assert_eq!(&out[8..12], &8192u32.to_be_bytes());
6677 assert_eq!(&out[12..], &payload, "canonical stream follows verbatim");
6678 }
6679
6680 #[test]
6681 fn test_direct_chunk_write_lz4_multiframe_extends_leading_axis() {
6682 let path = temp_path("hdf5_dcw_lz4_multi");
6683 let mut writer = Hdf5Writer::new();
6684 let frames: Vec<NDArray> = (0..3u16)
6685 .map(|f| {
6686 let data: Vec<u16> = (0..20u16).map(|i| i + f * 100).collect();
6687 NDArray::with_data(
6688 vec![NDDimension::new(4), NDDimension::new(5)],
6689 NDDataBuffer::U16(data),
6690 )
6691 })
6692 .collect();
6693 let comp: Vec<NDArray> = frames.iter().map(crate::codec::compress_lz4).collect();
6694 writer
6695 .open_file(&path, NDFileMode::Stream, &comp[0])
6696 .unwrap();
6697 for c in &comp {
6698 writer.write_file(c).unwrap();
6699 }
6700 writer.close_file().unwrap();
6701
6702 let h5 = H5File::open(&path).unwrap();
6703 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
6704 assert_eq!(ds.shape(), vec![3, 5, 4], "three compressed frames stacked");
6705 let read = ds.read_raw::<u16>().unwrap();
6706 let mut expect = Vec::new();
6707 for f in 0..3u16 {
6708 for i in 0..20u16 {
6709 expect.push(i + f * 100);
6710 }
6711 }
6712 assert_eq!(read, expect, "every frame's pixels recovered in order");
6713 std::fs::remove_file(&path).ok();
6714 }
6715
6716 #[test]
6717 fn test_direct_chunk_write_jpeg_records_chunked_dataset() {
6718 let path = temp_path("hdf5_dcw_jpeg");
6723 let mut writer = Hdf5Writer::new();
6724 let mono: Vec<u8> = (0..64u16).map(|i| (i * 3) as u8).collect();
6725 let src = NDArray::with_data(
6726 vec![NDDimension::new(8), NDDimension::new(8)],
6727 NDDataBuffer::U8(mono),
6728 );
6729 let comp = crate::codec::compress_jpeg(&src, 90).expect("jpeg encode");
6730 assert_eq!(comp.codec.as_ref().unwrap().name, CodecName::JPEG);
6731 writer.open_file(&path, NDFileMode::Single, &comp).unwrap();
6732 writer.write_file(&comp).unwrap();
6733 writer.close_file().unwrap();
6734
6735 let h5 = H5File::open(&path).unwrap();
6736 let ds = h5.dataset("entry/instrument/detector/data").unwrap();
6737 assert_eq!(ds.shape(), vec![1, 8, 8]);
6738 assert!(ds.is_chunked());
6739 std::fs::remove_file(&path).ok();
6740 }
6741
6742 #[test]
6743 fn test_codec_change_mid_stream_errors() {
6744 let path = temp_path("hdf5_codec_change");
6748 let mut writer = Hdf5Writer::new();
6749 let f0 = crate::codec::compress_lz4(&ramp_u16(4, 5));
6750 let f1_plain = ramp_u16(4, 5); writer.open_file(&path, NDFileMode::Stream, &f0).unwrap();
6752 writer.write_file(&f0).unwrap();
6753 assert!(
6754 writer.write_file(&f1_plain).is_err(),
6755 "an uncompressed frame must be rejected for a compressed dataset"
6756 );
6757 writer.close_file().ok();
6758 std::fs::remove_file(&path).ok();
6759 }
6760
6761 #[test]
6762 fn test_swmr_rejects_compressed_array() {
6763 let path = temp_path("hdf5_swmr_compressed");
6766 let mut writer = Hdf5Writer::new();
6767 writer.set_swmr_mode(true);
6768 let comp = crate::codec::compress_lz4(&ramp_u16(4, 5));
6769 writer.open_file(&path, NDFileMode::Stream, &comp).unwrap();
6770 assert!(
6771 writer.write_file(&comp).is_err(),
6772 "SWMR mode cannot direct-chunk-write a pre-compressed array"
6773 );
6774 writer.close_file().ok();
6775 std::fs::remove_file(&path).ok();
6776 }
6777
6778 #[test]
6779 fn test_unsupported_codec_rejected_on_open() {
6780 let path = temp_path("hdf5_dcw_zlib");
6783 let mut writer = Hdf5Writer::new();
6784 let comp = crate::codec::compress_zlib(&ramp_u16(4, 5));
6785 assert_eq!(comp.codec.as_ref().unwrap().name, CodecName::Zlib);
6786 writer.open_file(&path, NDFileMode::Single, &comp).unwrap();
6787 assert!(
6788 writer.write_file(&comp).is_err(),
6789 "an unsupported (zlib) codec must be rejected, not silently mis-written"
6790 );
6791 writer.close_file().ok();
6792 std::fs::remove_file(&path).ok();
6793 }
6794}